From 819663d1343a5025e2f677803b0faa91acc83318 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 22 May 2018 15:36:19 +0800 Subject: [PATCH 0001/1120] Update the content about Import Format Following by https://git.openstack.org/cgit/openstack-dev/hacking/tree/HACKING.rst#n71 Change-Id: I2f32d773c12d484e8c0e435a78a3fe16d0eeae03 --- doc/source/contributor/developing.rst | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index 721a016a03..1488816846 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -177,18 +177,21 @@ or Standardize Import Format ========================= -.. _`Import Order Guide`: https://docs.openstack.org/hacking/latest/user/hacking.html#imports +More information about Import Format, see `Import Order Guide +`__. The import order shows below: -* {{stdlib imports in human alphabetical order}} -* \n -* {{third-party lib imports in human alphabetical order}} -* \n -* {{project imports in human alphabetical order}} -* \n -* \n -* {{begin your code}} +.. code-block:: none + + {{stdlib imports in human alphabetical order}} + \n + {{third-party lib imports in human alphabetical order}} + \n + {{project imports in human alphabetical order}} + \n + \n + {{begin your code}} Example ~~~~~~~ From 8db3933feb35f91f3ff3d121c155286973c66122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Thu, 10 May 2018 17:16:48 +0200 Subject: [PATCH 0002/1120] Don't display router's is_ha and is_distributed attributes always In case when is_ha or is_distributed attribute of Neutron's router is set to None, it means that it wasn't returned from server and should not be displayed. Otherwise it might be confusing for user is making openstack router show call as an admin will return e.g. is_ha=True but same call done as regular user will return False or None. It might happen like that because returning of those attributes is forbidden for regular users in Neutron's policy.json Depends-On: https://review.openstack.org/567606/ Change-Id: I626b5193d9ecb308baad7b27939f9673c32b4182 Closes-Bug: #1689510 Task: 19789 Story: 2002110 --- openstackclient/network/v2/router.py | 24 ++++++++--- .../tests/unit/network/v2/test_router.py | 43 ++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index f0a5196744..4819f279f2 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -71,7 +71,15 @@ def _get_columns(item): } if hasattr(item, 'interfaces_info'): column_map['interfaces_info'] = 'interfaces_info' - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + invisible_columns = [] + if item.is_ha is None: + invisible_columns.append('is_ha') + column_map.pop('is_ha') + if item.is_distributed is None: + invisible_columns.append('is_distributed') + column_map.pop('is_distributed') + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, invisible_columns) def _get_attrs(client_manager, parsed_args): @@ -330,8 +338,6 @@ def take_action(self, parsed_args): 'name', 'status', 'is_admin_state_up', - 'is_distributed', - 'is_ha', 'project_id', ) column_headers = ( @@ -339,8 +345,6 @@ def take_action(self, parsed_args): 'Name', 'Status', 'State', - 'Distributed', - 'HA', 'Project', ) @@ -376,6 +380,16 @@ def take_action(self, parsed_args): else: data = client.routers(**args) + # check if "HA" and "Distributed" columns should be displayed also + data = list(data) + for d in data: + if (d.is_distributed is not None and + 'is_distributed' not in columns): + columns = columns + ('is_distributed',) + column_headers = column_headers + ('Distributed',) + if d.is_ha is not None and 'is_ha' not in columns: + columns = columns + ('is_ha',) + column_headers = column_headers + ('HA',) if parsed_args.long: columns = columns + ( 'routes', diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index f383c1ddb7..7a7bcf90a4 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -400,9 +400,9 @@ class TestListRouter(TestRouter): 'Name', 'Status', 'State', + 'Project', 'Distributed', 'HA', - 'Project', ) columns_long = columns + ( 'Routes', @@ -423,9 +423,9 @@ class TestListRouter(TestRouter): r.name, r.status, router._format_admin_state(r.admin_state_up), + r.tenant_id, r.distributed, r.ha, - r.tenant_id, )) router_agent_data = [] @@ -496,6 +496,25 @@ def test_router_list_no_options(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_router_list_no_ha_no_distributed(self): + _routers = network_fakes.FakeRouter.create_routers({ + 'ha': None, + 'distributed': None}, + count=3) + + arglist = [] + verifylist = [ + ('long', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object( + self.network, "routers", return_value=_routers): + columns, data = self.cmd.take_action(parsed_args) + + self.assertNotIn("is_distributed", columns) + self.assertNotIn("is_ha", columns) + def test_router_list_long(self): arglist = [ '--long', @@ -1196,6 +1215,26 @@ def test_show_all_options(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_show_no_ha_no_distributed(self): + _router = network_fakes.FakeRouter.create_one_router({ + 'ha': None, + 'distributed': None}) + + arglist = [ + _router.name, + ] + verifylist = [ + ('router', _router.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object( + self.network, "find_router", return_value=_router): + columns, data = self.cmd.take_action(parsed_args) + + self.assertNotIn("is_distributed", columns) + self.assertNotIn("is_ha", columns) + class TestUnsetRouter(TestRouter): From 181f14319bac0917fcbe5f232cae0603c6939d10 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 8 Jun 2018 10:13:10 -0400 Subject: [PATCH 0003/1120] Add note about version 2.5 when listing servers using --ip6 The --ip6 filter when listing servers as a non-admin user only applies when also using --os-compute-api-microversion 2.5 or greater. This change simply adds a note about that in the --ip6 option help text. We could probably get more sophisticated by trying to determine if the user has the admin role or not and if not, and using --ip6 without microversion >= 2.5, we could error out, but that seems excessive at this point. Change-Id: I665c64e0bdac04c695fa119a479df43f70b0fa62 Story: #2002184 Task: #20057 --- openstackclient/compute/v2/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c80b5a3c25..c414f7e18e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -977,7 +977,9 @@ def get_parser(self, prog_name): parser.add_argument( '--ip6', metavar='', - help=_('Regular expression to match IPv6 addresses'), + help=_('Regular expression to match IPv6 addresses. Note ' + 'that this option only applies for non-admin users ' + 'when using ``--os-compute-api-version`` 2.5 or greater.'), ) parser.add_argument( '--name', From 577e2e850c58cfd9e8d0731508db5be9fec407f1 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 8 Jun 2018 16:37:26 -0400 Subject: [PATCH 0004/1120] Mention 2.51 in help for openstack server event show With the 2.51 compute API microversion, non-admin users can also see event details for a given request. This change mentions that in the help text for "openstack server event show". While in here, change the _info private attribute access to the to_dict() usage. Change-Id: I5fd487b17c4b85bd7e619112ad262ffdd3a940c8 Task: 21199 Story: 2002193 --- openstackclient/compute/v2/server_event.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py index d8fbda0f2d..c7d2e2e385 100644 --- a/openstackclient/compute/v2/server_event.py +++ b/openstackclient/compute/v2/server_event.py @@ -91,7 +91,9 @@ def take_action(self, parsed_args): class ShowServerEvent(command.ShowOne): - _description = _("Show server event details") + _description = _( + "Show server event details. Specify ``--os-compute-api-version 2.51`` " + "or higher to show events for non-admin users.") def get_parser(self, prog_name): parser = super(ShowServerEvent, self).get_parser(prog_name) @@ -114,4 +116,4 @@ def take_action(self, parsed_args): action_detail = compute_client.instance_action.get( server_id, parsed_args.request_id) - return zip(*sorted(six.iteritems(action_detail._info))) + return zip(*sorted(six.iteritems(action_detail.to_dict()))) From 71f138b172e95515b9d54eee9b3e318df3381791 Mon Sep 17 00:00:00 2001 From: Yang Youseok Date: Wed, 27 Dec 2017 18:34:11 +0900 Subject: [PATCH 0005/1120] Fix RuntimeError when showing project which has extra properties If you use python3, items() returns iterator which is not allowed to remove item during iteration. Fix to iterate by copied list. Change-Id: I64c037d04e2b127d8f19f56cab65122af89a7200 Closes-Bug: 1740232 --- openstackclient/identity/v2_0/project.py | 2 +- releasenotes/notes/bug-1740232-91ad72c2ac165f35.yaml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/bug-1740232-91ad72c2ac165f35.yaml diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 04d422ecdb..9bb5fc4d08 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -289,7 +289,7 @@ def take_action(self, parsed_args): # the API has and handle the extra top level properties. reserved = ('name', 'id', 'enabled', 'description') properties = {} - for k, v in info.items(): + for k, v in list(info.items()): if k not in reserved: # If a key is not in `reserved` it's a property, pop it info.pop(k) diff --git a/releasenotes/notes/bug-1740232-91ad72c2ac165f35.yaml b/releasenotes/notes/bug-1740232-91ad72c2ac165f35.yaml new file mode 100644 index 0000000000..fb9ad7be04 --- /dev/null +++ b/releasenotes/notes/bug-1740232-91ad72c2ac165f35.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix RuntimeError in ``project show`` command running under Python 3. + [Bug `1740232 `_] From b18e79c09bcaea86b3f8470c909664e5fe940682 Mon Sep 17 00:00:00 2001 From: yanpuqing Date: Tue, 19 Jun 2018 05:13:48 -0400 Subject: [PATCH 0006/1120] Delete the LB object quotas set command in openstackclient Setting octavia quotas should use "openstack loadbalancer quota set", not "openstack quota set". The vip parameter had be removed from octavia. The patch removes '--vips', '--health-monitors', '--l7policies' parameter in "openstack quota set" command. Change-Id: Id0046195aa93bae62264d9de7d123cf63bd0fb7e Task: 19657 Story: 2002016 --- doc/source/cli/command-objects/quota.rst | 3 --- openstackclient/common/quota.py | 3 --- openstackclient/tests/unit/common/test_quota.py | 9 --------- 3 files changed, 15 deletions(-) diff --git a/doc/source/cli/command-objects/quota.rst b/doc/source/cli/command-objects/quota.rst index f39536af8e..126b210c4c 100644 --- a/doc/source/cli/command-objects/quota.rst +++ b/doc/source/cli/command-objects/quota.rst @@ -70,10 +70,7 @@ Set quotas for project [--ports ] [--routers ] [--rbac-policies ] - [--vips ] [--subnetpools ] - [--members ] - [--health-monitors ] diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 282ea4284d..1027859acd 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -75,10 +75,7 @@ 'port': 'ports', 'router': 'routers', 'rbac_policy': 'rbac-policies', - 'vip': 'vips', 'subnetpool': 'subnetpools', - 'healthmonitor': 'health-monitors', - 'l7policy': 'l7policies', } NETWORK_KEYS = ['floating_ips', 'networks', 'rbac_policies', 'routers', diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 1a3da31d78..fc975b980d 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -644,9 +644,6 @@ def test_quota_set_network(self): '--routers', str(network_fakes.QUOTA['router']), '--rbac-policies', str(network_fakes.QUOTA['rbac_policy']), '--ports', str(network_fakes.QUOTA['port']), - '--vips', str(network_fakes.QUOTA['vip']), - '--health-monitors', str(network_fakes.QUOTA['healthmonitor']), - '--l7policies', str(network_fakes.QUOTA['l7policy']), self.projects[0].name, ] verifylist = [ @@ -660,9 +657,6 @@ def test_quota_set_network(self): ('router', network_fakes.QUOTA['router']), ('rbac_policy', network_fakes.QUOTA['rbac_policy']), ('port', network_fakes.QUOTA['port']), - ('vip', network_fakes.QUOTA['vip']), - ('healthmonitor', network_fakes.QUOTA['healthmonitor']), - ('l7policy', network_fakes.QUOTA['l7policy']), ('project', self.projects[0].name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -679,9 +673,6 @@ def test_quota_set_network(self): 'router': network_fakes.QUOTA['router'], 'rbac_policy': network_fakes.QUOTA['rbac_policy'], 'port': network_fakes.QUOTA['port'], - 'vip': network_fakes.QUOTA['vip'], - 'healthmonitor': network_fakes.QUOTA['healthmonitor'], - 'l7policy': network_fakes.QUOTA['l7policy'], } self.network_mock.update_quota.assert_called_once_with( self.projects[0].id, From a5865b176304b92736a71cd6ad2dd1199c588ba5 Mon Sep 17 00:00:00 2001 From: Bernard Cafarelli Date: Tue, 4 Sep 2018 17:52:46 +0200 Subject: [PATCH 0007/1120] Partially Revert "Add command to unset information from Subnet-pools" We do not support removing a prefix from a subnet pool, only updating with a larger prefix (which is handled by the set command) This reverts commit 063c722a110031883e9615064092644de6df8da2. Change-Id: I11224fbdb94dc1caef42a8a64cbcebaf1dc542fe Story: #1670230 Task: #13697 --- .../cli/command-objects/subnet-pool.rst | 6 ---- openstackclient/network/v2/subnet_pool.py | 23 ------------ .../tests/unit/network/v2/test_subnet_pool.py | 35 +------------------ 3 files changed, 1 insertion(+), 63 deletions(-) diff --git a/doc/source/cli/command-objects/subnet-pool.rst b/doc/source/cli/command-objects/subnet-pool.rst index 0cff4d7f56..4606881b54 100644 --- a/doc/source/cli/command-objects/subnet-pool.rst +++ b/doc/source/cli/command-objects/subnet-pool.rst @@ -292,15 +292,9 @@ Unset subnet pool properties .. code:: bash openstack subnet pool unset - [--pool-prefix [...]] [--tag | --all-tag] -.. option:: --pool-prefix - - Remove subnet pool prefixes (in CIDR notation). - (repeat option to unset multiple prefixes). - .. option:: --tag Tag to be removed from the subnet pool diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index a583986856..d85410691a 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -13,7 +13,6 @@ """Subnet pool action implementations""" -import copy import logging from osc_lib.cli import parseractions @@ -440,14 +439,6 @@ class UnsetSubnetPool(command.Command): def get_parser(self, prog_name): parser = super(UnsetSubnetPool, self).get_parser(prog_name) - parser.add_argument( - '--pool-prefix', - metavar='', - action='append', - dest='prefixes', - help=_('Remove subnet pool prefixes (in CIDR notation). ' - '(repeat option to unset multiple prefixes).'), - ) parser.add_argument( 'subnet_pool', metavar="", @@ -460,19 +451,5 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_subnet_pool( parsed_args.subnet_pool, ignore_missing=False) - tmp_prefixes = copy.deepcopy(obj.prefixes) - attrs = {} - if parsed_args.prefixes: - for prefix in parsed_args.prefixes: - try: - tmp_prefixes.remove(prefix) - except ValueError: - msg = _( - "Subnet pool does not " - "contain prefix %s") % prefix - raise exceptions.CommandError(msg) - attrs['prefixes'] = tmp_prefixes - if attrs: - client.update_subnet_pool(obj, **attrs) # tags is a subresource and it needs to be updated separately. _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 81f0278f26..3d8f1028e6 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -1016,9 +1016,7 @@ class TestUnsetSubnetPool(TestSubnetPool): def setUp(self): super(TestUnsetSubnetPool, self).setUp() self._subnetpool = network_fakes.FakeSubnetPool.create_one_subnet_pool( - {'prefixes': ['10.0.10.0/24', '10.1.10.0/24', - '10.2.10.0/24'], - 'tags': ['green', 'red']}) + {'tags': ['green', 'red']}) self.network.find_subnet_pool = mock.Mock( return_value=self._subnetpool) self.network.update_subnet_pool = mock.Mock(return_value=None) @@ -1026,37 +1024,6 @@ def setUp(self): # Get the command object to test self.cmd = subnet_pool.UnsetSubnetPool(self.app, self.namespace) - def test_unset_subnet_pool(self): - arglist = [ - '--pool-prefix', '10.0.10.0/24', - '--pool-prefix', '10.1.10.0/24', - self._subnetpool.name, - ] - verifylist = [ - ('prefixes', ['10.0.10.0/24', '10.1.10.0/24']), - ('subnet_pool', self._subnetpool.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - attrs = {'prefixes': ['10.2.10.0/24']} - self.network.update_subnet_pool.assert_called_once_with( - self._subnetpool, **attrs) - self.assertIsNone(result) - - def test_unset_subnet_pool_prefix_not_existent(self): - arglist = [ - '--pool-prefix', '10.100.1.1/25', - self._subnetpool.name, - ] - verifylist = [ - ('prefixes', ['10.100.1.1/25']), - ('subnet_pool', self._subnetpool.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) - def _test_unset_tags(self, with_tags=True): if with_tags: arglist = ['--tag', 'red', '--tag', 'blue'] From eb4f839ec7c1a5e791d6f798c5efb705d55b1e01 Mon Sep 17 00:00:00 2001 From: Nguyen Van Duc Date: Wed, 18 Jul 2018 15:50:18 +0700 Subject: [PATCH 0008/1120] Replace port 35357 with 5000 for "auth_url" Based on the change in Keystone Install Guide [1], this patch replace port 35357 with 5000 for "auth_url". For more details, please check similar changes which have been done on other projects: Nova [2], Neutron [3], Cinder [4], Glance [5]. [1] https://review.openstack.org/#/c/541857 [2] https://review.openstack.org/#/c/562812 [3] https://review.openstack.org/#/c/566491 [4] https://review.openstack.org/#/c/565464 [5] https://review.openstack.org/#/c/558932 Change-Id: I4faabbb107f912c7ed1cc5d3467ea5a94197d4a0 --- doc/source/cli/man/openstack.rst | 4 ++-- doc/source/configuration/index.rst | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/cli/man/openstack.rst b/doc/source/cli/man/openstack.rst index 0745fb7a06..649657523b 100644 --- a/doc/source/cli/man/openstack.rst +++ b/doc/source/cli/man/openstack.rst @@ -278,14 +278,14 @@ The keys match the :program:`openstack` global options but without the clouds: devstack: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: demo username: demo password: 0penstack region_name: RegionOne ds-admin: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: admin username: admin password: 0penstack diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst index d2b273d7ee..b74bb3659b 100644 --- a/doc/source/configuration/index.rst +++ b/doc/source/configuration/index.rst @@ -61,14 +61,14 @@ The keys match the :program:`openstack` global options but without the clouds: devstack: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: demo username: demo password: 0penstack region_name: RegionOne ds-admin: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: admin username: admin password: 0penstack @@ -121,7 +121,7 @@ domain name as shown in the example below: clouds: devstack: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: demo username: demo password: 0penstack @@ -171,7 +171,7 @@ By setting `log_level` or `log_file` in the configuration clouds: devstack: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: demo username: demo password: 0penstack @@ -182,7 +182,7 @@ By setting `log_level` or `log_file` in the configuration level: info ds-admin: auth: - auth_url: http://192.168.122.10:35357/ + auth_url: http://192.168.122.10:5000/ project_name: admin username: admin password: 0penstack From eb001733fd3c1a98027f7439b84e952f1eb2a406 Mon Sep 17 00:00:00 2001 From: M V P Nitesh Date: Mon, 17 Jul 2017 18:08:58 +0530 Subject: [PATCH 0009/1120] Now we can add description for role creation in OSC Now user can add the description when user create's the role using OSC ``openstack role create`` command. User can add the description by adding `--description ` to OSC ``openstack role create`` command. Co-Authored-By: Deepak Mourya Change-Id: I858e004c3b29c687b6a39c8a1ed5fb029eb19c67 Depends-on: I230af9cc833af13064636b5d9a7ce6334c3f6e9a Closes-Bug: #1669080 --- doc/source/cli/command-objects/role.rst | 4 + openstackclient/identity/v3/role.py | 16 +++- .../tests/functional/identity/v3/common.py | 2 +- .../tests/functional/identity/v3/test_role.py | 25 ++++++ .../tests/unit/identity/v3/fakes.py | 1 + .../tests/unit/identity/v3/test_role.py | 79 +++++++++++++++++++ ...-description-to-role-afe7b6ff668df261.yaml | 8 ++ 7 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-description-to-role-afe7b6ff668df261.yaml diff --git a/doc/source/cli/command-objects/role.rst b/doc/source/cli/command-objects/role.rst index 9819fd1231..d84eec9d4b 100644 --- a/doc/source/cli/command-objects/role.rst +++ b/doc/source/cli/command-objects/role.rst @@ -115,6 +115,10 @@ Create new role New role name +.. option:: --description + + Add description about the role + role delete ----------- diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 58a76f8a65..09e914b5c8 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -177,6 +177,11 @@ def get_parser(self, prog_name): metavar='', help=_('New role name'), ) + parser.add_argument( + '--description', + metavar='', + help=_('Add description about the role'), + ) parser.add_argument( '--domain', metavar='', @@ -199,7 +204,8 @@ def take_action(self, parsed_args): try: role = identity_client.roles.create( - name=parsed_args.name, domain=domain_id) + name=parsed_args.name, domain=domain_id, + description=parsed_args.description) except ks_exc.Conflict: if parsed_args.or_show: @@ -449,6 +455,11 @@ def get_parser(self, prog_name): metavar='', help=_('Role to modify (name or ID)'), ) + parser.add_argument( + '--description', + metavar='', + help=_('Add description about the role'), + ) parser.add_argument( '--domain', metavar='', @@ -473,7 +484,8 @@ def take_action(self, parsed_args): parsed_args.role, domain_id=domain_id) - identity_client.roles.update(role.id, name=parsed_args.name) + identity_client.roles.update(role.id, name=parsed_args.name, + description=parsed_args.description) class ShowRole(command.ShowOne): diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 58468bc7e7..434bd949d7 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -32,7 +32,7 @@ class IdentityTests(base.TestCase): 'password_expires_at'] PROJECT_FIELDS = ['description', 'id', 'domain_id', 'is_domain', 'enabled', 'name', 'parent_id'] - ROLE_FIELDS = ['id', 'name', 'domain_id'] + ROLE_FIELDS = ['id', 'name', 'domain_id', 'description'] SERVICE_FIELDS = ['id', 'enabled', 'name', 'type', 'description'] REGION_FIELDS = ['description', 'enabled', 'parent_region', 'region'] ENDPOINT_FIELDS = ['id', 'region', 'region_id', 'service_id', diff --git a/openstackclient/tests/functional/identity/v3/test_role.py b/openstackclient/tests/functional/identity/v3/test_role.py index fb9e061424..8e9f0f90b8 100644 --- a/openstackclient/tests/functional/identity/v3/test_role.py +++ b/openstackclient/tests/functional/identity/v3/test_role.py @@ -20,6 +20,21 @@ class RoleTests(common.IdentityTests): def test_role_create(self): self._create_dummy_role() + def test_role_create_with_description(self): + role_name = data_utils.rand_name('TestRole') + description = data_utils.rand_name('description') + raw_output = self.openstack( + 'role create ' + '--description %(description)s ' + '%(name)s' % {'description': description, + 'name': role_name}) + role = self.parse_show_as_object(raw_output) + self.addCleanup(self.openstack, 'role delete %s' % role['id']) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.ROLE_FIELDS) + self.assertEqual(description, role['description']) + return role_name + def test_role_delete(self): role_name = self._create_dummy_role(add_clean_up=False) raw_output = self.openstack('role delete %s' % role_name) @@ -88,6 +103,16 @@ def test_role_set(self): role = self.parse_show_as_object(raw_output) self.assertEqual(new_role_name, role['name']) + def test_role_set_description(self): + role_name = self._create_dummy_role() + description = data_utils.rand_name("NewDescription") + raw_output = self.openstack('role set --description %s %s' + % (description, role_name)) + self.assertEqual(0, len(raw_output)) + raw_output = self.openstack('role show %s' % role_name) + role = self.parse_show_as_object(raw_output) + self.assertEqual(description, role['description']) + def test_role_add(self): role_name = self._create_dummy_role() username = self._create_dummy_user() diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 27ee9fd026..2bca48a7c8 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -175,6 +175,7 @@ role_id = 'r1' role_name = 'roller' +role_description = 'role description' ROLE = { 'id': role_id, diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 281d530c7e..14569ffac4 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -332,6 +332,7 @@ def test_role_create_no_options(self): kwargs = { 'domain': None, 'name': identity_fakes.role_name, + 'description': None, } # RoleManager.create(name=, domain=) @@ -375,6 +376,7 @@ def test_role_create_with_domain(self): kwargs = { 'domain': identity_fakes.domain_id, 'name': identity_fakes.ROLE_2['name'], + 'description': None, } # RoleManager.create(name=, domain=) @@ -391,6 +393,49 @@ def test_role_create_with_domain(self): ) self.assertEqual(datalist, data) + def test_role_create_with_description(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--description', identity_fakes.role_description, + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('description', identity_fakes.role_description), + ('name', identity_fakes.ROLE_2['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) + + # Set expected values + kwargs = { + 'description': identity_fakes.role_description, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + class TestRoleDelete(TestRole): @@ -1057,6 +1102,7 @@ def test_role_set_no_options(self): # Set expected values kwargs = { 'name': 'over', + 'description': None, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( @@ -1088,6 +1134,39 @@ def test_role_set_domain_role(self): # Set expected values kwargs = { 'name': 'over', + 'description': None, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_description(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--description', identity_fakes.role_description, + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('description', identity_fakes.role_description), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': identity_fakes.role_description, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( diff --git a/releasenotes/notes/add-description-to-role-afe7b6ff668df261.yaml b/releasenotes/notes/add-description-to-role-afe7b6ff668df261.yaml new file mode 100644 index 0000000000..31efb3778f --- /dev/null +++ b/releasenotes/notes/add-description-to-role-afe7b6ff668df261.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Now user can add the description when user create's + the role using OSC ``openstack role create`` command. + User can add the description by adding + `--description ` to OSC + ``openstack role create`` command. From 341333b7c3e65e3aaf9d88a9d4bd5fddbe5263a4 Mon Sep 17 00:00:00 2001 From: melissaml Date: Sun, 23 Sep 2018 23:06:16 +0800 Subject: [PATCH 0010/1120] Update the URL in doc Change-Id: I1c0c58dada83a930fafc7141d5446c754ca5c80c --- releasenotes/notes/bug-1519502-f72236598d14d350.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/bug-1519502-f72236598d14d350.yaml b/releasenotes/notes/bug-1519502-f72236598d14d350.yaml index 1ace59bd66..b793b99b0d 100644 --- a/releasenotes/notes/bug-1519502-f72236598d14d350.yaml +++ b/releasenotes/notes/bug-1519502-f72236598d14d350.yaml @@ -9,4 +9,4 @@ features: upgrade: - Output of command ``ip floating list`` for nova network has been changed. And it is different from the output of neutron network. - [Ref ``_] + [Ref ``_] From a4fcae2ac2375191daea14fe524f0175d4c3300d Mon Sep 17 00:00:00 2001 From: Chen Date: Fri, 28 Sep 2018 19:20:07 +0800 Subject: [PATCH 0011/1120] trivial: remove commented-out code These comments have existed from the beginning. But they seem to be meaningless. Change-Id: Ic38272ecfb321d77219d477634e9e29b968e7f00 --- openstackclient/api/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 04d88f31e9..7e2fe38f0e 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -186,7 +186,6 @@ def list( ret = self._request( 'POST', path, - # service=self.service_type, json=body, params=params, ) @@ -194,7 +193,6 @@ def list( ret = self._request( 'GET', path, - # service=self.service_type, params=params, ) try: From f82c5b85ce9d6fee62550044fbabe54155e4b367 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Tue, 19 Jun 2018 16:25:40 +0800 Subject: [PATCH 0012/1120] Add --key-name and --key-unset option for server rebuild API. Change-Id: I6d4793a8e961080ea1d6d414cef8d6bbed0c53e7 Story: 2002609 Task: 22228 Signed-off-by: Fan Zhang --- lower-constraints.txt | 2 +- openstackclient/compute/v2/server.py | 26 ++++++ .../tests/unit/compute/v2/test_server.py | 90 +++++++++++++++++++ ...ld-with-keypair-name-83c1aa20db136d91.yaml | 6 ++ requirements.txt | 2 +- 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/server-rebuild-with-keypair-name-83c1aa20db136d91.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index db92fef36b..64911cf947 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -100,7 +100,7 @@ python-mimeparse==1.6.0 python-mistralclient==3.1.0 python-muranoclient==0.8.2 python-neutronclient==6.7.0 -python-novaclient==9.1.0 +python-novaclient==10.0.0 python-octaviaclient==1.3.0 python-rsdclient==0.1.0 python-saharaclient==1.4.0 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 67b4140a4e..5723dae306 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1529,6 +1529,22 @@ def get_parser(self, prog_name): action='store_true', help=_('Wait for rebuild to complete'), ) + key_group = parser.add_mutually_exclusive_group() + key_group.add_argument( + '--key-name', + metavar='', + help=_("Set the key name of key pair on the rebuilt instance." + " Cannot be specified with the '--key-unset' option." + " (Supported by API versions '2.54' - '2.latest')"), + ) + key_group.add_argument( + '--key-unset', + action='store_true', + default=False, + help=_("Unset the key name of key pair on the rebuilt instance." + " Cannot be specified with the '--key-name' option." + " (Supported by API versions '2.54' - '2.latest')"), + ) return parser def take_action(self, parsed_args): @@ -1553,6 +1569,16 @@ def _show_progress(progress): if parsed_args.property: kwargs['meta'] = parsed_args.property + if parsed_args.key_name or parsed_args.key_unset: + if compute_client.api_version < api_versions.APIVersion('2.54'): + msg = _('--os-compute-api-version 2.54 or later is required') + raise exceptions.CommandError(msg) + + if parsed_args.key_unset: + kwargs['key_name'] = None + if parsed_args.key_name: + kwargs['key_name'] = parsed_args.key_name + server = server.rebuild(image, parsed_args.password, **kwargs) if parsed_args.wait: if utils.wait_for_status( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index e938564b50..9adc3cc6df 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2517,6 +2517,96 @@ def test_rebuild_with_property(self): self.server.rebuild.assert_called_with( self.image, None, meta=expected_property) + def test_rebuild_with_keypair_name(self): + self.server.key_name = 'mykey' + arglist = [ + self.server.id, + '--key-name', self.server.key_name, + ] + verifylist = [ + ('server', self.server.id), + ('key_name', self.server.key_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = 2.54 + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.54): + self.cmd.take_action(parsed_args) + args = ( + self.image, + None, + ) + kwargs = dict( + key_name=self.server.key_name, + ) + self.servers_mock.get.assert_called_with(self.server.id) + self.images_mock.get.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(*args, **kwargs) + + def test_rebuild_with_keypair_name_older_version(self): + self.server.key_name = 'mykey' + arglist = [ + self.server.id, + '--key-name', self.server.key_name, + ] + verifylist = [ + ('server', self.server.id), + ('key_name', self.server.key_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = 2.53 + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.54): + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_rebuild_with_keypair_unset(self): + self.server.key_name = 'mykey' + arglist = [ + self.server.id, + '--key-unset', + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = 2.54 + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.54): + self.cmd.take_action(parsed_args) + args = ( + self.image, + None, + ) + kwargs = dict( + key_name=None, + ) + self.servers_mock.get.assert_called_with(self.server.id) + self.images_mock.get.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(*args, **kwargs) + + def test_rebuild_with_key_name_and_unset(self): + self.server.key_name = 'mykey' + arglist = [ + self.server.id, + '--key-name', self.server.key_name, + '--key-unset', + ] + verifylist = [ + ('server', self.server.id), + ('key_name', self.server.key_name) + ] + self.assertRaises(utils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + class TestServerRemoveFixedIP(TestServer): diff --git a/releasenotes/notes/server-rebuild-with-keypair-name-83c1aa20db136d91.yaml b/releasenotes/notes/server-rebuild-with-keypair-name-83c1aa20db136d91.yaml new file mode 100644 index 0000000000..c670740828 --- /dev/null +++ b/releasenotes/notes/server-rebuild-with-keypair-name-83c1aa20db136d91.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add ``--key-name`` option to ``server rebuild`` command to set keypair of + the server. Note that it requires --os-compute-api-version 2.54 or later. + - Add ``--key-unset`` option to ``server rebuild`` command to unset + keypair. Note that it requires --os-compute-api-version 2.54 or later. diff --git a/requirements.txt b/requirements.txt index 205d4e64ec..c986667206 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.17.0 # Apache-2.0 -python-novaclient>=9.1.0 # Apache-2.0 +python-novaclient>=10.0.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From e0615e8d69152ce0417f873b30050802a2da0da8 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 12 Oct 2018 15:54:02 -0500 Subject: [PATCH 0013/1120] Address issues from volume backend commands This fixes some minor issues in release notes and the command list for the new volume backend commands. Also sorts the fakes used for volume unit tests to allow for multiple command update patches to hopefully reduce the odds of merge conflicts. Change-Id: Ic6e40f4c639368338cf085c68c17038f81da5361 Signed-off-by: Sean McGinnis --- doc/source/cli/commands.rst | 3 +- openstackclient/tests/unit/volume/v2/fakes.py | 66 +++++++++---------- .../volume-backend-c5faae0b31556a24.yaml | 8 +-- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 1ca674d543..d7c9124042 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -156,7 +156,8 @@ referring to both Compute and Volume quotas. * ``user role``: (**Identity**) roles assigned to a user * ``volume``: (**Volume**) block volumes * ``volume backup``: (**Volume**) backup for volumes -* ``volume backend``: (**volume**) volume backend storage +* ``volume backend capability``: (**volume**) volume backend storage capabilities +* ``volume backend pool``: (**volume**) volume backend storage pools * ``volume host``: (**Volume**) the physical computer for volumes * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes * ``volume snapshot``: (**Volume**) a point-in-time copy of a volume diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 8d1db63e9b..59b08d0b90 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -290,47 +290,47 @@ def create_one_pool(attrs=None): class FakeVolumeClient(object): def __init__(self, **kwargs): - self.volumes = mock.Mock() - self.volumes.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + 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.volume_snapshots = mock.Mock() - self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) - self.backups = mock.Mock() - self.backups.resource_class = fakes.FakeResource(None, {}) - self.volume_types = mock.Mock() - self.volume_types.resource_class = fakes.FakeResource(None, {}) - self.volume_type_access = mock.Mock() - self.volume_type_access.resource_class = fakes.FakeResource(None, {}) - self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = ( - fakes.FakeResource(None, {})) - self.restores = mock.Mock() - self.restores.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.availability_zones = mock.Mock() - self.availability_zones.resource_class = fakes.FakeResource(None, {}) - self.transfers = mock.Mock() - self.transfers.resource_class = fakes.FakeResource(None, {}) - self.services = mock.Mock() - self.services.resource_class = fakes.FakeResource(None, {}) - self.quotas = mock.Mock() - self.quotas.resource_class = fakes.FakeResource(None, {}) self.quota_classes = mock.Mock() self.quota_classes.resource_class = fakes.FakeResource(None, {}) - self.consistencygroups = mock.Mock() - self.consistencygroups.resource_class = fakes.FakeResource(None, {}) - self.cgsnapshots = mock.Mock() - self.cgsnapshots.resource_class = fakes.FakeResource(None, {}) - self.auth_token = kwargs['token'] - self.management_url = kwargs['endpoint'] - self.capabilities = mock.Mock() - self.capabilities.resource_class = fakes.FakeResource(None, {}) - self.pools = mock.Mock() - self.pools.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): diff --git a/releasenotes/notes/volume-backend-c5faae0b31556a24.yaml b/releasenotes/notes/volume-backend-c5faae0b31556a24.yaml index 6698309860..f4f131e648 100644 --- a/releasenotes/notes/volume-backend-c5faae0b31556a24.yaml +++ b/releasenotes/notes/volume-backend-c5faae0b31556a24.yaml @@ -1,11 +1,11 @@ --- features: - | - A new command, ``openstack volume backend capability show `` was - added which will provide a list of all capabilities that can be configured + Add ``openstack volume backend capability show `` command that + provides a list of all capabilities that can be configured for the requested backend. The required `` parameter takes the form `host@backend-name`. - | - A new command, ``openstack volume backend pool list`` was added which will - provide a list of all backend storage pools. The optional ``-long`` + Add ``openstack volume backend pool list`` command that provides + a list of all backend storage pools. The optional ``--long`` parameter includes some basic configuration and stats for each pool. From 4173690b2492680ab504ef0d60498eeb5a3d8ae2 Mon Sep 17 00:00:00 2001 From: Robin Cernin Date: Thu, 18 Oct 2018 10:39:49 +1000 Subject: [PATCH 0014/1120] Improve document 'openstack complete' The openstack complete command requires bash-completion package Without the relevant package the bash completion doesn't work. Change-Id: I47c77f3e7efe112417d2b96936fcd0cafeb9442e Closes-Bug: #1798493 --- doc/source/cli/command-objects/complete.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/command-objects/complete.rst b/doc/source/cli/command-objects/complete.rst index 20e5c41da4..165d5d07d5 100644 --- a/doc/source/cli/command-objects/complete.rst +++ b/doc/source/cli/command-objects/complete.rst @@ -11,8 +11,11 @@ Typical usage for this command is:: openstack complete | sudo tee /etc/bash_completion.d/osc.bash_completion > /dev/null -If installing ``python-openstackclient`` from a package (``apt-get`` or ``yum``), -then this command will likely be run for you. +It is highly recommended to install ``python-openstackclient`` from a package +(``apt-get`` or ``yum``). In some distributions the package ``bash-completion`` is shipped +as dependency, and the `openstack complete` command will be run as a post-install action, +however not every distribution include this dependency and you might need to install +``bash-completion`` package to enable autocomplete feature. complete -------- From dfd37a2e6e49fc547e7a2e8f7840561ece8ff46f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 23 Jul 2018 15:53:53 -0500 Subject: [PATCH 0015/1120] Make use of keystoneauth service-type filtering for versions The first version of the versions show command does client-side service-type filtering, which while functional, causes many more API calls than needed. Now that keystoneauth supports the filtering at the source, use it. Change-Id: I57c49e67f9cb285a5f5bc19ec53a42d10de9f0da --- lower-constraints.txt | 2 +- openstackclient/common/versions.py | 16 ++-------------- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index db92fef36b..acd11d3238 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.4.0 +keystoneauth1==3.6.2 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 diff --git a/openstackclient/common/versions.py b/openstackclient/common/versions.py index 6a93d3002b..3c267bfe0a 100644 --- a/openstackclient/common/versions.py +++ b/openstackclient/common/versions.py @@ -14,7 +14,6 @@ """Versions Action Implementation""" -import os_service_types from osc_lib.command import command from openstackclient.i18n import _ @@ -67,7 +66,8 @@ def take_action(self, parsed_args): session = self.app.client_manager.session version_data = session.get_all_version_data( interface=interface, - region_name=parsed_args.region_name) + region_name=parsed_args.region_name, + service_type=parsed_args.service) columns = [ "Region Name", @@ -83,22 +83,10 @@ def take_action(self, parsed_args): if status: status = status.upper() - service = parsed_args.service - if service: - # Normalize service type argument to official type - service_type_manager = os_service_types.ServiceTypes() - service = service_type_manager.get_service_type(service) - versions = [] for region_name, interfaces in version_data.items(): for interface, services in interfaces.items(): for service_type, service_versions in services.items(): - if service and service != service_type: - # TODO(mordred) Once there is a version of - # keystoneauth that can do this filtering - # before making all the discovery calls, switch - # to that. - continue for data in service_versions: if status and status != data['status']: continue diff --git a/requirements.txt b/requirements.txt index 205d4e64ec..78c10b4497 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 -keystoneauth1>=3.4.0 # Apache-2.0 +keystoneauth1>=3.6.2 # Apache-2.0 openstacksdk>=0.17.0 # Apache-2.0 osc-lib>=1.10.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 From 746d42430b26b2d4fc3217044a10a2f8ae08eee1 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 23 Oct 2018 11:52:39 -0500 Subject: [PATCH 0016/1120] Update release note version reference table The table indicating the openstackclient release at the time of the overall OpenStack release had not been updated since Pike. This adds release versions for Queens and Rocky. Change-Id: Icd9a72c2460fae3d1b714b6da473564e6c709ef6 Signed-off-by: Sean McGinnis --- releasenotes/source/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 2413a57425..19df6740de 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -26,6 +26,8 @@ OpenStack release was made is shown below: ================= ======================= OpenStack Release OpenStackClient Release ================= ======================= +Rocky 3.16.0 +Queens 3.14.0 Pike 3.12.0 Ocata 3.8.1 Newton 3.2.0 From dd958bc1ee13d1604b85b37eb783dc345b8a72de Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Fri, 26 Oct 2018 10:39:47 -0400 Subject: [PATCH 0017/1120] Update the Neutron CLI decoder document The floatingip-disassociate mapping had 'port' instead of '--port', also fixed a few places where 'unset' was missing from some of the mappings. Trivialfix Change-Id: I3b01db28dda674e9988176d496154fbd26e4449f --- doc/source/cli/data/neutron.csv | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/cli/data/neutron.csv b/doc/source/cli/data/neutron.csv index be3f32a867..080114e241 100644 --- a/doc/source/cli/data/neutron.csv +++ b/doc/source/cli/data/neutron.csv @@ -50,7 +50,7 @@ flavor-update,network flavor set,Update a Neutron service flavor. floatingip-associate,floating ip set port --fixed-ip,Create a mapping between a floating IP and a fixed IP. floatingip-create,floating ip create,Create a floating IP for a given tenant. floatingip-delete,floating ip delete,Delete a given floating IP. -floatingip-disassociate,floating ip unset port,Remove a mapping from a floating IP to a fixed IP. +floatingip-disassociate,floating ip unset --port,Remove a mapping from a floating IP to a fixed IP. floatingip-list,floating ip list,List floating IPs that belong to a given tenant. floatingip-show,floating ip show,Show information of a given floating IP. help,help,print detailed help for another command @@ -142,12 +142,12 @@ net-ip-availability-show,ip availability show,Show IP usage of specific network net-list,network list,List networks that belong to a given tenant. net-list-on-dhcp-agent,network list --agent,List the networks on a DHCP agent. net-show,network show,Show information of a given network. -net-update,network set,Update network's information. +net-update,network set / network unset,Update network's information. port-create,port create,Create a port for a given tenant. port-delete,port delete,Delete a given port. port-list,port list,List ports that belong to a given tenant. port-show,port show,Show information of a given port. -port-update,port set/port unset,Update port's information. +port-update,port set / port unset,Update port's information. purge,,Delete all resources that belong to a given tenant. qos-available-rule-types,network qos rule type list,List available qos rule types. qos-bandwidth-limit-rule-create,network qos rule create --type bandwidth-limit,Create a qos bandwidth limit rule. @@ -183,7 +183,7 @@ rbac-update,network rbac set,Update RBAC policy for given tenant. router-create,router create,Create a router for a given tenant. router-delete,router delete,Delete a given router. router-gateway-clear,router unset,Remove an external network gateway from a router. -router-gateway-set,router set,Set the external network gateway for a router. +router-gateway-set,router set / router unset,Set the external network gateway for a router. router-interface-add,router add subnet / router add port,Add an internal network interface to a router. router-interface-delete,router remove subnet / router remove port,Remove an internal network interface from a router. router-list,router list,List routers that belong to a given tenant. @@ -199,7 +199,7 @@ security-group-rule-delete,security group rule delete,Delete a given security gr security-group-rule-list,security group rule list,List security group rules that belong to a given tenant. security-group-rule-show,security group rule show,Show information of a given security group rule. security-group-show,security group show,Show information of a given security group. -security-group-update,security group set,Update a given security group. +security-group-update,security group set / security group unset,Update a given security group. service-provider-list,network service provider list,List service providers. subnet-create,subnet create,Create a subnet for a given tenant. subnet-delete,subnet delete,Delete a given subnet. From f00ffebea61f94f2334d1c1578bc23986bbcf58c Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Wed, 4 Apr 2018 14:56:27 -0500 Subject: [PATCH 0018/1120] Remove invalid 'unlock-volume' migration arg There is an optional flag that can be passed in to a volume migration to tell Cinder to 'lock' a volume so no other process can abort the migration. This is reflected correctly with the --lock-volume argument flag to `openstack volume migrate`, but there is another --unlock-volume flag that is shown in the help text for this command that does not do anything and is not used anywhere. Since there is no action to "unlock" a volume, this just causes confusion - including for Cinder developers that know this API. To avoid confusion, this invalid flag should just be removed from the command. Change-Id: I5f111ed58803a1bf5d34e828341d735099247108 --- doc/source/cli/command-objects/volume.rst | 9 +------ .../tests/unit/volume/v2/test_volume.py | 24 ------------------- openstackclient/volume/v2/volume.py | 10 +------- .../notes/unlock-volume-a6990fc3bf1f5f67.yaml | 5 ++++ 4 files changed, 7 insertions(+), 41 deletions(-) create mode 100644 releasenotes/notes/unlock-volume-a6990fc3bf1f5f67.yaml diff --git a/doc/source/cli/command-objects/volume.rst b/doc/source/cli/command-objects/volume.rst index a06a5d4007..fd704b04c2 100644 --- a/doc/source/cli/command-objects/volume.rst +++ b/doc/source/cli/command-objects/volume.rst @@ -226,7 +226,7 @@ Migrate volume to a new host openstack volume migrate --host [--force-host-copy] - [--lock-volume | --unlock-volume] + [--lock-volume] .. option:: --host @@ -245,13 +245,6 @@ Migrate volume to a new host *Volume version 2 only* -.. option:: --unlock-volume - - If specified, the volume state will not be locked and the a - migration can be aborted (default) (possibly by another operation) - - *Volume version 2 only* - .. _volume_migrate-volume: .. describe:: diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 2fa924b8a5..f9a8b7c680 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -1320,7 +1320,6 @@ def test_volume_migrate(self): verifylist = [ ("force_host_copy", False), ("lock_volume", False), - ("unlock_volume", False), ("host", "host@backend-name#pool"), ("volume", self._volume.id), ] @@ -1342,7 +1341,6 @@ def test_volume_migrate_with_option(self): verifylist = [ ("force_host_copy", True), ("lock_volume", True), - ("unlock_volume", False), ("host", "host@backend-name#pool"), ("volume", self._volume.id), ] @@ -1354,27 +1352,6 @@ def test_volume_migrate_with_option(self): self._volume.id, "host@backend-name#pool", True, True) self.assertIsNone(result) - def test_volume_migrate_with_unlock_volume(self): - arglist = [ - "--unlock-volume", - "--host", "host@backend-name#pool", - self._volume.id, - ] - verifylist = [ - ("force_host_copy", False), - ("lock_volume", False), - ("unlock_volume", True), - ("host", "host@backend-name#pool"), - ("volume", self._volume.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_once_with(self._volume.id) - self.volumes_mock.migrate_volume.assert_called_once_with( - self._volume.id, "host@backend-name#pool", False, False) - self.assertIsNone(result) - def test_volume_migrate_without_host(self): arglist = [ self._volume.id, @@ -1382,7 +1359,6 @@ def test_volume_migrate_without_host(self): verifylist = [ ("force_host_copy", False), ("lock_volume", False), - ("unlock_volume", False), ("volume", self._volume.id), ] diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 61f846b02f..55b7fac300 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -472,21 +472,13 @@ def get_parser(self, prog_name): help=_("Enable generic host-based force-migration, " "which bypasses driver optimizations") ) - lock_group = parser.add_mutually_exclusive_group() - lock_group.add_argument( + parser.add_argument( '--lock-volume', action="store_true", help=_("If specified, the volume state will be locked " "and will not allow a migration to be aborted " "(possibly by another operation)") ) - lock_group.add_argument( - '--unlock-volume', - action="store_true", - help=_("If specified, the volume state will not be " - "locked and the a migration can be aborted " - "(default) (possibly by another operation)") - ) return parser def take_action(self, parsed_args): diff --git a/releasenotes/notes/unlock-volume-a6990fc3bf1f5f67.yaml b/releasenotes/notes/unlock-volume-a6990fc3bf1f5f67.yaml new file mode 100644 index 0000000000..60124bf890 --- /dev/null +++ b/releasenotes/notes/unlock-volume-a6990fc3bf1f5f67.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The ``volume migrate --unlock`` argument did not actually do anything and + has now been removed. From 4c387fe94cd33ce9357df52d848d04b69c90b3fd Mon Sep 17 00:00:00 2001 From: lvxianguo Date: Thu, 1 Nov 2018 16:45:27 +0800 Subject: [PATCH 0019/1120] trivial: modify spelling error of project Change-Id: I6cf68325bc58cfd073c5fca4eb2773d108735399 --- doc/source/cli/data/keystone.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/data/keystone.csv b/doc/source/cli/data/keystone.csv index 03c34704da..bcc305515f 100644 --- a/doc/source/cli/data/keystone.csv +++ b/doc/source/cli/data/keystone.csv @@ -18,7 +18,7 @@ service-get,service show,Display service from Service Catalog. service-list,service list,List all services in Service Catalog. tenant-create,project create,Create new tenant. tenant-delete,project delete,Delete tenant. -tenant-get,proejct show,Display tenant details. +tenant-get,project show,Display tenant details. tenant-list,project list,List all tenants. tenant-update,project set,"Update tenant name, description, enabled status." token-get,token issue,Display the current user token. From e782f49927a6b142a35f85a1a4a759fd2b1ceedf Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Mon, 14 May 2018 18:13:28 +0000 Subject: [PATCH 0020/1120] Add --name-lookup-one-by-one option to server list usually in a big cloud there are many images and flavors, while each given project might use only some of those. This patch introduces '--name-lookup-one-by-one' argument to server list command (mutually exclusive with '--no-name-lookup') When provided (or either '--image' or '--flavor' is specified) to the `server list` command, name resolving for corresponding entity is now using targeted GET commands instead of full entities list. In some situations this can significantly speedup the execution of the `server list` command by reducing the number of API requests performed. Change-Id: I59cbf3f75c55e5d3747654edcc9be86ad954cf40 Story: #2002039 Task: #19682 --- openstackclient/compute/v2/server.py | 70 +++++++++++++------ .../tests/unit/compute/v2/test_server.py | 30 +++++++- ...me-lookup-one-by-one-e0f15f4eab329b19.yaml | 14 ++++ 3 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/name-lookup-one-by-one-e0f15f4eab329b19.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 777f7744e7..9247c40e53 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1044,11 +1044,22 @@ def get_parser(self, prog_name): default=False, help=_('List additional fields in output'), ) - parser.add_argument( + name_lookup_group = parser.add_mutually_exclusive_group() + name_lookup_group.add_argument( '-n', '--no-name-lookup', action='store_true', default=False, - help=_('Skip flavor and image name lookup.'), + help=_('Skip flavor and image name lookup.' + 'Mutually exclusive with "--name-lookup-one-by-one"' + ' option.'), + ) + name_lookup_group.add_argument( + '--name-lookup-one-by-one', + action='store_true', + default=False, + help=_('When looking up flavor and image names, look them up' + 'one by one as needed instead of all together (default). ' + 'Mutually exclusive with "--no-name-lookup|-n" option.'), ) parser.add_argument( '--marker', @@ -1223,28 +1234,43 @@ def take_action(self, parsed_args): limit=parsed_args.limit) images = {} - # Create a dict that maps image_id to image object. - # Needed so that we can display the "Image Name" column. - # "Image Name" is not crucial, so we swallow any exceptions. - if data and not parsed_args.no_name_lookup: - try: - images_list = self.app.client_manager.image.images.list() - for i in images_list: - images[i.id] = i - except Exception: - pass - flavors = {} - # Create a dict that maps flavor_id to flavor object. - # Needed so that we can display the "Flavor Name" column. - # "Flavor Name" is not crucial, so we swallow any exceptions. if data and not parsed_args.no_name_lookup: - try: - flavors_list = compute_client.flavors.list(is_public=None) - for i in flavors_list: - flavors[i.id] = i - except Exception: - pass + # Create a dict that maps image_id to image object. + # Needed so that we can display the "Image Name" column. + # "Image Name" is not crucial, so we swallow any exceptions. + if parsed_args.name_lookup_one_by_one or image_id: + for i_id in set(filter(lambda x: x is not None, + (s.image.get('id') for s in data))): + try: + images[i_id] = image_client.images.get(i_id) + except Exception: + pass + else: + try: + images_list = image_client.images.list() + for i in images_list: + images[i.id] = i + except Exception: + pass + + # Create a dict that maps flavor_id to flavor object. + # Needed so that we can display the "Flavor Name" column. + # "Flavor Name" is not crucial, so we swallow any exceptions. + if parsed_args.name_lookup_one_by_one or flavor_id: + for f_id in set(filter(lambda x: x is not None, + (s.flavor.get('id') for s in data))): + try: + flavors[f_id] = compute_client.flavors.get(f_id) + except Exception: + pass + else: + try: + flavors_list = compute_client.flavors.list(is_public=None) + for i in flavors_list: + flavors[i.id] = i + except Exception: + pass # Populate image_name, image_id, flavor_name and flavor_id attributes # of server objects so that we can display those columns. diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 46d4c24114..6243b49ddf 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1938,12 +1938,18 @@ def test_server_list_no_option(self): ('all_projects', False), ('long', False), ('deleted', False), + ('name_lookup_one_by_one', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) + self.images_mock.list.assert_called() + self.flavors_mock.list.assert_called() + # we did not pass image or flavor, so gets on those must be absent + self.assertFalse(self.flavors_mock.get.call_count) + self.assertFalse(self.images_mock.get.call_count) self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) @@ -2014,6 +2020,28 @@ def test_server_list_n_option(self): self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data_no_name_lookup), tuple(data)) + def test_server_list_name_lookup_one_by_one(self): + arglist = [ + '--name-lookup-one-by-one' + ] + verifylist = [ + ('all_projects', False), + ('no_name_lookup', False), + ('name_lookup_one_by_one', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + 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.get.assert_called() + self.flavors_mock.get.assert_called() + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + def test_server_list_with_image(self): arglist = [ @@ -2046,7 +2074,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.assert_called_with(self.flavor.id) + self.flavors_mock.get.has_calls(self.flavor.id) self.search_opts['flavor'] = self.flavor.id self.servers_mock.list.assert_called_with(**self.kwargs) diff --git a/releasenotes/notes/name-lookup-one-by-one-e0f15f4eab329b19.yaml b/releasenotes/notes/name-lookup-one-by-one-e0f15f4eab329b19.yaml new file mode 100644 index 0000000000..b572b75d5e --- /dev/null +++ b/releasenotes/notes/name-lookup-one-by-one-e0f15f4eab329b19.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Add ``--name-lookup-one-by-one`` option to the ``server list`` command + that is (mutually exclusive with ``-n | --no-name-lookup``). + When provided, the names of images and flavors will be resolved one by one + only for those images and flavors that are needed to display the obtained + list of servers instead of fetching all the images and flavors. + Depending on amount of images in your deployment this can speed up the + execution of this command. + - | + When given ``--image`` or ``--flavor`` argument, the ``server list`` + command now resolves only single image or flavor instead of fetching + all the images or flavors for name lookup purposes. From 013c9a4f3a44cb0b81fc7affe9b933e701cb5dba Mon Sep 17 00:00:00 2001 From: melanie witt Date: Fri, 2 Nov 2018 22:27:55 +0000 Subject: [PATCH 0021/1120] Handle multiple ports in AddFloatingIP AddFloatingIP refers to an old nova proxy API to neutron that was deprecated in nova. The neutron API for floating IP associate requires a port to be specified. Currently, the code is selecting the first port if the server has multiple ports. But, an attempt to associate the first port with a floating IP can fail if the first port is not on a network that is attached to an external gateway. In order to make the command work better for users who have a server with multiple ports, we can: 1. Select the port corresponding to the fixed_ip_address, if one was specified 2. Try to associate the floating IP with each port until one of the attempts succeeds, else re-raise the last exception. (404 ExternalGatewayForFloatingIPNotFound from neutron) This also fixes incorrect FakeFloatingIP attributes that were being set in the TestServerAddFloatingIPNetwork unit tests, which were causing the tests to use None as parsed args for ip-address and --fixed-ip-address and thus bypassing code in the 'if parsed_args.fixed_ip_address:' block. Task: 27800 Story: 2004263 Change-Id: I11fbcebf6b00f12a030b000c84dcf1d6b5e86250 --- openstackclient/compute/v2/server.py | 51 ++++++-- .../tests/unit/compute/v2/test_server.py | 114 ++++++++++++++++-- ...oating-ip-multi-port-9779e88f590cae23.yaml | 14 +++ 3 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/floating-ip-multi-port-9779e88f590cae23.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 4b0aedd7ac..9051c8d25b 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -23,6 +23,7 @@ from novaclient import api_versions from novaclient.v2 import servers +from openstack import exceptions as sdk_exceptions from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -251,13 +252,16 @@ def update_parser_common(self, parser): parser.add_argument( "ip_address", metavar="", - help=_("Floating IP address to assign to server (IP only)"), + help=_("Floating IP address to assign to the first available " + "server port (IP only)"), ) parser.add_argument( "--fixed-ip-address", metavar="", help=_( - "Fixed IP address to associate with this floating IP address" + "Fixed IP address to associate with this floating IP address. " + "The first server port containing the fixed IP address will " + "be used" ), ) return parser @@ -274,12 +278,45 @@ def take_action_network(self, client, parsed_args): compute_client.servers, parsed_args.server, ) - port = list(client.ports(device_id=server.id))[0] - attrs['port_id'] = port.id + ports = list(client.ports(device_id=server.id)) + # If the fixed IP address was specified, we need to find the + # corresponding port. if parsed_args.fixed_ip_address: - attrs['fixed_ip_address'] = parsed_args.fixed_ip_address - - client.update_ip(obj, **attrs) + fip_address = parsed_args.fixed_ip_address + attrs['fixed_ip_address'] = fip_address + for port in ports: + for ip in port.fixed_ips: + if ip['ip_address'] == fip_address: + attrs['port_id'] = port.id + break + else: + continue + break + if 'port_id' not in attrs: + msg = _('No port found for fixed IP address %s') + raise exceptions.CommandError(msg % fip_address) + client.update_ip(obj, **attrs) + else: + # It's possible that one or more ports are not connected to a + # router and thus could fail association with a floating IP. + # Try each port until one succeeds. If none succeed, re-raise the + # last exception. + error = None + for port in ports: + attrs['port_id'] = port.id + try: + client.update_ip(obj, **attrs) + except sdk_exceptions.NotFoundException as exp: + # 404 ExternalGatewayForFloatingIPNotFound from neutron + LOG.info('Skipped port %s because it is not attached to ' + 'an external gateway', port.id) + error = exp + continue + else: + error = None + break + if error: + raise error def take_action_compute(self, client, parsed_args): client.api.floating_ip_add( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 928794fba7..afa2e80a46 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -19,6 +19,7 @@ import mock from mock import call from novaclient import api_versions +from openstack import exceptions as sdk_exceptions from osc_lib import exceptions from osc_lib import utils as common_utils from oslo_utils import timeutils @@ -222,11 +223,11 @@ def test_server_add_floating_ip_default(self): self.network.ports = mock.Mock(return_value=[_port]) arglist = [ _server.id, - _floating_ip['ip'], + _floating_ip['floating_ip_address'], ] verifylist = [ ('server', _server.id), - ('ip_address', _floating_ip['ip']), + ('ip_address', _floating_ip['floating_ip_address']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -237,7 +238,7 @@ def test_server_add_floating_ip_default(self): } self.network.find_ip.assert_called_once_with( - _floating_ip['ip'], + _floating_ip['floating_ip_address'], ignore_missing=False, ) self.network.ports.assert_called_once_with( @@ -248,6 +249,64 @@ def test_server_add_floating_ip_default(self): **attrs ) + def test_server_add_floating_ip_default_no_external_gateway(self, + success=False): + _server = compute_fakes.FakeServer.create_one_server() + self.servers_mock.get.return_value = _server + _port = network_fakes.FakePort.create_one_port() + _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() + self.network.find_ip = mock.Mock(return_value=_floating_ip) + return_value = [_port] + # In the success case, we'll have two ports, where the first port is + # not attached to an external gateway but the second port is. + if success: + return_value.append(_port) + self.network.ports = mock.Mock(return_value=return_value) + side_effect = [sdk_exceptions.NotFoundException()] + if success: + side_effect.append(None) + self.network.update_ip = mock.Mock(side_effect=side_effect) + arglist = [ + _server.id, + _floating_ip['floating_ip_address'], + ] + verifylist = [ + ('server', _server.id), + ('ip_address', _floating_ip['floating_ip_address']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + if success: + self.cmd.take_action(parsed_args) + else: + self.assertRaises(sdk_exceptions.NotFoundException, + self.cmd.take_action, parsed_args) + + attrs = { + 'port_id': _port.id, + } + + self.network.find_ip.assert_called_once_with( + _floating_ip['floating_ip_address'], + ignore_missing=False, + ) + self.network.ports.assert_called_once_with( + device_id=_server.id, + ) + if success: + self.assertEqual(2, self.network.update_ip.call_count) + calls = [mock.call(_floating_ip, **attrs)] * 2 + self.network.update_ip.assert_has_calls(calls) + else: + self.network.update_ip.assert_called_once_with( + _floating_ip, + **attrs + ) + + def test_server_add_floating_ip_default_one_external_gateway(self): + self.test_server_add_floating_ip_default_no_external_gateway( + success=True) + def test_server_add_floating_ip_fixed(self): _server = compute_fakes.FakeServer.create_one_server() self.servers_mock.get.return_value = _server @@ -255,26 +314,31 @@ def test_server_add_floating_ip_fixed(self): _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() self.network.find_ip = mock.Mock(return_value=_floating_ip) self.network.ports = mock.Mock(return_value=[_port]) + # The user has specified a fixed ip that matches one of the ports + # already attached to the instance. arglist = [ - '--fixed-ip-address', _floating_ip['fixed_ip'], + '--fixed-ip-address', _port.fixed_ips[0]['ip_address'], _server.id, - _floating_ip['ip'], + _floating_ip['floating_ip_address'], ] verifylist = [ - ('fixed_ip_address', _floating_ip['fixed_ip']), + ('fixed_ip_address', _port.fixed_ips[0]['ip_address']), ('server', _server.id), - ('ip_address', _floating_ip['ip']), + ('ip_address', _floating_ip['floating_ip_address']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) + # We expect the update_ip call to specify a new fixed_ip_address which + # will overwrite the floating ip's existing fixed_ip_address. attrs = { 'port_id': _port.id, + 'fixed_ip_address': _port.fixed_ips[0]['ip_address'], } self.network.find_ip.assert_called_once_with( - _floating_ip['ip'], + _floating_ip['floating_ip_address'], ignore_missing=False, ) self.network.ports.assert_called_once_with( @@ -285,6 +349,40 @@ def test_server_add_floating_ip_fixed(self): **attrs ) + def test_server_add_floating_ip_fixed_no_port_found(self): + _server = compute_fakes.FakeServer.create_one_server() + self.servers_mock.get.return_value = _server + _port = network_fakes.FakePort.create_one_port() + _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() + self.network.find_ip = mock.Mock(return_value=_floating_ip) + self.network.ports = mock.Mock(return_value=[_port]) + # The user has specified a fixed ip that does not match any of the + # ports already attached to the instance. + nonexistent_ip = '10.0.0.9' + arglist = [ + '--fixed-ip-address', nonexistent_ip, + _server.id, + _floating_ip['floating_ip_address'], + ] + verifylist = [ + ('fixed_ip_address', nonexistent_ip), + ('server', _server.id), + ('ip_address', _floating_ip['floating_ip_address']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + self.network.find_ip.assert_called_once_with( + _floating_ip['floating_ip_address'], + ignore_missing=False, + ) + self.network.ports.assert_called_once_with( + device_id=_server.id, + ) + self.network.update_ip.assert_not_called() + class TestServerAddPort(TestServer): diff --git a/releasenotes/notes/floating-ip-multi-port-9779e88f590cae23.yaml b/releasenotes/notes/floating-ip-multi-port-9779e88f590cae23.yaml new file mode 100644 index 0000000000..738190f90b --- /dev/null +++ b/releasenotes/notes/floating-ip-multi-port-9779e88f590cae23.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + The ``openstack server add floating ip`` command has been fixed to handle + servers with multiple ports attached. Previously, the command was using + the first port in the port list when attempting to associate the floating + ip. This could fail if the server had multiple ports and the first port + in the list was not attached to an external gateway. Another way it could + fail is if the ``--fixed-ip-address`` option was passed and the first port + did not have the specified fixed IP address attached to it. + Now, the ``openstack server add floating ip`` command will find the port + attached to the specified ``--fixed-ip-address``, if provided, else it will + try multiple ports until one is found attached to an external gateway. If + a suitable port is not found in the port list, an error will be returned. From 21e4c87bdeb78ef5c5a251ae9e671efd1b5e3102 Mon Sep 17 00:00:00 2001 From: Sven Wegener Date: Fri, 12 Jan 2018 14:19:11 +0100 Subject: [PATCH 0022/1120] image/v2: support multiple property filters Change-Id: I8ba40cb8ca647ec24b80c2824bb64e84430535d4 Signed-off-by: Sven Wegener --- openstackclient/image/v2/image.py | 18 +++++++++--------- ...r-multiple-properties-03c51d43131ee3b6.yaml | 6 ++++++ 2 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/image-list-filter-multiple-properties-03c51d43131ee3b6.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 1e67692a54..06eebe984c 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -515,7 +515,8 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Filter output based on property'), + help=_('Filter output based on property ' + '(repeat option to filter on multiple properties)'), ) parser.add_argument( '--name', @@ -643,14 +644,13 @@ def take_action(self, parsed_args): marker = page[-1]['id'] if parsed_args.property: - # NOTE(dtroyer): coerce to a list to subscript it in py3 - attr, value = list(parsed_args.property.items())[0] - api_utils.simple_filter( - data, - attr=attr, - value=value, - property_field='properties', - ) + for attr, value in parsed_args.property.items(): + api_utils.simple_filter( + data, + attr=attr, + value=value, + property_field='properties', + ) data = utils.sort_items(data, parsed_args.sort, str) diff --git a/releasenotes/notes/image-list-filter-multiple-properties-03c51d43131ee3b6.yaml b/releasenotes/notes/image-list-filter-multiple-properties-03c51d43131ee3b6.yaml new file mode 100644 index 0000000000..294f43810e --- /dev/null +++ b/releasenotes/notes/image-list-filter-multiple-properties-03c51d43131ee3b6.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``image list`` command now properly filters images on multiple + ``--property`` options. + [Bug `2004290 `_] From 0d764cdb5a1f9a8b113721634fcc8948ddd82c8d Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Fri, 19 Oct 2018 12:29:00 +0530 Subject: [PATCH 0023/1120] Add project param in LimitList parser when doing openstack limit list --project xyz_id, CLI raising error unrecognized arguments, whereas in api-ref document [1], user can pass project_id as query param.This addresses the above issue, by adding param --project in parser of LimitList. [1] https://developer.openstack.org/api-ref/identity/v3/index.html Change-Id: If4644cc99a3803f61f4a688b828aeb73977fc0dd Closes-Bug: #1798744 --- doc/source/cli/command-objects/limit.rst | 5 +++++ openstackclient/identity/v3/limit.py | 13 ++++++++++++- .../tests/unit/identity/v3/test_limit.py | 3 ++- .../notes/bug-1798744-5512256baf4dc633.yaml | 4 ++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/bug-1798744-5512256baf4dc633.yaml diff --git a/doc/source/cli/command-objects/limit.rst b/doc/source/cli/command-objects/limit.rst index 71cf2a420d..46da6c1b70 100644 --- a/doc/source/cli/command-objects/limit.rst +++ b/doc/source/cli/command-objects/limit.rst @@ -73,6 +73,7 @@ List project-specific limits [--service ] [--resource-name ] [--region ] + [--project ] .. option:: --service @@ -86,6 +87,10 @@ List project-specific limits The region name to filter the response by +.. option:: --project + + List resource limits associated with project + limit show ---------- diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py index c6f1cb1fb5..57d1dfd641 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -116,6 +116,11 @@ def get_parser(self, prog_name): metavar='', help=_('Region for the registered limit to affect.'), ) + parser.add_argument( + '--project', + metavar='', + help=_('List resource limits associated with project'), + ) return parser def take_action(self, parsed_args): @@ -131,11 +136,17 @@ def take_action(self, parsed_args): region = utils.find_resource( identity_client.regions, parsed_args.region ) + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, parsed_args.project + ) limits = identity_client.limits.list( service=service, resource_name=parsed_args.resource_name, - region=region + region=region, + project=project ) columns = ( diff --git a/openstackclient/tests/unit/identity/v3/test_limit.py b/openstackclient/tests/unit/identity/v3/test_limit.py index 44c0358d89..e5cd87b8bd 100644 --- a/openstackclient/tests/unit/identity/v3/test_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_limit.py @@ -362,7 +362,8 @@ def test_limit_list(self): columns, data = self.cmd.take_action(parsed_args) self.limit_mock.list.assert_called_with( - service=None, resource_name=None, region=None + service=None, resource_name=None, region=None, + project=None ) collist = ( diff --git a/releasenotes/notes/bug-1798744-5512256baf4dc633.yaml b/releasenotes/notes/bug-1798744-5512256baf4dc633.yaml new file mode 100644 index 0000000000..1dd01ba001 --- /dev/null +++ b/releasenotes/notes/bug-1798744-5512256baf4dc633.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add ``--project`` option to ``limit list`` command. + [Bug `1798744 `_] From 81fd5c995d7cce45f45e37af8a6b0309308a4cba Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Mon, 22 Oct 2018 13:51:30 +0530 Subject: [PATCH 0024/1120] Updated the take_actions for unified limits When user passes --region None, the find_resource of osc_lib calls get() of region. The get API of region ignores the name param returning all the regions in result. As the find_resource checks many cases against the result returned by get API. The output comes greater than 1, thus returning "More than one region ID exist" which is incorrect. However in case of region which cannot be filtered by name we do not require to check these many cases. The solution is to directly call the get method of APIs and returning No resource name exist with the xyz" on passing invaid parameter. And returning all in case of None. Thus created a new function get_resource which can be used in future too by these types of API's. Change-Id: Ib3f881d34a82af97199ce51bfbefc6f3f08599f1 Closes-bug: #1799153 --- openstackclient/identity/common.py | 19 ++++++++ openstackclient/identity/v3/limit.py | 29 +++++++++-- .../identity/v3/registered_limit.py | 48 +++++++++++++++---- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index f36f5f73a9..e825116649 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -68,6 +68,25 @@ def find_service(identity_client, name_type_or_id): raise exceptions.CommandError(msg % name_type_or_id) +def get_resource(manager, name_type_or_id): + # NOTE (vishakha): Due to bug #1799153 and for any another related case + # where GET resource API does not support the filter by name, + # osc_lib.utils.find_resource() method cannot be used because that method + # try to fall back to list all the resource if requested resource cannot + # be get via name. Which ends up with NoUniqueMatch error. + # This new function is the replacement for osc_lib.utils.find_resource() + # for resources does not support GET by name. + # For example: identity GET /regions. + """Find a resource by id or name.""" + + try: + return manager.get(name_type_or_id) + except identity_exc.NotFound: + # raise NotFound exception + msg = _("No resource with name or id of '%s' exists") + raise exceptions.CommandError(msg % name_type_or_id) + + def _get_token_resource(client, resource, parsed_name, parsed_domain=None): """Peek into the user's auth token to get resource IDs diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py index 57d1dfd641..f2af81e9f0 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -78,9 +78,19 @@ def take_action(self, parsed_args): ) region = None if parsed_args.region: - region = utils.find_resource( - identity_client.regions, parsed_args.region - ) + val = getattr(parsed_args, 'region', None) + if 'None' not in val: + # NOTE (vishakha): Due to bug #1799153 and for any another + # related case where GET resource API does not support the + # filter by name, osc_lib.utils.find_resource() method cannot + # be used because that method try to fall back to list all the + # resource if requested resource cannot be get via name. Which + # ends up with NoUniqueMatch error. + # So osc_lib.utils.find_resource() function cannot be used for + # 'regions', using common_utils.get_resource() instead. + region = common_utils.get_resource( + identity_client.regions, parsed_args.region + ) limit = identity_client.limits.create( project, @@ -136,6 +146,19 @@ def take_action(self, parsed_args): region = utils.find_resource( identity_client.regions, parsed_args.region ) + val = getattr(parsed_args, 'region', None) + if 'None' not in val: + # NOTE (vishakha): Due to bug #1799153 and for any another + # related case where GET resource API does not support the + # filter by name, osc_lib.utils.find_resource() method cannot + # be used because that method try to fall back to list all the + # resource if requested resource cannot be get via name. Which + # ends up with NoUniqueMatch error. + # So osc_lib.utils.find_resource() function cannot be used for + # 'regions', using common_utils.get_resource() instead. + region = common_utils.get_resource( + identity_client.regions, parsed_args.region + ) project = None if parsed_args.project: project = utils.find_resource( diff --git a/openstackclient/identity/v3/registered_limit.py b/openstackclient/identity/v3/registered_limit.py index 72e07297e6..b308080ea2 100644 --- a/openstackclient/identity/v3/registered_limit.py +++ b/openstackclient/identity/v3/registered_limit.py @@ -69,9 +69,19 @@ def take_action(self, parsed_args): ) region = None if parsed_args.region: - region = utils.find_resource( - identity_client.regions, parsed_args.region - ) + val = getattr(parsed_args, 'region', None) + if 'None' not in val: + # NOTE (vishakha): Due to bug #1799153 and for any another + # related case where GET resource API does not support the + # filter by name, osc_lib.utils.find_resource() method cannot + # be used because that method try to fall back to list all the + # resource if requested resource cannot be get via name. Which + # ends up with NoUniqueMatch error. + # So osc_lib.utils.find_resource() function cannot be used for + # 'regions', using common_utils.get_resource() instead. + region = common_utils.get_resource( + identity_client.regions, parsed_args.region + ) registered_limit = identity_client.registered_limits.create( service, @@ -153,9 +163,19 @@ def take_action(self, parsed_args): ) region = None if parsed_args.region: - region = utils.find_resource( - identity_client.regions, parsed_args.region - ) + val = getattr(parsed_args, 'region', None) + if 'None' not in val: + # NOTE (vishakha): Due to bug #1799153 and for any another + # related case where GET resource API does not support the + # filter by name, osc_lib.utils.find_resource() method cannot + # be used because that method try to fall back to list all the + # resource if requested resource cannot be get via name. Which + # ends up with NoUniqueMatch error. + # So osc_lib.utils.find_resource() function cannot be used for + # 'regions', using common_utils.get_resource() instead. + region = common_utils.get_resource( + identity_client.regions, parsed_args.region + ) registered_limits = identity_client.registered_limits.list( service=service, @@ -222,9 +242,19 @@ def take_action(self, parsed_args): region = None if parsed_args.region: - region = utils.find_resource( - identity_client.regions, parsed_args.region - ) + val = getattr(parsed_args, 'region', None) + if 'None' not in val: + # NOTE (vishakha): Due to bug #1799153 and for any another + # related case where GET resource API does not support the + # filter by name, osc_lib.utils.find_resource() method cannot + # be used because that method try to fall back to list all the + # resource if requested resource cannot be get via name. Which + # ends up with NoUniqueMatch error. + # So osc_lib.utils.find_resource() function cannot be used for + # 'regions', using common_utils.get_resource() instead. + region = common_utils.get_resource( + identity_client.regions, parsed_args.region + ) registered_limit = identity_client.registered_limits.update( parsed_args.registered_limit_id, From eb06a24e4ff1e92821dafc37015315191a593445 Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Fri, 12 Oct 2018 15:19:19 +0530 Subject: [PATCH 0025/1120] Modify the help message for 'registered limit set' Regsitered limit set CLI takes --service, --region and --resource-name as param which can be updated along with --default limit for existing registered limit. Default limit can be updated with same value and CLI return the success. But --service, --region and --resource- name cannot be same as existing one. CLI return 409 for this case. Which is valid behaviour because more than one limit with same service and same resource cannot exist. But help message of --service, --region and --resource-name are not much clear to tell that they cannot be passed with same value. This patch clarifies the help message for resigtered limit set CLI. Reference Scenario: * openstack registered limit set --default-limit 91 64c2e97fbe904b888544ffdcab21989b limit updated sucessfully Updating limit with exsiting service and resource-name: * openstack registered limit set --default-limit 92 --resource-name snapshot 64c2e97fbe904b888544ffdcab21989b Conflict occurred attempting to store registered_limit - Duplicate entry. (HTTP 409) *openstack registered limit set --default-limit 93 --service compute 64c2e97fbe904b888544ffdcab21989b Conflict occurred attempting to store registered_limit - Duplicate entry. (HTTP 409) *openstack registered limit set --default-limit 91 --resource-name snapshot --service glance 64c2e97fbe904b888544ffdcab21989b Conflict occurred attempting to store registered_limit - Duplicate entry. (HTTP 409) Change-Id: I9e78a6250567cd981adde96946818bb016760a49 --- openstackclient/identity/v3/registered_limit.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/registered_limit.py b/openstackclient/identity/v3/registered_limit.py index 72e07297e6..2bf069048d 100644 --- a/openstackclient/identity/v3/registered_limit.py +++ b/openstackclient/identity/v3/registered_limit.py @@ -186,12 +186,18 @@ def get_parser(self, prog_name): parser.add_argument( '--service', metavar='', - help=_('Service responsible for the resource to limit'), + help=_('Service to be updated responsible for the resource to ' + 'limit. Either --service, --resource-name or --region must ' + 'be different than existing value otherwise it will be ' + 'duplicate entry') ) parser.add_argument( '--resource-name', metavar='', - help=_('The name of the resource to limit'), + help=_('Resource to be updated responsible for the resource to ' + 'limit. Either --service, --resource-name or --region must ' + 'be different than existing value otherwise it will be ' + 'duplicate entry'), ) parser.add_argument( '--default-limit', @@ -202,12 +208,15 @@ def get_parser(self, prog_name): parser.add_argument( '--description', metavar='', - help=_('Description of the registered limit'), + help=_('Description to update of the registered limit'), ) parser.add_argument( '--region', metavar='', - help=_('Region for the registered limit to affect.'), + help=_('Region for the registered limit to affect. Either ' + '--service, --resource-name or --region must be ' + 'different than existing value otherwise it will be ' + 'duplicate entry'), ) return parser From b90c780d2b99a91dd479bcc5f20caddcfb652f76 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 23 Oct 2018 10:36:34 -0500 Subject: [PATCH 0026/1120] Add volume backup import/export commands This adds commands to import and export volume backup records so they can be imported and restored on other Cinder instances or to the original instance if the service or database has been lost and had to be rebuilt. I know this is a commonly used process by some users, so it would be good to have this functionality in osc so they do not have to switch clients. More details about the export and import process can be found here: https://docs.openstack.org/cinder/latest/admin/blockstorage-volume-backups-export-import.html Change-Id: Ic95f87b36a416a2b50cb2193fd5759ab59336975 Signed-off-by: Sean McGinnis --- .../cli/command-objects/volume-backup.rst | 198 +----------------- doc/source/cli/commands.rst | 2 + doc/source/cli/data/cinder.csv | 4 +- openstackclient/tests/unit/volume/v2/fakes.py | 29 +++ .../unit/volume/v2/test_backup_record.py | 114 ++++++++++ openstackclient/volume/v2/backup_record.py | 82 ++++++++ ...volume-backup-record-9f5987c45e294dc6.yaml | 15 ++ setup.cfg | 3 + 8 files changed, 250 insertions(+), 197 deletions(-) create mode 100644 openstackclient/tests/unit/volume/v2/test_backup_record.py create mode 100644 openstackclient/volume/v2/backup_record.py create mode 100644 releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml diff --git a/doc/source/cli/command-objects/volume-backup.rst b/doc/source/cli/command-objects/volume-backup.rst index 585f47d464..632e215e2b 100644 --- a/doc/source/cli/command-objects/volume-backup.rst +++ b/doc/source/cli/command-objects/volume-backup.rst @@ -2,200 +2,8 @@ volume backup ============= -Block Storage v1, v2 +Volume v1, v2 -volume backup create --------------------- +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup * -Create new volume backup - -.. program:: volume backup create -.. code:: bash - - openstack volume backup create - [--container ] - [--name ] - [--description ] - [--snapshot ] - [--force] - [--incremental] - - -.. option:: --container - - Optional backup container name - -.. option:: --name - - Name of the backup - -.. option:: --description - - Description of the backup - -.. option:: --snapshot - - Snapshot to backup (name or ID) - - *Volume version 2 only* - -.. option:: --force - - Allow to back up an in-use volume - - *Volume version 2 only* - -.. option:: --incremental - - Perform an incremental backup - - *Volume version 2 only* - -.. _volume_backup_create-backup: -.. describe:: - - Volume to backup (name or ID) - -volume backup delete --------------------- - -Delete volume backup(s) - -.. program:: volume backup delete -.. code:: bash - - openstack volume backup delete - [--force] - [ ...] - -.. option:: --force - - Allow delete in state other than error or available - - *Volume version 2 only* - -.. _volume_backup_delete-backup: -.. describe:: - - Backup(s) to delete (name or ID) - -volume backup list ------------------- - -List volume backups - -.. program:: volume backup list -.. code:: bash - - openstack volume backup list - [--long] - [--name ] - [--status ] - [--volume ] - [--marker ] - [--limit ] - [--all-projects] - -.. _volume_backup_list-backup: -.. option:: --long - - List additional fields in output - -.. option:: --name - - Filters results by the backup name - -.. option:: --status - - Filters results by the backup status - ('creating', 'available', 'deleting', 'error', 'restoring' or 'error_restoring') - -.. option:: --volume - - Filters results by the volume which they backup (name or ID)" - -.. option:: --marker - - The last backup of the previous page (name or ID) - - *Volume version 2 only* - -.. option:: --limit - - Maximum number of backups to display - - *Volume version 2 only* - -.. option:: --all-projects - - Include all projects (admin only) - -volume backup restore ---------------------- - -Restore volume backup - -.. program:: volume backup restore -.. code:: bash - - openstack volume backup restore - - - -.. _volume_backup_restore-backup: -.. describe:: - - Backup to restore (name or ID) - -.. describe:: - - Volume to restore to (name or ID) - -volume backup set ------------------ - -Set volume backup properties - -.. program:: volume backup set -.. code:: bash - - openstack volume backup set - [--name ] - [--description ] - [--state ] - - -.. option:: --name - - New backup name - -.. option:: --description - - New backup description - -.. option:: --state - - New backup state ("available" or "error") (admin only) - (This option simply changes the state of the backup in the database with - no regard to actual status, exercise caution when using) - -.. _backup_set-volume-backup: -.. describe:: - - Backup to modify (name or ID) - -volume backup show ------------------- - -Display volume backup details - -.. program:: volume backup show -.. code:: bash - - openstack volume backup show - - -.. _volume_backup_show-backup: -.. describe:: - - Backup to display (name or ID) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index d7c9124042..cdd5e63c95 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -158,6 +158,8 @@ referring to both Compute and Volume quotas. * ``volume backup``: (**Volume**) backup for volumes * ``volume backend capability``: (**volume**) volume backend storage capabilities * ``volume backend pool``: (**volume**) volume backend storage pools +* ``volume backup record``: (**Volume**) volume record that can be imported or exported +* ``volume backend``: (**volume**) volume backend storage * ``volume host``: (**Volume**) the physical computer for volumes * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes * ``volume snapshot``: (**Volume**) a point-in-time copy of a volume diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index cc8ef2d91e..22fb84cffa 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -2,8 +2,8 @@ absolute-limits,limits show --absolute,Lists absolute limits for a user. availability-zone-list,availability zone list --volume,Lists all availability zones. backup-create,volume backup create,Creates a volume backup. backup-delete,volume backup delete,Removes a backup. -backup-export,volume backup export,Export backup metadata record. -backup-import,volume backup import,Import backup metadata record. +backup-export,volume backup record export,Export backup metadata record. +backup-import,volume backup record import,Import backup metadata record. backup-list,volume backup list,Lists all backups. backup-reset-state,volume backup set --state,Explicitly updates the backup state. backup-restore,volume backup restore,Restores a backup. diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 59b08d0b90..c245cbf66b 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -596,6 +596,35 @@ def get_backups(backups=None, count=2): 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', + } + + @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'}, + ], + }, + } + class FakeConsistencyGroup(object): """Fake one or more consistency group.""" diff --git a/openstackclient/tests/unit/volume/v2/test_backup_record.py b/openstackclient/tests/unit/volume/v2/test_backup_record.py new file mode 100644 index 0000000000..0e24174c5e --- /dev/null +++ b/openstackclient/tests/unit/volume/v2/test_backup_record.py @@ -0,0 +1,114 @@ +# +# 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.tests.unit.volume.v2 import fakes as volume_fakes +from openstackclient.volume.v2 import backup_record + + +class TestBackupRecord(volume_fakes.TestVolume): + + def setUp(self): + super(TestBackupRecord, self).setUp() + + self.backups_mock = self.app.client_manager.volume.backups + self.backups_mock.reset_mock() + + +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() + + def setUp(self): + super(TestBackupRecordExport, self).setUp() + + self.backups_mock.export_record.return_value = self.new_record + self.backups_mock.get.return_value = self.new_backup + + # Get the command object to mock + self.cmd = backup_record.ExportBackupRecord(self.app, None) + + def test_backup_export_table(self): + arglist = [ + self.new_backup.name, + ] + verifylist = [ + ("backup", self.new_backup.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args.formatter = 'table' + columns, __ = self.cmd.take_action(parsed_args) + + self.backups_mock.export_record.assert_called_with( + self.new_backup.id, + ) + + expected_columns = ('Backup Service', 'Metadata') + self.assertEqual(columns, expected_columns) + + def test_backup_export_json(self): + arglist = [ + self.new_backup.name, + ] + verifylist = [ + ("backup", self.new_backup.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args.formatter = 'json' + columns, __ = self.cmd.take_action(parsed_args) + + self.backups_mock.export_record.assert_called_with( + self.new_backup.id, + ) + + expected_columns = ('backup_service', 'backup_url') + self.assertEqual(columns, expected_columns) + + +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() + + def setUp(self): + super(TestBackupRecordImport, self).setUp() + + self.backups_mock.import_record.return_value = self.new_import + + # Get the command object to mock + self.cmd = backup_record.ImportBackupRecord(self.app, None) + + def test_backup_import(self): + arglist = [ + "cinder.backup.drivers.swift.SwiftBackupDriver", + "fake_backup_record_data", + ] + verifylist = [ + ("backup_service", + "cinder.backup.drivers.swift.SwiftBackupDriver"), + ("backup_metadata", "fake_backup_record_data"), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, __ = self.cmd.take_action(parsed_args) + + self.backups_mock.import_record.assert_called_with( + "cinder.backup.drivers.swift.SwiftBackupDriver", + "fake_backup_record_data", + ) + self.assertEqual(columns, ('backup',)) diff --git a/openstackclient/volume/v2/backup_record.py b/openstackclient/volume/v2/backup_record.py new file mode 100644 index 0000000000..f491803272 --- /dev/null +++ b/openstackclient/volume/v2/backup_record.py @@ -0,0 +1,82 @@ +# +# 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. +# + +"""Volume v2 Backup action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class ExportBackupRecord(command.ShowOne): + _description = _('Export volume backup details. Backup information can be ' + 'imported into a new service instance to be able to ' + 'restore.') + + def get_parser(self, prog_name): + parser = super(ExportBackupRecord, self).get_parser(prog_name) + parser.add_argument( + "backup", + metavar="", + help=_("Backup to export (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + backup = utils.find_resource(volume_client.backups, parsed_args.backup) + backup_data = volume_client.backups.export_record(backup.id) + + # We only want to show "friendly" display names, but also want to keep + # json structure compatibility with cinderclient + if parsed_args.formatter == 'table': + backup_data['Backup Service'] = backup_data.pop('backup_service') + backup_data['Metadata'] = backup_data.pop('backup_url') + + return zip(*sorted(six.iteritems(backup_data))) + + +class ImportBackupRecord(command.ShowOne): + _description = _('Import volume backup details. Exported backup details ' + 'contain the metadata necessary to restore to a new or ' + 'rebuilt service instance') + + def get_parser(self, prog_name): + parser = super(ImportBackupRecord, self).get_parser(prog_name) + parser.add_argument( + "backup_service", + metavar="", + help=_("Backup service containing the backup.") + ) + parser.add_argument( + "backup_metadata", + metavar="", + help=_("Encoded backup metadata from export.") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + backup_data = volume_client.backups.import_record( + parsed_args.backup_service, + parsed_args.backup_metadata) + backup_data.pop('links', None) + return zip(*sorted(six.iteritems(backup_data))) diff --git a/releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml b/releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml new file mode 100644 index 0000000000..955e99714b --- /dev/null +++ b/releasenotes/notes/volume-backup-record-9f5987c45e294dc6.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Add ``openstack volume backup record export `` command that + provides encrypted backup record metadata that can then be used to import + and restore on a different or rebuilt block storage service. Information + about volume backup export and import can be found in the `Cinder block + storage service documentation `_. + - | + Add ``openstack volume backup record import + `` command that can be used to import and restore on a + different or rebuilt block storage service. Information about volume backup + export and import can be found in the `Cinder block storage service + documentation `_. + diff --git a/setup.cfg b/setup.cfg index 73c5fde933..818b6ef25e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -629,6 +629,9 @@ openstack.volume.v2 = volume_backup_set = openstackclient.volume.v2.backup:SetVolumeBackup volume_backup_show = openstackclient.volume.v2.backup:ShowVolumeBackup + volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord + volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord + volume_backend_capability_show = openstackclient.volume.v2.volume_backend:ShowCapability volume_backend_pool_list = openstackclient.volume.v2.volume_backend:ListPool From 4f66f66b6c80206e3d432f74c3763ffe2e5c2949 Mon Sep 17 00:00:00 2001 From: liuyamin Date: Tue, 13 Nov 2018 17:04:59 +0800 Subject: [PATCH 0027/1120] Fix i18n issue This patch fix some i18n issues in the files vapi/compute_v2.py. Change-Id: Ic4da472ca585a35ce64512cf0e72e2fe9d4c9d6e --- openstackclient/api/compute_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/api/compute_v2.py b/openstackclient/api/compute_v2.py index 0c89e91266..7dc4e4468a 100644 --- a/openstackclient/api/compute_v2.py +++ b/openstackclient/api/compute_v2.py @@ -52,7 +52,7 @@ def _check_integer(self, value, msg=None): value = int(value) except (TypeError, ValueError): if not msg: - msg = "%s is not an integer" % value + msg = _("%s is not an integer") % value raise InvalidValue(msg) return value From 5bec3b7e3b7d4fed1bedd5a848294f550d30ed47 Mon Sep 17 00:00:00 2001 From: liuyamin Date: Wed, 14 Nov 2018 11:19:25 +0800 Subject: [PATCH 0028/1120] Replace assertEqual(True/False, expr) with assertTrue/assertFalse In some cases, If the result of expr is a boolen value, we shoud use assertTrue/assertFalse to instead. Because it is clear and simple. Change-Id: I53b345fc3915a7b0e737e9dd4d58fe09c746d61c --- .../functional/compute/v2/test_flavor.py | 6 +- .../functional/identity/v2/test_project.py | 2 +- .../network/v2/test_address_scope.py | 25 ++----- .../functional/network/v2/test_network.py | 71 +++++-------------- .../network/v2/test_network_flavor.py | 10 +-- .../network/v2/test_network_flavor_profile.py | 30 ++------ .../network/v2/test_network_meter.py | 28 ++------ .../functional/network/v2/test_router.py | 5 +- 8 files changed, 41 insertions(+), 136 deletions(-) diff --git a/openstackclient/tests/functional/compute/v2/test_flavor.py b/openstackclient/tests/functional/compute/v2/test_flavor.py index eefd3fabd0..c274adf2e7 100644 --- a/openstackclient/tests/functional/compute/v2/test_flavor.py +++ b/openstackclient/tests/functional/compute/v2/test_flavor.py @@ -112,8 +112,7 @@ def test_flavor_list(self): 0, cmd_output["disk"], ) - self.assertEqual( - False, + self.assertFalse( cmd_output["os-flavor-access:is_public"], ) self.assertEqual( @@ -199,8 +198,7 @@ def test_flavor_properties(self): 20, cmd_output["disk"], ) - self.assertEqual( - False, + self.assertFalse( cmd_output["os-flavor-access:is_public"], ) self.assertEqual( diff --git a/openstackclient/tests/functional/identity/v2/test_project.py b/openstackclient/tests/functional/identity/v2/test_project.py index b6222a1bbf..38777c36c7 100644 --- a/openstackclient/tests/functional/identity/v2/test_project.py +++ b/openstackclient/tests/functional/identity/v2/test_project.py @@ -72,7 +72,7 @@ def test_project_set(self): self.assert_show_fields(items, fields) project = self.parse_show_as_object(raw_output) self.assertEqual(new_project_name, project['name']) - self.assertEqual('False', project['enabled']) + self.assertFalse(project['enabled']) self.assertEqual("k0='v0'", project['properties']) def test_project_show(self): diff --git a/openstackclient/tests/functional/network/v2/test_address_scope.py b/openstackclient/tests/functional/network/v2/test_address_scope.py index ebd2ba86c6..8a99ec5e61 100644 --- a/openstackclient/tests/functional/network/v2/test_address_scope.py +++ b/openstackclient/tests/functional/network/v2/test_address_scope.py @@ -42,10 +42,7 @@ def test_address_scope_delete(self): cmd_output['name'], ) # Check the default values - self.assertEqual( - False, - cmd_output['shared'], - ) + self.assertFalse(cmd_output['shared']) name2 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( @@ -80,10 +77,7 @@ def test_address_scope_list(self): 4, cmd_output['ip_version'], ) - self.assertEqual( - True, - cmd_output['shared'], - ) + self.assertTrue(cmd_output['shared']) name2 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( @@ -101,10 +95,7 @@ def test_address_scope_list(self): 6, cmd_output['ip_version'], ) - self.assertEqual( - False, - cmd_output['shared'], - ) + self.assertFalse(cmd_output['shared']) # Test list cmd_output = json.loads(self.openstack( @@ -149,10 +140,7 @@ def test_address_scope_set(self): 4, cmd_output['ip_version'], ) - self.assertEqual( - False, - cmd_output['shared'], - ) + self.assertFalse(cmd_output['shared']) raw_output = self.openstack( 'address scope set ' + @@ -174,7 +162,4 @@ def test_address_scope_set(self): 4, cmd_output['ip_version'], ) - self.assertEqual( - True, - cmd_output['shared'], - ) + self.assertTrue(cmd_output['shared']) diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py index 9cef135fe7..1a74496968 100644 --- a/openstackclient/tests/functional/network/v2/test_network.py +++ b/openstackclient/tests/functional/network/v2/test_network.py @@ -70,8 +70,7 @@ def test_network_create_compute(self): '1.2.4.0/28', cmd_output["cidr"], ) - self.assertEqual( - True, + self.assertTrue( cmd_output["share_address"], ) @@ -124,8 +123,7 @@ def test_network_create_network(self): 'UP', cmd_output["admin_state_up"], ) - self.assertEqual( - False, + self.assertFalse( cmd_output["shared"], ) self.assertEqual( @@ -236,21 +234,14 @@ def test_network_list(self): 'UP', cmd_output["admin_state_up"], ) - self.assertEqual( - False, - cmd_output["shared"], - ) + self.assertFalse(cmd_output["shared"]) self.assertEqual( 'Internal', cmd_output["router:external"], ) - self.assertEqual( - False, - cmd_output["is_default"], - ) - self.assertEqual( - True, - cmd_output["port_security_enabled"], + self.assertFalse(cmd_output["is_default"]) + self.assertTrue( + cmd_output["port_security_enabled"] ) else: self.assertEqual( @@ -278,27 +269,15 @@ def test_network_list(self): 'DOWN', cmd_output["admin_state_up"], ) - self.assertEqual( - True, - cmd_output["shared"], - ) - self.assertEqual( - False, - cmd_output["is_default"], - ) - self.assertEqual( - True, - cmd_output["port_security_enabled"], - ) + self.assertTrue(cmd_output["shared"]) + self.assertFalse(cmd_output["is_default"]) + self.assertTrue(cmd_output["port_security_enabled"]) else: self.assertEqual( '4.5.6.0/28', cmd_output["cidr"], ) - self.assertEqual( - True, - cmd_output["share_address"], - ) + self.assertTrue(cmd_output["share_address"]) # Test list cmd_output = json.loads(self.openstack( @@ -422,22 +401,15 @@ def test_network_set(self): 'UP', cmd_output["admin_state_up"], ) - self.assertEqual( - False, - cmd_output["shared"], - ) + self.assertFalse(cmd_output["shared"]) self.assertEqual( 'Internal', cmd_output["router:external"], ) - self.assertEqual( - False, - cmd_output["is_default"], - ) - self.assertEqual( - True, - cmd_output["port_security_enabled"], + self.assertFalse(cmd_output["is_default"]) + self.assertTrue( + cmd_output["port_security_enabled"] ) raw_output = self.openstack( @@ -463,19 +435,12 @@ def test_network_set(self): 'DOWN', cmd_output["admin_state_up"], ) - self.assertEqual( - True, - cmd_output["shared"], - ) + self.assertTrue(cmd_output["shared"]) self.assertEqual( 'External', cmd_output["router:external"], ) - self.assertEqual( - False, - cmd_output["is_default"], - ) - self.assertEqual( - False, - cmd_output["port_security_enabled"], + self.assertFalse(cmd_output["is_default"]) + self.assertFalse( + cmd_output["port_security_enabled"] ) diff --git a/openstackclient/tests/functional/network/v2/test_network_flavor.py b/openstackclient/tests/functional/network/v2/test_network_flavor.py index ba3de2cdb5..cf68a096a2 100644 --- a/openstackclient/tests/functional/network/v2/test_network_flavor.py +++ b/openstackclient/tests/functional/network/v2/test_network_flavor.py @@ -87,10 +87,7 @@ def test_network_flavor_delete(self): name1, cmd_output['name'], ) - self.assertEqual( - True, - cmd_output['enabled'], - ) + self.assertTrue(cmd_output['enabled']) self.assertEqual( 'testdescription', cmd_output['description'], @@ -105,10 +102,7 @@ def test_network_flavor_delete(self): name2, cmd_output['name'], ) - self.assertEqual( - False, - cmd_output['enabled'], - ) + self.assertFalse(cmd_output['enabled']) self.assertEqual( 'testdescription1', cmd_output['description'], diff --git a/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py b/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py index 2207c84749..5b5ec926de 100644 --- a/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py +++ b/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py @@ -35,10 +35,7 @@ def test_network_flavor_profile_create(self): )) ID = json_output.get('id') self.assertIsNotNone(ID) - self.assertEqual( - True, - json_output.get('enabled'), - ) + self.assertTrue(json_output.get('enabled')) self.assertEqual( 'fakedescription', json_output.get('description'), @@ -61,10 +58,7 @@ def test_network_flavor_profile_list(self): )) ID1 = json_output.get('id') self.assertIsNotNone(ID1) - self.assertEqual( - True, - json_output.get('enabled'), - ) + self.assertTrue(json_output.get('enabled')) self.assertEqual( 'fakedescription', json_output.get('description'), @@ -82,10 +76,7 @@ def test_network_flavor_profile_list(self): )) ID2 = json_output.get('id') self.assertIsNotNone(ID2) - self.assertEqual( - False, - json_output.get('enabled'), - ) + self.assertFalse(json_output.get('enabled')) self.assertEqual( 'fakedescription', json_output.get('description'), @@ -120,10 +111,7 @@ def test_network_flavor_profile_set(self): )) ID = json_output_1.get('id') self.assertIsNotNone(ID) - self.assertEqual( - True, - json_output_1.get('enabled'), - ) + self.assertTrue(json_output_1.get('enabled')) self.assertEqual( 'fakedescription', json_output_1.get('description'), @@ -138,10 +126,7 @@ def test_network_flavor_profile_set(self): json_output = json.loads(self.openstack( 'network flavor profile show -f json ' + ID )) - self.assertEqual( - False, - json_output.get('enabled'), - ) + self.assertFalse(json_output.get('enabled')) self.assertEqual( 'fakedescription', json_output.get('description'), @@ -171,10 +156,7 @@ def test_network_flavor_profile_show(self): ID, json_output["id"], ) - self.assertEqual( - True, - json_output["enabled"], - ) + self.assertTrue(json_output["enabled"]) self.assertEqual( 'fakedescription', json_output["description"], diff --git a/openstackclient/tests/functional/network/v2/test_network_meter.py b/openstackclient/tests/functional/network/v2/test_network_meter.py index 7f6da28d3c..0a8b89cae0 100644 --- a/openstackclient/tests/functional/network/v2/test_network_meter.py +++ b/openstackclient/tests/functional/network/v2/test_network_meter.py @@ -48,10 +48,7 @@ def test_meter_delete(self): json_output.get('name'), ) # Check if default shared values - self.assertEqual( - False, - json_output.get('shared'), - ) + self.assertFalse(json_output.get('shared')) self.assertEqual( 'fakedescription', json_output.get('description'), @@ -67,10 +64,7 @@ def test_meter_delete(self): json_output_2.get('name'), ) # Check if default shared values - self.assertEqual( - False, - json_output_2.get('shared'), - ) + self.assertFalse(json_output_2.get('shared')) self.assertEqual( 'fakedescription', json_output_2.get('description'), @@ -99,10 +93,7 @@ def test_meter_list(self): 'Test1', json_output.get('description'), ) - self.assertEqual( - True, - json_output.get('shared'), - ) + self.assertTrue(json_output.get('shared')) name2 = uuid.uuid4().hex json_output_2 = json.loads(self.openstack( @@ -117,8 +108,7 @@ def test_meter_list(self): 'Test2', json_output_2.get('description'), ) - self.assertEqual( - False, + self.assertFalse( json_output_2.get('shared'), ) @@ -143,10 +133,7 @@ def test_meter_show(self): json_output = json.loads(self.openstack( 'network meter show -f json ' + meter_id )) - self.assertEqual( - False, - json_output.get('shared'), - ) + self.assertFalse(json_output.get('shared')) self.assertEqual( 'fakedescription', json_output.get('description'), @@ -164,10 +151,7 @@ def test_meter_show(self): meter_id, json_output.get('id'), ) - self.assertEqual( - False, - json_output.get('shared'), - ) + self.assertFalse(json_output.get('shared')) self.assertEqual( 'fakedescription', json_output.get('description'), diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 95c5a96f8b..9d5beff059 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -247,10 +247,7 @@ def test_router_set_show_unset(self): 'router show -f json ' + new_name )) - self.assertEqual( - True, - cmd_output["distributed"], - ) + self.assertTrue(cmd_output["distributed"]) self.assertIsNotNone(cmd_output["external_gateway_info"]) # Test unset From 8120cb8b55091d9027c7b8ff519b82a87e7d6b17 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 6 Dec 2017 09:33:23 -0600 Subject: [PATCH 0029/1120] Use devstack functional base job We extracted some of our functional base job to the devstack repo. Consume it. Change-Id: I11e6f9dab935e4b2cd16228f031f7e0adb3a6c89 Depends-On: I84de60181cb88574e341ff83cd4857cce241f2dd --- .zuul.yaml | 12 +----------- playbooks/osc-devstack/post.yaml | 4 ---- playbooks/osc-devstack/pre.yaml | 8 -------- playbooks/osc-devstack/run.yaml | 5 ----- 4 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 playbooks/osc-devstack/post.yaml delete mode 100644 playbooks/osc-devstack/pre.yaml delete mode 100644 playbooks/osc-devstack/run.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 7cf30cf138..d646a0f766 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -62,16 +62,9 @@ - job: name: osc-functional-devstack-base - parent: devstack + parent: devstack-tox-functional description: | Base job for devstack-based functional tests - pre-run: playbooks/osc-devstack/pre.yaml - run: playbooks/osc-devstack/run.yaml - post-run: playbooks/osc-devstack/post.yaml - required-projects: - - name: openstack/swift - roles: - - zuul: openstack-infra/devstack timeout: 9000 irrelevant-files: - ^.*\.rst$ @@ -79,7 +72,6 @@ - ^releasenotes/.*$ vars: devstack_localrc: - SWIFT_HASH: '1234123412341234' LIBS_FROM_GIT: python-openstackclient # NOTE(dtroyer): OSC needs to support Image v1 for a while yet so re-enable GLANCE_V1_ENABLED: true @@ -99,7 +91,6 @@ ceilometer-anotification: false ceilometer-api: false ceilometer-collector: false - horizon: false s-account: true s-container: true s-object: true @@ -107,7 +98,6 @@ osc_environment: PYTHONUNBUFFERED: 'true' OS_CLOUD: devstack-admin - tox_install_siblings: false zuul_work_dir: src/git.openstack.org/openstack/python-openstackclient # The Neutron bits are here rather than in osc-functional-devstack-base to diff --git a/playbooks/osc-devstack/post.yaml b/playbooks/osc-devstack/post.yaml deleted file mode 100644 index 7f0cb19824..0000000000 --- a/playbooks/osc-devstack/post.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- hosts: all - roles: - - fetch-tox-output - - fetch-subunit-output diff --git a/playbooks/osc-devstack/pre.yaml b/playbooks/osc-devstack/pre.yaml deleted file mode 100644 index 3ec41c9cbd..0000000000 --- a/playbooks/osc-devstack/pre.yaml +++ /dev/null @@ -1,8 +0,0 @@ -- hosts: all - roles: - - run-devstack - - role: bindep - bindep_profile: test - bindep_dir: "{{ zuul_work_dir }}" - - test-setup - - ensure-tox diff --git a/playbooks/osc-devstack/run.yaml b/playbooks/osc-devstack/run.yaml deleted file mode 100644 index d1101dd4a1..0000000000 --- a/playbooks/osc-devstack/run.yaml +++ /dev/null @@ -1,5 +0,0 @@ -- hosts: all - environment: - OS_CLOUD: devstack-admin - roles: - - tox From 029c148b2979eecc39bee36d79f523f4777192d8 Mon Sep 17 00:00:00 2001 From: qingszhao Date: Fri, 30 Nov 2018 07:13:39 +0000 Subject: [PATCH 0030/1120] Add Python 3.6 classifier to setup.cfg Change-Id: Id723040f895f06527d46755dd408dec16200c001 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 818b6ef25e..05ff1268e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 [files] packages = From 8be53a50e5278281640b5023c4d8c4b28da22cb9 Mon Sep 17 00:00:00 2001 From: sunjia Date: Mon, 3 Dec 2018 22:11:36 -0500 Subject: [PATCH 0031/1120] Change openstack-dev to openstack-discuss Mailinglists have been updated. Openstack-discuss replaces openstack-dev. Change-Id: I7c7fdfb71348f77aa6063ec99b3c4a90bde21d06 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 05ff1268e4..48c2247ff7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ summary = OpenStack Command-line Client description-file = README.rst author = OpenStack -author-email = openstack-dev@lists.openstack.org +author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-openstackclient/latest/ classifier = Environment :: OpenStack From c82f4237e59dd61f180b2bfbd3383c6540cd6cef Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Fri, 27 Jul 2018 16:41:45 -0400 Subject: [PATCH 0032/1120] Support enable/disable uplink status propagation Add options to enable/disable uplink status propagation on creating a neutron port. Related patches: * neutron: https://review.openstack.org/#/c/571899/ * openstacksdk: https://review.openstack.org/#/c/586687/ Depends-On: https://review.openstack.org/#/c/586687/ Change-Id: I095a98fc5f5aee62d979a16b3cd79d91ec3b9ddb Related-Bug: #1722720 --- doc/source/cli/command-objects/port.rst | 9 +++++ openstackclient/network/v2/port.py | 18 +++++++++ .../tests/unit/network/v2/fakes.py | 3 ++ .../tests/unit/network/v2/test_port.py | 39 +++++++++++++++++++ ...k_status_propagation-4d37452bcf03e0f8.yaml | 5 +++ 5 files changed, 74 insertions(+) create mode 100644 releasenotes/notes/support-uplink_status_propagation-4d37452bcf03e0f8.yaml diff --git a/doc/source/cli/command-objects/port.rst b/doc/source/cli/command-objects/port.rst index bb037fa3e5..bc9f5dde02 100644 --- a/doc/source/cli/command-objects/port.rst +++ b/doc/source/cli/command-objects/port.rst @@ -26,6 +26,7 @@ Create new port [--binding-profile ] [--host ] [--enable | --disable] + [--enable-uplink-status-propagation | --disable-uplink-status-propagation] [--mac-address ] [--security-group | --no-security-group] [--dns-domain ] @@ -87,6 +88,14 @@ Create new port Disable port +.. option:: --enable-uplink-status-propagation + + Enable uplink status propagate + +.. option:: --disable-uplink-status-propagation + + Disable uplink status propagate (default) + .. option:: --mac-address MAC address of this port diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 0d276d9da4..1001e6cf41 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -163,6 +163,13 @@ def _get_attrs(client_manager, parsed_args): attrs['qos_policy_id'] = client_manager.network.find_qos_policy( parsed_args.qos_policy, ignore_missing=False).id + if ('enable_uplink_status_propagation' in parsed_args and + parsed_args.enable_uplink_status_propagation): + attrs['propagate_uplink_status'] = True + if ('disable_uplink_status_propagation' in parsed_args and + parsed_args.disable_uplink_status_propagation): + attrs['propagate_uplink_status'] = False + return attrs @@ -349,6 +356,17 @@ def get_parser(self, prog_name): action='store_true', help=_("Disable port") ) + uplink_status_group = parser.add_mutually_exclusive_group() + uplink_status_group.add_argument( + '--enable-uplink-status-propagation', + action='store_true', + help=_("Enable uplink status propagate") + ) + uplink_status_group.add_argument( + '--disable-uplink-status-propagation', + action='store_true', + help=_("Disable uplink status propagate (default)") + ) parser.add_argument( '--project', metavar='', diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index aec7402f94..28e92d1196 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -581,6 +581,7 @@ def create_one_port(attrs=None): 'tenant_id': 'project-id-' + uuid.uuid4().hex, 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'tags': [], + 'uplink_status_propagation': False, } # Overwrite default attributes. @@ -600,6 +601,8 @@ def create_one_port(attrs=None): port.project_id = port_attrs['tenant_id'] port.security_group_ids = port_attrs['security_group_ids'] port.qos_policy_id = port_attrs['qos_policy_id'] + port.uplink_status_propagation = port_attrs[ + 'uplink_status_propagation'] return port diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 78d7fd6ca1..8ac3e54f4a 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -64,6 +64,7 @@ def _get_common_cols_data(fake_port): 'security_group_ids', 'status', 'tags', + 'uplink_status_propagation', ) data = ( @@ -93,6 +94,7 @@ def _get_common_cols_data(fake_port): utils.format_list(fake_port.security_group_ids), fake_port.status, utils.format_list(fake_port.tags), + fake_port.uplink_status_propagation, ) return columns, data @@ -571,6 +573,43 @@ def test_create_with_tags(self): def test_create_with_no_tag(self): self._test_create_with_tag(add_tags=False) + def _test_create_with_uplink_status_propagation(self, enable=True): + arglist = [ + '--network', self._port.network_id, + 'test-port', + ] + if enable: + arglist += ['--enable-uplink-status-propagation'] + else: + arglist += ['--disable-uplink-status-propagation'] + verifylist = [ + ('network', self._port.network_id,), + ('name', 'test-port'), + ] + if enable: + verifylist.append(('enable_uplink_status_propagation', True)) + else: + verifylist.append(('disable_uplink_status_propagation', True)) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'propagate_uplink_status': enable, + 'name': 'test-port', + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_with_uplink_status_propagation_enabled(self): + self._test_create_with_uplink_status_propagation(enable=True) + + def test_create_with_uplink_status_propagation_disabled(self): + self._test_create_with_uplink_status_propagation(enable=False) + class TestDeletePort(TestPort): diff --git a/releasenotes/notes/support-uplink_status_propagation-4d37452bcf03e0f8.yaml b/releasenotes/notes/support-uplink_status_propagation-4d37452bcf03e0f8.yaml new file mode 100644 index 0000000000..766e8abbf8 --- /dev/null +++ b/releasenotes/notes/support-uplink_status_propagation-4d37452bcf03e0f8.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--enable-uplink-status-propagation`` option and + ``--disable-uplink-status-propagation`` option to ``port create`` command. From fd23025227a07e879e0b29c67063401b1d392518 Mon Sep 17 00:00:00 2001 From: LIU Yulong Date: Wed, 18 Jul 2018 15:28:51 +0800 Subject: [PATCH 0033/1120] Supports router gateway IP QoS Adds --qos-policy and --no-qos-policy to `openstack router set`: --qos-policy Attach QoS policy to router gateway IPs --no-qos-policy Remove QoS policy from router gateway IPs Adds --qos-policy to `openstack router unset`: --qos-policy Remove QoS policy from router gateway IPs Partially-Implements blueprint: router-gateway-ip-qos Closes-Bug: #1757044 Change-Id: Ifec3b2cf9bdb59513c8bcd7bd60305506a071192 --- openstackclient/network/v2/router.py | 53 ++++++ .../tests/unit/network/v2/test_router.py | 156 +++++++++++++++++- ...outer-gateway-IP-QoS-c8ba95e180bca05f.yaml | 9 + 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/router-gateway-IP-QoS-c8ba95e180bca05f.yaml diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 746452e40c..a3a4eb756a 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -579,6 +579,17 @@ def get_parser(self, prog_name): action='store_true', help=_("Disable Source NAT on external gateway") ) + qos_policy_group = parser.add_mutually_exclusive_group() + qos_policy_group.add_argument( + '--qos-policy', + metavar='', + help=_("Attach QoS policy to router gateway IPs") + ) + qos_policy_group.add_argument( + '--no-qos-policy', + action='store_true', + help=_("Remove QoS policy from router gateway IPs") + ) _tag.add_tag_option_to_parser_for_set(parser, _('router')) return parser @@ -638,6 +649,27 @@ def take_action(self, parsed_args): ips.append(ip_spec) gateway_info['external_fixed_ips'] = ips attrs['external_gateway_info'] = gateway_info + + if ((parsed_args.qos_policy or parsed_args.no_qos_policy) and + not parsed_args.external_gateway): + try: + original_net_id = obj.external_gateway_info['network_id'] + except (KeyError, TypeError): + msg = (_("You must specify '--external-gateway' or the router " + "must already have an external network in order to " + "set router gateway IP QoS")) + raise exceptions.CommandError(msg) + else: + if not attrs.get('external_gateway_info'): + attrs['external_gateway_info'] = {} + attrs['external_gateway_info']['network_id'] = original_net_id + if parsed_args.qos_policy: + check_qos_id = client.find_qos_policy( + parsed_args.qos_policy, ignore_missing=False).id + attrs['external_gateway_info']['qos_policy_id'] = check_qos_id + + if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: + attrs['external_gateway_info']['qos_policy_id'] = None if attrs: client.update_router(obj, **attrs) # tags is a subresource and it needs to be updated separately. @@ -701,6 +733,12 @@ def get_parser(self, prog_name): action='store_true', default=False, help=_("Remove external gateway information from the router")) + parser.add_argument( + '--qos-policy', + action='store_true', + default=False, + help=_("Remove QoS policy from router gateway IPs") + ) parser.add_argument( 'router', metavar="", @@ -713,6 +751,7 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) tmp_routes = copy.deepcopy(obj.routes) + tmp_external_gateway_info = copy.deepcopy(obj.external_gateway_info) attrs = {} if parsed_args.routes: try: @@ -723,6 +762,20 @@ def take_action(self, parsed_args): msg = (_("Router does not contain route %s") % route) raise exceptions.CommandError(msg) attrs['routes'] = tmp_routes + if parsed_args.qos_policy: + try: + if (tmp_external_gateway_info['network_id'] and + tmp_external_gateway_info['qos_policy_id']): + pass + except (KeyError, TypeError): + msg = _("Router does not have external network or qos policy") + raise exceptions.CommandError(msg) + else: + attrs['external_gateway_info'] = { + 'network_id': tmp_external_gateway_info['network_id'], + 'qos_policy_id': None + } + if parsed_args.external_gateway: attrs['external_gateway_info'] = {} if attrs: diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index f383c1ddb7..5102d09102 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -1113,6 +1113,102 @@ def test_set_with_tags(self): def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) + def test_set_gateway_ip_qos(self): + qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + self.network.find_qos_policy = mock.Mock(return_value=qos_policy) + arglist = [ + "--external-gateway", self._network.id, + "--qos-policy", qos_policy.id, + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ('external_gateway', self._network.id), + ('qos_policy', qos_policy.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.update_router.assert_called_with( + self._router, **{'external_gateway_info': { + 'network_id': self._network.id, + 'qos_policy_id': qos_policy.id, }}) + self.assertIsNone(result) + + def test_unset_gateway_ip_qos(self): + arglist = [ + "--external-gateway", self._network.id, + "--no-qos-policy", + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ('external_gateway', self._network.id), + ('no_qos_policy', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.update_router.assert_called_with( + self._router, **{'external_gateway_info': { + 'network_id': self._network.id, + 'qos_policy_id': None, }}) + self.assertIsNone(result) + + def test_set_unset_gateway_ip_qos(self): + qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + self.network.find_qos_policy = mock.Mock(return_value=qos_policy) + arglist = [ + "--external-gateway", self._network.id, + "--qos-policy", qos_policy.id, + "--no-qos-policy", + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ('external_gateway', self._network.id), + ('qos_policy', qos_policy.id), + ('no_qos_policy', True), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_set_gateway_ip_qos_no_gateway(self): + qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + self.network.find_qos_policy = mock.Mock(return_value=qos_policy) + router = network_fakes.FakeRouter.create_one_router() + self.network.find_router = mock.Mock(return_value=router) + arglist = [ + "--qos-policy", qos_policy.id, + router.id, + ] + verifylist = [ + ('router', router.id), + ('qos_policy', qos_policy.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + + def test_unset_gateway_ip_qos_no_gateway(self): + qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + self.network.find_qos_policy = mock.Mock(return_value=qos_policy) + router = network_fakes.FakeRouter.create_one_router() + self.network.find_router = mock.Mock(return_value=router) + arglist = [ + "--no-qos-policy", + router.id, + ] + verifylist = [ + ('router', router.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + class TestShowRouter(TestRouter): @@ -1201,12 +1297,19 @@ class TestUnsetRouter(TestRouter): def setUp(self): super(TestUnsetRouter, self).setUp() + self.fake_network = network_fakes.FakeNetwork.create_one_network() + self.fake_qos_policy = ( + network_fakes.FakeNetworkQosPolicy.create_one_qos_policy()) self._testrouter = network_fakes.FakeRouter.create_one_router( {'routes': [{"destination": "192.168.101.1/24", "nexthop": "172.24.4.3"}, {"destination": "192.168.101.2/24", "nexthop": "172.24.4.3"}], - 'tags': ['green', 'red'], }) + 'tags': ['green', 'red'], + 'external_gateway_info': { + 'network_id': self.fake_network.id, + 'qos_policy_id': self.fake_qos_policy.id + }}) self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() self.network.find_router = mock.Mock(return_value=self._testrouter) self.network.update_router = mock.Mock(return_value=None) @@ -1289,3 +1392,54 @@ def test_unset_with_tags(self): def test_unset_with_all_tag(self): self._test_unset_tags(with_tags=False) + + def test_unset_router_qos_policy(self): + arglist = [ + '--qos-policy', + self._testrouter.name, + ] + verifylist = [ + ('qos_policy', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = {'external_gateway_info': {"network_id": self.fake_network.id, + "qos_policy_id": None}} + self.network.update_router.assert_called_once_with( + self._testrouter, **attrs) + self.assertIsNone(result) + + def test_unset_gateway_ip_qos_no_network(self): + qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + self.network.find_qos_policy = mock.Mock(return_value=qos_policy) + router = network_fakes.FakeRouter.create_one_router() + self.network.find_router = mock.Mock(return_value=router) + arglist = [ + "--qos-policy", + router.id, + ] + verifylist = [ + ('router', router.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + + def test_unset_gateway_ip_qos_no_qos(self): + qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + self.network.find_qos_policy = mock.Mock(return_value=qos_policy) + router = network_fakes.FakeRouter.create_one_router( + {"external_gateway_info": {"network_id": "fake-id"}}) + self.network.find_router = mock.Mock(return_value=router) + arglist = [ + "--qos-policy", + router.id, + ] + verifylist = [ + ('router', router.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) diff --git a/releasenotes/notes/router-gateway-IP-QoS-c8ba95e180bca05f.yaml b/releasenotes/notes/router-gateway-IP-QoS-c8ba95e180bca05f.yaml new file mode 100644 index 0000000000..efe9a37624 --- /dev/null +++ b/releasenotes/notes/router-gateway-IP-QoS-c8ba95e180bca05f.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add support for attaching and removing qos policy to router gateway IPs. + + Add ``--qos-policy`` and ``--no-qos-policy`` options to the + ``router set`` command. + + Add ``--qos-policy`` option to the ``router unset`` command. From f9df3ce3cd0c98f3cd1e875b9b0c97161a15160d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Dec 2018 14:58:02 -0600 Subject: [PATCH 0034/1120] More volume functional test fixes Remove the use of class setup/teardown from volume transfer functional tests as that just doesn't work too well here. Also wait for volume status before attempting transfer request operations, some test nodes take a while to create the volumes. Change-Id: Ib9378ab5c973deb2aa86c9b9ed31408f3a05115a Signed-off-by: Dean Troyer --- .../volume/v2/test_transfer_request.py | 91 ++++++++++--------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/openstackclient/tests/functional/volume/v2/test_transfer_request.py b/openstackclient/tests/functional/volume/v2/test_transfer_request.py index 33d8ce77f7..d6aff73c48 100644 --- a/openstackclient/tests/functional/volume/v2/test_transfer_request.py +++ b/openstackclient/tests/functional/volume/v2/test_transfer_request.py @@ -19,75 +19,80 @@ class TransferRequestTests(common.BaseVolumeTests): """Functional tests for transfer request. """ - NAME = uuid.uuid4().hex - VOLUME_NAME = uuid.uuid4().hex API_VERSION = '2' - @classmethod - def setUpClass(cls): - super(TransferRequestTests, cls).setUpClass() - - cmd_output = json.loads(cls.openstack( - '--os-volume-api-version ' + cls.API_VERSION + ' ' + - 'volume create -f json --size 1 ' + cls.VOLUME_NAME)) - cls.assertOutput(cls.VOLUME_NAME, cmd_output['name']) - - cls.wait_for_status("volume", cls.VOLUME_NAME, "available") - - @classmethod - def tearDownClass(cls): - try: - raw_output_volume = cls.openstack( - 'volume delete ' + cls.VOLUME_NAME) - cls.assertOutput('', raw_output_volume) - finally: - super(TransferRequestTests, cls).tearDownClass() - def test_volume_transfer_request_accept(self): volume_name = uuid.uuid4().hex - name = uuid.uuid4().hex + xfer_name = uuid.uuid4().hex # create a volume cmd_output = json.loads(self.openstack( - 'volume create -f json --size 1 ' + volume_name)) + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) self.assertEqual(volume_name, cmd_output['name']) + self.addCleanup( + self.openstack, + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume delete ' + + volume_name + ) + self.wait_for_status("volume", volume_name, "available") # create volume transfer request for the volume # and get the auth_key of the new transfer request cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request create -f json ' + - volume_name + - ' --name ' + name)) + ' --name ' + xfer_name + ' ' + + volume_name + )) + self.assertEqual(xfer_name, cmd_output['name']) auth_key = cmd_output['auth_key'] self.assertTrue(auth_key) # accept the volume transfer request cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request accept -f json ' + - name + ' ' + - '--auth-key ' + auth_key + '--auth-key ' + auth_key + ' ' + + xfer_name )) - self.assertEqual(name, cmd_output['name']) - - # the volume transfer will be removed by default after accepted - # so just need to delete the volume here - raw_output = self.openstack( - 'volume delete ' + volume_name) - self.assertEqual('', raw_output) + self.assertEqual(xfer_name, cmd_output['name']) def test_volume_transfer_request_list_show(self): - name = uuid.uuid4().hex + volume_name = uuid.uuid4().hex + xfer_name = uuid.uuid4().hex + + # create a volume + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + self.assertEqual(volume_name, cmd_output['name']) + self.addCleanup( + self.openstack, + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume delete ' + + volume_name + ) + self.wait_for_status("volume", volume_name, "available") + cmd_output = json.loads(self.openstack( '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request create -f json ' + - ' --name ' + name + ' ' + - self.VOLUME_NAME + ' --name ' + xfer_name + ' ' + + volume_name )) self.addCleanup( self.openstack, - 'volume transfer request delete ' + name + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request delete ' + + xfer_name ) - self.assertEqual(name, cmd_output['name']) + self.assertEqual(xfer_name, cmd_output['name']) auth_key = cmd_output['auth_key'] self.assertTrue(auth_key) @@ -95,11 +100,11 @@ def test_volume_transfer_request_list_show(self): '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request list -f json' )) - self.assertIn(name, [req['Name'] for req in cmd_output]) + self.assertIn(xfer_name, [req['Name'] for req in cmd_output]) cmd_output = json.loads(self.openstack( '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request show -f json ' + - name + xfer_name )) - self.assertEqual(name, cmd_output['name']) + self.assertEqual(xfer_name, cmd_output['name']) From 7fb866af4ee0ad050a081f88b9b3a899746644bb Mon Sep 17 00:00:00 2001 From: Rui Yuan Dou Date: Thu, 20 Dec 2018 13:53:39 +0800 Subject: [PATCH 0035/1120] Remove testr.conf as it's been replaced by stestr Change-Id: Ia596789a916b9271933c5b10f00399d83f18d44a --- .testr.conf | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .testr.conf diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 633aae1faf..0000000000 --- a/.testr.conf +++ /dev/null @@ -1,9 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./openstackclient/tests/unit} $LISTOPT $IDOPTION - -test_id_option=--load-list $IDFILE -test_list_option=--list -group_regex=([^\.]+\.)+ From aaa106059768735e5dae5218ebd3a75ae2ec23d3 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Thu, 20 Dec 2018 14:46:03 +0000 Subject: [PATCH 0036/1120] Add osc repo to the base job definition The osc-functional-devstack job is to be run against devstack changes in order to verify that the devstack-tox-functional job, which our job is based upon, doesn't introduce a regression[0]. In order to prevent this from failing, include our own repo as required-project. [0] https://review.openstack.org/526115 Change-Id: I782cd51dda1477c2e3a067cd902a3d3f29490083 --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index d646a0f766..3ce2f6ff21 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -70,6 +70,8 @@ - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ + required-projects: + - openstack/python-openstackclient vars: devstack_localrc: LIBS_FROM_GIT: python-openstackclient From 2dd5393167119c043ae125fead9f8ed9ba84241d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 8 Jan 2019 15:39:58 +0000 Subject: [PATCH 0037/1120] Use os-cloud instead of OS env vars for functional tests In order to support switching auth contexts, such as for registered_limits which take a system scoped token, switch the functional tests to using the --os-cloud command line parameter. However, honor the OS_CLOUD env var as a way that someone can select a different cloud, including 'envvars', to use. Use devstack-system-admin cloud for limit tests Keystone requires these to have system scope now. Change-Id: Ia81eebd3e00ae986cf3ba7e3d98f3e8a1647b622 --- openstackclient/tests/functional/base.py | 7 ++- .../tests/functional/identity/v3/common.py | 18 ++++-- .../functional/identity/v3/test_limit.py | 57 ++++++++++++++----- .../identity/v3/test_registered_limit.py | 34 +++++++---- .../tests/functional/run_stestr.sh | 16 ------ tox.ini | 6 +- 6 files changed, 87 insertions(+), 51 deletions(-) delete mode 100755 openstackclient/tests/functional/run_stestr.sh diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 90bbc24d29..7705c65539 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -24,6 +24,7 @@ FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..')) ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..')) EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples') +ADMIN_CLOUD = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin') def execute(cmd, fail_ok=False, merge_stderr=False): @@ -59,9 +60,11 @@ class TestCase(testtools.TestCase): delimiter_line = re.compile('^\+\-[\+\-]+\-\+$') @classmethod - def openstack(cls, cmd, fail_ok=False): + def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): """Executes openstackclient command for the given action.""" - return execute('openstack ' + cmd, fail_ok=fail_ok) + return execute( + 'openstack --os-cloud={cloud} '.format(cloud=cloud) + + cmd, fail_ok=fail_ok) @classmethod def get_openstack_configuration_value(cls, configuration): diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 58468bc7e7..43b416aabe 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -19,6 +19,7 @@ BASIC_LIST_HEADERS = ['ID', 'Name'] +SYSTEM_CLOUD = os.environ.get('OS_SYSTEM_CLOUD', 'devstack-system-admin') class IdentityTests(base.TestCase): @@ -341,7 +342,8 @@ def _create_dummy_registered_limit(self, add_clean_up=True): 'registered limit create' ' --service %(service_name)s' ' --default-limit %(default_limit)s' - ' %(resource_name)s' % params + ' %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) registered_limit_id = self._extract_value_from_items('id', items) @@ -349,7 +351,8 @@ def _create_dummy_registered_limit(self, add_clean_up=True): if add_clean_up: self.addCleanup( self.openstack, - 'registered limit delete %s' % registered_limit_id + 'registered limit delete %s' % registered_limit_id, + cloud=SYSTEM_CLOUD ) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -365,7 +368,8 @@ def _create_dummy_limit(self, add_clean_up=True): registered_limit_id = self._create_dummy_registered_limit() raw_output = self.openstack( - 'registered limit show %s' % registered_limit_id + 'registered limit show %s' % registered_limit_id, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) resource_name = self._extract_value_from_items('resource_name', items) @@ -389,13 +393,17 @@ def _create_dummy_limit(self, add_clean_up=True): ' --project %(project_id)s' ' --service %(service_id)s' ' --resource-limit %(resource_limit)s' - ' %(resource_name)s' % params + ' %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) limit_id = self._extract_value_from_items('id', items) if add_clean_up: - self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + self.addCleanup( + self.openstack, 'limit delete %s' % limit_id, + cloud=SYSTEM_CLOUD + ) self.assert_show_fields(items, self.LIMIT_FIELDS) return limit_id diff --git a/openstackclient/tests/functional/identity/v3/test_limit.py b/openstackclient/tests/functional/identity/v3/test_limit.py index 03bcb06e4b..b03f0f282a 100644 --- a/openstackclient/tests/functional/identity/v3/test_limit.py +++ b/openstackclient/tests/functional/identity/v3/test_limit.py @@ -10,17 +10,22 @@ # License for the specific language governing permissions and limitations # under the License. +import os + from tempest.lib.common.utils import data_utils from openstackclient.tests.functional.identity.v3 import common +SYSTEM_CLOUD = os.environ.get('OS_SYSTEM_CLOUD', 'devstack-system-admin') + class LimitTestCase(common.IdentityTests): def test_limit_create_with_service_name(self): registered_limit_id = self._create_dummy_registered_limit() raw_output = self.openstack( - 'registered limit show %s' % registered_limit_id + 'registered limit show %s' % registered_limit_id, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) service_id = self._extract_value_from_items('service_id', items) @@ -46,18 +51,24 @@ def test_limit_create_with_service_name(self): ' --project %(project_id)s' ' --service %(service_name)s' ' --resource-limit %(resource_limit)s' - ' %(resource_name)s' % params + ' %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) limit_id = self._extract_value_from_items('id', items) - self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + self.addCleanup( + self.openstack, + 'limit delete %s' % limit_id, + cloud=SYSTEM_CLOUD + ) self.assert_show_fields(items, self.LIMIT_FIELDS) def test_limit_create_with_project_name(self): registered_limit_id = self._create_dummy_registered_limit() raw_output = self.openstack( - 'registered limit show %s' % registered_limit_id + 'registered limit show %s' % registered_limit_id, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) service_id = self._extract_value_from_items('service_id', items) @@ -80,11 +91,16 @@ def test_limit_create_with_project_name(self): ' --project %(project_name)s' ' --service %(service_name)s' ' --resource-limit %(resource_limit)s' - ' %(resource_name)s' % params + ' %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) limit_id = self._extract_value_from_items('id', items) - self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + self.addCleanup( + self.openstack, + 'limit delete %s' % limit_id, + cloud=SYSTEM_CLOUD + ) self.assert_show_fields(items, self.LIMIT_FIELDS) registered_limit_id = self._create_dummy_registered_limit() @@ -107,7 +123,8 @@ def test_limit_create_with_options(self): raw_output = self.openstack( 'registered limit set' ' %(registered_limit_id)s' - ' --region %(region_id)s' % params + ' --region %(region_id)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) service_id = self._extract_value_from_items('service_id', items) @@ -134,17 +151,25 @@ def test_limit_create_with_options(self): ' --resource-limit %(resource_limit)s' ' --region %(region_id)s' ' --description %(description)s' - ' %(resource_name)s' % params + ' %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) limit_id = self._extract_value_from_items('id', items) - self.addCleanup(self.openstack, 'limit delete %s' % limit_id) + self.addCleanup( + self.openstack, + 'limit delete %s' % limit_id, + cloud=SYSTEM_CLOUD + ) self.assert_show_fields(items, self.LIMIT_FIELDS) def test_limit_show(self): limit_id = self._create_dummy_limit() - raw_output = self.openstack('limit show %s' % limit_id) + raw_output = self.openstack( + 'limit show %s' % limit_id, + cloud=SYSTEM_CLOUD + ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.LIMIT_FIELDS) @@ -159,7 +184,8 @@ def test_limit_set_description(self): raw_output = self.openstack( 'limit set' ' --description %(description)s' - ' %(limit_id)s' % params + ' %(limit_id)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.LIMIT_FIELDS) @@ -175,18 +201,21 @@ def test_limit_set_resource_limit(self): raw_output = self.openstack( 'limit set' ' --resource-limit %(resource_limit)s' - ' %(limit_id)s' % params + ' %(limit_id)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.LIMIT_FIELDS) def test_limit_list(self): self._create_dummy_limit() - raw_output = self.openstack('limit list') + raw_output = self.openstack('limit list', cloud=SYSTEM_CLOUD) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.LIMIT_LIST_HEADERS) def test_limit_delete(self): limit_id = self._create_dummy_limit(add_clean_up=False) - raw_output = self.openstack('limit delete %s' % limit_id) + raw_output = self.openstack( + 'limit delete %s' % limit_id, + cloud=SYSTEM_CLOUD) self.assertEqual(0, len(raw_output)) diff --git a/openstackclient/tests/functional/identity/v3/test_registered_limit.py b/openstackclient/tests/functional/identity/v3/test_registered_limit.py index 09e90ce206..80f51ad99f 100644 --- a/openstackclient/tests/functional/identity/v3/test_registered_limit.py +++ b/openstackclient/tests/functional/identity/v3/test_registered_limit.py @@ -10,10 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. +import os + from tempest.lib.common.utils import data_utils from openstackclient.tests.functional.identity.v3 import common +SYSTEM_CLOUD = os.environ.get('OS_SYSTEM_CLOUD', 'devstack-system-admin') + class RegisteredLimitTestCase(common.IdentityTests): @@ -37,7 +41,8 @@ def test_registered_limit_create_with_service_id(self): 'service_id': service_id, 'default_limit': 10, 'resource_name': 'cores' - } + }, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) registered_limit_id = self._extract_value_from_items('id', items) @@ -46,7 +51,8 @@ def test_registered_limit_create_with_service_id(self): 'registered limit delete' ' %(registered_limit_id)s' % { 'registered_limit_id': registered_limit_id - } + }, + cloud=SYSTEM_CLOUD ) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -68,7 +74,8 @@ def test_registered_limit_create_with_options(self): ' --region %(region_id)s' ' --service %(service_name)s' ' --default-limit %(default_limit)s' - ' %(resource_name)s' % params + ' %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) registered_limit_id = self._extract_value_from_items('id', items) @@ -76,7 +83,8 @@ def test_registered_limit_create_with_options(self): self.openstack, 'registered limit delete %(registered_limit_id)s' % { 'registered_limit_id': registered_limit_id - } + }, + cloud=SYSTEM_CLOUD ) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -102,7 +110,8 @@ def test_registered_limit_set_region_id(self): raw_output = self.openstack( 'registered limit set' ' %(registered_limit_id)s' - ' --region %(region_id)s' % params + ' --region %(region_id)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -116,7 +125,8 @@ def test_registered_limit_set_description(self): raw_output = self.openstack( 'registered limit set' ' %(registered_limit_id)s' - ' --description \'%(description)s\'' % params + ' --description \'%(description)s\'' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -131,7 +141,8 @@ def test_registered_limit_set_service(self): raw_output = self.openstack( 'registered limit set' ' %(registered_limit_id)s' - ' --service %(service)s' % params + ' --service %(service)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -145,7 +156,8 @@ def test_registered_limit_set_default_limit(self): raw_output = self.openstack( 'registered limit set' ' %(registered_limit_id)s' - ' --default-limit %(default_limit)s' % params + ' --default-limit %(default_limit)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -160,7 +172,8 @@ def test_registered_limit_set_resource_name(self): raw_output = self.openstack( 'registered limit set' ' %(registered_limit_id)s' - ' --resource-name %(resource_name)s' % params + ' --resource-name %(resource_name)s' % params, + cloud=SYSTEM_CLOUD ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.REGISTERED_LIMIT_FIELDS) @@ -179,6 +192,7 @@ def test_registered_limit_delete(self): 'registered limit delete' ' %(registered_limit_id)s' % { 'registered_limit_id': registered_limit_id - } + }, + cloud=SYSTEM_CLOUD ) self.assertEqual(0, len(raw_output)) diff --git a/openstackclient/tests/functional/run_stestr.sh b/openstackclient/tests/functional/run_stestr.sh deleted file mode 100755 index 229b42b676..0000000000 --- a/openstackclient/tests/functional/run_stestr.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# This is a script that runs ostestr with the openrc OS_ variables sourced. -# Do not run this script unless you know what you're doing. -# For more information refer to: -# https://docs.openstack.org/python-openstackclient/latest/ - -# Source environment variables to kick things off -if [ -f ~stack/devstack/openrc ] ; then - source ~stack/devstack/openrc admin admin -fi - -echo 'Running tests with:' -env | grep OS - -stestr run $* diff --git a/tox.ini b/tox.ini index 0b2aa78f50..67eccd4e76 100644 --- a/tox.ini +++ b/tox.ini @@ -70,14 +70,12 @@ whitelist_externals = stestr [testenv:functional] setenv = OS_TEST_PATH=./openstackclient/tests/functional passenv = OS_* -whitelist_externals = openstackclient/tests/functional/run_stestr.sh commands = - {toxinidir}/openstackclient/tests/functional/run_stestr.sh {posargs} + stestr run {posargs} [testenv:functional-tips] setenv = OS_TEST_PATH=./openstackclient/tests/functional passenv = OS_* -whitelist_externals = openstackclient/tests/functional/run_stestr.sh commands = pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" @@ -85,7 +83,7 @@ commands = pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" pip install -q -U -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" pip freeze - {toxinidir}/openstackclient/tests/functional/run_stestr.sh {posargs} + stestr run {posargs} [testenv:venv] basepython = python3 From b8438adbbf6f2024c9440ffec064088a02c807df Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Mon, 7 Jan 2019 13:02:57 +0100 Subject: [PATCH 0038/1120] Add floating IP filter to floating IP list command Add a parameter ``--floating-ip-address`` to ``floating ip list`` because it's supported by the API and also more efficient than the current ``floating ip show``. This also works as a work-around for pagination issues ``floating ip show`` might run into with an IP parameter. Change-Id: I113e3fa2495e1e86bb553c55c44f71a3f9f49d23 --- doc/source/cli/command-objects/floating-ip.rst | 6 ++++++ openstackclient/network/v2/floating_ip.py | 8 ++++++++ .../unit/network/v2/test_floating_ip_network.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/doc/source/cli/command-objects/floating-ip.rst b/doc/source/cli/command-objects/floating-ip.rst index 14615749ca..749c32d6b8 100644 --- a/doc/source/cli/command-objects/floating-ip.rst +++ b/doc/source/cli/command-objects/floating-ip.rst @@ -144,6 +144,12 @@ List floating IP(s) *Network version 2 only* +.. option:: --floating-ip-address + + List floating IP(s) according to given floating IP address + + *Network version 2 only* + .. option:: --long List additional fields in output diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index e1ec82748f..8ac8e10703 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -236,6 +236,12 @@ def update_parser_network(self, parser): help=_("List floating IP(s) according to " "given fixed IP address") ) + parser.add_argument( + '--floating-ip-address', + metavar='', + help=_("List floating IP(s) according to " + "given floating IP address") + ) parser.add_argument( '--long', action='store_true', @@ -316,6 +322,8 @@ def take_action_network(self, client, parsed_args): query['port_id'] = port.id if parsed_args.fixed_ip_address is not None: query['fixed_ip_address'] = parsed_args.fixed_ip_address + if parsed_args.floating_ip_address is not None: + query['floating_ip_address'] = parsed_args.floating_ip_address if parsed_args.status: query['status'] = parsed_args.status if parsed_args.project is not None: diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py index 209c01cf1c..cbd4da38ca 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py @@ -504,6 +504,23 @@ def test_floating_ip_list_fixed_ip_address(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_floating_ip_list_floating_ip_address(self): + arglist = [ + '--floating-ip-address', self.floating_ips[0].floating_ip_address, + ] + verifylist = [ + ('floating_ip_address', self.floating_ips[0].floating_ip_address), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with(**{ + 'floating_ip_address': self.floating_ips[0].floating_ip_address, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_floating_ip_list_long(self): arglist = ['--long', ] verifylist = [('long', True), ] From 7276610595499ab45872d1b0587effc3dba195e2 Mon Sep 17 00:00:00 2001 From: Noam Angel Date: Thu, 27 Dec 2018 08:35:45 +0000 Subject: [PATCH 0039/1120] fix multiple server delete produce multiple new lines Closes-Bug: #1809874 Change-Id: Ib988b189b41af03d3d871b660bb5b5cc090c3f30 --- openstackclient/compute/v2/server.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 3534fc7d9a..b608ecbd52 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -999,13 +999,9 @@ def _show_progress(progress): compute_client.servers, server) compute_client.servers.delete(server_obj.id) if parsed_args.wait: - if utils.wait_for_delete( - compute_client.servers, - server_obj.id, - callback=_show_progress, - ): - self.app.stdout.write('\n') - else: + if not utils.wait_for_delete(compute_client.servers, + server_obj.id, + callback=_show_progress): LOG.error(_('Error deleting server: %s'), server_obj.id) self.app.stdout.write(_('Error deleting server\n')) From aaf73cbf6d51a4a3b25cc0a5f1b7c5fb0d462c57 Mon Sep 17 00:00:00 2001 From: Radoslaw Smigielski Date: Mon, 10 Dec 2018 19:18:05 +0100 Subject: [PATCH 0040/1120] Fix --limit option in image list sub-command Client site fix of --limit option. This bugfix makes client "image list" command working again with "--limit" option. This option was ignored and even if user specified it, still list of all available images was returned. Story: 2004314 Change-Id: I30a78d65a644c9b7d23706a6637ce77bca2c2386 Depends-On: https://review.openstack.org/#/c/634776/ --- openstackclient/image/v2/image.py | 5 +++++ openstackclient/tests/unit/image/v2/test_image.py | 9 +++++---- releasenotes/notes/bug-27882-402ced7ffe930058.yaml | 5 +++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bug-27882-402ced7ffe930058.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 06eebe984c..1464c7b85d 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -630,6 +630,9 @@ def take_action(self, parsed_args): # List of image data received data = [] + limit = None + if 'limit' in kwargs: + limit = kwargs['limit'] if 'marker' in kwargs: data = image_client.api.image_list(**kwargs) else: @@ -642,6 +645,8 @@ def take_action(self, parsed_args): data.extend(page) # Set the marker to the id of the last item we received marker = page[-1]['id'] + if limit: + break if parsed_args.property: for attr, value in parsed_args.property.items(): diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 170a7f036b..a1a3035a65 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -744,21 +744,22 @@ def test_image_list_sort_option(self, si_mock): self.assertEqual(self.datalist, tuple(data)) def test_image_list_limit_option(self): + ret_limit = 1 arglist = [ - '--limit', str(1), + '--limit', str(ret_limit), ] verifylist = [ - ('limit', 1), + ('limit', ret_limit), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - limit=1, marker=self._image.id + limit=ret_limit, marker=None ) self.assertEqual(self.columns, columns) - self.assertEqual(len(self.datalist), len(tuple(data))) + self.assertEqual(ret_limit, len(tuple(data))) @mock.patch('osc_lib.utils.find_resource') def test_image_list_marker_option(self, fr_mock): diff --git a/releasenotes/notes/bug-27882-402ced7ffe930058.yaml b/releasenotes/notes/bug-27882-402ced7ffe930058.yaml new file mode 100644 index 0000000000..6bd090aa5c --- /dev/null +++ b/releasenotes/notes/bug-27882-402ced7ffe930058.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The ``--limit`` option of the ``image list`` command was previously ignored. + [Bug `https://storyboard.openstack.org/#!/story/2004314`] From 1a0bef2b46c76e1c6308def709199c30c68aae47 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 4 Feb 2019 12:48:20 -0600 Subject: [PATCH 0041/1120] More state handling in volume transfer requests functional tests Using addCleanup() for removing the pending volume transfer request has no way to wait for the volume status to become available before cleaning up the volume and gets racy when the tests are run with slow performance in the volume backend. So we pause at the end of the test after either accepting the transfer request or explicitly deleting it so the cleanup can delete the volume. Change-Id: I04862069cab28bc76eeafd60ba32be646f478d86 Signed-off-by: Dean Troyer --- .../volume/v2/test_transfer_request.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openstackclient/tests/functional/volume/v2/test_transfer_request.py b/openstackclient/tests/functional/volume/v2/test_transfer_request.py index d6aff73c48..00d0865c5c 100644 --- a/openstackclient/tests/functional/volume/v2/test_transfer_request.py +++ b/openstackclient/tests/functional/volume/v2/test_transfer_request.py @@ -49,17 +49,20 @@ def test_volume_transfer_request_accept(self): volume_name )) self.assertEqual(xfer_name, cmd_output['name']) + xfer_id = cmd_output['id'] auth_key = cmd_output['auth_key'] self.assertTrue(auth_key) + self.wait_for_status("volume", volume_name, "awaiting-transfer") # accept the volume transfer request cmd_output = json.loads(self.openstack( '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request accept -f json ' + '--auth-key ' + auth_key + ' ' + - xfer_name + xfer_id )) self.assertEqual(xfer_name, cmd_output['name']) + self.wait_for_status("volume", volume_name, "available") def test_volume_transfer_request_list_show(self): volume_name = uuid.uuid4().hex @@ -86,15 +89,11 @@ def test_volume_transfer_request_list_show(self): ' --name ' + xfer_name + ' ' + volume_name )) - self.addCleanup( - self.openstack, - '--os-volume-api-version ' + self.API_VERSION + ' ' + - 'volume transfer request delete ' + - xfer_name - ) self.assertEqual(xfer_name, cmd_output['name']) + xfer_id = cmd_output['id'] auth_key = cmd_output['auth_key'] self.assertTrue(auth_key) + self.wait_for_status("volume", volume_name, "awaiting-transfer") cmd_output = json.loads(self.openstack( '--os-volume-api-version ' + self.API_VERSION + ' ' + @@ -105,6 +104,18 @@ def test_volume_transfer_request_list_show(self): cmd_output = json.loads(self.openstack( '--os-volume-api-version ' + self.API_VERSION + ' ' + 'volume transfer request show -f json ' + - xfer_name + xfer_id )) self.assertEqual(xfer_name, cmd_output['name']) + + # NOTE(dtroyer): We need to delete the transfer request to allow the + # volume to be deleted. The addCleanup() route does + # not have a mechanism to wait for the volume status + # to become 'available' before attempting to delete + # the volume. + cmd_output = self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request delete ' + + xfer_id + ) + self.wait_for_status("volume", volume_name, "available") From 811b001234b9652f8fbfb7419bc9be51ee43de64 Mon Sep 17 00:00:00 2001 From: Jeremy Houser Date: Mon, 11 Feb 2019 11:21:36 -0600 Subject: [PATCH 0042/1120] This fix removes an erroneous underscore found within the function named test_snapshot_delete within test_snapshot.py found in both volume v1 and v2 of python-openstackclient. Story: 2004977 Change-Id: Iae29ba7992dcf8596f4fb4333d8bcf1889ecd7e6 --- openstackclient/tests/functional/volume/v1/test_snapshot.py | 2 +- openstackclient/tests/functional/volume/v2/test_snapshot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/functional/volume/v1/test_snapshot.py b/openstackclient/tests/functional/volume/v1/test_snapshot.py index c60472c55e..083cd1b087 100644 --- a/openstackclient/tests/functional/volume/v1/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v1/test_snapshot.py @@ -42,7 +42,7 @@ def tearDownClass(cls): finally: super(VolumeSnapshotTests, cls).tearDownClass() - def test_volume_snapshot__delete(self): + def test_volume_snapshot_delete(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( diff --git a/openstackclient/tests/functional/volume/v2/test_snapshot.py b/openstackclient/tests/functional/volume/v2/test_snapshot.py index ba6b2c2837..264f4adb6b 100644 --- a/openstackclient/tests/functional/volume/v2/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_snapshot.py @@ -43,7 +43,7 @@ def tearDownClass(cls): finally: super(VolumeSnapshotTests, cls).tearDownClass() - def test_volume_snapshot__delete(self): + def test_volume_snapshot_delete(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( From 0a187905c01f6bc2b9855081ac0042f00715dedf Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Wed, 13 Feb 2019 12:39:11 +0530 Subject: [PATCH 0043/1120] Add py36 env While running `tox` command in binoic env, we see the following message, ERROR: InterpreterNotFound: python3.5 It is because the default py3 version for binoic is py36. This patch adds the env in OSC also maintaining consistency with setup.cfg Change-Id: I8e5cf72901cba34ad44f2b356609f85b3b0c431f --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 67eccd4e76..e008f9562c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.3 -envlist = py35,py27,pep8 +envlist = py36,py35,py27,pep8 skipdist = True [testenv] From 05521bf84cb108c73bb36b270569b1986ad13f53 Mon Sep 17 00:00:00 2001 From: Bernard Cafarelli Date: Mon, 28 Jan 2019 14:17:46 +0100 Subject: [PATCH 0044/1120] Remove str() when setting network objects names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most network commands use str() on name argument, which fails on python 2 with Unicode characters. This comes from parsed arguments so does not actually need this call. Sample command failing with current code: openstack network create test_unicode™ Change-Id: Ie10b67864c912ee5c33e90b10c3d9705ee8307e7 Story: 2004356 Task: 27955 --- openstackclient/network/v2/network.py | 4 ++-- openstackclient/network/v2/network_agent.py | 2 +- openstackclient/network/v2/network_qos_policy.py | 2 +- openstackclient/network/v2/port.py | 2 +- openstackclient/network/v2/router.py | 2 +- openstackclient/network/v2/subnet.py | 2 +- openstackclient/network/v2/subnet_pool.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 0fdf62c95b..f5123932a3 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -72,7 +72,7 @@ def _get_columns_compute(item): def _get_attrs_network(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name if parsed_args.enable: attrs['admin_state_up'] = True if parsed_args.disable: @@ -143,7 +143,7 @@ def _get_attrs_network(client_manager, parsed_args): def _get_attrs_compute(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name if parsed_args.share: attrs['share_subnet'] = True if parsed_args.no_share: diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index ba2a2633ba..46e8d4b21c 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -351,7 +351,7 @@ def take_action(self, parsed_args): obj = client.get_agent(parsed_args.network_agent) attrs = {} if parsed_args.description is not None: - attrs['description'] = str(parsed_args.description) + attrs['description'] = parsed_args.description if parsed_args.enable: attrs['is_admin_state_up'] = True attrs['admin_state_up'] = True diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index 2c6b841b49..fd5ff93771 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -38,7 +38,7 @@ def _get_columns(item): def _get_attrs(client_manager, parsed_args): attrs = {} if 'name' in parsed_args and parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name if 'description' in parsed_args and parsed_args.description is not None: attrs['description'] = parsed_args.description if parsed_args.share: diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 1001e6cf41..f6d6fc7280 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -134,7 +134,7 @@ def _get_attrs(client_manager, parsed_args): attrs['dns_name'] = parsed_args.dns_name # It is possible that name is not updated during 'port set' if parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name # The remaining options do not support 'port set' command, so they require # additional check if 'network' in parsed_args and parsed_args.network is not None: diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 1b7d6374a9..2ec3e2f094 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -85,7 +85,7 @@ def _get_columns(item): def _get_attrs(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name if parsed_args.enable: attrs['admin_state_up'] = True if parsed_args.disable: diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index d252a7a51d..5f8113bb8d 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -169,7 +169,7 @@ def _get_attrs(client_manager, parsed_args, is_create=True): attrs = {} client = client_manager.network if 'name' in parsed_args and parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name if is_create: if 'project' in parsed_args and parsed_args.project is not None: diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 81765ca17e..d8c06eb882 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -52,7 +52,7 @@ def _get_attrs(client_manager, parsed_args): network_client = client_manager.network if parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + attrs['name'] = parsed_args.name if parsed_args.prefixes is not None: attrs['prefixes'] = parsed_args.prefixes if parsed_args.default_prefix_length is not None: From fa3c5e636e02b521d22c186603f15666c2363210 Mon Sep 17 00:00:00 2001 From: ZhongShengping Date: Tue, 19 Feb 2019 16:45:05 +0800 Subject: [PATCH 0045/1120] add python 3.7 unit test job This is a mechanically generated patch to add a unit test job running under Python 3.7. See ML discussion here [1] for context. [1] http://lists.openstack.org/pipermail/openstack-dev/2018-October/135626.html Change-Id: Ic1d05a80d286ff95777eb30d66086ef2b57bdb7f Story: #2004073 Task: #27438 --- .zuul.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zuul.yaml b/.zuul.yaml index 3ce2f6ff21..1ae311e844 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -189,6 +189,7 @@ - openstack-python-jobs - openstack-python35-jobs - openstack-python36-jobs + - openstack-python37-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 From 55cbbbe4692002e58120b0c129eb2add4f0bc18a Mon Sep 17 00:00:00 2001 From: David Rabel Date: Tue, 19 Feb 2019 12:49:18 +0100 Subject: [PATCH 0046/1120] Fix help message of image add project Only with the admin role you can use the project name with 'image add project'. With the normal member role you have to use the project id instead. If you try to use the name, you don't receive an error, but it won't work. Change-Id: I2d11c07a256917d12c46a7c302c5a5e8752a1df0 Task: 29543 Story: 2002535 --- openstackclient/image/v2/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 06eebe984c..a898e20fef 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -86,7 +86,7 @@ def get_parser(self, prog_name): parser.add_argument( "project", metavar="", - help=_("Project to associate with image (name or ID)"), + help=_("Project to associate with image (ID)"), ) common.add_project_domain_option_to_parser(parser) return parser From 4d76d7539a1c2bb430b040d2fbc8728fbf7a4eee Mon Sep 17 00:00:00 2001 From: David Rabel Date: Wed, 20 Feb 2019 10:32:16 +0100 Subject: [PATCH 0047/1120] Fix help message of image add project Only with the admin role you can use the project name with 'image add project'. With the normal member role you have to use the project id instead. If you try to use the name, you don't receive an error, but it won't work. Change-Id: I61d402b39558320502dc08905b8c3a146e5e740a Task: 29543 Story: 2002535 --- doc/source/cli/command-objects/image.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/command-objects/image.rst b/doc/source/cli/command-objects/image.rst index 95486e334b..b6e3594b16 100644 --- a/doc/source/cli/command-objects/image.rst +++ b/doc/source/cli/command-objects/image.rst @@ -32,7 +32,7 @@ Associate project with image .. _image_add_project-project: .. describe:: - Project to associate with image (name or ID) + Project to associate with image (ID) image create ------------ From 75cba9d1cbdd7b14b0d507af27f896c6c45e713e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Fri, 19 Oct 2018 12:46:21 +0300 Subject: [PATCH 0048/1120] Add support for get details of Quota With passing "--detail" argument to "openstack quota list", details about current usage should be returned. It is currently supported by Nova and Neutron so details of resources from those projects can be returned. Change-Id: I48fda15b34283bb7c66ea18ed28262f48b9229fe Related-Bug: #1716043 --- doc/source/cli/command-objects/quota.rst | 10 + openstackclient/common/quota.py | 231 +++++++++++++----- .../tests/functional/common/test_quota.py | 32 +++ .../tests/unit/common/test_quota.py | 87 ++++++- .../tests/unit/compute/v2/fakes.py | 32 +++ .../tests/unit/network/v2/fakes.py | 23 ++ ...d-quota-informations-1755129e1c68a252.yaml | 6 + 7 files changed, 353 insertions(+), 68 deletions(-) create mode 100644 releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml diff --git a/doc/source/cli/command-objects/quota.rst b/doc/source/cli/command-objects/quota.rst index f39536af8e..c538cfb3dd 100644 --- a/doc/source/cli/command-objects/quota.rst +++ b/doc/source/cli/command-objects/quota.rst @@ -17,6 +17,8 @@ List quotas for all projects with non-default quota values openstack quota list --compute | --network | --volume + [--project ] + [--detail] .. option:: --network @@ -30,6 +32,14 @@ List quotas for all projects with non-default quota values List volume quotas +.. option:: --project + + List quotas for this project (name or ID) + +.. option:: --detail + + Show details about quotas usage + quota set --------- diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 282ea4284d..dba6873ff9 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -97,12 +97,164 @@ def _xform_get_quota(data, value, keys): return res -class ListQuota(command.Lister): - _description = _("List quotas for all projects " - "with non-default quota values") +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 + 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) + 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) + except Exception as e: + if type(e).__name__ == 'EndpointNotFound': + return {} + else: + raise + return quota._info + + 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) + if type(network_quota) is not dict: + network_quota = network_quota.to_dict() + else: + network_quota = client.get_quota(project, + details=detail) + if type(network_quota) is not dict: + network_quota = network_quota.to_dict() + 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[u'in_use'] = values.pop("used") + network_quota[key] = values + return network_quota + else: + return {} + + +class ListQuota(command.Lister, BaseQuota): + _description = _( + "List quotas for all projects with non-default quota values or " + "list detailed quota informations 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)) + + result = [] + for resource, values in quotas.items(): + # NOTE(slaweq): there is no detailed quotas info for some resources + # and it should'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.add_argument( + '--project', + metavar='', + help=_('List quotas for this project (name or ID)'), + ) + parser.add_argument( + '--detail', + dest='detail', + action='store_true', + default=False, + help=_('Show details about quotas usage') + ) option = parser.add_mutually_exclusive_group(required=True) option.add_argument( '--compute', @@ -130,6 +282,8 @@ def take_action(self, parsed_args): project_ids = [getattr(p, 'id', '') for p in projects] 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: @@ -193,6 +347,9 @@ def take_action(self, parsed_args): ) for s in result)) if parsed_args.volume: + if parsed_args.detail: + LOG.warning("Volume service doesn't provide detailed quota" + " information") volume_client = self.app.client_manager.volume for p in project_ids: try: @@ -243,6 +400,8 @@ def take_action(self, parsed_args): ) 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: @@ -410,7 +569,7 @@ def take_action(self, parsed_args): **network_kwargs) -class ShowQuota(command.ShowOne): +class ShowQuota(command.ShowOne, BaseQuota): _description = _("Show quotas for project or class") def get_parser(self, prog_name): @@ -438,62 +597,6 @@ def get_parser(self, prog_name): ) return parser - 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 - 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_volume_quota(self, client, parsed_args): - try: - if parsed_args.quota_class: - quota = client.quota_classes.get(parsed_args.project) - else: - project_info = self._get_project(parsed_args) - project = project_info['id'] - if parsed_args.default: - quota = client.quotas.defaults(project) - else: - quota = client.quotas.get(project) - except Exception as e: - if type(e).__name__ == 'EndpointNotFound': - return {} - else: - raise - return quota._info - - def get_network_quota(self, parsed_args): - if parsed_args.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 parsed_args.default: - network_quota = client.get_quota_default(project) - else: - network_quota = client.get_quota(project) - return network_quota - else: - return {} - def take_action(self, parsed_args): compute_client = self.app.client_manager.compute @@ -504,10 +607,10 @@ def take_action(self, parsed_args): # 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_volume_quota(compute_client, - parsed_args) - volume_quota_info = self.get_compute_volume_quota(volume_client, - parsed_args) + 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 diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 76c69a4d03..859422812b 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -31,6 +31,38 @@ def setUpClass(cls): cls.PROJECT_NAME =\ cls.get_openstack_configuration_value('auth.project_name') + 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' + )) + self.assertIsNotNone(cmd_output) + resources = [] + for row in cmd_output: + row_headers = [str(r) for r in row.keys()] + self.assertEqual(sorted(expected_headers), sorted(row_headers)) + resources.append(row['Resource']) + # Ensure that returned quota is compute quota + self.assertIn("instances", resources) + # and that there is no network quota here + self.assertNotIn("networks", resources) + + 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' + )) + self.assertIsNotNone(cmd_output) + resources = [] + for row in cmd_output: + row_headers = [str(r) for r in row.keys()] + self.assertEqual(sorted(expected_headers), sorted(row_headers)) + resources.append(row['Resource']) + # Ensure that returned quota is network quota + self.assertIn("networks", resources) + # and that there is no compute quota here + self.assertNotIn("instances", resources) + def test_quota_list_network_option(self): if not self.haz_network: self.skipTest("No Network service present") diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 1a3da31d78..4f9e321b92 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -197,6 +197,85 @@ def setUp(self): self.cmd = quota.ListQuota(self.app, None) + @staticmethod + def _get_detailed_reference_data(quota): + reference_data = [] + for name, values in quota.to_dict().items(): + if type(values) is dict: + if 'used' in values: + # For network quota it's "used" key instead of "in_use" + in_use = values['used'] + else: + in_use = values['in_use'] + resource_values = [ + in_use, + values['reserved'], + values['limit']] + reference_data.append(tuple([name] + resource_values)) + return reference_data + + def test_quota_list_details_compute(self): + detailed_quota = ( + compute_fakes.FakeQuota.create_one_comp_detailed_quota()) + + detailed_column_header = ( + 'Resource', + 'In Use', + 'Reserved', + 'Limit', + ) + detailed_reference_data = ( + self._get_detailed_reference_data(detailed_quota)) + + self.compute.quotas.get = mock.Mock(return_value=detailed_quota) + + arglist = [ + '--detail', '--compute', + ] + verifylist = [ + ('detail', True), + ('compute', 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_details_network(self): + detailed_quota = ( + network_fakes.FakeQuota.create_one_net_detailed_quota()) + + detailed_column_header = ( + 'Resource', + 'In Use', + 'Reserved', + 'Limit', + ) + detailed_reference_data = ( + self._get_detailed_reference_data(detailed_quota)) + + self.network.get_quota = mock.Mock(return_value=detailed_quota) + + arglist = [ + '--detail', '--network', + ] + verifylist = [ + ('detail', True), + ('network', 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( @@ -827,13 +906,13 @@ def test_quota_show(self): self.cmd.take_action(parsed_args) self.compute_quotas_mock.get.assert_called_once_with( - self.projects[0].id, + self.projects[0].id, detail=False ) self.volume_quotas_mock.get.assert_called_once_with( self.projects[0].id, ) self.network.get_quota.assert_called_once_with( - self.projects[0].id, + self.projects[0].id, details=False ) self.assertNotCalled(self.network.get_quota_default) @@ -889,12 +968,12 @@ def test_quota_show_no_project(self): self.cmd.take_action(parsed_args) self.compute_quotas_mock.get.assert_called_once_with( - identity_fakes.project_id, + identity_fakes.project_id, detail=False ) self.volume_quotas_mock.get.assert_called_once_with( identity_fakes.project_id, ) self.network.get_quota.assert_called_once_with( - identity_fakes.project_id, + identity_fakes.project_id, details=False ) self.assertNotCalled(self.network.get_quota_default) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 234bbd9b80..37535aa017 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1400,6 +1400,38 @@ def create_one_default_comp_quota(attrs=None): return quota + @staticmethod + def create_one_comp_detailed_quota(attrs=None): + """Create one quota""" + + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'cores': {'reserved': 0, 'in_use': 0, 'limit': 20}, + 'fixed_ips': {'reserved': 0, 'in_use': 0, 'limit': 30}, + 'injected_files': {'reserved': 0, 'in_use': 0, 'limit': 100}, + 'injected_file_content_bytes': { + 'reserved': 0, 'in_use': 0, 'limit': 10240}, + 'injected_file_path_bytes': { + 'reserved': 0, 'in_use': 0, 'limit': 255}, + 'instances': {'reserved': 0, 'in_use': 0, 'limit': 50}, + 'key_pairs': {'reserved': 0, 'in_use': 0, 'limit': 20}, + 'metadata_items': {'reserved': 0, 'in_use': 0, 'limit': 10}, + 'ram': {'reserved': 0, 'in_use': 0, 'limit': 51200}, + 'server_groups': {'reserved': 0, 'in_use': 0, 'limit': 10}, + 'server_group_members': {'reserved': 0, 'in_use': 0, 'limit': 10} + } + + quota_attrs.update(attrs) + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + + quota.project_id = quota_attrs['id'] + + return quota + class FakeLimits(object): """Fake limits""" diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 28e92d1196..ee0919fdd5 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1700,3 +1700,26 @@ def create_one_default_net_quota(attrs=None): info=copy.deepcopy(quota_attrs), loaded=True) return quota + + @staticmethod + def create_one_net_detailed_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'floating_ips': {'used': 0, 'reserved': 0, 'limit': 20}, + 'networks': {'used': 0, 'reserved': 0, 'limit': 25}, + 'ports': {'used': 0, 'reserved': 0, 'limit': 11}, + 'rbac_policies': {'used': 0, 'reserved': 0, 'limit': 15}, + 'routers': {'used': 0, 'reserved': 0, 'limit': 40}, + 'security_groups': {'used': 0, 'reserved': 0, 'limit': 10}, + 'security_group_rules': {'used': 0, 'reserved': 0, 'limit': 100}, + 'subnets': {'used': 0, 'reserved': 0, 'limit': 20}, + 'subnet_pools': {'used': 0, 'reserved': 0, 'limit': 30}} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + return quota diff --git a/releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml b/releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml new file mode 100644 index 0000000000..7dbe202cdd --- /dev/null +++ b/releasenotes/notes/list-detailed-quota-informations-1755129e1c68a252.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for list detailed ``quota`` usage for project. + This can be done by passing ``--detail`` parameter to `quota list` command. + [Bug `1716043 `_] From 444a40c656b9f6007364ecd3bcf38964bbcd4556 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 26 Feb 2019 11:13:25 +0100 Subject: [PATCH 0049/1120] Add possibility to filter images using member_status In order to see image sharing membership it is required to additionally pass member_status filter to API. Otherwise only those with status 'all' will be returned. Thus adding possibility to see images shared with project to be approved or rejected. Change-Id: Ifd6e13e5a4ef09fbc29e76d464c93fbdbb178ae4 --- doc/source/cli/command-objects/image.rst | 7 +++ openstackclient/image/v2/image.py | 13 ++++++ .../tests/unit/image/v2/test_image.py | 43 +++++++++++++++++++ ...member-status-filter-2e118b2c93151223.yaml | 3 ++ 4 files changed, 66 insertions(+) create mode 100644 releasenotes/notes/add-member-status-filter-2e118b2c93151223.yaml diff --git a/doc/source/cli/command-objects/image.rst b/doc/source/cli/command-objects/image.rst index 95486e334b..0c5b0333cc 100644 --- a/doc/source/cli/command-objects/image.rst +++ b/doc/source/cli/command-objects/image.rst @@ -209,6 +209,7 @@ List available images [--property ] [--name ] [--status ] + [--member-status ] [--tag ] [--long] [--sort [:]] @@ -251,6 +252,12 @@ List available images *Image version 2 only* +.. option:: --member-status + + Filter images based on member status + + *Image version 2 only* + .. option:: --tag Filter images based on tag diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 06eebe984c..f4ab5bdc62 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -37,6 +37,7 @@ DEFAULT_DISK_FORMAT = 'raw' DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", "vdi", "iso", "ploop"] +MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"] LOG = logging.getLogger(__name__) @@ -530,6 +531,16 @@ def get_parser(self, prog_name): default=None, help=_("Filter images based on status.") ) + parser.add_argument( + '--member-status', + metavar='', + default=None, + type=lambda s: s.lower(), + choices=MEMBER_STATUS_CHOICES, + help=(_("Filter images based on member status. " + "The supported options are: %s. ") % + ', '.join(MEMBER_STATUS_CHOICES)) + ) parser.add_argument( '--tag', metavar='', @@ -595,6 +606,8 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.status: kwargs['status'] = parsed_args.status + if parsed_args.member_status: + kwargs['member_status'] = parsed_args.member_status if parsed_args.tag: kwargs['tag'] = parsed_args.tag if parsed_args.long: diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 170a7f036b..087d8751e7 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -644,6 +644,49 @@ def test_image_list_shared_option(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) + def test_image_list_shared_member_status_option(self): + arglist = [ + '--shared', + '--member-status', 'all' + ] + verifylist = [ + ('public', False), + ('private', False), + ('community', False), + ('shared', True), + ('long', False), + ('member_status', 'all') + ] + 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.api_mock.image_list.assert_called_with( + shared=True, + member_status='all', + marker=self._image.id, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_image_list_shared_member_status_lower(self): + arglist = [ + '--shared', + '--member-status', 'ALl' + ] + verifylist = [ + ('public', False), + ('private', False), + ('community', False), + ('shared', True), + ('long', False), + ('member_status', 'all') + ] + self.check_parser(self.cmd, arglist, verifylist) + def test_image_list_long_option(self): arglist = [ '--long', diff --git a/releasenotes/notes/add-member-status-filter-2e118b2c93151223.yaml b/releasenotes/notes/add-member-status-filter-2e118b2c93151223.yaml new file mode 100644 index 0000000000..f7440df1c4 --- /dev/null +++ b/releasenotes/notes/add-member-status-filter-2e118b2c93151223.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add ``--member-status`` option to ``image list`` command. From d43178c3a429f8f0f8d6c2e7f15cee75d7c4ba0e Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Tue, 26 Feb 2019 23:48:40 +0530 Subject: [PATCH 0050/1120] Disabling c-backup service for osc-functional-devstack-tips job Since swift isn't compatible with py3 currently and disabled for this gate job, c-backup service will always fail configuring. The backup related tests can be handled by other jobs having swift enabled. The c-backup service can be enabled along with swift services once swift is compatible with py3. This patch disables the the c-backup service for osc-functional-devstack-tips gate job. Change-Id: Ifd3a4e1a15f1365107a2a1367513e2ef79bd13cc --- .zuul.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 3ce2f6ff21..356a97e6d5 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -166,6 +166,11 @@ s-container: false s-object: false s-proxy: false + # As swift is not available for this job, c-backup 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-backup: false tox_envlist: functional tox_install_siblings: true From 6475882fd8eaae98141d85c40c760cd220aedad1 Mon Sep 17 00:00:00 2001 From: Christian Schneemann Date: Thu, 7 Feb 2019 11:10:20 +0100 Subject: [PATCH 0051/1120] Typo fix Just a typo fix. Change-Id: I1d1fe6eb95c0b167265b3664314d764e3c316fe2 --- openstackclient/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b608ecbd52..eb012e02ec 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -604,7 +604,7 @@ def get_parser(self, prog_name): type=_prefix_checked_value('port-id='), help=_("Create a NIC on the server and connect it to port. " "Specify option multiple times to create multiple NICs. " - "This is a wrapper for the '--nic port-id=' " + "This is a wrapper for the '--nic port-id=' " "parameter that provides simple syntax for the standard " "use case of connecting a new server to a given port. For " "more advanced use cases, refer to the '--nic' parameter."), From 24255ad0dde608a19d371a27c13714098097f185 Mon Sep 17 00:00:00 2001 From: whoami-rajat Date: Thu, 13 Dec 2018 11:15:09 +0530 Subject: [PATCH 0052/1120] Fix: Restore output 'VolumeBackupsRestore' object is not iterable VolumeBackupsRetore object has '_info' attribute which contains the output data of the restore command which should be returned instead of the 'VolumeBackupsRestore' object. Change-Id: I64b75649c1ac9c24e05a197f7280975564b4d386 Story: 2004740 Task: 28811 --- .zuul.yaml | 4 +- .../tests/functional/volume/v2/test_backup.py | 58 +++++++++++++++++++ .../tests/unit/volume/v2/test_backup.py | 6 +- openstackclient/volume/v2/backup.py | 4 +- 4 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 openstackclient/tests/functional/volume/v2/test_backup.py diff --git a/.zuul.yaml b/.zuul.yaml index 356a97e6d5..9a94dd934b 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -166,11 +166,11 @@ s-container: false s-object: false s-proxy: false - # As swift is not available for this job, c-backup service won't be functional. + # 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-backup: false + c-bak: false tox_envlist: functional tox_install_siblings: true diff --git a/openstackclient/tests/functional/volume/v2/test_backup.py b/openstackclient/tests/functional/volume/v2/test_backup.py new file mode 100644 index 0000000000..e4890b00ea --- /dev/null +++ b/openstackclient/tests/functional/volume/v2/test_backup.py @@ -0,0 +1,58 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import uuid + +from openstackclient.tests.functional.volume.v2 import common + + +class VolumeBackupTests(common.BaseVolumeTests): + """Functional tests for volume backups. """ + + def setUp(self): + super(VolumeBackupTests, self).setUp() + self.backup_enabled = False + serv_list = json.loads(self.openstack('volume service list -f json')) + for service in serv_list: + if service['Binary'] == 'cinder-backup': + if service['Status'] == 'enabled': + self.backup_enabled = True + + def test_volume_backup_restore(self): + """Test restore backup""" + if not self.backup_enabled: + self.skipTest('Backup service is not enabled') + vol_id = uuid.uuid4().hex + # create a volume + json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + vol_id + )) + # create a backup + backup = json.loads(self.openstack( + 'volume backup create -f json ' + + vol_id + )) + + self.wait_for_status("volume", vol_id, "available") + self.wait_for_status("backup", backup['id'], "available") + # restore the backup + backup_restored = json.loads(self.openstack( + 'volume backup restore -f json %s %s' + % (backup['id'], vol_id))) + self.assertEqual(backup_restored['backup_id'], backup['id']) + self.wait_for_status("backup", backup['id'], "available") + self.wait_for_status("volume", backup_restored['volume_id'], + "available") + self.addCleanup(self.openstack, 'volume delete %s' % vol_id) diff --git a/openstackclient/tests/unit/volume/v2/test_backup.py b/openstackclient/tests/unit/volume/v2/test_backup.py index a8e81c7eb8..9a2ce71809 100644 --- a/openstackclient/tests/unit/volume/v2/test_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_backup.py @@ -367,7 +367,9 @@ 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 = None + self.restores_mock.restore.return_value = ( + volume_fakes.FakeVolume.create_one_volume( + {'id': self.volume['id']})) # Get the command object to mock self.cmd = backup.RestoreVolumeBackup(self.app, None) @@ -385,7 +387,7 @@ def test_backup_restore(self): result = self.cmd.take_action(parsed_args) self.restores_mock.restore.assert_called_with(self.backup.id, self.backup.volume_id) - self.assertIsNone(result) + self.assertIsNotNone(result) class TestBackupSet(TestBackup): diff --git a/openstackclient/volume/v2/backup.py b/openstackclient/volume/v2/backup.py index 60633a7080..d4aec8d747 100644 --- a/openstackclient/volume/v2/backup.py +++ b/openstackclient/volume/v2/backup.py @@ -319,7 +319,9 @@ def take_action(self, parsed_args): backup = utils.find_resource(volume_client.backups, parsed_args.backup) destination_volume = utils.find_resource(volume_client.volumes, parsed_args.volume) - return volume_client.restores.restore(backup.id, destination_volume.id) + backup = volume_client.restores.restore(backup.id, + destination_volume.id) + return zip(*sorted(six.iteritems(backup._info))) class RestoreBackup(RestoreVolumeBackup): From e776a4f0260af1d2ae66439e647794395d470578 Mon Sep 17 00:00:00 2001 From: David Rabel Date: Fri, 22 Feb 2019 15:21:17 +0100 Subject: [PATCH 0053/1120] Add --attached / --detached parameter to volume set As to reflect cinder reset-state --attach-status functionality, this patch adds --attached / --detached parameter to OSC's volume set command. Change-Id: Ic8ee928c9ab0e579512cfb7608f63bfcc2993c7b Closes-Bug: #1745699 --- doc/source/cli/command-objects/volume.rst | 17 +++++++++ .../tests/unit/volume/v2/test_volume.py | 36 +++++++++++++++++++ openstackclient/volume/v2/volume.py | 35 ++++++++++++++++++ .../notes/bug-1745699-afa7318b9dc96696.yaml | 7 ++++ 4 files changed, 95 insertions(+) create mode 100644 releasenotes/notes/bug-1745699-afa7318b9dc96696.yaml diff --git a/doc/source/cli/command-objects/volume.rst b/doc/source/cli/command-objects/volume.rst index 5c86d10deb..fc6188c0a8 100644 --- a/doc/source/cli/command-objects/volume.rst +++ b/doc/source/cli/command-objects/volume.rst @@ -262,6 +262,7 @@ Set volume properties [--property [...] ] [--image-property [...] ] [--state ] + [--attached | --detached ] [--type ] [--retype-policy ] [--bootable | --non-bootable] @@ -341,6 +342,22 @@ Set volume properties *Volume version 2 only* +.. option:: --attached + + Set volume attachment status to "attached" (admin only) + (This option simply changes the state of the volume in the database with + no regard to actual status, exercise caution when using) + + *Volume version 2 only* + +.. option:: --deattach + + Set volume attachment status to "detached" (admin only) + (This option simply changes the state of the volume in the database with + no regard to actual status, exercise caution when using) + + *Volume version 2 only* + .. _volume_set-volume: .. describe:: diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 1f53ce9471..dbe69ea0ff 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -1327,6 +1327,42 @@ def test_volume_set_state_failed(self): self.volumes_mock.reset_state.assert_called_with( self.new_volume.id, 'error') + def test_volume_set_attached(self): + arglist = [ + '--attached', + self.new_volume.id + ] + verifylist = [ + ('attached', True), + ('detached', False), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.reset_state.assert_called_with( + self.new_volume.id, attach_status='attached', state=None) + self.assertIsNone(result) + + def test_volume_set_detached(self): + arglist = [ + '--detached', + self.new_volume.id + ] + verifylist = [ + ('attached', False), + ('detached', True), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.reset_state.assert_called_with( + self.new_volume.id, attach_status='detached', state=None) + self.assertIsNone(result) + def test_volume_set_bootable(self): arglist = [ ['--bootable', self.new_volume.id], diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 14a454c228..fa587b5f67 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -559,6 +559,25 @@ def get_parser(self, prog_name): 'in the database with no regard to actual status, ' 'exercise caution when using)'), ) + attached_group = parser.add_mutually_exclusive_group() + attached_group.add_argument( + "--attached", + action="store_true", + help=_('Set volume attachment status to "attached" ' + '(admin only) ' + '(This option simply changes the state of the volume ' + 'in the database with no regard to actual status, ' + 'exercise caution when using)'), + ) + attached_group.add_argument( + "--detached", + action="store_true", + help=_('Set volume attachment status to "detached" ' + '(admin only) ' + '(This option simply changes the state of the volume ' + 'in the database with no regard to actual status, ' + 'exercise caution when using)'), + ) parser.add_argument( '--type', metavar='', @@ -645,6 +664,22 @@ def take_action(self, parsed_args): except Exception as e: LOG.error(_("Failed to set volume state: %s"), e) result += 1 + if parsed_args.attached: + try: + volume_client.volumes.reset_state( + volume.id, state=None, + attach_status="attached") + except Exception as e: + LOG.error(_("Failed to set volume attach-status: %s"), e) + result += 1 + if parsed_args.detached: + try: + volume_client.volumes.reset_state( + volume.id, state=None, + attach_status="detached") + except Exception as e: + LOG.error(_("Failed to set volume attach-status: %s"), e) + result += 1 if parsed_args.bootable or parsed_args.non_bootable: try: volume_client.volumes.set_bootable( diff --git a/releasenotes/notes/bug-1745699-afa7318b9dc96696.yaml b/releasenotes/notes/bug-1745699-afa7318b9dc96696.yaml new file mode 100644 index 0000000000..68b48cb03d --- /dev/null +++ b/releasenotes/notes/bug-1745699-afa7318b9dc96696.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add ``--attached`` and ``--detached`` options to ``volume set`` command to set the + volume status in the database. This is the functional equivalent to + ``cinder reset-state --attach-status``. + [`bug 1745699 `_ \ No newline at end of file From c79de8a90bc1f1149b8302052580e5a76876724c Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 27 Feb 2019 14:21:08 +0200 Subject: [PATCH 0054/1120] Paginate over usage list to return all usages since nova api 2.40 the os-simple-tenant-usage API supports pagination and will by default return a number of entities configured internally in Nova. This means that when there are many enough projects, the single call to usage.list() will not return usages for all projects. This patch effectively copy-pastes the logic to paginate over usage list results from novaclient/v2/shell.py code. Change-Id: I1b639fe386b7b7db3223f6965495094b9d51533a Story: #2005099 Task: #29713 --- openstackclient/compute/v2/usage.py | 50 ++++++++++++++++++- .../tests/unit/compute/v2/fakes.py | 1 + .../tests/unit/compute/v2/test_usage.py | 26 ++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 4320bf90b6..f84cd61dfb 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -15,8 +15,10 @@ """Usage action implementations""" +import collections import datetime +from novaclient import api_versions from osc_lib.command import command from osc_lib import utils import six @@ -24,6 +26,36 @@ from openstackclient.i18n import _ +def _get_usage_marker(usage): + marker = None + if hasattr(usage, 'server_usages') and usage.server_usages: + marker = usage.server_usages[-1]['instance_id'] + return marker + + +def _get_usage_list_marker(usage_list): + marker = None + if usage_list: + marker = _get_usage_marker(usage_list[-1]) + return marker + + +def _merge_usage(usage, next_usage): + usage.server_usages.extend(next_usage.server_usages) + usage.total_hours += next_usage.total_hours + usage.total_memory_mb_usage += next_usage.total_memory_mb_usage + usage.total_vcpus_usage += next_usage.total_vcpus_usage + usage.total_local_gb_usage += next_usage.total_local_gb_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) + else: + usages[next_usage.tenant_id] = next_usage + + class ListUsage(command.Lister): _description = _("List resource usage per project") @@ -83,7 +115,23 @@ def _format_project(project): else: end = now + datetime.timedelta(days=1) - usage_list = compute_client.usage.list(start, end, detailed=True) + 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()) # Cache the project list project_cache = {} diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 234bbd9b80..38f4ff67f1 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1304,6 +1304,7 @@ def create_one_usage(attrs=None): 'local_gb': 1, 'memory_mb': 512, 'name': 'usage-name-' + uuid.uuid4().hex, + 'instance_id': uuid.uuid4().hex, 'state': 'active', 'uptime': 3600, 'vcpus': 1 diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py index a7aa1374e6..76dcc963fb 100644 --- a/openstackclient/tests/unit/compute/v2/test_usage.py +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -14,6 +14,7 @@ import datetime import mock +from novaclient import api_versions from openstackclient.compute.v2 import usage from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -104,6 +105,31 @@ def test_usage_list_with_options(self): self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) + def test_usage_list_with_pagination(self): + arglist = [] + verifylist = [ + ('start', None), + ('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.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + class TestUsageShow(TestUsage): From be7a75814ca4a503dd384f36166f93f12f6cb8da Mon Sep 17 00:00:00 2001 From: Doug Wiegley Date: Wed, 13 Feb 2019 14:17:33 -0700 Subject: [PATCH 0055/1120] Add 'security_group' type support to network rbac commands Partial-Bug: #1817119 Depends-On: https://review.openstack.org/635311 Change-Id: I5f132fa54714514d8dae62df8bc494f3f6476768 --- .../cli/command-objects/network-rbac.rst | 4 +- openstackclient/network/v2/network_rbac.py | 13 ++++-- .../tests/unit/network/v2/fakes.py | 33 +++++++++++++++ .../unit/network/v2/test_network_rbac.py | 40 +++++++++++++++++++ ...c-add-security-group-35370701a06ac906.yaml | 5 +++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/rbac-add-security-group-35370701a06ac906.yaml diff --git a/doc/source/cli/command-objects/network-rbac.rst b/doc/source/cli/command-objects/network-rbac.rst index 45fd354deb..22733a2cd2 100644 --- a/doc/source/cli/command-objects/network-rbac.rst +++ b/doc/source/cli/command-objects/network-rbac.rst @@ -26,7 +26,7 @@ Create network RBAC policy .. option:: --type - Type of the object that RBAC policy affects ("qos_policy" or "network") (required) + Type of the object that RBAC policy affects ("security_group", "qos_policy" or "network") (required) .. option:: --action @@ -90,7 +90,7 @@ List network RBAC policies .. option:: --type - List network RBAC policies according to given object type ("qos_policy" or "network") + List network RBAC policies according to given object type ("security_group", "qos_policy" or "network") .. option:: --action diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 6cf82559dd..140c837e38 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -48,6 +48,10 @@ def _get_attrs(client_manager, parsed_args): object_id = network_client.find_qos_policy( parsed_args.rbac_object, ignore_missing=False).id + if parsed_args.type == 'security_group': + object_id = network_client.find_security_group( + parsed_args.rbac_object, + ignore_missing=False).id attrs['object_id'] = object_id identity_client = client_manager.identity @@ -87,9 +91,9 @@ def get_parser(self, prog_name): '--type', metavar="", required=True, - choices=['qos_policy', 'network'], + choices=['security_group', 'qos_policy', 'network'], help=_('Type of the object that RBAC policy ' - 'affects ("qos_policy" or "network")') + 'affects ("security_group", "qos_policy" or "network")') ) parser.add_argument( '--action', @@ -178,9 +182,10 @@ def get_parser(self, prog_name): parser.add_argument( '--type', metavar='', - choices=['qos_policy', 'network'], + choices=['security_group', 'qos_policy', 'network'], help=_('List network RBAC policies according to ' - 'given object type ("qos_policy" or "network")') + 'given object type ("security_group", "qos_policy" ' + 'or "network")') ) parser.add_argument( '--action', diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 28e92d1196..5860ff452f 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -908,6 +908,39 @@ def get_qos_policies(qos_policies=None, count=2): return mock.Mock(side_effect=qos_policies) +class FakeNetworkSecGroup(object): + """Fake one security group.""" + + @staticmethod + def create_one_security_group(attrs=None): + """Create a fake security group. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + sg_id = attrs.get('id') or 'security-group-id-' + uuid.uuid4().hex + + # Set default attributes. + security_group_attrs = { + 'name': 'security-group-name-' + uuid.uuid4().hex, + 'id': sg_id, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'description': 'security-group-description-' + uuid.uuid4().hex + } + + security_group = fakes.FakeResource( + info=copy.deepcopy(security_group_attrs), + loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + security_group.project_id = security_group_attrs['tenant_id'] + + return security_group + + class FakeNetworkQosRule(object): """Fake one or more Network QoS rules.""" diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index 70c3852868..96440091c1 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -37,6 +37,7 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): network_object = network_fakes.FakeNetwork.create_one_network() qos_object = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + sg_object = network_fakes.FakeNetworkSecGroup.create_one_security_group() project = identity_fakes_v3.FakeProject.create_one_project() rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac( attrs={'tenant_id': project.id, @@ -74,6 +75,8 @@ def setUp(self): return_value=self.network_object) self.network.find_qos_policy = mock.Mock( return_value=self.qos_object) + self.network.find_security_group = mock.Mock( + return_value=self.sg_object) self.projects_mock.get.return_value = self.project def test_network_rbac_create_no_type(self): @@ -258,6 +261,43 @@ def test_network_rbac_create_qos_object(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_rbac_create_security_group_object(self): + self.rbac_policy.object_type = 'security_group' + self.rbac_policy.object_id = self.sg_object.id + arglist = [ + '--type', 'security_group', + '--action', self.rbac_policy.action, + '--target-project', self.rbac_policy.target_tenant, + self.sg_object.name, + ] + verifylist = [ + ('type', 'security_group'), + ('action', self.rbac_policy.action), + ('target_project', self.rbac_policy.target_tenant), + ('rbac_object', self.sg_object.name), + ] + 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.create_rbac_policy.assert_called_with(**{ + 'object_id': self.sg_object.id, + 'object_type': 'security_group', + 'action': self.rbac_policy.action, + 'target_tenant': self.rbac_policy.target_tenant, + }) + self.data = [ + self.rbac_policy.action, + self.rbac_policy.id, + self.sg_object.id, + 'security_group', + self.rbac_policy.tenant_id, + self.rbac_policy.target_tenant, + ] + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestDeleteNetworkRBAC(TestNetworkRBAC): diff --git a/releasenotes/notes/rbac-add-security-group-35370701a06ac906.yaml b/releasenotes/notes/rbac-add-security-group-35370701a06ac906.yaml new file mode 100644 index 0000000000..f21eebb795 --- /dev/null +++ b/releasenotes/notes/rbac-add-security-group-35370701a06ac906.yaml @@ -0,0 +1,5 @@ +features: + - | + Add ``security_group`` as a valid ``--type`` value for the + ``network rbac create`` and ``network rbac list`` commands. + From 239b103849b96213dc9cb317006346ce311228e4 Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Mon, 28 Jan 2019 19:30:00 +0100 Subject: [PATCH 0056/1120] API microversion 2.69: Handles Down Cells This patch explicitly points out the change needed while forming the detailed lists for servers. In those cases where the server response for ``openstack server list`` has the flavor and image keys missing for the instances in the down cell, the servers will be skipped from being processed. Depends-On: https://review.openstack.org/591657/ Related to blueprint handling-down-cell Change-Id: Ibcfe9febdc45db1cb86c6e88f65976feceb01c02 --- openstackclient/compute/v2/server.py | 8 ++++ .../tests/unit/compute/v2/test_server.py | 44 +++++++++++++++++++ ...p-handling-down-cell-ab8af2897f1a8c85.yaml | 9 ++++ 3 files changed, 61 insertions(+) create mode 100644 releasenotes/notes/bp-handling-down-cell-ab8af2897f1a8c85.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b608ecbd52..36a674339f 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1314,6 +1314,14 @@ def take_action(self, parsed_args): # Populate image_name, image_id, flavor_name and flavor_id attributes # of server objects so that we can display those columns. for s in data: + if compute_client.api_version >= api_versions.APIVersion('2.69'): + # NOTE(tssurya): From 2.69, we will have the keys 'flavor' + # and 'image' missing in the server response during + # infrastructure failure situations. + # For those servers with partial constructs we just skip the + # processing of the image and flavor informations. + if not hasattr(s, 'image') or not hasattr(s, 'flavor'): + continue if 'id' in s.image: image = images.get(s.image['id']) if image: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c529d840de..c30af8fb8e 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2272,6 +2272,50 @@ def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): 'Invalid time value' ) + def test_server_list_v269_with_partial_constructs(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.69') + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # include "partial results" from non-responsive part of + # infrastructure. + server_dict = { + "id": "server-id-95a56bfc4xxxxxx28d7e418bfd97813a", + "status": "UNKNOWN", + "tenant_id": "6f70656e737461636b20342065766572", + "created": "2018-12-03T21:06:18Z", + "links": [ + { + "href": "http://fake/v2.1/", + "rel": "self" + }, + { + "href": "http://fake", + "rel": "bookmark" + } + ], + # We need to pass networks as {} because its defined as a property + # of the novaclient Server class which gives {} by default. If not + # it will fail at formatting the networks info later on. + "networks": {} + } + server = compute_fakes.fakes.FakeResource( + info=server_dict, + ) + self.servers.append(server) + columns, data = self.cmd.take_action(parsed_args) + # get the first three servers out since our interest is in the partial + # server. + next(data) + next(data) + next(data) + partial_server = next(data) + expected_row = ( + 'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', '', + 'UNKNOWN', '', '', '') + self.assertEqual(expected_row, partial_server) + class TestServerLock(TestServer): diff --git a/releasenotes/notes/bp-handling-down-cell-ab8af2897f1a8c85.yaml b/releasenotes/notes/bp-handling-down-cell-ab8af2897f1a8c85.yaml new file mode 100644 index 0000000000..f9e1ad278d --- /dev/null +++ b/releasenotes/notes/bp-handling-down-cell-ab8af2897f1a8c85.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + From microversion 2.69 the results of ``openstack server list`` and + ``openstack server show`` may contain missing information in their outputs + when there are partial infrastructure failure periods in the deployment. + See `Handling Down Cells`_ for more information on the missing keys/info. + + .. _Handling Down Cells: https://developer.openstack.org/api-guide/compute/down_cells.html \ No newline at end of file From d52920b3878479f7dd549cba1679870b4f00ee33 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Wed, 27 Feb 2019 23:02:52 +0800 Subject: [PATCH 0057/1120] Add network segment range command object Add network segment range command object in support of network segment range management. This patch set includes documentation, unit tests and functional tests (currently skipped unit network segment range enabled in Neutron by default) for the following new commands: - "os network segment range create" - "os network segment range delete" - "os network segment range list" - "os network segment range set" - "os network segment range show" Co-authored-by: Allain Legacy [depends on removed by dtroyer as those are all +W and trying to pass the gate, OSC has it's freeze dealine looming] Depends: https://review.openstack.org/624708 Depends: https://review.openstack.org/624709 Depends: https://review.openstack.org/638386 Partially-implements: blueprint network-segment-range-management Change-Id: I335692f2db5be07c1c164f09b13f1abb80b7ba33 --- .zuul.yaml | 1 + .../command-objects/network_segment_range.rst | 168 ++++++ doc/source/cli/commands.rst | 1 + .../network/v2/network_segment_range.py | 458 +++++++++++++++ .../network/v2/test_network_segment_range.py | 145 +++++ .../tests/unit/network/v2/fakes.py | 60 ++ .../network/v2/test_network_segment_range.py | 552 ++++++++++++++++++ ...ent-range-management-0abf03fe03eea149.yaml | 6 + setup.cfg | 6 + 9 files changed, 1397 insertions(+) create mode 100644 doc/source/cli/command-objects/network_segment_range.rst create mode 100644 openstackclient/network/v2/network_segment_range.py create mode 100644 openstackclient/tests/functional/network/v2/test_network_segment_range.py create mode 100644 openstackclient/tests/unit/network/v2/test_network_segment_range.py create mode 100644 releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 3ce2f6ff21..9d3845c639 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -113,6 +113,7 @@ # NOTE(amotoki): Some neutron features are enabled by devstack plugin neutron: https://git.openstack.org/openstack/neutron devstack_services: + neutron-network-segment-range: true neutron-segments: true q-metering: true q-qos: true diff --git a/doc/source/cli/command-objects/network_segment_range.rst b/doc/source/cli/command-objects/network_segment_range.rst new file mode 100644 index 0000000000..71155d2224 --- /dev/null +++ b/doc/source/cli/command-objects/network_segment_range.rst @@ -0,0 +1,168 @@ +===================== +network segment range +===================== + +A **network segment range** is a resource for tenant network segment +allocation. +A network segment range exposes the segment range management to be administered +via the Neutron API. In addition, it introduces the ability for the +administrator to control the segment ranges globally or on a per-tenant basis. + +Network v2 + +network segment range create +---------------------------- + +Create new network segment range + +.. program:: network segment range create +.. code:: bash + + openstack network segment range create + (--private | --shared) + [--project [--project-domain ]] + --network-type + [--physical-network ] + --minimum + --maximum + + +.. option:: --private + + Network segment range is assigned specifically to the project + +.. option:: --shared + + Network segment range is shared with other projects + +.. option:: --project + + Network segment range owner (name or ID). Optional when the segment + range is shared + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. option:: --physical-network + + Physical network name of this network segment range + +.. option:: --network-type + + Network type of this network segment range + (geneve, gre, vlan or vxlan) + +.. option:: --minimum + + Minimum segment identifier for this network segment range which is based + on the network type, VLAN ID for vlan network type and tunnel ID for + geneve, gre and vxlan network types + +.. option:: --maximum + + Maximum segment identifier for this network segment range which is based + on the network type, VLAN ID for vlan network type and tunnel ID for + geneve, gre and vxlan network types + +.. _network_segment_range_create-name: +.. describe:: + + Name of new network segment range + +network segment range delete +---------------------------- + +Delete network segment range(s) + +.. program:: network segment range delete +.. code:: bash + + openstack network segment range delete + [ ...] + +.. _network_segment_range_delete-network-segment-range: +.. describe:: + + Network segment range (s) to delete (name or ID) + +network segment range list +-------------------------- + +List network segment ranges + +.. program:: network segment range list +.. code:: bash + + openstack network segment range list + [--long] + [--used | --unused] + [--available | --unavailable] + +.. option:: --long + + List additional fields in output + +.. option:: --used + + List network segment ranges that have segments in use + +.. option:: --unused + + List network segment ranges that do not have segments not in use + +.. option:: --available + + List network segment ranges that have available segments + +.. option:: --unavailable + + List network segment ranges without available segments + +network segment range set +------------------------- + +Set network segment range properties + +.. program:: network segment range set +.. code:: bash + + openstack network segment range set + [--name ] + [--minimum ] + [--maximum ] + + +.. option:: --name + + Set network segment range name + +.. option:: --minimum + + Set network segment range minimum segment identifier + +.. option:: --maximum + + Set network segment range maximum segment identifier + +.. _network_segment_range_set-network-segment-range: +.. describe:: + + Network segment range to modify (name or ID) + +network segment range show +-------------------------- + +Display network segment range details + +.. program:: network segment range show +.. code:: bash + + openstack network segment range show + + +.. _network_segment_range_show-network-segment-range: +.. describe:: + + Network segment range to display (name or ID) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index cdd5e63c95..e302fdad05 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -125,6 +125,7 @@ referring to both Compute and Volume quotas. * ``network qos policy``: (**Network**) - a QoS policy for network resources * ``network qos rule type``: (**Network**) - list of QoS available rule types * ``network segment``: (**Network**) - a segment of a virtual network +* ``network segment range``: (**Network**) - a segment range for tenant network segment allocation * ``network service provider``: (**Network**) - a driver providing a network service * ``object``: (**Object Storage**) a single file in the Object Storage * ``object store account``: (**Object Storage**) owns a group of Object Storage resources diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py new file mode 100644 index 0000000000..f5c8ccbc60 --- /dev/null +++ b/openstackclient/network/v2/network_segment_range.py @@ -0,0 +1,458 @@ +# Copyright (c) 2019, Intel Corporation. +# All Rights Reserved. +# +# 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. +# + +"""Network segment action implementations""" + +import itertools +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) + + +def _get_ranges(item): + item = [int(i) if isinstance(i, six.string_types) else i for i in item] + for a, b in itertools.groupby(enumerate(item), lambda xy: xy[1] - xy[0]): + b = list(b) + yield "%s-%s" % (b[0][1], b[-1][1]) if b[0][1] != b[-1][1] else \ + str(b[0][1]) + + +def _hack_tuple_value_update_by_index(tup, index, value): + lot = list(tup) + lot[index] = value + return tuple(lot) + + +def _is_prop_empty(columns, props, prop_name): + return True if not props[columns.index(prop_name)] else False + + +def _exchange_dict_keys_with_values(orig_dict): + updated_dict = dict() + for k, v in six.iteritems(orig_dict): + k = [k] + if not updated_dict.get(v): + updated_dict[v] = k + else: + updated_dict[v].extend(k) + return updated_dict + + +def _update_available_from_props(columns, props): + index_available = columns.index('available') + props = _hack_tuple_value_update_by_index( + props, index_available, list(_get_ranges(props[index_available]))) + return props + + +def _update_used_from_props(columns, props): + index_used = columns.index('used') + updated_used = _exchange_dict_keys_with_values(props[index_used]) + for k, v in six.iteritems(updated_used): + updated_used[k] = list(_get_ranges(v)) + props = _hack_tuple_value_update_by_index( + props, index_used, updated_used) + return props + + +def _update_additional_fields_from_props(columns, props): + props = _update_available_from_props(columns, props) + props = _update_used_from_props(columns, props) + return props + + +class CreateNetworkSegmentRange(command.ShowOne): + _description = _("Create new network segment range") + + def get_parser(self, prog_name): + parser = super(CreateNetworkSegmentRange, self).get_parser(prog_name) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + "--private", + dest="private", + action="store_true", + help=_('Network segment range is assigned specifically to the ' + 'project'), + ) + shared_group.add_argument( + "--shared", + dest="shared", + action="store_true", + help=_('Network segment range is shared with other projects'), + ) + parser.add_argument( + 'name', + metavar='', + help=_('Name of new network segment range') + ) + parser.add_argument( + '--project', + metavar='', + help=_('Network segment range owner (name or ID). Optional when ' + 'the segment range is shared'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--network-type', + metavar='', + choices=['geneve', 'gre', 'vlan', 'vxlan'], + required=True, + help=_('Network type of this network segment range ' + '(geneve, gre, vlan or vxlan)'), + ) + parser.add_argument( + '--physical-network', + metavar='', + help=_('Physical network name of this network segment range'), + ) + parser.add_argument( + '--minimum', + metavar='', + type=int, + required=True, + help=_('Minimum segment identifier for this network segment ' + 'range which is based on the network type, VLAN ID for ' + 'vlan network type and tunnel ID for geneve, gre and vxlan ' + 'network types'), + ) + parser.add_argument( + '--maximum', + metavar='', + type=int, + required=True, + help=_('Maximum segment identifier for this network segment ' + 'range which is based on the network type, VLAN ID for ' + 'vlan network type and tunnel ID for geneve, gre and vxlan ' + 'network types'), + ) + + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + # Verify that the extension exists. + network_client.find_extension('network-segment-range', + ignore_missing=False) + except Exception as e: + msg = (_('Network segment range create not supported by ' + 'Network API: %(e)s') % {'e': e}) + raise exceptions.CommandError(msg) + + identity_client = self.app.client_manager.identity + + if parsed_args.shared and parsed_args.project: + msg = _("--project is only allowed with --private") + raise exceptions.CommandError(msg) + + if (parsed_args.network_type.lower() != 'vlan' and + parsed_args.physical_network): + msg = _("--physical-network is only allowed with --network-type " + "vlan") + raise exceptions.CommandError(msg) + + attrs = {} + if parsed_args.shared or parsed_args.private: + attrs['shared'] = parsed_args.shared + else: + # default to be ``shared`` if not specified + attrs['shared'] = True + attrs['network_type'] = parsed_args.network_type + attrs['minimum'] = parsed_args.minimum + attrs['maximum'] = parsed_args.maximum + if parsed_args.name: + attrs['name'] = parsed_args.name + + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + if project_id: + attrs['project_id'] = project_id + else: + msg = (_("Failed to create the network segment range for " + "project %(project_id)s") % parsed_args.project_id) + raise exceptions.CommandError(msg) + elif not parsed_args.shared: + # default to the current project if no project specified and shared + # is not specified. + # Get the project id from the current auth. + attrs['project_id'] = self.app.client_manager.auth_ref.project_id + else: + attrs['project_id'] = None + + if parsed_args.physical_network: + attrs['physical_network'] = parsed_args.physical_network + obj = network_client.create_network_segment_range(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + data = _update_additional_fields_from_props(columns, props=data) + return (display_columns, data) + + +class DeleteNetworkSegmentRange(command.Command): + _description = _("Delete network segment range(s)") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkSegmentRange, self).get_parser(prog_name) + parser.add_argument( + 'network_segment_range', + metavar='', + nargs='+', + help=_('Network segment range(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + # Verify that the extension exists. + network_client.find_extension('network-segment-range', + ignore_missing=False) + except Exception as e: + msg = (_('Network segment range delete not supported by ' + 'Network API: %(e)s') % {'e': e}) + raise exceptions.CommandError(msg) + + result = 0 + for network_segment_range in parsed_args.network_segment_range: + try: + obj = network_client.find_network_segment_range( + network_segment_range, ignore_missing=False) + network_client.delete_network_segment_range(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete network segment range with " + "ID '%(network_segment_range)s': %(e)s"), + {'network_segment_range': network_segment_range, + 'e': e}) + + if result > 0: + total = len(parsed_args.network_segment_range) + msg = (_("%(result)s of %(total)s network segment ranges failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListNetworkSegmentRange(command.Lister): + _description = _("List network segment ranges") + + def get_parser(self, prog_name): + parser = super(ListNetworkSegmentRange, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_('List additional fields in output'), + ) + used_group = parser.add_mutually_exclusive_group() + used_group.add_argument( + '--used', + action='store_true', + help=_('List network segment ranges that have segments in use'), + ) + used_group.add_argument( + '--unused', + action='store_true', + help=_('List network segment ranges that have segments ' + 'not in use'), + ) + available_group = parser.add_mutually_exclusive_group() + available_group.add_argument( + '--available', + action='store_true', + help=_('List network segment ranges that have available segments'), + ) + available_group.add_argument( + '--unavailable', + action='store_true', + help=_('List network segment ranges without available segments'), + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + # Verify that the extension exists. + network_client.find_extension('network-segment-range', + ignore_missing=False) + except Exception as e: + msg = (_('Network segment ranges list not supported by ' + 'Network API: %(e)s') % {'e': e}) + raise exceptions.CommandError(msg) + + filters = {} + data = network_client.network_segment_ranges(**filters) + + headers = ( + 'ID', + 'Name', + 'Default', + 'Shared', + 'Project ID', + 'Network Type', + 'Physical Network', + 'Minimum ID', + 'Maximum ID' + ) + columns = ( + 'id', + 'name', + 'default', + 'shared', + 'project_id', + 'network_type', + 'physical_network', + 'minimum', + 'maximum', + ) + if parsed_args.available or parsed_args.unavailable or \ + parsed_args.used or parsed_args.unused: + # If one of `--available`, `--unavailable`, `--used`, + # `--unused` is specified, we assume that additional fields + # should be listed in output. + parsed_args.long = True + if parsed_args.long: + headers = headers + ( + 'Used', + 'Available', + ) + columns = columns + ( + 'used', + 'available', + ) + + display_props = tuple() + for s in data: + props = utils.get_item_properties(s, columns) + if parsed_args.available and \ + _is_prop_empty(columns, props, 'available') or \ + parsed_args.unavailable and \ + not _is_prop_empty(columns, props, 'available') or \ + parsed_args.used and _is_prop_empty(columns, props, 'used') or \ + parsed_args.unused and \ + not _is_prop_empty(columns, props, 'used'): + continue + if parsed_args.long: + props = _update_additional_fields_from_props(columns, props) + display_props += (props,) + + return headers, display_props + + +class SetNetworkSegmentRange(command.Command): + _description = _("Set network segment range properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkSegmentRange, self).get_parser(prog_name) + parser.add_argument( + 'network_segment_range', + metavar='', + help=_('Network segment range to modify (name or ID)'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Set network segment name'), + ) + parser.add_argument( + '--minimum', + metavar='', + type=int, + help=_('Set network segment range minimum segment identifier'), + ) + parser.add_argument( + '--maximum', + metavar='', + type=int, + help=_('Set network segment range maximum segment identifier'), + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + # Verify that the extension exists. + network_client.find_extension('network-segment-range', + ignore_missing=False) + except Exception as e: + msg = (_('Network segment range set not supported by ' + 'Network API: %(e)s') % {'e': e}) + raise exceptions.CommandError(msg) + + if (parsed_args.minimum and not parsed_args.maximum) or \ + (parsed_args.maximum and not parsed_args.minimum): + msg = _("--minimum and --maximum are both required") + raise exceptions.CommandError(msg) + + obj = network_client.find_network_segment_range( + parsed_args.network_segment_range, ignore_missing=False) + attrs = {} + if parsed_args.name: + attrs['name'] = parsed_args.name + if parsed_args.minimum: + attrs['minimum'] = parsed_args.minimum + if parsed_args.maximum: + attrs['maximum'] = parsed_args.maximum + network_client.update_network_segment_range(obj, **attrs) + + +class ShowNetworkSegmentRange(command.ShowOne): + _description = _("Display network segment range details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkSegmentRange, self).get_parser(prog_name) + parser.add_argument( + 'network_segment_range', + metavar='', + help=_('Network segment range to display (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + # Verify that the extension exists. + network_client.find_extension('network-segment-range', + ignore_missing=False) + except Exception as e: + msg = (_('Network segment range show not supported by ' + 'Network API: %(e)s') % {'e': e}) + raise exceptions.CommandError(msg) + + obj = network_client.find_network_segment_range( + parsed_args.network_segment_range, + ignore_missing=False + ) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + data = _update_additional_fields_from_props(columns, props=data) + return (display_columns, data) diff --git a/openstackclient/tests/functional/network/v2/test_network_segment_range.py b/openstackclient/tests/functional/network/v2/test_network_segment_range.py new file mode 100644 index 0000000000..95402dcb3f --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_segment_range.py @@ -0,0 +1,145 @@ +# Copyright (c) 2019, Intel Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import json +import uuid + +from openstackclient.tests.functional.network.v2 import common + + +class NetworkSegmentRangeTests(common.NetworkTests): + """Functional tests for network segment range""" + + def setUp(self): + super(NetworkSegmentRangeTests, self).setUp() + # Nothing in this class works with Nova Network + if not self.haz_network: + self.skipTest("No Network service present") + self.PROJECT_NAME = uuid.uuid4().hex + + def test_network_segment_range_create_delete(self): + # Make a project + project_id = json.loads(self.openstack( + 'project create -f json ' + self.PROJECT_NAME))['id'] + name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + ' network segment range create -f json ' + + '--private ' + + "--project " + self.PROJECT_NAME + " " + + '--network-type vxlan ' + + '--minimum 2018 ' + + '--maximum 2055 ' + + name + )) + self.assertEqual( + name, + json_output["name"], + ) + self.assertEqual( + project_id, + json_output["project_id"], + ) + + raw_output = self.openstack( + 'network segment range delete ' + name, + ) + self.assertOutput('', raw_output) + raw_output_project = self.openstack( + 'project delete ' + self.PROJECT_NAME) + self.assertEqual('', raw_output_project) + + def test_network_segment_range_list(self): + name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + ' network segment range create -f json ' + + '--shared ' + + '--network-type geneve ' + + '--minimum 2018 ' + + '--maximum 2055 ' + + name + )) + network_segment_range_id = json_output.get('id') + network_segment_range_name = json_output.get('name') + self.addCleanup( + self.openstack, + 'network segment range delete ' + network_segment_range_id + ) + self.assertEqual( + name, + json_output["name"], + ) + + json_output = json.loads(self.openstack( + 'network segment list -f json' + )) + item_map = { + item.get('ID'): item.get('Name') for item in json_output + } + self.assertIn(network_segment_range_id, item_map.keys()) + self.assertIn(network_segment_range_name, item_map.values()) + + def test_network_segment_range_set_show(self): + project_id = json.loads(self.openstack( + 'project create -f json ' + self.PROJECT_NAME))['id'] + name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + ' network segment range create -f json ' + + '--private ' + + "--project " + self.PROJECT_NAME + " " + + '--network-type geneve ' + + '--minimum 2018 ' + + '--maximum 2055 ' + + name + )) + self.addCleanup( + self.openstack, + 'network segment range delete ' + name + ) + self.assertEqual( + name, + json_output["name"], + ) + self.assertEqual( + project_id, + json_output["project_id"], + ) + + new_minimum = '2010' + new_maximum = '2060' + cmd_output = self.openstack( + 'network segment range set ' + + '--minimum ' + new_minimum + ' ' + + '--maximum ' + new_maximum + ' ' + + name + ) + self.assertOutput('', cmd_output) + + json_output = json.loads(self.openstack( + 'network segment range show -f json ' + + name + )) + self.assertEqual( + new_minimum, + json_output["minimum"], + ) + self.assertEqual( + new_maximum, + json_output["maximum"], + ) + + raw_output_project = self.openstack( + 'project delete ' + self.PROJECT_NAME) + self.assertEqual('', raw_output_project) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 28e92d1196..38c865dae2 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -538,6 +538,66 @@ def create_network_segments(attrs=None, count=2): return network_segments +class FakeNetworkSegmentRange(object): + """Fake one or more network segment ranges.""" + + @staticmethod + def create_one_network_segment_range(attrs=None): + """Create a fake network segment range. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the network segment range + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuid.uuid4().hex + network_segment_range_attrs = { + 'id': 'network-segment-range-id-' + fake_uuid, + 'name': 'network-segment-name-' + fake_uuid, + 'default': False, + 'shared': False, + 'project_id': 'project-id-' + fake_uuid, + 'network_type': 'vlan', + 'physical_network': 'physical-network-name-' + fake_uuid, + 'minimum': 100, + 'maximum': 106, + 'used': {104: '3312e4ba67864b2eb53f3f41432f8efc', + 106: '3312e4ba67864b2eb53f3f41432f8efc'}, + 'available': [100, 101, 102, 103, 105], + } + + # Overwrite default attributes. + network_segment_range_attrs.update(attrs) + + network_segment_range = fakes.FakeResource( + info=copy.deepcopy(network_segment_range_attrs), + loaded=True + ) + + return network_segment_range + + @staticmethod + def create_network_segment_ranges(attrs=None, count=2): + """Create multiple fake network segment ranges. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of network segment ranges to fake + :return: + A list of FakeResource objects faking the network segment ranges + """ + network_segment_ranges = [] + for i in range(0, count): + network_segment_ranges.append( + FakeNetworkSegmentRange.create_one_network_segment_range(attrs) + ) + return network_segment_ranges + + class FakePort(object): """Fake one or more ports.""" diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py new file mode 100644 index 0000000000..6387a28194 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -0,0 +1,552 @@ +# Copyright (c) 2019, Intel Corporation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import mock +from mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_segment_range +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestNetworkSegmentRange(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkSegmentRange, self).setUp() + + # Get a shortcut to the network client + self.network = self.app.client_manager.network + + +class TestCreateNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment range to create. + _network_segment_range = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range() + + columns = ( + 'available', + 'default', + 'id', + 'maximum', + 'minimum', + 'name', + 'network_type', + 'physical_network', + 'project_id', + 'shared', + 'used', + ) + + data = ( + ['100-103', '105'], + _network_segment_range.default, + _network_segment_range.id, + _network_segment_range.maximum, + _network_segment_range.minimum, + _network_segment_range.name, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.project_id, + _network_segment_range.shared, + {'3312e4ba67864b2eb53f3f41432f8efc': ['104', '106']}, + ) + + def setUp(self): + super(TestCreateNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.create_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + + # Get the command object to test + self.cmd = network_segment_range.CreateNetworkSegmentRange( + self.app, + self.namespace + ) + + def test_create_no_options(self): + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, [], []) + + def test_create_invalid_network_type(self): + arglist = [ + '--private', + '--project', self._network_segment_range.project_id, + '--network-type', 'foo', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, []) + + def test_create_shared_with_project_id(self): + arglist = [ + '--shared', + '--project', self._network_segment_range.project_id, + '--network-type', 'vxlan', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', True), + ('project', self._network_segment_range.project_id), + ('network_type', 'vxlan'), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_create_tunnel_with_physical_network(self): + arglist = [ + '--shared', + '--network-type', 'vxlan', + '--physical-network', self._network_segment_range.physical_network, + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', True), + ('network_type', 'vxlan'), + ('physical_network', self._network_segment_range.physical_network), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_create_minimum_options(self): + arglist = [ + '--private', + '--project', self._network_segment_range.project_id, + '--network-type', self._network_segment_range.network_type, + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', self._network_segment_range.shared), + ('project', self._network_segment_range.project_id), + ('network_type', self._network_segment_range.network_type), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_network_segment_range.assert_called_once_with(**{ + 'shared': self._network_segment_range.shared, + 'project_id': mock.ANY, + 'network_type': self._network_segment_range.network_type, + 'minimum': self._network_segment_range.minimum, + 'maximum': self._network_segment_range.maximum, + 'name': self._network_segment_range.name, + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--private', + '--project', self._network_segment_range.project_id, + '--network-type', self._network_segment_range.network_type, + '--physical-network', self._network_segment_range.physical_network, + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', self._network_segment_range.shared), + ('project', self._network_segment_range.project_id), + ('network_type', self._network_segment_range.network_type), + ('physical_network', self._network_segment_range.physical_network), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_network_segment_range.assert_called_once_with(**{ + 'shared': self._network_segment_range.shared, + 'project_id': mock.ANY, + 'network_type': self._network_segment_range.network_type, + 'physical_network': self._network_segment_range.physical_network, + 'minimum': self._network_segment_range.minimum, + 'maximum': self._network_segment_range.maximum, + 'name': self._network_segment_range.name, + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment ranges to delete. + _network_segment_ranges = \ + network_fakes.FakeNetworkSegmentRange.create_network_segment_ranges() + + def setUp(self): + super(TestDeleteNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.delete_network_segment_range = mock.Mock( + return_value=None) + self.network.find_network_segment_range = mock.Mock( + side_effect=self._network_segment_ranges + ) + + # Get the command object to test + self.cmd = network_segment_range.DeleteNetworkSegmentRange( + self.app, + self.namespace + ) + + def test_delete(self): + arglist = [ + self._network_segment_ranges[0].id, + ] + verifylist = [ + ('network_segment_range', [self._network_segment_ranges[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.delete_network_segment_range.assert_called_once_with( + self._network_segment_ranges[0] + ) + self.assertIsNone(result) + + def test_delete_multiple(self): + arglist = [] + for _network_segment_range in self._network_segment_ranges: + arglist.append(_network_segment_range.id) + verifylist = [ + ('network_segment_range', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for _network_segment_range in self._network_segment_ranges: + calls.append(call(_network_segment_range)) + self.network.delete_network_segment_range.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_with_exception(self): + arglist = [ + self._network_segment_ranges[0].id, + 'doesnotexist' + ] + verifylist = [ + ('network_segment_range', + [self._network_segment_ranges[0].id, 'doesnotexist']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._network_segment_ranges[0], + exceptions.CommandError] + self.network.find_network_segment_range = ( + mock.Mock(side_effect=find_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 network segment ranges failed to delete.', + str(e)) + + self.network.find_network_segment_range.assert_any_call( + self._network_segment_ranges[0].id, ignore_missing=False) + self.network.find_network_segment_range.assert_any_call( + 'doesnotexist', ignore_missing=False) + self.network.delete_network_segment_range.assert_called_once_with( + self._network_segment_ranges[0] + ) + + +class TestListNetworkSegmentRange(TestNetworkSegmentRange): + _network_segment_ranges = network_fakes.FakeNetworkSegmentRange.\ + create_network_segment_ranges(count=3) + + columns = ( + 'ID', + 'Name', + 'Default', + 'Shared', + 'Project ID', + 'Network Type', + 'Physical Network', + 'Minimum ID', + 'Maximum ID' + ) + columns_long = columns + ( + 'Used', + 'Available', + ) + + data = [] + for _network_segment_range in _network_segment_ranges: + data.append(( + _network_segment_range.id, + _network_segment_range.name, + _network_segment_range.default, + _network_segment_range.shared, + _network_segment_range.project_id, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.minimum, + _network_segment_range.maximum, + )) + + data_long = [] + for _network_segment_range in _network_segment_ranges: + data_long.append(( + _network_segment_range.id, + _network_segment_range.name, + _network_segment_range.default, + _network_segment_range.shared, + _network_segment_range.project_id, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.minimum, + _network_segment_range.maximum, + {'3312e4ba67864b2eb53f3f41432f8efc': ['104', '106']}, + ['100-103', '105'], + )) + + def setUp(self): + super(TestListNetworkSegmentRange, self).setUp() + + # Get the command object to test + self.cmd = network_segment_range.ListNetworkSegmentRange( + self.app, self.namespace) + + self.network.find_extension = mock.Mock() + self.network.network_segment_ranges = mock.Mock( + return_value=self._network_segment_ranges) + + def test_list_no_option(self): + arglist = [] + verifylist = [ + ('long', False), + ('available', False), + ('unavailable', False), + ('used', False), + ('unused', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.network_segment_ranges.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ('available', False), + ('unavailable', False), + ('used', False), + ('unused', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.network_segment_ranges.assert_called_once_with() + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + +class TestSetNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment range to set. + _network_segment_range = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range() + # The network segment range updated. + minimum_updated = _network_segment_range.minimum - 5 + maximum_updated = _network_segment_range.maximum + 5 + available_updated = (list(range(minimum_updated, 104)) + [105] + + list(range(107, maximum_updated + 1))) + _network_segment_range_updated = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range( + attrs={'minimum': minimum_updated, + 'maximum': maximum_updated, + 'used': {104: '3312e4ba67864b2eb53f3f41432f8efc', + 106: '3312e4ba67864b2eb53f3f41432f8efc'}, + 'available': available_updated} + ) + + def setUp(self): + super(TestSetNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.find_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + + # Get the command object to test + self.cmd = network_segment_range.SetNetworkSegmentRange(self.app, + self.namespace) + + def test_set_no_options(self): + arglist = [ + self._network_segment_range.id, + ] + verifylist = [ + ('network_segment_range', self._network_segment_range.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.network.update_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + result = self.cmd.take_action(parsed_args) + + self.network.update_network_segment_range.assert_called_once_with( + self._network_segment_range, **{} + ) + self.assertIsNone(result) + + def test_set_all_options(self): + arglist = [ + '--name', 'new name', + '--minimum', str(self.minimum_updated), + '--maximum', str(self.maximum_updated), + self._network_segment_range.id, + ] + verifylist = [ + ('name', 'new name'), + ('minimum', self.minimum_updated), + ('maximum', self.maximum_updated), + ('network_segment_range', self._network_segment_range.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.network.update_network_segment_range = mock.Mock( + return_value=self._network_segment_range_updated + ) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'name': 'new name', + 'minimum': self.minimum_updated, + 'maximum': self.maximum_updated, + } + self.network.update_network_segment_range.assert_called_once_with( + self._network_segment_range, **attrs + ) + self.assertIsNone(result) + + +class TestShowNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment range to show. + _network_segment_range = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range() + + columns = ( + 'available', + 'default', + 'id', + 'maximum', + 'minimum', + 'name', + 'network_type', + 'physical_network', + 'project_id', + 'shared', + 'used', + ) + + data = ( + ['100-103', '105'], + _network_segment_range.default, + _network_segment_range.id, + _network_segment_range.maximum, + _network_segment_range.minimum, + _network_segment_range.name, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.project_id, + _network_segment_range.shared, + {'3312e4ba67864b2eb53f3f41432f8efc': ['104', '106']}, + ) + + def setUp(self): + super(TestShowNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.find_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + + # Get the command object to test + self.cmd = network_segment_range.ShowNetworkSegmentRange( + self.app, self.namespace) + + def test_show_no_options(self): + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, [], []) + + def test_show_all_options(self): + arglist = [ + self._network_segment_range.id, + ] + verifylist = [ + ('network_segment_range', self._network_segment_range.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_network_segment_range.assert_called_once_with( + self._network_segment_range.id, + ignore_missing=False + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml b/releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml new file mode 100644 index 0000000000..4ff4f57536 --- /dev/null +++ b/releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add ``network segment range create``, ``network segment range delete``, + ``network segment range list``, ``network segment range show`` and + ``network segment range set`` commands. + [Blueprint `network-segment-range-management `_] diff --git a/setup.cfg b/setup.cfg index 48c2247ff7..c73f2ce9f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -463,6 +463,12 @@ openstack.network.v2 = network_segment_set = openstackclient.network.v2.network_segment:SetNetworkSegment network_segment_show = openstackclient.network.v2.network_segment:ShowNetworkSegment + network_segment_range_create = openstackclient.network.v2.network_segment_range:CreateNetworkSegmentRange + network_segment_range_delete = openstackclient.network.v2.network_segment_range:DeleteNetworkSegmentRange + network_segment_range_list = openstackclient.network.v2.network_segment_range:ListNetworkSegmentRange + network_segment_range_set = openstackclient.network.v2.network_segment_range:SetNetworkSegmentRange + network_segment_range_show = openstackclient.network.v2.network_segment_range:ShowNetworkSegmentRange + network_service_provider_list = openstackclient.network.v2.network_service_provider:ListNetworkServiceProvider port_create = openstackclient.network.v2.port:CreatePort From 626a3a021c50759841804eeb9aad832ea9e57551 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 8 Jun 2018 16:59:08 -0400 Subject: [PATCH 0058/1120] Mention compute API 2.50 in openstack quota show --class There is a bug in the compute API until microversion 2.50 where the server-groups and server-group-members class quota fields aren't returned. This just mentions that microversion in the command help text. Change-Id: I029a614a922d642c578618c478c4d0a29a394fc2 Task: 21490 Story: 2002194 --- doc/source/cli/command-objects/quota.rst | 4 +++- openstackclient/common/quota.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/source/cli/command-objects/quota.rst b/doc/source/cli/command-objects/quota.rst index c538cfb3dd..8c8cea6ddd 100644 --- a/doc/source/cli/command-objects/quota.rst +++ b/doc/source/cli/command-objects/quota.rst @@ -244,7 +244,9 @@ Set quotas for class quota show ---------- -Show quotas for project or 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. .. program:: quota show .. code:: bash diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index dba6873ff9..dae1b18e39 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -570,7 +570,10 @@ def take_action(self, parsed_args): class ShowQuota(command.ShowOne, BaseQuota): - _description = _("Show quotas for project or class") + _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.") def get_parser(self, prog_name): parser = super(ShowQuota, self).get_parser(prog_name) From 28c06d06885b3ae93da07eb14411d92c3df7e792 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Wed, 13 Mar 2019 01:37:31 +0800 Subject: [PATCH 0059/1120] Fix: set invalid None project_id on range creation "project_id" attribute should not be set to None on shared network segment range creation since it is not a valid string type which is required for the API. Change-Id: Ia2bab12e39b4bb7e05ff2acfffb851252c100651 Story: 2005205 Task: 29975 --- .../network/v2/network_segment_range.py | 2 - .../network/v2/test_network_segment_range.py | 42 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index f5c8ccbc60..75820bc5ef 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -207,8 +207,6 @@ def take_action(self, parsed_args): # is not specified. # Get the project id from the current auth. attrs['project_id'] = self.app.client_manager.auth_ref.project_id - else: - attrs['project_id'] = None if parsed_args.physical_network: attrs['physical_network'] = parsed_args.physical_network diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index 6387a28194..63257086cd 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -144,19 +144,19 @@ def test_create_tunnel_with_physical_network(self): self.cmd.take_action, parsed_args) - def test_create_minimum_options(self): + def test_create_private_minimum_options(self): arglist = [ '--private', '--project', self._network_segment_range.project_id, - '--network-type', self._network_segment_range.network_type, + '--network-type', 'vxlan', '--minimum', str(self._network_segment_range.minimum), '--maximum', str(self._network_segment_range.maximum), self._network_segment_range.name, ] verifylist = [ - ('shared', self._network_segment_range.shared), + ('shared', False), ('project', self._network_segment_range.project_id), - ('network_type', self._network_segment_range.network_type), + ('network_type', 'vxlan'), ('minimum', self._network_segment_range.minimum), ('maximum', self._network_segment_range.maximum), ('name', self._network_segment_range.name), @@ -166,9 +166,39 @@ def test_create_minimum_options(self): columns, data = self.cmd.take_action(parsed_args) self.network.create_network_segment_range.assert_called_once_with(**{ - 'shared': self._network_segment_range.shared, + 'shared': False, 'project_id': mock.ANY, - 'network_type': self._network_segment_range.network_type, + 'network_type': 'vxlan', + 'minimum': self._network_segment_range.minimum, + 'maximum': self._network_segment_range.maximum, + 'name': self._network_segment_range.name, + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_shared_minimum_options(self): + arglist = [ + '--shared', + '--network-type', 'vxlan', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', True), + ('network_type', 'vxlan'), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_network_segment_range.assert_called_once_with(**{ + 'shared': True, + 'network_type': 'vxlan', 'minimum': self._network_segment_range.minimum, 'maximum': self._network_segment_range.maximum, 'name': self._network_segment_range.name, From 7741347041b078ca4d687597897194d7797d202d Mon Sep 17 00:00:00 2001 From: Glenn Van de Water Date: Fri, 8 Mar 2019 16:31:23 +0100 Subject: [PATCH 0060/1120] Fix service discovery in functional tests If a required service is not enabled then we skip the test. The discovery is done by tests/functional/base.py:is_service_enabled but this method is broken, credentials are not passed to the 'openstack service show' command so every call will fail and every test that relies on it will be skipped. This commit fixed that method and the issues that popped up when re-enabling tests. Network segment range: - issue where we assumed network-segment-range extension is always present - issue where we compare integers and string representations of numbers Subnet: - issue where we try to deepcopy an uncopyable object in UnsetSubnet Change-Id: Id3cc907c1ed2a25b49cf6f4a7233e0401a02383a Story: 2005169 Task: 29908 --- openstackclient/network/v2/subnet.py | 19 +++++----- openstackclient/tests/functional/base.py | 35 ++++++++++--------- .../tests/functional/common/test_extension.py | 2 +- .../tests/functional/common/test_quota.py | 2 +- .../functional/compute/v2/test_server.py | 3 +- .../tests/functional/network/v2/common.py | 2 +- .../network/v2/test_network_segment_range.py | 14 ++++---- .../tests/functional/object/v1/common.py | 2 +- .../tests/functional/volume/v1/common.py | 3 +- 9 files changed, 41 insertions(+), 41 deletions(-) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 5f8113bb8d..0733f37c8a 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -681,29 +681,30 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) - tmp_obj = copy.deepcopy(obj) + attrs = {} if parsed_args.dns_nameservers: - _update_arguments(tmp_obj.dns_nameservers, + attrs['dns_nameservers'] = copy.deepcopy(obj.dns_nameservers) + _update_arguments(attrs['dns_nameservers'], parsed_args.dns_nameservers, 'dns-nameserver') - attrs['dns_nameservers'] = tmp_obj.dns_nameservers if parsed_args.host_routes: + attrs['host_routes'] = copy.deepcopy(obj.host_routes) _update_arguments( - tmp_obj.host_routes, + attrs['host_routes'], convert_entries_to_nexthop(parsed_args.host_routes), 'host-route') - attrs['host_routes'] = tmp_obj.host_routes if parsed_args.allocation_pools: - _update_arguments(tmp_obj.allocation_pools, + attrs['allocation_pools'] = copy.deepcopy(obj.allocation_pools) + _update_arguments(attrs['allocation_pools'], parsed_args.allocation_pools, 'allocation-pool') - attrs['allocation_pools'] = tmp_obj.allocation_pools + if parsed_args.service_types: - _update_arguments(tmp_obj.service_types, + attrs['service_types'] = copy.deepcopy(obj.service_types) + _update_arguments(attrs['service_types'], parsed_args.service_types, 'service-type') - attrs['service_types'] = tmp_obj.service_types if attrs: client.update_subnet(obj, **attrs) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 7705c65539..1414e6bbb7 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -11,7 +11,6 @@ # under the License. import os -import re import shlex import subprocess @@ -30,8 +29,6 @@ def execute(cmd, fail_ok=False, merge_stderr=False): """Executes specified command for the given action.""" cmdlist = shlex.split(cmd) - result = '' - result_err = '' stdout = subprocess.PIPE stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr) @@ -43,22 +40,8 @@ def execute(cmd, fail_ok=False, merge_stderr=False): return result -def is_service_enabled(service): - """Ask client cloud if service is available""" - try: - ret = execute('openstack service show -f value -c enabled ' + service) - except exceptions.CommandFailed: - # We get here for multiple reasons, all of them mean that a working - # service is not available - return False - - return "True" in ret - - class TestCase(testtools.TestCase): - delimiter_line = re.compile('^\+\-[\+\-]+\-\+$') - @classmethod def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): """Executes openstackclient command for the given action.""" @@ -66,6 +49,24 @@ def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): 'openstack --os-cloud={cloud} '.format(cloud=cloud) + cmd, fail_ok=fail_ok) + @classmethod + def is_service_enabled(cls, service): + """Ask client cloud if service is available""" + cmd = ('service show -f value -c enabled {service}' + .format(service=service)) + try: + return "True" in cls.openstack(cmd) + except exceptions.CommandFailed as e: + if "No service with a type, name or ID of" in str(e): + return False + else: + raise # Unable to determine if service is enabled + + @classmethod + def is_extension_enabled(cls, alias): + """Ask client cloud if extension is enabled""" + return alias in cls.openstack('extension list -f value -c Alias') + @classmethod def get_openstack_configuration_value(cls, configuration): opts = cls.get_opts([configuration]) diff --git a/openstackclient/tests/functional/common/test_extension.py b/openstackclient/tests/functional/common/test_extension.py index db50855f71..92efabefe9 100644 --- a/openstackclient/tests/functional/common/test_extension.py +++ b/openstackclient/tests/functional/common/test_extension.py @@ -26,7 +26,7 @@ class ExtensionTests(base.TestCase): @classmethod def setUpClass(cls): super(ExtensionTests, cls).setUpClass() - cls.haz_network = base.is_service_enabled('network') + cls.haz_network = cls.is_service_enabled('network') def test_extension_list_compute(self): """Test compute extension list""" diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 859422812b..9c05746077 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -27,7 +27,7 @@ class QuotaTests(base.TestCase): @classmethod def setUpClass(cls): super(QuotaTests, cls).setUpClass() - cls.haz_network = base.is_service_enabled('network') + cls.haz_network = cls.is_service_enabled('network') cls.PROJECT_NAME =\ cls.get_openstack_configuration_value('auth.project_name') diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 3cb72d9f9f..c8fb44d380 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -16,7 +16,6 @@ from tempest.lib import exceptions -from openstackclient.tests.functional import base from openstackclient.tests.functional.compute.v2 import common from openstackclient.tests.functional.volume.v2 import common as volume_common @@ -27,7 +26,7 @@ class ServerTests(common.ComputeTestCase): @classmethod def setUpClass(cls): super(ServerTests, cls).setUpClass() - cls.haz_network = base.is_service_enabled('network') + cls.haz_network = cls.is_service_enabled('network') def test_server_list(self): """Test server list, set""" diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py index a18bc48faf..5243ecd0b9 100644 --- a/openstackclient/tests/functional/network/v2/common.py +++ b/openstackclient/tests/functional/network/v2/common.py @@ -22,7 +22,7 @@ class NetworkTests(base.TestCase): @classmethod def setUpClass(cls): super(NetworkTests, cls).setUpClass() - cls.haz_network = base.is_service_enabled('network') + cls.haz_network = cls.is_service_enabled('network') class NetworkTagTests(NetworkTests): diff --git a/openstackclient/tests/functional/network/v2/test_network_segment_range.py b/openstackclient/tests/functional/network/v2/test_network_segment_range.py index 95402dcb3f..c42f8744c1 100644 --- a/openstackclient/tests/functional/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/functional/network/v2/test_network_segment_range.py @@ -28,6 +28,8 @@ def setUp(self): # Nothing in this class works with Nova Network if not self.haz_network: self.skipTest("No Network service present") + if not self.is_extension_enabled('network-segment-range'): + self.skipTest("No network-segment-range extension present") self.PROJECT_NAME = uuid.uuid4().hex def test_network_segment_range_create_delete(self): @@ -83,7 +85,7 @@ def test_network_segment_range_list(self): ) json_output = json.loads(self.openstack( - 'network segment list -f json' + 'network segment range list -f json' )) item_map = { item.get('ID'): item.get('Name') for item in json_output @@ -117,13 +119,11 @@ def test_network_segment_range_set_show(self): json_output["project_id"], ) - new_minimum = '2010' - new_maximum = '2060' + new_minimum = 2010 + new_maximum = 2060 cmd_output = self.openstack( - 'network segment range set ' + - '--minimum ' + new_minimum + ' ' + - '--maximum ' + new_maximum + ' ' + - name + 'network segment range set --minimum {min} --maximum {max} {name}' + .format(min=new_minimum, max=new_maximum, name=name) ) self.assertOutput('', cmd_output) diff --git a/openstackclient/tests/functional/object/v1/common.py b/openstackclient/tests/functional/object/v1/common.py index 44771aaabe..b013343027 100644 --- a/openstackclient/tests/functional/object/v1/common.py +++ b/openstackclient/tests/functional/object/v1/common.py @@ -19,4 +19,4 @@ class ObjectStoreTests(base.TestCase): @classmethod def setUpClass(cls): super(ObjectStoreTests, cls).setUpClass() - cls.haz_object_store = base.is_service_enabled('object-store') + cls.haz_object_store = cls.is_service_enabled('object-store') diff --git a/openstackclient/tests/functional/volume/v1/common.py b/openstackclient/tests/functional/volume/v1/common.py index bb3c674ea4..04eb1f48ae 100644 --- a/openstackclient/tests/functional/volume/v1/common.py +++ b/openstackclient/tests/functional/volume/v1/common.py @@ -12,7 +12,6 @@ import fixtures -from openstackclient.tests.functional import base from openstackclient.tests.functional.volume import base as volume_base @@ -25,7 +24,7 @@ def setUpClass(cls): # TODO(dtroyer): This needs to be updated to specifically check for # Volume v1 rather than just 'volume', but for now # that is enough until we get proper version negotiation - cls.haz_volume_v1 = base.is_service_enabled('volume') + cls.haz_volume_v1 = cls.is_service_enabled('volume') def setUp(self): super(BaseVolumeTests, self).setUp() From 510e9a7b8e567ce644473cad9e10651ff4bf2ca1 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Wed, 13 Mar 2019 02:43:59 +0800 Subject: [PATCH 0061/1120] Fix: incorrect check when no shared/private input When neither of "--shared" and "--private" is input, we should not allow to specify "--project". Defaulting the created network segment range to shared is expected. Therefore, "project_id" attr should only be populated on a private range creation. Change-Id: Iab345e1651dd8b7904ff64a20633f194d719bb84 Story: 2005206 Task: 29980 --- .../network/v2/network_segment_range.py | 4 +- .../network/v2/test_network_segment_range.py | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index 75820bc5ef..567b5b6eb9 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -168,7 +168,7 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - if parsed_args.shared and parsed_args.project: + if not parsed_args.private and parsed_args.project: msg = _("--project is only allowed with --private") raise exceptions.CommandError(msg) @@ -202,7 +202,7 @@ def take_action(self, parsed_args): msg = (_("Failed to create the network segment range for " "project %(project_id)s") % parsed_args.project_id) raise exceptions.CommandError(msg) - elif not parsed_args.shared: + elif not attrs['shared']: # default to the current project if no project specified and shared # is not specified. # Get the project id from the current auth. diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index 63257086cd..22e25df133 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -98,6 +98,27 @@ def test_create_invalid_network_type(self): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) + def test_create_default_with_project_id(self): + arglist = [ + '--project', self._network_segment_range.project_id, + '--network-type', 'vxlan', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('project', self._network_segment_range.project_id), + ('network_type', 'vxlan'), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + def test_create_shared_with_project_id(self): arglist = [ '--shared', @@ -144,6 +165,34 @@ def test_create_tunnel_with_physical_network(self): self.cmd.take_action, parsed_args) + def test_create_minimum_options(self): + arglist = [ + '--network-type', 'vxlan', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('network_type', 'vxlan'), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_network_segment_range.assert_called_once_with(**{ + 'shared': True, + 'network_type': 'vxlan', + 'minimum': self._network_segment_range.minimum, + 'maximum': self._network_segment_range.maximum, + 'name': self._network_segment_range.name, + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + def test_create_private_minimum_options(self): arglist = [ '--private', From c684fd926a9b4d54bda147fe306d1324b3805d19 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 22 Mar 2019 00:46:00 +0000 Subject: [PATCH 0062/1120] Update master for stable/stein Add file to the reno documentation build to show release notes for stable/stein. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/stein. Change-Id: I3edfae7c1c5f8268186455efc7add28dc38810fb Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/stein.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/stein.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 19df6740de..9e2d8ce10b 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + stein rocky queens pike diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 0000000000..efaceb667b --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein From c53de3214ed74ffd5b53e6d1cf8a0c0fa73dac99 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Mon, 1 Apr 2019 15:41:17 -0400 Subject: [PATCH 0063/1120] Ignore case in security group rule --ethertype Currently, this only allows 'IPv4' or 'IPv6', but one can imagine a user frequently typing e.g. 'ipv6' and getting frustrated. Allow any case, while still keeping correct case for the choices and the value sent to Neutron. Change-Id: I70ce1f43d32aad01b174437d03c984a5b608b161 --- .../network/v2/security_group_rule.py | 9 +++++++ .../v2/test_security_group_rule_network.py | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index ca0e00b9d8..961125a9a7 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -73,6 +73,14 @@ def _convert_to_lowercase(string): return string.lower() +def _convert_ipvx_case(string): + if string.lower() == 'ipv4': + return 'IPv4' + if string.lower() == 'ipv6': + return 'IPv6' + return string + + def _is_icmp_protocol(protocol): # NOTE(rtheis): Neutron has deprecated protocol icmpv6. # However, while the OSC CLI doesn't document the protocol, @@ -183,6 +191,7 @@ def update_parser_network(self, parser): '--ethertype', metavar='', choices=['IPv4', 'IPv6'], + type=_convert_ipvx_case, help=_("Ethertype of network traffic " "(IPv4, IPv6; default: based on IP protocol)") ) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index fe6d364928..b070ab6aab 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -125,6 +125,30 @@ def test_create_bad_ethertype(self): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) + def test_lowercase_ethertype(self): + arglist = [ + '--ethertype', 'ipv4', + self._security_group.id, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.assertEqual('IPv4', parsed_args.ethertype) + + def test_lowercase_v6_ethertype(self): + arglist = [ + '--ethertype', 'ipv6', + self._security_group.id, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.assertEqual('IPv6', parsed_args.ethertype) + + def test_proper_case_ethertype(self): + arglist = [ + '--ethertype', 'IPv6', + self._security_group.id, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.assertEqual('IPv6', parsed_args.ethertype) + def test_create_all_protocol_options(self): arglist = [ '--protocol', 'tcp', From 589026cdd4707556058b5bebf341e2af5062bed4 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 2 Apr 2019 08:36:33 -0500 Subject: [PATCH 0064/1120] Volume backup functional test tweak Waiting for status in all the wrong places... Change-Id: I531ee6e0c00b623c6fd30d40df1f1f36bf86233f Signed-off-by: Dean Troyer --- openstackclient/tests/functional/volume/v2/test_backup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/functional/volume/v2/test_backup.py b/openstackclient/tests/functional/volume/v2/test_backup.py index e4890b00ea..8f5c032c0a 100644 --- a/openstackclient/tests/functional/volume/v2/test_backup.py +++ b/openstackclient/tests/functional/volume/v2/test_backup.py @@ -39,14 +39,15 @@ def test_volume_backup_restore(self): '--size 1 ' + vol_id )) + self.wait_for_status("volume", vol_id, "available") + # create a backup backup = json.loads(self.openstack( 'volume backup create -f json ' + vol_id )) - - self.wait_for_status("volume", vol_id, "available") self.wait_for_status("backup", backup['id'], "available") + # restore the backup backup_restored = json.loads(self.openstack( 'volume backup restore -f json %s %s' From 415b48056d9d021e04ec972029040a89a6b13928 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Thu, 7 Jun 2018 16:40:58 -0700 Subject: [PATCH 0065/1120] Before writing object data to stdout, re-open it in binary mode Otherwise, you can hit TypeErrors on Python3. Change-Id: I9a891508886feddac3982ce593bd95130392e035 Closes-Bug: 1775482 --- openstackclient/api/object_store_v1.py | 5 +++-- .../tests/unit/object/v1/test_object_all.py | 22 +++++++++++++++++-- .../notes/bug-1775482-7ed2a9a8765b313e.yaml | 6 +++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bug-1775482-7ed2a9a8765b313e.yaml diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 3103352503..d1e5dfaf45 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -378,8 +378,9 @@ def object_save( ) if response.status_code == 200: if file == '-': - for chunk in response.iter_content(64 * 1024): - sys.stdout.write(chunk) + with os.fdopen(sys.stdout.fileno(), 'wb') as f: + for chunk in response.iter_content(64 * 1024): + f.write(chunk) else: if not os.path.exists(os.path.dirname(file)): if len(os.path.dirname(file)) > 0: diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py index 363f2ea21e..08a7534d06 100644 --- a/openstackclient/tests/unit/object/v1/test_object_all.py +++ b/openstackclient/tests/unit/object/v1/test_object_all.py @@ -241,7 +241,25 @@ def test_save_to_stdout(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch('sys.stdout', new=six.BytesIO()) as fake_stdout: + class FakeStdout(six.BytesIO): + def __init__(self): + six.BytesIO.__init__(self) + self.context_manager_calls = [] + + def __enter__(self): + self.context_manager_calls.append('__enter__') + return self + + def __exit__(self, *a): + self.context_manager_calls.append('__exit__') + + with mock.patch('sys.stdout') as fake_stdout, mock.patch( + 'os.fdopen', return_value=FakeStdout()) as fake_fdopen: + fake_stdout.fileno.return_value = 123 self.cmd.take_action(parsed_args) - self.assertEqual(fake_stdout.getvalue(), object_fakes.object_1_content) + self.assertEqual(fake_fdopen.return_value.getvalue(), + object_fakes.object_1_content) + self.assertEqual(fake_fdopen.mock_calls, [mock.call(123, 'wb')]) + self.assertEqual(fake_fdopen.return_value.context_manager_calls, + ['__enter__', '__exit__']) diff --git a/releasenotes/notes/bug-1775482-7ed2a9a8765b313e.yaml b/releasenotes/notes/bug-1775482-7ed2a9a8765b313e.yaml new file mode 100644 index 0000000000..88d9890a76 --- /dev/null +++ b/releasenotes/notes/bug-1775482-7ed2a9a8765b313e.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Re-open stdout in binary mode before writing object data in + ``object save --file -`` command. + [Bug `1775482 `_] From 4f3cda730f9e9cb070e45846b3583ef53971c391 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 12 Apr 2019 13:52:44 -0500 Subject: [PATCH 0066/1120] Tweak network segment range fiunction tests We seem to be having occasional overlaps in the ranges, as they were identical in all tests, change each test to not overlap the others so running in parallel is not racy. Change-Id: I7ea467a3aa2e4a4b4a334c10ea6ba21409c46af0 Signed-off-by: Dean Troyer --- .../network/v2/test_network_segment_range.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openstackclient/tests/functional/network/v2/test_network_segment_range.py b/openstackclient/tests/functional/network/v2/test_network_segment_range.py index c42f8744c1..37c87dd59d 100644 --- a/openstackclient/tests/functional/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/functional/network/v2/test_network_segment_range.py @@ -42,8 +42,8 @@ def test_network_segment_range_create_delete(self): '--private ' + "--project " + self.PROJECT_NAME + " " + '--network-type vxlan ' + - '--minimum 2018 ' + - '--maximum 2055 ' + + '--minimum 2005 ' + + '--maximum 2009 ' + name )) self.assertEqual( @@ -69,8 +69,8 @@ def test_network_segment_range_list(self): ' network segment range create -f json ' + '--shared ' + '--network-type geneve ' + - '--minimum 2018 ' + - '--maximum 2055 ' + + '--minimum 2013 ' + + '--maximum 2017 ' + name )) network_segment_range_id = json_output.get('id') @@ -102,8 +102,8 @@ def test_network_segment_range_set_show(self): '--private ' + "--project " + self.PROJECT_NAME + " " + '--network-type geneve ' + - '--minimum 2018 ' + - '--maximum 2055 ' + + '--minimum 2021 ' + + '--maximum 2025 ' + name )) self.addCleanup( @@ -119,8 +119,8 @@ def test_network_segment_range_set_show(self): json_output["project_id"], ) - new_minimum = 2010 - new_maximum = 2060 + new_minimum = 2020 + new_maximum = 2029 cmd_output = self.openstack( 'network segment range set --minimum {min} --maximum {max} {name}' .format(min=new_minimum, max=new_maximum, name=name) From 8ce203f879ab3be6135d5ce01315bfc15aa70d57 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 16 Apr 2019 18:40:14 -0400 Subject: [PATCH 0067/1120] Fix docs bug link to go to storyboard rather than launchpad This fixes the docs bug link generation for the normal docs and release notes docs. The requirement on openstackdocstheme is bumped to 1.23.2 to pick up fix I2ed164b9b0badade702c50543ac1a5eea4d1867b. Change-Id: I89711a391ee0fb7e40c1fbf83f950e2b582358d9 Story: #2005467 Task: #30546 --- doc/requirements.txt | 2 +- doc/source/conf.py | 3 +-- releasenotes/source/conf.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 1afc73d6a6..a2649afb1f 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ # 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>=1.18.1 # Apache-2.0 +openstackdocstheme>=1.23.2 # Apache-2.0 reno>=2.5.0 # Apache-2.0 sphinx!=1.6.6,!=1.6.7,>=1.6.5 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD diff --git a/doc/source/conf.py b/doc/source/conf.py index 003bfcaa34..45045002aa 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -32,8 +32,7 @@ # openstackdocstheme options repository_name = 'openstack/python-openstackclient' -bug_project = 'python-openstackclient' -bug_tag = '' +use_storyboard = True # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 3c4e4107e1..4060f49c79 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -45,8 +45,7 @@ # openstackdocstheme options repository_name = 'openstack/python-openstackclient' -bug_project = 'python-openstackclient' -bug_tag = '' +use_storyboard = True # Set aliases for extlinks # * lpbug - generic Launchpad bug :lpbug:`123456` From b3da2a67262852b18a522034604d056e7c7c730c Mon Sep 17 00:00:00 2001 From: OpenDev Sysadmins Date: Fri, 19 Apr 2019 19:45:05 +0000 Subject: [PATCH 0068/1120] OpenDev Migration Patch This commit was bulk generated and pushed by the OpenDev sysadmins as a part of the Git hosting and code review systems migration detailed in these mailing list posts: http://lists.openstack.org/pipermail/openstack-discuss/2019-March/003603.html http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004920.html Attempts have been made to correct repository namespaces and hostnames based on simple pattern matching, but it's possible some were updated incorrectly or missed entirely. Please reach out to us via the contact information listed at https://opendev.org/ with any questions you may have. --- .gitreview | 2 +- .zuul.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitreview b/.gitreview index 784c1900ea..4eee726db6 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/python-openstackclient.git diff --git a/.zuul.yaml b/.zuul.yaml index e1ffb0ebe0..69c39a64c4 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -16,7 +16,7 @@ tox_envlist: py27 # Set work dir to openstackclient so that if it's triggered by one of the # other repos the tests will run in the same place - zuul_work_dir: src/git.openstack.org/openstack/python-openstackclient + zuul_work_dir: src/opendev.org/openstack/python-openstackclient - job: name: osc-tox-py27-tips @@ -37,7 +37,7 @@ vars: # Set work dir to openstackclient so that if it's triggered by one of the # other repos the tests will run in the same place - zuul_work_dir: src/git.openstack.org/openstack/python-openstackclient + zuul_work_dir: src/opendev.org/openstack/python-openstackclient - job: name: osc-tox-py35-tips @@ -58,7 +58,7 @@ vars: # Set work dir to openstackclient so that if it's triggered by one of the # other repos the tests will run in the same place - zuul_work_dir: src/git.openstack.org/openstack/python-openstackclient + zuul_work_dir: src/opendev.org/openstack/python-openstackclient - job: name: osc-functional-devstack-base @@ -100,7 +100,7 @@ osc_environment: PYTHONUNBUFFERED: 'true' OS_CLOUD: devstack-admin - zuul_work_dir: src/git.openstack.org/openstack/python-openstackclient + zuul_work_dir: src/opendev.org/openstack/python-openstackclient # The Neutron bits are here rather than in osc-functional-devstack-base to # simplify removing Neutron in the osc-functional-devstack-n-net job. @@ -111,7 +111,7 @@ vars: devstack_plugins: # NOTE(amotoki): Some neutron features are enabled by devstack plugin - neutron: https://git.openstack.org/openstack/neutron + neutron: https://opendev.org/openstack/neutron devstack_services: neutron-network-segment-range: true neutron-segments: true From 0f56b7d07421bb7267a2be4c8e78efbfcc77a81e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 19 Apr 2019 23:01:53 -0500 Subject: [PATCH 0069/1120] Followup opendev cleanup and test jobs * upper-constraints references need s/plain/raw/ Change-Id: I04368dc42f1a62a048ac9d11497747ef6f600515 Signed-off-by: Dean Troyer --- README.rst | 2 +- doc/source/index.rst | 2 +- tox.ini | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 8e37b325f7..e1824aed6d 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ language to describe operations in OpenStack. .. _Launchpad project: https://launchpad.net/python-openstackclient .. _Blueprints: https://blueprints.launchpad.net/python-openstackclient .. _Bugs: https://storyboard.openstack.org/#!/project/975 -.. _Source: https://git.openstack.org/cgit/openstack/python-openstackclient +.. _Source: https://opendev.org/openstack/python-openstackclient .. _Developer: https://docs.openstack.org/project-team-guide/project-setup/python.html .. _Contributing: https://docs.openstack.org/infra/manual/developers.html .. _Testing: https://docs.openstack.org/python-openstackclient/latest/contributor/developing.html#testing diff --git a/doc/source/index.rst b/doc/source/index.rst index a6d1e89606..fe9e66a115 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -60,7 +60,7 @@ on `Launchpad`_. Code may be submitted to the :code:`openstack/python-openstackclient` project using `Gerrit`_. Developers may also be found in the `IRC channel`_ ``#openstack-sdks``. -.. _`on OpenStack's Git server`: https://git.openstack.org/cgit/openstack/python-openstackclient/tree +.. _`on OpenStack's Git server`: https://opendev.org/openstack/python-openstackclient/tree .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Bug reports: https://storyboard.openstack.org/#!/project/975 diff --git a/tox.ini b/tox.ini index e008f9562c..c8854e5c5d 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ setenv = VIRTUAL_ENV={envdir} OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -88,7 +88,7 @@ commands = [testenv:venv] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = {posargs} @@ -112,7 +112,7 @@ commands = [testenv:docs] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -122,7 +122,7 @@ commands = [testenv:releasenotes] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From a8309a2a85e96907d277b9763f13fd1f414f81e7 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Mon, 22 Apr 2019 23:45:06 +0000 Subject: [PATCH 0070/1120] Dropping the py35 testing All the integration testing has been moved to Bionic now[1] and py3.5 is not tested runtime for Train or stable/stein[2]. As per below ML thread, we are good to drop the py35 testing now: http://lists.openstack.org/pipermail/openstack-discuss/2019-April/005097.html [1] http://lists.openstack.org/pipermail/openstack-discuss/2019-April/004647.html [2] https://governance.openstack.org/tc/reference/runtimes/stein.html https://governance.openstack.org/tc/reference/runtimes/train.html Change-Id: Ie7bcc327fd588a1ff6b2556d49017df56bc55bf8 --- .zuul.yaml | 9 ++++----- setup.cfg | 1 - tox.ini | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 69c39a64c4..95a6890c71 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -40,8 +40,8 @@ zuul_work_dir: src/opendev.org/openstack/python-openstackclient - job: - name: osc-tox-py35-tips - parent: openstack-tox-py35 + name: osc-tox-py36-tips + parent: openstack-tox-py36 description: | Run unit tests for OpenStackClient with master branch of important libs. @@ -180,11 +180,11 @@ check: jobs: - osc-tox-py27-tips - - osc-tox-py35-tips + - osc-tox-py36-tips gate: jobs: - osc-tox-py27-tips - - osc-tox-py35-tips + - osc-tox-py36-tips - project: templates: @@ -193,7 +193,6 @@ - openstack-cover-jobs - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python35-jobs - openstack-python36-jobs - openstack-python37-jobs - publish-openstack-docs-pti diff --git a/setup.cfg b/setup.cfg index c73f2ce9f6..db03c48423 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 [files] diff --git a/tox.ini b/tox.ini index c8854e5c5d..8145179ece 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.3 -envlist = py36,py35,py27,pep8 +envlist = py36,py27,pep8 skipdist = True [testenv] From 42cd4b2e40455591de42439352ad1516bc1fa788 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 1 May 2019 17:59:16 -0400 Subject: [PATCH 0071/1120] Document that server dump create requires 2.17 There is no indication to the user in the command help that they have to use 2.17 or greater [1] to run the "openstack server dump create" command. This mentions that requirement in the help of the command. [1] https://developer.openstack.org/api-ref/compute/#trigger-crash-dump-in-server Change-Id: I02c06e10a26eb38ddecb70f970cfcbfad962201c --- openstackclient/compute/v2/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a040e4f088..cb9f8d43eb 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -947,6 +947,8 @@ class CreateServerDump(command.Command): 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. + + This command requires ``--os-compute-api-version`` 2.17 or greater. """ def get_parser(self, prog_name): From 8c1ec6f97f2100e0a689204b7718c3eeed8df83b Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Wed, 1 May 2019 16:48:54 -0600 Subject: [PATCH 0072/1120] Fix link to new opendev repo The switch to an opendev URL wasn't quite right, resulting in a 404. Change-Id: I652f093384a584a56290a9b080913392873efd9f --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index fe9e66a115..abe0803374 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -60,7 +60,7 @@ on `Launchpad`_. Code may be submitted to the :code:`openstack/python-openstackclient` project using `Gerrit`_. Developers may also be found in the `IRC channel`_ ``#openstack-sdks``. -.. _`on OpenStack's Git server`: https://opendev.org/openstack/python-openstackclient/tree +.. _`on OpenStack's Git server`: https://opendev.org/openstack/python-openstackclient/ .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Bug reports: https://storyboard.openstack.org/#!/project/975 From 33a255612c661f174d2cb5d4ca93f8d7096e9290 Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Fri, 10 Nov 2017 10:58:58 -0500 Subject: [PATCH 0073/1120] Change default security group protocol to 'any' The default protocol used to create a security rule was changed to ``tcp``, which was a regression from the neutron client. Change it back to ``any``, which skips sending the protocol to the API server entirely when using the Neutron v2 API. Users that had been creating rules without specifying a protocol and expecting ``tcp`` need to change to use ``--protocol tcp`` explicitly. Change-Id: Iedaa027240e00dced551513d8fa828564386b79f Closes-bug: #1716789 --- .../cli/command-objects/security-group-rule.rst | 6 +++--- .../network/v2/security_group_rule.py | 12 ++++++------ openstackclient/tests/unit/network/v2/fakes.py | 2 +- .../v2/test_security_group_rule_network.py | 4 ++++ .../notes/bug-1716789-abfae897b7e61246.yaml | 17 +++++++++++++++++ 5 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/bug-1716789-abfae897b7e61246.yaml diff --git a/doc/source/cli/command-objects/security-group-rule.rst b/doc/source/cli/command-objects/security-group-rule.rst index 1dbf16d22b..5809e00278 100644 --- a/doc/source/cli/command-objects/security-group-rule.rst +++ b/doc/source/cli/command-objects/security-group-rule.rst @@ -61,8 +61,8 @@ Create a new security group rule IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, - udp, udplite, vrrp and integer representations [0-255]; - default: tcp) + udp, udplite, vrrp and integer representations [0-255] + or any; default: any (all protocols)) *Network version 2* @@ -157,7 +157,7 @@ List security group rules List rules by the IP protocol (ah, dhcp, egp, esp, gre, icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt,ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp and integer - representations [0-255]) + representations [0-255] or any; default: any (all protocols)) *Network version 2* diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 961125a9a7..c93b3af461 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -168,7 +168,7 @@ def update_parser_network(self, parser): "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " "udp, udplite, vrrp and integer representations [0-255] " - "or any; default: tcp)") + "or any; default: any (all protocols))") ) protocol_group.add_argument( '--proto', @@ -233,8 +233,8 @@ def update_parser_compute(self, parser): ) return parser - def _get_protocol(self, parsed_args): - protocol = 'tcp' + def _get_protocol(self, parsed_args, default_protocol='any'): + protocol = default_protocol if parsed_args.protocol is not None: protocol = parsed_args.protocol if parsed_args.proto is not None: @@ -355,7 +355,7 @@ def take_action_network(self, client, parsed_args): def take_action_compute(self, client, parsed_args): group = client.api.security_group_find(parsed_args.group) - protocol = self._get_protocol(parsed_args) + protocol = self._get_protocol(parsed_args, default_protocol='tcp') if protocol == 'icmp': from_port, to_port = -1, -1 else: @@ -462,8 +462,8 @@ def update_parser_network(self, parser): "ah, dhcp, egp, esp, gre, icmp, igmp, " "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " - "udp, udplite, vrrp and integer representations [0-255])." - ) + "udp, udplite, vrrp and integer representations [0-255] " + "or any; default: any (all protocols))") ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 100ea2b1ad..e41621a48e 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1305,7 +1305,7 @@ def create_one_security_group_rule(attrs=None): 'id': 'security-group-rule-id-' + uuid.uuid4().hex, 'port_range_max': None, 'port_range_min': None, - 'protocol': 'tcp', + 'protocol': None, 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'security_group_id': 'security-group-id-' + uuid.uuid4().hex, diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index b070ab6aab..06849112e2 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -177,10 +177,12 @@ def test_create_all_port_range_options(self): def test_create_default_rule(self): self._setup_security_group_rule({ + 'protocol': 'tcp', 'port_range_max': 443, 'port_range_min': 443, }) arglist = [ + '--protocol', 'tcp', '--dst-port', str(self._security_group_rule.port_range_min), self._security_group.id, ] @@ -267,11 +269,13 @@ def test_create_protocol_any(self): def test_create_remote_group(self): self._setup_security_group_rule({ + 'protocol': 'tcp', 'port_range_max': 22, 'port_range_min': 22, 'remote_group_id': self._security_group.id, }) arglist = [ + '--protocol', 'tcp', '--dst-port', str(self._security_group_rule.port_range_min), '--ingress', '--src-group', self._security_group.name, diff --git a/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml b/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml new file mode 100644 index 0000000000..1fd0a13de6 --- /dev/null +++ b/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Change to use ``any`` as the default ``--protocol`` option to + ``security group rule create`` command when using the Neutron v2 API. + [Bug `1716789 `_] +fixes: + - | + The default protocol used to create a security rule was changed to + ``tcp``, which was a regression from the neutron client when using + the Neutron v2 API. Change it back to ``any``, which skips sending + the protocol to the API server entirely. +upgrade: + - | + Users that had been creating rules without specifying a protocol + and expecting ``tcp`` need to change to use ``--protocol tcp`` + explicitly when using the Neutron v2 API. From e6bbc995c5e53d41f3712f1cd032261f4dc12ea4 Mon Sep 17 00:00:00 2001 From: Guang Yee Date: Tue, 7 May 2019 09:34:00 -0700 Subject: [PATCH 0074/1120] document the --timing option Change-Id: I2d13088ea026ac7288213fe808874c4a3a81313a Story: #2005315 Task: #30863 --- doc/source/cli/man/openstack.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/cli/man/openstack.rst b/doc/source/cli/man/openstack.rst index 0745fb7a06..1ba9db0ddd 100644 --- a/doc/source/cli/man/openstack.rst +++ b/doc/source/cli/man/openstack.rst @@ -205,6 +205,10 @@ OPTIONS Show help message and exit +.. option:: --timing + + Print API call timing information + COMMANDS ======== From 04e03b2a1fe34046e2148d3c1d17b9053010c8af Mon Sep 17 00:00:00 2001 From: Jose Castro Leon Date: Thu, 14 Mar 2019 12:33:51 +0000 Subject: [PATCH 0075/1120] Fix bug in endpoint group deletion There is a typo in the endpoint group deletion, due to this you can't remove endpoint groups once assigned. I am adding also the unit tests to avoid this kind of issues in the future Task: 30640 Story: 2005521 Change-Id: Ie938f2c9894bb39b4c0ed1f7aa3a6a751a303058 --- openstackclient/identity/v3/endpoint_group.py | 6 +- .../tests/unit/identity/v3/fakes.py | 62 +++ .../unit/identity/v3/test_endpoint_group.py | 495 ++++++++++++++++++ .../notes/bug-2005521-0274fc26bd9b3842.yaml | 5 + 4 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 openstackclient/tests/unit/identity/v3/test_endpoint_group.py create mode 100644 releasenotes/notes/bug-2005521-0274fc26bd9b3842.yaml diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py index e254973bb5..66bd164d0d 100644 --- a/openstackclient/identity/v3/endpoint_group.py +++ b/openstackclient/identity/v3/endpoint_group.py @@ -204,11 +204,11 @@ def take_action(self, parsed_args): if endpointgroup: # List projects associated to the endpoint group - columns = ('ID', 'Name') + columns = ('ID', 'Name', 'Description') data = client.endpoint_filter.list_projects_for_endpoint_group( endpoint_group=endpointgroup.id) elif project: - columns = ('ID', 'Name') + columns = ('ID', 'Name', 'Description') data = client.endpoint_filter.list_endpoint_groups_for_project( project=project.id) else: @@ -251,7 +251,7 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain) - client.endpoint_filter.delete_endpoint_group_to_project( + client.endpoint_filter.delete_endpoint_group_from_project( endpoint_group=endpointgroup.id, project=project.id) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 27ee9fd026..e5727a6a20 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -235,6 +235,10 @@ 'service_id': service_id, 'region_id': endpoint_region, } +endpoint_group_filters_2 = { + 'region_id': endpoint_region, +} +endpoint_group_file_path = '/tmp/path/to/file' ENDPOINT_GROUP = { 'id': endpoint_group_id, @@ -1044,6 +1048,64 @@ def create_one_endpoint_filter(attrs=None): return endpoint_filter +class FakeEndpointGroup(object): + """Fake one or more endpoint group.""" + + @staticmethod + def create_one_endpointgroup(attrs=None): + """Create a fake endpoint group. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id, url, and so on + """ + + attrs = attrs or {} + + # set default attributes. + endpointgroup_info = { + 'id': 'endpoint-group-id-' + uuid.uuid4().hex, + 'name': 'endpoint-group-name-' + uuid.uuid4().hex, + 'filters': { + 'region': 'region-' + uuid.uuid4().hex, + 'service_id': 'service-id-' + uuid.uuid4().hex, + }, + 'description': 'endpoint-group-description-' + uuid.uuid4().hex, + 'links': 'links-' + uuid.uuid4().hex, + } + endpointgroup_info.update(attrs) + + endpoint = fakes.FakeResource(info=copy.deepcopy(endpointgroup_info), + loaded=True) + return endpoint + + @staticmethod + def create_one_endpointgroup_filter(attrs=None): + """Create a fake endpoint project relationship. + + :param Dictionary attrs: + A dictionary with all attributes of endpointgroup filter + :return: + A FakeResource object with project, endpointgroup and so on + """ + attrs = attrs or {} + + # Set default attribute + endpointgroup_filter_info = { + 'project': 'project-id-' + uuid.uuid4().hex, + 'endpointgroup': 'endpointgroup-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + endpointgroup_filter_info.update(attrs) + + endpointgroup_filter = fakes.FakeModel( + copy.deepcopy(endpointgroup_filter_info)) + + return endpointgroup_filter + + class FakeService(object): """Fake one or more service.""" diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint_group.py b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py new file mode 100644 index 0000000000..6e9da9c78e --- /dev/null +++ b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py @@ -0,0 +1,495 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import mock + +from openstackclient.identity.v3 import endpoint_group +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +class TestEndpointGroup(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestEndpointGroup, self).setUp() + + # Get a shortcut to the EndpointManager Mock + self.endpoint_groups_mock = ( + self.app.client_manager.identity.endpoint_groups + ) + self.endpoint_groups_mock.reset_mock() + self.epf_mock = ( + self.app.client_manager.identity.endpoint_filter + ) + self.epf_mock.reset_mock() + + # Get a shortcut to the ServiceManager Mock + self.services_mock = self.app.client_manager.identity.services + self.services_mock.reset_mock() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + +class TestEndpointGroupCreate(TestEndpointGroup): + + columns = ( + 'description', + 'filters', + 'id', + 'name', + ) + + def setUp(self): + super(TestEndpointGroupCreate, self).setUp() + + self.endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup( + attrs={'filters': identity_fakes.endpoint_group_filters})) + + self.endpoint_groups_mock.create.return_value = self.endpoint_group + + # Get the command object to test + self.cmd = endpoint_group.CreateEndpointGroup(self.app, None) + + def test_endpointgroup_create_no_options(self): + arglist = [ + '--description', self.endpoint_group.description, + self.endpoint_group.name, + identity_fakes.endpoint_group_file_path, + ] + verifylist = [ + ('name', self.endpoint_group.name), + ('filters', identity_fakes.endpoint_group_file_path), + ('description', self.endpoint_group.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.endpoint_group_filters + with mock.patch("openstackclient.identity.v3.endpoint_group." + "CreateEndpointGroup._read_filters", mocker): + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.endpoint_group.name, + 'filters': identity_fakes.endpoint_group_filters, + 'description': self.endpoint_group.description, + } + + self.endpoint_groups_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + datalist = ( + self.endpoint_group.description, + identity_fakes.endpoint_group_filters, + self.endpoint_group.id, + self.endpoint_group.name, + ) + self.assertEqual(datalist, data) + + +class TestEndpointGroupDelete(TestEndpointGroup): + + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + def setUp(self): + super(TestEndpointGroupDelete, self).setUp() + + # This is the return value for utils.find_resource(endpoint) + self.endpoint_groups_mock.get.return_value = self.endpoint_group + self.endpoint_groups_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint_group.DeleteEndpointGroup(self.app, None) + + def test_endpointgroup_delete(self): + arglist = [ + self.endpoint_group.id, + ] + verifylist = [ + ('endpointgroup', [self.endpoint_group.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.endpoint_groups_mock.delete.assert_called_with( + self.endpoint_group.id, + ) + self.assertIsNone(result) + + +class TestEndpointGroupList(TestEndpointGroup): + + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + + columns = ( + 'ID', + 'Name', + 'Description', + ) + + def setUp(self): + super(TestEndpointGroupList, self).setUp() + + self.endpoint_groups_mock.list.return_value = [self.endpoint_group] + self.endpoint_groups_mock.get.return_value = self.endpoint_group + self.epf_mock.list_projects_for_endpoint_group.return_value = [ + self.project] + self.epf_mock.list_endpoint_groups_for_project.return_value = [ + self.endpoint_group] + + # Get the command object to test + self.cmd = endpoint_group.ListEndpointGroup(self.app, None) + + def test_endpoint_group_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + self.endpoint_groups_mock.list.assert_called_with() + + self.assertEqual(self.columns, columns) + datalist = ( + ( + self.endpoint_group.id, + self.endpoint_group.name, + self.endpoint_group.description, + ), + ) + self.assertEqual(datalist, tuple(data)) + + def test_endpoint_group_list_projects_by_endpoint_group(self): + arglist = [ + '--endpointgroup', self.endpoint_group.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ] + 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.epf_mock.list_projects_for_endpoint_group.assert_called_with( + endpoint_group=self.endpoint_group.id + ) + + self.assertEqual(self.columns, columns) + datalist = ( + ( + self.project.id, + self.project.name, + self.project.description, + ), + ) + self.assertEqual(datalist, tuple(data)) + + def test_endpoint_group_list_by_project(self): + self.epf_mock.list_endpoints_for_project.return_value = [ + self.endpoint_group + ] + self.projects_mock.get.return_value = self.project + + arglist = [ + '--project', self.project.name, + '--domain', self.domain.name + ] + verifylist = [ + ('project', self.project.name), + ('domain', self.domain.name), + ] + 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.epf_mock.list_endpoint_groups_for_project.assert_called_with( + project=self.project.id + ) + + self.assertEqual(self.columns, columns) + datalist = ( + ( + self.endpoint_group.id, + self.endpoint_group.name, + self.endpoint_group.description, + ), + ) + self.assertEqual(datalist, tuple(data)) + + +class TestEndpointGroupSet(TestEndpointGroup): + + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + def setUp(self): + super(TestEndpointGroupSet, self).setUp() + + # This is the return value for utils.find_resource(endpoint) + self.endpoint_groups_mock.get.return_value = self.endpoint_group + + self.endpoint_groups_mock.update.return_value = self.endpoint_group + + # Get the command object to test + self.cmd = endpoint_group.SetEndpointGroup(self.app, None) + + def test_endpoint_group_set_no_options(self): + arglist = [ + self.endpoint_group.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + 'name': None, + 'filters': None, + 'description': '' + } + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_endpoint_group_set_name(self): + arglist = [ + '--name', 'qwerty', + self.endpoint_group.id + ] + verifylist = [ + ('name', 'qwerty'), + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'qwerty', + 'filters': None, + 'description': '' + } + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_endpoint_group_set_filters(self): + arglist = [ + '--filters', identity_fakes.endpoint_group_file_path, + self.endpoint_group.id, + ] + verifylist = [ + ('filters', identity_fakes.endpoint_group_file_path), + ('endpointgroup', self.endpoint_group.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.endpoint_group_filters_2 + with mock.patch("openstackclient.identity.v3.endpoint_group." + "SetEndpointGroup._read_filters", mocker): + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': None, + 'filters': identity_fakes.endpoint_group_filters_2, + 'description': '', + } + + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + + self.assertIsNone(result) + + def test_endpoint_group_set_description(self): + arglist = [ + '--description', 'qwerty', + self.endpoint_group.id + ] + verifylist = [ + ('description', 'qwerty'), + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': None, + 'filters': None, + 'description': 'qwerty', + } + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + self.assertIsNone(result) + + +class TestAddProjectToEndpointGroup(TestEndpointGroup): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + new_ep_filter = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup_filter( + attrs={'endpointgroup': endpoint_group.id, + 'project': project.id})) + + def setUp(self): + super(TestAddProjectToEndpointGroup, self).setUp() + + # This is the return value for utils.find_resource() + self.endpoint_groups_mock.get.return_value = self.endpoint_group + + # Update the image_id in the MEMBER dict + self.epf_mock.create.return_value = self.new_ep_filter + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + + # Get the command object to test + self.cmd = endpoint_group.AddProjectToEndpointGroup(self.app, None) + + def test_add_project_to_endpoint_no_option(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.epf_mock.add_endpoint_group_to_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) + + def test_add_project_to_endpoint_with_option(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + '--project-domain', self.domain.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ('project_domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.epf_mock.add_endpoint_group_to_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) + + +class TestRemoveProjectEndpointGroup(TestEndpointGroup): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + def setUp(self): + super(TestRemoveProjectEndpointGroup, self).setUp() + + # This is the return value for utils.find_resource() + self.endpoint_groups_mock.get.return_value = self.endpoint_group + + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + self.epf_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint_group.RemoveProjectFromEndpointGroup( + self.app, None) + + def test_remove_project_endpoint_no_options(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.epf_mock.delete_endpoint_group_from_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) + + def test_remove_project_endpoint_with_options(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + '--project-domain', self.domain.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ('project_domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.epf_mock.delete_endpoint_group_from_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) diff --git a/releasenotes/notes/bug-2005521-0274fc26bd9b3842.yaml b/releasenotes/notes/bug-2005521-0274fc26bd9b3842.yaml new file mode 100644 index 0000000000..2d5cacfbe6 --- /dev/null +++ b/releasenotes/notes/bug-2005521-0274fc26bd9b3842.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix ``endpoint group delete`` command to properly delete endpoint groups. + [Story `2005521 `_] \ No newline at end of file From 6385d64237c9973dd4c7dd53efb6664ea2c719da Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 9 May 2019 16:47:41 -0500 Subject: [PATCH 0076/1120] Blacklist Bandit 1.6.0 due to directory exclusion bug Bandit 1.6.0 introduces a regression[0] with the -x option, a fix is expected to be included in 1.6.1 soon. [0] https://github.com/PyCQA/bandit/issues/488 [1] https://github.com/PyCQA/bandit/pull/489 Change-Id: I110829ef960e3ee146f47871ef076491244bf4fa Signed-off-by: Dean Troyer --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8ec69331b5..4cb66cfdb6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,7 +16,7 @@ stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 -bandit>=1.1.0 # Apache-2.0 +bandit!=1.6.0,>=1.1.0 # Apache-2.0 wrapt>=1.7.0 # BSD License # Install these to generate sphinx autodocs From c44f26eb7e41c28bb13ef9bd31c8ddda9e638862 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Wed, 3 May 2017 14:19:27 +0000 Subject: [PATCH 0077/1120] Use cliff formattable columns in network commands Use cliff formattable columns not to convert complex fields into a string when a machine readable format like JSON or YAML is requested. Partial-Bug: #1687955 Partially implement blueprint osc-formattable-columns Change-Id: I9878f327e39f56852cc0fb6e4eee9105b7141da9 --- openstackclient/network/v2/ip_availability.py | 3 +- openstackclient/network/v2/network.py | 30 +++--- openstackclient/network/v2/network_agent.py | 23 +++-- openstackclient/network/v2/port.py | 31 +++--- openstackclient/network/v2/router.py | 47 +++++---- openstackclient/network/v2/security_group.py | 15 ++- openstackclient/network/v2/subnet.py | 30 +++--- openstackclient/network/v2/subnet_pool.py | 5 +- .../tests/functional/network/v2/common.py | 14 +-- .../functional/network/v2/test_network.py | 18 ++-- .../network/v2/test_network_agent.py | 4 +- .../tests/functional/network/v2/test_port.py | 29 +++--- .../functional/network/v2/test_router.py | 6 +- .../functional/network/v2/test_subnet.py | 4 +- .../functional/network/v2/test_subnet_pool.py | 16 +-- .../unit/network/v2/test_ip_availability.py | 12 +-- .../tests/unit/network/v2/test_network.py | 97 ++++++++++--------- .../unit/network/v2/test_network_agent.py | 26 ++--- .../tests/unit/network/v2/test_port.py | 87 +++++++++-------- .../tests/unit/network/v2/test_router.py | 76 +++++++-------- .../network/v2/test_security_group_compute.py | 15 ++- .../network/v2/test_security_group_network.py | 20 ++-- .../tests/unit/network/v2/test_subnet.py | 92 +++++++++--------- .../tests/unit/network/v2/test_subnet_pool.py | 57 +++++------ openstackclient/tests/unit/utils.py | 17 ++++ 25 files changed, 407 insertions(+), 367 deletions(-) diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index 1d96358054..ddc88e557e 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -13,6 +13,7 @@ """IP Availability Info implementations""" +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -21,7 +22,7 @@ from openstackclient.network import sdk_utils _formatters = { - 'subnet_ip_availability': utils.format_list_of_dicts, + 'subnet_ip_availability': format_columns.ListDictColumn, } diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index f5123932a3..63aec71497 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -13,6 +13,8 @@ """Network action implementations""" +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -23,24 +25,26 @@ from openstackclient.network.v2 import _tag -def _format_admin_state(item): - return 'UP' if item else 'DOWN' +class AdminStateColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'UP' if self._value else 'DOWN' -def _format_router_external(item): - return 'External' if item else 'Internal' +class RouterExternalColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'External' if self._value else 'Internal' _formatters = { - 'subnets': utils.format_list, - 'subnet_ids': utils.format_list, - 'admin_state_up': _format_admin_state, - 'is_admin_state_up': _format_admin_state, - 'router:external': _format_router_external, - 'is_router_external': _format_router_external, - 'availability_zones': utils.format_list, - 'availability_zone_hints': utils.format_list, - 'tags': utils.format_list, + 'subnets': format_columns.ListColumn, + 'subnet_ids': format_columns.ListColumn, + 'admin_state_up': AdminStateColumn, + 'is_admin_state_up': AdminStateColumn, + 'router:external': RouterExternalColumn, + 'is_router_external': RouterExternalColumn, + 'availability_zones': format_columns.ListColumn, + 'availability_zone_hints': format_columns.ListColumn, + 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index 46e8d4b21c..1678485434 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -15,6 +15,8 @@ import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,19 +28,22 @@ LOG = logging.getLogger(__name__) -def _format_alive(alive): - return ":-)" if alive else "XXX" +class AliveColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return ":-)" if self._value else "XXX" -def _format_admin_state(state): - return 'UP' if state else 'DOWN' +class AdminStateColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'UP' if self._value else 'DOWN' + _formatters = { - 'is_alive': _format_alive, - 'alive': _format_alive, - 'admin_state_up': _format_admin_state, - 'is_admin_state_up': _format_admin_state, - 'configurations': utils.format_dict, + 'is_alive': AliveColumn, + 'alive': AliveColumn, + 'admin_state_up': AdminStateColumn, + 'is_admin_state_up': AdminStateColumn, + 'configurations': format_columns.DictColumn, } diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index f6d6fc7280..6094dfc4d3 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -18,6 +18,8 @@ import json import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -33,23 +35,24 @@ LOG = logging.getLogger(__name__) -def _format_admin_state(state): - return 'UP' if state else 'DOWN' +class AdminStateColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'UP' if self._value else 'DOWN' _formatters = { - 'admin_state_up': _format_admin_state, - 'is_admin_state_up': _format_admin_state, - 'allowed_address_pairs': utils.format_list_of_dicts, - 'binding_profile': utils.format_dict, - 'binding_vif_details': utils.format_dict, - 'binding:profile': utils.format_dict, - 'binding:vif_details': utils.format_dict, - 'dns_assignment': utils.format_list_of_dicts, - 'extra_dhcp_opts': utils.format_list_of_dicts, - 'fixed_ips': utils.format_list_of_dicts, - 'security_group_ids': utils.format_list, - 'tags': utils.format_list, + 'admin_state_up': AdminStateColumn, + 'is_admin_state_up': AdminStateColumn, + 'allowed_address_pairs': format_columns.ListDictColumn, + 'binding_profile': format_columns.DictColumn, + 'binding_vif_details': format_columns.DictColumn, + 'binding:profile': format_columns.DictColumn, + 'binding:vif_details': format_columns.DictColumn, + 'dns_assignment': format_columns.ListDictColumn, + 'extra_dhcp_opts': format_columns.ListDictColumn, + 'fixed_ips': format_columns.ListDictColumn, + 'security_group_ids': format_columns.ListColumn, + 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 2ec3e2f094..fd6a24fdb9 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -18,6 +18,8 @@ import json import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -32,33 +34,36 @@ LOG = logging.getLogger(__name__) -def _format_admin_state(state): - return 'UP' if state else 'DOWN' +class AdminStateColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return 'UP' if self._value else 'DOWN' -def _format_router_info(info): - try: - return json.dumps(info) - except (TypeError, KeyError): - return '' +class RouterInfoColumn(cliff_columns.FormattableColumn): + def human_readable(self): + try: + return json.dumps(self._value) + except (TypeError, KeyError): + return '' -def _format_routes(routes): - # Map the route keys to match --route option. - for route in routes: - if 'nexthop' in route: - route['gateway'] = route.pop('nexthop') - return utils.format_list_of_dicts(routes) +class RoutesColumn(cliff_columns.FormattableColumn): + def human_readable(self): + # Map the route keys to match --route option. + for route in self._value: + if 'nexthop' in route: + route['gateway'] = route.pop('nexthop') + return utils.format_list_of_dicts(self._value) _formatters = { - 'admin_state_up': _format_admin_state, - 'is_admin_state_up': _format_admin_state, - 'external_gateway_info': _format_router_info, - 'availability_zones': utils.format_list, - 'availability_zone_hints': utils.format_list, - 'routes': _format_routes, - 'tags': utils.format_list, + 'admin_state_up': AdminStateColumn, + 'is_admin_state_up': AdminStateColumn, + 'external_gateway_info': RouterInfoColumn, + 'availability_zones': format_columns.ListColumn, + 'availability_zone_hints': format_columns.ListColumn, + 'routes': RoutesColumn, + 'tags': format_columns.ListColumn, } @@ -720,7 +725,7 @@ def take_action(self, parsed_args): setattr(obj, 'interfaces_info', interfaces_info) display_columns, columns = _get_columns(obj) - _formatters['interfaces_info'] = _format_router_info + _formatters['interfaces_info'] = RouterInfoColumn data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index ed6c8d7c24..e389473828 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -15,6 +15,7 @@ import argparse +from cliff import columns as cliff_columns from osc_lib.command import command from osc_lib import utils import six @@ -65,13 +66,23 @@ def _format_compute_security_group_rules(sg_rules): return utils.format_list(rules, separator='\n') +class NetworkSecurityGroupRulesColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return _format_network_security_group_rules(self._value) + + +class ComputeSecurityGroupRulesColumn(cliff_columns.FormattableColumn): + def human_readable(self): + return _format_compute_security_group_rules(self._value) + + _formatters_network = { - 'security_group_rules': _format_network_security_group_rules, + 'security_group_rules': NetworkSecurityGroupRulesColumn, } _formatters_compute = { - 'rules': _format_compute_security_group_rules, + 'rules': ComputeSecurityGroupRulesColumn, } diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 0733f37c8a..1f0c2d9478 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -16,6 +16,8 @@ import copy import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -40,23 +42,27 @@ def _update_arguments(obj_list, parsed_args_list, option): raise exceptions.CommandError(msg) -def _format_allocation_pools(data): - pool_formatted = ['%s-%s' % (pool.get('start', ''), pool.get('end', '')) - for pool in data] - return ','.join(pool_formatted) +class AllocationPoolsColumn(cliff_columns.FormattableColumn): + def human_readable(self): + pool_formatted = ['%s-%s' % (pool.get('start', ''), + pool.get('end', '')) + for pool in self._value] + return ','.join(pool_formatted) -def _format_host_routes(data): - # Map the host route keys to match --host-route option. - return utils.format_list_of_dicts(convert_entries_to_gateway(data)) +class HostRoutesColumn(cliff_columns.FormattableColumn): + def human_readable(self): + # Map the host route keys to match --host-route option. + return utils.format_list_of_dicts( + convert_entries_to_gateway(self._value)) _formatters = { - 'allocation_pools': _format_allocation_pools, - 'dns_nameservers': utils.format_list, - 'host_routes': _format_host_routes, - 'service_types': utils.format_list, - 'tags': utils.format_list, + 'allocation_pools': AllocationPoolsColumn, + 'dns_nameservers': format_columns.ListColumn, + 'host_routes': HostRoutesColumn, + 'service_types': format_columns.ListColumn, + 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index ba0b6c452c..7ece263a36 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -41,8 +42,8 @@ def _get_columns(item): _formatters = { - 'prefixes': utils.format_list, - 'tags': utils.format_list, + 'prefixes': format_columns.ListColumn, + 'tags': format_columns.ListColumn, } diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py index 5243ecd0b9..2287f32930 100644 --- a/openstackclient/tests/functional/network/v2/common.py +++ b/openstackclient/tests/functional/network/v2/common.py @@ -62,21 +62,13 @@ def test_tag_operation(self): self._set_resource_and_tag_check('unset', name1, '--all-tag', []) self._set_resource_and_tag_check('set', name2, '--no-tag', []) - def _assertTagsEqual(self, expected, actual): - # TODO(amotoki): Should migrate to cliff format columns. - # At now, unit test assert method needs to be replaced - # to handle format columns, so format_list() is used. - # NOTE: The order of tag is undeterminestic. - actual_tags = filter(bool, actual.split(', ')) - self.assertEqual(set(expected), set(actual_tags)) - def _list_tag_check(self, project_id, expected): cmd_output = json.loads(self.openstack( '{} list --long --project {} -f json'.format(self.base_command, project_id))) for name, tags in expected: net = [n for n in cmd_output if n['Name'] == name][0] - self._assertTagsEqual(tags, net['Tags']) + self.assertEqual(set(tags), set(net['Tags'])) def _create_resource_for_tag_test(self, name, args): return json.loads(self.openstack( @@ -89,7 +81,7 @@ def _create_resource_and_tag_check(self, args, expected): self.addCleanup( self.openstack, '{} delete {}'.format(self.base_command, name)) self.assertIsNotNone(cmd_output["id"]) - self._assertTagsEqual(expected, cmd_output['tags']) + self.assertEqual(set(expected), set(cmd_output['tags'])) return name def _set_resource_and_tag_check(self, command, name, args, expected): @@ -100,4 +92,4 @@ def _set_resource_and_tag_check(self, command, name, args, expected): cmd_output = json.loads(self.openstack( '{} show -f json {}'.format(self.base_command, name) )) - self._assertTagsEqual(expected, cmd_output['tags']) + self.assertEqual(set(expected), set(cmd_output['tags'])) diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py index 1a74496968..71b533ed22 100644 --- a/openstackclient/tests/functional/network/v2/test_network.py +++ b/openstackclient/tests/functional/network/v2/test_network.py @@ -120,14 +120,14 @@ def test_network_create_network(self): cmd_output["description"], ) self.assertEqual( - 'UP', + True, cmd_output["admin_state_up"], ) self.assertFalse( cmd_output["shared"], ) self.assertEqual( - 'Internal', + False, cmd_output["router:external"], ) @@ -231,12 +231,12 @@ def test_network_list(self): ) # Check the default values self.assertEqual( - 'UP', + True, cmd_output["admin_state_up"], ) self.assertFalse(cmd_output["shared"]) self.assertEqual( - 'Internal', + False, cmd_output["router:external"], ) self.assertFalse(cmd_output["is_default"]) @@ -266,7 +266,7 @@ def test_network_list(self): cmd_output["description"], ) self.assertEqual( - 'DOWN', + False, cmd_output["admin_state_up"], ) self.assertTrue(cmd_output["shared"]) @@ -398,12 +398,12 @@ def test_network_set(self): cmd_output["description"], ) self.assertEqual( - 'UP', + True, cmd_output["admin_state_up"], ) self.assertFalse(cmd_output["shared"]) self.assertEqual( - 'Internal', + False, cmd_output["router:external"], ) @@ -432,12 +432,12 @@ def test_network_set(self): cmd_output["description"], ) self.assertEqual( - 'DOWN', + False, cmd_output["admin_state_up"], ) self.assertTrue(cmd_output["shared"]) self.assertEqual( - 'External', + True, cmd_output["router:external"], ) self.assertFalse(cmd_output["is_default"]) diff --git a/openstackclient/tests/functional/network/v2/test_network_agent.py b/openstackclient/tests/functional/network/v2/test_network_agent.py index 86769e0c8d..963227de48 100644 --- a/openstackclient/tests/functional/network/v2/test_network_agent.py +++ b/openstackclient/tests/functional/network/v2/test_network_agent.py @@ -59,7 +59,7 @@ def test_network_agent_list_show_set(self): 'network agent show -f json %s' % agent_ids[0] )) self.assertEqual( - "DOWN", + False, cmd_output['admin_state_up'], ) @@ -72,7 +72,7 @@ def test_network_agent_list_show_set(self): 'network agent show -f json %s' % agent_ids[0] )) self.assertEqual( - "UP", + True, cmd_output['admin_state_up'], ) diff --git a/openstackclient/tests/functional/network/v2/test_port.py b/openstackclient/tests/functional/network/v2/test_port.py index e3067d90a3..a20d2043fd 100644 --- a/openstackclient/tests/functional/network/v2/test_port.py +++ b/openstackclient/tests/functional/network/v2/test_port.py @@ -155,7 +155,7 @@ def test_port_set(self): self.addCleanup(self.openstack, 'port delete %s' % id1) self.assertEqual(name, json_output.get('name')) self.assertEqual('xyzpdq', json_output.get('description')) - self.assertEqual('DOWN', json_output.get('admin_state_up')) + self.assertEqual(False, json_output.get('admin_state_up')) raw_output = self.openstack( 'port set --enable %s' % @@ -166,11 +166,11 @@ def test_port_set(self): json_output = json.loads(self.openstack( 'port show -f json %s' % name )) - sg_id = json_output.get('security_group_ids') + sg_id = json_output.get('security_group_ids')[0] self.assertEqual(name, json_output.get('name')) self.assertEqual('xyzpdq', json_output.get('description')) - self.assertEqual('UP', json_output.get('admin_state_up')) + self.assertEqual(True, json_output.get('admin_state_up')) self.assertIsNotNone(json_output.get('mac_address')) raw_output = self.openstack( @@ -180,7 +180,7 @@ def test_port_set(self): json_output = json.loads(self.openstack( 'port show -f json %s' % name )) - self.assertEqual('', json_output.get('security_group_ids')) + self.assertEqual([], json_output.get('security_group_ids')) def test_port_admin_set(self): """Test create, set (as admin), show, delete""" @@ -229,7 +229,7 @@ def test_port_set_sg(self): id1 = json_output.get('id') self.addCleanup(self.openstack, 'port delete %s' % id1) self.assertEqual(name, json_output.get('name')) - self.assertEqual(sg_id1, json_output.get('security_group_ids')) + self.assertEqual([sg_id1], json_output.get('security_group_ids')) raw_output = self.openstack( 'port set ' @@ -242,16 +242,10 @@ def test_port_set_sg(self): 'port show -f json %s' % name )) self.assertEqual(name, json_output.get('name')) - self.assertIn( - # TODO(dtroyer): output formatters should not mess with JSON! - sg_id1, - json_output.get('security_group_ids'), - ) - self.assertIn( - # TODO(dtroyer): output formatters should not mess with JSON! - sg_id2, - json_output.get('security_group_ids'), - ) + # NOTE(amotoki): The order of the field is not predictable, + self.assertIsInstance(json_output.get('security_group_ids'), list) + self.assertEqual(sorted([sg_id1, sg_id2]), + sorted(json_output.get('security_group_ids'))) raw_output = self.openstack( 'port unset --security-group %s %s' % (sg_id1, id1)) @@ -261,9 +255,8 @@ def test_port_set_sg(self): 'port show -f json %s' % name )) self.assertEqual( - # TODO(dtroyer): output formatters should do this on JSON! - sg_id2, - json_output.get('security_group_ids'), + [sg_id2], + json_output.get('security_group_ids') ) def _create_resource_for_tag_test(self, name, args): diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 9d5beff059..05aad7a013 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -90,7 +90,7 @@ def test_router_list(self): cmd_output["name"], ) self.assertEqual( - "DOWN", + False, cmd_output["admin_state_up"], ) self.assertEqual( @@ -109,7 +109,7 @@ def test_router_list(self): cmd_output["name"], ) self.assertEqual( - "UP", + True, cmd_output["admin_state_up"], ) self.assertEqual( @@ -230,7 +230,7 @@ def test_router_set_show_unset(self): cmd_output["description"], ) self.assertEqual( - 'DOWN', + False, cmd_output["admin_state_up"], ) diff --git a/openstackclient/tests/functional/network/v2/test_subnet.py b/openstackclient/tests/functional/network/v2/test_subnet.py index d5309ee674..38030e0198 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet.py +++ b/openstackclient/tests/functional/network/v2/test_subnet.py @@ -236,7 +236,7 @@ def test_subnet_set_show_unset(self): cmd_output["gateway_ip"], ) self.assertEqual( - 'network:floatingip_agent_gateway', + ['network:floatingip_agent_gateway'], cmd_output["service_types"], ) @@ -253,7 +253,7 @@ def test_subnet_set_show_unset(self): new_name )) self.assertEqual( - '', + [], cmd_output["service_types"], ) diff --git a/openstackclient/tests/functional/network/v2/test_subnet_pool.py b/openstackclient/tests/functional/network/v2/test_subnet_pool.py index 46aa6f1433..dad97f8457 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/functional/network/v2/test_subnet_pool.py @@ -38,7 +38,7 @@ def test_subnet_pool_create_delete(self): cmd_output["name"] ) self.assertEqual( - pool_prefix, + [pool_prefix], cmd_output["prefixes"] ) @@ -50,7 +50,7 @@ def test_subnet_pool_create_delete(self): cmd_output["name"] ) self.assertEqual( - pool_prefix, + [pool_prefix], cmd_output["prefixes"] ) @@ -104,7 +104,7 @@ def test_subnet_pool_list(self): cmd_output["project_id"], ) self.assertEqual( - pool_prefix, + [pool_prefix], cmd_output["prefixes"], ) @@ -126,7 +126,7 @@ def test_subnet_pool_list(self): cmd_output["project_id"], ) self.assertEqual( - pool_prefix, + [pool_prefix], cmd_output["prefixes"], ) @@ -193,7 +193,7 @@ def test_subnet_pool_set_show(self): cmd_output["description"], ) self.assertEqual( - pool_prefix, + [pool_prefix], cmd_output["prefixes"], ) self.assertEqual( @@ -239,9 +239,9 @@ def test_subnet_pool_set_show(self): 'bbbb', cmd_output["description"], ) - self.assertInOutput( - "10.110.0.0/16", - cmd_output["prefixes"], + self.assertEqual( + sorted(["10.110.0.0/16", pool_prefix]), + sorted(cmd_output["prefixes"]), ) self.assertEqual( 8, diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index c7c5a9b49e..21508a8df1 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -13,7 +13,7 @@ import mock -from osc_lib import utils as common_utils +from osc_lib.cli import format_columns from openstackclient.network.v2 import ip_availability from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -75,7 +75,7 @@ def test_list_no_options(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_ip_version(self): arglist = [ @@ -93,7 +93,7 @@ def test_list_ip_version(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_project(self): arglist = [ @@ -113,7 +113,7 @@ def test_list_project(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) class TestShowIPAvailability(TestIPAvailability): @@ -135,7 +135,7 @@ class TestShowIPAvailability(TestIPAvailability): _ip_availability.network_id, _ip_availability.network_name, _ip_availability.tenant_id, - common_utils.format_list( + format_columns.ListDictColumn( _ip_availability.subnet_ip_availability), _ip_availability.total_ips, _ip_availability.used_ips, @@ -176,4 +176,4 @@ def test_show_all_options(self): self._ip_availability.network_name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 0f57f0eec9..5c97c363f5 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -15,8 +15,9 @@ import mock from mock import call + +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.network.v2 import network from openstackclient.tests.unit import fakes @@ -81,9 +82,9 @@ class TestCreateNetworkIdentityV3(TestNetwork): ) data = ( - network._format_admin_state(_network.admin_state_up), - utils.format_list(_network.availability_zone_hints), - utils.format_list(_network.availability_zones), + network.AdminStateColumn(_network.admin_state_up), + format_columns.ListColumn(_network.availability_zone_hints), + format_columns.ListColumn(_network.availability_zones), _network.description, _network.dns_domain, _network.id, @@ -98,11 +99,11 @@ class TestCreateNetworkIdentityV3(TestNetwork): _network.provider_physical_network, _network.provider_segmentation_id, _network.qos_policy_id, - network._format_router_external(_network.is_router_external), + network.RouterExternalColumn(_network.is_router_external), _network.shared, _network.status, - utils.format_list(_network.subnets), - utils.format_list(_network.tags), + format_columns.ListColumn(_network.subnets), + format_columns.ListColumn(_network.tags), ) def setUp(self): @@ -146,7 +147,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -211,7 +212,7 @@ def test_create_all_options(self): 'dns_domain': 'example.org.', }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_other_options(self): arglist = [ @@ -238,7 +239,7 @@ def test_create_other_options(self): 'port_security_enabled': False, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self._network.name] @@ -270,7 +271,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -313,9 +314,9 @@ class TestCreateNetworkIdentityV2(TestNetwork): ) data = ( - network._format_admin_state(_network.admin_state_up), - utils.format_list(_network.availability_zone_hints), - utils.format_list(_network.availability_zones), + network.AdminStateColumn(_network.admin_state_up), + format_columns.ListColumn(_network.availability_zone_hints), + format_columns.ListColumn(_network.availability_zones), _network.description, _network.dns_domain, _network.id, @@ -330,11 +331,11 @@ class TestCreateNetworkIdentityV2(TestNetwork): _network.provider_physical_network, _network.provider_segmentation_id, _network.qos_policy_id, - network._format_router_external(_network.is_router_external), + network.RouterExternalColumn(_network.is_router_external), _network.shared, _network.status, - utils.format_list(_network.subnets), - utils.format_list(_network.tags), + format_columns.ListColumn(_network.subnets), + format_columns.ListColumn(_network.tags), ) def setUp(self): @@ -385,7 +386,7 @@ def test_create_with_project_identityv2(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_domain_identityv2(self): arglist = [ @@ -525,7 +526,7 @@ class TestListNetwork(TestNetwork): data.append(( net.id, net.name, - utils.format_list(net.subnets), + format_columns.ListColumn(net.subnets), )) data_long = [] @@ -535,13 +536,13 @@ class TestListNetwork(TestNetwork): net.name, net.status, net.project_id, - network._format_admin_state(net.admin_state_up), + network.AdminStateColumn(net.admin_state_up), net.shared, - utils.format_list(net.subnets), + format_columns.ListColumn(net.subnets), net.provider_network_type, - network._format_router_external(net.is_router_external), - utils.format_list(net.availability_zones), - utils.format_list(net.tags), + network.RouterExternalColumn(net.is_router_external), + format_columns.ListColumn(net.availability_zones), + format_columns.ListColumn(net.tags), )) def setUp(self): @@ -577,7 +578,7 @@ def test_network_list_no_options(self): self.network.networks.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_external(self): arglist = [ @@ -598,7 +599,7 @@ def test_list_external(self): **{'router:external': True, 'is_router_external': True} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_internal(self): arglist = [ @@ -615,7 +616,7 @@ def test_list_internal(self): **{'router:external': False, 'is_router_external': False} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_long(self): arglist = [ @@ -634,7 +635,7 @@ def test_network_list_long(self): self.network.networks.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_list_name(self): test_name = "fakename" @@ -653,7 +654,7 @@ def test_list_name(self): **{'name': test_name} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_enable(self): arglist = [ @@ -671,7 +672,7 @@ def test_network_list_enable(self): **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_disable(self): arglist = [ @@ -689,7 +690,7 @@ def test_network_list_disable(self): **{'admin_state_up': False, 'is_admin_state_up': False} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -708,7 +709,7 @@ def test_network_list_project(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -727,7 +728,7 @@ def test_network_list_project_domain(self): self.network.networks.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_share(self): arglist = [ @@ -744,7 +745,7 @@ def test_network_list_share(self): **{'shared': True, 'is_shared': True} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_no_share(self): arglist = [ @@ -761,7 +762,7 @@ def test_network_list_no_share(self): **{'shared': False, 'is_shared': False} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_status(self): choices = ['ACTIVE', 'BUILD', 'DOWN', 'ERROR'] @@ -780,7 +781,7 @@ def test_network_list_status(self): **{'status': test_status} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_provider_network_type(self): network_type = self._network[0].provider_network_type @@ -798,7 +799,7 @@ def test_network_list_provider_network_type(self): 'provider_network_type': network_type} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_provider_physical_network(self): physical_network = self._network[0].provider_physical_network @@ -816,7 +817,7 @@ def test_network_list_provider_physical_network(self): 'provider_physical_network': physical_network} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_provider_segment(self): segmentation_id = self._network[0].provider_segmentation_id @@ -834,7 +835,7 @@ def test_network_list_provider_segment(self): 'provider_segmentation_id': segmentation_id} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_list_dhcp_agent(self): arglist = [ @@ -853,7 +854,7 @@ def test_network_list_dhcp_agent(self): *attrs) self.assertEqual(self.columns, columns) - self.assertEqual(list(data), list(self.data)) + self.assertListItemEqual(list(data), list(self.data)) def test_list_with_tag_options(self): arglist = [ @@ -878,7 +879,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) class TestSetNetwork(TestNetwork): @@ -1057,9 +1058,9 @@ class TestShowNetwork(TestNetwork): ) data = ( - network._format_admin_state(_network.admin_state_up), - utils.format_list(_network.availability_zone_hints), - utils.format_list(_network.availability_zones), + network.AdminStateColumn(_network.admin_state_up), + format_columns.ListColumn(_network.availability_zone_hints), + format_columns.ListColumn(_network.availability_zones), _network.description, _network.dns_domain, _network.id, @@ -1074,11 +1075,11 @@ class TestShowNetwork(TestNetwork): _network.provider_physical_network, _network.provider_segmentation_id, _network.qos_policy_id, - network._format_router_external(_network.is_router_external), + network.RouterExternalColumn(_network.is_router_external), _network.shared, _network.status, - utils.format_list(_network.subnets), - utils.format_list(_network.tags), + format_columns.ListColumn(_network.subnets), + format_columns.ListColumn(_network.tags), ) def setUp(self): @@ -1111,7 +1112,7 @@ def test_show_all_options(self): self._network.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestUnsetNetwork(TestNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 709fb1c6cd..8500d08ea6 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -14,8 +14,8 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.network.v2 import network_agent from openstackclient.tests.unit.network.v2 import fakes as network_fakes @@ -207,8 +207,8 @@ class TestListNetworkAgent(TestNetworkAgent): agent.agent_type, agent.host, agent.availability_zone, - network_agent._format_alive(agent.alive), - network_agent._format_admin_state(agent.admin_state_up), + network_agent.AliveColumn(agent.alive), + network_agent.AdminStateColumn(agent.admin_state_up), agent.binary, )) @@ -246,7 +246,7 @@ def test_network_agents_list(self): self.network.agents.assert_called_once_with(**{}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_agents_list_agent_type(self): arglist = [ @@ -263,7 +263,7 @@ def test_network_agents_list_agent_type(self): 'agent_type': 'DHCP agent', }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_agents_list_host(self): arglist = [ @@ -280,7 +280,7 @@ def test_network_agents_list_host(self): 'host': self.network_agents[0].host, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_agents_list_networks(self): arglist = [ @@ -298,7 +298,7 @@ def test_network_agents_list_networks(self): self.network.network_hosting_dhcp_agents.assert_called_once_with( *attrs) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_agents_list_routers(self): arglist = [ @@ -318,7 +318,7 @@ def test_network_agents_list_routers(self): *attrs) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_network_agents_list_routers_with_long_option(self): arglist = [ @@ -343,7 +343,7 @@ def test_network_agents_list_routers_with_long_option(self): router_agent_data = [d + ('',) for d in self.data] self.assertEqual(router_agent_columns, columns) - self.assertEqual(router_agent_data, list(data)) + self.assertListItemEqual(router_agent_data, list(data)) class TestRemoveNetworkFromAgent(TestNetworkAgent): @@ -531,12 +531,12 @@ class TestShowNetworkAgent(TestNetworkAgent): 'id', ) data = ( - network_agent._format_admin_state(_network_agent.is_admin_state_up), + network_agent.AdminStateColumn(_network_agent.admin_state_up), _network_agent.agent_type, - network_agent._format_alive(_network_agent.is_alive), + network_agent.AliveColumn(_network_agent.is_alive), _network_agent.availability_zone, _network_agent.binary, - utils.format_dict(_network_agent.configurations), + format_columns.DictColumn(_network_agent.configurations), _network_agent.host, _network_agent.id, ) @@ -571,4 +571,4 @@ def test_show_all_options(self): self.network.get_agent.assert_called_once_with( self._network_agent.id) self.assertEqual(self.columns, columns) - self.assertEqual(list(self.data), list(data)) + self.assertItemEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 8ac3e54f4a..acf85f4652 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -15,6 +15,7 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -68,22 +69,22 @@ def _get_common_cols_data(fake_port): ) data = ( - port._format_admin_state(fake_port.admin_state_up), - utils.format_list_of_dicts(fake_port.allowed_address_pairs), + port.AdminStateColumn(fake_port.admin_state_up), + format_columns.ListDictColumn(fake_port.allowed_address_pairs), fake_port.binding_host_id, - utils.format_dict(fake_port.binding_profile), - utils.format_dict(fake_port.binding_vif_details), + format_columns.DictColumn(fake_port.binding_profile), + format_columns.DictColumn(fake_port.binding_vif_details), fake_port.binding_vif_type, fake_port.binding_vnic_type, fake_port.data_plane_status, fake_port.description, fake_port.device_id, fake_port.device_owner, - utils.format_list_of_dicts(fake_port.dns_assignment), + format_columns.ListDictColumn(fake_port.dns_assignment), fake_port.dns_domain, fake_port.dns_name, - utils.format_list_of_dicts(fake_port.extra_dhcp_opts), - utils.format_list_of_dicts(fake_port.fixed_ips), + format_columns.ListDictColumn(fake_port.extra_dhcp_opts), + format_columns.ListDictColumn(fake_port.fixed_ips), fake_port.id, fake_port.mac_address, fake_port.name, @@ -91,9 +92,9 @@ def _get_common_cols_data(fake_port): fake_port.port_security_enabled, fake_port.project_id, fake_port.qos_policy_id, - utils.format_list(fake_port.security_group_ids), + format_columns.ListColumn(fake_port.security_group_ids), fake_port.status, - utils.format_list(fake_port.tags), + format_columns.ListColumn(fake_port.tags), fake_port.uplink_status_propagation, ) @@ -141,7 +142,7 @@ def test_create_default_options(self): self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_full_options(self): arglist = [ @@ -199,7 +200,7 @@ def test_create_full_options(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_invalid_json_binding_profile(self): arglist = [ @@ -250,7 +251,7 @@ def test_create_json_binding_profile(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_security_group(self): secgroup = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -279,7 +280,7 @@ def test_create_with_security_group(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_port_with_dns_name(self): arglist = [ @@ -305,7 +306,7 @@ def test_create_port_with_dns_name(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_security_groups(self): sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -335,7 +336,7 @@ def test_create_with_security_groups(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_no_security_groups(self): arglist = [ @@ -361,7 +362,7 @@ def test_create_with_no_security_groups(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_no_fixed_ips(self): arglist = [ @@ -387,7 +388,7 @@ def test_create_with_no_fixed_ips(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_port_with_allowed_address_pair_ipaddr(self): pairs = [{'ip_address': '192.168.1.123'}, @@ -417,7 +418,7 @@ def test_create_port_with_allowed_address_pair_ipaddr(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_port_with_allowed_address_pair(self): pairs = [{'ip_address': '192.168.1.123', @@ -453,7 +454,7 @@ def test_create_port_with_allowed_address_pair(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_port_with_qos(self): qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() @@ -481,7 +482,7 @@ def test_create_port_with_qos(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_port_security_enabled(self): arglist = [ @@ -565,7 +566,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -602,7 +603,7 @@ def _test_create_with_uplink_status_propagation(self, enable=True): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_uplink_status_propagation_enabled(self): self._test_create_with_uplink_status_propagation(enable=True) @@ -719,7 +720,7 @@ class TestListPort(TestPort): prt.id, prt.name, prt.mac_address, - utils.format_list_of_dicts(prt.fixed_ips), + format_columns.ListDictColumn(prt.fixed_ips), prt.status, )) @@ -729,11 +730,11 @@ class TestListPort(TestPort): prt.id, prt.name, prt.mac_address, - utils.format_list_of_dicts(prt.fixed_ips), + format_columns.ListDictColumn(prt.fixed_ips), prt.status, - utils.format_list(prt.security_group_ids), + format_columns.ListColumn(prt.security_group_ids), prt.device_owner, - utils.format_list(prt.tags), + format_columns.ListColumn(prt.tags), )) def setUp(self): @@ -762,7 +763,7 @@ def test_port_list_no_options(self): self.network.ports.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_router_opt(self): arglist = [ @@ -781,7 +782,7 @@ def test_port_list_router_opt(self): 'device_id': 'fake-router-id' }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) @mock.patch.object(utils, 'find_resource') def test_port_list_with_server_option(self, mock_find): @@ -801,7 +802,7 @@ def test_port_list_with_server_option(self, mock_find): device_id=fake_server.id) mock_find.assert_called_once_with(mock.ANY, 'fake-server-name') self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_device_id_opt(self): arglist = [ @@ -820,7 +821,7 @@ def test_port_list_device_id_opt(self): 'device_id': self._ports[0].device_id }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_device_owner_opt(self): arglist = [ @@ -839,7 +840,7 @@ def test_port_list_device_owner_opt(self): 'device_owner': self._ports[0].device_owner }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_all_opt(self): arglist = [ @@ -867,7 +868,7 @@ def test_port_list_all_opt(self): 'mac_address': self._ports[0].mac_address }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_mac_address_opt(self): arglist = [ @@ -886,7 +887,7 @@ def test_port_list_mac_address_opt(self): 'mac_address': self._ports[0].mac_address }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_ip_address(self): ip_address = self._ports[0].fixed_ips[0]['ip_address'] @@ -904,7 +905,7 @@ def test_port_list_fixed_ip_opt_ip_address(self): self.network.ports.assert_called_once_with(**{ 'fixed_ips': ['ip_address=%s' % ip_address]}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_ip_address_substr(self): ip_address_ss = self._ports[0].fixed_ips[0]['ip_address'][:-1] @@ -922,7 +923,7 @@ def test_port_list_fixed_ip_opt_ip_address_substr(self): self.network.ports.assert_called_once_with(**{ 'fixed_ips': ['ip_address_substr=%s' % ip_address_ss]}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_subnet_id(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -942,7 +943,7 @@ def test_port_list_fixed_ip_opt_subnet_id(self): self.network.ports.assert_called_once_with(**{ 'fixed_ips': ['subnet_id=%s' % subnet_id]}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_fixed_ip_opts(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -966,7 +967,7 @@ def test_port_list_fixed_ip_opts(self): 'fixed_ips': ['subnet_id=%s' % subnet_id, 'ip_address=%s' % ip_address]}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_fixed_ips(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -990,7 +991,7 @@ def test_port_list_fixed_ips(self): 'fixed_ips': ['subnet_id=%s' % subnet_id, 'ip_address=%s' % ip_address]}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_port_with_long(self): arglist = [ @@ -1007,7 +1008,7 @@ def test_list_port_with_long(self): self.network.ports.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_port_list_project(self): project = identity_fakes.FakeProject.create_one_project() @@ -1025,7 +1026,7 @@ def test_port_list_project(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_port_list_project_domain(self): project = identity_fakes.FakeProject.create_one_project() @@ -1045,7 +1046,7 @@ def test_port_list_project_domain(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -1070,7 +1071,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) class TestSetPort(TestPort): @@ -1644,7 +1645,7 @@ def test_show_all_options(self): self._port.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestUnsetPort(TestPort): diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 618adf3516..3e5ceac436 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -14,8 +14,8 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils as osc_utils from openstackclient.network.v2 import router from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 @@ -132,19 +132,19 @@ class TestCreateRouter(TestRouter): 'tags', ) data = ( - router._format_admin_state(new_router.admin_state_up), - osc_utils.format_list(new_router.availability_zone_hints), - osc_utils.format_list(new_router.availability_zones), + router.AdminStateColumn(new_router.admin_state_up), + format_columns.ListColumn(new_router.availability_zone_hints), + format_columns.ListColumn(new_router.availability_zones), new_router.description, new_router.distributed, - router._format_router_info(new_router.external_gateway_info), + router.RouterInfoColumn(new_router.external_gateway_info), new_router.ha, new_router.id, new_router.name, new_router.tenant_id, - router._format_routes(new_router.routes), + router.RoutesColumn(new_router.routes), new_router.status, - osc_utils.format_list(new_router.tags), + format_columns.ListColumn(new_router.tags), ) def setUp(self): @@ -184,7 +184,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def _test_create_with_ha_options(self, option, ha): arglist = [ @@ -208,7 +208,7 @@ def _test_create_with_ha_options(self, option, ha): 'ha': ha, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_ha_option(self): self._test_create_with_ha_options('--ha', True) @@ -237,7 +237,7 @@ def _test_create_with_distributed_options(self, option, distributed): 'distributed': distributed, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_distributed_option(self): self._test_create_with_distributed_options('--distributed', True) @@ -268,7 +268,7 @@ def test_create_with_AZ_hints(self): }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self.new_router.name] @@ -301,7 +301,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -422,7 +422,7 @@ class TestListRouter(TestRouter): r.id, r.name, r.status, - router._format_admin_state(r.admin_state_up), + router.AdminStateColumn(r.admin_state_up), r.tenant_id, r.distributed, r.ha, @@ -447,10 +447,10 @@ class TestListRouter(TestRouter): r = routers[i] data_long.append( data[i] + ( - router._format_routes(r.routes), - router._format_router_info(r.external_gateway_info), - osc_utils.format_list(r.availability_zones), - osc_utils.format_list(r.tags), + router.RoutesColumn(r.routes), + router.RouterInfoColumn(r.external_gateway_info), + format_columns.ListColumn(r.availability_zones), + format_columns.ListColumn(r.tags), ) ) data_long_no_az = [] @@ -458,9 +458,9 @@ class TestListRouter(TestRouter): r = routers[i] data_long_no_az.append( data[i] + ( - router._format_routes(r.routes), - router._format_router_info(r.external_gateway_info), - osc_utils.format_list(r.tags), + router.RoutesColumn(r.routes), + router.RouterInfoColumn(r.external_gateway_info), + format_columns.ListColumn(r.tags), ) ) @@ -494,7 +494,7 @@ def test_router_list_no_options(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_router_list_no_ha_no_distributed(self): _routers = network_fakes.FakeRouter.create_routers({ @@ -531,7 +531,7 @@ def test_router_list_long(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_router_list_long_no_az(self): arglist = [ @@ -552,7 +552,7 @@ def test_router_list_long_no_az(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long_no_az, columns) - self.assertEqual(self.data_long_no_az, list(data)) + self.assertListItemEqual(self.data_long_no_az, list(data)) def test_list_name(self): test_name = "fakename" @@ -570,7 +570,7 @@ def test_list_name(self): **{'name': test_name} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_router_list_enable(self): arglist = [ @@ -587,7 +587,7 @@ def test_router_list_enable(self): **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_router_list_disable(self): arglist = [ @@ -605,7 +605,7 @@ def test_router_list_disable(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_router_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -623,7 +623,7 @@ def test_router_list_project(self): self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_router_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -643,7 +643,7 @@ def test_router_list_project_domain(self): self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_router_list_agents_no_args(self): arglist = [ @@ -671,7 +671,7 @@ def test_router_list_agents(self): self.network.agent_hosted_routers( *attrs) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -696,7 +696,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) class TestRemovePortFromRouter(TestRouter): @@ -1260,20 +1260,20 @@ class TestShowRouter(TestRouter): 'tags', ) data = ( - router._format_admin_state(_router.admin_state_up), - osc_utils.format_list(_router.availability_zone_hints), - osc_utils.format_list(_router.availability_zones), + router.AdminStateColumn(_router.admin_state_up), + format_columns.ListColumn(_router.availability_zone_hints), + format_columns.ListColumn(_router.availability_zones), _router.description, _router.distributed, - router._format_router_info(_router.external_gateway_info), + router.RouterInfoColumn(_router.external_gateway_info), _router.ha, _router.id, - router._format_router_info(_router.interfaces_info), + router.RouterInfoColumn(_router.interfaces_info), _router.name, _router.tenant_id, - router._format_routes(_router.routes), + router.RoutesColumn(_router.routes), _router.status, - osc_utils.format_list(_router.tags), + format_columns.ListColumn(_router.tags), ) def setUp(self): @@ -1309,7 +1309,7 @@ def test_show_all_options(self): 'device_id': self._router.id }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_show_no_ha_no_distributed(self): _router = network_fakes.FakeRouter.create_one_router({ diff --git a/openstackclient/tests/unit/network/v2/test_security_group_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_compute.py index c949e2c82d..df36006899 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_compute.py @@ -56,7 +56,7 @@ class TestCreateSecurityGroupCompute(TestSecurityGroupCompute): _security_group['id'], _security_group['name'], _security_group['tenant_id'], - '', + security_group.ComputeSecurityGroupRulesColumn([]), ) def setUp(self): @@ -88,7 +88,7 @@ def test_security_group_create_min_options(self, sg_mock): self._security_group['name'], ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_security_group_create_all_options(self, sg_mock): sg_mock.return_value = self._security_group @@ -109,7 +109,7 @@ def test_security_group_create_all_options(self, sg_mock): self._security_group['description'], ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) @mock.patch( @@ -255,7 +255,7 @@ def test_security_group_list_no_options(self, sg_mock): kwargs = {'search_opts': {'all_tenants': False}} sg_mock.assert_called_once_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_security_group_list_all_projects(self, sg_mock): sg_mock.return_value = self._security_groups @@ -272,7 +272,7 @@ def test_security_group_list_all_projects(self, sg_mock): kwargs = {'search_opts': {'all_tenants': True}} sg_mock.assert_called_once_with(**kwargs) self.assertEqual(self.columns_all_projects, columns) - self.assertEqual(self.data_all_projects, list(data)) + self.assertListItemEqual(self.data_all_projects, list(data)) @mock.patch( @@ -372,8 +372,7 @@ class TestShowSecurityGroupCompute(TestSecurityGroupCompute): _security_group['id'], _security_group['name'], _security_group['tenant_id'], - security_group._format_compute_security_group_rules( - [_security_group_rule]), + security_group.ComputeSecurityGroupRulesColumn([_security_group_rule]), ) def setUp(self): @@ -402,4 +401,4 @@ def test_security_group_show_all_options(self, sg_mock): sg_mock.assert_called_once_with(self._security_group['id']) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index 83208287cc..57698ec586 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -57,7 +57,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.id, _security_group.name, _security_group.project_id, - '', + security_group.NetworkSecurityGroupRulesColumn([]), _security_group.tags, ) @@ -94,7 +94,7 @@ def test_create_min_options(self): 'name': self._security_group.name, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -119,7 +119,7 @@ def test_create_all_options(self): 'tenant_id': self.project.id, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self._security_group.name] @@ -150,7 +150,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -287,7 +287,7 @@ def test_security_group_list_no_options(self): self.network.security_groups.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_security_group_list_all_projects(self): arglist = [ @@ -302,7 +302,7 @@ def test_security_group_list_all_projects(self): self.network.security_groups.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_security_group_list_project(self): project = identity_fakes.FakeProject.create_one_project() @@ -320,7 +320,7 @@ def test_security_group_list_project(self): self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_security_group_list_project_domain(self): project = identity_fakes.FakeProject.create_one_project() @@ -340,7 +340,7 @@ def test_security_group_list_project_domain(self): self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -490,7 +490,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.id, _security_group.name, _security_group.project_id, - security_group._format_network_security_group_rules( + security_group.NetworkSecurityGroupRulesColumn( [_security_group_rule._info]), _security_group.tags, ) @@ -522,7 +522,7 @@ def test_show_all_options(self): self.network.find_security_group.assert_called_once_with( self._security_group.id, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestUnsetSecurityGroupNetwork(TestSecurityGroupNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 39cb4f537d..9903b0423a 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -14,8 +14,8 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.network.v2 import subnet as subnet_v2 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 @@ -131,13 +131,13 @@ def _init_subnet_variables(self): ) self.data = ( - subnet_v2._format_allocation_pools(self._subnet.allocation_pools), + subnet_v2.AllocationPoolsColumn(self._subnet.allocation_pools), self._subnet.cidr, self._subnet.description, - utils.format_list(self._subnet.dns_nameservers), + format_columns.ListColumn(self._subnet.dns_nameservers), self._subnet.enable_dhcp, self._subnet.gateway_ip, - subnet_v2._format_host_routes(self._subnet.host_routes), + subnet_v2.HostRoutesColumn(self._subnet.host_routes), self._subnet.id, self._subnet.ip_version, self._subnet.ipv6_address_mode, @@ -146,20 +146,20 @@ def _init_subnet_variables(self): self._subnet.network_id, self._subnet.project_id, self._subnet.segment_id, - utils.format_list(self._subnet.service_types), + format_columns.ListColumn(self._subnet.service_types), self._subnet.subnetpool_id, - utils.format_list(self._subnet.tags), + format_columns.ListColumn(self._subnet.tags), ) self.data_subnet_pool = ( - subnet_v2._format_allocation_pools( + subnet_v2.AllocationPoolsColumn( self._subnet_from_pool.allocation_pools), self._subnet_from_pool.cidr, self._subnet_from_pool.description, - utils.format_list(self._subnet_from_pool.dns_nameservers), + format_columns.ListColumn(self._subnet_from_pool.dns_nameservers), self._subnet_from_pool.enable_dhcp, self._subnet_from_pool.gateway_ip, - subnet_v2._format_host_routes(self._subnet_from_pool.host_routes), + subnet_v2.HostRoutesColumn(self._subnet_from_pool.host_routes), self._subnet_from_pool.id, self._subnet_from_pool.ip_version, self._subnet_from_pool.ipv6_address_mode, @@ -168,20 +168,20 @@ def _init_subnet_variables(self): self._subnet_from_pool.network_id, self._subnet_from_pool.project_id, self._subnet_from_pool.segment_id, - utils.format_list(self._subnet_from_pool.service_types), + format_columns.ListColumn(self._subnet_from_pool.service_types), self._subnet_from_pool.subnetpool_id, - utils.format_list(self._subnet.tags), + format_columns.ListColumn(self._subnet_from_pool.tags), ) self.data_ipv6 = ( - subnet_v2._format_allocation_pools( + subnet_v2.AllocationPoolsColumn( self._subnet_ipv6.allocation_pools), self._subnet_ipv6.cidr, self._subnet_ipv6.description, - utils.format_list(self._subnet_ipv6.dns_nameservers), + format_columns.ListColumn(self._subnet_ipv6.dns_nameservers), self._subnet_ipv6.enable_dhcp, self._subnet_ipv6.gateway_ip, - subnet_v2._format_host_routes(self._subnet_ipv6.host_routes), + subnet_v2.HostRoutesColumn(self._subnet_ipv6.host_routes), self._subnet_ipv6.id, self._subnet_ipv6.ip_version, self._subnet_ipv6.ipv6_address_mode, @@ -190,9 +190,9 @@ def _init_subnet_variables(self): self._subnet_ipv6.network_id, self._subnet_ipv6.project_id, self._subnet_ipv6.segment_id, - utils.format_list(self._subnet_ipv6.service_types), + format_columns.ListColumn(self._subnet_ipv6.service_types), self._subnet_ipv6.subnetpool_id, - utils.format_list(self._subnet.tags), + format_columns.ListColumn(self._subnet_ipv6.tags), ) def setUp(self): @@ -255,7 +255,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_from_subnet_pool_options(self): # Mock SDK calls for this test. @@ -317,7 +317,7 @@ def test_create_from_subnet_pool_options(self): 'service_types': self._subnet_from_pool.service_types, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data_subnet_pool, data) + self.assertItemEqual(self.data_subnet_pool, data) def test_create_options_subnet_range_ipv6(self): # Mock SDK calls for this test. @@ -390,7 +390,7 @@ def test_create_options_subnet_range_ipv6(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data_ipv6, data) + self.assertItemEqual(self.data_ipv6, data) def test_create_with_network_segment(self): # Mock SDK calls for this test. @@ -424,7 +424,7 @@ def test_create_with_network_segment(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_description(self): # Mock SDK calls for this test. @@ -458,7 +458,7 @@ def test_create_with_description(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [ @@ -497,7 +497,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -625,13 +625,13 @@ class TestListSubnet(TestSubnet): subnet.cidr, subnet.tenant_id, subnet.enable_dhcp, - utils.format_list(subnet.dns_nameservers), - subnet_v2._format_allocation_pools(subnet.allocation_pools), - utils.format_list(subnet.host_routes), + format_columns.ListColumn(subnet.dns_nameservers), + subnet_v2.AllocationPoolsColumn(subnet.allocation_pools), + subnet_v2.HostRoutesColumn(subnet.host_routes), subnet.ip_version, subnet.gateway_ip, - utils.format_list(subnet.service_types), - utils.format_list(subnet.tags), + format_columns.ListColumn(subnet.service_types), + format_columns.ListColumn(subnet.tags), )) def setUp(self): @@ -653,7 +653,7 @@ def test_subnet_list_no_options(self): self.network.subnets.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_long(self): arglist = [ @@ -668,7 +668,7 @@ def test_subnet_list_long(self): self.network.subnets.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_subnet_list_ip_version(self): arglist = [ @@ -684,7 +684,7 @@ def test_subnet_list_ip_version(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_dhcp(self): arglist = [ @@ -700,7 +700,7 @@ def test_subnet_list_dhcp(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_no_dhcp(self): arglist = [ @@ -716,7 +716,7 @@ def test_subnet_list_no_dhcp(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_service_type(self): arglist = [ @@ -731,7 +731,7 @@ def test_subnet_list_service_type(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -749,7 +749,7 @@ def test_subnet_list_project(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_service_type_multiple(self): arglist = [ @@ -767,7 +767,7 @@ def test_subnet_list_service_type_multiple(self): 'network:floatingip_agent_gateway']} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -787,7 +787,7 @@ def test_subnet_list_project_domain(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_network(self): network = network_fakes.FakeNetwork.create_one_network() @@ -805,7 +805,7 @@ def test_subnet_list_network(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_gateway(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -823,7 +823,7 @@ def test_subnet_list_gateway(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_name(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -841,7 +841,7 @@ def test_subnet_list_name(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_list_subnet_range(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -859,7 +859,7 @@ def test_subnet_list_subnet_range(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -1154,13 +1154,13 @@ class TestShowSubnet(TestSubnet): ) data = ( - subnet_v2._format_allocation_pools(_subnet.allocation_pools), + subnet_v2.AllocationPoolsColumn(_subnet.allocation_pools), _subnet.cidr, _subnet.description, - utils.format_list(_subnet.dns_nameservers), + format_columns.ListColumn(_subnet.dns_nameservers), _subnet.enable_dhcp, _subnet.gateway_ip, - utils.format_list(_subnet.host_routes), + subnet_v2.HostRoutesColumn(_subnet.host_routes), _subnet.id, _subnet.ip_version, _subnet.ipv6_address_mode, @@ -1169,9 +1169,9 @@ class TestShowSubnet(TestSubnet): _subnet.network_id, _subnet.tenant_id, _subnet.segment_id, - utils.format_list(_subnet.service_types), + format_columns.ListColumn(_subnet.service_types), _subnet.subnetpool_id, - utils.format_list(_subnet.tags), + format_columns.ListColumn(_subnet.tags), ) def setUp(self): @@ -1206,7 +1206,7 @@ def test_show_all_options(self): self._subnet.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestUnsetSubnet(TestSubnet): diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 3d8f1028e6..2271c08944 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -15,8 +15,9 @@ import mock from mock import call + +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.network.v2 import subnet_pool from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 @@ -73,10 +74,10 @@ class TestCreateSubnetPool(TestSubnetPool): _subnet_pool.max_prefixlen, _subnet_pool.min_prefixlen, _subnet_pool.name, - utils.format_list(_subnet_pool.prefixes), + format_columns.ListColumn(_subnet_pool.prefixes), _subnet_pool.project_id, _subnet_pool.shared, - utils.format_list(_subnet_pool.tags), + format_columns.ListColumn(_subnet_pool.tags), ) def setUp(self): @@ -133,7 +134,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_prefixlen_options(self): arglist = [ @@ -163,7 +164,7 @@ def test_create_prefixlen_options(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_len_negative(self): arglist = [ @@ -201,7 +202,7 @@ def test_create_project_domain(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_address_scope_option(self): arglist = [ @@ -224,7 +225,7 @@ def test_create_address_scope_option(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_default_and_shared_options(self): arglist = [ @@ -250,7 +251,7 @@ def test_create_default_and_shared_options(self): 'shared': True, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_description(self): arglist = [ @@ -273,7 +274,7 @@ def test_create_with_description(self): 'description': self._subnet_pool.description, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_default_quota(self): arglist = [ @@ -294,7 +295,7 @@ def test_create_with_default_quota(self): 'default_quota': 10, }) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [ @@ -328,7 +329,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -441,7 +442,7 @@ class TestListSubnetPool(TestSubnetPool): data.append(( pool.id, pool.name, - utils.format_list(pool.prefixes), + format_columns.ListColumn(pool.prefixes), )) data_long = [] @@ -449,12 +450,12 @@ class TestListSubnetPool(TestSubnetPool): data_long.append(( pool.id, pool.name, - utils.format_list(pool.prefixes), + format_columns.ListColumn(pool.prefixes), pool.default_prefixlen, pool.address_scope_id, pool.is_default, pool.shared, - utils.format_list(pool.tags), + format_columns.ListColumn(pool.tags), )) def setUp(self): @@ -476,7 +477,7 @@ def test_subnet_pool_list_no_option(self): self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_long(self): arglist = [ @@ -491,7 +492,7 @@ def test_subnet_pool_list_long(self): self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_subnet_pool_list_no_share(self): arglist = [ @@ -507,7 +508,7 @@ def test_subnet_pool_list_no_share(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_share(self): arglist = [ @@ -523,7 +524,7 @@ def test_subnet_pool_list_share(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_no_default(self): arglist = [ @@ -539,7 +540,7 @@ def test_subnet_pool_list_no_default(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_default(self): arglist = [ @@ -555,7 +556,7 @@ def test_subnet_pool_list_default(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -573,7 +574,7 @@ def test_subnet_pool_list_project(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -593,7 +594,7 @@ def test_subnet_pool_list_project_domain(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_name(self): subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() @@ -611,7 +612,7 @@ def test_subnet_pool_list_name(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_subnet_pool_list_address_scope(self): addr_scope = network_fakes.FakeAddressScope.create_one_address_scope() @@ -629,7 +630,7 @@ def test_subnet_pool_list_address_scope(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -654,7 +655,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) class TestSetSubnetPool(TestSubnetPool): @@ -969,10 +970,10 @@ class TestShowSubnetPool(TestSubnetPool): _subnet_pool.max_prefixlen, _subnet_pool.min_prefixlen, _subnet_pool.name, - utils.format_list(_subnet_pool.prefixes), + format_columns.ListColumn(_subnet_pool.prefixes), _subnet_pool.tenant_id, _subnet_pool.shared, - utils.format_list(_subnet_pool.tags), + format_columns.ListColumn(_subnet_pool.tags), ) def setUp(self): @@ -1008,7 +1009,7 @@ def test_show_all_options(self): ignore_missing=False ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestUnsetSubnetPool(TestSubnetPool): diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index 926dad87ea..c15d8bbfe2 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -19,6 +19,8 @@ import fixtures import testtools +from cliff import columns as cliff_columns + from openstackclient.tests.unit import fakes @@ -80,3 +82,18 @@ def check_parser(self, cmd, args, verify_args): self.assertIn(attr, parsed_args) self.assertEqual(value, getattr(parsed_args, attr)) return parsed_args + + def assertListItemEqual(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for item_expected, item_actual in zip(expected, actual): + self.assertItemEqual(item_expected, item_actual) + + def assertItemEqual(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for col_expected, col_actual in zip(expected, actual): + if isinstance(col_expected, cliff_columns.FormattableColumn): + self.assertIsInstance(col_actual, col_expected.__class__) + self.assertEqual(col_expected.human_readable(), + col_actual.human_readable()) + else: + self.assertEqual(col_expected, col_actual) From 4b91cd49658bd5a9224976ebd3a6f352d1eef5b0 Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Thu, 9 May 2019 15:08:54 -0400 Subject: [PATCH 0078/1120] Stop leaving temp files after unit test runs test_shell.CLOUD_2 is using an absolute path for a temp file, so leaves /tmp/test_log_file around after the unit tests are run. Use a fixture instead so it's cleaned automatically, which also removes the possibility of two tests using the same file and interfering with each other. Change-Id: If722b860be4010b91635c6d46f634da980e17152 --- .../tests/unit/integ/cli/test_shell.py | 19 +++++++++- openstackclient/tests/unit/test_shell.py | 37 ++++++++++--------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/openstackclient/tests/unit/integ/cli/test_shell.py b/openstackclient/tests/unit/integ/cli/test_shell.py index 70303be398..200f9b1869 100644 --- a/openstackclient/tests/unit/integ/cli/test_shell.py +++ b/openstackclient/tests/unit/integ/cli/test_shell.py @@ -12,6 +12,7 @@ import copy +import fixtures import mock from osc_lib.tests import utils as osc_lib_utils @@ -399,6 +400,16 @@ def setUp(self): test_shell.PUBLIC_1['public-clouds']['megadodo']['auth']['auth_url'] \ = test_base.V3_AUTH_URL + def get_temp_file_path(self, filename): + """Returns an absolute path for a temporary file. + + :param filename: filename + :type filename: string + :returns: absolute file path string + """ + temp_dir = self.useFixture(fixtures.TempDir()) + return temp_dir.join(filename) + @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_vendor_file") @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_config_file") def test_shell_args_precedence_1(self, config_mock, vendor_mock): @@ -408,7 +419,9 @@ def test_shell_args_precedence_1(self, config_mock, vendor_mock): """ def config_mock_return(): - return ('file.yaml', copy.deepcopy(test_shell.CLOUD_2)) + log_file = self.get_temp_file_path('test_log_file') + cloud2 = test_shell.get_cloud(log_file) + return ('file.yaml', cloud2) config_mock.side_effect = config_mock_return def vendor_mock_return(): @@ -478,7 +491,9 @@ def test_shell_args_precedence_2(self, config_mock, vendor_mock): """ def config_mock_return(): - return ('file.yaml', copy.deepcopy(test_shell.CLOUD_2)) + log_file = self.get_temp_file_path('test_log_file') + cloud2 = test_shell.get_cloud(log_file) + return ('file.yaml', cloud2) config_mock.side_effect = config_mock_return def vendor_mock_return(): diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py index dff37f10b7..5d413e7e3c 100644 --- a/openstackclient/tests/unit/test_shell.py +++ b/openstackclient/tests/unit/test_shell.py @@ -70,23 +70,6 @@ } } -CLOUD_2 = { - 'clouds': { - 'megacloud': { - 'cloud': 'megadodo', - 'auth': { - 'project_name': 'heart-o-gold', - 'username': 'zaphod', - }, - 'region_name': 'occ-cloud,krikkit,occ-env', - 'log_file': '/tmp/test_log_file', - 'log_level': 'debug', - 'cert': 'mycert', - 'key': 'mickey', - } - } -} - PUBLIC_1 = { 'public-clouds': { 'megadodo': { @@ -118,6 +101,26 @@ } +def get_cloud(log_file): + CLOUD = { + 'clouds': { + 'megacloud': { + 'cloud': 'megadodo', + 'auth': { + 'project_name': 'heart-o-gold', + 'username': 'zaphod', + }, + 'region_name': 'occ-cloud,krikkit,occ-env', + 'log_file': log_file, + 'log_level': 'debug', + 'cert': 'mycert', + 'key': 'mickey', + } + } + } + return CLOUD + + # Wrap the osc_lib make_shell() function to set the shell class since # osc-lib's TestShell class doesn't allow us to specify it yet. # TODO(dtroyer): remove this once the shell_class_patch patch is released From aabc67f3a2946c783056b5f282c9facad164e630 Mon Sep 17 00:00:00 2001 From: melissaml Date: Sun, 12 May 2019 04:40:55 +0800 Subject: [PATCH 0079/1120] Rename review.openstack.org to review.opendev.org There are many references to review.openstack.org, and while the redirect should work, we can also go ahead and fix them. Change-Id: I82e3797dd4c05e4944f40c950b4fafe9a5334cbf --- doc/source/cli/backwards-incompatible.rst | 36 +++++++++---------- openstackclient/compute/v2/flavor.py | 2 +- .../functional/compute/v2/test_server.py | 2 +- .../tests/unit/api/test_object_store_v1.py | 2 +- openstackclient/volume/v2/volume_type.py | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index fcb68684bf..51c1d134b2 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -46,7 +46,7 @@ Release 3.12 * As of: 3.12.0 * Removed in: n/a * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1657956 - * Commit: https://review.openstack.org/#/c/423081/ + * Commit: https://review.opendev.org/#/c/423081/ Release 3.10 ------------ @@ -55,7 +55,7 @@ Release 3.10 with Nova-network clouds. * As of: 3.10 - * Commit: https://review.openstack.org/460679 + * Commit: https://review.opendev.org/460679 2. The positional argument ```` of the ``volume snapshot create`` command is no longer optional. @@ -67,7 +67,7 @@ Release 3.10 * As of: 3.10 * Bug: 1659894 - * Commit: https://review.openstack.org/440497 + * Commit: https://review.opendev.org/440497 Release 3.0 ----------- @@ -81,7 +81,7 @@ Release 3.0 * As of: 3.0 * Removed in: n/a * Bug: n/a - * Commit: https://review.openstack.org/332938 + * Commit: https://review.opendev.org/332938 Releases Before 3.0 @@ -95,7 +95,7 @@ Releases Before 3.0 * As of: 1.0.2 * Removed in: TBD * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1406654 - * Commit: https://review.openstack.org/#/c/147379/ + * Commit: https://review.opendev.org/#/c/147379/ 2. should not be optional for command `openstack service create` @@ -107,7 +107,7 @@ Releases Before 3.0 * As of: 1.0.2 * Removed in: TBD * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1404073 - * Commit: https://review.openstack.org/#/c/143242/ + * Commit: https://review.opendev.org/#/c/143242/ 3. Command `openstack security group rule delete` now requires rule id @@ -119,7 +119,7 @@ Releases Before 3.0 * As of: 1.2.1 * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1450872 - * Commit: https://review.openstack.org/#/c/179446/ + * Commit: https://review.opendev.org/#/c/179446/ 4. Command `openstack image create` does not update already existing image @@ -133,7 +133,7 @@ Releases Before 3.0 * As of: 1.5.0 * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1461817 - * Commit: https://review.openstack.org/#/c/194654/ + * Commit: https://review.opendev.org/#/c/194654/ 5. Command `openstack network list --dhcp` has been removed @@ -147,7 +147,7 @@ Releases Before 3.0 * As of: 1.6.0 * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/472613 - * Commit: https://review.openstack.org/#/c/194654/ + * Commit: https://review.opendev.org/#/c/194654/ 6. Plugin interface change for default API versions @@ -162,7 +162,7 @@ Releases Before 3.0 * As of: 1.2.1 * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1453229 - * Commit: https://review.openstack.org/#/c/181514/ + * Commit: https://review.opendev.org/#/c/181514/ 7. `image set` commands will no longer return the modified resource @@ -186,7 +186,7 @@ Releases Before 3.0 * As of 1.9.0 * Removed in: NA * Bug: https://launchpad.net/bugs/1506841 - * Commit: https://review.openstack.org/#/c/236736/ + * Commit: https://review.opendev.org/#/c/236736/ 9. `flavor set/unset` commands will no longer return the modified resource @@ -198,7 +198,7 @@ Releases Before 3.0 * As of: NA * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1546065 - * Commit: https://review.openstack.org/#/c/280663/ + * Commit: https://review.opendev.org/#/c/280663/ 10. `security group set` commands will no longer return the modified resource @@ -210,7 +210,7 @@ Releases Before 3.0 * As of: NA * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1546065 - * Commit: https://review.openstack.org/#/c/281087/ + * Commit: https://review.opendev.org/#/c/281087/ 11. `compute agent set` commands will no longer return the modified resource @@ -222,7 +222,7 @@ Releases Before 3.0 * As of: NA * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1546065 - * Commit: https://review.openstack.org/#/c/281088/ + * Commit: https://review.opendev.org/#/c/281088/ 12. ` ` should be optional for command `openstack compute agent set` @@ -235,7 +235,7 @@ Releases Before 3.0 * As of: NA * Removed in: NA * Bug: NA - * Commit: https://review.openstack.org/#/c/328819/ + * Commit: https://review.opendev.org/#/c/328819/ 13. `aggregate set` commands will no longer return the modified resource @@ -247,7 +247,7 @@ Releases Before 3.0 * As of: NA * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1546065 - * Commit: https://review.openstack.org/#/c/281089/ + * Commit: https://review.opendev.org/#/c/281089/ 14. Output of `ip floating list` command has changed. @@ -291,7 +291,7 @@ Releases Before 3.0 * As of: NA * Removed in: NA * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1519502 - * Commit: https://review.openstack.org/#/c/277720/ + * Commit: https://review.opendev.org/#/c/277720/ For Developers ============== @@ -302,4 +302,4 @@ update this file. To review all changes that are affected, use the following query: -https://review.openstack.org/#/q/project:openstack/python-openstackclient+AND+message:BackwardsIncompatibleImpact,n,z +https://review.opendev.org/#/q/project:openstack/python-openstackclient+AND+message:BackwardsIncompatibleImpact,n,z diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 2cc5f1e891..4f1e48af06 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -432,7 +432,7 @@ def take_action(self, parsed_args): projects = [utils.get_field(access, 'tenant_id') for access in flavor_access] # TODO(Huanxuan Ao): This format case can be removed after - # patch https://review.openstack.org/#/c/330223/ merged. + # patch https://review.opendev.org/#/c/330223/ merged. access_projects = utils.format_list(projects) except Exception as e: msg = _("Failed to get access projects list " diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index c8fb44d380..aa7d7cb7be 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -407,7 +407,7 @@ def test_server_boot_from_volume(self): cmd_output['status'], ) - # NOTE(dtroyer): Prior to https://review.openstack.org/#/c/407111 + # NOTE(dtroyer): Prior to https://review.opendev.org/#/c/407111 # --block-device-mapping was ignored if --volume # present on the command line. Now we should see the # attachment. diff --git a/openstackclient/tests/unit/api/test_object_store_v1.py b/openstackclient/tests/unit/api/test_object_store_v1.py index acf955501c..74b62493db 100644 --- a/openstackclient/tests/unit/api/test_object_store_v1.py +++ b/openstackclient/tests/unit/api/test_object_store_v1.py @@ -184,7 +184,7 @@ def base_object_create(self, file_contents, mock_open): } # TODO(dtroyer): When requests_mock gains the ability to # match against request.body add this check - # https://review.openstack.org/127316 + # https://review.opendev.org/127316 self.requests_mock.register_uri( 'PUT', FAKE_URL + '/qaz/counter.txt', diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 71e94a2b4e..749d1dd6fd 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -501,7 +501,7 @@ def take_action(self, parsed_args): project_ids = [utils.get_field(item, 'project_id') for item in volume_type_access] # TODO(Rui Chen): This format list case can be removed after - # patch https://review.openstack.org/#/c/330223/ merged. + # patch https://review.opendev.org/#/c/330223/ merged. access_project_ids = utils.format_list(project_ids) except Exception as e: msg = _('Failed to get access project list for volume type ' From f1791179768115b6d074f70f9a8695f9c1e0b9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natal=20Ng=C3=A9tal?= Date: Wed, 15 May 2019 14:13:49 +0200 Subject: [PATCH 0080/1120] Update sphinx requirement. Sphinx 2.0 no longer works on python 2.7, start cappingit there as well. Change-Id: I8a7d227b2f925066fc8213aa62b5756927ee263b --- doc/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index a2649afb1f..91854914a5 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,5 +3,6 @@ # process, which may cause wedges in the gate later. openstackdocstheme>=1.23.2 # Apache-2.0 reno>=2.5.0 # Apache-2.0 -sphinx!=1.6.6,!=1.6.7,>=1.6.5 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.5,<2.0.0;python_version=='2.7' # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.5;python_version>='3.4' # BSD sphinxcontrib-apidoc>=0.2.0 # BSD From bb659cf438dd22fa06a31d23c4f30c30f2a3376c Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 16 May 2019 07:43:49 -0500 Subject: [PATCH 0081/1120] Aggregate functional test tweak This seems to still be racy, lengthen the timeout to wait for agregate creation. Change-Id: I3601c5baee03745ae21714b9dff0e278ad016877 Signed-off-by: Dean Troyer --- .../tests/functional/compute/v2/test_aggregate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index 7102675716..590d28cb03 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -54,9 +54,10 @@ def test_aggregate_create_and_delete(self): # Loop a few times since this is timing-sensitive # Just hard-code it for now, since there is no pause and it is - # racy we shouldn't have to wait too long, a minute seems reasonable + # racy we shouldn't have to wait too long, a minute or two + # seems reasonable wait_time = 0 - while wait_time < 60: + while wait_time <= 120: cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + name2 From 1b2595a9594e1a6965e6086aad7782cf3e39bafa Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 16 May 2019 08:24:58 -0500 Subject: [PATCH 0082/1120] Remove code migrated to osc-lib long ago * Remove openstackclient.api.utils and use osc_lib.api.utils * Remove openstackclient.common.clientmanager.ClientManager.auth_ref * Remove openstackclient.common.commandmanager Change-Id: I67e1dbc53cc0b37967c0011bcb2fc09bdef62d94 Signed-off-by: Dean Troyer --- openstackclient/api/utils.py | 84 ------------- openstackclient/common/clientmanager.py | 7 -- openstackclient/common/commandmanager.py | 59 --------- openstackclient/image/v1/image.py | 2 +- openstackclient/image/v2/image.py | 2 +- openstackclient/shell.py | 2 +- openstackclient/tests/unit/api/test_utils.py | 115 ------------------ .../tests/unit/common/test_commandmanager.py | 107 ---------------- .../tests/unit/image/v1/test_image.py | 2 +- .../tests/unit/image/v2/test_image.py | 2 +- 10 files changed, 5 insertions(+), 377 deletions(-) delete mode 100644 openstackclient/api/utils.py delete mode 100644 openstackclient/common/commandmanager.py delete mode 100644 openstackclient/tests/unit/api/test_utils.py delete mode 100644 openstackclient/tests/unit/common/test_commandmanager.py diff --git a/openstackclient/api/utils.py b/openstackclient/api/utils.py deleted file mode 100644 index 6407cd4457..0000000000 --- a/openstackclient/api/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -# 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. -# - -"""API Utilities Library""" - - -def simple_filter( - data=None, - attr=None, - value=None, - property_field=None, -): - """Filter a list of dicts - - :param list data: - The list to be filtered. The list is modified in-place and will - be changed if any filtering occurs. - :param string attr: - The name of the attribute to filter. If attr does not exist no - match will succeed and no rows will be returned. If attr is - None no filtering will be performed and all rows will be returned. - :param string value: - The value to filter. None is considered to be a 'no filter' value. - '' matches against a Python empty string. - :param string property_field: - The name of the data field containing a property dict to filter. - If property_field is None, attr is a field name. If property_field - is not None, attr is a property key name inside the named property - field. - - :returns: - Returns the filtered list - :rtype list: - - This simple filter (one attribute, one exact-match value) searches a - list of dicts to select items. It first searches the item dict for a - matching ``attr`` then does an exact-match on the ``value``. If - ``property_field`` is given, it will look inside that field (if it - exists and is a dict) for a matching ``value``. - """ - - # Take the do-nothing case shortcut - if not data or not attr or value is None: - return data - - # NOTE:(dtroyer): This filter modifies the provided list in-place using - # list.remove() so we need to start at the end so the loop pointer does - # not skip any items after a deletion. - for d in reversed(data): - if attr in d: - # Searching data fields - search_value = d[attr] - elif (property_field and property_field in d and - isinstance(d[property_field], dict)): - # Searching a properties field - do this separately because - # we don't want to fail over to checking the fields if a - # property name is given. - if attr in d[property_field]: - search_value = d[property_field][attr] - else: - search_value = None - else: - search_value = None - - # could do regex here someday... - if not search_value or search_value != value: - # remove from list - try: - data.remove(d) - except ValueError: - # it's already gone! - pass - - return data diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index aa1045e469..c1118ad3ef 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -91,13 +91,6 @@ def setup_auth(self): return super(ClientManager, self).setup_auth() - @property - def auth_ref(self): - if not self._auth_required: - return None - else: - return super(ClientManager, self).auth_ref - def _fallback_load_auth_plugin(self, e): # NOTES(RuiChen): Hack to avoid auth plugins choking on data they don't # expect, delete fake token and endpoint, then try to diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py deleted file mode 100644 index c190e33e0d..0000000000 --- a/openstackclient/common/commandmanager.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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. -# - -"""Modify cliff.CommandManager""" - -import pkg_resources - -import cliff.commandmanager - - -class CommandManager(cliff.commandmanager.CommandManager): - """Add additional functionality to cliff.CommandManager - - Load additional command groups after initialization - Add _command_group() methods - """ - - def __init__(self, namespace, convert_underscores=True): - self.group_list = [] - super(CommandManager, self).__init__(namespace, convert_underscores) - - def load_commands(self, namespace): - self.group_list.append(namespace) - return super(CommandManager, self).load_commands(namespace) - - def add_command_group(self, group=None): - """Adds another group of command entrypoints""" - if group: - self.load_commands(group) - - def get_command_groups(self): - """Returns a list of the loaded command groups""" - return self.group_list - - def get_command_names(self, group=None): - """Returns a list of commands loaded for the specified group""" - group_list = [] - if group is not None: - for ep in pkg_resources.iter_entry_points(group): - cmd_name = ( - ep.name.replace('_', ' ') - if self.convert_underscores - else ep.name - ) - group_list.append(cmd_name) - return group_list - return list(self.commands.keys()) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 7ecaa3efed..64c4049cfd 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -22,12 +22,12 @@ import sys from glanceclient.common import utils as gc_utils +from osc_lib.api import utils as api_utils from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils import six -from openstackclient.api import utils as api_utils from openstackclient.i18n import _ if os.name == "nt": diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 3efde80879..bdec99d7be 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -21,13 +21,13 @@ from glanceclient.common import utils as gc_utils from openstack.image import image_signer +from osc_lib.api import utils as api_utils from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils import six -from openstackclient.api import utils as api_utils from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 58a7712026..22d8412cc7 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -20,13 +20,13 @@ import sys from osc_lib.api import auth +from osc_lib.command import commandmanager from osc_lib import shell import six import openstackclient from openstackclient.common import client_config as cloud_config from openstackclient.common import clientmanager -from openstackclient.common import commandmanager DEFAULT_DOMAIN = 'default' diff --git a/openstackclient/tests/unit/api/test_utils.py b/openstackclient/tests/unit/api/test_utils.py deleted file mode 100644 index 1f52855864..0000000000 --- a/openstackclient/tests/unit/api/test_utils.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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. -# - -"""API Utilities Library Tests""" - -import copy - -from openstackclient.api import api -from openstackclient.api import utils as api_utils -from openstackclient.tests.unit.api import fakes as api_fakes - - -class TestBaseAPIFilter(api_fakes.TestSession): - """The filters can be tested independently""" - - def setUp(self): - super(TestBaseAPIFilter, self).setUp() - self.api = api.BaseAPI( - session=self.sess, - endpoint=self.BASE_URL, - ) - - self.input_list = [ - api_fakes.RESP_ITEM_1, - api_fakes.RESP_ITEM_2, - api_fakes.RESP_ITEM_3, - ] - - def test_simple_filter_none(self): - output = api_utils.simple_filter( - ) - self.assertIsNone(output) - - def test_simple_filter_no_attr(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - ) - self.assertEqual(self.input_list, output) - - def test_simple_filter_attr_only(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - ) - self.assertEqual(self.input_list, output) - - def test_simple_filter_attr_value(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - value='', - ) - self.assertEqual([], output) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - value='UP', - ) - self.assertEqual( - [api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_3], - output, - ) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='fred', - value='UP', - ) - self.assertEqual([], output) - - def test_simple_filter_prop_attr_only(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='b', - property_field='props', - ) - self.assertEqual(self.input_list, output) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - property_field='props', - ) - self.assertEqual(self.input_list, output) - - def test_simple_filter_prop_attr_value(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='b', - value=2, - property_field='props', - ) - self.assertEqual( - [api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_2], - output, - ) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='b', - value=9, - property_field='props', - ) - self.assertEqual([], output) diff --git a/openstackclient/tests/unit/common/test_commandmanager.py b/openstackclient/tests/unit/common/test_commandmanager.py deleted file mode 100644 index 0c6c99c05d..0000000000 --- a/openstackclient/tests/unit/common/test_commandmanager.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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. -# - -import mock - -from openstackclient.common import commandmanager -from openstackclient.tests.unit import utils - - -class FakeCommand(object): - - @classmethod - def load(cls): - return cls - - def __init__(self): - return - -FAKE_CMD_ONE = FakeCommand -FAKE_CMD_TWO = FakeCommand -FAKE_CMD_ALPHA = FakeCommand -FAKE_CMD_BETA = FakeCommand - - -class FakeCommandManager(commandmanager.CommandManager): - commands = {} - - def load_commands(self, namespace): - if namespace == 'test': - self.commands['one'] = FAKE_CMD_ONE - self.commands['two'] = FAKE_CMD_TWO - self.group_list.append(namespace) - elif namespace == 'greek': - self.commands['alpha'] = FAKE_CMD_ALPHA - self.commands['beta'] = FAKE_CMD_BETA - self.group_list.append(namespace) - - -class TestCommandManager(utils.TestCase): - - def test_add_command_group(self): - mgr = FakeCommandManager('test') - - # Make sure add_command() still functions - mock_cmd_one = mock.Mock() - mgr.add_command('mock', mock_cmd_one) - cmd_mock, name, args = mgr.find_command(['mock']) - self.assertEqual(mock_cmd_one, cmd_mock) - - # Find a command added in initialization - cmd_one, name, args = mgr.find_command(['one']) - self.assertEqual(FAKE_CMD_ONE, cmd_one) - - # Load another command group - mgr.add_command_group('greek') - - # Find a new command - cmd_alpha, name, args = mgr.find_command(['alpha']) - self.assertEqual(FAKE_CMD_ALPHA, cmd_alpha) - - # Ensure that the original commands were not overwritten - cmd_two, name, args = mgr.find_command(['two']) - self.assertEqual(FAKE_CMD_TWO, cmd_two) - - def test_get_command_groups(self): - mgr = FakeCommandManager('test') - - # Make sure add_command() still functions - mock_cmd_one = mock.Mock() - mgr.add_command('mock', mock_cmd_one) - cmd_mock, name, args = mgr.find_command(['mock']) - self.assertEqual(mock_cmd_one, cmd_mock) - - # Load another command group - mgr.add_command_group('greek') - - gl = mgr.get_command_groups() - self.assertEqual(['test', 'greek'], gl) - - def test_get_command_names(self): - mock_cmd_one = mock.Mock() - mock_cmd_one.name = 'one' - mock_cmd_two = mock.Mock() - mock_cmd_two.name = 'cmd two' - mock_pkg_resources = mock.Mock( - return_value=[mock_cmd_one, mock_cmd_two], - ) - with mock.patch( - 'pkg_resources.iter_entry_points', - mock_pkg_resources, - ) as iter_entry_points: - mgr = commandmanager.CommandManager('test') - iter_entry_points.assert_called_once_with('test') - cmds = mgr.get_command_names('test') - self.assertEqual(['one', 'cmd two'], cmds) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index ae578d9138..6babd4ffca 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -417,7 +417,7 @@ def test_image_list_long_option(self): ), ) self.assertEqual(datalist, tuple(data)) - @mock.patch('openstackclient.api.utils.simple_filter') + @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): sf_mock.side_effect = [ [self.image_info], [], diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 16a393df7d..4cc8f44827 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -734,7 +734,7 @@ def test_image_list_long_option(self): ), ) self.assertEqual(datalist, tuple(data)) - @mock.patch('openstackclient.api.utils.simple_filter') + @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): sf_mock.return_value = [copy.deepcopy(self._image)] From c77a9621beaf7748e5ccfecb3ac37fa9602c5bfe Mon Sep 17 00:00:00 2001 From: Chen Date: Tue, 15 May 2018 15:25:40 +0800 Subject: [PATCH 0083/1120] Compute: Add description support for server This patch adds functionality to configure server's description with: 1 server create 2 server set 3 server unset 4 server rebuild Change-Id: Ic06d97b29e51828b29d7ac5172645c288e4ada9e Story: 2002005 Task: 19640 --- doc/source/cli/data/nova.csv | 3 +- openstackclient/compute/v2/server.py | 57 +++++ .../tests/unit/compute/v2/test_server.py | 223 ++++++++++++++++++ .../server-description-ae9618fc09544cac.yaml | 6 + 4 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/server-description-ae9618fc09544cac.yaml diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index fe2aa362fe..872d6e09b2 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -125,7 +125,8 @@ unlock,server unlock,Unlock a server. unpause,server unpause,Unpause a server. unrescue,server unrescue,Restart the server from normal boot disk again. unshelve,server unshelve,Unshelve a server. -update,server set / unset,Update the name or the description for a server. +update,server set / unset --description,Update or unset the description for a server. +update,server set --name,Update the name 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. diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 4f67242862..4bf3b90a5d 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -502,6 +502,12 @@ def get_parser(self, prog_name): metavar='', help=_('User data file to serve from the metadata server'), ) + parser.add_argument( + '--description', + metavar='', + help=_('Set description for the server (supported by ' + '--os-compute-api-version 2.19 or above)'), + ) parser.add_argument( '--availability-zone', metavar='', @@ -712,6 +718,12 @@ def _match_image(image_api, wanted_properties): "exception": e} ) + if parsed_args.description: + if compute_client.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + block_device_mapping_v2 = [] if volume: block_device_mapping_v2 = [{'uuid': volume, @@ -872,6 +884,9 @@ def _match_image(image_api, wanted_properties): scheduler_hints=hints, config_drive=config_drive) + if parsed_args.description: + boot_kwargs['description'] = parsed_args.description + LOG.debug('boot_args: %s', boot_args) LOG.debug('boot_kwargs: %s', boot_kwargs) @@ -1529,6 +1544,12 @@ def get_parser(self, prog_name): help=_('Set a property on the rebuilt instance ' '(repeat option to set multiple values)'), ) + parser.add_argument( + '--description', + metavar='', + help=_('New description for the server (supported by ' + '--os-compute-api-version 2.19 or above'), + ) parser.add_argument( '--wait', action='store_true', @@ -1557,6 +1578,12 @@ def _show_progress(progress): kwargs = {} if parsed_args.property: kwargs['meta'] = parsed_args.property + if parsed_args.description: + if server.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + kwargs['description'] = parsed_args.description server = server.rebuild(image, parsed_args.password, **kwargs) if parsed_args.wait: @@ -1968,6 +1995,12 @@ def get_parser(self, prog_name): choices=['active', 'error'], help=_('New server state (valid value: active, error)'), ) + parser.add_argument( + '--description', + metavar='', + help=_('New server description (supported by ' + '--os-compute-api-version 2.19 or above)'), + ) return parser def take_action(self, parsed_args): @@ -1999,6 +2032,13 @@ def take_action(self, parsed_args): msg = _("Passwords do not match, password unchanged") raise exceptions.CommandError(msg) + if parsed_args.description: + if server.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + server.update(description=parsed_args.description) + class ShelveServer(command.Command): _description = _("Shelve server(s)") @@ -2358,6 +2398,13 @@ def get_parser(self, prog_name): help=_('Property key to remove from server ' '(repeat option to remove multiple values)'), ) + parser.add_argument( + '--description', + dest='description', + action='store_true', + help=_('Unset server description (supported by ' + '--os-compute-api-version 2.19 or above)'), + ) return parser def take_action(self, parsed_args): @@ -2373,6 +2420,16 @@ def take_action(self, parsed_args): parsed_args.property, ) + if parsed_args.description: + if compute_client.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + compute_client.servers.update( + server, + description="", + ) + class UnshelveServer(command.Command): _description = _("Unshelve server(s)") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 44e433d89d..b8fa5e7c78 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1736,6 +1736,90 @@ def test_server_create_image_property_missed(self): self.cmd.take_action, parsed_args) + def test_server_create_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.app.client_manager.compute.api_version = 2.19 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--description', 'description1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('description', 'description1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + # In base command class ShowOne in cliff, abstract method + # take_action() returns a two-part tuple with a tuple of + # column names and a tuple of data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + description='description1', + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.app.client_manager.compute.api_version = 2.18 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--description', 'description1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('description', 'description1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerDelete(TestServer): @@ -2493,6 +2577,55 @@ def test_rebuild_with_current_image_and_password(self): self.images_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) + def test_rebuild_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.server.api_version = 2.18 + + description = 'description1' + arglist = [ + self.server.id, + '--description', description + ] + verifylist = [ + ('server', self.server.id), + ('description', description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_rebuild_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.server.api_version = 2.19 + + description = 'description1' + arglist = [ + self.server.id, + '--description', description + ] + verifylist = [ + ('server', self.server.id), + ('description', description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.images_mock.get.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(self.image, None, + description=description) + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_rebuild_with_wait_ok(self, mock_wait_for_status): arglist = [ @@ -3140,6 +3273,10 @@ class TestServerSet(TestServer): def setUp(self): super(TestServerSet, self).setUp() + self.attrs = { + 'api_version': None, + } + self.methods = { 'update': None, 'reset_state': None, @@ -3242,6 +3379,48 @@ def test_server_set_with_root_password(self, mock_getpass): mock.sentinel.fake_pass) self.assertIsNone(result) + def test_server_set_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.fake_servers[0].api_version = 2.19 + + arglist = [ + '--description', 'foo_description', + 'foo_vm', + ] + verifylist = [ + ('description', 'foo_description'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + result = self.cmd.take_action(parsed_args) + self.fake_servers[0].update.assert_called_once_with( + description='foo_description') + self.assertIsNone(result) + + def test_server_set_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.fake_servers[0].api_version = 2.18 + + arglist = [ + '--description', 'foo_description', + 'foo_vm', + ] + verifylist = [ + ('description', 'foo_description'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerShelve(TestServer): @@ -3523,6 +3702,50 @@ def test_server_unset_with_property(self): self.fake_server, ['key1', 'key2']) self.assertIsNone(result) + def test_server_unset_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.app.client_manager.compute.api_version = 2.19 + + arglist = [ + '--description', + 'foo_vm', + ] + verifylist = [ + ('description', True), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + result = self.cmd.take_action(parsed_args) + self.servers_mock.update.assert_called_once_with( + self.fake_server, description="") + self.assertIsNone(result) + + def test_server_unset_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.app.client_manager.compute.api_version = 2.18 + + arglist = [ + '--description', + 'foo_vm', + ] + verifylist = [ + ('description', True), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerUnshelve(TestServer): diff --git a/releasenotes/notes/server-description-ae9618fc09544cac.yaml b/releasenotes/notes/server-description-ae9618fc09544cac.yaml new file mode 100644 index 0000000000..469f18454b --- /dev/null +++ b/releasenotes/notes/server-description-ae9618fc09544cac.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--description`` option to ``server create``, ``server rebuild``, + ``server set`` and ``server unset`` commands. + [Bug `2002005 `_] From 99c3be93c852af0c8f701d8d65c54310f53b30d2 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 May 2019 16:40:12 -0500 Subject: [PATCH 0084/1120] Serialize more aggregate functional tests These tests are showing signs of problems running in parallel so serialse the create/delete/list/set/unset tests. They all used two aggregates each anyway... Change-Id: Iba4b52c179e6914eaeefea1da0f7eaefcdcf1f87 Signed-off-by: Dean Troyer --- openstackclient/compute/v2/aggregate.py | 2 + .../functional/compute/v2/test_aggregate.py | 148 ++++++------------ 2 files changed, 52 insertions(+), 98 deletions(-) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 7f9161a996..fa64647899 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -101,6 +101,8 @@ def take_action(self, parsed_args): parsed_args.property, )._info) + # TODO(dtroyer): re-format metadata field to properites as + # in the set command return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index 590d28cb03..c591dd61fd 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -11,7 +11,6 @@ # under the License. import json -import time import uuid from openstackclient.tests.functional import base @@ -20,12 +19,13 @@ class AggregateTests(base.TestCase): """Functional tests for aggregate""" - def test_aggregate_create_and_delete(self): + def test_aggregate_crud(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'aggregate create -f json ' + '--zone nova ' + + '--property a=b ' + name1 )) self.assertEqual( @@ -36,11 +36,17 @@ def test_aggregate_create_and_delete(self): 'nova', cmd_output['availability_zone'] ) + # TODO(dtroyer): enable the following once the properties field + # is correctly formatted in the create output + # self.assertIn( + # "a='b'", + # cmd_output['properties'] + # ) name2 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'aggregate create -f json ' + - '--zone nova ' + + '--zone external ' + name2 )) self.assertEqual( @@ -48,102 +54,15 @@ def test_aggregate_create_and_delete(self): cmd_output['name'] ) self.assertEqual( - 'nova', + 'external', cmd_output['availability_zone'] ) - # Loop a few times since this is timing-sensitive - # Just hard-code it for now, since there is no pause and it is - # racy we shouldn't have to wait too long, a minute or two - # seems reasonable - wait_time = 0 - while wait_time <= 120: - cmd_output = json.loads(self.openstack( - 'aggregate show -f json ' + - name2 - )) - if cmd_output['name'] != name2: - # Hang out for a bit and try again - print('retrying aggregate check') - wait_time += 10 - time.sleep(10) - else: - break - - del_output = self.openstack( - 'aggregate delete ' + - name1 + ' ' + - name2 - ) - self.assertOutput('', del_output) - - def test_aggregate_list(self): - """Test aggregate list""" - name1 = uuid.uuid4().hex - self.openstack( - 'aggregate create ' + - '--zone nova ' + - '--property a=b ' + - name1 - ) - self.addCleanup( - self.openstack, - 'aggregate delete ' + name1, - fail_ok=True, - ) - - name2 = uuid.uuid4().hex - self.openstack( - 'aggregate create ' + - '--zone internal ' + - '--property c=d ' + - name2 - ) - self.addCleanup( - self.openstack, - 'aggregate delete ' + name2, - fail_ok=True, - ) - - cmd_output = json.loads(self.openstack( - 'aggregate list -f json' - )) - names = [x['Name'] for x in cmd_output] - self.assertIn(name1, names) - self.assertIn(name2, names) - zones = [x['Availability Zone'] for x in cmd_output] - self.assertIn('nova', zones) - self.assertIn('internal', zones) - - # Test aggregate list --long - cmd_output = json.loads(self.openstack( - 'aggregate list --long -f json' - )) - names = [x['Name'] for x in cmd_output] - self.assertIn(name1, names) - self.assertIn(name2, names) - zones = [x['Availability Zone'] for x in cmd_output] - self.assertIn('nova', zones) - self.assertIn('internal', zones) - properties = [x['Properties'] for x in cmd_output] - self.assertIn({'a': 'b'}, properties) - self.assertIn({'c': 'd'}, properties) - - def test_aggregate_set_and_unset(self): - """Test aggregate set, show and unset""" - name1 = uuid.uuid4().hex - name2 = uuid.uuid4().hex - self.openstack( - 'aggregate create ' + - '--zone nova ' + - '--property a=b ' + - name1 - ) - self.addCleanup(self.openstack, 'aggregate delete ' + name2) - + # Test aggregate set + name3 = uuid.uuid4().hex raw_output = self.openstack( 'aggregate set ' + - '--name ' + name2 + ' ' + + '--name ' + name3 + ' ' + '--zone internal ' + '--no-property ' + '--property c=d ' + @@ -153,10 +72,10 @@ def test_aggregate_set_and_unset(self): cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + - name2 + name3 )) self.assertEqual( - name2, + name3, cmd_output['name'] ) self.assertEqual( @@ -172,23 +91,56 @@ def test_aggregate_set_and_unset(self): cmd_output['properties'] ) + # Test aggregate list + cmd_output = json.loads(self.openstack( + 'aggregate list -f json' + )) + names = [x['Name'] for x in cmd_output] + self.assertIn(name3, names) + self.assertIn(name2, names) + zones = [x['Availability Zone'] for x in cmd_output] + self.assertIn('external', zones) + self.assertIn('internal', zones) + + # Test aggregate list --long + cmd_output = json.loads(self.openstack( + 'aggregate list --long -f json' + )) + names = [x['Name'] for x in cmd_output] + self.assertIn(name3, names) + self.assertIn(name2, names) + zones = [x['Availability Zone'] for x in cmd_output] + self.assertIn('external', zones) + self.assertIn('internal', zones) + properties = [x['Properties'] for x in cmd_output] + self.assertNotIn({'a': 'b'}, properties) + self.assertIn({'c': 'd'}, properties) + # Test unset raw_output = self.openstack( 'aggregate unset ' + '--property c ' + - name2 + name3 ) self.assertOutput('', raw_output) cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + - name2 + name3 )) self.assertNotIn( "c='d'", cmd_output['properties'] ) + # test aggregate delete + del_output = self.openstack( + 'aggregate delete ' + + name3 + ' ' + + name2 + ) + self.assertOutput('', del_output) + def test_aggregate_add_and_remove_host(self): """Test aggregate add and remove host""" # Get a host From b7742b59371b9e0b2e417547eb2cdaa80954b50d Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 23 Oct 2018 14:02:49 -0500 Subject: [PATCH 0085/1120] Remove deprecated compute commands The following were deprecated over two years ago and can now be removed: * ``ip fixed add|remove`` in favor of ``server add|remove fixed ip`` * ``ip floating add|remove`` in favor of ``server add|remove floating ip`` These are backwards incompatible changes and will require a major version bump after they are merged. Change-Id: I10c4d32a3c0b55ad41a02afd3b14249bafcb55a9 Signed-off-by: Sean McGinnis Signed-off-by: Dean Troyer --- doc/source/cli/backwards-incompatible.rst | 17 ++- .../cli/command-objects/floating-ip-pool.rst | 2 +- .../cli/command-objects/floating-ip.rst | 2 +- doc/source/cli/command-objects/ip-fixed.rst | 47 -------- doc/source/cli/commands.rst | 9 +- openstackclient/compute/v2/fixedip.py | 100 ------------------ openstackclient/compute/v2/floatingip.py | 97 ----------------- .../notes/osc4-compute-09246008eff260cb.yaml | 8 ++ setup.cfg | 6 -- 9 files changed, 27 insertions(+), 261 deletions(-) delete mode 100644 doc/source/cli/command-objects/ip-fixed.rst delete mode 100644 openstackclient/compute/v2/fixedip.py delete mode 100644 openstackclient/compute/v2/floatingip.py create mode 100644 releasenotes/notes/osc4-compute-09246008eff260cb.yaml diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index fcb68684bf..a86ce35977 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -16,9 +16,20 @@ from this backwards incompatible change handling. Backwards Incompatible Changes ============================== -.. Carry this section as comments until 4.0 release -.. Release 4.0 -.. ----------- +Release 4.0 +----------- + +1. Remove ``ip fixed add|remove`` commands. + Use ``server add|remove fixed ip`` commands instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612781 + +2. Remove ``ip floating add|remove`` commands. + Use ``server add|remove floating ip`` commands instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612781 .. 1. Change ``volume transfer request accept`` to use new option ``--auth-key`` .. rather than a second positional argument. diff --git a/doc/source/cli/command-objects/floating-ip-pool.rst b/doc/source/cli/command-objects/floating-ip-pool.rst index 9213b86dd0..5c8f3aa13d 100644 --- a/doc/source/cli/command-objects/floating-ip-pool.rst +++ b/doc/source/cli/command-objects/floating-ip-pool.rst @@ -2,7 +2,7 @@ floating ip pool ================ -Compute v2, Network v2 +Network v2 floating ip pool list --------------------- diff --git a/doc/source/cli/command-objects/floating-ip.rst b/doc/source/cli/command-objects/floating-ip.rst index 749c32d6b8..d122ccbea3 100644 --- a/doc/source/cli/command-objects/floating-ip.rst +++ b/doc/source/cli/command-objects/floating-ip.rst @@ -2,7 +2,7 @@ floating ip =========== -Compute v2, Network v2 +Network v2 floating ip create ------------------ diff --git a/doc/source/cli/command-objects/ip-fixed.rst b/doc/source/cli/command-objects/ip-fixed.rst deleted file mode 100644 index f5b11dc61b..0000000000 --- a/doc/source/cli/command-objects/ip-fixed.rst +++ /dev/null @@ -1,47 +0,0 @@ -======== -ip fixed -======== - -Compute v2 - -ip fixed add ------------- - -Add fixed IP address to server -(Deprecated, please use ``server add fixed ip`` instead) - -.. program:: ip fixed add -.. code:: bash - - openstack ip fixed add - - - -.. describe:: - - Network to fetch an IP address from (name or ID) - -.. describe:: - - Server to receive the IP address (name or ID) - -ip fixed remove ---------------- - -Remove fixed IP address from server -(Deprecated, please use ``server remove fixed ip`` instead) - -.. program:: ip fixed remove -.. code:: bash - - openstack ip fixed remove - - - -.. describe:: - - IP address to remove from server (name only) - -.. describe:: - - Server to remove the IP address from (name or ID) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index e302fdad05..7c38aa5b5c 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -95,9 +95,9 @@ referring to both Compute and Volume quotas. * ``extension``: (**Compute**, **Identity**, **Network**, **Volume**) OpenStack server API extensions * ``federation protocol``: (**Identity**) the underlying protocol used while federating identities * ``flavor``: (**Compute**) predefined server configurations: ram, root disk and so on -* ``fixed ip``: (**Compute**, **Network**) - an internal IP address assigned to a server -* ``floating ip``: (**Compute**, **Network**) - a public IP address that can be mapped to a server -* ``floating ip pool``: (**Compute**, **Network**) - a pool of public IP addresses +* ``fixed ip``: (**Compute**) - an internal IP address assigned to a server +* ``floating ip``: (**Network**) - a public IP address that can be mapped to a server +* ``floating ip pool``: (**Network**) - a pool of public IP addresses * ``group``: (**Identity**) a grouping of users * ``host``: (**Compute**) - the physical computer running compute services * ``hypervisor``: (**Compute**) the virtual machine manager @@ -106,9 +106,6 @@ referring to both Compute and Volume quotas. * ``image``: (**Image**) a disk image * ``image member``: (**Image**) a project that is a member of an Image * ``ip availability``: (**Network**) - details of IP usage of a network -* ``ip fixed``: (**Compute**, **Network**) - an internal IP address assigned to a server -* ``ip floating``: (**Compute**, **Network**) - a public IP address that can be mapped to a server -* ``ip floating pool``: (**Compute**, **Network**) - a pool of public IP addresses * ``keypair``: (**Compute**) an SSH public key * ``limits``: (**Compute**, **Volume**) resource usage limits * ``mapping``: (**Identity**) a definition to translate identity provider attributes to Identity concepts diff --git a/openstackclient/compute/v2/fixedip.py b/openstackclient/compute/v2/fixedip.py deleted file mode 100644 index 0c0b619e8e..0000000000 --- a/openstackclient/compute/v2/fixedip.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 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. -# - -"""Fixed IP action implementations""" - -import logging - -from osc_lib.command import command -from osc_lib import utils - -from openstackclient.i18n import _ - - -class AddFixedIP(command.Command): - _description = _("Add fixed IP address to server") - - # TODO(tangchen): Remove this class and ``ip fixed add`` command - # two cycles after Mitaka. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def get_parser(self, prog_name): - parser = super(AddFixedIP, self).get_parser(prog_name) - parser.add_argument( - "network", - metavar="", - help=_("Network to fetch an IP address from (name or ID)"), - ) - parser.add_argument( - "server", - metavar="", - help=_("Server to receive the IP address (name or ID)"), - ) - return parser - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "server add fixed ip" instead.')) - - compute_client = self.app.client_manager.compute - - network = utils.find_resource( - compute_client.networks, parsed_args.network) - - server = utils.find_resource( - compute_client.servers, parsed_args.server) - - server.add_fixed_ip(network.id) - - -class RemoveFixedIP(command.Command): - _description = _("Remove fixed IP address from server") - - # TODO(tangchen): Remove this class and ``ip fixed remove`` command - # two cycles after Mitaka. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def get_parser(self, prog_name): - parser = super(RemoveFixedIP, self).get_parser(prog_name) - parser.add_argument( - "ip_address", - metavar="", - help=_("IP address to remove from server (name only)"), - ) - parser.add_argument( - "server", - metavar="", - help=_("Server to remove the IP address from (name or ID)"), - ) - return parser - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "server remove fixed ip" instead.')) - - compute_client = self.app.client_manager.compute - - server = utils.find_resource( - compute_client.servers, parsed_args.server) - - server.remove_fixed_ip(parsed_args.ip_address) diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py deleted file mode 100644 index 69595bed2f..0000000000 --- a/openstackclient/compute/v2/floatingip.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 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. -# - -"""Floating IP action implementations""" - -import logging - -from osc_lib.command import command -from osc_lib import utils - -from openstackclient.i18n import _ - - -class AddFloatingIP(command.Command): - _description = _("Add floating IP address to server") - - # TODO(tangchen): Remove this class and ``ip floating add`` command - # two cycles after Mitaka. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def get_parser(self, prog_name): - parser = super(AddFloatingIP, self).get_parser(prog_name) - parser.add_argument( - "ip_address", - metavar="", - help=_("IP address to add to server (name only)"), - ) - parser.add_argument( - "server", - metavar="", - help=_("Server to receive the IP address (name or ID)"), - ) - return parser - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "server add floating ip" instead.')) - - compute_client = self.app.client_manager.compute - - server = utils.find_resource( - compute_client.servers, parsed_args.server) - - server.add_floating_ip(parsed_args.ip_address) - - -class RemoveFloatingIP(command.Command): - _description = _("Remove floating IP address from server") - - # TODO(tangchen): Remove this class and ``ip floating remove`` command - # two cycles after Mitaka. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def get_parser(self, prog_name): - parser = super(RemoveFloatingIP, self).get_parser(prog_name) - parser.add_argument( - "ip_address", - metavar="", - help=_("IP address to remove from server (name only)"), - ) - parser.add_argument( - "server", - metavar="", - help=_("Server to remove the IP address from (name or ID)"), - ) - return parser - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "server remove floating ip" instead.')) - - compute_client = self.app.client_manager.compute - - server = utils.find_resource( - compute_client.servers, parsed_args.server) - - server.remove_floating_ip(parsed_args.ip_address) diff --git a/releasenotes/notes/osc4-compute-09246008eff260cb.yaml b/releasenotes/notes/osc4-compute-09246008eff260cb.yaml new file mode 100644 index 0000000000..ab7baa67a0 --- /dev/null +++ b/releasenotes/notes/osc4-compute-09246008eff260cb.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + Remove deprecated ``ip fixed add|remove`` commands. + Use ``server add|remove fixed ip`` commands instead. + - | + Remove deprecated ``ip floating add|remove`` commands. + Use ``server add|remove floating ip`` commands instead. diff --git a/setup.cfg b/setup.cfg index db03c48423..031776a99d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -91,12 +91,6 @@ openstack.compute.v2 = hypervisor_stats_show = openstackclient.compute.v2.hypervisor_stats:ShowHypervisorStats - ip_fixed_add = openstackclient.compute.v2.fixedip:AddFixedIP - ip_fixed_remove = openstackclient.compute.v2.fixedip:RemoveFixedIP - - ip_floating_add = openstackclient.compute.v2.floatingip:AddFloatingIP - ip_floating_remove = openstackclient.compute.v2.floatingip:RemoveFloatingIP - keypair_create = openstackclient.compute.v2.keypair:CreateKeypair keypair_delete = openstackclient.compute.v2.keypair:DeleteKeypair keypair_list = openstackclient.compute.v2.keypair:ListKeypair From f9fdc296bc6f2c1f7d6196352b754a7617c4f2d2 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 23 Oct 2018 15:37:38 -0500 Subject: [PATCH 0086/1120] Remove deprecated identity commands and args The following were deprecated over two years ago and can now be removed/changed: * Remove ``service create`` option ``--type`` * Remove ``role list`` options ``--project`` and ``--user`` * Remove ``user role list`` command These are backwards incompatible changes and will require a major version bump after they are merged. Change-Id: I29e2fc9516dffbfd83eef0bc91e834dde99b4105 Signed-off-by: Sean McGinnis Signed-off-by: Dean Troyer --- doc/source/cli/backwards-incompatible.rst | 18 ++ doc/source/cli/command-objects/role.rst | 58 +---- doc/source/cli/command-objects/user-role.rst | 27 -- openstackclient/identity/v2_0/role.py | 148 +---------- openstackclient/identity/v2_0/service.py | 30 +-- openstackclient/identity/v3/role.py | 125 +--------- .../tests/functional/identity/v2/test_role.py | 32 --- .../tests/functional/identity/v3/test_role.py | 41 ---- .../tests/unit/identity/v2_0/test_role.py | 133 +--------- .../tests/unit/identity/v2_0/test_service.py | 45 +--- .../tests/unit/identity/v3/test_role.py | 232 ------------------ .../notes/osc4-identity-6564257c67d43106.yaml | 12 + setup.cfg | 2 - 13 files changed, 56 insertions(+), 847 deletions(-) delete mode 100644 doc/source/cli/command-objects/user-role.rst create mode 100644 releasenotes/notes/osc4-identity-6564257c67d43106.yaml diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index a86ce35977..51743cb2b2 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -31,6 +31,24 @@ Release 4.0 * Removed in: 4.0 * Commit: https://review.opendev.org/612781 +3. Remove ``service create`` option ``--type``. Service type is + a positional argument. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612798 + +4. Remove ``role list`` options ``--project`` and ``--user``. + Use ``role assignment list`` options ``--project`` and ``--user`` instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612798 + +5. Remove ``user role list`` command. + Use ``role assignment list`` options ``--project`` and ``--user`` instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612798 + .. 1. Change ``volume transfer request accept`` to use new option ``--auth-key`` .. rather than a second positional argument. diff --git a/doc/source/cli/command-objects/role.rst b/doc/source/cli/command-objects/role.rst index 9819fd1231..e2a2b457a0 100644 --- a/doc/source/cli/command-objects/role.rst +++ b/doc/source/cli/command-objects/role.rst @@ -146,68 +146,12 @@ List roles .. code:: bash openstack role list - --domain | --project [--project-domain ] - --user [--user-domain ] | --group [--group-domain ] - --inherited + [--domain ] .. option:: --domain Filter roles by (name or ID) - (Deprecated if being used to list assignments in conjunction with the - ``--user ``, option, please use ``role assignment list`` instead) - -.. option:: --project - - Filter roles by (name or ID) - - (Deprecated, please use ``role assignment list`` instead) - -.. option:: --user - - Filter roles by (name or ID) - - (Deprecated, please use ``role assignment list`` instead) - -.. option:: --group - - Filter roles by (name or ID) - - (Deprecated, please use ``role assignment list`` instead) - -.. option:: --user-domain - - Domain the user belongs to (name or ID). - This can be used in case collisions between user names exist. - - (Deprecated, please use ``role assignment list`` instead) - - .. versionadded:: 3 - -.. option:: --group-domain - - Domain the group belongs to (name or ID). - This can be used in case collisions between group names exist. - - (Deprecated, please use ``role assignment list`` instead) - - .. versionadded:: 3 - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - (Deprecated, please use ``role assignment list`` instead) - - .. versionadded:: 3 - -.. option:: --inherited - - Specifies if the role grant is inheritable to the sub projects. - - (Deprecated, please use ``role assignment list`` instead) - .. versionadded:: 3 role remove diff --git a/doc/source/cli/command-objects/user-role.rst b/doc/source/cli/command-objects/user-role.rst deleted file mode 100644 index 4f443f3126..0000000000 --- a/doc/source/cli/command-objects/user-role.rst +++ /dev/null @@ -1,27 +0,0 @@ -========= -user role -========= - -Identity v2 - -user role list --------------- - -List user-role assignments - -*Removed in version 3.* - -.. program:: user role list -.. code:: bash - - openstack user role list - [--project ] - [] - -.. option:: --project - - Filter users by `` (name or ID) - -.. describe:: - - User to list (name or ID) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index e254e05fd8..e9fe50fa4a 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -148,155 +148,11 @@ def take_action(self, parsed_args): class ListRole(command.Lister): _description = _("List roles") - def get_parser(self, prog_name): - parser = super(ListRole, self).get_parser(prog_name) - parser.add_argument( - '--project', - metavar='', - help=_('Filter roles by (name or ID)'), - ) - parser.add_argument( - '--user', - metavar='', - help=_('Filter roles by (name or ID)'), - ) - return parser - def take_action(self, parsed_args): - - def _deprecated(): - # NOTE(henry-nash): Deprecated as of Newton, so we should remove - # this in the 'P' release. - self.log.warning(_('Listing assignments using role list is ' - 'deprecated as of the Newton release. Use role ' - 'assignment list --user --project ' - ' --names instead.')) - identity_client = self.app.client_manager.identity - auth_ref = self.app.client_manager.auth_ref - - # No user or project specified, list all roles in the system - if not parsed_args.user and not parsed_args.project: - columns = ('ID', 'Name') - data = identity_client.roles.list() - elif parsed_args.user and parsed_args.project: - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) - _deprecated() - data = identity_client.roles.roles_for_user(user.id, project.id) - - elif parsed_args.user: - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) - if self.app.client_manager.auth_ref: - project = utils.find_resource( - identity_client.projects, - auth_ref.project_id - ) - else: - msg = _("Project must be specified") - raise exceptions.CommandError(msg) - _deprecated() - data = identity_client.roles.roles_for_user(user.id, project.id) - elif parsed_args.project: - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) - if self.app.client_manager.auth_ref: - user = utils.find_resource( - identity_client.users, - auth_ref.user_id - ) - else: - msg = _("User must be specified") - raise exceptions.CommandError(msg) - _deprecated() - data = identity_client.roles.roles_for_user(user.id, project.id) - - if parsed_args.user or parsed_args.project: - columns = ('ID', 'Name', 'Project', 'User') - for user_role in data: - user_role.user = user.name - user_role.project = project.name - - return (columns, - (utils.get_item_properties( - s, columns, - formatters={}, - ) for s in data)) - - -class ListUserRole(command.Lister): - _description = _("List user-role assignments") - - def get_parser(self, prog_name): - parser = super(ListUserRole, self).get_parser(prog_name) - parser.add_argument( - 'user', - metavar='', - nargs='?', - help=_('User to list (name or ID)'), - ) - parser.add_argument( - '--project', - metavar='', - help=_('Filter users by (name or ID)'), - ) - return parser - - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - auth_ref = self.app.client_manager.auth_ref - - # Project and user are required, if not included in command args - # default to the values used for authentication. For token-flow - # authentication they must be included on the command line. - if (not parsed_args.project and - self.app.client_manager.auth_ref.project_id): - parsed_args.project = auth_ref.project_id - if not parsed_args.project: - msg = _("Project must be specified") - raise exceptions.CommandError(msg) - - if (not parsed_args.user and - self.app.client_manager.auth_ref.user_id): - parsed_args.user = auth_ref.user_id - if not parsed_args.user: - msg = _("User must be specified") - raise exceptions.CommandError(msg) - - self.log.warning(_('Listing assignments using user role list is ' - 'deprecated as of the Newton release. Use role ' - 'assignment list --user --project ' - ' --names instead.')) - project = utils.find_resource( - identity_client.tenants, - parsed_args.project, - ) - user = utils.find_resource(identity_client.users, parsed_args.user) - - data = identity_client.roles.roles_for_user(user.id, project.id) - - columns = ( - 'ID', - 'Name', - 'Project', - 'User', - ) - # Add the names to the output even though they will be constant - for role in data: - role.user = user.name - role.project = project.name + columns = ('ID', 'Name') + data = identity_client.roles.list() return (columns, (utils.get_item_properties( diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 80f2d72a98..653de8eb47 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -15,7 +15,6 @@ """Service action implementations""" -import argparse import logging from osc_lib.command import command @@ -36,17 +35,11 @@ class CreateService(command.ShowOne): def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) parser.add_argument( - 'type_or_name', + 'type', metavar='', help=_('New service type (compute, image, identity, volume, etc)'), ) - type_or_name_group = parser.add_mutually_exclusive_group() - type_or_name_group.add_argument( - '--type', - metavar='', - help=argparse.SUPPRESS, - ) - type_or_name_group.add_argument( + parser.add_argument( '--name', metavar='', help=_('New service name'), @@ -61,29 +54,14 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - type_or_name = parsed_args.type_or_name name = parsed_args.name type = parsed_args.type - # If only a single positional is present, it's a . - # This is not currently legal so it is considered a new case. - if not type and not name: - type = type_or_name - # If --type option is present then positional is handled as ; - # display deprecation message. - elif type: - name = type_or_name - LOG.warning(_('The argument --type is deprecated, use service' - ' create --name type instead.')) - # If --name option is present the positional is handled as . - # Making --type optional is new, but back-compatible - elif name: - type = type_or_name - service = identity_client.services.create( name, type, - parsed_args.description) + parsed_args.description, + ) info = {} info.update(service._info) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 58a76f8a65..0eeddd37fb 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -266,131 +266,28 @@ class ListRole(command.Lister): def get_parser(self, prog_name): parser = super(ListRole, self).get_parser(prog_name) - - # TODO(henry-nash): The use of the List Role command to list - # assignments (as well as roles) has been deprecated. In order - # to support domain specific roles, we are overriding the domain - # option to allow specification of the domain for the role. This does - # not conflict with any existing commands, since for the deprecated - # assignments listing you were never allowed to only specify a domain - # (you also needed to specify a user). - # - # Once we have removed the deprecated options entirely, we must - # replace the call to _add_identity_and_resource_options_to_parser() - # below with just adding the domain option into the parser. - _add_identity_and_resource_options_to_parser(parser) + parser.add_argument( + '--domain', + metavar='', + help=_('Include (name or ID)'), + ) return parser def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - if parsed_args.user: - user = common.find_user( - identity_client, - parsed_args.user, - parsed_args.user_domain, - ) - elif parsed_args.group: - group = common.find_group( - identity_client, - parsed_args.group, - parsed_args.group_domain, - ) - if parsed_args.domain: domain = common.find_domain( identity_client, parsed_args.domain, ) - elif parsed_args.project: - project = common.find_project( - identity_client, - parsed_args.project, - parsed_args.project_domain, - ) - - # no user or group specified, list all roles in the system - if not parsed_args.user and not parsed_args.group: - if not parsed_args.domain: - columns = ('ID', 'Name') - data = identity_client.roles.list() - else: - columns = ('ID', 'Name', 'Domain') - data = identity_client.roles.list(domain_id=domain.id) - for role in data: - role.domain = domain.name - elif parsed_args.user and parsed_args.domain: - columns = ('ID', 'Name', 'Domain', 'User') - data = identity_client.roles.list( - user=user, - domain=domain, - os_inherit_extension_inherited=parsed_args.inherited - ) - for user_role in data: - user_role.user = user.name - user_role.domain = domain.name - self.log.warning(_('Listing assignments using role list is ' - 'deprecated. Use role assignment list --user ' - ' --domain --names ' - 'instead.')) - elif parsed_args.user and parsed_args.project: - columns = ('ID', 'Name', 'Project', 'User') - data = identity_client.roles.list( - user=user, - project=project, - os_inherit_extension_inherited=parsed_args.inherited - ) - for user_role in data: - user_role.user = user.name - user_role.project = project.name - self.log.warning(_('Listing assignments using role list is ' - 'deprecated. Use role assignment list --user ' - ' --project --names ' - 'instead.')) - elif parsed_args.user: - columns = ('ID', 'Name') - data = identity_client.roles.list( - user=user, - domain='default', - os_inherit_extension_inherited=parsed_args.inherited - ) - self.log.warning(_('Listing assignments using role list is ' - 'deprecated. Use role assignment list --user ' - ' --domain default --names ' - 'instead.')) - elif parsed_args.group and parsed_args.domain: - columns = ('ID', 'Name', 'Domain', 'Group') - data = identity_client.roles.list( - group=group, - domain=domain, - os_inherit_extension_inherited=parsed_args.inherited - ) - for group_role in data: - group_role.group = group.name - group_role.domain = domain.name - self.log.warning(_('Listing assignments using role list is ' - 'deprecated. Use role assignment list --group ' - ' --domain --names ' - 'instead.')) - elif parsed_args.group and parsed_args.project: - columns = ('ID', 'Name', 'Project', 'Group') - data = identity_client.roles.list( - group=group, - project=project, - os_inherit_extension_inherited=parsed_args.inherited - ) - for group_role in data: - group_role.group = group.name - group_role.project = project.name - self.log.warning(_('Listing assignments using role list is ' - 'deprecated. Use role assignment list --group ' - ' --project --names ' - 'instead.')) + columns = ('ID', 'Name', 'Domain') + data = identity_client.roles.list(domain_id=domain.id) + for role in data: + role.domain = domain.name else: - msg = _("Error: If a user or group is specified, " - "either --domain or --project must also be " - "specified to list role grants.") - raise exceptions.CommandError(msg) + columns = ('ID', 'Name') + data = identity_client.roles.list() return (columns, (utils.get_item_properties( diff --git a/openstackclient/tests/functional/identity/v2/test_role.py b/openstackclient/tests/functional/identity/v2/test_role.py index 82e19aaba9..124603d8b6 100644 --- a/openstackclient/tests/functional/identity/v2/test_role.py +++ b/openstackclient/tests/functional/identity/v2/test_role.py @@ -29,38 +29,6 @@ def test_role_list(self): items = self.parse_listing(raw_output) self.assert_table_structure(items, common.BASIC_LIST_HEADERS) - def test_role_list_with_user_project(self): - project_name = self._create_dummy_project() - role_name = self._create_dummy_role() - username = self._create_dummy_user() - raw_output = self.openstack( - 'role add ' - '--project %(project)s ' - '--user %(user)s ' - '%(role)s' % {'project': project_name, - 'user': username, - 'role': role_name}) - self.addCleanup( - self.openstack, - 'role remove ' - '--project %(project)s ' - '--user %(user)s ' - '%(role)s' % {'project': project_name, - 'user': username, - 'role': role_name}) - items = self.parse_show(raw_output) - self.assert_show_fields(items, self.ROLE_FIELDS) - - raw_output = self.openstack( - 'role list ' - '--project %(project)s ' - '--user %(user)s ' - '' % {'project': project_name, - 'user': username}) - items = self.parse_listing(raw_output) - self.assert_table_structure(items, common.BASIC_LIST_HEADERS) - self.assertEqual(1, len(items)) - def test_role_show(self): role_name = self._create_dummy_role() raw_output = self.openstack('role show %s' % role_name) diff --git a/openstackclient/tests/functional/identity/v3/test_role.py b/openstackclient/tests/functional/identity/v3/test_role.py index fb9e061424..38bfff7187 100644 --- a/openstackclient/tests/functional/identity/v3/test_role.py +++ b/openstackclient/tests/functional/identity/v3/test_role.py @@ -31,47 +31,6 @@ def test_role_list(self): items = self.parse_listing(raw_output) self.assert_table_structure(items, common.BASIC_LIST_HEADERS) - def test_role_list_with_user_project(self): - role_name = self._create_dummy_role() - username = self._create_dummy_user() - raw_output = self.openstack( - 'role add ' - '--project %(project)s ' - '--project-domain %(project_domain)s ' - '--user %(user)s ' - '--user-domain %(user_domain)s ' - '%(role)s' % {'project': self.project_name, - 'project_domain': self.domain_name, - 'user': username, - 'user_domain': self.domain_name, - 'role': role_name}) - self.addCleanup( - self.openstack, - 'role remove ' - '--project %(project)s ' - '--project-domain %(project_domain)s ' - '--user %(user)s ' - '--user-domain %(user_domain)s ' - '%(role)s' % {'project': self.project_name, - 'project_domain': self.domain_name, - 'user': username, - 'user_domain': self.domain_name, - 'role': role_name}) - self.assertEqual(0, len(raw_output)) - raw_output = self.openstack( - 'role list ' - '--project %(project)s ' - '--project-domain %(project_domain)s ' - '--user %(user)s ' - '--user-domain %(user_domain)s ' - '' % {'project': self.project_name, - 'project_domain': self.domain_name, - 'user': username, - 'user_domain': self.domain_name}) - items = self.parse_listing(raw_output) - self.assert_table_structure(items, common.BASIC_LIST_HEADERS) - self.assertEqual(1, len(items)) - def test_role_show(self): role_name = self._create_dummy_role() raw_output = self.openstack('role show %s' % role_name) diff --git a/openstackclient/tests/unit/identity/v2_0/test_role.py b/openstackclient/tests/unit/identity/v2_0/test_role.py index 684ce803a2..643d77f600 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role.py @@ -278,7 +278,7 @@ def setUp(self): # Get the command object to test self.cmd = role.ListRole(self.app, None) - def test_role_list_no_options(self): + def test_role_list(self): arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -299,137 +299,6 @@ def test_role_list_no_options(self): self.assertEqual(datalist, tuple(data)) -class TestUserRoleList(TestRole): - - columns = ( - 'ID', - 'Name', - 'Project', - 'User' - ) - - def setUp(self): - super(TestUserRoleList, self).setUp() - - self.projects_mock.get.return_value = self.fake_project - - self.users_mock.get.return_value = self.fake_user - - self.roles_mock.roles_for_user.return_value = [self.fake_role] - - # Get the command object to test - self.cmd = role.ListUserRole(self.app, None) - - def test_user_role_list_no_options_unscoped_token(self): - auth_ref = identity_fakes.fake_auth_ref( - identity_fakes.UNSCOPED_TOKEN, - fake_service=self.fake_service, - ) - self.ar_mock = mock.PropertyMock(return_value=auth_ref) - type(self.app.client_manager).auth_ref = self.ar_mock - - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # This argument combination should raise a CommandError - self.assertRaises( - exceptions.CommandError, - self.cmd.take_action, - parsed_args, - ) - - def test_user_role_list_no_options_scoped_token(self): - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - self.roles_mock.roles_for_user.assert_called_with( - self.fake_user.id, - self.fake_project.id, - ) - - collist = ('ID', 'Name', 'Project', 'User') - self.assertEqual(collist, columns) - datalist = (( - self.fake_role.id, - self.fake_role.name, - self.fake_project.name, - self.fake_user.name, - ), ) - self.assertEqual(datalist, tuple(data)) - - def test_user_role_list_project_unscoped_token(self): - auth_ref = identity_fakes.fake_auth_ref( - identity_fakes.UNSCOPED_TOKEN, - fake_service=self.fake_service, - ) - self.ar_mock = mock.PropertyMock(return_value=auth_ref) - type(self.app.client_manager).auth_ref = self.ar_mock - - self.projects_mock.get.return_value = self.fake_project - arglist = [ - '--project', self.fake_project.name, - ] - verifylist = [ - ('project', self.fake_project.name), - ] - 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.roles_mock.roles_for_user.assert_called_with( - self.fake_user.id, - self.fake_project.id, - ) - - self.assertEqual(columns, columns) - datalist = (( - self.fake_role.id, - self.fake_role.name, - self.fake_project.name, - self.fake_user.name, - ), ) - self.assertEqual(datalist, tuple(data)) - - def test_user_role_list_project_scoped_token(self): - self.projects_mock.get.return_value = self.fake_project - arglist = [ - '--project', self.fake_project.name, - ] - verifylist = [ - ('project', self.fake_project.name), - ] - 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.roles_mock.roles_for_user.assert_called_with( - self.fake_user.id, - self.fake_project.id, - ) - - self.assertEqual(columns, columns) - datalist = (( - self.fake_role.id, - self.fake_role.name, - self.fake_project.name, - self.fake_user.name, - ), ) - self.assertEqual(datalist, tuple(data)) - - class TestRoleRemove(TestRole): def setUp(self): diff --git a/openstackclient/tests/unit/identity/v2_0/test_service.py b/openstackclient/tests/unit/identity/v2_0/test_service.py index 1948bf4ab0..6c4374eff3 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_service.py +++ b/openstackclient/tests/unit/identity/v2_0/test_service.py @@ -55,43 +55,14 @@ def setUp(self): # Get the command object to test self.cmd = service.CreateService(self.app, None) - def test_service_create_with_type_positional(self): + def test_service_create(self): arglist = [ self.fake_service_c.type, ] verifylist = [ - ('type_or_name', self.fake_service_c.type), - ('type', None), - ('description', None), - ('name', None), - ] - 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) - - # ServiceManager.create(name, service_type, description) - self.services_mock.create.assert_called_with( - None, - self.fake_service_c.type, - None, - ) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) - - def test_service_create_with_type_option(self): - arglist = [ - '--type', self.fake_service_c.type, - self.fake_service_c.name, - ] - verifylist = [ - ('type_or_name', self.fake_service_c.name), ('type', self.fake_service_c.type), - ('description', None), ('name', None), + ('description', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -102,7 +73,7 @@ def test_service_create_with_type_option(self): # ServiceManager.create(name, service_type, description) self.services_mock.create.assert_called_with( - self.fake_service_c.name, + None, self.fake_service_c.type, None, ) @@ -116,10 +87,9 @@ def test_service_create_with_name_option(self): self.fake_service_c.type, ] verifylist = [ - ('type_or_name', self.fake_service_c.type), - ('type', None), - ('description', None), + ('type', self.fake_service_c.type), ('name', self.fake_service_c.name), + ('description', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -145,10 +115,9 @@ def test_service_create_description(self): self.fake_service_c.type, ] verifylist = [ - ('type_or_name', self.fake_service_c.type), - ('type', None), - ('description', self.fake_service_c.description), + ('type', self.fake_service_c.type), ('name', self.fake_service_c.name), + ('description', self.fake_service_c.description), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 281d530c7e..99f3a2de74 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -508,21 +508,6 @@ def setUp(self): copy.deepcopy(identity_fakes.DOMAIN), loaded=True, ) - self.projects_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROJECT), - loaded=True, - ) - self.users_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.USER), - loaded=True, - ) - self.groups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.GROUP), - loaded=True, - ) # Get the command object to test self.cmd = role.ListRole(self.app, None) @@ -542,212 +527,6 @@ def test_role_list_no_options(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_user_list_inherited(self): - arglist = [ - '--user', identity_fakes.user_id, - '--inherited', - ] - verifylist = [ - ('user', identity_fakes.user_id), - ('inherited', 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 = { - 'domain': 'default', - 'user': self.users_mock.get(), - 'os_inherit_extension_inherited': True, - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) - - def test_user_list_user(self): - arglist = [ - '--user', identity_fakes.user_id, - ] - verifylist = [ - ('user', identity_fakes.user_id), - ] - 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 = { - 'domain': 'default', - 'user': self.users_mock.get(), - 'os_inherit_extension_inherited': False - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) - - def test_role_list_domain_user(self): - arglist = [ - '--domain', identity_fakes.domain_name, - '--user', identity_fakes.user_id, - ] - verifylist = [ - ('domain', identity_fakes.domain_name), - ('user', identity_fakes.user_id), - ] - 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 = { - 'domain': self.domains_mock.get(), - 'user': self.users_mock.get(), - 'os_inherit_extension_inherited': False - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name', 'Domain', 'User') - self.assertEqual(collist, columns) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - identity_fakes.domain_name, - identity_fakes.user_name, - ), ) - self.assertEqual(datalist, tuple(data)) - - def test_role_list_domain_group(self): - arglist = [ - '--domain', identity_fakes.domain_name, - '--group', identity_fakes.group_id, - ] - verifylist = [ - ('domain', identity_fakes.domain_name), - ('group', identity_fakes.group_id), - ] - 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 = { - 'domain': self.domains_mock.get(), - 'group': self.groups_mock.get(), - 'os_inherit_extension_inherited': False - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name', 'Domain', 'Group') - self.assertEqual(collist, columns) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - identity_fakes.domain_name, - identity_fakes.group_name, - ), ) - self.assertEqual(datalist, tuple(data)) - - def test_role_list_project_user(self): - arglist = [ - '--project', identity_fakes.project_name, - '--user', identity_fakes.user_id, - ] - verifylist = [ - ('project', identity_fakes.project_name), - ('user', identity_fakes.user_id), - ] - 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 = { - 'project': self.projects_mock.get(), - 'user': self.users_mock.get(), - 'os_inherit_extension_inherited': False - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name', 'Project', 'User') - self.assertEqual(collist, columns) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - identity_fakes.project_name, - identity_fakes.user_name, - ), ) - self.assertEqual(datalist, tuple(data)) - - def test_role_list_project_group(self): - arglist = [ - '--project', identity_fakes.project_name, - '--group', identity_fakes.group_id, - ] - verifylist = [ - ('project', identity_fakes.project_name), - ('group', identity_fakes.group_id), - ] - 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 = { - 'project': self.projects_mock.get(), - 'group': self.groups_mock.get(), - 'os_inherit_extension_inherited': False - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name', 'Project', 'Group') - self.assertEqual(collist, columns) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - identity_fakes.project_name, - identity_fakes.group_name, - ), ) - self.assertEqual(datalist, tuple(data)) - def test_role_list_domain_role(self): self.roles_mock.list.return_value = [ fakes.FakeResource( @@ -787,17 +566,6 @@ def test_role_list_domain_role(self): ), ) self.assertEqual(datalist, tuple(data)) - def test_role_list_group_with_error(self): - arglist = [ - '--group', identity_fakes.group_id, - ] - verifylist = [ - ('group', identity_fakes.group_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) - class TestRoleRemove(TestRole): diff --git a/releasenotes/notes/osc4-identity-6564257c67d43106.yaml b/releasenotes/notes/osc4-identity-6564257c67d43106.yaml new file mode 100644 index 0000000000..a5105c801c --- /dev/null +++ b/releasenotes/notes/osc4-identity-6564257c67d43106.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + Remove deprecated ``role list`` options ``--project`` and ``--user``. + Use ``role assignment list`` options ``--project`` and ``--user`` instead. + - | + Remove deprecated ``user role list`` command. + Use ``role assignment list`` options ``--project`` and ``--user`` instead. + - | + Remove deprecated ``service create`` option ``--type``. + The type is supplied as a positional argument in The + ``service create --name type`` command. diff --git a/setup.cfg b/setup.cfg index 031776a99d..cc57a1037a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -192,8 +192,6 @@ openstack.identity.v2 = user_set = openstackclient.identity.v2_0.user:SetUser user_show = openstackclient.identity.v2_0.user:ShowUser - user_role_list = openstackclient.identity.v2_0.role:ListUserRole - openstack.identity.v3 = access_token_create = openstackclient.identity.v3.token:CreateAccessToken From 67dadda746759d8cf47822e6f426c473e46acc27 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 15 May 2019 22:20:25 -0500 Subject: [PATCH 0087/1120] Remove deprecated image commands * Remove ``image create|set`` option ``--owner`` Change-Id: I1fabab98c8660eba6d0dd75e74544c6c9d432b9e Signed-off-by: Dean Troyer --- doc/source/cli/backwards-incompatible.rst | 6 +++ doc/source/cli/data/glance.csv | 48 ++++++++--------- openstackclient/image/v1/image.py | 30 +---------- openstackclient/image/v2/image.py | 49 +++-------------- .../tests/unit/image/v2/test_image.py | 52 ------------------- .../notes/osc4-image-e922ee6f8e028648.yaml | 5 ++ 6 files changed, 43 insertions(+), 147 deletions(-) create mode 100644 releasenotes/notes/osc4-image-e922ee6f8e028648.yaml diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index 51743cb2b2..84fa4897d5 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -49,6 +49,12 @@ Release 4.0 * Removed in: 4.0 * Commit: https://review.opendev.org/612798 +6. Remove ``image create|set`` option ``--owner``. + Use ``--project`` option instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/659431 + .. 1. Change ``volume transfer request accept`` to use new option ``--auth-key`` .. rather than a second positional argument. diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index 2985e307c7..994a8fdaae 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -1,24 +1,24 @@ -explain,WONTFIX,Describe a specific model. -image-create,image create,Create a new image. -image-deactivate,image set --deactivate,Deactivate specified image. -image-delete,image delete,Delete specified image. -image-download,image save,Download a specific image. -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-tag-delete,image set --tag ,Delete the tag associated with the given image. -image-tag-update,image unset --tag ,Update an image with the given tag. -image-update,image set,Update an existing image. -image-upload,,Upload data for a specific image. -location-add,,Add a location (and related metadata) to an image. -location-delete,,Remove locations (and related metadata) from an image. -location-update,,Update metadata of an image's location. -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-update,image set --accept --reject --status,Update the status of a member for a given image. -task-create,,Create a new task. -task-list,,List tasks you can access. -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. \ No newline at end of file +explain,WONTFIX,Describe a specific model. +image-create,image create,Create a new image. +image-deactivate,image set --deactivate,Deactivate specified image. +image-delete,image delete,Delete specified image. +image-download,image save,Download a specific image. +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-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. +image-upload,,Upload data for a specific image. +location-add,,Add a location (and related metadata) to an image. +location-delete,,Remove locations (and related metadata) from an image. +location-update,,Update metadata of an image's location. +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-update,image set --accept --reject --status,Update the status of a member for a given image. +task-create,,Create a new task. +task-list,,List tasks you can access. +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/v1/image.py b/openstackclient/image/v1/image.py index 64c4049cfd..caf3d54f47 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -182,29 +182,16 @@ def get_parser(self, prog_name): help=_("Set a property on this image " "(repeat option to set multiple properties)"), ) - # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early - # 2.x release. Do not remove before Jan 2017 - # and a 3.x release. - project_group = parser.add_mutually_exclusive_group() - project_group.add_argument( + parser.add_argument( "--project", metavar="", help=_("Set an alternate project on this image (name or ID)"), ) - project_group.add_argument( - "--owner", - metavar="", - help=argparse.SUPPRESS, - ) return parser def take_action(self, parsed_args): image_client = self.app.client_manager.image - if getattr(parsed_args, 'owner', None) is not None: - LOG.warning(_('The --owner option is deprecated, ' - 'please use --project instead.')) - # Build an attribute dict from the parsed args, only include # attributes that were actually set on the command line kwargs = {} @@ -599,29 +586,16 @@ def get_parser(self, prog_name): metavar="", help=_("Image hash used for verification"), ) - # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early - # 2.x release. Do not remove before Jan 2017 - # and a 3.x release. - project_group = parser.add_mutually_exclusive_group() - project_group.add_argument( + parser.add_argument( "--project", metavar="", help=_("Set an alternate project on this image (name or ID)"), ) - project_group.add_argument( - "--owner", - metavar="", - help=argparse.SUPPRESS, - ) return parser def take_action(self, parsed_args): image_client = self.app.client_manager.image - if getattr(parsed_args, 'owner', None) is not None: - LOG.warning(_('The --owner option is deprecated, ' - 'please use --project instead.')) - kwargs = {} copy_attrs = ('name', 'owner', 'min_disk', 'min_ram', 'properties', 'container_format', 'disk_format', 'size', 'store', diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index bdec99d7be..97169a9237 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -250,20 +250,11 @@ def get_parser(self, prog_name): help=_("Set a tag on this image " "(repeat option to set multiple tags)"), ) - # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early - # 2.x release. Do not remove before Jan 2017 - # and a 3.x release. - project_group = parser.add_mutually_exclusive_group() - project_group.add_argument( + parser.add_argument( "--project", metavar="", help=_("Set an alternate project on this image (name or ID)"), ) - project_group.add_argument( - "--owner", - metavar="", - help=argparse.SUPPRESS, - ) common.add_project_domain_option_to_parser(parser) for deadopt in self.deadopts: parser.add_argument( @@ -321,16 +312,10 @@ def take_action(self, parsed_args): kwargs['visibility'] = 'community' if parsed_args.shared: kwargs['visibility'] = 'shared' - # Handle deprecated --owner option - project_arg = parsed_args.project - if parsed_args.owner: - project_arg = parsed_args.owner - LOG.warning(_('The --owner option is deprecated, ' - 'please use --project instead.')) - if project_arg: + if parsed_args.project: kwargs['owner'] = common.find_project( identity_client, - project_arg, + parsed_args.project, parsed_args.project_domain, ).id @@ -347,13 +332,6 @@ def take_action(self, parsed_args): LOG.warning(_("Failed to get an image file.")) return {}, {} - if parsed_args.owner: - kwargs['owner'] = common.find_project( - identity_client, - parsed_args.owner, - parsed_args.project_domain, - ).id - # 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: @@ -933,20 +911,11 @@ def get_parser(self, prog_name): action="store_true", help=_("Activate the image"), ) - # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early - # 2.x release. Do not remove before Jan 2017 - # and a 3.x release. - project_group = parser.add_mutually_exclusive_group() - project_group.add_argument( + parser.add_argument( "--project", metavar="", help=_("Set an alternate project on this image (name or ID)"), ) - project_group.add_argument( - "--owner", - metavar="", - help=argparse.SUPPRESS, - ) common.add_project_domain_option_to_parser(parser) for deadopt in self.deadopts: parser.add_argument( @@ -1020,17 +989,11 @@ def take_action(self, parsed_args): kwargs['visibility'] = 'community' if parsed_args.shared: kwargs['visibility'] = 'shared' - # Handle deprecated --owner option - project_arg = parsed_args.project - if parsed_args.owner: - project_arg = parsed_args.owner - LOG.warning(_('The --owner option is deprecated, ' - 'please use --project instead.')) project_id = None - if project_arg: + if parsed_args.project: project_id = common.find_project( identity_client, - project_arg, + parsed_args.project, parsed_args.project_domain, ).id kwargs['owner'] = project_id diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 4cc8f44827..45b5a0a8cb 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -188,40 +188,6 @@ def test_image_reserve_options(self, mock_open): image_fakes.FakeImage.get_image_data(self.new_image), data) - def test_image_create_with_unexist_owner(self): - self.project_mock.get.side_effect = exceptions.NotFound(None) - self.project_mock.find.side_effect = exceptions.NotFound(None) - - arglist = [ - '--container-format', 'ovf', - '--disk-format', 'ami', - '--min-disk', '10', - '--min-ram', '4', - '--owner', 'unexist_owner', - '--protected', - '--private', - image_fakes.image_name, - ] - verifylist = [ - ('container_format', 'ovf'), - ('disk_format', 'ami'), - ('min_disk', 10), - ('min_ram', 4), - ('owner', 'unexist_owner'), - ('protected', True), - ('unprotected', False), - ('public', False), - ('private', True), - ('name', image_fakes.image_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises( - exceptions.CommandError, - self.cmd.take_action, - parsed_args, - ) - def test_image_create_with_unexist_project(self): self.project_mock.get.side_effect = exceptions.NotFound(None) self.project_mock.find.side_effect = exceptions.NotFound(None) @@ -1146,24 +1112,6 @@ def test_image_set_options(self): image_fakes.image_id, **kwargs) self.assertIsNone(result) - def test_image_set_with_unexist_owner(self): - self.project_mock.get.side_effect = exceptions.NotFound(None) - self.project_mock.find.side_effect = exceptions.NotFound(None) - - arglist = [ - '--owner', 'unexist_owner', - image_fakes.image_id, - ] - verifylist = [ - ('owner', 'unexist_owner'), - ('image', image_fakes.image_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises( - exceptions.CommandError, - self.cmd.take_action, parsed_args) - def test_image_set_with_unexist_project(self): self.project_mock.get.side_effect = exceptions.NotFound(None) self.project_mock.find.side_effect = exceptions.NotFound(None) diff --git a/releasenotes/notes/osc4-image-e922ee6f8e028648.yaml b/releasenotes/notes/osc4-image-e922ee6f8e028648.yaml new file mode 100644 index 0000000000..eab3f70c21 --- /dev/null +++ b/releasenotes/notes/osc4-image-e922ee6f8e028648.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Remove ``image create|set`` option ``--owner``. + Use ``--project`` option instead. From 5a0fc68a87d1c9733c1dd5bb6f68b2e518fe2105 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 26 Oct 2018 11:50:32 -0500 Subject: [PATCH 0088/1120] Remove deprecated network options The following were deprecated for several releases and can now be removed: * Remove ``port create|set`` options ``--device-id`` and ``--port-id`` * Remove ``router set`` option ``--clear-routes`` * Remove ``security group rule create`` options ``--src-group`` and ``--src-ip`` These are backwards incompatible changes and will require a major version bump after they are merged. Change-Id: Ieae74c14f6b3e263721a3146cf76f94a9ab792f6 Signed-off-by: Sean McGinnis Signed-off-by: Dean Troyer --- doc/source/cli/backwards-incompatible.rst | 18 ++++ openstackclient/network/v2/port.py | 35 +------- openstackclient/network/v2/router.py | 19 +---- .../network/v2/security_group_rule.py | 63 ++------------ .../tests/unit/network/v2/test_router.py | 46 ---------- .../v2/test_security_group_rule_compute.py | 83 +------------------ .../v2/test_security_group_rule_network.py | 32 +++---- .../notes/osc4-network-db2aed696d964ca6.yaml | 12 +++ 8 files changed, 56 insertions(+), 252 deletions(-) create mode 100644 releasenotes/notes/osc4-network-db2aed696d964ca6.yaml diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index 84fa4897d5..cbf1823c13 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -55,6 +55,24 @@ Release 4.0 * Removed in: 4.0 * Commit: https://review.opendev.org/659431 +7. Remove ``port create|set`` options ``--device-id`` and ``--host-id``. + Use ``--device`` and ``--host`` instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/613644 + +8. Remove ``router set`` option ``--clear-routes``. + Use ``no-route`` option instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/613644 + +9. Remove ``security group rule create`` options ``--src-ip`` and ``--src-group``. + Use ``--remote-ip`` and ``--remote-group`` options instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/613644 + .. 1. Change ``volume transfer request accept`` to use new option ``--auth-key`` .. rather than a second positional argument. diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index f6d6fc7280..50bc742f28 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -96,21 +96,6 @@ def __call__(self, parser, namespace, values, option_string=None): def _get_attrs(client_manager, parsed_args): attrs = {} - # Handle deprecated options - # NOTE(dtroyer): --device-id and --host-id were deprecated in Mar 2016. - # Do not remove before 3.x release or Mar 2017. - if parsed_args.device_id: - attrs['device_id'] = parsed_args.device_id - LOG.warning(_( - 'The --device-id option is deprecated, ' - 'please use --device instead.' - )) - if parsed_args.host_id: - attrs['binding:host_id'] = parsed_args.host_id - LOG.warning(_( - 'The --host-id option is deprecated, ' - 'please use --host instead.' - )) if parsed_args.description is not None: attrs['description'] = parsed_args.description if parsed_args.device: @@ -235,19 +220,11 @@ def _add_updatable_args(parser): metavar='', help=_("Description of this port") ) - # NOTE(dtroyer): --device-id is deprecated in Mar 2016. Do not - # remove before 3.x release or Mar 2017. - device_group = parser.add_mutually_exclusive_group() - device_group.add_argument( + parser.add_argument( '--device', metavar='', help=_("Port device ID") ) - device_group.add_argument( - '--device-id', - metavar='', - help=argparse.SUPPRESS, - ) parser.add_argument( '--mac-address', metavar='', @@ -268,19 +245,11 @@ def _add_updatable_args(parser): "macvtap | normal | baremetal | virtio-forwarder, " "default: normal)") ) - # NOTE(dtroyer): --host-id is deprecated in Mar 2016. Do not - # remove before 3.x release or Mar 2017. - host_group = parser.add_mutually_exclusive_group() - host_group.add_argument( + parser.add_argument( '--host', metavar='', help=_("Allocate port on host (ID only)") ) - host_group.add_argument( - '--host-id', - metavar='', - help=argparse.SUPPRESS, - ) parser.add_argument( '--dns-domain', metavar='dns-domain', diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 2ec3e2f094..13de66aaa7 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -13,7 +13,6 @@ """Router action implementations""" -import argparse import copy import json import logging @@ -528,8 +527,6 @@ def get_parser(self, prog_name): action='store_true', help=_("Set router to centralized mode (disabled router only)") ) - routes_group = parser.add_mutually_exclusive_group() - # ToDo(Reedip):Remove mutual exclusiveness once clear-routes is removed parser.add_argument( '--route', metavar='destination=,gateway=', @@ -542,18 +539,13 @@ def get_parser(self, prog_name): "gateway: nexthop IP address " "(repeat option to set multiple routes)") ) - routes_group.add_argument( + parser.add_argument( '--no-route', action='store_true', help=_("Clear routes associated with the router. " "Specify both --route and --no-route to overwrite " "current value of route.") ) - routes_group.add_argument( - '--clear-routes', - action='store_true', - help=argparse.SUPPRESS, - ) routes_ha = parser.add_mutually_exclusive_group() routes_ha.add_argument( '--ha', @@ -619,21 +611,16 @@ def take_action(self, parsed_args): attrs['ha'] = True elif parsed_args.no_ha: attrs['ha'] = False - if parsed_args.clear_routes: - LOG.warning(_( - 'The --clear-routes option is deprecated, ' - 'please use --no-route instead.' - )) if parsed_args.routes is not None: for route in parsed_args.routes: route['nexthop'] = route.pop('gateway') attrs['routes'] = parsed_args.routes - if not (parsed_args.no_route or parsed_args.clear_routes): + if not parsed_args.no_route: # Map the route keys and append to the current routes. # The REST API will handle route validation and duplicates. attrs['routes'] += obj.routes - elif parsed_args.no_route or parsed_args.clear_routes: + elif parsed_args.no_route: attrs['routes'] = [] if (parsed_args.disable_snat or parsed_args.enable_snat or parsed_args.fixed_ip) and not parsed_args.external_gateway: diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 961125a9a7..df19af201f 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -115,19 +115,6 @@ def update_parser_common(self, parser): metavar="", help=_("Remote security group (name or ID)"), ) - # Handle deprecated options - # NOTE(dtroyer): --src-ip and --src-group were deprecated in Nov 2016. - # Do not remove before 4.x release or Nov 2017. - remote_group.add_argument( - "--src-ip", - metavar="", - help=argparse.SUPPRESS, - ) - remote_group.add_argument( - "--src-group", - metavar="", - help=argparse.SUPPRESS, - ) return parser def update_parser_network(self, parser): @@ -310,31 +297,13 @@ def take_action_network(self, client, parsed_args): if parsed_args.icmp_code is not None and parsed_args.icmp_code >= 0: attrs['port_range_max'] = parsed_args.icmp_code - # NOTE(dtroyer): --src-ip and --src-group were deprecated in Nov 2016. - # Do not remove before 4.x release or Nov 2017. - if not (parsed_args.remote_group is None and - parsed_args.src_group is None): + if parsed_args.remote_group is not None: attrs['remote_group_id'] = client.find_security_group( - parsed_args.remote_group or parsed_args.src_group, + parsed_args.remote_group, ignore_missing=False ).id - if parsed_args.src_group: - LOG.warning( - _("The %(old)s option is deprecated, " - "please use %(new)s instead."), - {'old': '--src-group', 'new': '--remote-group'}, - ) - elif not (parsed_args.remote_ip is None and - parsed_args.src_ip is None): - attrs['remote_ip_prefix'] = ( - parsed_args.remote_ip or parsed_args.src_ip - ) - if parsed_args.src_ip: - LOG.warning( - _("The %(old)s option is deprecated, " - "please use %(new)s instead."), - {'old': '--src-ip', 'new': '--remote-ip'}, - ) + elif parsed_args.remote_ip is not None: + attrs['remote_ip_prefix'] = parsed_args.remote_ip elif attrs['ethertype'] == 'IPv4': attrs['remote_ip_prefix'] = '0.0.0.0/0' attrs['security_group_id'] = security_group_id @@ -361,29 +330,13 @@ def take_action_compute(self, client, parsed_args): else: from_port, to_port = parsed_args.dst_port - # NOTE(dtroyer): --src-ip and --src-group were deprecated in Nov 2016. - # Do not remove before 4.x release or Nov 2017. remote_ip = None - if not (parsed_args.remote_group is None and - parsed_args.src_group is None): + if parsed_args.remote_group is not None: parsed_args.remote_group = client.api.security_group_find( - parsed_args.remote_group or parsed_args.src_group, + parsed_args.remote_group, )['id'] - if parsed_args.src_group: - LOG.warning( - _("The %(old)s option is deprecated, " - "please use %(new)s instead."), - {'old': '--src-group', 'new': '--remote-group'}, - ) - if not (parsed_args.remote_ip is None and - parsed_args.src_ip is None): - remote_ip = parsed_args.remote_ip or parsed_args.src_ip - if parsed_args.src_ip: - LOG.warning( - _("The %(old)s option is deprecated, " - "please use %(new)s instead."), - {'old': '--src-ip', 'new': '--remote-ip'}, - ) + if parsed_args.remote_ip is not None: + remote_ip = parsed_args.remote_ip else: remote_ip = '0.0.0.0/0' diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 618adf3516..a07b2e4b12 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -939,52 +939,6 @@ def test_set_route_overwrite_route(self): _testrouter, **attrs) self.assertIsNone(result) - def test_set_clear_routes(self): - arglist = [ - self._router.name, - '--clear-routes', - ] - verifylist = [ - ('router', self._router.name), - ('clear_routes', True), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - attrs = { - 'routes': [], - } - self.network.update_router.assert_called_once_with( - self._router, **attrs) - self.assertIsNone(result) - - def test_overwrite_route_clear_routes(self): - _testrouter = network_fakes.FakeRouter.create_one_router( - {'routes': [{"destination": "10.0.0.2", - "nexthop": "1.1.1.1"}]}) - self.network.find_router = mock.Mock(return_value=_testrouter) - arglist = [ - _testrouter.name, - '--route', 'destination=10.20.30.0/24,gateway=10.20.30.1', - '--clear-routes', - ] - verifylist = [ - ('router', _testrouter.name), - ('routes', [{'destination': '10.20.30.0/24', - 'gateway': '10.20.30.1'}]), - ('clear_routes', True), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - attrs = { - 'routes': [{'destination': '10.20.30.0/24', - 'nexthop': '10.20.30.1'}] - } - self.network.update_router.assert_called_once_with( - _testrouter, **attrs) - self.assertIsNone(result) - def test_set_nothing(self): arglist = [ self._router.name, diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py index 5c1937e307..6814c197b3 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py @@ -72,15 +72,6 @@ def test_security_group_rule_create_no_options(self, sgr_mock): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, [], []) - def test_security_group_rule_create_all_source_options(self, sgr_mock): - arglist = [ - '--src-ip', '10.10.0.0/24', - '--src-group', self._security_group['id'], - self._security_group['id'], - ] - self.assertRaises(tests_utils.ParserException, - self.check_parser, self.cmd, arglist, []) - def test_security_group_rule_create_all_remote_options(self, sgr_mock): arglist = [ '--remote-ip', '10.10.0.0/24', @@ -151,41 +142,6 @@ def test_security_group_rule_create_default_rule(self, sgr_mock): self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) - def test_security_group_rule_create_source_group(self, sgr_mock): - expected_columns, expected_data = self._setup_security_group_rule({ - 'from_port': 22, - 'to_port': 22, - 'group': {'name': self._security_group['name']}, - }) - sgr_mock.return_value = self._security_group_rule - arglist = [ - '--dst-port', str(self._security_group_rule['from_port']), - '--src-group', self._security_group['name'], - self._security_group['id'], - ] - verifylist = [ - ('dst_port', (self._security_group_rule['from_port'], - self._security_group_rule['to_port'])), - ('src_group', self._security_group['name']), - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # TODO(dtroyer): save this for the security group rule changes - # self.compute.api.security_group_rule_create.assert_called_once_with( - sgr_mock.assert_called_once_with( - security_group_id=self._security_group['id'], - ip_protocol=self._security_group_rule['ip_protocol'], - from_port=self._security_group_rule['from_port'], - to_port=self._security_group_rule['to_port'], - remote_ip=self._security_group_rule['ip_range']['cidr'], - remote_group=self._security_group['id'], - ) - self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) - def test_security_group_rule_create_remote_group(self, sgr_mock): expected_columns, expected_data = self._setup_security_group_rule({ 'from_port': 22, @@ -221,41 +177,6 @@ def test_security_group_rule_create_remote_group(self, sgr_mock): self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) - def test_security_group_rule_create_source_ip(self, sgr_mock): - expected_columns, expected_data = self._setup_security_group_rule({ - 'ip_protocol': 'icmp', - 'from_port': -1, - 'to_port': -1, - 'ip_range': {'cidr': '10.0.2.0/24'}, - }) - sgr_mock.return_value = self._security_group_rule - arglist = [ - '--protocol', self._security_group_rule['ip_protocol'], - '--src-ip', self._security_group_rule['ip_range']['cidr'], - self._security_group['id'], - ] - verifylist = [ - ('protocol', self._security_group_rule['ip_protocol']), - ('src_ip', self._security_group_rule['ip_range']['cidr']), - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # TODO(dtroyer): save this for the security group rule changes - # self.compute.api.security_group_rule_create.assert_called_once_with( - sgr_mock.assert_called_once_with( - security_group_id=self._security_group['id'], - ip_protocol=self._security_group_rule['ip_protocol'], - from_port=self._security_group_rule['from_port'], - to_port=self._security_group_rule['to_port'], - remote_ip=self._security_group_rule['ip_range']['cidr'], - remote_group=None, - ) - self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) - def test_security_group_rule_create_remote_ip(self, sgr_mock): expected_columns, expected_data = self._setup_security_group_rule({ 'ip_protocol': 'icmp', @@ -301,13 +222,13 @@ def test_security_group_rule_create_proto_option(self, sgr_mock): sgr_mock.return_value = self._security_group_rule arglist = [ '--proto', self._security_group_rule['ip_protocol'], - '--src-ip', self._security_group_rule['ip_range']['cidr'], + '--remote-ip', self._security_group_rule['ip_range']['cidr'], self._security_group['id'], ] verifylist = [ ('proto', self._security_group_rule['ip_protocol']), ('protocol', None), - ('src_ip', self._security_group_rule['ip_range']['cidr']), + ('remote_ip', self._security_group_rule['ip_range']['cidr']), ('group', self._security_group['id']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index b070ab6aab..2b0de0d2a1 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -99,15 +99,6 @@ def test_create_no_options(self): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, [], []) - def test_create_all_source_options(self): - arglist = [ - '--src-ip', '10.10.0.0/24', - '--src-group', self._security_group.id, - self._security_group.id, - ] - self.assertRaises(tests_utils.ParserException, - self.check_parser, self.cmd, arglist, []) - def test_create_all_remote_options(self): arglist = [ '--remote-ip', '10.10.0.0/24', @@ -212,13 +203,13 @@ def test_create_proto_option(self): }) arglist = [ '--proto', self._security_group_rule.protocol, - '--src-ip', self._security_group_rule.remote_ip_prefix, + '--remote-ip', self._security_group_rule.remote_ip_prefix, self._security_group.id, ] verifylist = [ ('proto', self._security_group_rule.protocol), ('protocol', None), - ('src_ip', self._security_group_rule.remote_ip_prefix), + ('remote_ip', self._security_group_rule.remote_ip_prefix), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -242,13 +233,13 @@ def test_create_protocol_any(self): }) arglist = [ '--proto', 'any', - '--src-ip', self._security_group_rule.remote_ip_prefix, + '--remote-ip', self._security_group_rule.remote_ip_prefix, self._security_group.id, ] verifylist = [ ('proto', 'any'), ('protocol', None), - ('src_ip', self._security_group_rule.remote_ip_prefix), + ('remote_ip', self._security_group_rule.remote_ip_prefix), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -269,19 +260,18 @@ def test_create_remote_group(self): self._setup_security_group_rule({ 'port_range_max': 22, 'port_range_min': 22, - 'remote_group_id': self._security_group.id, }) arglist = [ '--dst-port', str(self._security_group_rule.port_range_min), '--ingress', - '--src-group', self._security_group.name, + '--remote-group', self._security_group.name, self._security_group.id, ] verifylist = [ ('dst_port', (self._security_group_rule.port_range_min, self._security_group_rule.port_range_max)), ('ingress', True), - ('src_group', self._security_group.name), + ('remote_group', self._security_group.name), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -294,7 +284,7 @@ def test_create_remote_group(self): 'port_range_max': self._security_group_rule.port_range_max, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, - 'remote_group_id': self._security_group_rule.remote_group_id, + 'remote_group_id': self._security_group.id, 'security_group_id': self._security_group.id, }) self.assertEqual(self.expected_columns, columns) @@ -306,12 +296,12 @@ def test_create_source_group(self): }) arglist = [ '--ingress', - '--src-group', self._security_group.name, + '--remote-group', self._security_group.name, self._security_group.id, ] verifylist = [ ('ingress', True), - ('src_group', self._security_group.name), + ('remote_group', self._security_group.name), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -335,12 +325,12 @@ def test_create_source_ip(self): }) arglist = [ '--protocol', self._security_group_rule.protocol, - '--src-ip', self._security_group_rule.remote_ip_prefix, + '--remote-ip', self._security_group_rule.remote_ip_prefix, self._security_group.id, ] verifylist = [ ('protocol', self._security_group_rule.protocol), - ('src_ip', self._security_group_rule.remote_ip_prefix), + ('remote_ip', self._security_group_rule.remote_ip_prefix), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/releasenotes/notes/osc4-network-db2aed696d964ca6.yaml b/releasenotes/notes/osc4-network-db2aed696d964ca6.yaml new file mode 100644 index 0000000000..7ae300158f --- /dev/null +++ b/releasenotes/notes/osc4-network-db2aed696d964ca6.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + Remove deprecated ``port create|set`` options ``--device-id`` and ``--host-id``. + Use ``--device`` and ``--host`` options instead. + - | + Remove deprecated ``router set`` option ``--clear-routes``. + Use ``--no-route`` option instead. + - | + Remove deprecated ``security group rule create`` options ``--src-ip`` and + ``--src-group``. + Use ``--remote-ip`` and ``--remote-group`` options instead. From 6f1f44d422ed3f384ea6d4d729227277f0988b99 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 May 2019 16:02:13 -0500 Subject: [PATCH 0089/1120] Batch up minor cleanups for release Change-Id: Id45788e17c5388cee54e79cab1c120cfcc8f9f62 Signed-off-by: Dean Troyer --- .../tests/functional/network/v2/test_floating_ip.py | 2 +- .../tests/functional/network/v2/test_subnet_pool.py | 4 ++-- releasenotes/notes/bug-1633582-df2bee534c2da7fc.yaml | 4 ++-- releasenotes/notes/bug-27882-402ced7ffe930058.yaml | 2 +- releasenotes/source/pre_20_releases.rst | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index 1d11fc5d2d..f189c2da6c 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -83,7 +83,7 @@ def setUpClass(cls): raise pass else: - # break and no longer retry if create sucessfully + # break and no longer retry if create successfully break @classmethod diff --git a/openstackclient/tests/functional/network/v2/test_subnet_pool.py b/openstackclient/tests/functional/network/v2/test_subnet_pool.py index 46aa6f1433..6be505292c 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/functional/network/v2/test_subnet_pool.py @@ -266,7 +266,7 @@ def test_subnet_pool_set_show(self): # pool. The error appears to be in a lower layer, # once that is fixed add a test for subnet pool unset # --default-quota. - # The unset command of --pool-prefixes also doesnt work + # The unset command of --pool-prefixes also doesn't work # right now. It would be fixed in a separate patch once # the lower layer is fixed. # cmd_output = self.openstack( @@ -319,7 +319,7 @@ def _subnet_pool_create(self, cmd, name, is_type_ipv4=True): raise pass else: - # Break and no longer retry if create is sucessful + # Break and no longer retry if create is successful break return cmd_output, pool_prefix diff --git a/releasenotes/notes/bug-1633582-df2bee534c2da7fc.yaml b/releasenotes/notes/bug-1633582-df2bee534c2da7fc.yaml index 2cd3a07ced..5d195a393b 100644 --- a/releasenotes/notes/bug-1633582-df2bee534c2da7fc.yaml +++ b/releasenotes/notes/bug-1633582-df2bee534c2da7fc.yaml @@ -2,8 +2,8 @@ deprecations: - | ``volume transfer request accept`` has been changed to move the ``auth-key`` - positional argument to a requried option ``--auth-key``. This leaves - the transfer request ID as the only positional arguemnt, as per the + positional argument to a required option ``--auth-key``. This leaves + the transfer request ID as the only positional argument, as per the OpenStackClient command format. The old format is still functional, but is deprecated and will be removed in the next major release. fixes: diff --git a/releasenotes/notes/bug-27882-402ced7ffe930058.yaml b/releasenotes/notes/bug-27882-402ced7ffe930058.yaml index 6bd090aa5c..bf300de429 100644 --- a/releasenotes/notes/bug-27882-402ced7ffe930058.yaml +++ b/releasenotes/notes/bug-27882-402ced7ffe930058.yaml @@ -2,4 +2,4 @@ fixes: - | The ``--limit`` option of the ``image list`` command was previously ignored. - [Bug `https://storyboard.openstack.org/#!/story/2004314`] + [Bug `2004314 `_] diff --git a/releasenotes/source/pre_20_releases.rst b/releasenotes/source/pre_20_releases.rst index 06e7c667c2..049be75b5f 100644 --- a/releasenotes/source/pre_20_releases.rst +++ b/releasenotes/source/pre_20_releases.rst @@ -347,7 +347,7 @@ Pre-2.0 Releases ``--proto ICMP`` is selected. Bug `1443963 `_ -* Correctly pass ``--location`` arguemnt in ``image create`` command. +* Correctly pass ``--location`` argument in ``image create`` command. Bug `1445460 `_ * Correctly handle use of ``role`` commands for project admins. Using IDs will From e76e10c0bac9cf87c564a7f0201df189f7cd8b52 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Tue, 23 Oct 2018 11:34:07 -0500 Subject: [PATCH 0090/1120] Remove deprecated volume commands and args The following were deprecated over two years ago and can now be removed: * Remove ``backup`` commands in favor of ``volume backup`` * Remove ``snapshot`` commands in favor of ``volume snapshot`` * Remove ``volume create`` options ``--project``, ``--user`` and ``--multi-attach`` * Use of an auth-key positional argument in volume transfers * ``volume transfer request`` no longer accepts 'auth_key' as a positional arg, ``--auth-key`` is now required Internal (non-user-visible) * Rename backup.py to volume_backup.py for Volume v1 and v2, update tests These are backwards incompatible changes and will require a major version bump after they are merged. Change-Id: I94aa7a9824e44f9585ffb45e5e7637b9588539b4 Signed-off-by: Sean McGinnis Signed-off-by: Dean Troyer --- doc/source/cli/backwards-incompatible.rst | 26 +- doc/source/cli/command-objects/backup.rst | 137 ---- doc/source/cli/command-objects/snapshot.rst | 176 ----- .../{test_backup.py => test_volume_backup.py} | 4 +- ...st_snapshot.py => test_volume_snapshot.py} | 0 ...st_snapshot.py => test_volume_snapshot.py} | 2 +- .../tests/unit/volume/v1/test_snapshot.py | 580 -------------- .../unit/volume/v1/test_transfer_request.py | 20 - .../{test_backup.py => test_volume_backup.py} | 12 +- .../tests/unit/volume/v2/test_snapshot.py | 741 ------------------ .../unit/volume/v2/test_transfer_request.py | 30 +- .../tests/unit/volume/v2/test_volume.py | 34 - .../{test_backup.py => test_volume_backup.py} | 14 +- openstackclient/volume/v1/snapshot.py | 318 -------- .../volume/v1/{backup.py => volume_backup.py} | 85 -- .../volume/v1/volume_transfer_request.py | 22 +- openstackclient/volume/v2/snapshot.py | 351 --------- openstackclient/volume/v2/volume.py | 36 - .../volume/v2/{backup.py => volume_backup.py} | 85 -- .../volume/v2/volume_transfer_request.py | 23 +- .../notes/osc4-volume-470422e5a453310e.yaml | 14 + setup.cfg | 48 +- 22 files changed, 71 insertions(+), 2687 deletions(-) delete mode 100644 doc/source/cli/command-objects/backup.rst delete mode 100644 doc/source/cli/command-objects/snapshot.rst rename openstackclient/tests/functional/volume/v2/{test_backup.py => test_volume_backup.py} (93%) rename openstackclient/tests/functional/volume/v2/{test_snapshot.py => test_volume_snapshot.py} (100%) rename openstackclient/tests/functional/volume/v3/{test_snapshot.py => test_volume_snapshot.py} (89%) delete mode 100644 openstackclient/tests/unit/volume/v1/test_snapshot.py rename openstackclient/tests/unit/volume/v1/{test_backup.py => test_volume_backup.py} (96%) delete mode 100644 openstackclient/tests/unit/volume/v2/test_snapshot.py rename openstackclient/tests/unit/volume/v2/{test_backup.py => test_volume_backup.py} (97%) delete mode 100644 openstackclient/volume/v1/snapshot.py rename openstackclient/volume/v1/{backup.py => volume_backup.py} (73%) delete mode 100644 openstackclient/volume/v2/snapshot.py rename openstackclient/volume/v2/{backup.py => volume_backup.py} (80%) create mode 100644 releasenotes/notes/osc4-volume-470422e5a453310e.yaml diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index cbf1823c13..adb638d126 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -73,12 +73,28 @@ Release 4.0 * Removed in: 4.0 * Commit: https://review.opendev.org/613644 -.. 1. Change ``volume transfer request accept`` to use new option ``--auth-key`` -.. rather than a second positional argument. +10. Remove ``backup`` commands. + Use ``volume backup`` commands instead. -.. * As of: 4.0 -.. * Remove in: <5.0> -.. * Commit: + * Removed in: 4.0 + * Commit: https://review.opendev.org/612751 + +11. Remove ``snapshot`` commands. + Use ``volume snapshot`` commands instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612751 + +12. Remove ``volume create`` options ``--project``, ``--user``, ``--multi-attach``. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612751 + +13. Change ``volume transfer request accept`` to use new option ``--auth-key`` + rather than a second positional argument. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/612751 Release 3.12 ------------ diff --git a/doc/source/cli/command-objects/backup.rst b/doc/source/cli/command-objects/backup.rst deleted file mode 100644 index f892327695..0000000000 --- a/doc/source/cli/command-objects/backup.rst +++ /dev/null @@ -1,137 +0,0 @@ -====== -backup -====== - -Block Storage v1, v2 - -backup create -------------- - -Create new backup -(Deprecated, please use ``volume backup create`` instead) - -.. program:: backup create -.. code:: bash - - openstack backup create - [--container ] - [--name ] - [--description ] - [--snapshot ] - [--force] - [--incremental] - - -.. option:: --container - - Optional backup container name - -.. option:: --name - - Name of the backup - -.. option:: --description - - Description of the backup - -.. option:: --snapshot - - Snapshot to backup (name or ID) - - *Volume version 2 only* - -.. option:: --force - - Allow to back up an in-use volume - - *Volume version 2 only* - -.. option:: --incremental - - Perform an incremental backup - - *Volume version 2 only* - -.. _backup_create-backup: -.. describe:: - - Volume to backup (name or ID) - -backup delete -------------- - -Delete backup(s) -(Deprecated, please use ``volume backup delete`` instead) - -.. program:: backup delete -.. code:: bash - - openstack backup delete - [--force] - [ ...] - -.. option:: --force - - Allow delete in state other than error or available - - *Volume version 2 only* - -.. _backup_delete-backup: -.. describe:: - - Backup(s) to delete (name or ID) - -backup list ------------ - -List backups -(Deprecated, please use ``volume backup list`` instead) - -.. program:: backup list -.. code:: bash - - openstack backup list - -.. _backup_list-backup: -.. option:: --long - - List additional fields in output - -backup restore --------------- - -Restore backup -(Deprecated, please use ``volume backup restore`` instead) - -.. program:: backup restore -.. code:: bash - - openstack backup restore - - - -.. _backup_restore-backup: -.. describe:: - - Backup to restore (name or ID) - -.. describe:: - - Volume to restore to (name or ID) - -backup show ------------ - -Display backup details -(Deprecated, please use ``volume backup show`` instead) - -.. program:: backup show -.. code:: bash - - openstack backup show - - -.. _backup_show-backup: -.. describe:: - - Backup to display (name or ID) diff --git a/doc/source/cli/command-objects/snapshot.rst b/doc/source/cli/command-objects/snapshot.rst deleted file mode 100644 index fc516067cb..0000000000 --- a/doc/source/cli/command-objects/snapshot.rst +++ /dev/null @@ -1,176 +0,0 @@ -======== -snapshot -======== - -Block Storage v1, v2 - -snapshot create ---------------- - -Create new snapshot -(Deprecated, please use ``volume snapshot create`` instead) - -.. program:: snapshot create -.. code:: bash - - openstack snapshot create - [--name ] - [--description ] - [--force] - [--property [...] ] - - -.. option:: --name - - Name of the snapshot - -.. option:: --description - - Description of the snapshot - -.. option:: --force - - Create a snapshot attached to an instance. Default is False - -.. option:: --property - - Set a property to this snapshot (repeat option to set multiple properties) - - *Volume version 2 only* - -.. _snapshot_create-snapshot: -.. describe:: - - Volume to snapshot (name or ID) - -snapshot delete ---------------- - -Delete snapshot(s) -(Deprecated, please use ``volume snapshot delete`` instead) - -.. program:: snapshot delete -.. code:: bash - - openstack snapshot delete - [ ...] - -.. _snapshot_delete-snapshot: -.. describe:: - - Snapshot(s) to delete (name or ID) - -snapshot list -------------- - -List snapshots -(Deprecated, please use ``volume snapshot list`` instead) - -.. program:: snapshot list -.. code:: bash - - openstack snapshot list - [--all-projects] - [--long] - [--limit ] - [--marker ] - -.. option:: --all-projects - - Include all projects (admin only) - -.. option:: --long - - List additional fields in output - -.. option:: --limit - - Maximum number of snapshots to display - - *Volume version 2 only* - -.. option:: --marker - - The last snapshot ID of the previous page - - *Volume version 2 only* - -snapshot set ------------- - -Set snapshot properties -(Deprecated, please use ``volume snapshot set`` instead) - -.. program:: snapshot set -.. code:: bash - - openstack snapshot set - [--name ] - [--description ] - [--property [...] ] - [--state ] - - -.. _snapshot_restore-snapshot: -.. option:: --name - - New snapshot name - -.. option:: --description - - New snapshot description - -.. option:: --property - - Property to add or modify for this snapshot (repeat option to set multiple properties) - -.. option:: --state - - New snapshot state. - ("available", "error", "creating", "deleting", or "error_deleting") (admin only) - (This option simply changes the state of the snapshot in the database with - no regard to actual status, exercise caution when using) - - *Volume version 2 only* - -.. describe:: - - Snapshot to modify (name or ID) - -snapshot show -------------- - -Display snapshot details -(Deprecated, please use ``volume snapshot show`` instead) - -.. program:: snapshot show -.. code:: bash - - openstack snapshot show - - -.. _snapshot_show-snapshot: -.. describe:: - - Snapshot to display (name or ID) - -snapshot unset --------------- - -Unset snapshot properties -(Deprecated, please use ``volume snapshot unset`` instead) - -.. program:: snapshot unset -.. code:: bash - - openstack snapshot unset - [--property ] - - -.. option:: --property - - Property to remove from snapshot (repeat option to remove multiple properties) - -.. describe:: - - Snapshot to modify (name or ID) diff --git a/openstackclient/tests/functional/volume/v2/test_backup.py b/openstackclient/tests/functional/volume/v2/test_volume_backup.py similarity index 93% rename from openstackclient/tests/functional/volume/v2/test_backup.py rename to openstackclient/tests/functional/volume/v2/test_volume_backup.py index 8f5c032c0a..6868bd40d7 100644 --- a/openstackclient/tests/functional/volume/v2/test_backup.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_backup.py @@ -46,14 +46,14 @@ def test_volume_backup_restore(self): 'volume backup create -f json ' + vol_id )) - self.wait_for_status("backup", backup['id'], "available") + self.wait_for_status("volume backup", backup['id'], "available") # restore the backup backup_restored = json.loads(self.openstack( 'volume backup restore -f json %s %s' % (backup['id'], vol_id))) self.assertEqual(backup_restored['backup_id'], backup['id']) - self.wait_for_status("backup", backup['id'], "available") + self.wait_for_status("volume backup", backup['id'], "available") self.wait_for_status("volume", backup_restored['volume_id'], "available") self.addCleanup(self.openstack, 'volume delete %s' % vol_id) diff --git a/openstackclient/tests/functional/volume/v2/test_snapshot.py b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py similarity index 100% rename from openstackclient/tests/functional/volume/v2/test_snapshot.py rename to openstackclient/tests/functional/volume/v2/test_volume_snapshot.py diff --git a/openstackclient/tests/functional/volume/v3/test_snapshot.py b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py similarity index 89% rename from openstackclient/tests/functional/volume/v3/test_snapshot.py rename to openstackclient/tests/functional/volume/v3/test_volume_snapshot.py index 38e0563a7c..28eee6d24a 100644 --- a/openstackclient/tests/functional/volume/v3/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.volume.v2 import test_snapshot as v2 +from openstackclient.tests.functional.volume.v2 import test_volume_snapshot as v2 # noqa from openstackclient.tests.functional.volume.v3 import common diff --git a/openstackclient/tests/unit/volume/v1/test_snapshot.py b/openstackclient/tests/unit/volume/v1/test_snapshot.py deleted file mode 100644 index 70b55ce235..0000000000 --- a/openstackclient/tests/unit/volume/v1/test_snapshot.py +++ /dev/null @@ -1,580 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -import mock -from mock import call - -from osc_lib import exceptions -from osc_lib import utils - -from openstackclient.tests.unit import utils as tests_utils -from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes -from openstackclient.volume.v1 import volume_snapshot - - -class TestSnapshot(volume_fakes.TestVolumev1): - - def setUp(self): - super(TestSnapshot, self).setUp() - - self.snapshots_mock = self.app.client_manager.volume.volume_snapshots - self.snapshots_mock.reset_mock() - self.volumes_mock = self.app.client_manager.volume.volumes - self.volumes_mock.reset_mock() - - -class TestSnapshotCreate(TestSnapshot): - - columns = ( - 'created_at', - 'display_description', - 'display_name', - 'id', - 'properties', - 'size', - 'status', - 'volume_id', - ) - - def setUp(self): - super(TestSnapshotCreate, self).setUp() - - self.volume = volume_fakes.FakeVolume.create_one_volume() - self.new_snapshot = volume_fakes.FakeSnapshot.create_one_snapshot( - attrs={'volume_id': self.volume.id}) - - self.data = ( - self.new_snapshot.created_at, - self.new_snapshot.display_description, - self.new_snapshot.display_name, - self.new_snapshot.id, - utils.format_dict(self.new_snapshot.metadata), - self.new_snapshot.size, - self.new_snapshot.status, - self.new_snapshot.volume_id, - ) - - self.volumes_mock.get.return_value = self.volume - self.snapshots_mock.create.return_value = self.new_snapshot - # Get the command object to test - self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) - - def test_snapshot_create(self): - arglist = [ - "--volume", self.new_snapshot.volume_id, - "--description", self.new_snapshot.display_description, - "--force", - self.new_snapshot.display_name, - ] - verifylist = [ - ("volume", self.new_snapshot.volume_id), - ("description", self.new_snapshot.display_description), - ("force", True), - ("snapshot_name", self.new_snapshot.display_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.create.assert_called_with( - self.new_snapshot.volume_id, - True, - self.new_snapshot.display_name, - self.new_snapshot.display_description, - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - def test_snapshot_create_without_name(self): - arglist = [ - "--volume", self.new_snapshot.volume_id, - ] - verifylist = [ - ("volume", self.new_snapshot.volume_id), - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist, - ) - - def test_snapshot_create_without_volume(self): - arglist = [ - "--description", self.new_snapshot.display_description, - "--force", - self.new_snapshot.display_name - ] - verifylist = [ - ("description", self.new_snapshot.display_description), - ("force", True), - ("snapshot_name", self.new_snapshot.display_name) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.volumes_mock.get.assert_called_once_with( - self.new_snapshot.display_name) - self.snapshots_mock.create.assert_called_once_with( - self.new_snapshot.volume_id, - True, - self.new_snapshot.display_name, - self.new_snapshot.display_description, - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - -class TestSnapshotDelete(TestSnapshot): - - snapshots = volume_fakes.FakeSnapshot.create_snapshots(count=2) - - def setUp(self): - super(TestSnapshotDelete, self).setUp() - - self.snapshots_mock.get = ( - volume_fakes.FakeSnapshot.get_snapshots(self.snapshots)) - self.snapshots_mock.delete.return_value = None - - # Get the command object to mock - self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) - - def test_snapshot_delete(self): - arglist = [ - self.snapshots[0].id - ] - verifylist = [ - ("snapshots", [self.snapshots[0].id]) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id) - self.assertIsNone(result) - - def test_delete_multiple_snapshots(self): - arglist = [] - for s in self.snapshots: - arglist.append(s.id) - verifylist = [ - ('snapshots', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - calls = [] - for s in self.snapshots: - calls.append(call(s.id)) - self.snapshots_mock.delete.assert_has_calls(calls) - self.assertIsNone(result) - - def test_delete_multiple_snapshots_with_exception(self): - arglist = [ - self.snapshots[0].id, - 'unexist_snapshot', - ] - verifylist = [ - ('snapshots', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - find_mock_result = [self.snapshots[0], exceptions.CommandError] - with mock.patch.object(utils, 'find_resource', - side_effect=find_mock_result) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 snapshots failed to delete.', - str(e)) - - find_mock.assert_any_call( - self.snapshots_mock, self.snapshots[0].id) - find_mock.assert_any_call(self.snapshots_mock, 'unexist_snapshot') - - self.assertEqual(2, find_mock.call_count) - self.snapshots_mock.delete.assert_called_once_with( - self.snapshots[0].id - ) - - -class TestSnapshotList(TestSnapshot): - - volume = volume_fakes.FakeVolume.create_one_volume() - snapshots = volume_fakes.FakeSnapshot.create_snapshots( - attrs={'volume_id': volume.display_name}, count=3) - - columns = [ - "ID", - "Name", - "Description", - "Status", - "Size" - ] - columns_long = columns + [ - "Created At", - "Volume", - "Properties" - ] - - data = [] - for s in snapshots: - data.append(( - s.id, - s.display_name, - s.display_description, - s.status, - s.size, - )) - data_long = [] - for s in snapshots: - data_long.append(( - s.id, - s.display_name, - s.display_description, - s.status, - s.size, - s.created_at, - s.volume_id, - utils.format_dict(s.metadata), - )) - - def setUp(self): - super(TestSnapshotList, self).setUp() - - self.volumes_mock.list.return_value = [self.volume] - self.volumes_mock.get.return_value = self.volume - self.snapshots_mock.list.return_value = self.snapshots - # Get the command to test - self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) - - def test_snapshot_list_without_options(self): - arglist = [] - verifylist = [ - ('all_projects', False), - ("long", False) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': False, - 'display_name': None, - 'status': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_with_long(self): - arglist = [ - "--long", - ] - verifylist = [ - ("long", True), - ('all_projects', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': False, - 'display_name': None, - 'status': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) - - def test_snapshot_list_name_option(self): - arglist = [ - '--name', self.snapshots[0].display_name, - ] - verifylist = [ - ('all_projects', False), - ('long', False), - ('name', self.snapshots[0].display_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': False, - 'display_name': self.snapshots[0].display_name, - 'status': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_status_option(self): - arglist = [ - '--status', self.snapshots[0].status, - ] - verifylist = [ - ('all_projects', False), - ('long', False), - ('status', self.snapshots[0].status), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': False, - 'display_name': None, - 'status': self.snapshots[0].status, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_volumeid_option(self): - arglist = [ - '--volume', self.volume.id, - ] - verifylist = [ - ('all_projects', False), - ('long', False), - ('volume', self.volume.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': False, - 'display_name': None, - 'status': None, - 'volume_id': self.volume.id - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_all_projects(self): - arglist = [ - '--all-projects', - ] - verifylist = [ - ('long', False), - ('all_projects', True) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': True, - 'display_name': None, - 'status': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - -class TestSnapshotSet(TestSnapshot): - - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - - def setUp(self): - super(TestSnapshotSet, self).setUp() - - self.snapshots_mock.get.return_value = self.snapshot - self.snapshots_mock.set_metadata.return_value = None - # Get the command object to mock - self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) - - def test_snapshot_set_all(self): - arglist = [ - "--name", "new_snapshot", - "--description", "new_description", - "--property", "foo_1=foo_1", - "--property", "foo_2=foo_2", - "--no-property", - self.snapshot.id, - ] - new_property = {"foo_1": "foo_1", "foo_2": "foo_2"} - verifylist = [ - ("name", "new_snapshot"), - ("description", "new_description"), - ("property", new_property), - ("no_property", True), - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - kwargs = { - "display_name": "new_snapshot", - "display_description": "new_description", - } - self.snapshot.update.assert_called_with(**kwargs) - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) - self.snapshots_mock.set_metadata.assert_called_with( - self.snapshot.id, {"foo_2": "foo_2", "foo_1": "foo_1"} - ) - self.assertIsNone(result) - - def test_snapshot_set_nothing(self): - arglist = [ - self.snapshot.id, - ] - verifylist = [ - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - self.assertIsNone(result) - - def test_snapshot_set_fail(self): - self.snapshots_mock.set_metadata.side_effect = ( - exceptions.CommandError()) - arglist = [ - "--name", "new_snapshot", - "--description", "new_description", - "--property", "x=y", - "--property", "foo=foo", - self.snapshot.id, - ] - new_property = {"x": "y", "foo": "foo"} - verifylist = [ - ("name", "new_snapshot"), - ("description", "new_description"), - ("property", new_property), - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) - - -class TestSnapshotShow(TestSnapshot): - - columns = ( - 'created_at', - 'display_description', - 'display_name', - 'id', - 'properties', - 'size', - 'status', - 'volume_id', - ) - - def setUp(self): - super(TestSnapshotShow, self).setUp() - - self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - - self.data = ( - self.snapshot.created_at, - self.snapshot.display_description, - self.snapshot.display_name, - self.snapshot.id, - utils.format_dict(self.snapshot.metadata), - self.snapshot.size, - self.snapshot.status, - self.snapshot.volume_id, - ) - - self.snapshots_mock.get.return_value = self.snapshot - # Get the command object to test - self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) - - def test_snapshot_show(self): - arglist = [ - self.snapshot.id - ] - verifylist = [ - ("snapshot", self.snapshot.id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_with(self.snapshot.id) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - -class TestSnapshotUnset(TestSnapshot): - - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - - def setUp(self): - super(TestSnapshotUnset, self).setUp() - - self.snapshots_mock.get.return_value = self.snapshot - self.snapshots_mock.delete_metadata.return_value = None - # Get the command object to mock - self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) - - def test_snapshot_unset(self): - arglist = [ - "--property", "foo", - self.snapshot.id, - ] - verifylist = [ - ("property", ["foo"]), - ("snapshot", self.snapshot.id), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) - self.assertIsNone(result) - - def test_snapshot_unset_nothing(self): - arglist = [ - self.snapshot.id, - ] - verifylist = [ - ("snapshot", self.snapshot.id), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v1/test_transfer_request.py b/openstackclient/tests/unit/volume/v1/test_transfer_request.py index 4c013dc007..680561d522 100644 --- a/openstackclient/tests/unit/volume/v1/test_transfer_request.py +++ b/openstackclient/tests/unit/volume/v1/test_transfer_request.py @@ -85,26 +85,6 @@ def test_transfer_accept(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_transfer_accept_deprecated(self): - arglist = [ - self.volume_transfer.id, - 'key_value', - ] - verifylist = [ - ('transfer_request', self.volume_transfer.id), - ('old_auth_key', 'key_value'), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.transfer_mock.accept.assert_called_once_with( - self.volume_transfer.id, - 'key_value', - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - def test_transfer_accept_no_option(self): arglist = [ self.volume_transfer.id, diff --git a/openstackclient/tests/unit/volume/v1/test_backup.py b/openstackclient/tests/unit/volume/v1/test_volume_backup.py similarity index 96% rename from openstackclient/tests/unit/volume/v1/test_backup.py rename to openstackclient/tests/unit/volume/v1/test_volume_backup.py index 1097d3f121..22c1f8cf58 100644 --- a/openstackclient/tests/unit/volume/v1/test_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_volume_backup.py @@ -19,7 +19,7 @@ from osc_lib import utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes -from openstackclient.volume.v1 import backup +from openstackclient.volume.v1 import volume_backup class TestBackup(volume_fakes.TestVolumev1): @@ -74,7 +74,7 @@ def setUp(self): self.backups_mock.create.return_value = self.new_backup # Get the command object to test - self.cmd = backup.CreateVolumeBackup(self.app, None) + self.cmd = volume_backup.CreateVolumeBackup(self.app, None) def test_backup_create(self): arglist = [ @@ -139,7 +139,7 @@ def setUp(self): self.backups_mock.delete.return_value = None # Get the command object to mock - self.cmd = backup.DeleteVolumeBackup(self.app, None) + self.cmd = volume_backup.DeleteVolumeBackup(self.app, None) def test_backup_delete(self): arglist = [ @@ -251,7 +251,7 @@ def setUp(self): self.backups_mock.list.return_value = self.backups self.volumes_mock.get.return_value = self.volume # Get the command to test - self.cmd = backup.ListVolumeBackup(self.app, None) + self.cmd = volume_backup.ListVolumeBackup(self.app, None) def test_backup_list_without_options(self): arglist = [] @@ -325,7 +325,7 @@ def setUp(self): self.volumes_mock.get.return_value = self.volume self.restores_mock.restore.return_value = None # Get the command object to mock - self.cmd = backup.RestoreVolumeBackup(self.app, None) + self.cmd = volume_backup.RestoreVolumeBackup(self.app, None) def test_backup_restore(self): arglist = [ @@ -376,7 +376,7 @@ def setUp(self): ) self.backups_mock.get.return_value = self.backup # Get the command object to test - self.cmd = backup.ShowVolumeBackup(self.app, None) + self.cmd = volume_backup.ShowVolumeBackup(self.app, None) def test_backup_show(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v2/test_snapshot.py b/openstackclient/tests/unit/volume/v2/test_snapshot.py deleted file mode 100644 index e8f4ae5a4a..0000000000 --- a/openstackclient/tests/unit/volume/v2/test_snapshot.py +++ /dev/null @@ -1,741 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -import argparse - -import mock -from mock import call -from osc_lib import exceptions -from osc_lib import utils - -from openstackclient.tests.unit.identity.v3 import fakes as project_fakes -from openstackclient.tests.unit import utils as tests_utils -from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes -from openstackclient.volume.v2 import volume_snapshot - - -class TestSnapshot(volume_fakes.TestVolume): - - def setUp(self): - super(TestSnapshot, self).setUp() - - self.snapshots_mock = self.app.client_manager.volume.volume_snapshots - self.snapshots_mock.reset_mock() - self.volumes_mock = self.app.client_manager.volume.volumes - self.volumes_mock.reset_mock() - self.project_mock = self.app.client_manager.identity.projects - self.project_mock.reset_mock() - - -class TestSnapshotCreate(TestSnapshot): - - columns = ( - 'created_at', - 'description', - 'id', - 'name', - 'properties', - 'size', - 'status', - 'volume_id', - ) - - def setUp(self): - super(TestSnapshotCreate, self).setUp() - - self.volume = volume_fakes.FakeVolume.create_one_volume() - self.new_snapshot = volume_fakes.FakeSnapshot.create_one_snapshot( - attrs={'volume_id': self.volume.id}) - - self.data = ( - self.new_snapshot.created_at, - self.new_snapshot.description, - self.new_snapshot.id, - self.new_snapshot.name, - utils.format_dict(self.new_snapshot.metadata), - self.new_snapshot.size, - self.new_snapshot.status, - self.new_snapshot.volume_id, - ) - - self.volumes_mock.get.return_value = self.volume - self.snapshots_mock.create.return_value = self.new_snapshot - self.snapshots_mock.manage.return_value = self.new_snapshot - # Get the command object to test - self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) - - def test_snapshot_create(self): - arglist = [ - "--volume", self.new_snapshot.volume_id, - "--description", self.new_snapshot.description, - "--force", - '--property', 'Alpha=a', - '--property', 'Beta=b', - self.new_snapshot.name, - ] - verifylist = [ - ("volume", self.new_snapshot.volume_id), - ("description", self.new_snapshot.description), - ("force", True), - ('property', {'Alpha': 'a', 'Beta': 'b'}), - ("snapshot_name", self.new_snapshot.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.create.assert_called_with( - self.new_snapshot.volume_id, - force=True, - name=self.new_snapshot.name, - description=self.new_snapshot.description, - metadata={'Alpha': 'a', 'Beta': 'b'}, - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - def test_snapshot_create_without_name(self): - arglist = [ - "--volume", self.new_snapshot.volume_id, - ] - verifylist = [ - ("volume", self.new_snapshot.volume_id), - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist, - ) - - def test_snapshot_create_without_volume(self): - arglist = [ - "--description", self.new_snapshot.description, - "--force", - self.new_snapshot.name - ] - verifylist = [ - ("description", self.new_snapshot.description), - ("force", True), - ("snapshot_name", self.new_snapshot.name) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.volumes_mock.get.assert_called_once_with( - self.new_snapshot.name) - self.snapshots_mock.create.assert_called_once_with( - self.new_snapshot.volume_id, - force=True, - name=self.new_snapshot.name, - description=self.new_snapshot.description, - metadata=None, - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - def test_snapshot_create_with_remote_source(self): - arglist = [ - '--remote-source', 'source-name=test_source_name', - '--remote-source', 'source-id=test_source_id', - '--volume', self.new_snapshot.volume_id, - self.new_snapshot.name, - ] - ref_dict = {'source-name': 'test_source_name', - 'source-id': 'test_source_id'} - verifylist = [ - ('remote_source', ref_dict), - ('volume', self.new_snapshot.volume_id), - ("snapshot_name", self.new_snapshot.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.manage.assert_called_with( - volume_id=self.new_snapshot.volume_id, - ref=ref_dict, - name=self.new_snapshot.name, - description=None, - metadata=None, - ) - self.snapshots_mock.create.assert_not_called() - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - -class TestSnapshotDelete(TestSnapshot): - - snapshots = volume_fakes.FakeSnapshot.create_snapshots(count=2) - - def setUp(self): - super(TestSnapshotDelete, self).setUp() - - self.snapshots_mock.get = ( - volume_fakes.FakeSnapshot.get_snapshots(self.snapshots)) - self.snapshots_mock.delete.return_value = None - - # Get the command object to mock - self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) - - def test_snapshot_delete(self): - arglist = [ - self.snapshots[0].id - ] - verifylist = [ - ("snapshots", [self.snapshots[0].id]) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id, False) - self.assertIsNone(result) - - def test_snapshot_delete_with_force(self): - arglist = [ - '--force', - self.snapshots[0].id - ] - verifylist = [ - ('force', True), - ("snapshots", [self.snapshots[0].id]) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id, True) - self.assertIsNone(result) - - def test_delete_multiple_snapshots(self): - arglist = [] - for s in self.snapshots: - arglist.append(s.id) - verifylist = [ - ('snapshots', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - calls = [] - for s in self.snapshots: - calls.append(call(s.id, False)) - self.snapshots_mock.delete.assert_has_calls(calls) - self.assertIsNone(result) - - def test_delete_multiple_snapshots_with_exception(self): - arglist = [ - self.snapshots[0].id, - 'unexist_snapshot', - ] - verifylist = [ - ('snapshots', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - find_mock_result = [self.snapshots[0], exceptions.CommandError] - with mock.patch.object(utils, 'find_resource', - side_effect=find_mock_result) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 snapshots failed to delete.', - str(e)) - - find_mock.assert_any_call( - self.snapshots_mock, self.snapshots[0].id) - find_mock.assert_any_call(self.snapshots_mock, 'unexist_snapshot') - - self.assertEqual(2, find_mock.call_count) - self.snapshots_mock.delete.assert_called_once_with( - self.snapshots[0].id, False - ) - - -class TestSnapshotList(TestSnapshot): - - volume = volume_fakes.FakeVolume.create_one_volume() - project = project_fakes.FakeProject.create_one_project() - snapshots = volume_fakes.FakeSnapshot.create_snapshots( - attrs={'volume_id': volume.name}, count=3) - - columns = [ - "ID", - "Name", - "Description", - "Status", - "Size" - ] - columns_long = columns + [ - "Created At", - "Volume", - "Properties" - ] - - data = [] - for s in snapshots: - data.append(( - s.id, - s.name, - s.description, - s.status, - s.size, - )) - data_long = [] - for s in snapshots: - data_long.append(( - s.id, - s.name, - s.description, - s.status, - s.size, - s.created_at, - s.volume_id, - utils.format_dict(s.metadata), - )) - - def setUp(self): - super(TestSnapshotList, self).setUp() - - self.volumes_mock.list.return_value = [self.volume] - self.volumes_mock.get.return_value = self.volume - self.project_mock.get.return_value = self.project - self.snapshots_mock.list.return_value = self.snapshots - # Get the command to test - self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) - - def test_snapshot_list_without_options(self): - arglist = [] - verifylist = [ - ('all_projects', False), - ('long', False) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': None, - 'status': None, - 'project_id': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_with_options(self): - arglist = [ - "--long", - "--limit", "2", - "--project", self.project.id, - "--marker", self.snapshots[0].id, - ] - verifylist = [ - ("long", True), - ("limit", 2), - ("project", self.project.id), - ("marker", self.snapshots[0].id), - ('all_projects', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - limit=2, - marker=self.snapshots[0].id, - search_opts={ - 'all_tenants': True, - 'project_id': self.project.id, - 'name': None, - 'status': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) - - def test_snapshot_list_all_projects(self): - arglist = [ - '--all-projects', - ] - verifylist = [ - ('long', False), - ('all_projects', True) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, - search_opts={ - 'all_tenants': True, - 'name': None, - 'status': None, - 'project_id': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_name_option(self): - arglist = [ - '--name', self.snapshots[0].name, - ] - verifylist = [ - ('all_projects', False), - ('long', False), - ('name', self.snapshots[0].name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': self.snapshots[0].name, - 'status': None, - 'project_id': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_status_option(self): - arglist = [ - '--status', self.snapshots[0].status, - ] - verifylist = [ - ('all_projects', False), - ('long', False), - ('status', self.snapshots[0].status), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': None, - 'status': self.snapshots[0].status, - 'project_id': None, - 'volume_id': None - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_volumeid_option(self): - arglist = [ - '--volume', self.volume.id, - ] - verifylist = [ - ('all_projects', False), - ('long', False), - ('volume', self.volume.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': None, - 'status': None, - 'project_id': None, - 'volume_id': self.volume.id - } - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - def test_snapshot_list_negative_limit(self): - arglist = [ - "--limit", "-2", - ] - verifylist = [ - ("limit", -2), - ] - self.assertRaises(argparse.ArgumentTypeError, self.check_parser, - self.cmd, arglist, verifylist) - - -class TestSnapshotSet(TestSnapshot): - - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - - def setUp(self): - super(TestSnapshotSet, self).setUp() - - self.snapshots_mock.get.return_value = self.snapshot - self.snapshots_mock.set_metadata.return_value = None - self.snapshots_mock.update.return_value = None - # Get the command object to mock - self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) - - def test_snapshot_set_no_option(self): - arglist = [ - self.snapshot.id, - ] - verifylist = [ - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) - self.assertNotCalled(self.snapshots_mock.reset_state) - self.assertNotCalled(self.snapshots_mock.update) - self.assertNotCalled(self.snapshots_mock.set_metadata) - self.assertIsNone(result) - - def test_snapshot_set_name_and_property(self): - arglist = [ - "--name", "new_snapshot", - "--property", "x=y", - "--property", "foo=foo", - self.snapshot.id, - ] - new_property = {"x": "y", "foo": "foo"} - verifylist = [ - ("name", "new_snapshot"), - ("property", new_property), - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - kwargs = { - "name": "new_snapshot", - } - self.snapshots_mock.update.assert_called_with( - self.snapshot.id, **kwargs) - self.snapshots_mock.set_metadata.assert_called_with( - self.snapshot.id, new_property - ) - self.assertIsNone(result) - - def test_snapshot_set_with_no_property(self): - arglist = [ - "--no-property", - self.snapshot.id, - ] - verifylist = [ - ("no_property", True), - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) - self.assertNotCalled(self.snapshots_mock.reset_state) - self.assertNotCalled(self.snapshots_mock.update) - self.assertNotCalled(self.snapshots_mock.set_metadata) - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) - self.assertIsNone(result) - - def test_snapshot_set_with_no_property_and_property(self): - arglist = [ - "--no-property", - "--property", "foo_1=bar_1", - self.snapshot.id, - ] - verifylist = [ - ("no_property", True), - ("property", {"foo_1": "bar_1"}), - ("snapshot", self.snapshot.id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) - self.assertNotCalled(self.snapshots_mock.reset_state) - self.assertNotCalled(self.snapshots_mock.update) - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) - self.snapshots_mock.set_metadata.assert_called_once_with( - self.snapshot.id, {"foo_1": "bar_1"}) - self.assertIsNone(result) - - def test_snapshot_set_state_to_error(self): - arglist = [ - "--state", "error", - self.snapshot.id - ] - verifylist = [ - ("state", "error"), - ("snapshot", self.snapshot.id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - self.snapshots_mock.reset_state.assert_called_with( - self.snapshot.id, "error") - self.assertIsNone(result) - - def test_volume_set_state_failed(self): - self.snapshots_mock.reset_state.side_effect = exceptions.CommandError() - arglist = [ - '--state', 'error', - self.snapshot.id - ] - verifylist = [ - ('state', 'error'), - ('snapshot', self.snapshot.id) - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('One or more of the set operations failed', - str(e)) - self.snapshots_mock.reset_state.assert_called_once_with( - self.snapshot.id, 'error') - - def test_volume_set_name_and_state_failed(self): - self.snapshots_mock.reset_state.side_effect = exceptions.CommandError() - arglist = [ - '--state', 'error', - "--name", "new_snapshot", - self.snapshot.id - ] - verifylist = [ - ('state', 'error'), - ("name", "new_snapshot"), - ('snapshot', self.snapshot.id) - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('One or more of the set operations failed', - str(e)) - kwargs = { - "name": "new_snapshot", - } - self.snapshots_mock.update.assert_called_once_with( - self.snapshot.id, **kwargs) - self.snapshots_mock.reset_state.assert_called_once_with( - self.snapshot.id, 'error') - - -class TestSnapshotShow(TestSnapshot): - - columns = ( - 'created_at', - 'description', - 'id', - 'name', - 'properties', - 'size', - 'status', - 'volume_id', - ) - - def setUp(self): - super(TestSnapshotShow, self).setUp() - - self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - - self.data = ( - self.snapshot.created_at, - self.snapshot.description, - self.snapshot.id, - self.snapshot.name, - utils.format_dict(self.snapshot.metadata), - self.snapshot.size, - self.snapshot.status, - self.snapshot.volume_id, - ) - - self.snapshots_mock.get.return_value = self.snapshot - # Get the command object to test - self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) - - def test_snapshot_show(self): - arglist = [ - self.snapshot.id - ] - verifylist = [ - ("snapshot", self.snapshot.id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_with(self.snapshot.id) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - -class TestSnapshotUnset(TestSnapshot): - - snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() - - def setUp(self): - super(TestSnapshotUnset, self).setUp() - - self.snapshots_mock.get.return_value = self.snapshot - self.snapshots_mock.delete_metadata.return_value = None - # Get the command object to mock - self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) - - def test_snapshot_unset(self): - arglist = [ - "--property", "foo", - self.snapshot.id, - ] - verifylist = [ - ("property", ["foo"]), - ("snapshot", self.snapshot.id), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) - self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v2/test_transfer_request.py b/openstackclient/tests/unit/volume/v2/test_transfer_request.py index 37eed11e50..1ea6648f6a 100644 --- a/openstackclient/tests/unit/volume/v2/test_transfer_request.py +++ b/openstackclient/tests/unit/volume/v2/test_transfer_request.py @@ -18,6 +18,7 @@ from osc_lib import exceptions 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.volume.v2 import volume_transfer_request @@ -85,26 +86,6 @@ def test_transfer_accept(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_transfer_accept_deprecated(self): - arglist = [ - self.volume_transfer.id, - 'key_value', - ] - verifylist = [ - ('transfer_request', self.volume_transfer.id), - ('old_auth_key', 'key_value'), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - self.transfer_mock.accept.assert_called_once_with( - self.volume_transfer.id, - 'key_value', - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - def test_transfer_accept_no_option(self): arglist = [ self.volume_transfer.id, @@ -112,12 +93,13 @@ def test_transfer_accept_no_option(self): verifylist = [ ('transfer_request', self.volume_transfer.id), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises( - exceptions.CommandError, - self.cmd.take_action, - parsed_args, + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, ) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index dbe69ea0ff..97da160148 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -183,40 +183,6 @@ def test_volume_create_options(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) - def test_volume_create_user(self): - arglist = [ - '--size', str(self.new_volume.size), - '--user', self.user.id, - self.new_volume.name, - ] - verifylist = [ - ('size', self.new_volume.size), - ('user', self.user.id), - ('name', self.new_volume.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - self.volumes_mock.create.assert_not_called() - - def test_volume_create_project(self): - arglist = [ - '--size', str(self.new_volume.size), - '--project', self.project.id, - self.new_volume.name, - ] - verifylist = [ - ('size', self.new_volume.size), - ('project', self.project.id), - ('name', self.new_volume.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - self.volumes_mock.create.assert_not_called() - def test_volume_create_properties(self): arglist = [ '--property', 'Alpha=a', diff --git a/openstackclient/tests/unit/volume/v2/test_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py similarity index 97% rename from openstackclient/tests/unit/volume/v2/test_backup.py rename to openstackclient/tests/unit/volume/v2/test_volume_backup.py index 9a2ce71809..d0d1da3b29 100644 --- a/openstackclient/tests/unit/volume/v2/test_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -19,7 +19,7 @@ from osc_lib import utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes -from openstackclient.volume.v2 import backup +from openstackclient.volume.v2 import volume_backup class TestBackup(volume_fakes.TestVolume): @@ -77,7 +77,7 @@ def setUp(self): self.backups_mock.create.return_value = self.new_backup # Get the command object to test - self.cmd = backup.CreateVolumeBackup(self.app, None) + self.cmd = volume_backup.CreateVolumeBackup(self.app, None) def test_backup_create(self): arglist = [ @@ -154,7 +154,7 @@ def setUp(self): self.backups_mock.delete.return_value = None # Get the command object to mock - self.cmd = backup.DeleteVolumeBackup(self.app, None) + self.cmd = volume_backup.DeleteVolumeBackup(self.app, None) def test_backup_delete(self): arglist = [ @@ -283,7 +283,7 @@ def setUp(self): self.volumes_mock.get.return_value = self.volume self.backups_mock.get.return_value = self.backups[0] # Get the command to test - self.cmd = backup.ListVolumeBackup(self.app, None) + self.cmd = volume_backup.ListVolumeBackup(self.app, None) def test_backup_list_without_options(self): arglist = [] @@ -371,7 +371,7 @@ def setUp(self): volume_fakes.FakeVolume.create_one_volume( {'id': self.volume['id']})) # Get the command object to mock - self.cmd = backup.RestoreVolumeBackup(self.app, None) + self.cmd = volume_backup.RestoreVolumeBackup(self.app, None) def test_backup_restore(self): arglist = [ @@ -400,7 +400,7 @@ def setUp(self): self.backups_mock.get.return_value = self.backup # Get the command object to test - self.cmd = backup.SetVolumeBackup(self.app, None) + self.cmd = volume_backup.SetVolumeBackup(self.app, None) def test_backup_set_name(self): arglist = [ @@ -517,7 +517,7 @@ def setUp(self): self.backups_mock.get.return_value = self.backup # Get the command object to test - self.cmd = backup.ShowVolumeBackup(self.app, None) + self.cmd = volume_backup.ShowVolumeBackup(self.app, None) def test_backup_show(self): arglist = [ diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py deleted file mode 100644 index e9e3894bab..0000000000 --- a/openstackclient/volume/v1/snapshot.py +++ /dev/null @@ -1,318 +0,0 @@ -# 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. -# - -# TODO(Huanxuan Ao): Remove this file and "snapshot create", "snapshot delete", -# "snapshot set", "snapshot show" and "snapshot unset" -# commands two cycles after Ocata. - -"""Volume v1 Snapshot action implementations""" - -import copy -import logging - -from osc_lib.cli import parseractions -from osc_lib.command import command -from osc_lib import exceptions -from osc_lib import utils -import six - -from openstackclient.i18n import _ - - -deprecated = True -LOG_DEP = logging.getLogger('deprecated') -LOG = logging.getLogger(__name__) - - -class CreateSnapshot(command.ShowOne): - _description = _("Create new snapshot") - - def get_parser(self, prog_name): - parser = super(CreateSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'volume', - metavar='', - help=_('Volume to snapshot (name or ID)'), - ) - parser.add_argument( - '--name', - metavar='', - help=_('Name of the snapshot'), - ) - parser.add_argument( - '--description', - metavar='', - help=_('Description of the snapshot'), - ) - parser.add_argument( - '--force', - dest='force', - action='store_true', - default=False, - help=_('Create a snapshot attached to an instance. ' - 'Default is False'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot create" instead.')) - volume_client = self.app.client_manager.volume - volume_id = utils.find_resource(volume_client.volumes, - parsed_args.volume).id - snapshot = volume_client.volume_snapshots.create( - volume_id, - parsed_args.force, - parsed_args.name, - parsed_args.description - ) - - snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} - ) - - return zip(*sorted(six.iteritems(snapshot._info))) - - -class DeleteSnapshot(command.Command): - _description = _("Delete snapshot(s)") - - def get_parser(self, prog_name): - parser = super(DeleteSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'snapshots', - metavar='', - nargs="+", - help=_('Snapshot(s) to delete (name or ID)'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot delete" instead.')) - volume_client = self.app.client_manager.volume - result = 0 - - for i in parsed_args.snapshots: - try: - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, i).id - volume_client.volume_snapshots.delete(snapshot_id) - except Exception as e: - result += 1 - LOG.error(_("Failed to delete snapshot with " - "name or ID '%(snapshot)s': %(e)s"), - {'snapshot': i, 'e': e}) - - if result > 0: - total = len(parsed_args.snapshots) - msg = (_("%(result)s of %(total)s snapshots failed " - "to delete.") % {'result': result, 'total': total}) - raise exceptions.CommandError(msg) - - -class ListSnapshot(command.Lister): - _description = _("List snapshots") - - def get_parser(self, prog_name): - parser = super(ListSnapshot, self).get_parser(prog_name) - parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=_('Include all projects (admin only)'), - ) - parser.add_argument( - '--long', - action='store_true', - default=False, - help=_('List additional fields in output'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot list" instead.')) - - def _format_volume_id(volume_id): - """Return a volume name if available - - :param volume_id: a volume ID - :rtype: either the volume ID or name - """ - - volume = volume_id - if volume_id in volume_cache.keys(): - volume = volume_cache[volume_id].display_name - return volume - - if parsed_args.long: - columns = ['ID', 'Display Name', 'Display Description', 'Status', - 'Size', 'Created At', 'Volume ID', 'Metadata'] - column_headers = copy.deepcopy(columns) - column_headers[6] = 'Volume' - column_headers[7] = 'Properties' - else: - columns = ['ID', 'Display Name', 'Display Description', 'Status', - 'Size'] - column_headers = copy.deepcopy(columns) - - # Always update Name and Description - column_headers[1] = 'Name' - column_headers[2] = 'Description' - - # Cache the volume list - volume_cache = {} - try: - for s in self.app.client_manager.volume.volumes.list(): - volume_cache[s.id] = s - except Exception: - # Just forget it if there's any trouble - pass - - search_opts = { - 'all_tenants': parsed_args.all_projects, - } - - data = self.app.client_manager.volume.volume_snapshots.list( - search_opts=search_opts) - return (column_headers, - (utils.get_item_properties( - s, columns, - formatters={'Metadata': utils.format_dict, - 'Volume ID': _format_volume_id}, - ) for s in data)) - - -class SetSnapshot(command.Command): - _description = _("Set snapshot properties") - - def get_parser(self, prog_name): - parser = super(SetSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'snapshot', - metavar='', - help=_('Snapshot to modify (name or ID)') - ) - parser.add_argument( - '--name', - metavar='', - help=_('New snapshot name') - ) - parser.add_argument( - '--description', - metavar='', - help=_('New snapshot description') - ) - parser.add_argument( - '--property', - metavar='', - action=parseractions.KeyValueAction, - help=_('Property to add/change for this snapshot ' - '(repeat option to set multiple properties)'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot set" instead.')) - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource(volume_client.volume_snapshots, - parsed_args.snapshot) - - result = 0 - if parsed_args.property: - try: - volume_client.volume_snapshots.set_metadata( - snapshot.id, parsed_args.property) - except Exception as e: - LOG.error(_("Failed to set snapshot property: %s"), e) - result += 1 - - kwargs = {} - if parsed_args.name: - kwargs['display_name'] = parsed_args.name - if parsed_args.description: - kwargs['display_description'] = parsed_args.description - if kwargs: - try: - snapshot.update(**kwargs) - except Exception as e: - LOG.error(_("Failed to update snapshot display name " - "or display description: %s"), e) - result += 1 - - if result > 0: - raise exceptions.CommandError(_("One or more of the " - "set operations failed")) - - -class ShowSnapshot(command.ShowOne): - _description = _("Display snapshot details") - - def get_parser(self, prog_name): - parser = super(ShowSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'snapshot', - metavar='', - help=_('Snapshot to display (name or ID)') - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot show" instead.')) - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource(volume_client.volume_snapshots, - parsed_args.snapshot) - - snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} - ) - - return zip(*sorted(six.iteritems(snapshot._info))) - - -class UnsetSnapshot(command.Command): - _description = _("Unset snapshot properties") - - def get_parser(self, prog_name): - parser = super(UnsetSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'snapshot', - metavar='', - help=_('Snapshot to modify (name or ID)'), - ) - parser.add_argument( - '--property', - metavar='', - action='append', - help=_('Property to remove from snapshot ' - '(repeat option to remove multiple properties)'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot unset" instead.')) - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot) - - if parsed_args.property: - volume_client.volume_snapshots.delete_metadata( - snapshot.id, - parsed_args.property, - ) diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/volume_backup.py similarity index 73% rename from openstackclient/volume/v1/backup.py rename to openstackclient/volume/v1/volume_backup.py index 9ac1302a55..9af327055b 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/volume_backup.py @@ -72,23 +72,6 @@ def take_action(self, parsed_args): return zip(*sorted(six.iteritems(backup._info))) -class CreateBackup(CreateVolumeBackup): - _description = _("Create new backup") - - # TODO(Huanxuan Ao): Remove this class and ``backup create`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup create" instead.')) - return super(CreateBackup, self).take_action(parsed_args) - - class DeleteVolumeBackup(command.Command): _description = _("Delete volume backup(s)") @@ -124,23 +107,6 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -class DeleteBackup(DeleteVolumeBackup): - _description = _("Delete backup(s)") - - # TODO(Huanxuan Ao): Remove this class and ``backup delete`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup delete" instead.')) - return super(DeleteBackup, self).take_action(parsed_args) - - class ListVolumeBackup(command.Lister): _description = _("List volume backups") @@ -234,23 +200,6 @@ def _format_volume_id(volume_id): ) for s in data)) -class ListBackup(ListVolumeBackup): - _description = _("List backups") - - # TODO(Huanxuan Ao): Remove this class and ``backup list`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup list" instead.')) - return super(ListBackup, self).take_action(parsed_args) - - class RestoreVolumeBackup(command.Command): _description = _("Restore volume backup") @@ -278,23 +227,6 @@ def take_action(self, parsed_args): destination_volume.id) -class RestoreBackup(RestoreVolumeBackup): - _description = _("Restore backup") - - # TODO(Huanxuan Ao): Remove this class and ``backup restore`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup restore" instead.')) - return super(RestoreBackup, self).take_action(parsed_args) - - class ShowVolumeBackup(command.ShowOne): _description = _("Display volume backup details") @@ -313,20 +245,3 @@ def take_action(self, parsed_args): parsed_args.backup) backup._info.pop('links') return zip(*sorted(six.iteritems(backup._info))) - - -class ShowBackup(ShowVolumeBackup): - _description = _("Display backup details") - - # TODO(Huanxuan Ao): Remove this class and ``backup show`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup show" instead.')) - return super(ShowBackup, self).take_action(parsed_args) diff --git a/openstackclient/volume/v1/volume_transfer_request.py b/openstackclient/volume/v1/volume_transfer_request.py index f5b567b94a..6f79658e5a 100644 --- a/openstackclient/volume/v1/volume_transfer_request.py +++ b/openstackclient/volume/v1/volume_transfer_request.py @@ -14,7 +14,6 @@ """Volume v1 transfer action implementations""" -import argparse import logging from osc_lib.command import command @@ -38,12 +37,6 @@ def get_parser(self, prog_name): metavar="", help=_('Volume transfer request to accept (ID only)'), ) - parser.add_argument( - 'old_auth_key', - metavar="", - nargs="?", - help=argparse.SUPPRESS, - ) parser.add_argument( '--auth-key', metavar="", @@ -64,20 +57,9 @@ def take_action(self, parsed_args): # move on and attempt with the user-supplied information transfer_request_id = parsed_args.transfer_request - # Remain backward-compatible for the previous command layout - # TODO(dtroyer): Remove this back-compat in 4.0 or Oct 2017 if not parsed_args.auth_key: - if parsed_args.old_auth_key: - # Move the old one into the correct place - parsed_args.auth_key = parsed_args.old_auth_key - self.log.warning(_( - 'Specifying the auth-key as a positional argument ' - 'has been deprecated. Please use the --auth-key ' - 'option in the future.' - )) - else: - msg = _("argument --auth-key is required") - raise exceptions.CommandError(msg) + msg = _("argument --auth-key is required") + raise exceptions.CommandError(msg) transfer_accept = volume_client.transfers.accept( transfer_request_id, diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py deleted file mode 100644 index 82b310338a..0000000000 --- a/openstackclient/volume/v2/snapshot.py +++ /dev/null @@ -1,351 +0,0 @@ -# -# 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. -# - -# TODO(Huanxuan Ao): Remove this file and "snapshot create", "snapshot delete", -# "snapshot set", "snapshot show" and "snapshot unset" -# commands two cycles after Ocata. - -"""Volume v2 snapshot action implementations""" - -import copy -import logging - -from osc_lib.cli import parseractions -from osc_lib.command import command -from osc_lib import exceptions -from osc_lib import utils -import six - -from openstackclient.i18n import _ - - -deprecated = True -LOG_DEP = logging.getLogger('deprecated') -LOG = logging.getLogger(__name__) - - -class CreateSnapshot(command.ShowOne): - _description = _("Create new snapshot") - - def get_parser(self, prog_name): - parser = super(CreateSnapshot, self).get_parser(prog_name) - parser.add_argument( - "volume", - metavar="", - help=_("Volume to snapshot (name or ID)") - ) - parser.add_argument( - "--name", - metavar="", - help=_("Name of the snapshot") - ) - parser.add_argument( - "--description", - metavar="", - help=_("Description of the snapshot") - ) - parser.add_argument( - "--force", - action="store_true", - default=False, - help=_("Create a snapshot attached to an instance. " - "Default is False") - ) - parser.add_argument( - "--property", - metavar="", - action=parseractions.KeyValueAction, - help=_("Set a property to this snapshot " - "(repeat option to set multiple properties)"), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot create" instead.')) - volume_client = self.app.client_manager.volume - volume_id = utils.find_resource( - volume_client.volumes, parsed_args.volume).id - snapshot = volume_client.volume_snapshots.create( - volume_id, - force=parsed_args.force, - name=parsed_args.name, - description=parsed_args.description, - metadata=parsed_args.property, - ) - snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} - ) - return zip(*sorted(six.iteritems(snapshot._info))) - - -class DeleteSnapshot(command.Command): - _description = _("Delete volume snapshot(s)") - - def get_parser(self, prog_name): - parser = super(DeleteSnapshot, self).get_parser(prog_name) - parser.add_argument( - "snapshots", - metavar="", - nargs="+", - help=_("Snapshot(s) to delete (name or ID)") - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot delete" instead.')) - volume_client = self.app.client_manager.volume - result = 0 - - for i in parsed_args.snapshots: - try: - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, i).id - volume_client.volume_snapshots.delete(snapshot_id) - except Exception as e: - result += 1 - LOG.error(_("Failed to delete snapshot with " - "name or ID '%(snapshot)s': %(e)s") - % {'snapshot': i, 'e': e}) - - if result > 0: - total = len(parsed_args.snapshots) - msg = (_("%(result)s of %(total)s snapshots failed " - "to delete.") % {'result': result, 'total': total}) - raise exceptions.CommandError(msg) - - -class ListSnapshot(command.Lister): - _description = _("List snapshots") - - def get_parser(self, prog_name): - parser = super(ListSnapshot, self).get_parser(prog_name) - parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=_('Include all projects (admin only)'), - ) - parser.add_argument( - '--long', - action='store_true', - default=False, - help=_('List additional fields in output'), - ) - parser.add_argument( - '--marker', - metavar='', - help=_('The last snapshot ID of the previous page'), - ) - parser.add_argument( - '--limit', - type=int, - action=parseractions.NonNegativeAction, - metavar='', - help=_('Maximum number of snapshots to display'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot list" instead.')) - - def _format_volume_id(volume_id): - """Return a volume name if available - - :param volume_id: a volume ID - :rtype: either the volume ID or name - """ - - volume = volume_id - if volume_id in volume_cache.keys(): - volume = volume_cache[volume_id].name - return volume - - if parsed_args.long: - columns = ['ID', 'Name', 'Description', 'Status', - 'Size', 'Created At', 'Volume ID', 'Metadata'] - column_headers = copy.deepcopy(columns) - column_headers[6] = 'Volume' - column_headers[7] = 'Properties' - else: - columns = ['ID', 'Name', 'Description', 'Status', 'Size'] - column_headers = copy.deepcopy(columns) - - # Cache the volume list - volume_cache = {} - try: - for s in self.app.client_manager.volume.volumes.list(): - volume_cache[s.id] = s - except Exception: - # Just forget it if there's any trouble - pass - - search_opts = { - 'all_tenants': parsed_args.all_projects, - } - - data = self.app.client_manager.volume.volume_snapshots.list( - search_opts=search_opts, - marker=parsed_args.marker, - limit=parsed_args.limit, - ) - return (column_headers, - (utils.get_item_properties( - s, columns, - formatters={'Metadata': utils.format_dict, - 'Volume ID': _format_volume_id}, - ) for s in data)) - - -class SetSnapshot(command.Command): - _description = _("Set snapshot properties") - - def get_parser(self, prog_name): - parser = super(SetSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'snapshot', - metavar='', - help=_('Snapshot to modify (name or ID)') - ) - parser.add_argument( - '--name', - metavar='', - help=_('New snapshot name') - ) - parser.add_argument( - '--description', - metavar='', - help=_('New snapshot description') - ) - parser.add_argument( - '--property', - metavar='', - action=parseractions.KeyValueAction, - help=_('Property to add/change for this snapshot ' - '(repeat option to set multiple properties)'), - ) - parser.add_argument( - '--state', - metavar='', - choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], - help=_('New snapshot state. ("available", "error", "creating", ' - '"deleting", or "error_deleting") (admin only) ' - '(This option simply changes the state of the snapshot ' - 'in the database with no regard to actual status, ' - 'exercise caution when using)'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot set" instead.')) - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource(volume_client.volume_snapshots, - parsed_args.snapshot) - - result = 0 - if parsed_args.property: - try: - volume_client.volume_snapshots.set_metadata( - snapshot.id, parsed_args.property) - except Exception as e: - LOG.error(_("Failed to set snapshot property: %s"), e) - result += 1 - - if parsed_args.state: - try: - volume_client.volume_snapshots.reset_state( - snapshot.id, parsed_args.state) - except Exception as e: - LOG.error(_("Failed to set snapshot state: %s"), e) - result += 1 - - kwargs = {} - if parsed_args.name: - kwargs['name'] = parsed_args.name - if parsed_args.description: - kwargs['description'] = parsed_args.description - if kwargs: - try: - volume_client.volume_snapshots.update( - snapshot.id, **kwargs) - except Exception as e: - LOG.error(_("Failed to update snapshot name " - "or description: %s"), e) - result += 1 - - if result > 0: - raise exceptions.CommandError(_("One or more of the " - "set operations failed")) - - -class ShowSnapshot(command.ShowOne): - _description = _("Display snapshot details") - - def get_parser(self, prog_name): - parser = super(ShowSnapshot, self).get_parser(prog_name) - parser.add_argument( - "snapshot", - metavar="", - help=_("Snapshot to display (name or ID)") - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot show" instead.')) - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot) - snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} - ) - return zip(*sorted(six.iteritems(snapshot._info))) - - -class UnsetSnapshot(command.Command): - _description = _("Unset snapshot properties") - - def get_parser(self, prog_name): - parser = super(UnsetSnapshot, self).get_parser(prog_name) - parser.add_argument( - 'snapshot', - metavar='', - help=_('Snapshot to modify (name or ID)'), - ) - parser.add_argument( - '--property', - metavar='', - action='append', - default=[], - help=_('Property to remove from snapshot ' - '(repeat option to remove multiple properties)'), - ) - return parser - - def take_action(self, parsed_args): - LOG_DEP.warning(_('This command has been deprecated. ' - 'Please use "volume snapshot unset" instead.')) - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot) - - if parsed_args.property: - volume_client.volume_snapshots.delete_metadata( - snapshot.id, - parsed_args.property, - ) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index fa587b5f67..ef65d0970a 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -93,16 +93,6 @@ def get_parser(self, prog_name): metavar="", help=_("Volume description"), ) - parser.add_argument( - '--user', - metavar='', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--project', - metavar='', - help=argparse.SUPPRESS, - ) parser.add_argument( "--availability-zone", metavar="", @@ -127,12 +117,6 @@ def get_parser(self, prog_name): help=_("Arbitrary scheduler hint key-value pairs to help boot " "an instance (repeat option to set multiple hints)"), ) - parser.add_argument( - "--multi-attach", - action="store_true", - help=_("Allow volume to be attached more than once " - "(default to False)") - ) bootable_group = parser.add_mutually_exclusive_group() bootable_group.add_argument( "--bootable", @@ -196,26 +180,6 @@ def take_action(self, parsed_args): # snapshot size. size = max(size or 0, snapshot_obj.size) - # NOTE(abishop): Cinder's volumes.create() has 'project_id' and - # 'user_id' args, but they're not wired up to anything. The only way - # to specify an alternate project or user for the volume is to use - # the identity overrides (e.g. "--os-project-id"). - # - # Now, if the project or user arg is specified then the command is - # rejected. Otherwise, Cinder would actually create a volume, but - # without the specified property. - if parsed_args.project: - raise exceptions.CommandError( - _("ERROR: --project is deprecated, please use" - " --os-project-name or --os-project-id instead.")) - if parsed_args.user: - raise exceptions.CommandError( - _("ERROR: --user is deprecated, please use" - " --os-username instead.")) - if parsed_args.multi_attach: - LOG.warning(_("'--multi-attach' option is no longer supported by " - "the block storage service.")) - volume = volume_client.volumes.create( size=size, snapshot_id=snapshot, diff --git a/openstackclient/volume/v2/backup.py b/openstackclient/volume/v2/volume_backup.py similarity index 80% rename from openstackclient/volume/v2/backup.py rename to openstackclient/volume/v2/volume_backup.py index d4aec8d747..1d2b0cde04 100644 --- a/openstackclient/volume/v2/backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -94,23 +94,6 @@ def take_action(self, parsed_args): return zip(*sorted(six.iteritems(backup._info))) -class CreateBackup(CreateVolumeBackup): - _description = _("Create new backup") - - # TODO(Huanxuan Ao): Remove this class and ``backup create`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup create" instead.')) - return super(CreateBackup, self).take_action(parsed_args) - - class DeleteVolumeBackup(command.Command): _description = _("Delete volume backup(s)") @@ -152,23 +135,6 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -class DeleteBackup(DeleteVolumeBackup): - _description = _("Delete backup(s)") - - # TODO(Huanxuan Ao): Remove this class and ``backup delete`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup delete" instead.')) - return super(DeleteBackup, self).take_action(parsed_args) - - class ListVolumeBackup(command.Lister): _description = _("List volume backups") @@ -280,23 +246,6 @@ def _format_volume_id(volume_id): ) for s in data)) -class ListBackup(ListVolumeBackup): - _description = _("List backups") - - # TODO(Huanxuan Ao): Remove this class and ``backup list`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup list" instead.')) - return super(ListBackup, self).take_action(parsed_args) - - class RestoreVolumeBackup(command.ShowOne): _description = _("Restore volume backup") @@ -324,23 +273,6 @@ def take_action(self, parsed_args): return zip(*sorted(six.iteritems(backup._info))) -class RestoreBackup(RestoreVolumeBackup): - _description = _("Restore backup") - - # TODO(Huanxuan Ao): Remove this class and ``backup restore`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup restore" instead.')) - return super(RestoreBackup, self).take_action(parsed_args) - - class SetVolumeBackup(command.Command): _description = _("Set volume backup properties") @@ -421,20 +353,3 @@ def take_action(self, parsed_args): parsed_args.backup) backup._info.pop("links", None) return zip(*sorted(six.iteritems(backup._info))) - - -class ShowBackup(ShowVolumeBackup): - _description = _("Display backup details") - - # TODO(Huanxuan Ao): Remove this class and ``backup show`` command - # two cycles after Newton. - - # This notifies cliff to not display the help for this command - deprecated = True - - log = logging.getLogger('deprecated') - - def take_action(self, parsed_args): - self.log.warning(_('This command has been deprecated. ' - 'Please use "volume backup show" instead.')) - return super(ShowBackup, self).take_action(parsed_args) diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index 2f531dc871..4c4741bc30 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -14,7 +14,6 @@ """Volume v2 transfer action implementations""" -import argparse import logging from osc_lib.command import command @@ -38,15 +37,10 @@ def get_parser(self, prog_name): metavar="", help=_('Volume transfer request to accept (ID only)'), ) - parser.add_argument( - 'old_auth_key', - metavar="", - nargs="?", - help=argparse.SUPPRESS, - ) parser.add_argument( '--auth-key', metavar="", + required=True, help=_('Volume transfer request authentication key'), ) return parser @@ -64,21 +58,6 @@ def take_action(self, parsed_args): # move on and attempt with the user-supplied information transfer_request_id = parsed_args.transfer_request - # Remain backward-compatible for the previous command layout - # TODO(dtroyer): Remove this back-compat in 4.0 or Oct 2017 - if not parsed_args.auth_key: - if parsed_args.old_auth_key: - # Move the old one into the correct place - parsed_args.auth_key = parsed_args.old_auth_key - self.log.warning(_( - 'Specifying the auth-key as a positional argument ' - 'has been deprecated. Please use the --auth-key ' - 'option in the future.' - )) - else: - msg = _("argument --auth-key is required") - raise exceptions.CommandError(msg) - transfer_accept = volume_client.transfers.accept( transfer_request_id, parsed_args.auth_key, diff --git a/releasenotes/notes/osc4-volume-470422e5a453310e.yaml b/releasenotes/notes/osc4-volume-470422e5a453310e.yaml new file mode 100644 index 0000000000..e03f0fdcd6 --- /dev/null +++ b/releasenotes/notes/osc4-volume-470422e5a453310e.yaml @@ -0,0 +1,14 @@ +--- +upgrade: + - | + Remove deprecated ``backup`` commands. + Use ``volume backup`` commands instead. + - | + Remove deprecated ``snapshot`` commands. + Use ``volume snapshot`` commands instead. + - | + Remove deprecated ``volume create`` options ``--project``, ``--user`` + and ``--multi-attach``. + - | + Change ``volume transfer request accept`` to use new required option + ``--auth-key`` rather than a second positional argument. diff --git a/setup.cfg b/setup.cfg index cc57a1037a..d3d79983b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -526,19 +526,6 @@ openstack.object_store.v1 = object_unset = openstackclient.object.v1.object:UnsetObject openstack.volume.v1 = - backup_create = openstackclient.volume.v1.backup:CreateBackup - backup_delete = openstackclient.volume.v1.backup:DeleteBackup - backup_list = openstackclient.volume.v1.backup:ListBackup - backup_restore = openstackclient.volume.v1.backup:RestoreBackup - backup_show = openstackclient.volume.v1.backup:ShowBackup - - snapshot_create = openstackclient.volume.v1.snapshot:CreateSnapshot - snapshot_delete = openstackclient.volume.v1.snapshot:DeleteSnapshot - snapshot_list = openstackclient.volume.v1.snapshot:ListSnapshot - snapshot_set = openstackclient.volume.v1.snapshot:SetSnapshot - snapshot_show = openstackclient.volume.v1.snapshot:ShowSnapshot - snapshot_unset = openstackclient.volume.v1.snapshot:UnsetSnapshot - volume_create = openstackclient.volume.v1.volume:CreateVolume volume_delete = openstackclient.volume.v1.volume:DeleteVolume volume_list = openstackclient.volume.v1.volume:ListVolume @@ -547,11 +534,11 @@ openstack.volume.v1 = volume_show = openstackclient.volume.v1.volume:ShowVolume volume_unset = openstackclient.volume.v1.volume:UnsetVolume - volume_backup_create = openstackclient.volume.v1.backup:CreateVolumeBackup - volume_backup_delete = openstackclient.volume.v1.backup:DeleteVolumeBackup - volume_backup_list = openstackclient.volume.v1.backup:ListVolumeBackup - volume_backup_restore = openstackclient.volume.v1.backup:RestoreVolumeBackup - volume_backup_show = openstackclient.volume.v1.backup:ShowVolumeBackup + volume_backup_create = openstackclient.volume.v1.volume_backup:CreateVolumeBackup + volume_backup_delete = openstackclient.volume.v1.volume_backup:DeleteVolumeBackup + volume_backup_list = openstackclient.volume.v1.volume_backup:ListVolumeBackup + volume_backup_restore = openstackclient.volume.v1.volume_backup:RestoreVolumeBackup + volume_backup_show = openstackclient.volume.v1.volume_backup:ShowVolumeBackup volume_snapshot_create = openstackclient.volume.v1.volume_snapshot:CreateVolumeSnapshot volume_snapshot_delete = openstackclient.volume.v1.volume_snapshot:DeleteVolumeSnapshot @@ -586,12 +573,6 @@ openstack.volume.v1 = volume_transfer_request_show = openstackclient.volume.v1.volume_transfer_request:ShowTransferRequest openstack.volume.v2 = - backup_create = openstackclient.volume.v2.backup:CreateBackup - backup_delete = openstackclient.volume.v2.backup:DeleteBackup - backup_list = openstackclient.volume.v2.backup:ListBackup - backup_restore = openstackclient.volume.v2.backup:RestoreBackup - backup_show = openstackclient.volume.v2.backup:ShowBackup - consistency_group_add_volume = openstackclient.volume.v2.consistency_group:AddVolumeToConsistencyGroup consistency_group_create = openstackclient.volume.v2.consistency_group:CreateConsistencyGroup consistency_group_delete = openstackclient.volume.v2.consistency_group:DeleteConsistencyGroup @@ -605,13 +586,6 @@ openstack.volume.v2 = consistency_group_snapshot_list = openstackclient.volume.v2.consistency_group_snapshot:ListConsistencyGroupSnapshot consistency_group_snapshot_show = openstackclient.volume.v2.consistency_group_snapshot:ShowConsistencyGroupSnapshot - snapshot_create = openstackclient.volume.v2.snapshot:CreateSnapshot - snapshot_delete = openstackclient.volume.v2.snapshot:DeleteSnapshot - snapshot_list = openstackclient.volume.v2.snapshot:ListSnapshot - snapshot_set = openstackclient.volume.v2.snapshot:SetSnapshot - snapshot_show = openstackclient.volume.v2.snapshot:ShowSnapshot - snapshot_unset = openstackclient.volume.v2.snapshot:UnsetSnapshot - volume_create = openstackclient.volume.v2.volume:CreateVolume volume_delete = openstackclient.volume.v2.volume:DeleteVolume volume_list = openstackclient.volume.v2.volume:ListVolume @@ -620,12 +594,12 @@ openstack.volume.v2 = volume_show = openstackclient.volume.v2.volume:ShowVolume volume_unset = openstackclient.volume.v2.volume:UnsetVolume - volume_backup_create = openstackclient.volume.v2.backup:CreateVolumeBackup - volume_backup_delete = openstackclient.volume.v2.backup:DeleteVolumeBackup - volume_backup_list = openstackclient.volume.v2.backup:ListVolumeBackup - volume_backup_restore = openstackclient.volume.v2.backup:RestoreVolumeBackup - volume_backup_set = openstackclient.volume.v2.backup:SetVolumeBackup - volume_backup_show = openstackclient.volume.v2.backup:ShowVolumeBackup + volume_backup_create = openstackclient.volume.v2.volume_backup:CreateVolumeBackup + volume_backup_delete = openstackclient.volume.v2.volume_backup:DeleteVolumeBackup + volume_backup_list = openstackclient.volume.v2.volume_backup:ListVolumeBackup + volume_backup_restore = openstackclient.volume.v2.volume_backup:RestoreVolumeBackup + volume_backup_set = openstackclient.volume.v2.volume_backup:SetVolumeBackup + volume_backup_show = openstackclient.volume.v2.volume_backup:ShowVolumeBackup volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord From 3057989714e5e510e275c454258e0f167ed551d2 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 15 May 2019 17:57:29 -0400 Subject: [PATCH 0091/1120] Deprecate openstack server migrate --host option Per the discussion at the Train Forum [1] this deprecates the problematic --live option on the server migrate command which, depending on the compute API version used, forcefully bypasses the scheduler and also does not allow you to live migrate a server and let the scheduler pick a host. The --live option is replaced here with two new options: * --live-migration: this simply tells the command you want to perform a live rather than cold migration; if specified with --live the --live-migration option takes priority. * --host: when specified, this will request a target host for the live migration and will be validated by the scheduler; if not specified, the scheduler will pick a host. This option is mutually exclusive with --live. We can build on the --host option by supporting cold migrations with a specified host when using compute API version 2.56 or greater but that will come in a separate change. If the --live option is ever used we log a warning. Note there are several related changes for this issue: - https://review.openstack.org/#/c/628334/ - https://review.openstack.org/#/c/626949/ - https://review.openstack.org/#/c/627801/ - https://review.openstack.org/#/c/589012/ - https://review.openstack.org/#/c/460059/ This change allows us to deprecate the --live option and provide a replacement which is backward compatible without having to use something potentially error-prone like nargs='?'. Closes-Bug: #1411190 [1] https://etherpad.openstack.org/p/DEN-osc-compute-api-gaps Change-Id: I95d3d588e4abeb6848bdccf6915f7b5da40b5d4f --- openstackclient/compute/v2/server.py | 78 ++++++++- .../tests/unit/compute/v2/test_server.py | 158 +++++++++++++++++- ...-live-migration-host-655ae6befa6a3de2.yaml | 27 +++ 3 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/bug-1411190-live-migration-host-655ae6befa6a3de2.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index cb9f8d43eb..fefd4b3773 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1407,9 +1407,38 @@ def get_parser(self, prog_name): help=_('Server (name or ID)'), ) parser.add_argument( + '--live-migration', + dest='live_migration', + action='store_true', + help=_('Live migrate the server. Use the ``--host`` option to ' + 'specify a target host for the migration which will be ' + 'validated by the scheduler.'), + ) + # The --live and --host options are mutually exclusive ways of asking + # for a target host during a live migration. + host_group = parser.add_mutually_exclusive_group() + # TODO(mriedem): Remove --live in the next major version bump after + # the Train release. + host_group.add_argument( '--live', metavar='', - help=_('Target hostname'), + help=_('**Deprecated** This option is problematic in that it ' + 'requires a host and prior to compute API version 2.30, ' + 'specifying a host during live migration will bypass ' + 'validation by the scheduler which could result in ' + 'failures to actually migrate the server to the specified ' + 'host or over-subscribe the host. Use the ' + '``--live-migration`` option instead. If both this option ' + 'and ``--live-migration`` are used, ``--live-migration`` ' + 'takes priority.'), + ) + # TODO(mriedem): Add support for --os-compute-api-version >= 2.56 where + # you can cold migrate to a specified target host. + host_group.add_argument( + '--host', + metavar='', + help=_('Live migrate the server to the specified host. Requires ' + '``--os-compute-api-version`` 2.30 or greater.'), ) migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( @@ -1447,6 +1476,15 @@ def get_parser(self, prog_name): ) return parser + def _log_warning_for_live(self, parsed_args): + if parsed_args.live: + # NOTE(mriedem): The --live option requires a host and if + # --os-compute-api-version is less than 2.30 it will forcefully + # bypass the scheduler which is dangerous. + self.log.warning(_( + 'The --live option has been deprecated. Please use the ' + '--live-migration option instead.')) + def take_action(self, parsed_args): def _show_progress(progress): @@ -1460,19 +1498,45 @@ def _show_progress(progress): compute_client.servers, parsed_args.server, ) - if parsed_args.live: + # Check for live migration. + if parsed_args.live or parsed_args.live_migration: + # Always log a warning if --live is used. + self._log_warning_for_live(parsed_args) kwargs = { - 'host': parsed_args.live, 'block_migration': parsed_args.block_migration } + # Prefer --live-migration over --live if both are specified. + if parsed_args.live_migration: + # Technically we could pass a non-None host with + # --os-compute-api-version < 2.30 but that is the same thing + # as the --live option bypassing the scheduler which we don't + # want to support, so if the user is using --live-migration + # and --host, we want to enforce that they are using version + # 2.30 or greater. + if (parsed_args.host and + compute_client.api_version < + api_versions.APIVersion('2.30')): + raise exceptions.CommandError( + '--os-compute-api-version 2.30 or greater is required ' + 'when using --host') + # The host parameter is required in the API even if None. + kwargs['host'] = parsed_args.host + else: + kwargs['host'] = parsed_args.live + if compute_client.api_version < api_versions.APIVersion('2.25'): kwargs['disk_over_commit'] = parsed_args.disk_overcommit server.live_migrate(**kwargs) else: - if parsed_args.block_migration or parsed_args.disk_overcommit: - raise exceptions.CommandError("--live must be specified if " - "--block-migration or " - "--disk-overcommit is specified") + if (parsed_args.block_migration or parsed_args.disk_overcommit or + parsed_args.host): + # TODO(mriedem): Allow --host for cold migration if + # --os-compute-api-version >= 2.56. + raise exceptions.CommandError( + "--live-migration must be specified if " + "--block-migration, --disk-overcommit or --host is " + "specified") + server.migrate() if parsed_args.wait: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c30af8fb8e..2a301b4541 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -23,6 +23,7 @@ from osc_lib import exceptions from osc_lib import utils as common_utils from oslo_utils import timeutils +import six from openstackclient.compute.v2 import server from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -2415,12 +2416,40 @@ def test_server_migrate_with_disk_overcommit(self): self.assertNotCalled(self.servers_mock.live_migrate) self.assertNotCalled(self.servers_mock.migrate) + def test_server_migrate_with_host(self): + # Tests that --host is not allowed for a cold migration. + arglist = [ + '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', False), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # Make sure it's the error we expect. + self.assertIn("--live-migration must be specified if " + "--block-migration, --disk-overcommit or --host is " + "specified", six.text_type(ex)) + self.servers_mock.get.assert_called_with(self.server.id) + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertNotCalled(self.servers_mock.migrate) + def test_server_live_migrate(self): arglist = [ '--live', 'fakehost', self.server.id, ] verifylist = [ ('live', 'fakehost'), + ('live_migration', False), + ('host', None), ('block_migration', False), ('disk_overcommit', False), ('wait', False), @@ -2430,7 +2459,8 @@ def test_server_live_migrate(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.24') - result = self.cmd.take_action(parsed_args) + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) self.server.live_migrate.assert_called_with(block_migration=False, @@ -2438,6 +2468,132 @@ def test_server_live_migrate(self): host='fakehost') self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) + # A warning should have been logged for using --live. + mock_warning.assert_called_once() + self.assertIn('The --live option has been deprecated.', + six.text_type(mock_warning.call_args[0][0])) + + def test_server_live_migrate_host_pre_2_30(self): + # Tests that the --host option is not supported for --live-migration + # before microversion 2.30 (the test defaults to 2.1). + arglist = [ + '--live-migration', '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # Make sure it's the error we expect. + self.assertIn('--os-compute-api-version 2.30 or greater is required ' + 'when using --host', six.text_type(ex)) + + self.servers_mock.get.assert_called_with(self.server.id) + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertNotCalled(self.servers_mock.migrate) + + def test_server_live_migrate_no_host(self): + # Tests the --live-migration option without --host or --live. + arglist = [ + '--live-migration', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('host', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=False, + host=None) + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + # Since --live wasn't used a warning shouldn't have been logged. + mock_warning.assert_not_called() + + def test_server_live_migrate_with_host(self): + # Tests the --live-migration option with --host but no --live. + # This requires --os-compute-api-version >= 2.30 so the test uses 2.30. + arglist = [ + '--live-migration', '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.30') + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + # No disk_overcommit with microversion >= 2.25. + self.server.live_migrate.assert_called_with(block_migration=False, + host='fakehost') + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + + def test_server_live_migrate_without_host_override_live(self): + # Tests the --live-migration option without --host and with --live. + # The --live-migration option will take precedence and a warning is + # logged for using --live. + arglist = [ + '--live', 'fakehost', '--live-migration', self.server.id, + ] + verifylist = [ + ('live', 'fakehost'), + ('live_migration', True), + ('host', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=False, + host=None) + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + # A warning should have been logged for using --live. + mock_warning.assert_called_once() + self.assertIn('The --live option has been deprecated.', + six.text_type(mock_warning.call_args[0][0])) + + def test_server_live_migrate_live_and_host_mutex(self): + # Tests specifying both the --live and --host options which are in a + # mutex group so argparse should fail. + arglist = [ + '--live', 'fakehost', '--host', 'fakehost', self.server.id, + ] + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, verify_args=[]) def test_server_block_live_migrate(self): arglist = [ diff --git a/releasenotes/notes/bug-1411190-live-migration-host-655ae6befa6a3de2.yaml b/releasenotes/notes/bug-1411190-live-migration-host-655ae6befa6a3de2.yaml new file mode 100644 index 0000000000..927d6a6451 --- /dev/null +++ b/releasenotes/notes/bug-1411190-live-migration-host-655ae6befa6a3de2.yaml @@ -0,0 +1,27 @@ +--- +deprecations: + - | + The ``--live`` option on the ``openstack server migrate`` command has + been deprecated and is being replaced with two new options: + + * ``--live-migration``: This will signal that the migration is a live + migration. + * ``--host``: This can be used to request a target host for the live + migration but requires ``--os-compute-api-version`` 2.30 or greater + so the requested host can be validated by the scheduler. + + The ``--live`` option is problematic in that it requires a host and + prior to compute API version 2.30, specifying a host during live migration + will bypass validation by the scheduler which could result in failures to + actually migrate the server to the specified host or over-subscribe the + host. + + The ``--live`` and ``--host`` options are mutually exclusive. Furthermore, + if both the ``--live`` and ``--live-migration`` options are used the + ``--live-migration`` option takes priority. +fixes: + - | + `Bug 1411190`_ has been fixed by providing a ``--live-migration`` and + ``--host`` option to the ``openstack server migrate`` command. + + .. _Bug 1411190: https://bugs.launchpad.net/python-openstackclient/+bug/1411190 From ef1fd388154eee11b9e83f80e5004670fdffb6cc Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Mon, 6 May 2019 19:06:55 +0800 Subject: [PATCH 0092/1120] Add changes-before attribute to server list Closes-Bug: #1827844 Part of bp support-to-query-nova-resources-filter-by-changes-before Change-Id: I4f28168188973730247bcbcb70ba0e70eb81e3be --- openstackclient/compute/v2/server.py | 27 ++++++- .../functional/compute/v2/test_server.py | 78 +++++++++++++++++++ .../tests/unit/compute/v2/test_server.py | 66 ++++++++++++++++ .../notes/bug-1827844-8f1de120087c6a22.yaml | 6 ++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/bug-1827844-8f1de120087c6a22.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index cb9f8d43eb..bc6b137bf1 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1128,13 +1128,22 @@ def get_parser(self, prog_name): default=False, help=_('Only display deleted servers (Admin only).') ) + parser.add_argument( + '--changes-before', + metavar='', + default=None, + help=_("List only servers changed before a certain point of time. " + "The provided time should be an ISO 8061 formatted time " + "(e.g., 2016-03-05T06:27:59Z). " + "(Supported by API versions '2.66' - '2.latest')") + ) parser.add_argument( '--changes-since', metavar='', default=None, help=_("List only servers changed after a certain point of time." - " The provided time should be an ISO 8061 formatted time." - " ex 2016-03-04T06:27:59Z .") + " The provided time should be an ISO 8061 formatted time" + " (e.g., 2016-03-04T06:27:59Z).") ) return parser @@ -1188,10 +1197,24 @@ def take_action(self, parsed_args): 'all_tenants': parsed_args.all_projects, 'user_id': user_id, 'deleted': parsed_args.deleted, + 'changes-before': parsed_args.changes_before, 'changes-since': parsed_args.changes_since, } LOG.debug('search options: %s', search_opts) + if search_opts['changes-before']: + if compute_client.api_version < api_versions.APIVersion('2.66'): + msg = _('--os-compute-api-version 2.66 or later is required') + raise exceptions.CommandError(msg) + + try: + timeutils.parse_isotime(search_opts['changes-before']) + except ValueError: + raise exceptions.CommandError( + _('Invalid changes-before value: %s') % + search_opts['changes-before'] + ) + if search_opts['changes-since']: try: timeutils.parse_isotime(search_opts['changes-since']) diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index c8fb44d380..6330ac98d5 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -63,6 +63,84 @@ def test_server_list(self): self.assertNotIn(name1, col_name) self.assertIn(name2, col_name) + def test_server_list_with_changes_before(self): + """Test server list. + + Getting the servers list with updated_at time equal or + before than changes-before. + """ + cmd_output = self.server_create() + server_name1 = cmd_output['name'] + + cmd_output = self.server_create() + server_name2 = cmd_output['name'] + updated_at2 = cmd_output['updated'] + + cmd_output = self.server_create() + server_name3 = cmd_output['name'] + + cmd_output = json.loads(self.openstack( + '--os-compute-api-version 2.66 ' + + 'server list -f json ' + '--changes-before ' + updated_at2 + )) + + col_updated = [server["Name"] for server in cmd_output] + self.assertIn(server_name1, col_updated) + self.assertIn(server_name2, col_updated) + self.assertNotIn(server_name3, col_updated) + + def test_server_list_with_changes_since(self): + """Test server list. + + Getting the servers list with updated_at time equal or + later than changes-since. + """ + cmd_output = self.server_create() + server_name1 = cmd_output['name'] + cmd_output = self.server_create() + server_name2 = cmd_output['name'] + updated_at2 = cmd_output['updated'] + cmd_output = self.server_create() + server_name3 = cmd_output['name'] + + cmd_output = json.loads(self.openstack( + 'server list -f json ' + '--changes-since ' + updated_at2 + )) + + col_updated = [server["Name"] for server in cmd_output] + self.assertNotIn(server_name1, col_updated) + self.assertIn(server_name2, col_updated) + self.assertIn(server_name3, col_updated) + + def test_server_list_with_changes_before_and_changes_since(self): + """Test server list. + + Getting the servers list with updated_at time equal or before than + changes-before and equal or later than changes-since. + """ + cmd_output = self.server_create() + server_name1 = cmd_output['name'] + cmd_output = self.server_create() + server_name2 = cmd_output['name'] + updated_at2 = cmd_output['updated'] + cmd_output = self.server_create() + server_name3 = cmd_output['name'] + updated_at3 = cmd_output['updated'] + + cmd_output = json.loads(self.openstack( + '--os-compute-api-version 2.66 ' + + 'server list -f json ' + + '--changes-since ' + updated_at2 + + ' --changes-before ' + updated_at3 + )) + + col_updated = [server["Name"] for server in cmd_output] + self.assertNotIn(server_name1, col_updated) + self.assertIn(server_name2, col_updated) + self.assertIn(server_name3, col_updated) + def test_server_set(self): """Test server create, delete, set, show""" cmd_output = self.server_create() diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c30af8fb8e..f0c8843b5d 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1991,6 +1991,7 @@ def setUp(self): 'user_id': None, 'deleted': False, 'changes-since': None, + 'changes-before': None, } # Default params of the core function of the command in the case of no @@ -2272,6 +2273,71 @@ def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): 'Invalid time value' ) + def test_server_list_v266_with_changes_before(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.66')) + arglist = [ + '--changes-before', '2016-03-05T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_before', '2016-03-05T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' + self.search_opts['deleted'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) + def test_server_list_v266_with_invalid_changes_before( + self, mock_parse_isotime): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.66')) + + arglist = [ + '--changes-before', 'Invalid time value', + ] + verifylist = [ + ('changes_before', 'Invalid time value'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('Invalid changes-before value: Invalid time ' + 'value', str(e)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) + + def test_server_with_changes_before_older_version(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.65')) + + arglist = [ + '--changes-before', '2016-03-05T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_before', '2016-03-05T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + def test_server_list_v269_with_partial_constructs(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.69') diff --git a/releasenotes/notes/bug-1827844-8f1de120087c6a22.yaml b/releasenotes/notes/bug-1827844-8f1de120087c6a22.yaml new file mode 100644 index 0000000000..56b69c1cde --- /dev/null +++ b/releasenotes/notes/bug-1827844-8f1de120087c6a22.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--changes-before`` option to the ``server list`` command. + This requires Compute API version '2.66' or later. + [:lpbug: `1827844`] From eb399c52ad45285ac50ea81bd3a8e4bc459544a5 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Jun 2019 11:23:06 -0400 Subject: [PATCH 0093/1120] Add server event command documentation for compute API 2.21 The 2.21 compute API microversion allows listing instance action events and getting action event details for a deleted server (which can be useful for auditing until the deleted server is purged). As far as OSC is concerned it's just a matter of specifying --os-compute-api-version 2.21 or higher when listing events or showing event details, so this change mentions 2.21 in the description of those commands. Related to nova blueprint os-instance-actions-read-deleted-instances Change-Id: If276c794f448b6fa5b0845499f3507a159acab85 --- openstackclient/compute/v2/server_event.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py index c7d2e2e385..6d33d02d50 100644 --- a/openstackclient/compute/v2/server_event.py +++ b/openstackclient/compute/v2/server_event.py @@ -28,7 +28,10 @@ class ListServerEvent(command.Lister): - _description = _("List recent events of a server") + _description = _( + "List recent events of a server. " + "Specify ``--os-compute-api-version 2.21`` " + "or higher to show events for a deleted server.") def get_parser(self, prog_name): parser = super(ListServerEvent, self).get_parser(prog_name) @@ -92,8 +95,11 @@ def take_action(self, parsed_args): class ShowServerEvent(command.ShowOne): _description = _( - "Show server event details. Specify ``--os-compute-api-version 2.51`` " - "or higher to show events for non-admin users.") + "Show server event details. " + "Specify ``--os-compute-api-version 2.21`` " + "or higher to show event details for a deleted server. " + "Specify ``--os-compute-api-version 2.51`` " + "or higher to show event details for non-admin users.") def get_parser(self, prog_name): parser = super(ShowServerEvent, self).get_parser(prog_name) From 3df5f92b447e647cdfff76010d729d74c89b39b9 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 12 Jun 2019 11:39:54 -0400 Subject: [PATCH 0094/1120] Add server add/remove volume description for microversion 2.20 The compute API 2.20 microversion allows attaching and detaching a volume to/from a server with status SHELVED or SHELVED_OFFLOADED. For OSC this just means the user has to specify the appropriate minimum microversion to make that work, so this change mentions that in the "server add volume" and "server remove volume" command description. Related to nova blueprint volume-ops-when-shelved Change-Id: I4824175e5d9e124e3bd9e9a8fd5a89277efc6cff --- openstackclient/compute/v2/server.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 3e1deed59a..654440fc55 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -424,7 +424,10 @@ def take_action(self, parsed_args): class AddServerVolume(command.Command): - _description = _("Add volume to server") + _description = _( + "Add volume to server. " + "Specify ``--os-compute-api-version 2.20`` or higher to add a volume " + "to a server with status ``SHELVED`` or ``SHELVED_OFFLOADED``.") def get_parser(self, prog_name): parser = super(AddServerVolume, self).get_parser(prog_name) @@ -1949,7 +1952,11 @@ def take_action(self, parsed_args): class RemoveServerVolume(command.Command): - _description = _("Remove volume from server") + _description = _( + "Remove volume from server. " + "Specify ``--os-compute-api-version 2.20`` or higher to remove a " + "volume from a server with status ``SHELVED`` or " + "``SHELVED_OFFLOADED``.") def get_parser(self, prog_name): parser = super(RemoveServerVolume, self).get_parser(prog_name) From 1aad94349bcd8f4b772bd6f216e4af897d46053b Mon Sep 17 00:00:00 2001 From: Martin Chlumsky Date: Thu, 13 Jun 2019 23:16:53 -0400 Subject: [PATCH 0095/1120] Allow "server migrate" (not live) to take "--host" option Currently, doing a cold migration while specifying a target host is not possible however nova api supports it since version 2.56. This patch allows passing "--host" when doing a cold migration. It runs normally if --os-compute-api-version is 2.56 or greater and returns an error otherwise. Change-Id: I960109008096ce8bb4e4c8ca6ffb22c33aacd995 Story: 2003325 Task: 24359 --- openstackclient/compute/v2/server.py | 26 ++++++++----- .../tests/unit/compute/v2/test_server.py | 38 ++++++++++++++++--- ...er-migrate-with-host-4884a71903c5c8a9.yaml | 8 ++++ 3 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 3e1deed59a..1d022f0336 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1470,13 +1470,13 @@ def get_parser(self, prog_name): 'and ``--live-migration`` are used, ``--live-migration`` ' 'takes priority.'), ) - # TODO(mriedem): Add support for --os-compute-api-version >= 2.56 where - # you can cold migrate to a specified target host. host_group.add_argument( '--host', metavar='', - help=_('Live migrate the server to the specified host. Requires ' - '``--os-compute-api-version`` 2.30 or greater.'), + help=_('Migrate the server to the specified host. Requires ' + '``--os-compute-api-version`` 2.30 or greater when used ' + 'with the ``--live-migration`` option, otherwise requires ' + '``--os-compute-api-version`` 2.56 or greater.'), ) migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( @@ -1566,16 +1566,22 @@ def _show_progress(progress): kwargs['disk_over_commit'] = parsed_args.disk_overcommit server.live_migrate(**kwargs) else: - if (parsed_args.block_migration or parsed_args.disk_overcommit or - parsed_args.host): - # TODO(mriedem): Allow --host for cold migration if - # --os-compute-api-version >= 2.56. + if parsed_args.block_migration or parsed_args.disk_overcommit: raise exceptions.CommandError( "--live-migration must be specified if " - "--block-migration, --disk-overcommit or --host is " + "--block-migration or --disk-overcommit is " "specified") + if parsed_args.host: + if (compute_client.api_version < + api_versions.APIVersion('2.56')): + msg = _( + '--os-compute-api-version 2.56 or greater is ' + 'required to use --host without --live-migration.' + ) + raise exceptions.CommandError(msg) - server.migrate() + kwargs = {'host': parsed_args.host} if parsed_args.host else {} + server.migrate(**kwargs) if parsed_args.wait: if utils.wait_for_status( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 4a37a81216..473de802a2 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2528,6 +2528,32 @@ def test_server_migrate_no_options(self): self.assertNotCalled(self.servers_mock.live_migrate) self.assertIsNone(result) + def test_server_migrate_with_host_2_56(self): + # Tests that --host is allowed for a cold migration + # for microversion 2.56 and greater. + arglist = [ + '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', False), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.56') + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.migrate.assert_called_with(host='fakehost') + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertIsNone(result) + def test_server_migrate_with_block_migration(self): arglist = [ '--block-migration', self.server.id, @@ -2566,8 +2592,9 @@ def test_server_migrate_with_disk_overcommit(self): self.assertNotCalled(self.servers_mock.live_migrate) self.assertNotCalled(self.servers_mock.migrate) - def test_server_migrate_with_host(self): - # Tests that --host is not allowed for a cold migration. + def test_server_migrate_with_host_pre_2_56(self): + # Tests that --host is not allowed for a cold migration + # before microversion 2.56 (the test defaults to 2.1). arglist = [ '--host', 'fakehost', self.server.id, ] @@ -2585,9 +2612,10 @@ def test_server_migrate_with_host(self): parsed_args) # Make sure it's the error we expect. - self.assertIn("--live-migration must be specified if " - "--block-migration, --disk-overcommit or --host is " - "specified", six.text_type(ex)) + self.assertIn('--os-compute-api-version 2.56 or greater is required ' + 'to use --host without --live-migration.', + six.text_type(ex)) + self.servers_mock.get.assert_called_with(self.server.id) self.assertNotCalled(self.servers_mock.live_migrate) self.assertNotCalled(self.servers_mock.migrate) diff --git a/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml b/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml new file mode 100644 index 0000000000..dd974ff0b3 --- /dev/null +++ b/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added the ability to specify ``--host`` with ``server migrate`` + (cold migration) to specify the target host of the migration. + Requires ``--os-compute-api-version`` 2.56 or greater to target a + specific host for the (cold) migration. + [Story `2003325 `_] From 187be0ac22be5d2d1c42d45e9299e62cfc3796ae Mon Sep 17 00:00:00 2001 From: Surya Seetharaman Date: Tue, 14 May 2019 18:35:54 +0200 Subject: [PATCH 0096/1120] Microversion 2.73: Support adding the reason behind a server lock This patch adds a new parameter ``--reason`` to ``openstack server lock`` command and ``--locked``, ``unlocked`` filtering parameters to ``openstack server list`` command. This can help users to provide a reason when locking the server and to filter instances based on their locked value from 2.73 microversion. Implements blueprint add-locked-reason Depends-On: https://review.opendev.org/#/c/661785/ Change-Id: Ib2714f98b24d47e570da8a6c231e765acd2ff595 --- lower-constraints.txt | 2 +- openstackclient/compute/v2/server.py | 47 +++++- .../tests/unit/compute/v2/test_server.py | 149 +++++++++++++++++- ...bp-add-locked-reason-425efd2def1144f1.yaml | 13 ++ requirements.txt | 2 +- 5 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/bp-add-locked-reason-425efd2def1144f1.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 43d724a1e0..f387827948 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -100,7 +100,7 @@ python-mimeparse==1.6.0 python-mistralclient==3.1.0 python-muranoclient==0.8.2 python-neutronclient==6.7.0 -python-novaclient==10.0.0 +python-novaclient==14.1.0 python-octaviaclient==1.3.0 python-rsdclient==0.1.0 python-saharaclient==1.4.0 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index cb9f8d43eb..b37b8c5294 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1136,6 +1136,21 @@ def get_parser(self, prog_name): " The provided time should be an ISO 8061 formatted time." " ex 2016-03-04T06:27:59Z .") ) + lock_group = parser.add_mutually_exclusive_group() + lock_group.add_argument( + '--locked', + action='store_true', + default=False, + help=_('Only display locked servers. ' + 'Requires ``--os-compute-api-version`` 2.73 or greater.'), + ) + lock_group.add_argument( + '--unlocked', + action='store_true', + default=False, + help=_('Only display unlocked servers. ' + 'Requires ``--os-compute-api-version`` 2.73 or greater.'), + ) return parser def take_action(self, parsed_args): @@ -1190,6 +1205,18 @@ def take_action(self, parsed_args): 'deleted': parsed_args.deleted, 'changes-since': parsed_args.changes_since, } + support_locked = (compute_client.api_version >= + api_versions.APIVersion('2.73')) + if not support_locked and (parsed_args.locked or parsed_args.unlocked): + msg = _('--os-compute-api-version 2.73 or greater is required to ' + 'use the (un)locked filter option.') + raise exceptions.CommandError(msg) + elif support_locked: + # Only from 2.73. + if parsed_args.locked: + search_opts['locked'] = True + if parsed_args.unlocked: + search_opts['locked'] = False LOG.debug('search options: %s', search_opts) if search_opts['changes-since']: @@ -1374,16 +1401,28 @@ def get_parser(self, prog_name): nargs='+', help=_('Server(s) to lock (name or ID)'), ) + parser.add_argument( + '--reason', + metavar='', + default=None, + help=_("Reason for locking the server(s). Requires " + "``--os-compute-api-version`` 2.73 or greater.") + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + support_reason = compute_client.api_version >= api_versions.APIVersion( + '2.73') + if not support_reason and parsed_args.reason: + msg = _('--os-compute-api-version 2.73 or greater is required to ' + 'use the --reason option.') + raise exceptions.CommandError(msg) for server in parsed_args.server: - utils.find_resource( - compute_client.servers, - server, - ).lock() + serv = utils.find_resource(compute_client.servers, server) + (serv.lock(reason=parsed_args.reason) if support_reason + else serv.lock()) # FIXME(dtroyer): Here is what I want, how with argparse/cliff? diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c30af8fb8e..c71a31cec8 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -90,7 +90,14 @@ def run_method_with_servers(self, method_name, server_count): for s in servers: method = getattr(s, method_name) - method.assert_called_with() + if method_name == 'lock': + version = self.app.client_manager.compute.api_version + if version >= api_versions.APIVersion('2.73'): + method.assert_called_with(reason=None) + else: + method.assert_called_with() + else: + method.assert_called_with() self.assertIsNone(result) @@ -2210,6 +2217,80 @@ def test_server_list_with_image(self): self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) + def test_server_list_with_locked_pre_v273(self): + + arglist = [ + '--locked' + ] + verifylist = [ + ('locked', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.73 or greater is required', str(ex)) + + def test_server_list_with_locked_v273(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + arglist = [ + '--locked' + ] + verifylist = [ + ('locked', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['locked'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_unlocked_v273(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + arglist = [ + '--unlocked' + ] + verifylist = [ + ('unlocked', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['locked'] = False + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_locked_and_unlocked_v273(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + arglist = [ + '--locked', + '--unlocked' + ] + verifylist = [ + ('locked', True), + ('unlocked', True) + ] + + ex = self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + self.assertIn('Argument parse failed', str(ex)) + def test_server_list_with_flavor(self): arglist = [ @@ -2336,6 +2417,72 @@ def test_server_lock_one_server(self): def test_server_lock_multi_servers(self): self.run_method_with_servers('lock', 3) + def test_server_lock_with_reason(self): + server = compute_fakes.FakeServer.create_one_server() + arglist = [ + server.id, + '--reason', "blah", + ] + verifylist = [ + ('reason', "blah"), + ('server', [server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.73 or greater is required', str(ex)) + + +class TestServerLockV273(TestServerLock): + + def setUp(self): + super(TestServerLockV273, self).setUp() + + self.server = compute_fakes.FakeServer.create_one_server( + methods=self.methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + + # Get the command object to test + self.cmd = server.LockServer(self.app, None) + + def test_server_lock_with_reason(self): + arglist = [ + self.server.id, + '--reason', "blah", + ] + verifylist = [ + ('reason', "blah"), + ('server', [self.server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.servers_mock.get.assert_called_with(self.server.id) + self.server.lock.assert_called_with(reason="blah") + + def test_server_lock_multi_servers_with_reason(self): + server2 = compute_fakes.FakeServer.create_one_server( + methods=self.methods) + arglist = [ + self.server.id, server2.id, + '--reason', "choo..choo", + ] + verifylist = [ + ('reason', "choo..choo"), + ('server', [self.server.id, server2.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.assertEqual(2, self.servers_mock.get.call_count) + self.server.lock.assert_called_with(reason="choo..choo") + self.assertEqual(2, self.server.lock.call_count) + class TestServerMigrate(TestServer): diff --git a/releasenotes/notes/bp-add-locked-reason-425efd2def1144f1.yaml b/releasenotes/notes/bp-add-locked-reason-425efd2def1144f1.yaml new file mode 100644 index 0000000000..e9f6cc6d9e --- /dev/null +++ b/releasenotes/notes/bp-add-locked-reason-425efd2def1144f1.yaml @@ -0,0 +1,13 @@ +--- +features: + - Add ``--reason`` option to the ``server lock`` command to specify a reason + when locking a server. + Requires ``–os-compute-api-version`` 2.73 or greater. + - Add ``--locked`` option to the ``server list`` command to list only locked + servers. + Requires ``–os-compute-api-version`` 2.73 or greater. + - Add ``--unlocked`` option to the ``server list`` command list only unlocked + servers. + Requires ``–os-compute-api-version`` 2.73 or greater. + [Blueprint `add-locked-reason `_] + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index aa5debf4ed..3db6caef0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.17.0 # Apache-2.0 -python-novaclient>=10.0.0 # Apache-2.0 +python-novaclient>=14.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From fa5046a3dbf8cf398b9a05c68e6ee4badbb14ee7 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Mon, 15 May 2017 04:00:32 +0000 Subject: [PATCH 0097/1120] Use cliff formattable columns in identity commands Partial-Bug: #1687955 Partially implement blueprint osc-formattable-columns Change-Id: Ia13314a012b3a7363ffb24a13c79c6ecdff1ed7b --- openstackclient/identity/v2_0/catalog.py | 34 ++++++------ openstackclient/identity/v2_0/project.py | 3 +- openstackclient/identity/v2_0/user.py | 41 ++++++++++---- openstackclient/identity/v3/catalog.py | 24 ++++---- .../identity/v3/identity_provider.py | 5 +- .../tests/unit/identity/v2_0/test_catalog.py | 55 ++++++++++++------- .../tests/unit/identity/v2_0/test_project.py | 5 +- .../tests/unit/identity/v2_0/test_user.py | 12 ++-- .../tests/unit/identity/v3/fakes.py | 3 +- .../tests/unit/identity/v3/test_catalog.py | 25 +++++---- .../identity/v3/test_identity_provider.py | 20 +++---- 11 files changed, 140 insertions(+), 87 deletions(-) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 993bdd5343..5d1e30626f 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -15,6 +15,7 @@ import logging +from cliff import columns as cliff_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,20 +27,21 @@ LOG = logging.getLogger(__name__) -def _format_endpoints(eps=None): - if not eps: - return "" - ret = '' - for index, ep in enumerate(eps): - region = eps[index].get('region') - if region is None: - region = '' - ret += region + '\n' - for endpoint_type in ['publicURL', 'internalURL', 'adminURL']: - url = eps[index].get(endpoint_type) - if url: - ret += " %s: %s\n" % (endpoint_type, url) - return ret +class EndpointsColumn(cliff_columns.FormattableColumn): + def human_readable(self): + if not self._value: + return "" + ret = '' + for ep in self._value: + region = ep.get('region') + if region is None: + region = '' + ret += region + '\n' + for endpoint_type in ['publicURL', 'internalURL', 'adminURL']: + url = ep.get(endpoint_type) + if url: + ret += " %s: %s\n" % (endpoint_type, url) + return ret class ListCatalog(command.Lister): @@ -60,7 +62,7 @@ def take_action(self, parsed_args): (utils.get_dict_properties( s, columns, formatters={ - 'Endpoints': _format_endpoints, + 'Endpoints': EndpointsColumn, }, ) for s in data)) @@ -91,7 +93,7 @@ def take_action(self, parsed_args): if (service.get('name') == parsed_args.service or service.get('type') == parsed_args.service): data = service - data['endpoints'] = _format_endpoints(data['endpoints']) + data['endpoints'] = EndpointsColumn(data['endpoints']) if 'endpoints_links' in data: data.pop('endpoints_links') break diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 04d422ecdb..df57574b0b 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -18,6 +18,7 @@ import logging from keystoneauth1 import exceptions as ks_exc +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -297,7 +298,7 @@ def take_action(self, parsed_args): if v is not None: properties[k] = v - info['properties'] = utils.format_dict(properties) + info['properties'] = format_columns.DictColumn(properties) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 2a3dde6b52..0675877b9b 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -15,8 +15,10 @@ """Identity v2.0 User action implementations""" +import functools import logging +from cliff import columns as cliff_columns from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command from osc_lib import exceptions @@ -29,6 +31,31 @@ LOG = logging.getLogger(__name__) +class ProjectColumn(cliff_columns.FormattableColumn): + """Formattable column for project column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes project_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(ProjectColumn, project_cache)``. + """ + + def __init__(self, value, project_cache=None): + super(ProjectColumn, self).__init__(value) + self.project_cache = project_cache or {} + + def human_readable(self): + project = self._value + if not project: + return "" + if project in self.project_cache.keys(): + return self.project_cache[project].name + else: + return project + + class CreateUser(command.ShowOne): _description = _("Create new user") @@ -187,15 +214,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - - def _format_project(project): - if not project: - return "" - if project in project_cache.keys(): - return project_cache[project].name - else: - return project - + formatters = {} project = None if parsed_args.project: project = utils.find_resource( @@ -227,6 +246,8 @@ def _format_project(project): except Exception: # Just forget it if there's any trouble pass + formatters['tenantId'] = functools.partial( + ProjectColumn, project_cache=project_cache) else: columns = column_headers = ('ID', 'Name') data = identity_client.users.list(tenant_id=project) @@ -251,7 +272,7 @@ def _format_project(project): (utils.get_item_properties( s, columns, mixed_case_fields=('tenantId',), - formatters={'tenantId': _format_project}, + formatters=formatters, ) for s in data)) diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index 28f4fadad5..59430c4ce0 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -15,6 +15,7 @@ import logging +from cliff import columns as cliff_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,15 +27,16 @@ LOG = logging.getLogger(__name__) -def _format_endpoints(eps=None): - if not eps: - return "" - ret = '' - for ep in eps: - region = ep.get('region_id') or ep.get('region') or '' - ret += region + '\n' - ret += " %s: %s\n" % (ep['interface'], ep['url']) - return ret +class EndpointsColumn(cliff_columns.FormattableColumn): + def human_readable(self): + if not self._value: + return "" + ret = '' + for ep in self._value: + region = ep.get('region_id') or ep.get('region') or '' + ret += region + '\n' + ret += " %s: %s\n" % (ep['interface'], ep['url']) + return ret class ListCatalog(command.Lister): @@ -55,7 +57,7 @@ def take_action(self, parsed_args): (utils.get_dict_properties( s, columns, formatters={ - 'Endpoints': _format_endpoints, + 'Endpoints': EndpointsColumn, }, ) for s in data)) @@ -86,7 +88,7 @@ def take_action(self, parsed_args): if (service.get('name') == parsed_args.service or service.get('type') == parsed_args.service): data = dict(service) - data['endpoints'] = _format_endpoints(data['endpoints']) + data['endpoints'] = EndpointsColumn(data['endpoints']) if 'links' in data: data.pop('links') break diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index d8951d31c2..b331518213 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -103,7 +104,7 @@ def take_action(self, parsed_args): enabled=parsed_args.enabled) idp._info.pop('links', None) - remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) + remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) idp._info['remote_ids'] = remote_ids return zip(*sorted(six.iteritems(idp._info))) @@ -245,6 +246,6 @@ def take_action(self, parsed_args): id=parsed_args.identity_provider) idp._info.pop('links', None) - remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) + remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) idp._info['remote_ids'] = remote_ids return zip(*sorted(six.iteritems(idp._info))) diff --git a/openstackclient/tests/unit/identity/v2_0/test_catalog.py b/openstackclient/tests/unit/identity/v2_0/test_catalog.py index 65af58a24e..362dec0885 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_catalog.py +++ b/openstackclient/tests/unit/identity/v2_0/test_catalog.py @@ -71,17 +71,9 @@ def test_catalog_list(self): datalist = (( 'supernova', 'compute', - 'one\n publicURL: https://public.one.example.com\n ' - 'internalURL: https://internal.one.example.com\n ' - 'adminURL: https://admin.one.example.com\n' - 'two\n publicURL: https://public.two.example.com\n ' - 'internalURL: https://internal.two.example.com\n ' - 'adminURL: https://admin.two.example.com\n' - '\n publicURL: https://public.none.example.com\n ' - 'internalURL: https://internal.none.example.com\n ' - 'adminURL: https://admin.none.example.com\n', + catalog.EndpointsColumn(self.service_catalog['endpoints']), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_catalog_list_with_endpoint_url(self): attr = { @@ -121,11 +113,9 @@ def test_catalog_list_with_endpoint_url(self): datalist = (( 'supernova', 'compute', - 'one\n publicURL: https://public.one.example.com\n' - 'two\n publicURL: https://public.two.example.com\n ' - 'internalURL: https://internal.two.example.com\n' + catalog.EndpointsColumn(service_catalog['endpoints']), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) class TestCatalogShow(TestCatalog): @@ -160,6 +150,18 @@ def test_catalog_show(self): collist = ('endpoints', 'id', 'name', 'type') self.assertEqual(collist, columns) datalist = ( + catalog.EndpointsColumn(self.service_catalog['endpoints']), + self.service_catalog.id, + 'supernova', + 'compute', + ) + self.assertItemEqual(datalist, data) + + +class TestFormatColumns(TestCatalog): + def test_endpoints_column_human_readabale(self): + col = catalog.EndpointsColumn(self.service_catalog['endpoints']) + self.assertEqual( 'one\n publicURL: https://public.one.example.com\n ' 'internalURL: https://internal.one.example.com\n ' 'adminURL: https://admin.one.example.com\n' @@ -169,8 +171,23 @@ def test_catalog_show(self): '\n publicURL: https://public.none.example.com\n ' 'internalURL: https://internal.none.example.com\n ' 'adminURL: https://admin.none.example.com\n', - self.service_catalog.id, - 'supernova', - 'compute', - ) - self.assertEqual(datalist, data) + col.human_readable()) + + def test_endpoints_column_human_readable_with_partial_endpoint_urls(self): + endpoints = [ + { + 'region': 'one', + 'publicURL': 'https://public.one.example.com', + }, + { + 'region': 'two', + 'publicURL': 'https://public.two.example.com', + 'internalURL': 'https://internal.two.example.com', + }, + ] + col = catalog.EndpointsColumn(endpoints) + self.assertEqual( + 'one\n publicURL: https://public.one.example.com\n' + 'two\n publicURL: https://public.two.example.com\n ' + 'internalURL: https://internal.two.example.com\n', + col.human_readable()) diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index c726f2a671..7af7b3946e 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -16,6 +16,7 @@ import mock from keystoneauth1 import exceptions as ks_exc +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -640,9 +641,9 @@ def test_project_show(self): True, self.fake_proj_show.id, self.fake_proj_show.name, - '', + format_columns.DictColumn({}), ) - self.assertEqual(datalist, data) + self.assertItemEqual(datalist, data) class TestProjectUnset(TestProject): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index a8b9497ecf..0a0d4b3651 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -482,7 +482,7 @@ def test_user_list_no_options(self): self.users_mock.list.assert_called_with(tenant_id=None) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_user_list_project(self): arglist = [ @@ -502,7 +502,7 @@ def test_user_list_project(self): self.users_mock.list.assert_called_with(tenant_id=project_id) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_user_list_long(self): arglist = [ @@ -525,11 +525,13 @@ def test_user_list_long(self): datalist = (( self.fake_user_l.id, self.fake_user_l.name, - self.fake_project_l.name, + user.ProjectColumn( + self.fake_project_l.id, + {self.fake_project_l.id: self.fake_project_l}), self.fake_user_l.email, True, ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) class TestUserSet(TestUser): @@ -817,4 +819,4 @@ def test_user_show(self): self.fake_user.name, self.fake_project.id, ) - self.assertEqual(datalist, data) + self.assertItemEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 27ee9fd026..5caf156b2c 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -20,6 +20,7 @@ from keystoneauth1 import access from keystoneauth1 import fixture import mock +from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils @@ -300,7 +301,7 @@ idp_id = 'test_idp' idp_description = 'super exciting IdP description' idp_remote_ids = ['entity1', 'entity2'] -formatted_idp_remote_ids = 'entity1, entity2' +formatted_idp_remote_ids = format_columns.ListColumn(idp_remote_ids) IDENTITY_PROVIDER = { 'id': idp_id, diff --git a/openstackclient/tests/unit/identity/v3/test_catalog.py b/openstackclient/tests/unit/identity/v3/test_catalog.py index 53008e8cbc..ba076dbddf 100644 --- a/openstackclient/tests/unit/identity/v3/test_catalog.py +++ b/openstackclient/tests/unit/identity/v3/test_catalog.py @@ -91,12 +91,9 @@ def test_catalog_list(self): datalist = (( 'supernova', 'compute', - 'onlyone\n public: https://public.example.com\n' - 'onlyone\n admin: https://admin.example.com\n' - '\n internal: https://internal.example.com\n' - '\n none: https://none.example.com\n', + catalog.EndpointsColumn(self.fake_service['endpoints']), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) class TestCatalogShow(TestCatalog): @@ -131,12 +128,20 @@ def test_catalog_show(self): collist = ('endpoints', 'id', 'name', 'type') self.assertEqual(collist, columns) datalist = ( - 'onlyone\n public: https://public.example.com\nonlyone\n' - ' admin: https://admin.example.com\n' - '\n internal: https://internal.example.com\n' - '\n none: https://none.example.com\n', + catalog.EndpointsColumn(self.fake_service['endpoints']), 'qwertyuiop', 'supernova', 'compute', ) - self.assertEqual(datalist, data) + self.assertItemEqual(datalist, data) + + +class TestFormatColumns(TestCatalog): + def test_endpoints_column_human_readabale(self): + col = catalog.EndpointsColumn(self.fake_service['endpoints']) + self.assertEqual( + 'onlyone\n public: https://public.example.com\n' + 'onlyone\n admin: https://admin.example.com\n' + '\n internal: https://internal.example.com\n' + '\n none: https://none.example.com\n', + col.human_readable()) diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index dc82ab7422..0163c6c8d4 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -90,7 +90,7 @@ def test_create_identity_provider_no_options(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_create_identity_provider_description(self): arglist = [ @@ -118,7 +118,7 @@ def test_create_identity_provider_description(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_create_identity_provider_remote_id(self): arglist = [ @@ -146,7 +146,7 @@ def test_create_identity_provider_remote_id(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_create_identity_provider_remote_ids_multiple(self): arglist = [ @@ -175,7 +175,7 @@ def test_create_identity_provider_remote_ids_multiple(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_create_identity_provider_remote_ids_file(self): arglist = [ @@ -208,7 +208,7 @@ def test_create_identity_provider_remote_ids_file(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_create_identity_provider_disabled(self): @@ -251,7 +251,7 @@ def test_create_identity_provider_disabled(self): identity_fakes.idp_id, identity_fakes.formatted_idp_remote_ids ) - self.assertEqual(datalist, data) + self.assertItemEqual(datalist, data) def test_create_identity_provider_domain_name(self): arglist = [ @@ -279,7 +279,7 @@ def test_create_identity_provider_domain_name(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_create_identity_provider_domain_id(self): arglist = [ @@ -307,7 +307,7 @@ def test_create_identity_provider_domain_id(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) class TestIdentityProviderDelete(TestIdentityProvider): @@ -383,7 +383,7 @@ def test_identity_provider_list_no_options(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) class TestIdentityProviderSet(TestIdentityProvider): @@ -668,4 +668,4 @@ def test_identity_provider_show(self): identity_fakes.idp_id, identity_fakes.formatted_idp_remote_ids ) - self.assertEqual(datalist, data) + self.assertItemEqual(datalist, data) From 8d63e3f0c38bbcc44db2e82a7e3d0ff5488d45af Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Mon, 15 May 2017 04:00:53 +0000 Subject: [PATCH 0098/1120] Use cliff formattable columns in image commands Related functional tests are converted into JSON format. Otherwise, it is not easy to check results. Partial-Bug: #1687955 Partially implement blueprint osc-formattable-columns Change-Id: Ib82e15738544975fede0c54cc5eaf239f4c67277 --- openstackclient/image/v1/image.py | 33 ++++++++++--------- openstackclient/image/v2/image.py | 7 ++-- .../tests/functional/image/v1/test_image.py | 2 +- .../tests/functional/image/v2/test_image.py | 21 +++++------- .../unit/compute/v2/test_server_backup.py | 9 ++--- .../unit/compute/v2/test_server_image.py | 9 ++--- .../tests/unit/image/v1/test_image.py | 22 +++++++------ openstackclient/tests/unit/image/v2/fakes.py | 6 ++-- .../tests/unit/image/v2/test_image.py | 26 +++++++-------- 9 files changed, 69 insertions(+), 66 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index caf3d54f47..c2dab3eec3 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -21,8 +21,10 @@ import os import sys +from cliff import columns as cliff_columns from glanceclient.common import utils as gc_utils from osc_lib.api import utils as api_utils +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils @@ -46,19 +48,18 @@ LOG = logging.getLogger(__name__) -def _format_visibility(data): - """Return a formatted visibility string +class VisibilityColumn(cliff_columns.FormattableColumn): + def human_readable(self): + """Return a formatted visibility string - :param data: - The server's visibility (is_public) status value: True, False - :rtype: - A string formatted to public/private - """ + :rtype: + A string formatted to public/private + """ - if data: - return 'public' - else: - return 'private' + if self._value: + return 'public' + else: + return 'private' class CreateImage(command.ShowOne): @@ -268,7 +269,8 @@ def take_action(self, parsed_args): kwargs['data'].close() info.update(image._info) - info['properties'] = utils.format_dict(info.get('properties', {})) + info['properties'] = format_columns.DictColumn( + info.get('properties', {})) return zip(*sorted(six.iteritems(info))) @@ -429,8 +431,8 @@ def take_action(self, parsed_args): s, columns, formatters={ - 'is_public': _format_visibility, - 'properties': utils.format_dict, + 'is_public': VisibilityColumn, + 'properties': format_columns.DictColumn, }, ) for s in data) ) @@ -714,5 +716,6 @@ def take_action(self, parsed_args): if parsed_args.human_readable: if 'size' in info: info['size'] = utils.format_size(info['size']) - info['properties'] = utils.format_dict(info.get('properties', {})) + info['properties'] = format_columns.DictColumn( + info.get('properties', {})) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 97169a9237..2b10c3adda 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -22,6 +22,7 @@ from glanceclient.common import utils as gc_utils from openstack.image import image_signer from osc_lib.api import utils as api_utils +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -65,11 +66,11 @@ def _format_image(image): properties[key] = image.get(key) # format the tags if they are there - info['tags'] = utils.format_list(image.get('tags')) + info['tags'] = format_columns.ListColumn(image.get('tags')) # add properties back into the dictionary as a top-level key if properties: - info['properties'] = utils.format_dict(properties) + info['properties'] = format_columns.DictColumn(properties) return info @@ -656,7 +657,7 @@ def take_action(self, parsed_args): s, columns, formatters={ - 'tags': utils.format_list, + 'tags': format_columns.ListColumn, }, ) for s in data) ) diff --git a/openstackclient/tests/functional/image/v1/test_image.py b/openstackclient/tests/functional/image/v1/test_image.py index 30490bf516..b9774ab53d 100644 --- a/openstackclient/tests/functional/image/v1/test_image.py +++ b/openstackclient/tests/functional/image/v1/test_image.py @@ -104,6 +104,6 @@ def test_image_attributes(self): self.name )) self.assertEqual( - "a='b', c='d'", + {'a': 'b', 'c': 'd'}, json_output["properties"], ) diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py index 3185c3bddf..264ba5199c 100644 --- a/openstackclient/tests/functional/image/v2/test_image.py +++ b/openstackclient/tests/functional/image/v2/test_image.py @@ -14,7 +14,6 @@ import uuid import fixtures -# from glanceclient import exc as image_exceptions from openstackclient.tests.functional.image import base @@ -81,7 +80,7 @@ def test_image_list_with_tag_filter(self): json_output = json.loads(self.openstack( 'image list --tag ' + self.image_tag + ' --long -f json' )) - for taglist in [img['Tags'].split(', ') for img in json_output]: + for taglist in [img['Tags'] for img in json_output]: self.assertIn( self.image_tag, taglist @@ -127,10 +126,8 @@ def test_image_attributes(self): 'image show -f json ' + self.name )) - # NOTE(dtroyer): Don't do a full-string compare so we are tolerant of - # new artributes in the returned data - self.assertIn("a='b'", json_output["properties"]) - self.assertIn("c='d'", json_output["properties"]) + self.assertIn("a", json_output["properties"]) + self.assertIn("c", json_output["properties"]) self.openstack( 'image unset ' + @@ -142,15 +139,13 @@ def test_image_attributes(self): 'image show -f json ' + self.name )) - # NOTE(dtroyer): Don't do a full-string compare so we are tolerant of - # new artributes in the returned data - self.assertNotIn("a='b'", json_output["properties"]) - self.assertNotIn("c='d'", json_output["properties"]) + self.assertNotIn("a", json_output["properties"]) + self.assertNotIn("c", json_output["properties"]) # Test tags self.assertNotIn( '01', - json_output["tags"].split(', ') + json_output["tags"] ) self.openstack( 'image set ' + @@ -163,7 +158,7 @@ def test_image_attributes(self): )) self.assertIn( '01', - json_output["tags"].split(', ') + json_output["tags"] ) self.openstack( @@ -177,7 +172,7 @@ def test_image_attributes(self): )) self.assertNotIn( '01', - json_output["tags"].split(', ') + json_output["tags"] ) def test_image_set_rename(self): diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 9a481e0a67..24a94531a4 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -13,6 +13,7 @@ import mock +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils as common_utils @@ -69,7 +70,7 @@ def image_data(self, image): image['owner'], image['protected'], 'active', - common_utils.format_list(image.get('tags')), + format_columns.ListColumn(image.get('tags')), image['visibility'], ) return datalist @@ -134,7 +135,7 @@ def test_server_backup_defaults(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertEqual(self.image_data(images[0]), data) + self.assertItemEqual(self.image_data(images[0]), data) def test_server_backup_create_options(self): servers = self.setup_servers_mock(count=1) @@ -168,7 +169,7 @@ def test_server_backup_create_options(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertEqual(self.image_data(images[0]), data) + self.assertItemEqual(self.image_data(images[0]), data) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_backup_wait_fail(self, mock_wait_for_status): @@ -268,4 +269,4 @@ def test_server_backup_wait_ok(self, mock_wait_for_status): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertEqual(self.image_data(images[0]), data) + self.assertItemEqual(self.image_data(images[0]), data) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 47bd682f17..02e43129ed 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -12,6 +12,7 @@ # import mock +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils as common_utils @@ -67,7 +68,7 @@ def image_data(self, image): image['owner'], image['protected'], 'active', - common_utils.format_list(image.get('tags')), + format_columns.ListColumn(image.get('tags')), image['visibility'], ) return datalist @@ -129,7 +130,7 @@ def test_server_image_create_defaults(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertEqual(self.image_data(images[0]), data) + self.assertItemEqual(self.image_data(images[0]), data) def test_server_image_create_options(self): servers = self.setup_servers_mock(count=1) @@ -157,7 +158,7 @@ def test_server_image_create_options(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertEqual(self.image_data(images[0]), data) + self.assertItemEqual(self.image_data(images[0]), data) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_create_image_wait_fail(self, mock_wait_for_status): @@ -225,4 +226,4 @@ def test_server_create_image_wait_ok(self, mock_wait_for_status): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertEqual(self.image_data(images[0]), data) + self.assertItemEqual(self.image_data(images[0]), data) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 6babd4ffca..0997d76546 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -16,8 +16,9 @@ import copy import mock + +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.image.v1 import image from openstackclient.tests.unit import fakes @@ -58,7 +59,7 @@ class TestImageCreate(TestImage): new_image.min_ram, new_image.name, new_image.owner, - utils.format_dict(new_image.properties), + format_columns.DictColumn(new_image.properties), new_image.protected, ) @@ -106,7 +107,7 @@ def test_image_reserve_no_options(self): self.assertEqual(self.images_mock.update.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_image_reserve_options(self): mock_exception = { @@ -160,7 +161,7 @@ def test_image_reserve_options(self): self.assertEqual(self.images_mock.update.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) @mock.patch('openstackclient.image.v1.image.io.open', name='Open') def test_image_create_file(self, mock_open): @@ -224,7 +225,7 @@ def test_image_create_file(self, mock_open): self.assertEqual(self.images_mock.update.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestImageDelete(TestImage): @@ -410,12 +411,13 @@ def test_image_list_long_option(self): '', '', '', - 'public', + image.VisibilityColumn(True), False, self._image.owner, - "Alpha='a', Beta='b', Gamma='g'", + format_columns.DictColumn( + {'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): @@ -742,7 +744,7 @@ class TestImageShow(TestImage): _image.min_ram, _image.name, _image.owner, - utils.format_dict(_image.properties), + format_columns.DictColumn(_image.properties), _image.protected, _image.size, ) @@ -773,7 +775,7 @@ def test_image_show(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_image_show_human_readable(self): arglist = [ diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index d82d81146f..f69a2bc311 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -19,7 +19,7 @@ from glanceclient.v2 import schemas import mock -from osc_lib import utils as common_utils +from osc_lib.cli import format_columns import warlock from openstackclient.tests.unit import fakes @@ -48,7 +48,7 @@ IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) IMAGE_SHOW = copy.copy(IMAGE) -IMAGE_SHOW['tags'] = '' +IMAGE_SHOW['tags'] = format_columns.ListColumn(IMAGE_SHOW['tags']) IMAGE_SHOW_data = tuple((IMAGE_SHOW[x] for x in sorted(IMAGE_SHOW))) # Just enough v2 schema to do some testing @@ -281,7 +281,7 @@ def get_image_data(image=None): if x == 'tags': # The 'tags' should be format_list data_list.append( - common_utils.format_list(getattr(image, x))) + format_columns.ListColumn(getattr(image, x))) else: data_list.append(getattr(image, x)) return tuple(data_list) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 45b5a0a8cb..748a61aaf4 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -18,8 +18,8 @@ from glanceclient.common import utils as glanceclient_utils from glanceclient.v2 import schemas import mock +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils as common_utils import warlock from openstackclient.image.v2 import image @@ -116,7 +116,7 @@ def test_image_reserve_no_options(self): self.assertEqual( image_fakes.FakeImage.get_image_columns(self.new_image), columns) - self.assertEqual( + self.assertItemEqual( image_fakes.FakeImage.get_image_data(self.new_image), data) @@ -184,7 +184,7 @@ def test_image_reserve_options(self, mock_open): self.assertEqual( image_fakes.FakeImage.get_image_columns(self.new_image), columns) - self.assertEqual( + self.assertItemEqual( image_fakes.FakeImage.get_image_data(self.new_image), data) @@ -284,7 +284,7 @@ def test_image_create_file(self, mock_open): self.assertEqual( image_fakes.FakeImage.get_image_columns(self.new_image), columns) - self.assertEqual( + self.assertItemEqual( image_fakes.FakeImage.get_image_data(self.new_image), data) @@ -508,7 +508,7 @@ def test_image_list_no_options(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_image_list_public_option(self): arglist = [ @@ -533,7 +533,7 @@ def test_image_list_public_option(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_image_list_private_option(self): arglist = [ @@ -558,7 +558,7 @@ def test_image_list_private_option(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_image_list_community_option(self): arglist = [ @@ -608,7 +608,7 @@ def test_image_list_shared_option(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_image_list_shared_member_status_option(self): arglist = [ @@ -696,9 +696,9 @@ def test_image_list_long_option(self): self._image.visibility, self._image.protected, self._image.owner, - common_utils.format_list(self._image.tags), + format_columns.ListColumn(self._image.tags), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): @@ -727,7 +727,7 @@ def test_image_list_property_option(self, sf_mock): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) @mock.patch('osc_lib.utils.sort_items') def test_image_list_sort_option(self, si_mock): @@ -750,7 +750,7 @@ def test_image_list_sort_option(self, si_mock): str, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_image_list_limit_option(self): ret_limit = 1 @@ -1460,7 +1460,7 @@ def test_image_show(self): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_SHOW_data, data) + self.assertItemEqual(image_fakes.IMAGE_SHOW_data, data) def test_image_show_human_readable(self): self.images_mock.get.return_value = self.new_image From c2630ae91a0e4bae662926a0e45d71c79683b584 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Mon, 15 May 2017 04:01:07 +0000 Subject: [PATCH 0099/1120] Use cliff formattable columns in object storage commands Partial-Bug: #1687955 Partially implement blueprint osc-formattable-columns Change-Id: I65737561c9b5ef29f5878316d2ff89f3d538158f --- openstackclient/object/v1/account.py | 5 +++-- openstackclient/object/v1/container.py | 3 ++- openstackclient/object/v1/object.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py index 4847f8bbc2..95be8132c9 100644 --- a/openstackclient/object/v1/account.py +++ b/openstackclient/object/v1/account.py @@ -13,9 +13,9 @@ """Account v1 action implementations""" +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command -from osc_lib import utils import six from openstackclient.i18n import _ @@ -48,7 +48,8 @@ class ShowAccount(command.ShowOne): def take_action(self, parsed_args): data = self.app.client_manager.object_store.account_show() if 'properties' in data: - data['properties'] = utils.format_dict(data.pop('properties')) + data['properties'] = format_columns.DictColumn( + data.pop('properties')) return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 9f689ab6b0..02e8d27780 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -17,6 +17,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils @@ -230,7 +231,7 @@ def take_action(self, parsed_args): container=parsed_args.container, ) if 'properties' in data: - data['properties'] = utils.format_dict(data.pop('properties')) + data['properties'] = format_columns.DictColumn(data['properties']) return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index aeb0253653..3747e19e47 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -17,6 +17,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -284,7 +285,7 @@ def take_action(self, parsed_args): object=parsed_args.object, ) if 'properties' in data: - data['properties'] = utils.format_dict(data.pop('properties')) + data['properties'] = format_columns.DictColumn(data['properties']) return zip(*sorted(six.iteritems(data))) From 1af3056e301807e4474cde49eef3e00931ab08c2 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Mon, 15 May 2017 07:55:41 +0000 Subject: [PATCH 0100/1120] Use cliff formattable columns in volume v1 commands Partial-Bug: #1687955 Partially implement blueprint osc-formattable-columns Change-Id: Ib4c5798171e32a8ddc08a37ee1d416e366a71d76 --- .../tests/functional/volume/v1/test_qos.py | 4 +- .../functional/volume/v1/test_snapshot.py | 9 +-- .../tests/functional/volume/v1/test_volume.py | 6 +- .../functional/volume/v1/test_volume_type.py | 61 +++++++------- .../tests/unit/volume/v1/test_qos_specs.py | 28 ++++--- .../tests/unit/volume/v1/test_type.py | 59 ++++++++++---- .../tests/unit/volume/v1/test_volume.py | 79 +++++++++++++------ .../unit/volume/v1/test_volume_backup.py | 12 +-- openstackclient/volume/v1/qos_specs.py | 13 +-- openstackclient/volume/v1/volume.py | 62 ++++++++++----- openstackclient/volume/v1/volume_backup.py | 45 ++++++++--- openstackclient/volume/v1/volume_snapshot.py | 54 +++++++++---- openstackclient/volume/v1/volume_type.py | 60 +++++++++++--- 13 files changed, 323 insertions(+), 169 deletions(-) diff --git a/openstackclient/tests/functional/volume/v1/test_qos.py b/openstackclient/tests/functional/volume/v1/test_qos.py index 434840f623..d8277dfc81 100644 --- a/openstackclient/tests/functional/volume/v1/test_qos.py +++ b/openstackclient/tests/functional/volume/v1/test_qos.py @@ -93,7 +93,7 @@ def test_volume_qos_set_show_unset(self): cmd_output['name'] ) self.assertEqual( - "Alpha='c', Beta='b'", + {'Alpha': 'c', 'Beta': 'b'}, cmd_output['properties'] ) @@ -114,7 +114,7 @@ def test_volume_qos_set_show_unset(self): cmd_output['name'] ) self.assertEqual( - "Beta='b'", + {'Beta': 'b'}, cmd_output['properties'] ) diff --git a/openstackclient/tests/functional/volume/v1/test_snapshot.py b/openstackclient/tests/functional/volume/v1/test_snapshot.py index 083cd1b087..5a76a2e950 100644 --- a/openstackclient/tests/functional/volume/v1/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v1/test_snapshot.py @@ -204,7 +204,7 @@ def test_snapshot_set(self): cmd_output["display_description"], ) self.assertEqual( - "Alpha='a', Beta='b'", + {'Alpha': 'a', 'Beta': 'b'}, cmd_output["properties"], ) @@ -221,7 +221,7 @@ def test_snapshot_set(self): new_name )) self.assertEqual( - "Beta='b'", + {'Beta': 'b'}, cmd_output["properties"], ) @@ -236,7 +236,4 @@ def test_snapshot_set(self): 'volume snapshot show -f json ' + new_name )) - self.assertNotIn( - "Beta='b'", - cmd_output["properties"], - ) + self.assertEqual({}, cmd_output["properties"]) diff --git a/openstackclient/tests/functional/volume/v1/test_volume.py b/openstackclient/tests/functional/volume/v1/test_volume.py index 001bbe6e90..013bc6a4a8 100644 --- a/openstackclient/tests/functional/volume/v1/test_volume.py +++ b/openstackclient/tests/functional/volume/v1/test_volume.py @@ -123,7 +123,7 @@ def test_volume_set_and_unset(self): cmd_output["display_description"], ) self.assertEqual( - "Alpha='a'", + {'Alpha': 'a'}, cmd_output["properties"], ) self.assertEqual( @@ -165,7 +165,7 @@ def test_volume_set_and_unset(self): cmd_output["display_description"], ) self.assertEqual( - "Beta='b', Gamma='c'", + {'Beta': 'b', 'Gamma': 'c'}, cmd_output["properties"], ) self.assertEqual( @@ -186,7 +186,7 @@ def test_volume_set_and_unset(self): new_name )) self.assertEqual( - "Gamma='c'", + {'Gamma': 'c'}, cmd_output["properties"], ) diff --git a/openstackclient/tests/functional/volume/v1/test_volume_type.py b/openstackclient/tests/functional/volume/v1/test_volume_type.py index eb9d7f64ae..fb8dabdb04 100644 --- a/openstackclient/tests/functional/volume/v1/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v1/test_volume_type.py @@ -66,8 +66,7 @@ def test_volume_type_set_unset_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - # TODO(amotoki): properties output should be machine-readable - self.assertEqual("a='b', c='d'", cmd_output['properties']) + self.assertEqual({'a': 'b', 'c': 'd'}, cmd_output['properties']) raw_output = self.openstack( 'volume type unset --property a %s' % name @@ -76,7 +75,7 @@ def test_volume_type_set_unset_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - self.assertEqual("c='d'", cmd_output['properties']) + self.assertEqual({'c': 'd'}, cmd_output['properties']) def test_volume_type_set_unset_multiple_properties(self): name = uuid.uuid4().hex @@ -97,7 +96,7 @@ def test_volume_type_set_unset_multiple_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - self.assertEqual("a='b', c='d'", cmd_output['properties']) + self.assertEqual({'a': 'b', 'c': 'd'}, cmd_output['properties']) raw_output = self.openstack( 'volume type unset --property a --property c %s' % name @@ -106,7 +105,7 @@ def test_volume_type_set_unset_multiple_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - self.assertEqual("", cmd_output['properties']) + self.assertEqual({}, cmd_output['properties']) def test_multi_delete(self): vol_type1 = uuid.uuid4().hex @@ -133,34 +132,32 @@ def test_encryption_type(self): '--encryption-key-size 128 ' '--encryption-control-location front-end ' + encryption_type)) - # TODO(amotoki): encryption output should be machine-readable - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test show encryption type cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + encryption_type)) - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test list encryption type cmd_output = json.loads(self.openstack( 'volume type list -f json --encryption-type')) encryption_output = [t['Encryption'] for t in cmd_output if t['Name'] == encryption_type][0] - # TODO(amotoki): encryption output should be machine-readable - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, encryption_output) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, encryption_output[attr]) # test set new encryption type raw_output = self.openstack( 'volume type set ' @@ -185,12 +182,12 @@ def test_encryption_type(self): cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + name )) - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test unset encryption type raw_output = self.openstack( 'volume type unset --encryption-type ' + name @@ -199,7 +196,7 @@ def test_encryption_type(self): cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + name )) - self.assertEqual('', cmd_output['encryption']) + self.assertEqual({}, cmd_output['encryption']) # test delete encryption type raw_output = self.openstack('volume type delete ' + encryption_type) self.assertEqual('', raw_output) diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index 442840f922..11dc80844a 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -17,6 +17,8 @@ import mock from mock import call + +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -85,7 +87,7 @@ def setUp(self): self.new_qos_spec.consumer, self.new_qos_spec.id, self.new_qos_spec.name, - utils.format_dict(self.new_qos_spec.specs) + format_columns.DictColumn(self.new_qos_spec.specs) ) self.qos_mock.create.return_value = self.new_qos_spec # Get the command object to test @@ -108,7 +110,7 @@ def test_qos_create_without_properties(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_qos_create_with_consumer(self): arglist = [ @@ -128,7 +130,7 @@ def test_qos_create_with_consumer(self): {'consumer': self.new_qos_spec.consumer} ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_qos_create_with_properties(self): arglist = [ @@ -154,7 +156,7 @@ def test_qos_create_with_properties(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) class TestQosDelete(TestQos): @@ -326,8 +328,8 @@ class TestQosList(TestQos): q.id, q.name, q.consumer, - qos_association.name, - utils.format_dict(q.specs), + format_columns.ListColumn([qos_association.name]), + format_columns.DictColumn(q.specs), )) def setUp(self): @@ -349,7 +351,7 @@ def test_qos_list(self): self.qos_mock.list.assert_called_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_qos_list_no_association(self): self.qos_mock.reset_mock() @@ -373,10 +375,10 @@ def test_qos_list_no_association(self): self.qos_specs[1].id, self.qos_specs[1].name, self.qos_specs[1].consumer, - None, - utils.format_dict(self.qos_specs[1].specs), + format_columns.ListColumn(None), + format_columns.DictColumn(self.qos_specs[1].specs), ) - self.assertEqual(ex_data, list(data)) + self.assertListItemEqual(ex_data, list(data)) class TestQosSet(TestQos): @@ -447,13 +449,13 @@ def test_qos_show(self): ) self.assertEqual(collist, columns) datalist = ( - self.qos_association.name, + format_columns.ListColumn([self.qos_association.name]), self.qos_spec.consumer, self.qos_spec.id, self.qos_spec.name, - utils.format_dict(self.qos_spec.specs), + format_columns.DictColumn(self.qos_spec.specs), ) - self.assertEqual(datalist, tuple(data)) + self.assertItemEqual(datalist, tuple(data)) class TestQosUnset(TestQos): diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index dcdd3d56dd..beff83364c 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -15,6 +15,7 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -77,7 +78,7 @@ def test_type_create(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_type_create_with_encryption(self): encryption_info = { @@ -102,7 +103,7 @@ def test_type_create_with_encryption(self): ) encryption_data = ( self.new_volume_type.description, - utils.format_dict(encryption_info), + format_columns.DictColumn(encryption_info), self.new_volume_type.id, True, self.new_volume_type.name, @@ -138,7 +139,7 @@ def test_type_create_with_encryption(self): body, ) self.assertEqual(encryption_columns, columns) - self.assertEqual(encryption_data, data) + self.assertItemEqual(encryption_data, data) class TestTypeDelete(TestType): @@ -246,7 +247,7 @@ class TestTypeList(TestType): t.id, t.name, t.is_public, - utils.format_dict(t.extra_specs), + format_columns.DictColumn(t.extra_specs), )) def setUp(self): @@ -269,7 +270,7 @@ def test_type_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = [ @@ -283,7 +284,7 @@ def test_type_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_type_list_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type( @@ -302,13 +303,16 @@ def test_type_list_with_encryption(self): self.volume_types[0].id, self.volume_types[0].name, self.volume_types[0].is_public, - utils.format_dict(encryption_info), + volume_type.EncryptionInfoColumn( + self.volume_types[0].id, + {self.volume_types[0].id: encryption_info}), )) encryption_data.append(( self.volume_types[1].id, self.volume_types[1].name, self.volume_types[1].is_public, - '-', + volume_type.EncryptionInfoColumn( + self.volume_types[1].id, {}), )) self.encryption_types_mock.list.return_value = [encryption_type] @@ -324,7 +328,7 @@ def test_type_list_with_encryption(self): self.encryption_types_mock.list.assert_called_once_with() self.types_mock.list.assert_called_once_with() self.assertEqual(encryption_columns, columns) - self.assertEqual(encryption_data, list(data)) + self.assertListItemEqual(encryption_data, list(data)) class TestTypeSet(TestType): @@ -443,7 +447,7 @@ def setUp(self): self.volume_type.id, True, self.volume_type.name, - utils.format_dict(self.volume_type.extra_specs) + format_columns.DictColumn(self.volume_type.extra_specs) ) self.types_mock.get.return_value = self.volume_type @@ -465,7 +469,7 @@ def test_type_show(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_type_show_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type() @@ -489,11 +493,11 @@ def test_type_show_with_encryption(self): ) encryption_data = ( self.volume_type.description, - utils.format_dict(encryption_info), + format_columns.DictColumn(encryption_info), self.volume_type.id, True, self.volume_type.name, - utils.format_dict(self.volume_type.extra_specs) + format_columns.DictColumn(self.volume_type.extra_specs) ) arglist = [ '--encryption-type', @@ -509,7 +513,7 @@ def test_type_show_with_encryption(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.encryption_types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(encryption_columns, columns) - self.assertEqual(encryption_data, data) + self.assertItemEqual(encryption_data, data) class TestTypeUnset(TestType): @@ -587,3 +591,30 @@ def test_type_unset_encryption_type(self): result = self.cmd.take_action(parsed_args) self.encryption_types_mock.delete.assert_called_with(self.volume_type) self.assertIsNone(result) + + +class TestColumns(TestType): + + def test_encryption_info_column_with_info(self): + fake_volume_type = volume_fakes.FakeType.create_one_type() + type_id = fake_volume_type.id + + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + col = volume_type.EncryptionInfoColumn(type_id, + {type_id: encryption_info}) + self.assertEqual(utils.format_dict(encryption_info), + col.human_readable()) + self.assertEqual(encryption_info, col.machine_readable()) + + def test_encryption_info_column_without_info(self): + fake_volume_type = volume_fakes.FakeType.create_one_type() + type_id = fake_volume_type.id + + col = volume_type.EncryptionInfoColumn(type_id, {}) + self.assertEqual('-', col.human_readable()) + self.assertIsNone(col.machine_readable()) diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index eee5acd7ea..c415455534 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -17,6 +17,8 @@ import mock from mock import call + +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -88,7 +90,7 @@ def setUp(self): self.new_volume.display_description, self.new_volume.id, self.new_volume.display_name, - utils.format_dict(self.new_volume.metadata), + format_columns.DictColumn(self.new_volume.metadata), self.new_volume.size, self.new_volume.snapshot_id, self.new_volume.status, @@ -134,7 +136,7 @@ def test_volume_create_min_options(self): None, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_options(self): arglist = [ @@ -178,7 +180,7 @@ def test_volume_create_options(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_user_project_id(self): # Return a project @@ -225,7 +227,7 @@ def test_volume_create_user_project_id(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_user_project_name(self): # Return a project @@ -272,7 +274,7 @@ def test_volume_create_user_project_name(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -313,7 +315,7 @@ def test_volume_create_properties(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() @@ -356,7 +358,7 @@ def test_volume_create_image_id(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() @@ -399,7 +401,7 @@ def test_volume_create_image_name(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_with_source(self): self.volumes_mock.get.return_value = self.new_volume @@ -429,7 +431,7 @@ def test_volume_create_with_source(self): None, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_with_bootable_and_readonly(self): arglist = [ @@ -467,7 +469,7 @@ def test_volume_create_with_bootable_and_readonly(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -509,7 +511,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, False) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -561,7 +563,7 @@ def test_volume_create_with_bootable_and_readonly_fail( self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -732,16 +734,13 @@ class TestVolumeList(TestVolume): 'Size', 'Attached to', ) - server = _volume.attachments[0]['server_id'] - device = _volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = ( ( _volume.id, _volume.display_name, _volume.status, _volume.size, - msg, + volume.AttachmentsColumn(_volume.attachments), ), ) @@ -767,7 +766,7 @@ def test_volume_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_volume_list_name(self): arglist = [ @@ -784,7 +783,7 @@ def test_volume_list_name(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_volume_list_status(self): arglist = [ @@ -801,7 +800,7 @@ def test_volume_list_status(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_volume_list_all_projects(self): arglist = [ @@ -818,7 +817,7 @@ def test_volume_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_volume_list_long(self): arglist = [ @@ -855,10 +854,10 @@ def test_volume_list_long(self): self._volume.size, self._volume.volume_type, self._volume.bootable, - self.msg, - utils.format_dict(self._volume.metadata), + volume.AttachmentsColumn(self._volume.attachments), + format_columns.DictColumn(self._volume.metadata), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_with_limit(self): arglist = [ @@ -883,7 +882,7 @@ def test_volume_list_with_limit(self): 'all_tenants': False, } ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertListItemEqual(self.datalist, tuple(data)) def test_volume_list_negative_limit(self): arglist = [ @@ -1251,7 +1250,7 @@ def setUp(self): self._volume.display_description, self._volume.id, self._volume.display_name, - utils.format_dict(self._volume.metadata), + format_columns.DictColumn(self._volume.metadata), self._volume.size, self._volume.snapshot_id, self._volume.status, @@ -1274,7 +1273,7 @@ def test_volume_show(self): self.volumes_mock.get.assert_called_with(self._volume.id) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_show_backward_compatibility(self): arglist = [ @@ -1339,3 +1338,31 @@ def test_volume_unset_property(self): self._volume.id, ['myprop'] ) self.assertIsNone(result) + + +class TestColumns(TestVolume): + + def test_attachments_column_without_server_cache(self): + _volume = volume_fakes.FakeVolume.create_one_volume() + server_id = _volume.attachments[0]['server_id'] + device = _volume.attachments[0]['device'] + + col = volume.AttachmentsColumn(_volume.attachments, {}) + self.assertEqual('Attached to %s on %s ' % (server_id, device), + col.human_readable()) + self.assertEqual(_volume.attachments, col.machine_readable()) + + def test_attachments_column_with_server_cache(self): + _volume = volume_fakes.FakeVolume.create_one_volume() + + server_id = _volume.attachments[0]['server_id'] + device = _volume.attachments[0]['device'] + fake_server = mock.Mock() + fake_server.name = 'fake-server-name' + server_cache = {server_id: fake_server} + + col = volume.AttachmentsColumn(_volume.attachments, server_cache) + self.assertEqual( + 'Attached to %s on %s ' % ('fake-server-name', device), + col.human_readable()) + self.assertEqual(_volume.attachments, col.machine_readable()) diff --git a/openstackclient/tests/unit/volume/v1/test_volume_backup.py b/openstackclient/tests/unit/volume/v1/test_volume_backup.py index 22c1f8cf58..20be6e46d7 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_volume_backup.py @@ -100,7 +100,7 @@ def test_backup_create(self): self.new_backup.description, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_backup_create_without_name(self): arglist = [ @@ -124,7 +124,7 @@ def test_backup_create_without_name(self): self.new_backup.description, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestBackupDelete(TestBackup): @@ -240,7 +240,7 @@ class TestBackupList(TestBackup): b.status, b.size, b.availability_zone, - b.volume_id, + volume_backup.VolumeIdColumn(b.volume_id), b.container, )) @@ -277,7 +277,7 @@ def test_backup_list_without_options(self): search_opts=search_opts, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = [ @@ -309,7 +309,7 @@ def test_backup_list_with_options(self): search_opts=search_opts, ) self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): @@ -391,4 +391,4 @@ def test_backup_show(self): self.backups_mock.get.assert_called_with(self.backup.id) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) diff --git a/openstackclient/volume/v1/qos_specs.py b/openstackclient/volume/v1/qos_specs.py index 26c9182971..0b6a7fa0ca 100644 --- a/openstackclient/volume/v1/qos_specs.py +++ b/openstackclient/volume/v1/qos_specs.py @@ -17,6 +17,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -95,7 +96,8 @@ def take_action(self, parsed_args): qos_spec = volume_client.qos_specs.create(parsed_args.name, specs) qos_spec._info.update( - {'properties': utils.format_dict(qos_spec._info.pop('specs'))} + {'properties': + format_columns.DictColumn(qos_spec._info.pop('specs'))} ) return zip(*sorted(six.iteritems(qos_spec._info))) @@ -208,8 +210,8 @@ def take_action(self, parsed_args): (utils.get_dict_properties( s._info, columns, formatters={ - 'Specs': utils.format_dict, - 'Associations': utils.format_list + 'Specs': format_columns.DictColumn, + 'Associations': format_columns.ListColumn }, ) for s in qos_specs_list)) @@ -265,10 +267,11 @@ def take_action(self, parsed_args): associations = [association.name for association in qos_associations] qos_spec._info.update({ - 'associations': utils.format_list(associations) + 'associations': format_columns.ListColumn(associations) }) qos_spec._info.update( - {'properties': utils.format_dict(qos_spec._info.pop('specs'))}) + {'properties': + format_columns.DictColumn(qos_spec._info.pop('specs'))}) return zip(*sorted(six.iteritems(qos_spec._info))) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index b29429ef82..36c7ef8985 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -16,8 +16,11 @@ """Volume v1 Volume action implementations""" import argparse +import functools import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -30,6 +33,37 @@ LOG = logging.getLogger(__name__) +class AttachmentsColumn(cliff_columns.FormattableColumn): + """Formattable column for attachments column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes server_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(AttachmentsColumn, server_cache)``. + """ + + def __init__(self, value, server_cache=None): + super(AttachmentsColumn, self).__init__(value) + self._server_cache = server_cache or {} + + def human_readable(self): + """Return a formatted string of a volume's attached instances + + :rtype: a string of formatted instances + """ + + msg = '' + for attachment in self._value: + server = attachment['server_id'] + if server in self._server_cache.keys(): + server = self._server_cache[server].name + device = attachment['device'] + msg += 'Attached to %s on %s ' % (server, device) + return msg + + def _check_size_arg(args): """Check whether --size option is required or not. @@ -207,7 +241,8 @@ def take_action(self, parsed_args): # Map 'metadata' column to 'properties' volume._info.update( { - 'properties': utils.format_dict(volume._info.pop('metadata')), + 'properties': + format_columns.DictColumn(volume._info.pop('metadata')), 'type': volume._info.pop('volume_type'), }, ) @@ -307,22 +342,6 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.volume compute_client = self.app.client_manager.compute - def _format_attach(attachments): - """Return a formatted string of a volume's attached instances - - :param attachments: a volume.attachments field - :rtype: a string of formatted instances - """ - - msg = '' - for attachment in attachments: - server = attachment['server_id'] - if server in server_cache.keys(): - server = server_cache[server].name - device = attachment['device'] - msg += 'Attached to %s on %s ' % (server, device) - return msg - if parsed_args.long: columns = ( 'ID', @@ -368,6 +387,8 @@ def _format_attach(attachments): except Exception: # Just forget it if there's any trouble pass + AttachmentsColumnWithCache = functools.partial( + AttachmentsColumn, server_cache=server_cache) search_opts = { 'all_tenants': parsed_args.all_projects, @@ -385,8 +406,8 @@ def _format_attach(attachments): return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Metadata': utils.format_dict, - 'Attachments': _format_attach}, + formatters={'Metadata': format_columns.DictColumn, + 'Attachments': AttachmentsColumnWithCache}, ) for s in data)) @@ -575,7 +596,8 @@ def take_action(self, parsed_args): # Map 'metadata' column to 'properties' volume._info.update( { - 'properties': utils.format_dict(volume._info.pop('metadata')), + 'properties': + format_columns.DictColumn(volume._info.pop('metadata')), 'type': volume._info.pop('volume_type'), }, ) diff --git a/openstackclient/volume/v1/volume_backup.py b/openstackclient/volume/v1/volume_backup.py index 9af327055b..2daa23cb92 100644 --- a/openstackclient/volume/v1/volume_backup.py +++ b/openstackclient/volume/v1/volume_backup.py @@ -16,8 +16,10 @@ """Volume v1 Backup action implementations""" import copy +import functools import logging +from cliff import columns as cliff_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -29,6 +31,33 @@ LOG = logging.getLogger(__name__) +class VolumeIdColumn(cliff_columns.FormattableColumn): + """Formattable column for volume ID column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes volume_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(VolumeIdColumn, volume_cache)``. + """ + + def __init__(self, value, volume_cache=None): + super(VolumeIdColumn, self).__init__(value) + self._volume_cache = volume_cache or {} + + def human_readable(self): + """Return a volume name if available + + :rtype: either the volume ID or name + """ + volume_id = self._value + volume = volume_id + if volume_id in self._volume_cache.keys(): + volume = self._volume_cache[volume_id].display_name + return volume + + class CreateVolumeBackup(command.ShowOne): _description = _("Create new volume backup") @@ -149,18 +178,6 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): volume_client = self.app.client_manager.volume - def _format_volume_id(volume_id): - """Return a volume name if available - - :param volume_id: a volume ID - :rtype: either the volume ID or name - """ - - volume = volume_id - if volume_id in volume_cache.keys(): - volume = volume_cache[volume_id].display_name - return volume - if parsed_args.long: columns = ['ID', 'Name', 'Description', 'Status', 'Size', 'Availability Zone', 'Volume ID', 'Container'] @@ -178,6 +195,8 @@ def _format_volume_id(volume_id): except Exception: # Just forget it if there's any trouble pass + VolumeIdColumnWithCache = functools.partial(VolumeIdColumn, + volume_cache=volume_cache) filter_volume_id = None if parsed_args.volume: @@ -196,7 +215,7 @@ def _format_volume_id(volume_id): return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Volume ID': _format_volume_id}, + formatters={'Volume ID': VolumeIdColumnWithCache}, ) for s in data)) diff --git a/openstackclient/volume/v1/volume_snapshot.py b/openstackclient/volume/v1/volume_snapshot.py index 3e83da5a7b..50f81771a0 100644 --- a/openstackclient/volume/v1/volume_snapshot.py +++ b/openstackclient/volume/v1/volume_snapshot.py @@ -16,8 +16,11 @@ """Volume v1 Snapshot action implementations""" import copy +import functools import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -30,6 +33,33 @@ LOG = logging.getLogger(__name__) +class VolumeIdColumn(cliff_columns.FormattableColumn): + """Formattable column for volume ID column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes volume_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(VolumeIdColumn, volume_cache)``. + """ + + def __init__(self, value, volume_cache=None): + super(VolumeIdColumn, self).__init__(value) + self._volume_cache = volume_cache or {} + + def human_readable(self): + """Return a volume name if available + + :rtype: either the volume ID or name + """ + volume_id = self._value + volume = volume_id + if volume_id in self._volume_cache.keys(): + volume = self._volume_cache[volume_id].display_name + return volume + + class CreateVolumeSnapshot(command.ShowOne): _description = _("Create new volume snapshot") @@ -76,7 +106,8 @@ def take_action(self, parsed_args): ) snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + {'properties': + format_columns.DictColumn(snapshot._info.pop('metadata'))} ) return zip(*sorted(six.iteritems(snapshot._info))) @@ -160,18 +191,6 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): volume_client = self.app.client_manager.volume - def _format_volume_id(volume_id): - """Return a volume name if available - - :param volume_id: a volume ID - :rtype: either the volume ID or name - """ - - volume = volume_id - if volume_id in volume_cache.keys(): - volume = volume_cache[volume_id].display_name - return volume - if parsed_args.long: columns = ['ID', 'Display Name', 'Display Description', 'Status', 'Size', 'Created At', 'Volume ID', 'Metadata'] @@ -195,6 +214,8 @@ def _format_volume_id(volume_id): except Exception: # Just forget it if there's any trouble pass + VolumeIdColumnWithCache = functools.partial(VolumeIdColumn, + volume_cache=volume_cache) volume_id = None if parsed_args.volume: @@ -213,8 +234,8 @@ def _format_volume_id(volume_id): return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Metadata': utils.format_dict, - 'Volume ID': _format_volume_id}, + formatters={'Metadata': format_columns.DictColumn, + 'Volume ID': VolumeIdColumnWithCache}, ) for s in data)) @@ -317,7 +338,8 @@ def take_action(self, parsed_args): parsed_args.snapshot) snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + {'properties': + format_columns.DictColumn(snapshot._info.pop('metadata'))} ) return zip(*sorted(six.iteritems(snapshot._info))) diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index b4d8eaca20..e744e92fdb 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -15,8 +15,11 @@ """Volume v1 Type action implementations""" +import functools import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -29,6 +32,36 @@ LOG = logging.getLogger(__name__) +class EncryptionInfoColumn(cliff_columns.FormattableColumn): + """Formattable column for encryption info column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes encryption_data as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(EncryptionInfoColumn encryption_data)``. + """ + + def __init__(self, value, encryption_data=None): + super(EncryptionInfoColumn, self).__init__(value) + self._encryption_data = encryption_data or {} + + def _get_encryption_info(self): + type_id = self._value + return self._encryption_data.get(type_id) + + def human_readable(self): + encryption_info = self._get_encryption_info() + if encryption_info: + return utils.format_dict(encryption_info) + else: + return '-' + + def machine_readable(self): + return self._get_encryption_info() + + def _create_encryption_type(volume_client, volume_type, parsed_args): if not parsed_args.encryption_provider: msg = _("'--encryption-provider' should be specified while " @@ -110,7 +143,8 @@ def take_action(self, parsed_args): volume_type._info.pop('extra_specs') if parsed_args.property: result = volume_type.set_keys(parsed_args.property) - volume_type._info.update({'properties': utils.format_dict(result)}) + volume_type._info.update( + {'properties': format_columns.DictColumn(result)}) if (parsed_args.encryption_provider or parsed_args.encryption_cipher or parsed_args.encryption_key_size or @@ -125,7 +159,7 @@ def take_action(self, parsed_args): # add encryption info in result encryption._info.pop("volume_type_id", None) volume_type._info.update( - {'encryption': utils.format_dict(encryption._info)}) + {'encryption': format_columns.DictColumn(encryption._info)}) volume_type._info.pop("os-volume-type-access:is_public", None) return zip(*sorted(six.iteritems(volume_type._info))) @@ -196,12 +230,7 @@ def take_action(self, parsed_args): column_headers = ['ID', 'Name', 'Is Public'] data = volume_client.volume_types.list() - def _format_encryption_info(type_id, encryption_data=None): - encryption_data = encryption - encryption_info = '-' - if type_id in encryption_data.keys(): - encryption_info = encryption_data[type_id] - return encryption_info + formatters = {'Extra Specs': format_columns.DictColumn} if parsed_args.encryption_type: encryption = {} @@ -218,18 +247,21 @@ def _format_encryption_info(type_id, encryption_data=None): for key in del_key: d._info.pop(key, None) # save the encryption information with their volume type ID - encryption[volume_type_id] = utils.format_dict(d._info) + encryption[volume_type_id] = d._info # We need to get volume type ID, then show encryption # information according to the ID, so use "id" to keep # difference to the real "ID" column. columns += ['id'] column_headers += ['Encryption'] + _EncryptionInfoColumn = functools.partial( + EncryptionInfoColumn, encryption_data=encryption) + formatters['id'] = _EncryptionInfoColumn + return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Extra Specs': utils.format_dict, - 'id': _format_encryption_info}, + formatters=formatters, ) for s in data)) @@ -340,7 +372,8 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) - properties = utils.format_dict(volume_type._info.pop('extra_specs')) + properties = format_columns.DictColumn( + volume_type._info.pop('extra_specs')) volume_type._info.update({'properties': properties}) if parsed_args.encryption_type: # show encryption type information for this volume type @@ -349,7 +382,8 @@ def take_action(self, parsed_args): volume_type.id) encryption._info.pop("volume_type_id", None) volume_type._info.update( - {'encryption': utils.format_dict(encryption._info)}) + {'encryption': + format_columns.DictColumn(encryption._info)}) except Exception as e: LOG.error(_("Failed to display the encryption information " "of this volume type: %s"), e) From 4cd614305fbe8b90d45ad6803bfa8b7978f19318 Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Mon, 15 May 2017 10:19:07 +0000 Subject: [PATCH 0101/1120] Use cliff formattable columns in volume v2 commands Partial-Bug: #1687955 Partially implement blueprint osc-formattable-columns Change-Id: I761ccac126208927594ad0d98a3cf5ad8b44bd48 --- .../tests/functional/volume/v2/test_qos.py | 6 +- .../tests/functional/volume/v2/test_volume.py | 6 +- .../volume/v2/test_volume_snapshot.py | 8 +- .../functional/volume/v2/test_volume_type.py | 74 +++++----- openstackclient/tests/unit/volume/v2/fakes.py | 5 +- .../unit/volume/v2/test_consistency_group.py | 17 +-- .../tests/unit/volume/v2/test_qos_specs.py | 28 ++-- .../tests/unit/volume/v2/test_type.py | 75 ++++++++--- .../tests/unit/volume/v2/test_volume.py | 126 +++++++++--------- .../unit/volume/v2/test_volume_backup.py | 6 +- .../volume/v2/consistency_group.py | 3 +- openstackclient/volume/v2/qos_specs.py | 13 +- openstackclient/volume/v2/volume.py | 62 ++++++--- openstackclient/volume/v2/volume_backup.py | 45 +++++-- openstackclient/volume/v2/volume_snapshot.py | 54 +++++--- openstackclient/volume/v2/volume_type.py | 61 +++++++-- 16 files changed, 360 insertions(+), 229 deletions(-) diff --git a/openstackclient/tests/functional/volume/v2/test_qos.py b/openstackclient/tests/functional/volume/v2/test_qos.py index 646becc1a7..f9f6e099fa 100644 --- a/openstackclient/tests/functional/volume/v2/test_qos.py +++ b/openstackclient/tests/functional/volume/v2/test_qos.py @@ -74,7 +74,7 @@ def test_volume_qos_set_show_unset(self): cmd_output['consumer'] ) self.assertEqual( - "Alpha='a'", + {'Alpha': 'a'}, cmd_output['properties'] ) @@ -97,7 +97,7 @@ def test_volume_qos_set_show_unset(self): cmd_output['name'] ) self.assertEqual( - "Alpha='c', Beta='b'", + {'Alpha': 'c', 'Beta': 'b'}, cmd_output['properties'] ) @@ -118,7 +118,7 @@ def test_volume_qos_set_show_unset(self): cmd_output['name'] ) self.assertEqual( - "Beta='b'", + {'Beta': 'b'}, cmd_output['properties'] ) diff --git a/openstackclient/tests/functional/volume/v2/test_volume.py b/openstackclient/tests/functional/volume/v2/test_volume.py index 2930d48370..19fd58950e 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume.py +++ b/openstackclient/tests/functional/volume/v2/test_volume.py @@ -128,7 +128,7 @@ def test_volume_set_and_unset(self): cmd_output["description"], ) self.assertEqual( - "Alpha='a'", + {'Alpha': 'a'}, cmd_output["properties"], ) self.assertEqual( @@ -170,7 +170,7 @@ def test_volume_set_and_unset(self): cmd_output["description"], ) self.assertEqual( - "Beta='b', Gamma='c'", + {'Beta': 'b', 'Gamma': 'c'}, cmd_output["properties"], ) self.assertEqual( @@ -196,7 +196,7 @@ def test_volume_set_and_unset(self): new_name )) self.assertEqual( - "Gamma='c'", + {'Gamma': 'c'}, cmd_output["properties"], ) self.assertEqual( diff --git a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py index 264f4adb6b..8d32d99731 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py @@ -182,7 +182,7 @@ def test_volume_snapshot_set(self): cmd_output["description"], ) self.assertEqual( - "Alpha='a'", + {'Alpha': 'a'}, cmd_output["properties"], ) self.wait_for_status('volume snapshot', name, 'available') @@ -216,7 +216,7 @@ def test_volume_snapshot_set(self): cmd_output["description"], ) self.assertEqual( - "Alpha='c', Beta='b'", + {'Alpha': 'c', 'Beta': 'b'}, cmd_output["properties"], ) @@ -233,7 +233,7 @@ def test_volume_snapshot_set(self): new_name )) self.assertEqual( - "Beta='b'", + {'Beta': 'b'}, cmd_output["properties"], ) @@ -249,6 +249,6 @@ def test_volume_snapshot_set(self): new_name )) self.assertNotIn( - "Beta='b'", + {'Beta': 'b'}, cmd_output["properties"], ) diff --git a/openstackclient/tests/functional/volume/v2/test_volume_type.py b/openstackclient/tests/functional/volume/v2/test_volume_type.py index d8dd5bd626..3f1a6ea8a5 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_type.py @@ -65,8 +65,7 @@ def test_volume_type_set_unset_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - # TODO(amotoki): properties output should be machine-readable - self.assertEqual("a='b', c='d'", cmd_output['properties']) + self.assertEqual({'a': 'b', 'c': 'd'}, cmd_output['properties']) raw_output = self.openstack( 'volume type unset --property a %s' % name @@ -75,7 +74,7 @@ def test_volume_type_set_unset_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - self.assertEqual("c='d'", cmd_output['properties']) + self.assertEqual({'c': 'd'}, cmd_output['properties']) def test_volume_type_set_unset_multiple_properties(self): name = uuid.uuid4().hex @@ -96,7 +95,7 @@ def test_volume_type_set_unset_multiple_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - self.assertEqual("a='b', c='d'", cmd_output['properties']) + self.assertEqual({'a': 'b', 'c': 'd'}, cmd_output['properties']) raw_output = self.openstack( 'volume type unset --property a --property c %s' % name @@ -105,7 +104,7 @@ def test_volume_type_set_unset_multiple_properties(self): cmd_output = json.loads(self.openstack( 'volume type show -f json %s' % name )) - self.assertEqual("", cmd_output['properties']) + self.assertEqual({}, cmd_output['properties']) def test_volume_type_set_unset_project(self): name = uuid.uuid4().hex @@ -155,35 +154,32 @@ def test_encryption_type(self): '--encryption-key-size 128 ' '--encryption-control-location front-end ' + encryption_type)) - # TODO(amotoki): encryption output should be machine-readable - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test show encryption type cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + encryption_type)) - # TODO(amotoki): encryption output should be machine-readable - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test list encryption type cmd_output = json.loads(self.openstack( 'volume type list -f json --encryption-type')) encryption_output = [t['Encryption'] for t in cmd_output if t['Name'] == encryption_type][0] - # TODO(amotoki): encryption output should be machine-readable - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, encryption_output) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, encryption_output[attr]) # test set existing encryption type raw_output = self.openstack( 'volume type set ' @@ -193,12 +189,12 @@ def test_encryption_type(self): self.assertEqual('', raw_output) cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + encryption_type)) - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='256'", - "control_location='back-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 256, + 'control_location': 'back-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test set new encryption type cmd_output = json.loads(self.openstack( 'volume type create -f json --private ' + @@ -222,12 +218,12 @@ def test_encryption_type(self): cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + name )) - expected = ["provider='LuksEncryptor'", - "cipher='aes-xts-plain64'", - "key_size='128'", - "control_location='front-end'"] - for attr in expected: - self.assertIn(attr, cmd_output['encryption']) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) # test unset encryption type raw_output = self.openstack( 'volume type unset --encryption-type ' + name @@ -236,7 +232,7 @@ def test_encryption_type(self): cmd_output = json.loads(self.openstack( 'volume type show -f json --encryption-type ' + name )) - self.assertEqual('', cmd_output['encryption']) + self.assertEqual({}, cmd_output['encryption']) # test delete encryption type raw_output = self.openstack('volume type delete ' + encryption_type) self.assertEqual('', raw_output) diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index c245cbf66b..5f976b0625 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -17,7 +17,8 @@ import uuid import mock -from osc_lib import utils as common_utils + +from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -468,7 +469,7 @@ def get_volume_data(volume=None): if x == 'tags': # The 'tags' should be format_list data_list.append( - common_utils.format_list(volume.info.get(x))) + format_columns.ListColumn(volume.info.get(x))) else: data_list.append(volume.info.get(x)) return tuple(data_list) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index 6eeeae393e..d238818245 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -15,6 +15,7 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -250,7 +251,7 @@ def test_consistency_group_create_without_name(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_consistency_group_create_from_source(self): arglist = [ @@ -278,7 +279,7 @@ def test_consistency_group_create_from_source(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_consistency_group_create_from_snapshot(self): arglist = [ @@ -306,7 +307,7 @@ def test_consistency_group_create_from_snapshot(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestConsistencyGroupDelete(TestConsistencyGroup): @@ -439,7 +440,7 @@ class TestConsistencyGroupList(TestConsistencyGroup): c.availability_zone, c.name, c.description, - utils.format_list(c.volume_types) + format_columns.ListColumn(c.volume_types) )) def setUp(self): @@ -462,7 +463,7 @@ def test_consistency_group_list_without_options(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_consistency_group_list_with_all_project(self): arglist = [ @@ -479,7 +480,7 @@ def test_consistency_group_list_with_all_project(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': True}) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_consistency_group_list_with_long(self): arglist = [ @@ -496,7 +497,7 @@ def test_consistency_group_list_with_long(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): @@ -704,4 +705,4 @@ def test_consistency_group_show(self): self.consistencygroups_mock.get.assert_called_once_with( self.consistency_group.id) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index 2b935e205f..454747f5aa 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -17,6 +17,8 @@ import mock from mock import call + +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -88,7 +90,7 @@ def setUp(self): self.new_qos_spec.consumer, self.new_qos_spec.id, self.new_qos_spec.name, - utils.format_dict(self.new_qos_spec.specs) + format_columns.DictColumn(self.new_qos_spec.specs) ) # Get the command object to test @@ -111,7 +113,7 @@ def test_qos_create_without_properties(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_qos_create_with_consumer(self): arglist = [ @@ -132,7 +134,7 @@ def test_qos_create_with_consumer(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_qos_create_with_properties(self): arglist = [ @@ -158,7 +160,7 @@ def test_qos_create_with_properties(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestQosDelete(TestQos): @@ -318,8 +320,8 @@ class TestQosList(TestQos): q.id, q.name, q.consumer, - qos_association.name, - utils.format_dict(q.specs), + format_columns.ListColumn([qos_association.name]), + format_columns.DictColumn(q.specs), )) def setUp(self): @@ -341,7 +343,7 @@ def test_qos_list(self): self.qos_mock.list.assert_called_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_qos_list_no_association(self): self.qos_mock.reset_mock() @@ -365,10 +367,10 @@ def test_qos_list_no_association(self): self.qos_specs[1].id, self.qos_specs[1].name, self.qos_specs[1].consumer, - None, - utils.format_dict(self.qos_specs[1].specs), + format_columns.ListColumn(None), + format_columns.DictColumn(self.qos_specs[1].specs), ) - self.assertEqual(ex_data, list(data)) + self.assertListItemEqual(ex_data, list(data)) class TestQosSet(TestQos): @@ -416,11 +418,11 @@ class TestQosShow(TestQos): 'properties' ) data = ( - qos_association.name, + format_columns.ListColumn([qos_association.name]), qos_spec.consumer, qos_spec.id, qos_spec.name, - utils.format_dict(qos_spec.specs), + format_columns.DictColumn(qos_spec.specs), ) def setUp(self): @@ -448,7 +450,7 @@ def test_qos_show(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, tuple(data)) + self.assertItemEqual(self.data, tuple(data)) class TestQosUnset(TestQos): diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 4023d55b3d..17915928dc 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -15,6 +15,7 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -92,7 +93,7 @@ def test_type_create_public(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_type_create_private(self): arglist = [ @@ -118,7 +119,7 @@ def test_type_create_private(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_public_type_create_with_project(self): arglist = [ @@ -158,7 +159,7 @@ def test_type_create_with_encryption(self): ) encryption_data = ( self.new_volume_type.description, - utils.format_dict(encryption_info), + format_columns.DictColumn(encryption_info), self.new_volume_type.id, True, self.new_volume_type.name, @@ -195,7 +196,7 @@ def test_type_create_with_encryption(self): body, ) self.assertEqual(encryption_columns, columns) - self.assertEqual(encryption_data, data) + self.assertItemEqual(encryption_data, data) class TestTypeDelete(TestType): @@ -305,7 +306,7 @@ class TestTypeList(TestType): t.name, t.is_public, t.description, - utils.format_dict(t.extra_specs), + format_columns.DictColumn(t.extra_specs), )) def setUp(self): @@ -329,7 +330,7 @@ def test_type_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=None) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = [ @@ -347,7 +348,7 @@ def test_type_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=True) self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) def test_type_list_with_private_option(self): arglist = [ @@ -364,7 +365,7 @@ def test_type_list_with_private_option(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=False) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_type_list_with_default_option(self): arglist = [ @@ -382,7 +383,7 @@ def test_type_list_with_default_option(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.default.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data_with_default_type, list(data)) + self.assertListItemEqual(self.data_with_default_type, list(data)) def test_type_list_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type( @@ -401,13 +402,16 @@ def test_type_list_with_encryption(self): self.volume_types[0].id, self.volume_types[0].name, self.volume_types[0].is_public, - utils.format_dict(encryption_info), + volume_type.EncryptionInfoColumn( + self.volume_types[0].id, + {self.volume_types[0].id: encryption_info}), )) encryption_data.append(( self.volume_types[1].id, self.volume_types[1].name, self.volume_types[1].is_public, - '-', + volume_type.EncryptionInfoColumn( + self.volume_types[1].id, {}), )) self.encryption_types_mock.list.return_value = [encryption_type] @@ -423,7 +427,7 @@ def test_type_list_with_encryption(self): self.encryption_types_mock.list.assert_called_once_with() self.types_mock.list.assert_called_once_with(is_public=None) self.assertEqual(encryption_columns, columns) - self.assertEqual(encryption_data, list(data)) + self.assertListItemEqual(encryption_data, list(data)) class TestTypeSet(TestType): @@ -687,7 +691,7 @@ def setUp(self): self.volume_type.id, True, self.volume_type.name, - utils.format_dict(self.volume_type.extra_specs) + format_columns.DictColumn(self.volume_type.extra_specs) ) self.types_mock.get.return_value = self.volume_type @@ -709,7 +713,7 @@ def test_type_show(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_type_show_with_access(self): arglist = [ @@ -735,14 +739,14 @@ def test_type_show_with_access(self): self.assertEqual(self.columns, columns) private_type_data = ( - utils.format_list([type_access_list.project_id]), + format_columns.ListColumn([type_access_list.project_id]), private_type.description, private_type.id, private_type.is_public, private_type.name, - utils.format_dict(private_type.extra_specs) + format_columns.DictColumn(private_type.extra_specs) ) - self.assertEqual(private_type_data, data) + self.assertItemEqual(private_type_data, data) def test_type_show_with_list_access_exec(self): arglist = [ @@ -772,9 +776,9 @@ def test_type_show_with_list_access_exec(self): private_type.id, private_type.is_public, private_type.name, - utils.format_dict(private_type.extra_specs) + format_columns.DictColumn(private_type.extra_specs) ) - self.assertEqual(private_type_data, data) + self.assertItemEqual(private_type_data, data) def test_type_show_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type() @@ -800,11 +804,11 @@ def test_type_show_with_encryption(self): encryption_data = ( None, self.volume_type.description, - utils.format_dict(encryption_info), + format_columns.DictColumn(encryption_info), self.volume_type.id, True, self.volume_type.name, - utils.format_dict(self.volume_type.extra_specs) + format_columns.DictColumn(self.volume_type.extra_specs) ) arglist = [ '--encryption-type', @@ -820,7 +824,7 @@ def test_type_show_with_encryption(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.encryption_types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(encryption_columns, columns) - self.assertEqual(encryption_data, data) + self.assertItemEqual(encryption_data, data) class TestTypeUnset(TestType): @@ -923,3 +927,30 @@ def test_type_unset_encryption_type(self): result = self.cmd.take_action(parsed_args) self.encryption_types_mock.delete.assert_called_with(self.volume_type) self.assertIsNone(result) + + +class TestColumns(TestType): + + def test_encryption_info_column_with_info(self): + fake_volume_type = volume_fakes.FakeType.create_one_type() + type_id = fake_volume_type.id + + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + col = volume_type.EncryptionInfoColumn(type_id, + {type_id: encryption_info}) + self.assertEqual(utils.format_dict(encryption_info), + col.human_readable()) + self.assertEqual(encryption_info, col.machine_readable()) + + def test_encryption_info_column_without_info(self): + fake_volume_type = volume_fakes.FakeType.create_one_type() + type_id = fake_volume_type.id + + col = volume_type.EncryptionInfoColumn(type_id, {}) + self.assertEqual('-', col.human_readable()) + self.assertIsNone(col.machine_readable()) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 97da160148..332b50a78f 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -16,6 +16,8 @@ import mock from mock import call + +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -94,7 +96,7 @@ def setUp(self): self.new_volume.description, self.new_volume.id, self.new_volume.name, - utils.format_dict(self.new_volume.metadata), + format_columns.DictColumn(self.new_volume.metadata), self.new_volume.size, self.new_volume.snapshot_id, self.new_volume.status, @@ -135,7 +137,7 @@ def test_volume_create_min_options(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_options(self): consistency_group = ( @@ -181,7 +183,7 @@ def test_volume_create_options(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -217,7 +219,7 @@ def test_volume_create_properties(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() @@ -255,7 +257,7 @@ def test_volume_create_image_id(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() @@ -293,7 +295,7 @@ def test_volume_create_image_name(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_with_snapshot(self): snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() @@ -330,7 +332,7 @@ def test_volume_create_with_snapshot(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) def test_volume_create_with_bootable_and_readonly(self): arglist = [ @@ -368,7 +370,7 @@ def test_volume_create_with_bootable_and_readonly(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -410,7 +412,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, False) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -462,7 +464,7 @@ def test_volume_create_with_bootable_and_readonly_fail( self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertItemEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -672,17 +674,14 @@ def test_volume_list_no_options(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_project(self): arglist = [ @@ -715,17 +714,14 @@ def test_volume_list_project(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_project_domain(self): arglist = [ @@ -760,17 +756,14 @@ def test_volume_list_project_domain(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_user(self): arglist = [ @@ -800,19 +793,16 @@ def test_volume_list_user(self): marker=None, limit=None, ) - self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) + datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_user_domain(self): arglist = [ @@ -847,17 +837,14 @@ def test_volume_list_user_domain(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_name(self): arglist = [ @@ -890,17 +877,14 @@ def test_volume_list_name(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_status(self): arglist = [ @@ -933,17 +917,14 @@ def test_volume_list_status(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_all_projects(self): arglist = [ @@ -976,17 +957,14 @@ def test_volume_list_all_projects(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_long(self): arglist = [ @@ -1030,9 +1008,6 @@ def test_volume_list_long(self): ] self.assertEqual(collist, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, @@ -1040,10 +1015,10 @@ def test_volume_list_long(self): self.mock_volume.size, self.mock_volume.volume_type, self.mock_volume.bootable, - msg, - utils.format_dict(self.mock_volume.metadata), + volume.AttachmentsColumn(self.mock_volume.attachments), + format_columns.DictColumn(self.mock_volume.metadata), ), ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_with_marker_and_limit(self): arglist = [ @@ -1064,15 +1039,12 @@ def test_volume_list_with_marker_and_limit(self): self.assertEqual(self.columns, columns) - server = self.mock_volume.attachments[0]['server_id'] - device = self.mock_volume.attachments[0]['device'] - msg = 'Attached to %s on %s ' % (server, device) datalist = (( self.mock_volume.id, self.mock_volume.name, self.mock_volume.status, self.mock_volume.size, - msg, + volume.AttachmentsColumn(self.mock_volume.attachments), ), ) self.volumes_mock.list.assert_called_once_with( @@ -1085,7 +1057,7 @@ def test_volume_list_with_marker_and_limit(self): 'name': None, 'all_tenants': False, } ) - self.assertEqual(datalist, tuple(data)) + self.assertListItemEqual(datalist, tuple(data)) def test_volume_list_negative_limit(self): arglist = [ @@ -1479,7 +1451,7 @@ def test_volume_show(self): volume_fakes.FakeVolume.get_volume_columns(self._volume), columns) - self.assertEqual( + self.assertItemEqual( volume_fakes.FakeVolume.get_volume_data(self._volume), data) @@ -1562,3 +1534,31 @@ def test_volume_unset_image_property_fail(self): self.new_volume.id, parsed_args.image_property) self.volumes_mock.delete_metadata.assert_called_with( self.new_volume.id, parsed_args.property) + + +class TestColumns(TestVolume): + + def test_attachments_column_without_server_cache(self): + _volume = volume_fakes.FakeVolume.create_one_volume() + server_id = _volume.attachments[0]['server_id'] + device = _volume.attachments[0]['device'] + + col = volume.AttachmentsColumn(_volume.attachments, {}) + self.assertEqual('Attached to %s on %s ' % (server_id, device), + col.human_readable()) + self.assertEqual(_volume.attachments, col.machine_readable()) + + def test_attachments_column_with_server_cache(self): + _volume = volume_fakes.FakeVolume.create_one_volume() + + server_id = _volume.attachments[0]['server_id'] + device = _volume.attachments[0]['device'] + fake_server = mock.Mock() + fake_server.name = 'fake-server-name' + server_cache = {server_id: fake_server} + + col = volume.AttachmentsColumn(_volume.attachments, server_cache) + self.assertEqual( + 'Attached to %s on %s ' % ('fake-server-name', device), + col.human_readable()) + self.assertEqual(_volume.attachments, col.machine_readable()) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index d0d1da3b29..30c7915dd9 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -271,7 +271,7 @@ class TestBackupList(TestBackup): b.status, b.size, b.availability_zone, - b.volume_id, + volume_backup.VolumeIdColumn(b.volume_id), b.container, )) @@ -314,7 +314,7 @@ def test_backup_list_without_options(self): limit=None, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + self.assertListItemEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = [ @@ -353,7 +353,7 @@ def test_backup_list_with_options(self): limit=3, ) self.assertEqual(self.columns_long, columns) - self.assertEqual(self.data_long, list(data)) + self.assertListItemEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 0a932f8454..26dd8ffca1 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -16,6 +16,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -238,7 +239,7 @@ def take_action(self, parsed_args): return (columns, ( utils.get_item_properties( s, columns, - formatters={'Volume Types': utils.format_list}) + formatters={'Volume Types': format_columns.ListColumn}) for s in consistency_groups)) diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index c71605818a..3037d34ae8 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -17,6 +17,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -96,7 +97,8 @@ def take_action(self, parsed_args): qos_spec = volume_client.qos_specs.create(parsed_args.name, specs) qos_spec._info.update( - {'properties': utils.format_dict(qos_spec._info.pop('specs'))} + {'properties': + format_columns.DictColumn(qos_spec._info.pop('specs'))} ) return zip(*sorted(six.iteritems(qos_spec._info))) @@ -210,8 +212,8 @@ def take_action(self, parsed_args): (utils.get_dict_properties( s._info, columns, formatters={ - 'Specs': utils.format_dict, - 'Associations': utils.format_list + 'Specs': format_columns.DictColumn, + 'Associations': format_columns.ListColumn }, ) for s in qos_specs_list)) @@ -267,10 +269,11 @@ def take_action(self, parsed_args): associations = [association.name for association in qos_associations] qos_spec._info.update({ - 'associations': utils.format_list(associations) + 'associations': format_columns.ListColumn(associations) }) qos_spec._info.update( - {'properties': utils.format_dict(qos_spec._info.pop('specs'))}) + {'properties': + format_columns.DictColumn(qos_spec._info.pop('specs'))}) return zip(*sorted(six.iteritems(qos_spec._info))) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index ef65d0970a..17ccd3d31e 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -16,8 +16,11 @@ import argparse import copy +import functools import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -31,6 +34,37 @@ LOG = logging.getLogger(__name__) +class AttachmentsColumn(cliff_columns.FormattableColumn): + """Formattable column for attachments column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes server_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(AttachmentsColumn, server_cache)``. + """ + + def __init__(self, value, server_cache=None): + super(AttachmentsColumn, self).__init__(value) + self._server_cache = server_cache or {} + + def human_readable(self): + """Return a formatted string of a volume's attached instances + + :rtype: a string of formatted instances + """ + + msg = '' + for attachment in self._value: + server = attachment['server_id'] + if server in self._server_cache.keys(): + server = self._server_cache[server].name + device = attachment['device'] + msg += 'Attached to %s on %s ' % (server, device) + return msg + + def _check_size_arg(args): """Check whether --size option is required or not. @@ -212,7 +246,8 @@ def take_action(self, parsed_args): # Remove key links from being displayed volume._info.update( { - 'properties': utils.format_dict(volume._info.pop('metadata')), + 'properties': + format_columns.DictColumn(volume._info.pop('metadata')), 'type': volume._info.pop('volume_type') } ) @@ -331,22 +366,6 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute identity_client = self.app.client_manager.identity - def _format_attach(attachments): - """Return a formatted string of a volume's attached instances - - :param attachments: a volume.attachments field - :rtype: a string of formatted instances - """ - - msg = '' - for attachment in attachments: - server = attachment['server_id'] - if server in server_cache: - server = server_cache[server].name - device = attachment['device'] - msg += 'Attached to %s on %s ' % (server, device) - return msg - if parsed_args.long: columns = [ 'ID', @@ -381,6 +400,8 @@ def _format_attach(attachments): except Exception: # Just forget it if there's any trouble pass + AttachmentsColumnWithCache = functools.partial( + AttachmentsColumn, server_cache=server_cache) project_id = None if parsed_args.project: @@ -417,8 +438,8 @@ def _format_attach(attachments): return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Metadata': utils.format_dict, - 'Attachments': _format_attach}, + formatters={'Metadata': format_columns.DictColumn, + 'Attachments': AttachmentsColumnWithCache}, ) for s in data)) @@ -722,7 +743,8 @@ def take_action(self, parsed_args): # 'volume_type' --> 'type' volume._info.update( { - 'properties': utils.format_dict(volume._info.pop('metadata')), + 'properties': + format_columns.DictColumn(volume._info.pop('metadata')), 'type': volume._info.pop('volume_type'), }, ) diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index 1d2b0cde04..4d0d54c1b5 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -15,8 +15,10 @@ """Volume v2 Backup action implementations""" import copy +import functools import logging +from cliff import columns as cliff_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -29,6 +31,33 @@ LOG = logging.getLogger(__name__) +class VolumeIdColumn(cliff_columns.FormattableColumn): + """Formattable column for volume ID column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes volume_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(VolumeIdColumn, volume_cache)``. + """ + + def __init__(self, value, volume_cache=None): + super(VolumeIdColumn, self).__init__(value) + self._volume_cache = volume_cache or {} + + def human_readable(self): + """Return a volume name if available + + :rtype: either the volume ID or name + """ + volume_id = self._value + volume = volume_id + if volume_id in self._volume_cache.keys(): + volume = self._volume_cache[volume_id].name + return volume + + class CreateVolumeBackup(command.ShowOne): _description = _("Create new volume backup") @@ -189,18 +218,6 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): volume_client = self.app.client_manager.volume - def _format_volume_id(volume_id): - """Return a volume name if available - - :param volume_id: a volume ID - :rtype: either the volume ID or name - """ - - volume = volume_id - if volume_id in volume_cache.keys(): - volume = volume_cache[volume_id].name - return volume - if parsed_args.long: columns = ['ID', 'Name', 'Description', 'Status', 'Size', 'Availability Zone', 'Volume ID', 'Container'] @@ -218,6 +235,8 @@ def _format_volume_id(volume_id): except Exception: # Just forget it if there's any trouble pass + _VolumeIdColumn = functools.partial(VolumeIdColumn, + volume_cache=volume_cache) filter_volume_id = None if parsed_args.volume: @@ -242,7 +261,7 @@ def _format_volume_id(volume_id): return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Volume ID': _format_volume_id}, + formatters={'Volume ID': _VolumeIdColumn}, ) for s in data)) diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index fe9694104a..2b26ae323b 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -15,8 +15,11 @@ """Volume v2 snapshot action implementations""" import copy +import functools import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -30,6 +33,33 @@ LOG = logging.getLogger(__name__) +class VolumeIdColumn(cliff_columns.FormattableColumn): + """Formattable column for volume ID column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes volume_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(VolumeIdColumn, volume_cache)``. + """ + + def __init__(self, value, volume_cache=None): + super(VolumeIdColumn, self).__init__(value) + self._volume_cache = volume_cache or {} + + def human_readable(self): + """Return a volume name if available + + :rtype: either the volume ID or name + """ + volume_id = self._value + volume = volume_id + if volume_id in self._volume_cache.keys(): + volume = self._volume_cache[volume_id].name + return volume + + class CreateVolumeSnapshot(command.ShowOne): _description = _("Create new volume snapshot") @@ -107,7 +137,8 @@ def take_action(self, parsed_args): metadata=parsed_args.property, ) snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + {'properties': + format_columns.DictColumn(snapshot._info.pop('metadata'))} ) return zip(*sorted(six.iteritems(snapshot._info))) @@ -216,18 +247,6 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.volume identity_client = self.app.client_manager.identity - def _format_volume_id(volume_id): - """Return a volume name if available - - :param volume_id: a volume ID - :rtype: either the volume ID or name - """ - - volume = volume_id - if volume_id in volume_cache.keys(): - volume = volume_cache[volume_id].name - return volume - if parsed_args.long: columns = ['ID', 'Name', 'Description', 'Status', 'Size', 'Created At', 'Volume ID', 'Metadata'] @@ -246,6 +265,8 @@ def _format_volume_id(volume_id): except Exception: # Just forget it if there's any trouble pass + _VolumeIdColumn = functools.partial(VolumeIdColumn, + volume_cache=volume_cache) volume_id = None if parsed_args.volume: @@ -279,8 +300,8 @@ def _format_volume_id(volume_id): return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Metadata': utils.format_dict, - 'Volume ID': _format_volume_id}, + formatters={'Metadata': format_columns.DictColumn, + 'Volume ID': _VolumeIdColumn}, ) for s in data)) @@ -402,7 +423,8 @@ def take_action(self, parsed_args): snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) snapshot._info.update( - {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + {'properties': + format_columns.DictColumn(snapshot._info.pop('metadata'))} ) return zip(*sorted(six.iteritems(snapshot._info))) diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 749d1dd6fd..54b1f49719 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -14,8 +14,11 @@ """Volume v2 Type action implementations""" +import functools import logging +from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -29,6 +32,36 @@ LOG = logging.getLogger(__name__) +class EncryptionInfoColumn(cliff_columns.FormattableColumn): + """Formattable column for encryption info column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes encryption_data as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(EncryptionInfoColumn encryption_data)``. + """ + + def __init__(self, value, encryption_data=None): + super(EncryptionInfoColumn, self).__init__(value) + self._encryption_data = encryption_data or {} + + def _get_encryption_info(self): + type_id = self._value + return self._encryption_data.get(type_id) + + def human_readable(self): + encryption_info = self._get_encryption_info() + if encryption_info: + return utils.format_dict(encryption_info) + else: + return '-' + + def machine_readable(self): + return self._get_encryption_info() + + def _create_encryption_type(volume_client, volume_type, parsed_args): if not parsed_args.encryption_provider: msg = _("'--encryption-provider' should be specified while " @@ -183,7 +216,8 @@ def take_action(self, parsed_args): LOG.error(msg % {'project': parsed_args.project, 'e': e}) if parsed_args.property: result = volume_type.set_keys(parsed_args.property) - volume_type._info.update({'properties': utils.format_dict(result)}) + volume_type._info.update( + {'properties': format_columns.DictColumn(result)}) if (parsed_args.encryption_provider or parsed_args.encryption_cipher or parsed_args.encryption_key_size or @@ -198,7 +232,7 @@ def take_action(self, parsed_args): # add encryption info in result encryption._info.pop("volume_type_id", None) volume_type._info.update( - {'encryption': utils.format_dict(encryption._info)}) + {'encryption': format_columns.DictColumn(encryption._info)}) volume_type._info.pop("os-volume-type-access:is_public", None) return zip(*sorted(six.iteritems(volume_type._info))) @@ -296,12 +330,7 @@ def take_action(self, parsed_args): data = volume_client.volume_types.list( is_public=is_public) - def _format_encryption_info(type_id, encryption_data=None): - encryption_data = encryption - encryption_info = '-' - if type_id in encryption_data.keys(): - encryption_info = encryption_data[type_id] - return encryption_info + formatters = {'Extra Specs': format_columns.DictColumn} if parsed_args.encryption_type: encryption = {} @@ -318,18 +347,21 @@ def _format_encryption_info(type_id, encryption_data=None): for key in del_key: d._info.pop(key, None) # save the encryption information with their volume type ID - encryption[volume_type_id] = utils.format_dict(d._info) + encryption[volume_type_id] = d._info # We need to get volume type ID, then show encryption # information according to the ID, so use "id" to keep # difference to the real "ID" column. columns += ['id'] column_headers += ['Encryption'] + _EncryptionInfoColumn = functools.partial( + EncryptionInfoColumn, encryption_data=encryption) + formatters['id'] = _EncryptionInfoColumn + return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Extra Specs': utils.format_dict, - 'id': _format_encryption_info}, + formatters=formatters, ) for s in data)) @@ -490,7 +522,7 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) - properties = utils.format_dict( + properties = format_columns.DictColumn( volume_type._info.pop('extra_specs', {})) volume_type._info.update({'properties': properties}) access_project_ids = None @@ -502,7 +534,7 @@ def take_action(self, parsed_args): for item in volume_type_access] # TODO(Rui Chen): This format list case can be removed after # patch https://review.opendev.org/#/c/330223/ merged. - access_project_ids = utils.format_list(project_ids) + access_project_ids = format_columns.ListColumn(project_ids) except Exception as e: msg = _('Failed to get access project list for volume type ' '%(type)s: %(e)s') @@ -515,7 +547,8 @@ def take_action(self, parsed_args): volume_type.id) encryption._info.pop("volume_type_id", None) volume_type._info.update( - {'encryption': utils.format_dict(encryption._info)}) + {'encryption': + format_columns.DictColumn(encryption._info)}) except Exception as e: LOG.error(_("Failed to display the encryption information " "of this volume type: %s"), e) From bfc34e11b3437506508b3e120accc0e212268ac6 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Fri, 28 Jun 2019 18:17:10 +0000 Subject: [PATCH 0102/1120] Fix BFV server list handling with --name-lookup-one-by-one When the --name-lookup-one-by-one option passed to the 'server list' command, the image and flavor names will be looked up for each server being listed instead of fetching all image/flavor names. The current code assumes all servers have an image attribute, but servers booted from volumes have no image, so the following error is raised when listing BFV servers with --name-lookup-one-by-one: AttributeError: ('unicode'|'str') object has no attribute 'get' The error occurs when the code attempts server.image.get('id'). This fixes the --name-lookup-one-by-one code not to assume an image for a server. The unit tests for 'server list' have also been robustified to feature one BFV server to enhance our test coverage. Story: #2006063 Task: #34777 Change-Id: I312c971346c7ded93f6fcaa515098554b8580295 --- openstackclient/compute/v2/server.py | 5 ++- .../tests/unit/compute/v2/test_server.py | 34 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 2792e315f6..a4216e65dc 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1350,9 +1350,12 @@ def take_action(self, parsed_args): # Create a dict that maps image_id to image object. # Needed so that we can display the "Image Name" column. # "Image Name" is not crucial, so we swallow any exceptions. + # The 'image' attribute can be an empty string if the server was + # booted from a volume. if parsed_args.name_lookup_one_by_one or image_id: for i_id in set(filter(lambda x: x is not None, - (s.image.get('id') for s in data))): + (s.image.get('id') for s in data + if s.image))): try: images[i_id] = image_client.images.get(i_id) except Exception: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 8ea59a3850..713bf07aa7 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -14,6 +14,7 @@ # import argparse import collections +import copy import getpass import mock @@ -65,9 +66,22 @@ def setUp(self): self.methods = {} def setup_servers_mock(self, count): - servers = compute_fakes.FakeServer.create_servers(attrs=self.attrs, - methods=self.methods, - count=count) + # If we are creating more than one server, make one of them + # boot-from-volume + include_bfv = count > 1 + servers = compute_fakes.FakeServer.create_servers( + attrs=self.attrs, + methods=self.methods, + count=count - 1 if include_bfv else count + ) + if include_bfv: + attrs = copy.deepcopy(self.attrs) + attrs['image'] = '' + bfv_server = compute_fakes.FakeServer.create_one_server( + attrs=attrs, + methods=self.methods + ) + servers.append(bfv_server) # This is the return value for utils.find_resource() self.servers_mock.get = compute_fakes.FakeServer.get_servers(servers, @@ -2129,7 +2143,8 @@ def setUp(self): Image = collections.namedtuple('Image', 'id name') self.images_mock.list.return_value = [ Image(id=s.image['id'], name=self.image.name) - for s in self.servers + # Image will be an empty string if boot-from-volume + for s in self.servers if s.image ] Flavor = collections.namedtuple('Flavor', 'id name') @@ -2144,7 +2159,8 @@ def setUp(self): s.name, s.status, server._format_servers_list_networks(s.networks), - self.image.name, + # Image will be an empty string if boot-from-volume + self.image.name if s.image else s.image, self.flavor.name, )) self.data_long.append(( @@ -2156,8 +2172,9 @@ def setUp(self): getattr(s, 'OS-EXT-STS:power_state') ), server._format_servers_list_networks(s.networks), - self.image.name, - s.image['id'], + # Image will be an empty string if boot-from-volume + self.image.name if s.image else s.image, + s.image['id'] if s.image else s.image, self.flavor.name, s.flavor['id'], getattr(s, 'OS-EXT-AZ:availability_zone'), @@ -2169,7 +2186,8 @@ def setUp(self): s.name, s.status, server._format_servers_list_networks(s.networks), - s.image['id'], + # Image will be an empty string if boot-from-volume + s.image['id'] if s.image else s.image, s.flavor['id'] )) From 5986f473064cd43bf09909b5c3f616bf44329724 Mon Sep 17 00:00:00 2001 From: pengyuesheng Date: Mon, 1 Jul 2019 15:06:43 +0800 Subject: [PATCH 0103/1120] Add Python 3 Train unit tests See the Train python3-updates goal document for details: https://governance.openstack.org/tc/goals/train/python3-updates.html Change-Id: I897645a4cb9f03ca464daa14f0895572212b81d9 --- .zuul.yaml | 3 +-- setup.cfg | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 95a6890c71..574734659e 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -193,8 +193,7 @@ - openstack-cover-jobs - openstack-lower-constraints-jobs - openstack-python-jobs - - openstack-python36-jobs - - openstack-python37-jobs + - openstack-python3-train-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 diff --git a/setup.cfg b/setup.cfg index d3d79983b3..c48088087f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 [files] packages = diff --git a/tox.ini b/tox.ini index 8145179ece..00c179ddb9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.3 -envlist = py36,py27,pep8 +envlist = py37,py36,py27,pep8 skipdist = True [testenv] From d16d98b27b87ea5d6186c241a19b1c60e00db3bb Mon Sep 17 00:00:00 2001 From: pengyuesheng Date: Mon, 1 Jul 2019 15:13:08 +0800 Subject: [PATCH 0104/1120] Update the constraints url For more detail, see http://lists.openstack.org/pipermail/openstack-discuss/2019-May/006478.html Change-Id: Ie0a41fa97696bcd8b5fd2e670efdf9379ff1080e --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 8145179ece..e50744367f 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ setenv = VIRTUAL_ENV={envdir} OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -88,7 +88,7 @@ commands = [testenv:venv] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = {posargs} @@ -112,7 +112,7 @@ commands = [testenv:docs] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -122,7 +122,7 @@ commands = [testenv:releasenotes] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From 879f8207786caf1cd9e1465e32e322abd111508c Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 1 Jul 2019 15:06:56 -0400 Subject: [PATCH 0105/1120] docs: clarify compute service --service option The compute service commands emit a "Binary" in the output but the --service filter option isn't as clear that it's the binary (for set it is but not list) nor do the docs give an example of a binary (typically nova-compute but could be others like nova-conductor, nova-scheduler, etc). This simply mentions that the --service option is the binary for "compute service list" and gives an example value for the option in both list and set help. Change-Id: If87fc37352c3a251cc89041723adbe04dedf4f8a --- doc/source/cli/command-objects/compute-service.rst | 5 +++-- openstackclient/compute/v2/service.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/source/cli/command-objects/compute-service.rst b/doc/source/cli/command-objects/compute-service.rst index ba624ea0a4..51d5c86433 100644 --- a/doc/source/cli/command-objects/compute-service.rst +++ b/doc/source/cli/command-objects/compute-service.rst @@ -39,7 +39,8 @@ List compute services .. option:: --service - List only specified service (name only) + List only specified service binaries (name only). For example, + ``nova-compute``, ``nova-conductor``, etc. .. option:: --long @@ -86,4 +87,4 @@ Set compute service properties .. describe:: - Name of service (Binary name) + Name of service (Binary name), for example ``nova-compute`` diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 18e6d9d95e..98347c9f86 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -72,7 +72,8 @@ def get_parser(self, prog_name): parser.add_argument( "--service", metavar="", - help=_("List only specified service (name only)") + help=_("List only specified service binaries (name only). For " + "example, ``nova-compute``, ``nova-conductor``, etc.") ) parser.add_argument( "--long", @@ -126,7 +127,8 @@ def get_parser(self, prog_name): parser.add_argument( "service", metavar="", - help=_("Name of service (Binary name)") + help=_("Name of service (Binary name), for example " + "``nova-compute``") ) enabled_group = parser.add_mutually_exclusive_group() enabled_group.add_argument( From b41d7518c381a453a66f3c09d38752f85b57b7e0 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Fri, 5 Jul 2019 11:54:01 -0400 Subject: [PATCH 0106/1120] Add Python 3 Train unit tests This is a mechanically generated patch to ensure unit testing is in place for all of the Tested Runtimes for Train. See the Train python3-updates goal document for details: https://governance.openstack.org/tc/goals/train/python3-updates.html Change-Id: I7d0a996b33d4d1eec436f92fbd390968cd37630c Story: #2005924 Task: #34232 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 629b74d00d..eaf66bc493 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.3 -envlist = py37,py36,py27,pep8 +envlist = py27,py37,pep8 skipdist = True [testenv] From 969e6abd20570ae64b3d1fd049da1521fa148b5c Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Thu, 30 Nov 2017 17:53:29 -0500 Subject: [PATCH 0107/1120] Support IPv6 addresses better When adding a security group rule, if no IP address is given we will use '0.0.0.0/0', but if the ethertype is IPv6 we will leave it as None. Change this to be '::/0' to match what we do for IPv4 - use the "any" address. The neutron server treats them both the same when checking for duplicates. Because there are most likely entries in the DB using None for the IP, print them as '0.0.0.0/0' or '::/0' so it is more obvious what address they are actually referring to. Also change to display the Ethertype column by default instead of with --long, since easily knowing IPv4 or IPv6 is useful. Change-Id: Ic396fc23caa66b6b0034c5d30b27c6ed499de5a6 Closes-bug: #1735575 --- .../command-objects/security-group-rule.rst | 13 +++-- .../network/v2/security_group_rule.py | 49 +++++++++++++++---- .../v2/test_security_group_rule_compute.py | 4 ++ .../v2/test_security_group_rule_network.py | 13 +++-- ...security-group-rules-95272847349982e5.yaml | 15 ++++++ 5 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml diff --git a/doc/source/cli/command-objects/security-group-rule.rst b/doc/source/cli/command-objects/security-group-rule.rst index 5809e00278..5a2d8342b3 100644 --- a/doc/source/cli/command-objects/security-group-rule.rst +++ b/doc/source/cli/command-objects/security-group-rule.rst @@ -27,8 +27,9 @@ Create a new security group rule .. option:: --remote-ip - Remote IP address block - (may use CIDR notation; default for IPv4 rule: 0.0.0.0/0) + Remote IP address block (may use CIDR notation; + default for IPv4 rule: 0.0.0.0/0, + default for IPv6 rule: ::/0) .. option:: --remote-group @@ -134,6 +135,7 @@ List security group rules openstack security group rule list [--all-projects] [--protocol ] + [--ethertype ] [--ingress | --egress] [--long] [] @@ -151,7 +153,6 @@ List security group rules *Compute version 2 does not have additional fields to display.* - .. option:: --protocol List rules by the IP protocol (ah, dhcp, egp, esp, gre, icmp, igmp, @@ -161,6 +162,12 @@ List security group rules *Network version 2* +.. option:: --ethertype + + List rules by the Ethertype (IPv4 or IPv6) + + *Network version 2* + .. option:: --ingress List rules applied to incoming network traffic diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 637fba1d95..dbea747300 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -62,6 +62,17 @@ def _format_network_port_range(rule): return port_range +def _format_remote_ip_prefix(rule): + remote_ip_prefix = rule['remote_ip_prefix'] + if remote_ip_prefix is None: + ethertype = rule['ether_type'] + if ethertype == 'IPv4': + remote_ip_prefix = '0.0.0.0/0' + elif ethertype == 'IPv6': + remote_ip_prefix = '::/0' + return remote_ip_prefix + + def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -108,7 +119,8 @@ def update_parser_common(self, parser): "--remote-ip", metavar="", help=_("Remote IP address block (may use CIDR notation; " - "default for IPv4 rule: 0.0.0.0/0)"), + "default for IPv4 rule: 0.0.0.0/0, " + "default for IPv6 rule: ::/0)"), ) remote_group.add_argument( "--remote-group", @@ -230,6 +242,14 @@ def _get_protocol(self, parsed_args, default_protocol='any'): protocol = None return protocol + def _get_ethertype(self, parsed_args, protocol): + ethertype = 'IPv4' + if parsed_args.ethertype is not None: + ethertype = parsed_args.ethertype + elif self._is_ipv6_protocol(protocol): + ethertype = 'IPv6' + return ethertype + def _is_ipv6_protocol(self, protocol): # NOTE(rtheis): Neutron has deprecated protocol icmpv6. # However, while the OSC CLI doesn't document the protocol, @@ -264,12 +284,8 @@ def take_action_network(self, client, parsed_args): # NOTE(rtheis): Use ethertype specified else default based # on IP protocol. - if parsed_args.ethertype: - attrs['ethertype'] = parsed_args.ethertype - elif self._is_ipv6_protocol(attrs['protocol']): - attrs['ethertype'] = 'IPv6' - else: - attrs['ethertype'] = 'IPv4' + attrs['ethertype'] = self._get_ethertype(parsed_args, + attrs['protocol']) # NOTE(rtheis): Validate the port range and ICMP type and code. # It would be ideal if argparse could do this. @@ -306,6 +322,8 @@ def take_action_network(self, client, parsed_args): attrs['remote_ip_prefix'] = parsed_args.remote_ip elif attrs['ethertype'] == 'IPv4': attrs['remote_ip_prefix'] = '0.0.0.0/0' + elif attrs['ethertype'] == 'IPv6': + attrs['remote_ip_prefix'] = '::/0' attrs['security_group_id'] = security_group_id if parsed_args.project is not None: identity_client = self.app.client_manager.identity @@ -387,6 +405,7 @@ def _format_network_security_group_rule(self, rule): """ rule = rule.to_dict() rule['port_range'] = _format_network_port_range(rule) + rule['remote_ip_prefix'] = _format_remote_ip_prefix(rule) return rule def update_parser_common(self, parser): @@ -418,6 +437,12 @@ def update_parser_network(self, parser): "udp, udplite, vrrp and integer representations [0-255] " "or any; default: any (all protocols))") ) + parser.add_argument( + '--ethertype', + metavar='', + type=_convert_to_lowercase, + help=_("List rules by the Ethertype (IPv4 or IPv6)") + ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( '--ingress', @@ -458,11 +483,12 @@ def _get_column_headers(self, parsed_args): column_headers = ( 'ID', 'IP Protocol', + 'Ethertype', 'IP Range', 'Port Range', ) if parsed_args.long: - column_headers = column_headers + ('Direction', 'Ethertype',) + column_headers = column_headers + ('Direction',) column_headers = column_headers + ('Remote Security Group',) if parsed_args.group is None: column_headers = column_headers + ('Security Group',) @@ -473,11 +499,12 @@ def take_action_network(self, client, parsed_args): columns = ( 'id', 'protocol', + 'ether_type', 'remote_ip_prefix', 'port_range', ) if parsed_args.long: - columns = columns + ('direction', 'ether_type',) + columns = columns + ('direction',) columns = columns + ('remote_group_id',) # Get the security group rules using the requested query. @@ -516,6 +543,7 @@ def take_action_compute(self, client, parsed_args): columns = ( "ID", "IP Protocol", + "Ethertype", "IP Range", "Port Range", "Remote Security Group", @@ -564,6 +592,9 @@ def update_parser_common(self, parser): def take_action_network(self, client, parsed_args): obj = client.find_security_group_rule(parsed_args.rule, ignore_missing=False) + # necessary for old rules that have None in this field + if not obj['remote_ip_prefix']: + obj['remote_ip_prefix'] = _format_remote_ip_prefix(obj) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py index 6814c197b3..cf5261b284 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py @@ -337,6 +337,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): _security_group_rule_tcp = \ compute_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'ip_protocol': 'tcp', + 'ethertype': 'IPv4', 'from_port': 80, 'to_port': 80, 'group': {'name': _security_group['name']}, @@ -344,6 +345,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): _security_group_rule_icmp = \ compute_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'ip_protocol': 'icmp', + 'ethertype': 'IPv4', 'from_port': -1, 'to_port': -1, 'ip_range': {'cidr': '10.0.2.0/24'}, @@ -357,6 +359,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): expected_columns_with_group = ( 'ID', 'IP Protocol', + 'Ethertype', 'IP Range', 'Port Range', 'Remote Security Group', @@ -373,6 +376,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): expected_rule_with_group = ( rule['id'], rule['ip_protocol'], + rule['ethertype'], rule['ip_range'], rule['port_range'], rule['remote_security_group'], diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index eb0cf310c0..49c3d5dbec 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -388,7 +388,7 @@ def test_create_network_options(self): 'port_range_min': 443, 'protocol': '6', 'remote_group_id': None, - 'remote_ip_prefix': None, + 'remote_ip_prefix': '::/0', }) arglist = [ '--dst-port', str(self._security_group_rule.port_range_min), @@ -419,6 +419,7 @@ def test_create_network_options(self): 'port_range_max': self._security_group_rule.port_range_max, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, 'security_group_id': self._security_group.id, 'tenant_id': self.project.id, }) @@ -664,6 +665,7 @@ def test_create_ipv6_icmp_type_code(self): 'port_range_min': 139, 'port_range_max': 2, 'protocol': 'ipv6-icmp', + 'remote_ip_prefix': '::/0', }) arglist = [ '--icmp-type', str(self._security_group_rule.port_range_min), @@ -688,6 +690,7 @@ def test_create_ipv6_icmp_type_code(self): 'port_range_min': self._security_group_rule.port_range_min, 'port_range_max': self._security_group_rule.port_range_max, 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, 'security_group_id': self._security_group.id, }) self.assertEqual(self.expected_columns, columns) @@ -698,6 +701,7 @@ def test_create_icmpv6_type(self): 'ether_type': 'IPv6', 'port_range_min': 139, 'protocol': 'icmpv6', + 'remote_ip_prefix': '::/0', }) arglist = [ '--icmp-type', str(self._security_group_rule.port_range_min), @@ -720,6 +724,7 @@ def test_create_icmpv6_type(self): 'ethertype': self._security_group_rule.ether_type, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, 'security_group_id': self._security_group.id, }) self.assertEqual(self.expected_columns, columns) @@ -868,15 +873,16 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): expected_columns_with_group_and_long = ( 'ID', 'IP Protocol', + 'Ethertype', 'IP Range', 'Port Range', 'Direction', - 'Ethertype', 'Remote Security Group', ) expected_columns_no_group = ( 'ID', 'IP Protocol', + 'Ethertype', 'IP Range', 'Port Range', 'Remote Security Group', @@ -889,16 +895,17 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): expected_data_with_group_and_long.append(( _security_group_rule.id, _security_group_rule.protocol, + _security_group_rule.ether_type, _security_group_rule.remote_ip_prefix, security_group_rule._format_network_port_range( _security_group_rule), _security_group_rule.direction, - _security_group_rule.ether_type, _security_group_rule.remote_group_id, )) expected_data_no_group.append(( _security_group_rule.id, _security_group_rule.protocol, + _security_group_rule.ether_type, _security_group_rule.remote_ip_prefix, security_group_rule._format_network_port_range( _security_group_rule), diff --git a/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml b/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml new file mode 100644 index 0000000000..6cac67cf88 --- /dev/null +++ b/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Security group rules can now be filtered by Ethertype in + ``security group rule list`` using ``--ethertype`` with either + ``ipv4`` or ``ipv6`` as an argument. +upgrade: + - | + Security group rule listings now have the ``Ethertype`` field displayed + by default to more easily differentiate between IPv4 and IPv6 rules. + In addition, the ``IP Range`` field of a security group will be + changed to ``0.0.0.0/0`` for IPv4 and ``::/0`` for IPv6 if no + value is returned for the address, based on the Ethertype field of + the rule. For further information see + [Bug `1735575 `_] From 340f25fa14d42205a4134ce4cba47792764b8542 Mon Sep 17 00:00:00 2001 From: "zhu.boxiang" Date: Mon, 8 Jul 2019 14:48:23 +0800 Subject: [PATCH 0108/1120] Add host and hypervisor_hostname to create servers Adds the --host and --hypervisor-hostname options to ``openstack server create`` CLI. Depends-On: https://review.opendev.org/670558 Change-Id: If188c3d96fa506dbe62ef256418f2f9bca1520c2 Blueprint: add-host-and-hypervisor-hostname-flag-to-create-server --- lower-constraints.txt | 2 +- openstackclient/compute/v2/server.py | 29 +++ .../tests/unit/compute/v2/test_server.py | 231 ++++++++++++++++++ ...lag-to-create-server-cb8b39a9f9311d42.yaml | 6 + requirements.txt | 2 +- 5 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-cb8b39a9f9311d42.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index f387827948..c3dc477eaf 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -100,7 +100,7 @@ python-mimeparse==1.6.0 python-mistralclient==3.1.0 python-muranoclient==0.8.2 python-neutronclient==6.7.0 -python-novaclient==14.1.0 +python-novaclient==14.2.0 python-octaviaclient==1.3.0 python-rsdclient==0.1.0 python-saharaclient==1.4.0 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 2792e315f6..240c7cb2a0 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -553,6 +553,20 @@ def get_parser(self, prog_name): metavar='', help=_('Select an availability zone for the server'), ) + parser.add_argument( + '--host', + metavar='', + help=_('Requested host to create servers. Admin only ' + 'by default. (supported by --os-compute-api-version 2.74 ' + 'or above)'), + ) + parser.add_argument( + '--hypervisor-hostname', + metavar='', + help=_('Requested hypervisor hostname to create servers. Admin ' + 'only by default. (supported by --os-compute-api-version ' + '2.74 or above)'), + ) parser.add_argument( '--block-device-mapping', metavar='', @@ -927,6 +941,21 @@ def _match_image(image_api, wanted_properties): if parsed_args.description: boot_kwargs['description'] = parsed_args.description + if parsed_args.host: + if compute_client.api_version < api_versions.APIVersion("2.74"): + msg = _("Specifying --host is not supported for " + "--os-compute-api-version less than 2.74") + raise exceptions.CommandError(msg) + boot_kwargs['host'] = parsed_args.host + + if parsed_args.hypervisor_hostname: + if compute_client.api_version < api_versions.APIVersion("2.74"): + msg = _("Specifying --hypervisor-hostname is not supported " + "for --os-compute-api-version less than 2.74") + raise exceptions.CommandError(msg) + boot_kwargs['hypervisor_hostname'] = ( + parsed_args.hypervisor_hostname) + LOG.debug('boot_args: %s', boot_args) LOG.debug('boot_kwargs: %s', boot_kwargs) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 8ea59a3850..dc7ff1aa3f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1926,6 +1926,237 @@ def test_server_create_with_description_api_older(self): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_create_with_host_v274(self): + + # Explicit host is supported for nova api version 2.74 or above + self.app.client_manager.compute.api_version = 2.74 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--host', 'host1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('host', 'host1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.74): + # In base command class ShowOne in cliff, abstract method + # take_action() returns a two-part tuple with a tuple of + # column names and a tuple of data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + host='host1', + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_host_pre_v274(self): + + # Host is not supported for nova api version below 2.74 + self.app.client_manager.compute.api_version = 2.73 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--host', 'host1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('host', 'host1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.74): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_server_create_with_hypervisor_hostname_v274(self): + + # Explicit hypervisor_hostname is supported for nova api version + # 2.74 or above + self.app.client_manager.compute.api_version = 2.74 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hypervisor-hostname', 'node1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('hypervisor_hostname', 'node1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.74): + # In base command class ShowOne in cliff, abstract method + # take_action() returns a two-part tuple with a tuple of + # column names and a tuple of data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + hypervisor_hostname='node1', + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_hypervisor_hostname_pre_v274(self): + + # Hypervisor_hostname is not supported for nova api version below 2.74 + self.app.client_manager.compute.api_version = 2.73 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hypervisor-hostname', 'node1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('hypervisor_hostname', 'node1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.74): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_server_create_with_host_and_hypervisor_hostname_v274(self): + + # Explicit host and hypervisor_hostname is supported for nova api + # version 2.74 or above + self.app.client_manager.compute.api_version = 2.74 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--host', 'host1', + '--hypervisor-hostname', 'node1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('host', 'host1'), + ('hypervisor_hostname', 'node1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.74): + # In base command class ShowOne in cliff, abstract method + # take_action() returns a two-part tuple with a tuple of + # column names and a tuple of data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + host='host1', + hypervisor_hostname='node1', + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + class TestServerDelete(TestServer): diff --git a/releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-cb8b39a9f9311d42.yaml b/releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-cb8b39a9f9311d42.yaml new file mode 100644 index 0000000000..fbd4c70402 --- /dev/null +++ b/releasenotes/notes/add-host-and-hypervisor-hostname-flag-to-create-server-cb8b39a9f9311d42.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--host`` and ``--hypervisor-hostname`` options to + ``server create`` command. + [Blueprint `add-host-and-hypervisor-hostname-flag-to-create-server `_] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3db6caef0b..cf67906646 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.17.0 # Apache-2.0 -python-novaclient>=14.1.0 # Apache-2.0 +python-novaclient>=14.2.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From c609b98c408d521cdd34884c912aeb8f64257753 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 22 Jul 2019 20:55:43 +0200 Subject: [PATCH 0109/1120] Update api-ref location The api documentation is now published on docs.openstack.org instead of developer.openstack.org. Update all links that are changed to the new location. Note that redirects will be set up as well but let's point now to the new location. For details, see: http://lists.openstack.org/pipermail/openstack-discuss/2019-July/007828.html Change-Id: I1572a21632740b4d9a233a6a31c49e3bac5394ef --- openstackclient/api/compute_v2.py | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openstackclient/api/compute_v2.py b/openstackclient/api/compute_v2.py index 7dc4e4468a..e30177a240 100644 --- a/openstackclient/api/compute_v2.py +++ b/openstackclient/api/compute_v2.py @@ -145,7 +145,7 @@ def floating_ip_create( ): """Create a new floating ip - https://developer.openstack.org/api-ref/compute/#create-allocate-floating-ip-address + https://docs.openstack.org/api-ref/compute/#create-allocate-floating-ip-address :param pool: Name of floating IP pool """ @@ -170,7 +170,7 @@ def floating_ip_delete( ): """Delete a floating IP - https://developer.openstack.org/api-ref/compute/#delete-deallocate-floating-ip-address + https://docs.openstack.org/api-ref/compute/#delete-deallocate-floating-ip-address :param string floating_ip_id: Floating IP ID @@ -189,7 +189,7 @@ def floating_ip_find( ): """Return a security group given name or ID - https://developer.openstack.org/api-ref/compute/#list-floating-ip-addresses + https://docs.openstack.org/api-ref/compute/#list-floating-ip-addresses :param string floating_ip: Floating IP address @@ -209,7 +209,7 @@ def floating_ip_list( ): """Get floating IPs - https://developer.openstack.org/api-ref/compute/#show-floating-ip-address-details + https://docs.openstack.org/api-ref/compute/#show-floating-ip-address-details :returns: list of floating IPs @@ -258,7 +258,7 @@ def floating_ip_pool_list( ): """Get floating IP pools - https://developer.openstack.org/api-ref/compute/?expanded=#list-floating-ip-pools + https://docs.openstack.org/api-ref/compute/?expanded=#list-floating-ip-pools :returns: list of floating IP pools @@ -276,7 +276,7 @@ def host_list( ): """Lists hypervisor Hosts - https://developer.openstack.org/api-ref/compute/#list-hosts + https://docs.openstack.org/api-ref/compute/#list-hosts Valid for Compute 2.0 - 2.42 :param string zone: @@ -299,7 +299,7 @@ def host_set( ): """Modify host properties - https://developer.openstack.org/api-ref/compute/#update-host-status + https://docs.openstack.org/api-ref/compute/#update-host-status Valid for Compute 2.0 - 2.42 status @@ -329,7 +329,7 @@ def host_show( ): """Show host - https://developer.openstack.org/api-ref/compute/#show-host-details + https://docs.openstack.org/api-ref/compute/#show-host-details Valid for Compute 2.0 - 2.42 """ @@ -356,7 +356,7 @@ def network_create( ): """Create a new network - https://developer.openstack.org/api-ref/compute/#create-network + https://docs.openstack.org/api-ref/compute/#create-network :param string name: Network label (required) @@ -387,7 +387,7 @@ def network_delete( ): """Delete a network - https://developer.openstack.org/api-ref/compute/#delete-network + https://docs.openstack.org/api-ref/compute/#delete-network :param string network: Network name or ID @@ -411,7 +411,7 @@ def network_find( ): """Return a network given name or ID - https://developer.openstack.org/api-ref/compute/#show-network-details + https://docs.openstack.org/api-ref/compute/#show-network-details :param string network: Network name or ID @@ -431,7 +431,7 @@ def network_list( ): """Get networks - https://developer.openstack.org/api-ref/compute/#list-networks + https://docs.openstack.org/api-ref/compute/#list-networks :returns: list of networks @@ -450,7 +450,7 @@ def security_group_create( ): """Create a new security group - https://developer.openstack.org/api-ref/compute/#create-security-group + https://docs.openstack.org/api-ref/compute/#create-security-group :param string name: Security group name @@ -476,7 +476,7 @@ def security_group_delete( ): """Delete a security group - https://developer.openstack.org/api-ref/compute/#delete-security-group + https://docs.openstack.org/api-ref/compute/#delete-security-group :param string security_group: Security group name or ID @@ -500,7 +500,7 @@ def security_group_find( ): """Return a security group given name or ID - https://developer.openstack.org/api-ref/compute/#show-security-group-details + https://docs.openstack.org/api-ref/compute/#show-security-group-details :param string security_group: Security group name or ID @@ -523,7 +523,7 @@ def security_group_list( ): """Get security groups - https://developer.openstack.org/api-ref/compute/#list-security-groups + https://docs.openstack.org/api-ref/compute/#list-security-groups :param integer limit: query return count limit @@ -556,7 +556,7 @@ def security_group_set( ): """Update a security group - https://developer.openstack.org/api-ref/compute/#update-security-group + https://docs.openstack.org/api-ref/compute/#update-security-group :param string security_group: Security group name or ID @@ -600,7 +600,7 @@ def security_group_rule_create( ): """Create a new security group rule - https://developer.openstack.org/api-ref/compute/#create-security-group-rule + https://docs.openstack.org/api-ref/compute/#create-security-group-rule :param string security_group_id: Security group ID @@ -643,7 +643,7 @@ def security_group_rule_delete( ): """Delete a security group rule - https://developer.openstack.org/api-ref/compute/#delete-security-group-rule + https://docs.openstack.org/api-ref/compute/#delete-security-group-rule :param string security_group_rule_id: Security group rule ID From b52a831f6b92daaba100e09299ec967679adf308 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 24 Jul 2019 12:03:01 -0400 Subject: [PATCH 0110/1120] Mention compute service set --up|--down requires 2.11 or greater This simply updates the docs for the compute service set --up and --down options to mention that --os-compute-api-version 2.11 or greater is required to use those options. Change-Id: I52891fe36c84d0df3e868ab4f3c8e2357e9ba529 --- doc/source/cli/command-objects/compute-service.rst | 5 +++-- openstackclient/compute/v2/service.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/source/cli/command-objects/compute-service.rst b/doc/source/cli/command-objects/compute-service.rst index 51d5c86433..45e781e298 100644 --- a/doc/source/cli/command-objects/compute-service.rst +++ b/doc/source/cli/command-objects/compute-service.rst @@ -74,11 +74,12 @@ Set compute service properties .. option:: --up - Force up service + Force up service. Requires ``--os-compute-api-version`` 2.11 or greater. .. option:: --down - Force down service + Force down service. . Requires ``--os-compute-api-version`` 2.11 or + greater. .. _compute_service_set-host: .. describe:: diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 98347c9f86..f59f85de26 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -152,12 +152,14 @@ def get_parser(self, prog_name): up_down_group.add_argument( '--up', action='store_true', - help=_('Force up service'), + help=_('Force up service. Requires ``--os-compute-api-version`` ' + '2.11 or greater.'), ) up_down_group.add_argument( '--down', action='store_true', - help=_('Force down service'), + help=_('Force down service. Requires ``--os-compute-api-version`` ' + '2.11 or greater.'), ) return parser From 68809fce5a1073659001a87aee4f9407affd5d0e Mon Sep 17 00:00:00 2001 From: zhouhenglc Date: Tue, 7 May 2019 10:52:49 +0800 Subject: [PATCH 0111/1120] openstack port create support --extra-dhcp-option neutron create-port API has extra_dhcp_opts parameter, this parameter can set port with special extra dhcp options. Change-Id: I199f17e95c509a33f809ac85c65f685a37acd198 --- openstackclient/network/v2/port.py | 30 ++++++++++++- .../tests/unit/network/v2/test_port.py | 42 +++++++++++++++++++ ...h-extra-dhcp-options-c2c40e4002b52e2a.yaml | 6 +++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/allow-port-create-with-extra-dhcp-options-c2c40e4002b52e2a.yaml diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index d42fbb683f..e6bfe8537f 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -280,6 +280,19 @@ def _convert_address_pairs(parsed_args): return ops +def _convert_extra_dhcp_options(parsed_args): + dhcp_options = [] + for opt in parsed_args.extra_dhcp_options: + option = {} + option['opt_name'] = opt['name'] + if 'value' in opt: + option['opt_value'] = opt['value'] + if 'ip-version' in opt: + option['ip_version'] = opt['ip-version'] + dhcp_options.append(option) + return dhcp_options + + class CreatePort(command.ShowOne): _description = _("Create a new port") @@ -350,8 +363,18 @@ def get_parser(self, prog_name): metavar='', help=_("Name of this port") ) - # TODO(singhj): Add support for extended options: - # dhcp + parser.add_argument( + '--extra-dhcp-option', + metavar='name=[,value=,ip-version={4,6}]', + default=[], + action=parseractions.MultiKeyValueCommaAction, + dest='extra_dhcp_options', + required_keys=['name'], + optional_keys=['value', "ip-version"], + help=_('Extra DHCP options to be assigned to this port: ' + 'name=[,value=,ip-version={4,6}] ' + '(repeat option to set multiple extra DHCP options)')) + secgroups = parser.add_mutually_exclusive_group() secgroups.add_argument( '--security-group', @@ -425,6 +448,9 @@ def take_action(self, parsed_args): attrs['allowed_address_pairs'] = ( _convert_address_pairs(parsed_args)) + if parsed_args.extra_dhcp_options: + attrs["extra_dhcp_opts"] = _convert_extra_dhcp_options(parsed_args) + if parsed_args.qos_policy: attrs['qos_policy_id'] = client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False).id diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index acf85f4652..c30d682f59 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -611,6 +611,48 @@ def test_create_with_uplink_status_propagation_enabled(self): def test_create_with_uplink_status_propagation_disabled(self): self._test_create_with_uplink_status_propagation(enable=False) + def test_create_port_with_extra_dhcp_option(self): + extra_dhcp_options = [{'opt_name': 'classless-static-route', + 'opt_value': '169.254.169.254/32,22.2.0.2,' + '0.0.0.0/0,22.2.0.1', + 'ip_version': '4'}, + {'opt_name': 'dns-server', + 'opt_value': '240C::6666', + 'ip_version': '6'}] + arglist = [ + '--network', self._port.network_id, + '--extra-dhcp-option', 'name=classless-static-route,' + 'value=169.254.169.254/32,22.2.0.2,' + '0.0.0.0/0,22.2.0.1,' + 'ip-version=4', + '--extra-dhcp-option', 'name=dns-server,value=240C::6666,' + 'ip-version=6', + 'test-port', + ] + + verifylist = [ + ('network', self._port.network_id,), + ('extra_dhcp_options', [{'name': 'classless-static-route', + 'value': '169.254.169.254/32,22.2.0.2,' + '0.0.0.0/0,22.2.0.1', + 'ip-version': '4'}, + {'name': 'dns-server', + 'value': '240C::6666', + 'ip-version': '6'}]), + ('name', 'test-port'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'extra_dhcp_opts': extra_dhcp_options, + 'name': 'test-port', + }) + class TestDeletePort(TestPort): diff --git a/releasenotes/notes/allow-port-create-with-extra-dhcp-options-c2c40e4002b52e2a.yaml b/releasenotes/notes/allow-port-create-with-extra-dhcp-options-c2c40e4002b52e2a.yaml new file mode 100644 index 0000000000..ed4fa1bca0 --- /dev/null +++ b/releasenotes/notes/allow-port-create-with-extra-dhcp-options-c2c40e4002b52e2a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--extra-dhcp-options`` parameter to the ``port create`` command. The + neutronclient ``port-create`` command can accept extra DHCP options, add + it to the openstackclient in order to be consistent. \ No newline at end of file From 1bad2cb3c34a2f39e5006f732978c72e9c2caa5a Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Fri, 26 Jul 2019 17:35:21 +0900 Subject: [PATCH 0112/1120] Fix module paths for volumev3 volume backup commands https://review.opendev.org/#/c/612751/ renamed volume.v2.backup to volume.v2.volume_backup. volume.v3 backup commands refer volume.v2.backup but they were not updated. This causes "volume backup xxx" commands when OS_VOLUME_API_VERSION=3 is specified. Change-Id: Ib897dd483a7963763016ee3f1e8e1c3cc81d0bb0 Story:o 2006284 Task: 35990 --- setup.cfg | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index c48088087f..c57bc41dda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -665,12 +665,12 @@ openstack.volume.v3 = volume_show = openstackclient.volume.v2.volume:ShowVolume volume_unset = openstackclient.volume.v2.volume:UnsetVolume - volume_backup_create = openstackclient.volume.v2.backup:CreateVolumeBackup - volume_backup_delete = openstackclient.volume.v2.backup:DeleteVolumeBackup - volume_backup_list = openstackclient.volume.v2.backup:ListVolumeBackup - volume_backup_restore = openstackclient.volume.v2.backup:RestoreVolumeBackup - volume_backup_set = openstackclient.volume.v2.backup:SetVolumeBackup - volume_backup_show = openstackclient.volume.v2.backup:ShowVolumeBackup + volume_backup_create = openstackclient.volume.v2.volume_backup:CreateVolumeBackup + volume_backup_delete = openstackclient.volume.v2.volume_backup:DeleteVolumeBackup + volume_backup_list = openstackclient.volume.v2.volume_backup:ListVolumeBackup + volume_backup_restore = openstackclient.volume.v2.volume_backup:RestoreVolumeBackup + volume_backup_set = openstackclient.volume.v2.volume_backup:SetVolumeBackup + volume_backup_show = openstackclient.volume.v2.volume_backup:ShowVolumeBackup volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost From ba0a8e9318d62d21596824d563a3420c8a0f3a7a Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Tue, 30 Jul 2019 11:49:51 -0700 Subject: [PATCH 0113/1120] Fix typo: "to and endpoint" Change-Id: I33b7bbf7f452991d7f066aa6c17e905f5a4ddb05 --- doc/source/cli/command-objects/endpoint.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/command-objects/endpoint.rst b/doc/source/cli/command-objects/endpoint.rst index 030947c229..6d0253278b 100644 --- a/doc/source/cli/command-objects/endpoint.rst +++ b/doc/source/cli/command-objects/endpoint.rst @@ -7,7 +7,7 @@ Identity v2, v3 endpoint add project -------------------- -Associate a project to and endpoint for endpoint filtering +Associate a project to an endpoint for endpoint filtering .. program:: endpoint add project .. code:: bash From 865e182970c9ce42d5be07cd3b81fb5dd1a3e656 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 26 Jul 2019 16:41:04 -0500 Subject: [PATCH 0114/1120] Make configuration show not require auth The configuration show should not require auth to just display the OSC config object. Changes to make it not require auth have knock-on effects of needing to change a bunch of tests that use it assuming it _does_ require auth so change those to use 'extension list' instead. This sets up further testing of the command line options for changes in behaviour when we switch to straight SDK usage for configuration. Change-Id: I6c52485341214ba401064c0f2d1e2b95fdc225c0 Signed-off-by: Dean Troyer --- openstackclient/common/configuration.py | 20 +++- openstackclient/tests/functional/base.py | 14 ++- .../functional/common/test_configuration.py | 27 +++++ .../tests/unit/integ/cli/test_project.py | 32 +++--- .../tests/unit/integ/cli/test_shell.py | 105 +++++++++++++----- .../notes/config-show-00512dc60882e5c0.yaml | 6 + 6 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 releasenotes/notes/config-show-00512dc60882e5c0.yaml diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 57825bb056..53b30d5fdd 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -25,6 +25,8 @@ class ShowConfiguration(command.ShowOne): _description = _("Display configuration details") + auth_required = False + def get_parser(self, prog_name): parser = super(ShowConfiguration, self).get_parser(prog_name) mask_group = parser.add_mutually_exclusive_group() @@ -45,13 +47,21 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): - auth_plg_name = self.app.client_manager.auth_plugin_name - secret_opts = [o.dest for o in base.get_plugin_options(auth_plg_name) - if o.secret] - info = self.app.client_manager.get_configuration() + + # Assume a default secret list in case we do not have an auth_plugin + secret_opts = ["password", "token"] + + if getattr(self.app.client_manager, "auth_plugin_name", None): + auth_plg_name = self.app.client_manager.auth_plugin_name + secret_opts = [ + o.dest for o in base.get_plugin_options(auth_plg_name) + if o.secret + ] + for key, value in six.iteritems(info.pop('auth', {})): if parsed_args.mask and key.lower() in secret_opts: - value = REDACTED + value = REDACTED info['auth.' + key] = value + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 1414e6bbb7..c34ca39332 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -45,9 +45,17 @@ class TestCase(testtools.TestCase): @classmethod def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): """Executes openstackclient command for the given action.""" - return execute( - 'openstack --os-cloud={cloud} '.format(cloud=cloud) + - cmd, fail_ok=fail_ok) + if cloud is not None: + return execute( + 'openstack --os-cloud={cloud} '.format(cloud=cloud) + cmd, + fail_ok=fail_ok + ) + else: + # Execute command with no auth + return execute( + 'openstack --os-auth-type none ' + cmd, + fail_ok=fail_ok + ) @classmethod def is_service_enabled(cls, service): diff --git a/openstackclient/tests/functional/common/test_configuration.py b/openstackclient/tests/functional/common/test_configuration.py index 63a17d0e77..17e0f45d1f 100644 --- a/openstackclient/tests/functional/common/test_configuration.py +++ b/openstackclient/tests/functional/common/test_configuration.py @@ -37,6 +37,10 @@ def test_configuration_show(self): configuration.REDACTED, cmd_output['auth.password'] ) + self.assertIn( + 'auth.password', + cmd_output.keys(), + ) # Test show --mask cmd_output = json.loads(self.openstack( @@ -65,3 +69,26 @@ def test_configuration_show(self): configuration.REDACTED, cmd_output['auth.password'] ) + + +class ConfigurationTestsNoAuth(base.TestCase): + """Functional test for configuration with no auth""" + + def test_configuration_show(self): + + # Test show without option + raw_output = self.openstack( + 'configuration show', + cloud=None, + ) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_CONFIG_HEADERS) + + cmd_output = json.loads(self.openstack( + 'configuration show -f json', + cloud=None, + )) + self.assertNotIn( + 'auth.password', + cmd_output, + ) diff --git a/openstackclient/tests/unit/integ/cli/test_project.py b/openstackclient/tests/unit/integ/cli/test_project.py index 6a7c6d1bd6..4e707a3762 100644 --- a/openstackclient/tests/unit/integ/cli/test_project.py +++ b/openstackclient/tests/unit/integ/cli/test_project.py @@ -36,10 +36,10 @@ def setUp(self): def test_project_id_env(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -57,10 +57,10 @@ def test_project_id_env(self): def test_project_id_arg(self): _shell = shell.OpenStackShell() - _shell.run("--os-project-id wsx configuration show".split()) + _shell.run("--os-project-id wsx extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -94,10 +94,10 @@ def setUp(self): def test_project_name_env(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -115,10 +115,10 @@ def test_project_name_env(self): def test_project_name_arg(self): _shell = shell.OpenStackShell() - _shell.run("--os-project-name qaz configuration show".split()) + _shell.run("--os-project-name qaz extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -154,10 +154,10 @@ def setUp(self): def test_project_id_env(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -173,10 +173,10 @@ def test_project_id_env(self): def test_project_id_arg(self): _shell = shell.OpenStackShell() - _shell.run("--os-project-id wsx configuration show".split()) + _shell.run("--os-project-id wsx extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -210,10 +210,10 @@ def setUp(self): def test_project_name_env(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -234,10 +234,10 @@ def test_project_name_env(self): def test_project_name_arg(self): _shell = shell.OpenStackShell() - _shell.run("--os-project-name wsx configuration show".split()) + _shell.run("--os-project-name wsx extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( diff --git a/openstackclient/tests/unit/integ/cli/test_shell.py b/openstackclient/tests/unit/integ/cli/test_shell.py index 200f9b1869..2598517113 100644 --- a/openstackclient/tests/unit/integ/cli/test_shell.py +++ b/openstackclient/tests/unit/integ/cli/test_shell.py @@ -31,6 +31,51 @@ CONFIG_MOCK_BASE = "os_client_config.config" +class TestIntegShellCliNoAuth(test_base.TestInteg): + + def setUp(self): + super(TestIntegShellCliNoAuth, self).setUp() + env = {} + self.useFixture(osc_lib_utils.EnvFixture(copy.deepcopy(env))) + + # self.token = test_base.make_v2_token(self.requests_mock) + + def test_shell_args_no_options(self): + _shell = shell.OpenStackShell() + _shell.run("configuration show".split()) + + # Check general calls + self.assertEqual(len(self.requests_mock.request_history), 0) + + def test_shell_args_verify(self): + _shell = shell.OpenStackShell() + _shell.run("--verify configuration show".split()) + + # Check general calls + self.assertEqual(len(self.requests_mock.request_history), 0) + + def test_shell_args_insecure(self): + _shell = shell.OpenStackShell() + _shell.run("--insecure configuration show".split()) + + # Check general calls + self.assertEqual(len(self.requests_mock.request_history), 0) + + def test_shell_args_cacert(self): + _shell = shell.OpenStackShell() + _shell.run("--os-cacert xyzpdq configuration show".split()) + + # Check general calls + self.assertEqual(len(self.requests_mock.request_history), 0) + + def test_shell_args_cacert_insecure(self): + _shell = shell.OpenStackShell() + _shell.run("--os-cacert xyzpdq --insecure configuration show".split()) + + # Check general calls + self.assertEqual(len(self.requests_mock.request_history), 0) + + class TestIntegShellCliV2(test_base.TestInteg): def setUp(self): @@ -48,10 +93,10 @@ def setUp(self): def test_shell_args_no_options(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -77,30 +122,30 @@ def test_shell_args_no_options(self): def test_shell_args_verify(self): _shell = shell.OpenStackShell() - _shell.run("--verify configuration show".split()) + _shell.run("--verify extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertTrue(self.requests_mock.request_history[0].verify) def test_shell_args_insecure(self): _shell = shell.OpenStackShell() - _shell.run("--insecure configuration show".split()) + _shell.run("--insecure extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertFalse(self.requests_mock.request_history[0].verify) def test_shell_args_cacert(self): _shell = shell.OpenStackShell() - _shell.run("--os-cacert xyzpdq configuration show".split()) + _shell.run("--os-cacert xyzpdq extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertEqual( @@ -110,10 +155,10 @@ def test_shell_args_cacert(self): def test_shell_args_cacert_insecure(self): _shell = shell.OpenStackShell() - _shell.run("--os-cacert xyzpdq --insecure configuration show".split()) + _shell.run("--os-cacert xyzpdq --insecure extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertFalse(self.requests_mock.request_history[0].verify) @@ -138,10 +183,10 @@ def setUp(self): def test_shell_args_ignore_v3(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -184,10 +229,10 @@ def setUp(self): def test_shell_args_no_options(self): _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -213,30 +258,30 @@ def test_shell_args_no_options(self): def test_shell_args_verify(self): _shell = shell.OpenStackShell() - _shell.run("--verify configuration show".split()) + _shell.run("--verify extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertTrue(self.requests_mock.request_history[0].verify) def test_shell_args_insecure(self): _shell = shell.OpenStackShell() - _shell.run("--insecure configuration show".split()) + _shell.run("--insecure extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertFalse(self.requests_mock.request_history[0].verify) def test_shell_args_cacert(self): _shell = shell.OpenStackShell() - _shell.run("--os-cacert xyzpdq configuration show".split()) + _shell.run("--os-cacert xyzpdq extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertEqual( @@ -248,10 +293,10 @@ def test_shell_args_cacert_insecure(self): # This test verifies the outcome of bug 1447784 # https://bugs.launchpad.net/python-openstackclient/+bug/1447784 _shell = shell.OpenStackShell() - _shell.run("--os-cacert xyzpdq --insecure configuration show".split()) + _shell.run("--os-cacert xyzpdq --insecure extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check verify self.assertFalse(self.requests_mock.request_history[0].verify) @@ -276,10 +321,10 @@ def setUp(self): def test_shell_callback(self, mock_prompt): mock_prompt.return_value = "qaz" _shell = shell.OpenStackShell() - _shell.run("configuration show".split()) + _shell.run("extension list".split()) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check password callback set correctly self.assertEqual( @@ -332,11 +377,11 @@ def test_shell_args_options(self): _shell = shell.OpenStackShell() _shell.run( "--os-username zarquon --os-password qaz " - "configuration show".split(), + "extension list".split(), ) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -431,11 +476,11 @@ def vendor_mock_return(): print("CONFIG_MOCK_BASE=%s" % CONFIG_MOCK_BASE) _shell = shell.OpenStackShell() _shell.run( - "--os-password qaz configuration show".split(), + "--os-password qaz extension list".split(), ) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( @@ -504,11 +549,11 @@ def vendor_mock_return(): _shell = shell.OpenStackShell() _shell.run( "--os-username zarquon --os-password qaz " - "--os-project-domain-id 5678 configuration show".split(), + "--os-project-domain-id 5678 extension list".split(), ) # Check general calls - self.assertEqual(len(self.requests_mock.request_history), 2) + self.assertNotEqual(len(self.requests_mock.request_history), 0) # Check discovery request self.assertEqual( diff --git a/releasenotes/notes/config-show-00512dc60882e5c0.yaml b/releasenotes/notes/config-show-00512dc60882e5c0.yaml new file mode 100644 index 0000000000..da57dff7f9 --- /dev/null +++ b/releasenotes/notes/config-show-00512dc60882e5c0.yaml @@ -0,0 +1,6 @@ +--- +other: + - | + The ``configuration show`` command no longer requires authentication. + This is useful in debugging cloud configurations, particularly + auth configurations. From 7561e062ebd80a28655523320cbe1a49e94ee509 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 13 Jun 2019 17:44:28 +0100 Subject: [PATCH 0115/1120] Add 'openstack server resize (confirm|revert)' commands These are currently exposed as flags on the 'openstack server resize' command but they are in fact operation and should be exposed as commands in their own right. The old flag-based variants are deprecated for removal in 4.0. Change-Id: I733796d3bda6c3755a3d3548bbe695abb474a6a0 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 55 +++++++++++++ .../tests/unit/compute/v2/test_server.py | 82 ++++++++++++++++++- ...firm-revert-commands-98854ca98965432a.yaml | 12 +++ setup.cfg | 2 + 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-server-resize-confirm-revert-commands-98854ca98965432a.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 3e1deed59a..9882922643 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2096,11 +2096,66 @@ def _show_progress(progress): self.app.stdout.write(_('Error resizing server\n')) raise SystemExit elif parsed_args.confirm: + self.log.warning(_( + "The --confirm option has been deprecated. Please use the " + "'openstack server resize confirm' command instead.")) compute_client.servers.confirm_resize(server) elif parsed_args.revert: + self.log.warning(_( + "The --revert option has been deprecated. Please use the " + "'openstack server resize revert' command instead.")) compute_client.servers.revert_resize(server) +class ResizeConfirm(command.Command): + _description = _("""Confirm server resize. + +Confirm (verify) success of resize operation and release the old server.""") + + def get_parser(self, prog_name): + parser = super(ResizeConfirm, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + + compute_client = self.app.client_manager.compute + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + server.confirm_resize() + + +class ResizeRevert(command.Command): + _description = _("""Revert server resize. + +Revert the resize operation. Release the new server and restart the old +one.""") + + def get_parser(self, prog_name): + parser = super(ResizeRevert, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + + compute_client = self.app.client_manager.compute + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + server.revert_resize() + + class RestoreServer(command.Command): _description = _("Restore server(s)") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 4a37a81216..974be24c45 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3603,13 +3603,18 @@ def test_server_resize_confirm(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) self.assertNotCalled(self.servers_mock.resize) self.servers_mock.confirm_resize.assert_called_with(self.server) self.assertNotCalled(self.servers_mock.revert_resize) self.assertIsNone(result) + # A warning should have been logged for using --confirm. + mock_warning.assert_called_once() + self.assertIn('The --confirm option has been deprecated.', + six.text_type(mock_warning.call_args[0][0])) def test_server_resize_revert(self): arglist = [ @@ -3623,13 +3628,18 @@ def test_server_resize_revert(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) self.assertNotCalled(self.servers_mock.resize) self.assertNotCalled(self.servers_mock.confirm_resize) self.servers_mock.revert_resize.assert_called_with(self.server) self.assertIsNone(result) + # A warning should have been logged for using --revert. + mock_warning.assert_called_once() + self.assertIn('The --revert option has been deprecated.', + six.text_type(mock_warning.call_args[0][0])) @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_server_resize_with_wait_ok(self, mock_wait_for_status): @@ -3710,6 +3720,74 @@ def test_server_resize_with_wait_fails(self, mock_wait_for_status): ) +class TestServerResizeConfirm(TestServer): + + def setUp(self): + super(TestServerResizeConfirm, self).setUp() + + methods = { + 'confirm_resize': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.confirm_resize.return_value = None + + # Get the command object to test + self.cmd = server.ResizeConfirm(self.app, None) + + def test_resize_confirm(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.confirm_resize.assert_called_with() + + +class TestServerResizeRevert(TestServer): + + def setUp(self): + super(TestServerResizeRevert, self).setUp() + + methods = { + 'revert_resize': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.revert_resize.return_value = None + + # Get the command object to test + self.cmd = server.ResizeRevert(self.app, None) + + def test_resize_revert(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.revert_resize.assert_called_with() + + class TestServerRestore(TestServer): def setUp(self): diff --git a/releasenotes/notes/add-server-resize-confirm-revert-commands-98854ca98965432a.yaml b/releasenotes/notes/add-server-resize-confirm-revert-commands-98854ca98965432a.yaml new file mode 100644 index 0000000000..2399a1a659 --- /dev/null +++ b/releasenotes/notes/add-server-resize-confirm-revert-commands-98854ca98965432a.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Add ``server resize confirm`` and ``server resize revert`` commands. + These replace the now deprecated ``--confirm`` and ``--revert`` + options to the ``server resize`` commands, respectively. +deprecations: + - | + Deprecate the ``--confirm`` and ``--revert`` options for the + ``server resize`` command. They have been replaced with the + ``server resize confirm`` and `server resize revert`` commands, + respectively. diff --git a/setup.cfg b/setup.cfg index db03c48423..aa72637184 100644 --- a/setup.cfg +++ b/setup.cfg @@ -124,6 +124,8 @@ openstack.compute.v2 = server_remove_volume = openstackclient.compute.v2.server:RemoveServerVolume server_rescue = openstackclient.compute.v2.server:RescueServer server_resize = openstackclient.compute.v2.server:ResizeServer + server_resize_confirm = openstackclient.compute.v2.server:ResizeConfirm + server_resize_revert = openstackclient.compute.v2.server:ResizeRevert server_restore = openstackclient.compute.v2.server:RestoreServer server_resume = openstackclient.compute.v2.server:ResumeServer server_set = openstackclient.compute.v2.server:SetServer From 4bd53dc1090fda86f6ce25b76a079e250c9206d8 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 24 Jul 2019 14:39:07 -0400 Subject: [PATCH 0116/1120] Fix compute service set handling for 2.53+ With compute API microversion 2.53 there is a single PUT /os-services/{service_id} API which takes the service id as a UUID. Since the openstack compute service set command only takes --host and --service (binary) to identify the service, this change checks if 2.53 or greater is being used and if so, looks up the service by host and binary and calls the appropriate methods in novaclient. If the command cannot uniquely identify a compute service with the given host and binary, an error is raised. A future change could add an --id option to be used with 2.53+ to pass the service id (as UUID) directly to avoid the host/binary filtering. Change-Id: I868e0868e8eb17e7e34eef3d2d58dceedd29c2b0 Story: 2005349 Task: 30302 --- openstackclient/compute/v2/service.py | 63 +++++++++-- .../tests/unit/compute/v2/test_service.py | 104 +++++++++++++++++- ...ute-service-set-2.53-3d2db875154e633a.yaml | 6 + 3 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/story-2005349-compute-service-set-2.53-3d2db875154e633a.yaml diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index f59f85de26..80c0be7ecb 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -163,6 +163,33 @@ def get_parser(self, prog_name): ) return parser + @staticmethod + def _find_service_by_host_and_binary(cs, host, binary): + """Utility method to find a compute service by host and binary + + :param host: the name of the compute service host + :param binary: the compute service binary, e.g. nova-compute + :returns: novaclient.v2.services.Service dict-like object + :raises: CommandError if no or multiple results were found + """ + services = cs.list(host=host, binary=binary) + # Did we find anything? + if not len(services): + msg = _('Compute service for host "%(host)s" and binary ' + '"%(binary)s" not found.') % { + 'host': host, 'binary': binary} + raise exceptions.CommandError(msg) + # Did we find more than one result? This should not happen but let's + # be safe. + if len(services) > 1: + # TODO(mriedem): If we have an --id option for 2.53+ then we can + # say to use that option to uniquely identify the service. + msg = _('Multiple compute services found for host "%(host)s" and ' + 'binary "%(binary)s". Unable to proceed.') % { + 'host': host, 'binary': binary} + raise exceptions.CommandError(msg) + return services[0] + def take_action(self, parsed_args): compute_client = self.app.client_manager.compute cs = compute_client.services @@ -173,6 +200,20 @@ def take_action(self, parsed_args): "--disable specified.") raise exceptions.CommandError(msg) + # Starting with microversion 2.53, there is a single + # PUT /os-services/{service_id} API for updating nova-compute + # services. If 2.53+ is used we need to find the nova-compute + # service using the --host and --service (binary) values. + requires_service_id = ( + compute_client.api_version >= api_versions.APIVersion('2.53')) + service_id = None + if requires_service_id: + # TODO(mriedem): Add an --id option so users can pass the service + # id (as a uuid) directly rather than make us look it up using + # host/binary. + service_id = SetService._find_service_by_host_and_binary( + cs, parsed_args.host, parsed_args.service).id + result = 0 enabled = None try: @@ -183,14 +224,21 @@ def take_action(self, parsed_args): if enabled is not None: if enabled: - cs.enable(parsed_args.host, parsed_args.service) + args = (service_id,) if requires_service_id else ( + parsed_args.host, parsed_args.service) + cs.enable(*args) else: if parsed_args.disable_reason: - cs.disable_log_reason(parsed_args.host, - parsed_args.service, - parsed_args.disable_reason) + args = (service_id, parsed_args.disable_reason) if \ + requires_service_id else ( + parsed_args.host, + parsed_args.service, + parsed_args.disable_reason) + cs.disable_log_reason(*args) else: - cs.disable(parsed_args.host, parsed_args.service) + args = (service_id,) if requires_service_id else ( + parsed_args.host, parsed_args.service) + cs.disable(*args) except Exception: status = "enabled" if enabled else "disabled" LOG.error("Failed to set service status to %s", status) @@ -208,8 +256,9 @@ def take_action(self, parsed_args): 'required') raise exceptions.CommandError(msg) try: - cs.force_down(parsed_args.host, parsed_args.service, - force_down=force_down) + args = (service_id, force_down) if requires_service_id else ( + parsed_args.host, parsed_args.service, force_down) + cs.force_down(*args) except Exception: state = "down" if force_down else "up" LOG.error("Failed to set service state to %s", state) diff --git a/openstackclient/tests/unit/compute/v2/test_service.py b/openstackclient/tests/unit/compute/v2/test_service.py index bd29912341..0d663b2e67 100644 --- a/openstackclient/tests/unit/compute/v2/test_service.py +++ b/openstackclient/tests/unit/compute/v2/test_service.py @@ -17,6 +17,7 @@ from mock import call from novaclient import api_versions from osc_lib import exceptions +import six from openstackclient.compute.v2 import service from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -344,7 +345,7 @@ def test_service_set_state_up(self): '2.11') result = self.cmd.take_action(parsed_args) self.service_mock.force_down.assert_called_once_with( - self.service.host, self.service.binary, force_down=False) + self.service.host, self.service.binary, False) self.assertNotCalled(self.service_mock.enable) self.assertNotCalled(self.service_mock.disable) self.assertIsNone(result) @@ -365,7 +366,7 @@ def test_service_set_state_down(self): '2.11') result = self.cmd.take_action(parsed_args) self.service_mock.force_down.assert_called_once_with( - self.service.host, self.service.binary, force_down=True) + self.service.host, self.service.binary, True) self.assertNotCalled(self.service_mock.enable) self.assertNotCalled(self.service_mock.disable) self.assertIsNone(result) @@ -390,7 +391,7 @@ def test_service_set_enable_and_state_down(self): self.service_mock.enable.assert_called_once_with( self.service.host, self.service.binary) self.service_mock.force_down.assert_called_once_with( - self.service.host, self.service.binary, force_down=True) + self.service.host, self.service.binary, True) self.assertIsNone(result) def test_service_set_enable_and_state_down_with_exception(self): @@ -415,4 +416,99 @@ def test_service_set_enable_and_state_down_with_exception(self): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) self.service_mock.force_down.assert_called_once_with( - self.service.host, self.service.binary, force_down=True) + self.service.host, self.service.binary, True) + + def test_service_set_2_53_disable_down(self): + # Tests disabling and forcing down a compute service with microversion + # 2.53 which requires looking up the service by host and binary. + arglist = [ + '--disable', + '--down', + self.service.host, + self.service.binary, + ] + verifylist = [ + ('disable', True), + ('down', True), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.53') + service_id = '339478d0-0b95-4a94-be63-d5be05dfeb1c' + self.service_mock.list.return_value = [mock.Mock(id=service_id)] + result = self.cmd.take_action(parsed_args) + self.service_mock.disable.assert_called_once_with(service_id) + self.service_mock.force_down.assert_called_once_with(service_id, True) + self.assertIsNone(result) + + def test_service_set_2_53_disable_reason(self): + # Tests disabling with reason a compute service with microversion + # 2.53 which requires looking up the service by host and binary. + reason = 'earthquake' + arglist = [ + '--disable', + '--disable-reason', reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('disable', True), + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.53') + service_id = '339478d0-0b95-4a94-be63-d5be05dfeb1c' + self.service_mock.list.return_value = [mock.Mock(id=service_id)] + result = self.cmd.take_action(parsed_args) + self.service_mock.disable_log_reason.assert_called_once_with( + service_id, reason) + self.assertIsNone(result) + + def test_service_set_2_53_enable_up(self): + # Tests enabling and bringing up a compute service with microversion + # 2.53 which requires looking up the service by host and binary. + arglist = [ + '--enable', + '--up', + self.service.host, + self.service.binary, + ] + verifylist = [ + ('enable', True), + ('up', True), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.53') + service_id = '339478d0-0b95-4a94-be63-d5be05dfeb1c' + self.service_mock.list.return_value = [mock.Mock(id=service_id)] + result = self.cmd.take_action(parsed_args) + self.service_mock.enable.assert_called_once_with(service_id) + self.service_mock.force_down.assert_called_once_with(service_id, False) + self.assertIsNone(result) + + def test_service_set_find_service_by_host_and_binary_no_results(self): + # Tests that no compute services are found by host and binary. + self.service_mock.list.return_value = [] + ex = self.assertRaises(exceptions.CommandError, + self.cmd._find_service_by_host_and_binary, + self.service_mock, 'fake-host', 'nova-compute') + self.assertIn('Compute service for host "fake-host" and binary ' + '"nova-compute" not found.', six.text_type(ex)) + + def test_service_set_find_service_by_host_and_binary_many_results(self): + # Tests that more than one compute service is found by host and binary. + self.service_mock.list.return_value = [mock.Mock(), mock.Mock()] + ex = self.assertRaises(exceptions.CommandError, + self.cmd._find_service_by_host_and_binary, + self.service_mock, 'fake-host', 'nova-compute') + self.assertIn('Multiple compute services found for host "fake-host" ' + 'and binary "nova-compute". Unable to proceed.', + six.text_type(ex)) diff --git a/releasenotes/notes/story-2005349-compute-service-set-2.53-3d2db875154e633a.yaml b/releasenotes/notes/story-2005349-compute-service-set-2.53-3d2db875154e633a.yaml new file mode 100644 index 0000000000..cff3196014 --- /dev/null +++ b/releasenotes/notes/story-2005349-compute-service-set-2.53-3d2db875154e633a.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The ``compute service set`` command now properly handles + ``--os-compute-api-version`` 2.53 and greater. + [Story `2005349 `_] From 6a199bd14152889f18ad95919a4bee9c0c083d5d Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 29 Jul 2019 12:59:52 -0400 Subject: [PATCH 0117/1120] Support type=image with --block-device-mapping option The --block-device-mapping option on the server create command currently only supports booting from volume and volume snapshot. A common boot-from-volume scenario is providing an image and letting nova orchestrate the creation of the image-backed volume and attaching it to the server. This adds support for type=image in the --block-device-mapping option. The volume size is required in this case. Note that the CLI currently says if type=snapshot that size is also required but that's technically not true. When booting from a volume snapshot, the compute API will use the size of the volume snapshot to create the volume if an explicit size is not provided. For the purposes of this patch, we need the size anyway for the image being the block device mapping source type. Change-Id: I57b3c261d8309f7b9f62a3e91612bce592a887a3 Story: 2006302 Task: 36016 --- openstackclient/compute/v2/server.py | 30 +++++-- .../functional/compute/v2/test_server.py | 87 +++++++++++++++++++ ...pport-bdm-type-image-0becfb63bd4fb969.yaml | 8 ++ 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ea87d9cea1..a0afa389f6 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -575,14 +575,17 @@ def get_parser(self, prog_name): # NOTE(RuiChen): Add '\n' at the end of line to put each item in # the separated line, avoid the help message looks # messy, see _SmartHelpFormatter in cliff. + # FIXME(mriedem): Technically can be the name or ID. help=_('Create a block device on the server.\n' 'Block device mapping in the format\n' '=:::\n' ': block device name, like: vdb, xvdc ' '(required)\n' - ': UUID of the volume or snapshot (required)\n' - ': volume or snapshot; default: volume (optional)\n' - ': volume size if create from snapshot ' + ': UUID of the volume, volume snapshot or image ' + '(required)\n' + ': volume, snapshot or image; default: volume ' + '(optional)\n' + ': volume size if create from image or snapshot ' '(optional)\n' ': true or false; default: false ' '(optional)\n' @@ -793,7 +796,7 @@ def _match_image(image_api, wanted_properties): mapping = {'device_name': dev_name} # 1. decide source and destination type if (len(dev_map) > 1 and - dev_map[1] in ('volume', 'snapshot')): + dev_map[1] in ('volume', 'snapshot', 'image')): mapping['source_type'] = dev_map[1] else: mapping['source_type'] = 'volume' @@ -808,14 +811,29 @@ def _match_image(image_api, wanted_properties): snapshot_id = utils.find_resource( volume_client.volume_snapshots, dev_map[0]).id mapping['uuid'] = snapshot_id + elif mapping['source_type'] == 'image': + # NOTE(mriedem): In case --image is specified with the same + # image, that becomes the root disk for the server. If the + # block device is specified with a root device name, e.g. + # vda, then the compute API will likely fail complaining + # that there is a conflict. So if using the same image ID, + # which doesn't really make sense but it's allowed, the + # device name would need to be a non-root device, e.g. vdb. + # Otherwise if the block device image is different from the + # one specified by --image, then the compute service will + # create a volume from the image and attach it to the + # server as a non-root volume. + image_id = utils.find_resource( + image_client.images, dev_map[0]).id + mapping['uuid'] = image_id # 3. append size and delete_on_termination if exist if len(dev_map) > 2 and dev_map[2]: mapping['volume_size'] = dev_map[2] if len(dev_map) > 3 and dev_map[3]: mapping['delete_on_termination'] = dev_map[3] else: - msg = _("Volume or snapshot (name or ID) must be specified if " - "--block-device-mapping is specified") + msg = _("Volume, volume snapshot or image (name or ID) must " + "be specified if --block-device-mapping is specified") raise exceptions.CommandError(msg) block_device_mapping_v2.append(mapping) diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index e52a42d3a1..67e2a66ef8 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -614,6 +614,93 @@ def test_server_boot_with_bdm_snapshot(self): # the attached volume had been deleted pass + def test_server_boot_with_bdm_image(self): + # Tests creating a server where the root disk is backed by the given + # --image but a --block-device-mapping with type=image is provided so + # that the compute service creates a volume from that image and + # attaches it as a non-root volume on the server. The block device is + # marked as delete_on_termination=True so it will be automatically + # deleted when the server is deleted. + + # create server with bdm type=image + # NOTE(mriedem): This test is a bit unrealistic in that specifying the + # same image in the block device as the --image option does not really + # make sense, but we just want to make sure everything is processed + # as expected where nova creates a volume from the image and attaches + # that volume to the server. + server_name = uuid.uuid4().hex + server = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--block-device-mapping ' + # This means create a 1GB volume from the specified image, attach + # it to the server at /dev/vdb and delete the volume when the + # server is deleted. + 'vdb=' + self.image_name + ':image:1:true ' + + self.network_arg + ' ' + + '--wait ' + + server_name + )) + self.assertIsNotNone(server["id"]) + self.assertEqual( + server_name, + server['name'], + ) + self.wait_for_status(server_name, 'ACTIVE') + + # check server volumes_attached, format is + # {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",} + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + server_name + )) + volumes_attached = cmd_output['volumes_attached'] + self.assertTrue(volumes_attached.startswith('id=')) + attached_volume_id = volumes_attached.replace('id=', '') + + # check the volume that attached on server + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + attached_volume_id + )) + attachments = cmd_output['attachments'] + self.assertEqual( + 1, + len(attachments), + ) + self.assertEqual( + server['id'], + attachments[0]['server_id'], + ) + self.assertEqual( + "in-use", + cmd_output['status'], + ) + # TODO(mriedem): If we can parse the volume_image_metadata field from + # the volume show output we could assert the image_name is what we + # specified. volume_image_metadata is something like this: + # {u'container_format': u'bare', u'min_ram': u'0', + # u'disk_format': u'qcow2', u'image_name': u'cirros-0.4.0-x86_64-disk', + # u'image_id': u'05496c83-e2df-4c2f-9e48-453b6e49160d', + # u'checksum': u'443b7623e27ecf03dc9e01ee93f67afe', u'min_disk': u'0', + # u'size': u'12716032'} + + # delete server, then check the attached volume has been deleted + self.openstack('server delete --wait ' + server_name) + cmd_output = json.loads(self.openstack( + 'volume list -f json' + )) + target_volume = [each_volume + for each_volume in cmd_output + if each_volume['ID'] == attached_volume_id] + if target_volume: + # check the attached volume is 'deleting' status + self.assertEqual('deleting', target_volume[0]['Status']) + else: + # the attached volume had been deleted + pass + def test_server_create_with_none_network(self): """Test server create with none network option.""" server_name = uuid.uuid4().hex diff --git a/releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml b/releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml new file mode 100644 index 0000000000..6e8e8ba0b8 --- /dev/null +++ b/releasenotes/notes/story-2006302-support-bdm-type-image-0becfb63bd4fb969.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``server create --block-device-mapping`` option now supports + an ``image`` type in addition to ``volume`` and ``snapshot``. When + specifying an image block device the compute service will create a volume + from the image of the specified size and attach it to the server. + [Story `2006302 `_] From c28ed25e3a6f2d9cf5fc349c544354a9d07aa957 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 1 Aug 2019 14:15:32 -0400 Subject: [PATCH 0118/1120] Fix description for --block-device-mapping The portion of a --block-device-mapping value can be the resource name or id since the code uses the appropriate type-specific find_resource utility to lookup the resource based on the value given. This change simply fixes the description of to mention it's name or id rather than just "UUID". My guess is the description was originally copied from novaclient where id must be an id since name resolution does not happen in novaclient. Change-Id: I567f6f6efb3a3b6d387133d21aa81354b2d753bc --- openstackclient/compute/v2/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a0afa389f6..fee2b27d4a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -575,13 +575,12 @@ def get_parser(self, prog_name): # NOTE(RuiChen): Add '\n' at the end of line to put each item in # the separated line, avoid the help message looks # messy, see _SmartHelpFormatter in cliff. - # FIXME(mriedem): Technically can be the name or ID. help=_('Create a block device on the server.\n' 'Block device mapping in the format\n' '=:::\n' ': block device name, like: vdb, xvdc ' '(required)\n' - ': UUID of the volume, volume snapshot or image ' + ': Name or ID of the volume, volume snapshot or image ' '(required)\n' ': volume, snapshot or image; default: volume ' '(optional)\n' From b9d63105566c84db11a976846844ad7b3a0b331e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 1 Aug 2019 15:11:29 -0400 Subject: [PATCH 0119/1120] Add openstack server create --boot-from-volume option This adds a --boot-from-volume option to the server create command which is used with the --image or --image-property option and will create a volume-backed server from the specified image with the specified size. Similar to the --volume option, the created root volume will not be deleted when the server is deleted. The --boot-from-volume option is not allowed with the --volume option since they both create a block device mapping with boot_index=0. Change-Id: I88c590361cb232c1df7b5bb010dcea307080d34c Story: 2006302 Task: 36017 --- openstackclient/compute/v2/server.py | 33 +++++++- .../functional/compute/v2/test_server.py | 76 +++++++++++++++++++ .../tests/unit/compute/v2/test_server.py | 27 +++++++ ...add-boot-from-volume-cd411b1ca776bb3c.yaml | 8 ++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index fee2b27d4a..95c2f28a81 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -567,6 +567,19 @@ def get_parser(self, prog_name): 'only by default. (supported by --os-compute-api-version ' '2.74 or above)'), ) + parser.add_argument( + '--boot-from-volume', + metavar='', + type=int, + help=_('When used in conjunction with the ``--image`` or ' + '``--image-property`` option, this option automatically ' + 'creates a block device mapping with a boot index of 0 ' + 'and tells the compute service to create a volume of the ' + 'given size (in GB) from the specified image and use it ' + 'as the root disk of the server. The root volume will not ' + 'be deleted when the server is deleted. This option is ' + 'mutually exclusive with the ``--volume`` option.') + ) parser.add_argument( '--block-device-mapping', metavar='', @@ -730,6 +743,10 @@ def _match_image(image_api, wanted_properties): # Lookup parsed_args.volume volume = None if parsed_args.volume: + # --volume and --boot-from-volume are mutually exclusive. + if parsed_args.boot_from_volume: + raise exceptions.CommandError( + _('--volume is not allowed with --boot-from-volume')) volume = utils.find_resource( volume_client.volumes, parsed_args.volume, @@ -739,8 +756,6 @@ def _match_image(image_api, wanted_properties): flavor = utils.find_resource(compute_client.flavors, parsed_args.flavor) - boot_args = [parsed_args.server_name, image, flavor] - files = {} for f in parsed_args.file: dst, src = f.split('=', 1) @@ -787,6 +802,20 @@ def _match_image(image_api, wanted_properties): 'source_type': 'volume', 'destination_type': 'volume' }] + elif parsed_args.boot_from_volume: + # Tell nova to create a root volume from the image provided. + block_device_mapping_v2 = [{ + 'uuid': image.id, + 'boot_index': '0', + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': parsed_args.boot_from_volume + }] + # If booting from volume we do not pass an image to compute. + image = None + + boot_args = [parsed_args.server_name, image, flavor] + # Handle block device by device name order, like: vdb -> vdc -> vdd for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)): dev_map = parsed_args.block_device_mapping[dev_name] diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 67e2a66ef8..2bca70d0ec 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -701,6 +701,82 @@ def test_server_boot_with_bdm_image(self): # the attached volume had been deleted pass + def test_boot_from_volume(self): + # Tests creating a server using --image and --boot-from-volume where + # the compute service will create a root volume of the specified size + # using the provided image, attach it as the root disk for the server + # and not delete the volume when the server is deleted. + server_name = uuid.uuid4().hex + server = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--boot-from-volume 1 ' + # create a 1GB volume from the image + self.network_arg + ' ' + + '--wait ' + + server_name + )) + self.assertIsNotNone(server["id"]) + self.assertEqual( + server_name, + server['name'], + ) + self.wait_for_status(server_name, 'ACTIVE') + + # check server volumes_attached, format is + # {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",} + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + server_name + )) + volumes_attached = cmd_output['volumes_attached'] + self.assertTrue(volumes_attached.startswith('id=')) + attached_volume_id = volumes_attached.replace('id=', '') + # Don't leak the volume when the test exits. + self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id) + + # Since the server is volume-backed the GET /servers/{server_id} + # response will have image=''. + self.assertEqual('', cmd_output['image']) + + # check the volume that attached on server + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + attached_volume_id + )) + # The volume size should be what we specified on the command line. + self.assertEqual(1, int(cmd_output['size'])) + attachments = cmd_output['attachments'] + self.assertEqual( + 1, + len(attachments), + ) + self.assertEqual( + server['id'], + attachments[0]['server_id'], + ) + self.assertEqual( + "in-use", + cmd_output['status'], + ) + # TODO(mriedem): If we can parse the volume_image_metadata field from + # the volume show output we could assert the image_name is what we + # specified. volume_image_metadata is something like this: + # {u'container_format': u'bare', u'min_ram': u'0', + # u'disk_format': u'qcow2', u'image_name': u'cirros-0.4.0-x86_64-disk', + # u'image_id': u'05496c83-e2df-4c2f-9e48-453b6e49160d', + # u'checksum': u'443b7623e27ecf03dc9e01ee93f67afe', u'min_disk': u'0', + # u'size': u'12716032'} + + # delete server, then check the attached volume was not deleted + self.openstack('server delete --wait ' + server_name) + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + attached_volume_id + )) + # check the volume is in 'available' status + self.assertEqual('available', cmd_output['status']) + def test_server_create_with_none_network(self): """Test server create with none network option.""" server_name = uuid.uuid4().hex diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index cd12db066f..ae6b6e40f4 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1698,6 +1698,33 @@ def test_server_create_with_block_device_mapping_no_uuid(self): self.cmd.take_action, parsed_args) + def test_server_create_volume_boot_from_volume_conflict(self): + # Tests that specifying --volume and --boot-from-volume results in + # an error. Since --boot-from-volume requires --image or + # --image-property but those are in a mutex group with --volume, we + # only specify --volume and --boot-from-volume for this test since + # the validation is not handled with argparse. + arglist = [ + '--flavor', self.flavor.id, + '--volume', 'volume1', + '--boot-from-volume', '1', + self.new_server.name, + ] + verifylist = [ + ('flavor', self.flavor.id), + ('volume', 'volume1'), + ('boot_from_volume', 1), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + # Assert it is the error we expect. + self.assertIn('--volume is not allowed with --boot-from-volume', + six.text_type(ex)) + def test_server_create_image_property(self): arglist = [ '--image-property', 'hypervisor_type=qemu', diff --git a/releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml b/releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml new file mode 100644 index 0000000000..1176c883f1 --- /dev/null +++ b/releasenotes/notes/story-2006302-add-boot-from-volume-cd411b1ca776bb3c.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``--boot-from-volume`` option to the ``server create`` command + to create a volume-backed server from the specified image with the + specified size when used in conjunction with the ``--image`` or + ``--image-property`` options. + [Story `2006302 `_] From 1557afb554e908e097abd39081891ea78083e20e Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 25 Jul 2019 14:28:43 -0400 Subject: [PATCH 0120/1120] Document 2.53 behavior for compute service list/delete With compute API microversion 2.53, nova-compute services can only be deleted with the ID as a UUID to uniquely identify the service in a multi-cell deployment. This documents that for the "compute service delete " argument. The description of the "compute service list" command is also updated to mention that the ID can be retrieved as a UUID using 2.53 or greater. Change-Id: If7d4a27c0aaef588bcd77dd9edddec1e535fbf31 Story: 2005349 Task: 30302 --- doc/source/cli/command-objects/compute-service.rst | 8 +++++++- openstackclient/compute/v2/service.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/command-objects/compute-service.rst b/doc/source/cli/command-objects/compute-service.rst index 45e781e298..a76a9c26b9 100644 --- a/doc/source/cli/command-objects/compute-service.rst +++ b/doc/source/cli/command-objects/compute-service.rst @@ -18,13 +18,19 @@ Delete compute service(s) .. _compute_service_delete-service: .. describe:: - Compute service(s) to delete (ID only) + Compute service(s) to delete (ID only). If using + ``--os-compute-api-version`` 2.53 or greater, the ID is a UUID which can + be retrieved by listing compute services using the same 2.53+ microversion. compute service list -------------------- List compute services +Using ``--os-compute-api-version`` 2.53 or greater will return the ID as a +UUID value which can be used to uniquely identify the service in a multi-cell +deployment. + .. program:: compute service list .. code:: bash diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 80c0be7ecb..625c0fad3a 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -37,7 +37,10 @@ def get_parser(self, prog_name): "service", metavar="", nargs='+', - help=_("Compute service(s) to delete (ID only)") + help=_("Compute service(s) to delete (ID only). If using " + "``--os-compute-api-version`` 2.53 or greater, the ID is " + "a UUID which can be retrieved by listing compute services " + "using the same 2.53+ microversion.") ) return parser @@ -60,7 +63,11 @@ def take_action(self, parsed_args): class ListService(command.Lister): - _description = _("List compute services") + _description = _("List compute services. Using " + "``--os-compute-api-version`` 2.53 or greater will " + "return the ID as a UUID value which can be used to " + "uniquely identify the service in a multi-cell " + "deployment.") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) From 3b2863e369ef472042a054e11d189b6fbc34bb42 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 22 Aug 2019 11:05:05 -0500 Subject: [PATCH 0121/1120] Fix functional.base.TestCase.openstack() to optionally omit --os-auth-type Change the functional test TestCase.openstack() method to add a way to not include the --os-auth-type option in order to test the default auth-type logic. Change-Id: I0f1ca2f7517a41278afaad5aaf4e98accb16bea2 Signed-off-by: Dean Troyer --- openstackclient/tests/functional/base.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index c34ca39332..08e9390e3e 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -44,16 +44,30 @@ class TestCase(testtools.TestCase): @classmethod def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): - """Executes openstackclient command for the given action.""" - if cloud is not None: + """Executes openstackclient command for the given action + + NOTE(dtroyer): There is a subtle distinction between pasing + 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. + """ + if cloud is None: + # Execute command with no auth return execute( - 'openstack --os-cloud={cloud} '.format(cloud=cloud) + cmd, + '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: - # Execute command with no auth + # Execure command with an explicit cloud specified return execute( - 'openstack --os-auth-type none ' + cmd, + 'openstack --os-cloud=' + cloud + ' ' + cmd, fail_ok=fail_ok ) From 75f0f82c411e0bc94648c845669dc9312f0152ba Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 21 Aug 2019 14:15:58 -0500 Subject: [PATCH 0122/1120] Add CLI argument tests before making changes Add these tests before hacking on the global args and removing the compatibility stuff so we can clearly see what actually changes. Change-Id: Ic86c89da1475b4914ff7cb2396199cd219a12097 Signed-off-by: Dean Troyer --- .../tests/functional/common/test_args.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 openstackclient/tests/functional/common/test_args.py diff --git a/openstackclient/tests/functional/common/test_args.py b/openstackclient/tests/functional/common/test_args.py new file mode 100644 index 0000000000..dd1fd0fb2c --- /dev/null +++ b/openstackclient/tests/functional/common/test_args.py @@ -0,0 +1,79 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from openstackclient.tests.functional import base + + +class 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', + cloud='', + )) + self.assertIsNotNone(cmd_output) + self.assertIn( + 'auth_type', + cmd_output.keys(), + ) + self.assertEqual( + 'password', + cmd_output['auth_type'], + ) + + def test_auth_type_none(self): + cmd_output = json.loads(self.openstack( + 'configuration show -f json', + cloud=None, + )) + self.assertIsNotNone(cmd_output) + self.assertIn( + 'auth_type', + cmd_output.keys(), + ) + self.assertEqual( + 'none', + cmd_output['auth_type'], + ) + + def test_auth_type_token_endpoint_opt(self): + cmd_output = json.loads(self.openstack( + 'configuration show -f json --os-auth-type token_endpoint', + cloud=None, + )) + self.assertIsNotNone(cmd_output) + self.assertIn( + 'auth_type', + cmd_output.keys(), + ) + self.assertEqual( + 'token_endpoint', + cmd_output['auth_type'], + ) + + def test_auth_type_password_opt(self): + cmd_output = json.loads(self.openstack( + 'configuration show -f json --os-auth-type password', + cloud=None, + )) + self.assertIsNotNone(cmd_output) + self.assertIn( + 'auth_type', + cmd_output.keys(), + ) + self.assertEqual( + 'password', + cmd_output['auth_type'], + ) From 03a2accb2f13f82b1090e1ae6f154bf263e7e252 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 May 2019 20:06:44 -0500 Subject: [PATCH 0123/1120] Format aggregate command fields and de-race functional tests Rename metadata to property in all aggregate commands Beef up functional tests to reduce street racing Change-Id: I4598da73b85a954f3e6a3981db21891b45d9548c Signed-off-by: Dean Troyer --- openstackclient/compute/v2/aggregate.py | 46 ++++++++++++++- .../functional/compute/v2/test_aggregate.py | 59 ++++++++++++++++--- .../tests/unit/compute/v2/test_aggregate.py | 41 +++++++------ 3 files changed, 117 insertions(+), 29 deletions(-) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index fa64647899..3834de1f0b 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -18,6 +18,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -58,6 +59,15 @@ def take_action(self, parsed_args): info = {} info.update(data._info) + + # Special mapping for columns to make the output easier to read: + # 'metadata' --> 'properties' + info.update( + { + 'hosts': format_columns.ListColumn(info.pop('hosts')), + 'properties': format_columns.DictColumn(info.pop('metadata')), + }, + ) return zip(*sorted(six.iteritems(info))) @@ -101,8 +111,20 @@ def take_action(self, parsed_args): parsed_args.property, )._info) - # TODO(dtroyer): re-format metadata field to properites as - # in the set command + # Special mapping for columns to make the output easier to read: + # 'metadata' --> 'properties' + hosts = None + properties = None + if 'hosts' in info.keys(): + hosts = format_columns.ListColumn(info.pop('hosts')) + if 'metadata' in info.keys(): + properties = format_columns.DictColumn(info.pop('metadata')) + info.update( + { + 'hosts': hosts, + 'properties': properties, + }, + ) return zip(*sorted(six.iteritems(info))) @@ -186,6 +208,10 @@ def take_action(self, parsed_args): return (column_headers, (utils.get_item_properties( s, columns, + formatters={ + 'Hosts': format_columns.ListColumn, + 'Metadata': format_columns.DictColumn, + }, ) for s in data)) @@ -220,6 +246,15 @@ def take_action(self, parsed_args): info = {} info.update(data._info) + + # Special mapping for columns to make the output easier to read: + # 'metadata' --> 'properties' + info.update( + { + 'hosts': format_columns.ListColumn(info.pop('hosts')), + 'properties': format_columns.DictColumn(info.pop('metadata')), + }, + ) return zip(*sorted(six.iteritems(info))) @@ -326,7 +361,12 @@ def take_action(self, parsed_args): # 'metadata' --> 'properties' data._info.update( { - 'properties': utils.format_dict(data._info.pop('metadata')), + 'hosts': format_columns.ListColumn( + data._info.pop('hosts') + ), + 'properties': format_columns.DictColumn( + data._info.pop('metadata') + ), }, ) diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index c591dd61fd..8a082e82ad 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -11,6 +11,7 @@ # under the License. import json +import time import uuid from openstackclient.tests.functional import base @@ -19,6 +20,32 @@ class AggregateTests(base.TestCase): """Functional tests for aggregate""" + @classmethod + def wait_for_status(cls, check_type, check_name, desired_status, + wait=120, interval=5, failures=None): + current_status = "notset" + if failures is None: + failures = ['error'] + total_sleep = 0 + while total_sleep < wait: + output = json.loads(cls.openstack( + check_type + ' show -f json ' + check_name)) + current_status = output['name'] + if (current_status == desired_status): + print('{} {} now has status {}' + .format(check_type, check_name, current_status)) + return + print('Checking {} {} Waiting for {} current status: {}' + .format(check_type, check_name, + desired_status, current_status)) + if current_status in failures: + raise Exception( + 'Current status {} of {} {} is one of failures {}' + .format(current_status, check_type, check_name, failures)) + time.sleep(interval) + total_sleep += interval + cls.assertOutput(desired_status, current_status) + def test_aggregate_crud(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex @@ -36,12 +63,16 @@ def test_aggregate_crud(self): 'nova', cmd_output['availability_zone'] ) - # TODO(dtroyer): enable the following once the properties field - # is correctly formatted in the create output - # self.assertIn( - # "a='b'", - # cmd_output['properties'] - # ) + self.assertIn( + 'a', + cmd_output['properties'] + ) + self.wait_for_status('aggregate', name1, name1) + self.addCleanup( + self.openstack, + 'aggregate delete ' + name1, + fail_ok=True, + ) name2 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( @@ -57,6 +88,12 @@ def test_aggregate_crud(self): 'external', cmd_output['availability_zone'] ) + self.wait_for_status('aggregate', name2, name2) + self.addCleanup( + self.openstack, + 'aggregate delete ' + name2, + fail_ok=True, + ) # Test aggregate set name3 = uuid.uuid4().hex @@ -69,6 +106,12 @@ def test_aggregate_crud(self): name1 ) self.assertOutput('', raw_output) + self.wait_for_status('aggregate', name3, name3) + self.addCleanup( + self.openstack, + 'aggregate delete ' + name3, + fail_ok=True, + ) cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + @@ -83,11 +126,11 @@ def test_aggregate_crud(self): cmd_output['availability_zone'] ) self.assertIn( - "c='d'", + 'c', cmd_output['properties'] ) self.assertNotIn( - "a='b'", + 'a', cmd_output['properties'] ) diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index 3efe0dbd36..0937047cf0 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -16,6 +16,7 @@ import mock from mock import call +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -31,16 +32,16 @@ class TestAggregate(compute_fakes.TestComputev2): 'availability_zone', 'hosts', 'id', - 'metadata', 'name', + 'properties', ) data = ( fake_ag.availability_zone, - fake_ag.hosts, + format_columns.ListColumn(fake_ag.hosts), fake_ag.id, - fake_ag.metadata, fake_ag.name, + format_columns.DictColumn(fake_ag.metadata), ) def setUp(self): @@ -75,7 +76,7 @@ def test_aggregate_add_host(self): self.aggregate_mock.add_host.assert_called_once_with(self.fake_ag, parsed_args.host) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestAggregateCreate(TestAggregate): @@ -99,7 +100,7 @@ def test_aggregate_create(self): self.aggregate_mock.create.assert_called_once_with(parsed_args.name, None) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_aggregate_create_with_zone(self): arglist = [ @@ -116,7 +117,7 @@ def test_aggregate_create_with_zone(self): self.aggregate_mock.create.assert_called_once_with(parsed_args.name, parsed_args.zone) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_aggregate_create_with_property(self): arglist = [ @@ -135,7 +136,7 @@ def test_aggregate_create_with_property(self): self.aggregate_mock.set_metadata.assert_called_once_with( self.fake_ag, parsed_args.property) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestAggregateDelete(TestAggregate): @@ -234,8 +235,11 @@ class TestAggregateList(TestAggregate): TestAggregate.fake_ag.id, TestAggregate.fake_ag.name, TestAggregate.fake_ag.availability_zone, - {key: value for key, value in TestAggregate.fake_ag.metadata.items() - if key != 'availability_zone'}, + format_columns.DictColumn({ + key: value + for key, value in TestAggregate.fake_ag.metadata.items() + if key != 'availability_zone' + }), ), ) def setUp(self): @@ -250,7 +254,7 @@ def test_aggregate_list(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.list_columns, columns) - self.assertEqual(self.list_data, tuple(data)) + self.assertItemEqual(self.list_data, tuple(data)) def test_aggregate_list_with_long(self): arglist = [ @@ -263,7 +267,7 @@ def test_aggregate_list_with_long(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.list_columns_long, columns) - self.assertEqual(self.list_data_long, tuple(data)) + self.assertListItemEqual(self.list_data_long, tuple(data)) class TestAggregateRemoveHost(TestAggregate): @@ -290,7 +294,7 @@ def test_aggregate_add_host(self): self.aggregate_mock.remove_host.assert_called_once_with( self.fake_ag, parsed_args.host) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) class TestAggregateSet(TestAggregate): @@ -440,13 +444,14 @@ class TestAggregateShow(TestAggregate): data = ( TestAggregate.fake_ag.availability_zone, - TestAggregate.fake_ag.hosts, + format_columns.ListColumn(TestAggregate.fake_ag.hosts), TestAggregate.fake_ag.id, TestAggregate.fake_ag.name, - utils.format_dict( - {key: value - for key, value in TestAggregate.fake_ag.metadata.items() - if key != 'availability_zone'}), + format_columns.DictColumn({ + key: value + for key, value in TestAggregate.fake_ag.metadata.items() + if key != 'availability_zone' + }), ) def setUp(self): @@ -467,7 +472,7 @@ def test_aggregate_show(self): self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, tuple(data)) class TestAggregateUnset(TestAggregate): From 6fcc2608b17d84cf3699bb4a5bae692404393ca1 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 21 Aug 2019 12:20:41 -0500 Subject: [PATCH 0124/1120] Remove token_endpoint auth type The token_endpoint was a compatibility auth type to maintain support for the --url global option that dated back to the beginning of OpenStack CLI auth. The common keystoneauth library implements 'admin_token' which provides the same functionality using --endpoint rather than --url. Change-Id: I1b9fbb96e447889a41b705324725a2ffc8ecfd9f Signed-off-by: Dean Troyer --- doc/source/cli/authentication.rst | 9 --- doc/source/cli/backwards-incompatible.rst | 8 +++ openstackclient/api/auth_plugin.py | 61 ------------------- openstackclient/shell.py | 8 +-- .../tests/functional/common/test_args.py | 26 ++++---- .../tests/unit/common/test_clientmanager.py | 8 +-- openstackclient/tests/unit/test_shell.py | 22 +++---- setup.cfg | 3 - 8 files changed, 40 insertions(+), 105 deletions(-) delete mode 100644 openstackclient/api/auth_plugin.py diff --git a/doc/source/cli/authentication.rst b/doc/source/cli/authentication.rst index b153f54111..f8aaadf492 100644 --- a/doc/source/cli/authentication.rst +++ b/doc/source/cli/authentication.rst @@ -39,15 +39,6 @@ There are at least three authentication types that are always available: (described below as token/endpoint) in that a token and an authentication URL are supplied and the plugin retrieves a new token. [Required: ``--os-auth-url``, ``--os-token``] -* **Token/Endpoint**: This is the original token authentication (known as 'token - flow' in the early CLI documentation in the OpenStack wiki). It requires - a token and a direct endpoint that is used in the API call. The difference - from the new Token type is this token is used as-is, no call is made - to the Identity service from the client. This type is most often used to - bootstrap a Keystone server where the token is the ``admin_token`` configured - in ``keystone.conf``. It will also work with other services and a regular - scoped token such as one obtained from a ``token issue`` command. - [Required: ``--os-url``, ``--os-token``] * **Others**: Other authentication plugins such as SAML, Kerberos, and OAuth1.0 are under development and also supported. To use them, they must be selected by supplying the ``--os-auth-type`` option. diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index da956b232b..9d43754e01 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -96,6 +96,14 @@ Release 4.0 * Removed in: 4.0 * Commit: https://review.opendev.org/612751 +14. Remove 'Token/Endpoint' auth plugin support (type ``token_endpoint``). + This remained as a compatibility for the ``admin_token`` auth type to + support the ``--url`` global option. That option is also now removed, + use ``--endpoint`` instead. + + * Removed in: 4.0 + * Commit: https://review.opendev.org/ + Release 3.12 ------------ diff --git a/openstackclient/api/auth_plugin.py b/openstackclient/api/auth_plugin.py deleted file mode 100644 index a106b1ebc1..0000000000 --- a/openstackclient/api/auth_plugin.py +++ /dev/null @@ -1,61 +0,0 @@ -# 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. -# - -"""Authentication Plugin Library""" - -from keystoneauth1 import loading -from keystoneauth1 import token_endpoint - -from openstackclient.i18n import _ - - -class TokenEndpoint(loading.BaseLoader): - """Auth plugin to handle traditional token/endpoint usage - - Keystoneauth contains a Token plugin class that now correctly - handles the token/endpoint auth compatible with OSC. However, - the AdminToken loader deprecates the 'url' argument, which breaks - OSC compatibility, so make one that works. - """ - - @property - def plugin_class(self): - return token_endpoint.Token - - def load_from_options(self, url, token, **kwargs): - """A plugin for static authentication with an existing token - - :param string url: Service endpoint - :param string token: Existing token - """ - - return super(TokenEndpoint, self).load_from_options( - endpoint=url, - token=token, - ) - - def get_options(self): - """Return the legacy options""" - - options = [ - loading.Opt( - 'url', - help=_('Specific service endpoint to use'), - ), - loading.Opt( - 'token', - secret=True, - help=_('Authentication token to use'), - ), - ] - return options diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 22d8412cc7..91a15fd4dc 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -58,10 +58,10 @@ def build_option_parser(self, description, version): def _final_defaults(self): super(OpenStackShell, self)._final_defaults() - # Set the default plugin to token_endpoint if url and token are given - if (self.options.url and self.options.token): - # Use service token authentication - self._auth_type = 'token_endpoint' + # Set the default plugin to admin_token if endpoint and token are given + if (self.options.endpoint and self.options.token): + # Use token authentication + self._auth_type = 'admin_token' else: self._auth_type = 'password' diff --git a/openstackclient/tests/functional/common/test_args.py b/openstackclient/tests/functional/common/test_args.py index dd1fd0fb2c..02cad6c16a 100644 --- a/openstackclient/tests/functional/common/test_args.py +++ b/openstackclient/tests/functional/common/test_args.py @@ -12,6 +12,8 @@ import json +from tempest.lib import exceptions as tempest_exc + from openstackclient.tests.functional import base @@ -49,19 +51,17 @@ def test_auth_type_none(self): ) def test_auth_type_token_endpoint_opt(self): - cmd_output = json.loads(self.openstack( - 'configuration show -f json --os-auth-type token_endpoint', - cloud=None, - )) - self.assertIsNotNone(cmd_output) - self.assertIn( - 'auth_type', - cmd_output.keys(), - ) - self.assertEqual( - 'token_endpoint', - cmd_output['auth_type'], - ) + # Make sure token_endpoint is really gone + try: + self.openstack( + 'configuration show -f json --os-auth-type token_endpoint', + cloud=None, + ) + except tempest_exc.CommandFailed as e: + self.assertIn('--os-auth-type: invalid choice:', str(e)) + self.assertIn('token_endpoint', str(e)) + else: + self.fail('CommandFailed should be raised') def test_auth_type_password_opt(self): cmd_output = json.loads(self.openstack( diff --git a/openstackclient/tests/unit/common/test_clientmanager.py b/openstackclient/tests/unit/common/test_clientmanager.py index f15f9af1ff..a83b131800 100644 --- a/openstackclient/tests/unit/common/test_clientmanager.py +++ b/openstackclient/tests/unit/common/test_clientmanager.py @@ -28,19 +28,19 @@ def _clientmanager_class(self): """Allow subclasses to override the ClientManager class""" return clientmanager.ClientManager - def test_client_manager_token_endpoint(self): + def test_client_manager_admin_token(self): token_auth = { - 'url': fakes.AUTH_URL, + 'endpoint': fakes.AUTH_URL, 'token': fakes.AUTH_TOKEN, } client_manager = self._make_clientmanager( auth_args=token_auth, - auth_plugin_name='token_endpoint', + auth_plugin_name='admin_token', ) self.assertEqual( fakes.AUTH_URL, - client_manager._cli_options.config['auth']['url'], + client_manager._cli_options.config['auth']['endpoint'], ) self.assertEqual( fakes.AUTH_TOKEN, diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py index 5d413e7e3c..31675c47e6 100644 --- a/openstackclient/tests/unit/test_shell.py +++ b/openstackclient/tests/unit/test_shell.py @@ -153,7 +153,7 @@ def setUp(self): # released in osc-lib self.shell_class = importutils.import_class(self.shell_class_name) - def _assert_token_endpoint_auth(self, cmd_options, default_args): + def _assert_admin_token_auth(self, cmd_options, default_args): with mock.patch( self.shell_class_name + ".initialize_app", self.app, @@ -172,9 +172,9 @@ def _assert_token_endpoint_auth(self, cmd_options, default_args): "token", ) self.assertEqual( - default_args.get("url", ''), - _shell.options.url, - "url", + default_args.get("endpoint", ''), + _shell.options.endpoint, + "endpoint", ) def _assert_token_auth(self, cmd_options, default_args): @@ -338,7 +338,7 @@ def setUp(self): super(TestShellTokenEndpointAuthEnv, self).setUp() env = { "OS_TOKEN": DEFAULT_TOKEN, - "OS_URL": DEFAULT_SERVICE_URL, + "OS_ENDPOINT": DEFAULT_SERVICE_URL, } self.useFixture(osc_lib_test_utils.EnvFixture(env.copy())) @@ -346,23 +346,23 @@ def test_env(self): flag = "" kwargs = { "token": DEFAULT_TOKEN, - "url": DEFAULT_SERVICE_URL, + "endpoint": DEFAULT_SERVICE_URL, } - self._assert_token_endpoint_auth(flag, kwargs) + self._assert_admin_token_auth(flag, kwargs) def test_only_token(self): flag = "--os-token xyzpdq" kwargs = { "token": "xyzpdq", - "url": DEFAULT_SERVICE_URL, + "endpoint": DEFAULT_SERVICE_URL, } self._assert_token_auth(flag, kwargs) def test_only_url(self): - flag = "--os-url http://cloud.local:555" + flag = "--os-endpoint http://cloud.local:555" kwargs = { "token": DEFAULT_TOKEN, - "url": "http://cloud.local:555", + "endpoint": "http://cloud.local:555", } self._assert_token_auth(flag, kwargs) @@ -371,7 +371,7 @@ def test_empty_auth(self): flag = "" kwargs = { "token": '', - "url": '', + "endpoint": '', } self._assert_token_auth(flag, kwargs) diff --git a/setup.cfg b/setup.cfg index a9f7de2ce1..a10a3d7647 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,9 +27,6 @@ packages = console_scripts = openstack = openstackclient.shell:main -keystoneauth1.plugin = - token_endpoint = openstackclient.api.auth_plugin:TokenEndpoint - openstack.cli = command_list = openstackclient.common.module:ListCommand module_list = openstackclient.common.module:ListModule From 6419533f436c6c369e05662c6ced26ad0bc68240 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 14 Jun 2019 11:51:52 +0100 Subject: [PATCH 0125/1120] Bump hacking version Pick up newer versions of this library. Thankfully no serious changes are needed. Change-Id: I69e523844529fc1c8aa0c1ce764182dbe29cfeb6 Signed-off-by: Stephen Finucane --- lower-constraints.txt | 7 +-- openstackclient/common/configuration.py | 2 +- openstackclient/compute/v2/hypervisor.py | 2 +- openstackclient/compute/v2/server.py | 58 +++++++++---------- .../network/v2/network_segment_range.py | 2 +- openstackclient/shell.py | 1 + .../tests/unit/compute/v2/fakes.py | 2 +- .../tests/unit/network/v2/fakes.py | 2 +- test-requirements.txt | 3 +- tox.ini | 5 +- 10 files changed, 41 insertions(+), 43 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index c3dc477eaf..710ff0e1c4 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -22,14 +22,14 @@ extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 flake8-import-order==0.13 -flake8==2.5.5 +flake8==2.6.2 future==0.16.0 futurist==1.2.0 gitdb==0.6.4 GitPython==1.0.1 gnocchiclient==3.3.1 greenlet==0.4.10 -hacking==0.12.0 +hacking==1.1.0 httplib2==0.9.1 idna==2.6 iso8601==0.1.11 @@ -71,14 +71,13 @@ paramiko==2.0.0 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 -pep8==1.5.7 pika-pool==0.1.3 pika==0.10.0 ply==3.10 positional==1.2.1 prettytable==0.7.2 pyasn1==0.1.8 -pycodestyle==2.3.1 +pycodestyle==2.0.0 pycparser==2.18 pyflakes==0.8.1 pyinotify==0.9.6 diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 57825bb056..ce646b1e45 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -52,6 +52,6 @@ def take_action(self, parsed_args): info = self.app.client_manager.get_configuration() for key, value in six.iteritems(info.pop('auth', {})): if parsed_args.mask and key.lower() in secret_opts: - value = REDACTED + value = REDACTED info['auth.' + key] = value return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 406aa9175b..0d367fee86 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -112,7 +112,7 @@ def take_action(self, parsed_args): # example: 17:37:14 up 2:33, 3 users, # load average: 0.33, 0.36, 0.34 m = re.match( - "\s*(.+)\sup\s+(.+),\s+(.+)\susers?,\s+load average:\s(.+)", + r"\s*(.+)\sup\s+(.+),\s+(.+)\susers?,\s+load average:\s(.+)", uptime['uptime']) if m: hypervisor["host_time"] = m.group(1) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 95c2f28a81..5922dc500b 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -79,37 +79,37 @@ def _format_servers_list_power_state(state): def _get_ip_address(addresses, address_type, ip_address_family): - # Old style addresses - if address_type in addresses: - for addy in addresses[address_type]: + # Old style addresses + if address_type in addresses: + for addy in addresses[address_type]: + if int(addy['version']) in ip_address_family: + return addy['addr'] + + # New style addresses + new_address_type = address_type + if address_type == 'public': + new_address_type = 'floating' + if address_type == 'private': + new_address_type = 'fixed' + for network in addresses: + for addy in addresses[network]: + # Case where it is list of strings + if isinstance(addy, six.string_types): + if new_address_type == 'fixed': + return addresses[network][0] + else: + return addresses[network][-1] + # Case where it is a dict + if 'OS-EXT-IPS:type' not in addy: + continue + if addy['OS-EXT-IPS:type'] == new_address_type: if int(addy['version']) in ip_address_family: return addy['addr'] - - # New style addresses - new_address_type = address_type - if address_type == 'public': - new_address_type = 'floating' - if address_type == 'private': - new_address_type = 'fixed' - for network in addresses: - for addy in addresses[network]: - # Case where it is list of strings - if isinstance(addy, six.string_types): - if new_address_type == 'fixed': - return addresses[network][0] - else: - return addresses[network][-1] - # Case where it is a dict - if 'OS-EXT-IPS:type' not in addy: - continue - if addy['OS-EXT-IPS:type'] == new_address_type: - if int(addy['version']) in ip_address_family: - return addy['addr'] - msg = _("ERROR: No %(type)s IP version %(family)s address found") - raise exceptions.CommandError( - msg % {"type": address_type, - "family": ip_address_family} - ) + msg = _("ERROR: No %(type)s IP version %(family)s address found") + raise exceptions.CommandError( + msg % {"type": address_type, + "family": ip_address_family} + ) def _prefix_checked_value(prefix): diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index 567b5b6eb9..b938bdd920 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -358,7 +358,7 @@ def take_action(self, parsed_args): parsed_args.used and _is_prop_empty(columns, props, 'used') or \ parsed_args.unused and \ not _is_prop_empty(columns, props, 'used'): - continue + continue if parsed_args.long: props = _update_additional_fields_from_props(columns, props) display_props += (props,) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 22d8412cc7..86aa032b61 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -209,5 +209,6 @@ def main(argv=None): return OpenStackShell().run(argv) + if __name__ == "__main__": sys.exit(main()) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index ee7d49831f..7357e1430a 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -505,7 +505,7 @@ def create_security_groups(attrs=None, count=2): @staticmethod def get_security_groups(security_groups=None, count=2): - """Get an iterable MagicMock object with a list of faked security groups. + """Get an iterable MagicMock with a list of faked security groups. If security groups list is provided, then initialize the Mock object with the list. Otherwise create one. diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index e41621a48e..1519bd4b8c 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1344,7 +1344,7 @@ def create_security_group_rules(attrs=None, count=2): @staticmethod def get_security_group_rules(security_group_rules=None, count=2): - """Get an iterable Mock object with a list of faked security group rules. + """Get an iterable Mock with a list of faked security group rules. If security group rules list is provided, then initialize the Mock object with the list. Otherwise create one. diff --git a/test-requirements.txt b/test-requirements.txt index 4cb66cfdb6..64e7a0773b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,8 +1,7 @@ # 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. -hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 - +hacking>=1.1.0,<1.2.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD flake8-import-order==0.13 # LGPLv3 diff --git a/tox.ini b/tox.ini index eaf66bc493..a973b107f7 100644 --- a/tox.ini +++ b/tox.ini @@ -132,9 +132,8 @@ show-source = True # H203: Use assertIs(Not)None to check for None enable-extensions = H203 exclude = .git,.tox,dist,doc,*lib/python*,*egg,build,tools -# If 'ignore' is not set there are default errors and warnings that are set -# Doc: http://flake8.readthedocs.org/en/latest/config.html#default -ignore = __ +# W504 is disabled since you must choose between this or W503 +ignore = W504 import-order-style = pep8 application_import_names = openstackclient From f044016e296fd261f61eda69fb2f5ef87de6e0a9 Mon Sep 17 00:00:00 2001 From: LIU Yulong Date: Wed, 12 Sep 2018 06:18:04 +0800 Subject: [PATCH 0126/1120] Add floating IP Port Forwarding commands Add following commands: floating ip port forwarding create floating ip port forwarding delete floating ip port forwarding list floating ip port forwarding set floating ip port forwarding show Closes-Bug: #1811352 Change-Id: I6a5642e8acce28fc830410d4fa3180597b862761 --- .../floating-ip-port-forwarding.rst | 173 ++++++ .../network/v2/floating_ip_port_forwarding.py | 371 +++++++++++++ .../tests/unit/network/v2/fakes.py | 77 +++ .../v2/test_floating_ip_port_forwarding.py | 502 ++++++++++++++++++ ...tforwarding-commands-6e4d8ace698ee308.yaml | 9 + setup.cfg | 6 + 6 files changed, 1138 insertions(+) create mode 100644 doc/source/cli/command-objects/floating-ip-port-forwarding.rst create mode 100644 openstackclient/network/v2/floating_ip_port_forwarding.py create mode 100644 openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py create mode 100644 releasenotes/notes/add-fip-portforwarding-commands-6e4d8ace698ee308.yaml diff --git a/doc/source/cli/command-objects/floating-ip-port-forwarding.rst b/doc/source/cli/command-objects/floating-ip-port-forwarding.rst new file mode 100644 index 0000000000..5012787aaf --- /dev/null +++ b/doc/source/cli/command-objects/floating-ip-port-forwarding.rst @@ -0,0 +1,173 @@ +=========================== +floating ip port forwarding +=========================== + +Network v2 + +floating ip port forwarding create +---------------------------------- + +Create floating IP Port Forwarding + +.. program:: floating ip port forwarding create +.. code:: bash + + openstack floating ip port forwarding create + --internal-ip-address + --port + --internal-protocol-port + --external-protocol-port + --protocol + + + +.. describe:: --internal-ip-address + + The fixed IPv4 address of the network port associated + to the floating IP port forwarding + +.. describe:: --port + + The name or ID of the network port associated to the + floating IP port forwarding + +.. describe:: --internal-protocol-port + + The protocol port number of the network port fixed + IPv4 address associated to the floating IP port + forwarding + +.. describe:: --external-protocol-port + + The protocol port number of the port forwarding's + floating IP address + +.. describe:: --protocol + + The protocol used in the floating IP port forwarding, + for instance: TCP, UDP + +.. describe:: + + Floating IP that the port forwarding belongs to (IP + address or ID) + +floating ip port forwarding delete +---------------------------------- + +Delete floating IP Port Forwarding(s) + +.. program:: floating ip port forwarding delete +.. code:: bash + + openstack floating ip port forwarding delete + [ ...] + +.. describe:: + + Floating IP that the port forwarding belongs to (IP + address or ID) + +.. describe:: + + The ID of the floating IP port forwarding + +floating ip port forwarding list +-------------------------------- + +List floating IP Port Forwarding(s) + +.. program:: floating ip port forwarding list +.. code:: bash + + openstack floating ip port forwarding list + [--port ] + [--external-protocol-port ] + [--protocol protocol] + + +.. option:: --port + + The ID of the network port associated to the floating + IP port forwarding + +.. option:: --protocol + + The IP protocol used in the floating IP port + forwarding + +.. describe:: + + Floating IP that the port forwarding belongs to (IP + address or ID) + +floating ip port forwarding set +------------------------------- + +Set floating IP Port Forwarding properties + +.. program:: floating ip port forwarding set +.. code:: bash + + openstack floating ip port forwarding set + [--port ] + [--internal-ip-address ] + [--internal-protocol-port ] + [--external-protocol-port ] + [--protocol ] + + + +.. option:: --port + + The ID of the network port associated to the floating + IP port forwarding + +.. option:: --internal-ip-address + + The fixed IPv4 address of the network port associated + to the floating IP port forwarding + +.. option:: --internal-protocol-port + + The TCP/UDP/other protocol port number of the network + port fixed IPv4 address associated to the floating IP + port forwarding + +.. option:: --external-protocol-port + + The TCP/UDP/other protocol port number of the port + forwarding's floating IP address + +.. option:: --protocol + + The IP protocol used in the floating IP port + forwarding + +.. describe:: + + Floating IP that the port forwarding belongs to (IP + address or ID) + +.. describe:: + + The ID of the floating IP port forwarding + +floating ip port forwarding show +-------------------------------- + +Display floating IP Port Forwarding details + +.. program:: floating ip port forwarding show +.. code:: bash + + openstack floating ip show + +.. describe:: + + Floating IP that the port forwarding belongs to (IP + address or ID) + +.. describe:: + + The ID of the floating IP port forwarding diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py new file mode 100644 index 0000000000..f94bcc065a --- /dev/null +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -0,0 +1,371 @@ +# 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. +# + +"""Floating IP Port Forwarding action implementations""" +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +class CreateFloatingIPPortForwarding(command.ShowOne): + _description = _("Create floating IP port forwarding") + + def get_parser(self, prog_name): + parser = super(CreateFloatingIPPortForwarding, + self).get_parser(prog_name) + parser.add_argument( + '--internal-ip-address', + required=True, + metavar='', + help=_("The fixed IPv4 address of the network " + "port associated to the floating IP port forwarding") + ) + parser.add_argument( + '--port', + metavar='', + required=True, + help=_("The name or ID of the network port associated " + "to the floating IP port forwarding") + ) + parser.add_argument( + '--internal-protocol-port', + type=int, + metavar='', + required=True, + help=_("The 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='', + required=True, + help=_("The protocol port number of " + "the port forwarding's floating IP address") + ) + parser.add_argument( + '--protocol', + metavar='', + required=True, + help=_("The protocol used in the floating IP " + "port forwarding, for instance: TCP, UDP") + ) + parser.add_argument( + 'floating_ip', + metavar='', + help=_("Floating IP that the port forwarding belongs to " + "(IP address or ID)") + ) + return parser + + def take_action(self, parsed_args): + attrs = {} + client = self.app.client_manager.network + floating_ip = client.find_ip( + parsed_args.floating_ip, + 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 + + if parsed_args.port: + port = client.find_port(parsed_args.port, + ignore_missing=False) + attrs['internal_port_id'] = port.id + attrs['internal_ip_address'] = parsed_args.internal_ip_address + attrs['protocol'] = parsed_args.protocol + + obj = client.create_floating_ip_port_forwarding( + floating_ip.id, + **attrs + ) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (display_columns, data) + + +class DeleteFloatingIPPortForwarding(command.Command): + _description = _("Delete floating IP port forwarding") + + def get_parser(self, prog_name): + parser = super(DeleteFloatingIPPortForwarding, + self).get_parser(prog_name) + parser.add_argument( + 'floating_ip', + metavar='', + help=_("Floating IP that the port forwarding belongs to " + "(IP address or ID)") + ) + parser.add_argument( + 'port_forwarding_id', + nargs="+", + metavar="", + help=_("The ID of the floating IP port forwarding(s) to delete") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + floating_ip = client.find_ip( + parsed_args.floating_ip, + ignore_missing=False, + ) + result = 0 + + for port_forwarding_id in parsed_args.port_forwarding_id: + try: + client.delete_floating_ip_port_forwarding( + floating_ip.id, + port_forwarding_id, + ignore_missing=False, + ) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete floating IP port forwarding " + "'%(port_forwarding_id)s': %(e)s"), + {'port_forwarding_id': port_forwarding_id, 'e': e}) + if result > 0: + total = len(parsed_args.port_forwarding_id) + msg = (_("%(result)s of %(total)s Port forwarding failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListFloatingIPPortForwarding(command.Lister): + _description = _("List floating IP port forwarding") + + def get_parser(self, prog_name): + parser = super(ListFloatingIPPortForwarding, + self).get_parser(prog_name) + parser.add_argument( + 'floating_ip', + metavar='', + help=_("Floating IP that the port forwarding belongs to " + "(IP address or ID)") + ) + parser.add_argument( + '--port', + metavar='', + help=_("Filter the list result by the ID or name of " + "the internal network port") + ) + parser.add_argument( + '--external-protocol-port', + metavar='', + dest='external_protocol_port', + help=_("Filter the list result by the " + "protocol port number of the floating IP") + ) + parser.add_argument( + '--protocol', + metavar='protocol', + help=_("Filter the list result by the port protocol") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'id', + 'internal_port_id', + 'internal_ip_address', + 'internal_port', + 'external_port', + 'protocol', + ) + headers = ( + 'ID', + 'Internal Port ID', + 'Internal IP Address', + 'Internal Port', + 'External Port', + 'Protocol', + ) + + query = {} + + if parsed_args.port: + 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 + if parsed_args.protocol is not None: + query['protocol'] = parsed_args.protocol + + obj = client.find_ip( + parsed_args.floating_ip, + ignore_missing=False, + ) + + data = client.floating_ip_port_forwardings(obj, **query) + + return (headers, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class SetFloatingIPPortForwarding(command.Command): + _description = _("Set floating IP Port Forwarding Properties") + + def get_parser(self, prog_name): + parser = super(SetFloatingIPPortForwarding, + self).get_parser(prog_name) + parser.add_argument( + 'floating_ip', + metavar='', + help=_("Floating IP that the port forwarding belongs to " + "(IP address or ID)") + ) + parser.add_argument( + 'port_forwarding_id', + metavar='', + help=_("The ID of the floating IP port forwarding") + ) + parser.add_argument( + '--port', + metavar='', + help=_("The ID of the network port associated to " + "the floating IP port forwarding") + ) + parser.add_argument( + '--internal-ip-address', + metavar='', + help=_("The fixed IPv4 address of the network port " + "associated to the floating IP port forwarding") + ) + 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") + ) + parser.add_argument( + '--protocol', + metavar='', + choices=['tcp', 'udp'], + help=_("The IP protocol used in the floating IP port forwarding") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + floating_ip = client.find_ip( + parsed_args.floating_ip, + ignore_missing=False, + ) + + attrs = {} + if parsed_args.port: + port = client.find_port(parsed_args.port, + ignore_missing=False) + attrs['internal_port_id'] = port.id + + 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 + + if parsed_args.protocol: + attrs['protocol'] = parsed_args.protocol + + client.update_floating_ip_port_forwarding( + floating_ip.id, parsed_args.port_forwarding_id, **attrs) + + +class ShowFloatingIPPortForwarding(command.ShowOne): + _description = _("Display floating IP Port Forwarding details") + + def get_parser(self, prog_name): + parser = super(ShowFloatingIPPortForwarding, + self).get_parser(prog_name) + parser.add_argument( + 'floating_ip', + metavar='', + help=_("Floating IP that the port forwarding belongs to " + "(IP address or ID)") + ) + parser.add_argument( + 'port_forwarding_id', + metavar="", + help=_("The ID of the floating IP port forwarding") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + floating_ip = client.find_ip( + parsed_args.floating_ip, + ignore_missing=False, + ) + obj = client.find_floating_ip_port_forwarding( + floating_ip, + parsed_args.port_forwarding_id, + ignore_missing=False, + ) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index e41621a48e..27a7485a40 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1816,3 +1816,80 @@ def create_one_net_detailed_quota(attrs=None): info=copy.deepcopy(quota_attrs), loaded=True) return quota + + +class FakeFloatingIPPortForwarding(object): + """"Fake one or more Port forwarding""" + + @staticmethod + def create_one_port_forwarding(attrs=None): + """Create a fake Port Forwarding. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + floatingip_id = ( + attrs.get('floatingip_id') or'floating-ip-id-' + uuid.uuid4().hex + ) + # Set default attributes. + port_forwarding_attrs = { + 'id': uuid.uuid4().hex, + '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', + } + + # Overwrite default attributes. + port_forwarding_attrs.update(attrs) + + port_forwarding = fakes.FakeResource( + info=copy.deepcopy(port_forwarding_attrs), + loaded=True + ) + return port_forwarding + + @staticmethod + def create_port_forwardings(attrs=None, count=2): + """Create multiple fake Port Forwarding. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of Port Forwarding rule to fake + :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) + ) + return port_forwardings + + @staticmethod + def get_port_forwardings(port_forwardings=None, count=2): + """Get a list of faked Port Forwardings. + + If port forwardings list is provided, then initialize the Mock object + with the list. Otherwise create one. + + :param List port forwardings: + A list of FakeResource objects faking port forwardings + :param int count: + The number of Port Forwardings to fake + :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) + ) + + 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 new file mode 100644 index 0000000000..b51158be37 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py @@ -0,0 +1,502 @@ +# Copyright (c) 2018 China Telecom Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import floating_ip_port_forwarding +from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes_v2 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestFloatingIPPortForwarding(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestFloatingIPPortForwarding, self).setUp() + self.network = self.app.client_manager.network + self.floating_ip = (network_fakes.FakeFloatingIP. + create_one_floating_ip()) + self.port = network_fakes.FakePort.create_one_port() + self.project = identity_fakes_v2.FakeProject.create_one_project() + self.network.find_port = mock.Mock(return_value=self.port) + + +class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): + + def setUp(self): + project_id = '' + super(TestCreateFloatingIPPortForwarding, self).setUp() + self.new_port_forwarding = ( + network_fakes.FakeFloatingIPPortForwarding. + create_one_port_forwarding( + 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) + + self.network.find_ip = mock.Mock( + return_value=self.floating_ip + ) + + # Get the command object to test + self.cmd = floating_ip_port_forwarding.CreateFloatingIPPortForwarding( + self.app, self.namespace) + + self.columns = ( + 'external_port', + 'floatingip_id', + 'id', + 'internal_ip_address', + 'internal_port', + 'internal_port_id', + 'project_id', + 'protocol' + ) + + self.data = ( + self.new_port_forwarding.external_port, + 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, + project_id, + self.new_port_forwarding.protocol, + ) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_all_options(self): + arglist = [ + '--port', self.new_port_forwarding.internal_port_id, + '--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, + self.new_port_forwarding.floatingip_id, + '--internal-ip-address', + self.new_port_forwarding.internal_ip_address, + ] + 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), + ('protocol', self.new_port_forwarding.protocol), + ('floating_ip', self.new_port_forwarding.floatingip_id), + ('internal_ip_address', self.new_port_forwarding. + internal_ip_address), + ] + 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': self.new_port_forwarding.external_port, + 'internal_ip_address': self.new_port_forwarding. + internal_ip_address, + 'internal_port': self.new_port_forwarding.internal_port, + 'internal_port_id': self.new_port_forwarding. + internal_port_id, + 'protocol': self.new_port_forwarding.protocol, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteFloatingIPPortForwarding(TestFloatingIPPortForwarding): + + def setUp(self): + super(TestDeleteFloatingIPPortForwarding, self).setUp() + self._port_forwarding = ( + network_fakes.FakeFloatingIPPortForwarding.create_port_forwardings( + count=2, attrs={ + 'floatingip_id': self.floating_ip.id, + } + ) + ) + self.network.delete_floating_ip_port_forwarding = mock.Mock( + return_value=None + ) + + self.network.find_ip = mock.Mock( + return_value=self.floating_ip + ) + # Get the command object to test + self.cmd = floating_ip_port_forwarding.DeleteFloatingIPPortForwarding( + self.app, self.namespace) + + def test_port_forwarding_delete(self): + arglist = [ + self.floating_ip.id, + self._port_forwarding[0].id, + ] + verifylist = [ + ('floating_ip', self.floating_ip.id), + ('port_forwarding_id', [self._port_forwarding[0].id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.delete_floating_ip_port_forwarding.\ + assert_called_once_with( + self.floating_ip.id, + self._port_forwarding[0].id, + ignore_missing=False + ) + + self.assertIsNone(result) + + def test_multi_port_forwardings_delete(self): + arglist = [] + pf_id = [] + + arglist.append(str(self.floating_ip)) + + for a in self._port_forwarding: + arglist.append(a.id) + pf_id.append(a.id) + + verifylist = [ + ('floating_ip', str(self.floating_ip)), + ('port_forwarding_id', pf_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for a in self._port_forwarding: + calls.append(call(a.floatingip_id, a.id, ignore_missing=False)) + + self.network.delete_floating_ip_port_forwarding.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multi_port_forwarding_delete_with_exception(self): + arglist = [ + self.floating_ip.id, + self._port_forwarding[0].id, + 'unexist_port_forwarding_id', + ] + verifylist = [ + ('floating_ip', self.floating_ip.id), + ('port_forwarding_id', + [self._port_forwarding[0].id, 'unexist_port_forwarding_id']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + delete_mock_result = [None, exceptions.CommandError] + + self.network.delete_floating_ip_port_forwarding = ( + mock.MagicMock(side_effect=delete_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 Port forwarding failed to delete.', + str(e) + ) + + self.network.delete_floating_ip_port_forwarding.\ + assert_any_call( + self.floating_ip.id, + 'unexist_port_forwarding_id', + ignore_missing=False + ) + self.network.delete_floating_ip_port_forwarding.\ + assert_any_call( + self.floating_ip.id, + self._port_forwarding[0].id, + ignore_missing=False + ) + + +class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding): + + columns = ( + 'ID', + 'Internal Port ID', + 'Internal IP Address', + 'Internal Port', + 'External Port', + 'Protocol' + ) + + def setUp(self): + super(TestListFloatingIPPortForwarding, self).setUp() + self.port_forwardings = ( + network_fakes.FakeFloatingIPPortForwarding.create_port_forwardings( + count=3, attrs={ + 'internal_port_id': self.port.id, + 'floatingip_id': self.floating_ip.id, + } + ) + ) + self.data = [] + for port_forwarding in self.port_forwardings: + self.data.append(( + port_forwarding.id, + port_forwarding.internal_port_id, + port_forwarding.internal_ip_address, + port_forwarding.internal_port, + port_forwarding.external_port, + port_forwarding.protocol, + )) + self.network.floating_ip_port_forwardings = mock.Mock( + return_value=self.port_forwardings + ) + self.network.find_ip = mock.Mock( + return_value=self.floating_ip + ) + # Get the command object to test + self.cmd = floating_ip_port_forwarding.ListFloatingIPPortForwarding( + self.app, + self.namespace + ) + + def test_port_forwarding_list(self): + arglist = [ + self.floating_ip.id + ] + verifylist = [ + ('floating_ip', self.floating_ip.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.floating_ip_port_forwardings.assert_called_once_with( + self.floating_ip, + **{} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_forwarding_list_all_options(self): + arglist = [ + '--port', self.port_forwardings[0].internal_port_id, + '--external-protocol-port', + str(self.port_forwardings[0].external_port), + '--protocol', self.port_forwardings[0].protocol, + self.port_forwardings[0].floatingip_id, + ] + + verifylist = [ + ('port', self.port_forwardings[0].internal_port_id), + ('external_protocol_port', + str(self.port_forwardings[0].external_port)), + ('protocol', self.port_forwardings[0].protocol), + ('floating_ip', self.port_forwardings[0].floatingip_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + query = { + 'internal_port_id': self.port_forwardings[0].internal_port_id, + 'external_port': str(self.port_forwardings[0].external_port), + 'protocol': self.port_forwardings[0].protocol, + } + + self.network.floating_ip_port_forwardings.assert_called_once_with( + self.floating_ip, + **query + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding): + + # The Port Forwarding to set. + def setUp(self): + super(TestSetFloatingIPPortForwarding, self).setUp() + self._port_forwarding = ( + network_fakes.FakeFloatingIPPortForwarding. + create_one_port_forwarding( + attrs={ + 'floatingip_id': self.floating_ip.id, + } + ) + ) + self.network.update_floating_ip_port_forwarding = mock.Mock( + return_value=None + ) + + self.network.find_floating_ip_port_forwarding = mock.Mock( + return_value=self._port_forwarding) + self.network.find_ip = mock.Mock( + return_value=self.floating_ip + ) + # Get the command object to test + self.cmd = floating_ip_port_forwarding.SetFloatingIPPortForwarding( + self.app, + self.namespace + ) + + def test_set_nothing(self): + arglist = [ + self._port_forwarding.floatingip_id, + self._port_forwarding.id, + ] + verifylist = [ + ('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 = {} + self.network.update_floating_ip_port_forwarding.assert_called_with( + self._port_forwarding.floatingip_id, + self._port_forwarding.id, + **attrs + ) + self.assertIsNone(result) + + def test_set_all_thing(self): + arglist = [ + '--port', self.port.id, + '--internal-ip-address', 'new_internal_ip_address', + '--internal-protocol-port', '100', + '--external-protocol-port', '200', + '--protocol', 'tcp', + self._port_forwarding.floatingip_id, + self._port_forwarding.id, + ] + verifylist = [ + ('port', self.port.id), + ('internal_ip_address', 'new_internal_ip_address'), + ('internal_protocol_port', 100), + ('external_protocol_port', 200), + ('protocol', 'tcp'), + ('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 = { + 'internal_port_id': self.port.id, + 'internal_ip_address': 'new_internal_ip_address', + 'internal_port': 100, + 'external_port': 200, + 'protocol': 'tcp', + } + self.network.update_floating_ip_port_forwarding.assert_called_with( + self._port_forwarding.floatingip_id, + self._port_forwarding.id, + **attrs + ) + self.assertIsNone(result) + + +class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): + + # The port forwarding to show. + columns = ( + 'external_port', + 'floatingip_id', + 'id', + 'internal_ip_address', + 'internal_port', + 'internal_port_id', + 'project_id', + 'protocol', + ) + + def setUp(self): + project_id = '' + super(TestShowFloatingIPPortForwarding, self).setUp() + self._port_forwarding = ( + network_fakes.FakeFloatingIPPortForwarding. + create_one_port_forwarding( + attrs={ + 'floatingip_id': self.floating_ip.id, + } + ) + ) + self.data = ( + self._port_forwarding.external_port, + 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, + project_id, + self._port_forwarding.protocol, + ) + self.network.find_floating_ip_port_forwarding = mock.Mock( + return_value=self._port_forwarding + ) + self.network.find_ip = mock.Mock( + return_value=self.floating_ip + ) + # Get the command object to test + self.cmd = floating_ip_port_forwarding.ShowFloatingIPPortForwarding( + self.app, + self.namespace + ) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_default_options(self): + arglist = [ + self._port_forwarding.floatingip_id, + self._port_forwarding.id, + ] + verifylist = [ + ('floating_ip', self._port_forwarding.floatingip_id), + ('port_forwarding_id', self._port_forwarding.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_floating_ip_port_forwarding.assert_called_once_with( + self.floating_ip, + self._port_forwarding.id, + ignore_missing=False + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) diff --git a/releasenotes/notes/add-fip-portforwarding-commands-6e4d8ace698ee308.yaml b/releasenotes/notes/add-fip-portforwarding-commands-6e4d8ace698ee308.yaml new file mode 100644 index 0000000000..87175e4f6e --- /dev/null +++ b/releasenotes/notes/add-fip-portforwarding-commands-6e4d8ace698ee308.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add floating IP Port Forwarding commands: + ``floating ip port forwarding create``, + ``floating ip port forwarding delete``, + ``floating ip port forwarding list``, + ``floating ip port forwarding set`` and + ``floating ip port forwarding show``. diff --git a/setup.cfg b/setup.cfg index a9f7de2ce1..80d7772653 100644 --- a/setup.cfg +++ b/setup.cfg @@ -384,6 +384,12 @@ openstack.network.v2 = floating_ip_pool_list = openstackclient.network.v2.floating_ip_pool:ListFloatingIPPool + floating_ip_port_forwarding_create = openstackclient.network.v2.floating_ip_port_forwarding:CreateFloatingIPPortForwarding + floating_ip_port_forwarding_delete = openstackclient.network.v2.floating_ip_port_forwarding:DeleteFloatingIPPortForwarding + floating_ip_port_forwarding_list = openstackclient.network.v2.floating_ip_port_forwarding:ListFloatingIPPortForwarding + floating_ip_port_forwarding_set = openstackclient.network.v2.floating_ip_port_forwarding:SetFloatingIPPortForwarding + floating_ip_port_forwarding_show = openstackclient.network.v2.floating_ip_port_forwarding:ShowFloatingIPPortForwarding + ip_availability_list = openstackclient.network.v2.ip_availability:ListIPAvailability ip_availability_show = openstackclient.network.v2.ip_availability:ShowIPAvailability From 6ee7b8d138e07bfc37c5cd887f7afa49cdabb02f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 30 Aug 2019 12:53:15 -0500 Subject: [PATCH 0127/1120] Format location columns in network commands These return a Munch from the SDK, which can be handled exactly like a dict so do that. Note that the location column has a nested project dict in the return value, this is addressed separately in osc_lib.format_columns in https://review.opendev.org/#/c/679474/. Change-Id: I99a6d192749a4ac76777f72be8118261c0521cb0 Signed-off-by: Dean Troyer --- openstackclient/network/v2/address_scope.py | 10 ++++++++-- openstackclient/network/v2/floating_ip.py | 2 ++ openstackclient/network/v2/ip_availability.py | 2 ++ openstackclient/network/v2/network.py | 1 + openstackclient/network/v2/network_agent.py | 1 + .../v2/network_auto_allocated_topology.py | 17 ++++++++++++----- openstackclient/network/v2/network_flavor.py | 10 ++++++++-- .../network/v2/network_flavor_profile.py | 10 ++++++++-- openstackclient/network/v2/network_meter.py | 10 ++++++++-- .../network/v2/network_meter_rule.py | 10 ++++++++-- .../network/v2/network_qos_policy.py | 10 ++++++++-- openstackclient/network/v2/network_qos_rule.py | 10 ++++++++-- .../network/v2/network_qos_rule_type.py | 8 +++++++- openstackclient/network/v2/network_rbac.py | 10 ++++++++-- openstackclient/network/v2/network_segment.py | 10 ++++++++-- .../network/v2/network_segment_range.py | 9 +++++++-- openstackclient/network/v2/port.py | 1 + openstackclient/network/v2/router.py | 1 + openstackclient/network/v2/security_group.py | 3 +++ .../network/v2/security_group_rule.py | 10 ++++++++-- openstackclient/network/v2/subnet.py | 1 + openstackclient/network/v2/subnet_pool.py | 1 + 22 files changed, 119 insertions(+), 28 deletions(-) diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index 71c1a9afb7..7efbb6311e 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,6 +28,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -100,7 +106,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_address_scope(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -289,6 +295,6 @@ def take_action(self, parsed_args): parsed_args.address_scope, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index 8ac8e10703..e0aa0435ed 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -13,6 +13,7 @@ """IP Floating action implementations""" +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -24,6 +25,7 @@ _formatters = { + 'location': format_columns.DictColumn, 'port_details': utils.format_dict, } diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index ddc88e557e..c026baa0cc 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -21,7 +21,9 @@ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils + _formatters = { + 'location': format_columns.DictColumn, 'subnet_ip_availability': format_columns.ListDictColumn, } diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 63aec71497..09f3556c43 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -40,6 +40,7 @@ def human_readable(self): 'subnet_ids': format_columns.ListColumn, 'admin_state_up': AdminStateColumn, 'is_admin_state_up': AdminStateColumn, + 'location': format_columns.DictColumn, 'router:external': RouterExternalColumn, 'is_router_external': RouterExternalColumn, 'availability_zones': format_columns.ListColumn, diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index 1678485434..a6ed36299c 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -43,6 +43,7 @@ def human_readable(self): 'alive': AliveColumn, 'admin_state_up': AdminStateColumn, 'is_admin_state_up': AdminStateColumn, + 'location': format_columns.DictColumn, 'configurations': format_columns.DictColumn, } diff --git a/openstackclient/network/v2/network_auto_allocated_topology.py b/openstackclient/network/v2/network_auto_allocated_topology.py index 36f392006e..f6070a0277 100644 --- a/openstackclient/network/v2/network_auto_allocated_topology.py +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -25,6 +26,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -93,16 +99,17 @@ def check_resource_topology(self, client, parsed_args): obj = client.validate_auto_allocated_topology(parsed_args.project) columns = _format_check_resource_columns() - data = utils.get_item_properties(_format_check_resource(obj), - columns, - formatters={}) - + data = utils.get_item_properties( + _format_check_resource(obj), + columns, + formatters=_formatters, + ) return (columns, data) def get_topology(self, client, parsed_args): obj = client.get_auto_allocated_topology(parsed_args.project) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) def take_action(self, parsed_args): diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py index c9d368bfc1..355d04c1af 100644 --- a/openstackclient/network/v2/network_flavor.py +++ b/openstackclient/network/v2/network_flavor.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,6 +28,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'is_enabled': 'enabled', @@ -136,7 +142,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_flavor(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -300,5 +306,5 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_flavor(parsed_args.flavor, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py index 6cf0c4124d..492fd432a9 100644 --- a/openstackclient/network/v2/network_flavor_profile.py +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -13,6 +13,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -25,6 +26,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'is_enabled': 'enabled', @@ -110,7 +116,7 @@ def take_action(self, parsed_args): obj = client.create_service_profile(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -246,5 +252,5 @@ def take_action(self, parsed_args): obj = client.find_service_profile(parsed_args.flavor_profile, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py index df0e1da119..cde7a30403 100644 --- a/openstackclient/network/v2/network_meter.py +++ b/openstackclient/network/v2/network_meter.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,6 +27,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -102,7 +108,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_metering_label(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -186,5 +192,5 @@ def take_action(self, parsed_args): obj = client.find_metering_label(parsed_args.meter, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index 49ff9e1b0e..5f31255a69 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,6 +27,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -116,7 +122,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_metering_label_rule(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -199,5 +205,5 @@ def take_action(self, parsed_args): obj = client.find_metering_label_rule(parsed_args.meter_rule_id, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index fd5ff93771..1622de4a7a 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,6 +28,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -119,7 +125,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_qos_policy(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters={}) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -279,5 +285,5 @@ def take_action(self, parsed_args): obj = client.find_qos_policy(parsed_args.policy, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index 28c5600aa6..d74beda70f 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -15,6 +15,7 @@ import itertools +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -46,6 +47,11 @@ ACTION_SHOW = 'get' +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -208,7 +214,7 @@ def take_action(self, parsed_args): msg = (_('Failed to create Network QoS rule: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data @@ -358,5 +364,5 @@ def take_action(self, parsed_args): {'rule': rule_id, 'e': e}) raise exceptions.CommandError(msg) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data diff --git a/openstackclient/network/v2/network_qos_rule_type.py b/openstackclient/network/v2/network_qos_rule_type.py index 7b92c8ad4b..e842944cce 100644 --- a/openstackclient/network/v2/network_qos_rule_type.py +++ b/openstackclient/network/v2/network_qos_rule_type.py @@ -13,6 +13,7 @@ # 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 osc_lib import utils @@ -20,6 +21,11 @@ from openstackclient.network import sdk_utils +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { "type": "rule_type_name", @@ -65,5 +71,5 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.get_qos_rule_type(parsed_args.rule_type) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 140c837e38..1781193f61 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,6 +28,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): column_map = { 'target_tenant': 'target_project_id', @@ -136,7 +142,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_rbac_policy(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data @@ -293,5 +299,5 @@ def take_action(self, parsed_args): obj = client.find_rbac_policy(parsed_args.rbac_policy, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return display_columns, data diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index c1a672e2d5..5899dc697b 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -15,6 +15,7 @@ import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,6 +27,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) @@ -90,7 +96,7 @@ def take_action(self, parsed_args): attrs['segmentation_id'] = parsed_args.segment obj = client.create_segment(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -242,5 +248,5 @@ def take_action(self, parsed_args): ignore_missing=False ) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index b938bdd920..f03bcc1c0b 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -19,6 +19,7 @@ import itertools import logging +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -31,6 +32,10 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) @@ -212,7 +217,7 @@ def take_action(self, parsed_args): attrs['physical_network'] = parsed_args.physical_network obj = network_client.create_network_segment_range(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) @@ -451,6 +456,6 @@ def take_action(self, parsed_args): ignore_missing=False ) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index e6bfe8537f..4d7e518975 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -51,6 +51,7 @@ def human_readable(self): 'dns_assignment': format_columns.ListDictColumn, 'extra_dhcp_opts': format_columns.ListDictColumn, 'fixed_ips': format_columns.ListDictColumn, + 'location': format_columns.DictColumn, 'security_group_ids': format_columns.ListColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 11b012e67e..02da50c38e 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -61,6 +61,7 @@ def human_readable(self): 'external_gateway_info': RouterInfoColumn, 'availability_zones': format_columns.ListColumn, 'availability_zone_hints': format_columns.ListColumn, + 'location': format_columns.DictColumn, 'routes': RoutesColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index e389473828..38b4e97a80 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -16,6 +16,7 @@ import argparse from cliff import columns as cliff_columns +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils import six @@ -77,11 +78,13 @@ def human_readable(self): _formatters_network = { + 'location': format_columns.DictColumn, 'security_group_rules': NetworkSecurityGroupRulesColumn, } _formatters_compute = { + 'location': format_columns.DictColumn, 'rules': ComputeSecurityGroupRulesColumn, } diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index dbea747300..15f099b1eb 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -16,6 +16,7 @@ import argparse import logging +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils @@ -31,6 +32,11 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'location': format_columns.DictColumn, +} + + def _format_security_group_rule_show(obj): data = network_utils.transform_compute_security_group_rule(obj) return zip(*sorted(six.iteritems(data))) @@ -337,7 +343,7 @@ def take_action_network(self, client, parsed_args): # Create and show the security group rule. obj = client.create_security_group_rule(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) def take_action_compute(self, client, parsed_args): @@ -596,7 +602,7 @@ def take_action_network(self, client, parsed_args): if not obj['remote_ip_prefix']: obj['remote_ip_prefix'] = _format_remote_ip_prefix(obj) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns) + data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) def take_action_compute(self, client, parsed_args): diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 1f0c2d9478..c53688615f 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -61,6 +61,7 @@ def human_readable(self): 'allocation_pools': AllocationPoolsColumn, 'dns_nameservers': format_columns.ListColumn, 'host_routes': HostRoutesColumn, + 'location': format_columns.DictColumn, 'service_types': format_columns.ListColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 7ece263a36..d5a15475ef 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -42,6 +42,7 @@ def _get_columns(item): _formatters = { + 'location': format_columns.DictColumn, 'prefixes': format_columns.ListColumn, 'tags': format_columns.ListColumn, } From a88e95873e3a5b2f5f7e8cb7ee82f426d9c320ae Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 29 Aug 2019 00:01:37 -0500 Subject: [PATCH 0128/1120] Bump min osc-lib to 1.14.0 This is required for osc4 and the movement of functions out of OSC. Change-Id: I690954b6dccb11dd1a4f512b6777d645de5191f9 Signed-off-by: Dean Troyer --- lower-constraints.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 710ff0e1c4..2593dd309a 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -54,7 +54,7 @@ openstacksdk==0.17.0 os-client-config==1.28.0 os-service-types==1.2.0 os-testr==1.0.0 -osc-lib==1.10.0 +osc-lib==1.14.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.19.2 diff --git a/requirements.txt b/requirements.txt index cf67906646..1044c39ee2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 keystoneauth1>=3.6.2 # Apache-2.0 openstacksdk>=0.17.0 # Apache-2.0 -osc-lib>=1.10.0 # Apache-2.0 +osc-lib>=1.14.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 From 31c47adebb68b7ef508310bc9651fec0a26cd818 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sat, 31 Aug 2019 09:03:41 -0500 Subject: [PATCH 0129/1120] Remove races in floating ip functional tests Multiple subnets with the same name are occasionally created when running tests in parallel. Change-Id: Ifb85e39ee53b529e2b97abf782c7fba93d48e9e2 Signed-off-by: Dean Troyer --- .../functional/network/v2/test_floating_ip.py | 120 ++++++++++-------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index f189c2da6c..9d109f878b 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -24,12 +24,9 @@ class FloatingIpTests(common.NetworkTests): def setUpClass(cls): common.NetworkTests.setUpClass() if cls.haz_network: + # Create common networks that all tests share cls.EXTERNAL_NETWORK_NAME = uuid.uuid4().hex - cls.EXTERNAL_SUBNET_NAME = uuid.uuid4().hex cls.PRIVATE_NETWORK_NAME = uuid.uuid4().hex - cls.PRIVATE_SUBNET_NAME = uuid.uuid4().hex - cls.ROUTER = uuid.uuid4().hex - cls.PORT_NAME = uuid.uuid4().hex # Create a network for the floating ip json_output = json.loads(cls.openstack( @@ -46,56 +43,10 @@ def setUpClass(cls): )) cls.private_network_id = json_output["id"] - # Try random subnet range for subnet creating - # Because we can not determine ahead of time what subnets are - # already in use, possibly by another test running in parallel, - # try 4 times - for i in range(4): - # Make a random subnet - cls.external_subnet = ".".join(map( - str, - (random.randint(0, 223) for _ in range(3)) - )) + ".0/26" - cls.private_subnet = ".".join(map( - str, - (random.randint(0, 223) for _ in range(3)) - )) + ".0/26" - try: - # Create a subnet for the network - json_output = json.loads(cls.openstack( - 'subnet create -f json ' + - '--network ' + cls.EXTERNAL_NETWORK_NAME + ' ' + - '--subnet-range ' + cls.external_subnet + ' ' + - cls.EXTERNAL_SUBNET_NAME - )) - cls.external_subnet_id = json_output["id"] - # Create a subnet for the private network - json_output = json.loads(cls.openstack( - 'subnet create -f json ' + - '--network ' + cls.PRIVATE_NETWORK_NAME + ' ' + - '--subnet-range ' + cls.private_subnet + ' ' + - cls.PRIVATE_SUBNET_NAME - )) - cls.private_subnet_id = json_output["id"] - except Exception: - if (i == 3): - # raise the exception at the last time - raise - pass - else: - # break and no longer retry if create successfully - break - @classmethod def tearDownClass(cls): try: if cls.haz_network: - del_output = cls.openstack( - 'subnet delete ' + - cls.EXTERNAL_SUBNET_NAME + ' ' + - cls.PRIVATE_SUBNET_NAME - ) - cls.assertOutput('', del_output) del_output = cls.openstack( 'network delete ' + cls.EXTERNAL_NETWORK_NAME + ' ' + @@ -114,11 +65,50 @@ def setUp(self): # Verify setup self.assertIsNotNone(self.external_network_id) self.assertIsNotNone(self.private_network_id) - self.assertIsNotNone(self.external_subnet_id) - self.assertIsNotNone(self.private_subnet_id) + + def _create_subnet(self, network_name, subnet_name): + subnet_id = None + + # Try random subnet range for subnet creating + # Because we can not determine ahead of time what subnets are + # already in use, possibly by another test running in parallel, + # try 4 times + for i in range(4): + # Make a random subnet + subnet = ".".join(map( + str, + (random.randint(0, 223) for _ in range(3)) + )) + ".0/26" + try: + # Create a subnet for the network + json_output = json.loads(self.openstack( + 'subnet create -f json ' + + '--network ' + network_name + ' ' + + '--subnet-range ' + subnet + ' ' + + subnet_name + )) + self.assertIsNotNone(json_output["id"]) + subnet_id = json_output["id"] + except Exception: + if (i == 3): + # raise the exception at the last time + raise + pass + else: + # break and no longer retry if create successfully + break + return subnet_id def test_floating_ip_delete(self): """Test create, delete multiple""" + + # Subnets must exist even if not directly referenced here + ext_subnet_id = self._create_subnet( + self.EXTERNAL_NETWORK_NAME, + "ext-test-delete" + ) + self.addCleanup(self.openstack, 'subnet delete ' + ext_subnet_id) + json_output = json.loads(self.openstack( 'floating ip create -f json ' + '--description aaaa ' + @@ -151,6 +141,14 @@ def test_floating_ip_delete(self): def test_floating_ip_list(self): """Test create defaults, list filters, delete""" + + # Subnets must exist even if not directly referenced here + ext_subnet_id = self._create_subnet( + self.EXTERNAL_NETWORK_NAME, + "ext-test-delete" + ) + self.addCleanup(self.openstack, 'subnet delete ' + ext_subnet_id) + json_output = json.loads(self.openstack( 'floating ip create -f json ' + '--description aaaa ' + @@ -237,6 +235,22 @@ def test_floating_ip_list(self): def test_floating_ip_set_and_unset_port(self): """Test Floating IP Set and Unset port""" + + # Subnets must exist even if not directly referenced here + ext_subnet_id = self._create_subnet( + self.EXTERNAL_NETWORK_NAME, + "ext-test-delete" + ) + self.addCleanup(self.openstack, 'subnet delete ' + ext_subnet_id) + priv_subnet_id = self._create_subnet( + self.PRIVATE_NETWORK_NAME, + "priv-test-delete" + ) + self.addCleanup(self.openstack, 'subnet delete ' + priv_subnet_id) + + self.ROUTER = uuid.uuid4().hex + self.PORT_NAME = uuid.uuid4().hex + json_output = json.loads(self.openstack( 'floating ip create -f json ' + '--description aaaa ' + @@ -253,7 +267,7 @@ def test_floating_ip_set_and_unset_port(self): json_output = json.loads(self.openstack( 'port create -f json ' + '--network ' + self.PRIVATE_NETWORK_NAME + ' ' + - '--fixed-ip subnet=' + self.PRIVATE_SUBNET_NAME + ' ' + + '--fixed-ip subnet=' + priv_subnet_id + ' ' + self.PORT_NAME )) self.assertIsNotNone(json_output["id"]) From 7549d260aa8724f2104a84cfb2cbd88e24b4778c Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Sun, 1 Sep 2019 22:35:58 +0000 Subject: [PATCH 0130/1120] Bump lower constraint of python-zunclient Projects that depends on python-zunclient should use the latest version as lower constraint. Change-Id: Idc865788f35427cc0f2926b31089ec4097831334 --- lower-constraints.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 2593dd309a..7993a18325 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -109,7 +109,7 @@ python-subunit==1.0.0 python-swiftclient==3.2.0 python-troveclient==2.2.0 python-zaqarclient==1.0.0 -python-zunclient==1.3.0 +python-zunclient==3.4.0 pytz==2013.6 PyYAML==3.12 repoze.lru==0.7 diff --git a/test-requirements.txt b/test-requirements.txt index 64e7a0773b..4db30c7fca 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -38,4 +38,4 @@ python-searchlightclient>=1.0.0 #Apache-2.0 python-senlinclient>=1.1.0 # Apache-2.0 python-troveclient>=2.2.0 # Apache-2.0 python-zaqarclient>=1.0.0 # Apache-2.0 -python-zunclient>=1.3.0 # Apache-2.0 +python-zunclient>=3.4.0 # Apache-2.0 From 7c1b6a799e0ac6fea511a2cf1e97aebb2f94e0d6 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 19 Aug 2019 11:13:14 +0900 Subject: [PATCH 0131/1120] Add parent project filter for listing projects This patch introduces a new option --parent into project list, to specify a parent project to filter projects which has the given project as their parent. Depends-on: https://review.opendev.org/#/c/677101 Change-Id: I6725262cf040e0ec6ceca9cf0462ce59224049c6 --- doc/source/cli/command-objects/project.rst | 7 ++++++ openstackclient/identity/v3/project.py | 10 +++++++++ .../tests/unit/identity/v3/test_project.py | 22 +++++++++++++++++++ ...t-option-to-projects-10382a7176993366.yaml | 5 +++++ 4 files changed, 44 insertions(+) create mode 100644 releasenotes/notes/add-parent-list-option-to-projects-10382a7176993366.yaml diff --git a/doc/source/cli/command-objects/project.rst b/doc/source/cli/command-objects/project.rst index 6891a79a48..ac7e8cd1ce 100644 --- a/doc/source/cli/command-objects/project.rst +++ b/doc/source/cli/command-objects/project.rst @@ -102,6 +102,7 @@ List projects openstack project list [--domain ] + [--parent ] [--user ] [--my-projects] [--long] @@ -115,6 +116,12 @@ List projects .. versionadded:: 3 +.. option:: --parent + + Filter projects whose parent is :option:`\ <--parent>` (name or ID) + + .. versionadded:: 3 + .. option:: --user Filter projects by :option:`\ <--user>` (name or ID) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index e819a0a89b..073fb6df11 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -185,6 +185,11 @@ def get_parser(self, prog_name): metavar='', help=_('Filter projects by (name or ID)'), ) + parser.add_argument( + '--parent', + metavar='', + help=_('Filter projects whose parent is (name or ID)'), + ) parser.add_argument( '--user', metavar='', @@ -226,6 +231,11 @@ def take_action(self, parsed_args): parsed_args.domain).id kwargs['domain'] = domain_id + if parsed_args.parent: + parent_id = common.find_project(identity_client, + parsed_args.parent).id + kwargs['parent'] = parent_id + if parsed_args.user: if parsed_args.domain: user_id = utils.find_resource(identity_client.users, diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 266da22775..db27fedcdf 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -645,6 +645,28 @@ def test_project_list_domain_no_perms(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) + def test_project_list_parent(self): + self.parent = identity_fakes.FakeProject.create_one_project() + self.project = identity_fakes.FakeProject.create_one_project( + attrs={'domain_id': self.domain.id, 'parent_id': self.parent.id}) + + arglist = [ + '--parent', self.parent.id, + ] + verifylist = [ + ('parent', self.parent.id), + ] + + self.projects_mock.get.return_value = self.parent + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with(parent=self.parent.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + def test_project_list_sort(self): self.projects_mock.list.return_value = self.projects diff --git a/releasenotes/notes/add-parent-list-option-to-projects-10382a7176993366.yaml b/releasenotes/notes/add-parent-list-option-to-projects-10382a7176993366.yaml new file mode 100644 index 0000000000..fe1801179b --- /dev/null +++ b/releasenotes/notes/add-parent-list-option-to-projects-10382a7176993366.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--parent`` option to ``project list`` command to filter projects + by the specified parent project. From fcd46acb69250205e18274cc9a59ab5f46d075c3 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Fri, 14 Jun 2019 16:40:40 +0800 Subject: [PATCH 0132/1120] Microversion 2.77: Support Specifying AZ to unshelve This patch adds a new parameter ``--availability-zone`` to ``openstack server unshelve`` command. This can help users to specify an ``availability_zone`` to unshelve a shelve offloaded server from 2.77 microversion. Depends-On: https://review.opendev.org/679295 Implements: blueprint support-specifying-az-when-restore-shelved-server Change-Id: Ia431e27c2a17fe16466707cc362532860ecf22df --- lower-constraints.txt | 2 +- openstackclient/compute/v2/server.py | 28 ++++++++-- .../tests/unit/compute/v2/test_server.py | 54 +++++++++++++++++++ ...store-shelved-server-16e864223d51b50a.yaml | 7 +++ requirements.txt | 2 +- 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 7993a18325..a84643caed 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -99,7 +99,7 @@ python-mimeparse==1.6.0 python-mistralclient==3.1.0 python-muranoclient==0.8.2 python-neutronclient==6.7.0 -python-novaclient==14.2.0 +python-novaclient==15.0.0 python-octaviaclient==1.3.0 python-rsdclient==0.1.0 python-saharaclient==1.4.0 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 9ba918123e..538c9c4fa0 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2811,12 +2811,32 @@ def get_parser(self, prog_name): nargs='+', help=_('Server(s) to unshelve (name or ID)'), ) + parser.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)'), + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + support_az = compute_client.api_version >= api_versions.APIVersion( + '2.77') + if not support_az and parsed_args.availability_zone: + msg = _("--os-compute-api-version 2.77 or greater is required " + "to support the '--availability-zone' option.") + raise exceptions.CommandError(msg) + for server in parsed_args.server: - utils.find_resource( - compute_client.servers, - server, - ).unshelve() + if support_az: + utils.find_resource( + compute_client.servers, + server + ).unshelve(availability_zone=parsed_args.availability_zone) + else: + utils.find_resource( + compute_client.servers, + server, + ).unshelve() diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 0793116af6..5c98188a76 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -109,6 +109,10 @@ def run_method_with_servers(self, method_name, server_count): version = self.app.client_manager.compute.api_version if version >= api_versions.APIVersion('2.73'): 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'): + method.assert_called_with(availability_zone=None) else: method.assert_called_with() else: @@ -4777,6 +4781,56 @@ def test_unshelve_one_server(self): def test_unshelve_multi_servers(self): self.run_method_with_servers('unshelve', 3) + def test_unshelve_server_with_specified_az(self): + server = compute_fakes.FakeServer.create_one_server() + arglist = [ + server.id, + '--availability-zone', "foo-az", + ] + verifylist = [ + ('availability_zone', "foo-az"), + ('server', [server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.77 or greater is required', str(ex)) + + +class TestServerUnshelveV277(TestServerUnshelve): + + def setUp(self): + super(TestServerUnshelveV277, self).setUp() + + self.server = compute_fakes.FakeServer.create_one_server( + methods=self.methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + # Get the command object to test + self.cmd = server.UnshelveServer(self.app, None) + + def test_specified_az_to_unshelve_with_v277(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.77') + + arglist = [ + '--availability-zone', "foo-az", + self.server.id, + ] + verifylist = [ + ('availability_zone', "foo-az"), + ('server', [self.server.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.servers_mock.get.assert_called_with(self.server.id) + self.server.unshelve.assert_called_with(availability_zone="foo-az") + class TestServerGeneral(TestServer): OLD = { diff --git a/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml b/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml new file mode 100644 index 0000000000..0e7dacbe17 --- /dev/null +++ b/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml @@ -0,0 +1,7 @@ +--- +features: + - Add ``--availability-zone`` option to ``server unshelve`` + command to enable users to specify an availability zone during + unshelve of a shelved offloaded server. Note that it requires + ``--os-compute-api-version 2.77`` or greater. + [Blueprint ` =3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.17.0 # Apache-2.0 -python-novaclient>=14.2.0 # Apache-2.0 +python-novaclient>=15.0.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From dd1ce370424264166be686578bcab0fa41ac973e Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Thu, 5 Sep 2019 11:11:24 +0800 Subject: [PATCH 0133/1120] Follow-up: fix the invalid releasenote link By reviewing OSC's releasenote document [1], I found that the blueprint index is invalid. This patch fixes this problem. [1]https://docs.openstack.org/releasenotes/python-openstackclient/unreleased.html#new-features Related-On: https://review.opendev.org/#/c/665336 Part of blueprint support-specifying-az-when-restore-shelved-server Change-Id: I14066dcfff5a585d51b4f365883a86a5cc086d45 --- ...cifying-az-when-restore-shelved-server-16e864223d51b50a.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml b/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml index 0e7dacbe17..adb55b1bbb 100644 --- a/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml +++ b/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-16e864223d51b50a.yaml @@ -4,4 +4,4 @@ features: command to enable users to specify an availability zone during unshelve of a shelved offloaded server. Note that it requires ``--os-compute-api-version 2.77`` or greater. - [Blueprint ` `_] From ee48777207e0682862cf43fdf2f2f5dffc37d22a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 22 Aug 2019 23:55:01 -0500 Subject: [PATCH 0134/1120] Clean up app initialization and config * Remove unnecessary code in OpenStackShell.initialize_app() - only the bits it instantiate our subclass of ClientManager remain * Remove OSC_Config - with https://review.opendev.org/#/c/678095/ the last remaining required bit moves to osc-lib Thos requires osc-lib 1.14.0 Change-Id: Ia4b3c737de9dc34949e74632441621014ef9eea9 Signed-off-by: Dean Troyer --- openstackclient/common/client_config.py | 71 ------------------------- openstackclient/shell.py | 33 +----------- 2 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 openstackclient/common/client_config.py diff --git a/openstackclient/common/client_config.py b/openstackclient/common/client_config.py deleted file mode 100644 index a22dd0cb3c..0000000000 --- a/openstackclient/common/client_config.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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. -# - -"""OpenStackConfig subclass for argument compatibility""" - -from osc_lib.cli import client_config - - -# Sublcass OpenStackConfig in order to munge config values -# before auth plugins are loaded -class OSC_Config(client_config.OSC_Config): - - # TODO(dtroyer): Remove _auth_default_domain when the v3otp fix is - # backported to osc-lib, should be in release 1.3.0 - def _auth_default_domain(self, config): - """Set a default domain from available arguments - - Migrated from clientmanager.setup_auth() - """ - - identity_version = config.get('identity_api_version', '') - auth_type = config.get('auth_type', None) - - # TODO(mordred): This is a usability improvement that's broadly useful - # We should port it back up into os-client-config. - default_domain = config.get('default_domain', None) - if (identity_version == '3' and - not auth_type.startswith('v2') and - default_domain): - - # NOTE(stevemar): If PROJECT_DOMAIN_ID or PROJECT_DOMAIN_NAME is - # present, then do not change the behaviour. Otherwise, set the - # PROJECT_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. - if ( - auth_type in ("password", "v3password", "v3totp") and - not config['auth'].get('project_domain_id') and - not config['auth'].get('project_domain_name') - ): - config['auth']['project_domain_id'] = default_domain - - # NOTE(stevemar): If USER_DOMAIN_ID or USER_DOMAIN_NAME is present, - # then do not change the behaviour. Otherwise, set the - # USER_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. - # NOTE(aloga): this should only be set if there is a username. - # TODO(dtroyer): Move this to os-client-config after the plugin has - # been loaded so we can check directly if the options are accepted. - if ( - auth_type in ("password", "v3password", "v3totp") and - not config['auth'].get('user_domain_id') and - not config['auth'].get('user_domain_name') - ): - config['auth']['user_domain_id'] = default_domain - return config - - def load_auth_plugin(self, config): - """Get auth plugin and validate args""" - - loader = self._get_auth_loader(config) - config = self._validate_auth(config, loader) - auth_plugin = loader.load_from_options(**config['auth']) - return auth_plugin diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 4489219f9c..7fbda1aee5 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -25,7 +25,6 @@ import six import openstackclient -from openstackclient.common import client_config as cloud_config from openstackclient.common import clientmanager @@ -133,37 +132,7 @@ def _load_commands(self): def initialize_app(self, argv): super(OpenStackShell, self).initialize_app(argv) - # Argument precedence is really broken in multiple places - # so we're just going to fix it here until o-c-c and osc-lib - # get sorted out. - # TODO(dtroyer): remove when os-client-config and osc-lib are fixed - - # First, throw away what has already been done with o-c-c and - # use our own. - try: - self.cloud_config = cloud_config.OSC_Config( - override_defaults={ - 'interface': None, - 'auth_type': self._auth_type, - }, - ) - except (IOError, OSError): - self.log.critical("Could not read clouds.yaml configuration file") - self.print_help_if_requested() - raise - - if not self.options.debug: - self.options.debug = None - - # NOTE(dtroyer): Need to do this with validate=False to defer the - # auth plugin handling to ClientManager.setup_auth() - self.cloud = self.cloud_config.get_one_cloud( - cloud=self.options.cloud, - argparse=self.options, - validate=False, - ) - - # Then, re-create the client_manager with the correct arguments + # Re-create the client_manager with our subclass self.client_manager = clientmanager.ClientManager( cli_options=self.cloud, api_version=self.api_version, From a96089ff6d54237a9e058dc1c7e6fc7e25695ecb Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Wed, 14 Aug 2019 14:35:21 -0500 Subject: [PATCH 0135/1120] Default to Cinder v3 API This switches the default Cinder API version to v3 to prepare for v2 going away. Change-Id: Icca1512b715409e3001c0fd2d1ea663d5e71ec02 Signed-off-by: Sean McGinnis --- openstackclient/volume/client.py | 2 +- releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index e0e670a9ce..fdd1794be5 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -22,7 +22,7 @@ LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '2' +DEFAULT_API_VERSION = '3' API_VERSION_OPTION = 'os_volume_api_version' API_NAME = "volume" API_VERSIONS = { diff --git a/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml b/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml new file mode 100644 index 0000000000..a38a5434d8 --- /dev/null +++ b/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + Volume commands now default to Volume API 3. On older clouds + that do not support Volume 3.x ``--os-volume-api-version 2`` + of the adition of ``volume_api_version: '2' in ``clouds.yaml`` + will be required. From 037a800538feb690af0541d4f276dcdf48af3f72 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 9 Sep 2019 10:31:43 -0500 Subject: [PATCH 0136/1120] Add doc and relnote for review 639652 https://review.opendev.org/639652/ Change-Id: I10c0f8a0e09150e7d516ed9cb7ffb2a8e8fe4911 Signed-off-by: Dean Troyer --- doc/source/cli/command-objects/usage.rst | 3 +++ releasenotes/notes/usage-list-all-49ca7a62c50e71d3.yaml | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 releasenotes/notes/usage-list-all-49ca7a62c50e71d3.yaml diff --git a/doc/source/cli/command-objects/usage.rst b/doc/source/cli/command-objects/usage.rst index 9cd0f70eb3..bb13176cc8 100644 --- a/doc/source/cli/command-objects/usage.rst +++ b/doc/source/cli/command-objects/usage.rst @@ -9,6 +9,9 @@ usage list List resource usage per project +Compute API v2.40+ returns all matching entities rather than being +limited to the API server configured maximum (``CONF.api.max_limit``). + .. program:: usage list .. code:: bash diff --git a/releasenotes/notes/usage-list-all-49ca7a62c50e71d3.yaml b/releasenotes/notes/usage-list-all-49ca7a62c50e71d3.yaml new file mode 100644 index 0000000000..76a09cc546 --- /dev/null +++ b/releasenotes/notes/usage-list-all-49ca7a62c50e71d3.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Compute API v2.40+ returns all matching entities rather than being + limited to the API server configured maximum (``CONF.api.max_limit``). + [Story `2005099 `_] From 609988ebac2c6bbb596aa77b46589274ffbc1e07 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 10 Jun 2019 17:09:56 +0100 Subject: [PATCH 0137/1120] Add 'openstack server migrate (confirm|revert)' commands While cold migration and resize are essentially the same operation under the hood, meaning one could use the 'openstack server resize confirm' and 'openstack server resize revert' commands instead, there is no reason the operator needs to know this. Add these flags as syntactic sugar to help simplify operators lives. The help texts for both the 'openstack server resize' and 'openstack server migrate' commands are updated to clarify the relationship between the two operations. Change-Id: I0cb6304c794bffaec785add9f7b8cf53ab28cacd Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 31 ++++++++++++++++--- ...firm-revert-commands-9d8079c9fddea36d.yaml | 6 ++++ setup.cfg | 2 ++ 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-server-migrate-confirm-revert-commands-9d8079c9fddea36d.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 493fd5ad9c..096291382e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1555,7 +1555,15 @@ def take_action(self, parsed_args): # then adding the groups doesn't seem to work class MigrateServer(command.Command): - _description = _("Migrate server to different host") + _description = _("""Migrate server to different host. + +A migrate operation is implemented as a resize operation using the same flavor +as the old server. This means that, like resize, migrate works by creating a +new server using the same flavor and copying the contents of the original disk +into a new one. As with resize, the migrate operation is a two-step process for +the user: the first step is to perform the migrate, and the second step is to +either confirm (verify) success and release the old server, or to declare a +revert to release the new server and restart the old one.""") def get_parser(self, prog_name): parser = super(MigrateServer, self).get_parser(prog_name) @@ -2159,10 +2167,10 @@ class ResizeServer(command.Command): _description = _("""Scale server to a new flavor. A resize operation is implemented by creating a new server and copying the -contents of the original disk into a new one. It is also a two-step process for -the user: the first is to perform the resize, the second is to either confirm -(verify) success and release the old server, or to declare a revert to release -the new server and restart the old one.""") +contents of the original disk into a new one. It is a two-step process for the +user: the first step is to perform the resize, and the second step is to either +confirm (verify) success and release the old server or to declare a revert to +release the new server and restart the old one.""") def get_parser(self, prog_name): parser = super(ResizeServer, self).get_parser(prog_name) @@ -2261,6 +2269,12 @@ def take_action(self, parsed_args): server.confirm_resize() +class MigrateConfirm(ResizeConfirm): + _description = _("""Confirm server migrate. + +Confirm (verify) success of migrate operation and release the old server.""") + + class ResizeRevert(command.Command): _description = _("""Revert server resize. @@ -2286,6 +2300,13 @@ def take_action(self, parsed_args): server.revert_resize() +class MigrateRevert(ResizeRevert): + _description = _("""Revert server migrate. + +Revert the migrate operation. Release the new server and restart the old +one.""") + + class RestoreServer(command.Command): _description = _("Restore server(s)") diff --git a/releasenotes/notes/add-server-migrate-confirm-revert-commands-9d8079c9fddea36d.yaml b/releasenotes/notes/add-server-migrate-confirm-revert-commands-9d8079c9fddea36d.yaml new file mode 100644 index 0000000000..d1fe2411dd --- /dev/null +++ b/releasenotes/notes/add-server-migrate-confirm-revert-commands-9d8079c9fddea36d.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``server migrate confirm`` and ``server migrate revert`` commands. + These are aliases of the ``server resize confirm`` and ``server resize revert`` + commands, respectively. diff --git a/setup.cfg b/setup.cfg index a9f7de2ce1..694b682c86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -108,6 +108,8 @@ openstack.compute.v2 = server_list = openstackclient.compute.v2.server:ListServer server_lock = openstackclient.compute.v2.server:LockServer server_migrate = openstackclient.compute.v2.server:MigrateServer + server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm + server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert server_pause = openstackclient.compute.v2.server:PauseServer server_reboot = openstackclient.compute.v2.server:RebootServer server_rebuild = openstackclient.compute.v2.server:RebuildServer From de8ab5e8fd05c3942a8ec332c01f4c92213f52d4 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 9 Sep 2019 10:54:34 -0500 Subject: [PATCH 0138/1120] More aggregate functional race chasing AggregateTests.wait_for_status() was a classmethod, those often are sources of conflict in parallel testing... Change-Id: I6211fd9c36926ca97de51a11923933d4d9d2dfda Signed-off-by: Dean Troyer --- .../tests/functional/compute/v2/test_aggregate.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index 8a082e82ad..be318ae5e3 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -20,15 +20,14 @@ class AggregateTests(base.TestCase): """Functional tests for aggregate""" - @classmethod - def wait_for_status(cls, check_type, check_name, desired_status, + def wait_for_status(self, check_type, check_name, desired_status, wait=120, interval=5, failures=None): current_status = "notset" if failures is None: failures = ['error'] total_sleep = 0 while total_sleep < wait: - output = json.loads(cls.openstack( + output = json.loads(self.openstack( check_type + ' show -f json ' + check_name)) current_status = output['name'] if (current_status == desired_status): @@ -44,7 +43,7 @@ def wait_for_status(cls, check_type, check_name, desired_status, .format(current_status, check_type, check_name, failures)) time.sleep(interval) total_sleep += interval - cls.assertOutput(desired_status, current_status) + self.assertOutput(desired_status, current_status) def test_aggregate_crud(self): """Test create, delete multiple""" @@ -106,7 +105,6 @@ def test_aggregate_crud(self): name1 ) self.assertOutput('', raw_output) - self.wait_for_status('aggregate', name3, name3) self.addCleanup( self.openstack, 'aggregate delete ' + name3, From cb0c20b23c564dc6b260492923aef58ee66620d9 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 9 Sep 2019 11:48:28 -0500 Subject: [PATCH 0139/1120] Update release table for Train and 4.0.0 Also clean up some docs and release notes. Change-Id: I73feec747ca1bd12be5e5700c9ca608ed3a8b2c2 Signed-off-by: Dean Troyer --- README.rst | 16 ++-------------- doc/source/cli/authentication.rst | 2 -- doc/source/index.rst | 3 +-- ...erver-migrate-with-host-4884a71903c5c8a9.yaml | 2 +- ...rt-security-group-rules-95272847349982e5.yaml | 5 ++--- .../notes/bug-1716789-abfae897b7e61246.yaml | 15 ++++----------- .../volume-v3-default-0ffa9bebb43b5057.yaml | 2 +- releasenotes/source/index.rst | 2 ++ 8 files changed, 13 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index e1824aed6d..cd80079fab 100644 --- a/README.rst +++ b/README.rst @@ -16,8 +16,8 @@ OpenStackClient :alt: Latest Version OpenStackClient (aka OSC) is a command-line client for OpenStack that brings -the command set for Compute, Identity, Image, Object Store and Block Storage -APIs together in a single shell with a uniform command structure. +the command set for Compute, Identity, Image, Network, Object Store and Block +Storage APIs together in a single shell with a uniform command structure. The primary goal is to provide a unified shell command structure and a common language to describe operations in OpenStack. @@ -100,15 +100,3 @@ The corresponding command-line options look very similar:: If a password is not provided above (in plaintext), you will be interactively prompted to provide one securely. - -Authentication may also be performed using an already-acquired token -and a URL pointing directly to the service API that presumably was acquired -from the Service Catalog:: - - export OS_TOKEN= - export OS_URL= - -The corresponding command-line options look very similar:: - - --os-token - --os-url diff --git a/doc/source/cli/authentication.rst b/doc/source/cli/authentication.rst index f8aaadf492..3b404bce5b 100644 --- a/doc/source/cli/authentication.rst +++ b/doc/source/cli/authentication.rst @@ -67,8 +67,6 @@ by the ``ClientManager`` object. is selected based on the existing options. This is a short-circuit evaluation, the first match wins. - * If ``--os-url`` and ``--os-token`` are both present ``token_endpoint`` - is selected * If ``--os-username`` is supplied ``password`` is selected * If ``--os-token`` is supplied ``token`` is selected * If no selection has been made by now exit with error diff --git a/doc/source/index.rst b/doc/source/index.rst index abe0803374..ea4cd3e9f9 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -55,7 +55,7 @@ Contributing OpenStackClient utilizes all of the usual OpenStack processes and requirements for contributions. The code is hosted `on OpenStack's Git server`_. `Bug reports`_ -and `blueprints`_ may be submitted to the :code:`python-openstackclient` project +may be submitted to the :code:`python-openstackclient` Storyboard project on `Launchpad`_. Code may be submitted to the :code:`openstack/python-openstackclient` project using `Gerrit`_. Developers may also be found in the `IRC channel`_ ``#openstack-sdks``. @@ -64,7 +64,6 @@ Developers may also be found in the `IRC channel`_ ``#openstack-sdks``. .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Bug reports: https://storyboard.openstack.org/#!/project/975 -.. _blueprints: https://blueprints.launchpad.net/python-openstackclient .. _PyPi: https://pypi.org/project/python-openstackclient .. _tarball: http://tarballs.openstack.org/python-openstackclient .. _IRC channel: https://wiki.openstack.org/wiki/IRC diff --git a/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml b/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml index dd974ff0b3..8da704ac2d 100644 --- a/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml +++ b/releasenotes/notes/add-server-migrate-with-host-4884a71903c5c8a9.yaml @@ -1,7 +1,7 @@ --- features: - | - Added the ability to specify ``--host`` with ``server migrate`` + Add ``--host`` option to ``server migrate`` command (cold migration) to specify the target host of the migration. Requires ``--os-compute-api-version`` 2.56 or greater to target a specific host for the (cold) migration. diff --git a/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml b/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml index 6cac67cf88..7177873a02 100644 --- a/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml +++ b/releasenotes/notes/better-ipv6-address-suppport-security-group-rules-95272847349982e5.yaml @@ -1,9 +1,8 @@ --- features: - | - Security group rules can now be filtered by Ethertype in - ``security group rule list`` using ``--ethertype`` with either - ``ipv4`` or ``ipv6`` as an argument. + Add ``--ethertype`` option to ``security group rule list`` command. + Valid values are ``ipv4`` and ``ipv6``. upgrade: - | Security group rule listings now have the ``Ethertype`` field displayed diff --git a/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml b/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml index 1fd0a13de6..4177f80bd1 100644 --- a/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml +++ b/releasenotes/notes/bug-1716789-abfae897b7e61246.yaml @@ -1,17 +1,10 @@ --- -features: +fixes: - | - Change to use ``any`` as the default ``--protocol`` option to + Change the default value of ``--protocol`` option to ``any`` in ``security group rule create`` command when using the Neutron v2 API. [Bug `1716789 `_] -fixes: - - | - The default protocol used to create a security rule was changed to - ``tcp``, which was a regression from the neutron client when using - the Neutron v2 API. Change it back to ``any``, which skips sending - the protocol to the API server entirely. upgrade: - | - Users that had been creating rules without specifying a protocol - and expecting ``tcp`` need to change to use ``--protocol tcp`` - explicitly when using the Neutron v2 API. + Commands that assumed the default value of ``--protocol`` to be ``tcp`` + now must include ``--protocol tcp`` explicitly in Network commands. diff --git a/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml b/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml index a38a5434d8..6b0840777f 100644 --- a/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml +++ b/releasenotes/notes/volume-v3-default-0ffa9bebb43b5057.yaml @@ -3,5 +3,5 @@ upgrade: - | Volume commands now default to Volume API 3. On older clouds that do not support Volume 3.x ``--os-volume-api-version 2`` - of the adition of ``volume_api_version: '2' in ``clouds.yaml`` + or the adition of ``volume_api_version: '2'`` in ``clouds.yaml`` will be required. diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 9e2d8ce10b..c9cc7a253c 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -27,6 +27,8 @@ OpenStack release was made is shown below: ================= ======================= OpenStack Release OpenStackClient Release ================= ======================= +Train 4.0.0 +Stein 3.18.0 Rocky 3.16.0 Queens 3.14.0 Pike 3.12.0 From b4e9b225b4435a7a3598b70617c1e6eb73613fa0 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Tue, 3 Sep 2019 17:36:56 +0000 Subject: [PATCH 0140/1120] Add dns_publish_fixed_ip attribute to subnets With the subnet_dns_publish_fixed_ip extension Neutron has added a new attribute to subnets, allowing to select whether DNS records should be published for fixed IPs from that subnet. Add support for this when creating and updating subnets. [0] https://bugs.launchpad.net/neutron/+bug/1784879 [1] https://review.opendev.org/662405 [2] https://review.opendev.org/662409 Depends-On: https://review.opendev.org/679833 Change-Id: Ia804e878acfd1f05e1f00c2ac9202c1d260827f4 --- openstackclient/network/v2/subnet.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 1f0c2d9478..60994acacd 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -229,6 +229,10 @@ def _get_attrs(client_manager, parsed_args, is_create=True): attrs['enable_dhcp'] = True if parsed_args.no_dhcp: attrs['enable_dhcp'] = False + if parsed_args.dns_publish_fixed_ip: + attrs['dns_publish_fixed_ip'] = True + if parsed_args.no_dns_publish_fixed_ip: + attrs['dns_publish_fixed_ip'] = False if ('dns_nameservers' in parsed_args and parsed_args.dns_nameservers is not None): attrs['dns_nameservers'] = parsed_args.dns_nameservers @@ -302,6 +306,17 @@ def get_parser(self, prog_name): action='store_true', help=_("Disable DHCP") ) + dns_publish_fixed_ip_group = parser.add_mutually_exclusive_group() + dns_publish_fixed_ip_group.add_argument( + '--dns-publish-fixed-ip', + action='store_true', + help=_("Enable publishing fixed IPs in DNS") + ) + dns_publish_fixed_ip_group.add_argument( + '--no-dns-publish-fixed-ip', + action='store_true', + help=_("Disable publishing fixed IPs in DNS (default)") + ) parser.add_argument( '--gateway', metavar='', @@ -557,6 +572,17 @@ def get_parser(self, prog_name): action='store_true', help=_("Disable DHCP") ) + dns_publish_fixed_ip_group = parser.add_mutually_exclusive_group() + dns_publish_fixed_ip_group.add_argument( + '--dns-publish-fixed-ip', + action='store_true', + help=_("Enable publishing fixed IPs in DNS") + ) + dns_publish_fixed_ip_group.add_argument( + '--no-dns-publish-fixed-ip', + action='store_true', + help=_("Disable publishing fixed IPs in DNS") + ) parser.add_argument( '--gateway', metavar='', From 9ad343968947fc025b530644c3369c60a2c14ea5 Mon Sep 17 00:00:00 2001 From: Andreas Florath Date: Thu, 19 Sep 2019 11:44:14 +0000 Subject: [PATCH 0141/1120] Fix osc-lib interface change: catch osc-lib Forbidden The patch https://review.opendev.org/#/c/673389/ introduced a regression by changing the osc-lib interface. The patch https://review.opendev.org/683119 changes the exception from the generic CommandError back to a specific Forbidden exception. This patch catches this exception and passes on, i.e. re-implements the same behavior as before. Story: 2006547 Change-Id: I17b1ec7abaa5b0828ccbcad40bd928565c5c59fb Signed-off-by: Andreas Florath --- openstackclient/identity/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index e825116649..d125d2851d 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -207,7 +207,7 @@ def _find_identity_resource(identity_client_manager, name_or_id, name_or_id, **kwargs) if identity_resource is not None: return identity_resource - except identity_exc.Forbidden: + except exceptions.Forbidden: pass return resource_type(None, {'id': name_or_id, 'name': name_or_id}) From 154df0d069cdf8a10678b9f6d1b715350847fe96 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 4 Oct 2019 13:06:18 +0000 Subject: [PATCH 0142/1120] Update master for stable/train Add file to the reno documentation build to show release notes for stable/train. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/train. Change-Id: I2cab236b14bc5eac173d815a634c8d8eb2afee04 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/train.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/train.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index c9cc7a253c..fed41f9ec4 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + train stein rocky queens diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 0000000000..583900393c --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train From cf1006bf0ef67a693357ba3913962037aaf6c3d5 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 21 Oct 2019 16:09:10 -0500 Subject: [PATCH 0143/1120] Fix plugin autodoc generation Documentation for plugins wasn't being generated because the plugin projects weren't being installed in the docs environment. Add them to doc/requirements.txt to make this work. Change-Id: Id9be39971110fd2eb4519a0582c9bf2514cdcacd Story: #1735016 Task: #13825 --- doc/requirements.txt | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index 91854914a5..3bf3a2708b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -6,3 +6,25 @@ reno>=2.5.0 # Apache-2.0 sphinx!=1.6.6,!=1.6.7,>=1.6.5,<2.0.0;python_version=='2.7' # BSD sphinx!=1.6.6,!=1.6.7,>=1.6.5;python_version>='3.4' # BSD sphinxcontrib-apidoc>=0.2.0 # BSD + +# Install these to generate sphinx autodocs +aodhclient>=0.9.0 # Apache-2.0 +gnocchiclient>=3.3.1 # Apache-2.0 +python-barbicanclient>=4.5.2 # Apache-2.0 +python-congressclient<2000,>=1.9.0 # Apache-2.0 +python-designateclient>=2.7.0 # Apache-2.0 +python-heatclient>=1.10.0 # Apache-2.0 +python-ironicclient>=2.3.0 # Apache-2.0 +python-ironic-inspector-client>=1.5.0 # Apache-2.0 +python-karborclient>=0.6.0 # Apache-2.0 +python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 +python-muranoclient>=0.8.2 # Apache-2.0 +python-neutronclient>=6.7.0 # Apache-2.0 +python-octaviaclient>=1.3.0 # Apache-2.0 +python-rsdclient>=0.1.0 # Apache-2.0 +python-saharaclient>=1.4.0 # Apache-2.0 +python-searchlightclient>=1.0.0 #Apache-2.0 +python-senlinclient>=1.1.0 # Apache-2.0 +python-troveclient>=2.2.0 # Apache-2.0 +python-zaqarclient>=1.0.0 # Apache-2.0 +python-zunclient>=3.4.0 # Apache-2.0 From 8cdc7348d184555cac3e32facaf949784f0564d2 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 21 Oct 2019 15:45:36 -0500 Subject: [PATCH 0144/1120] Add placement to known plugins Add the osc-placement project (osc plugin for the placement service) to the list of known plugins. Change-Id: I77b614b38ecf872d0d93473b834994913930b76f --- doc/requirements.txt | 1 + doc/source/cli/plugin-commands.rst | 6 ++++++ doc/source/contributor/plugins.rst | 1 + lower-constraints.txt | 1 + test-requirements.txt | 1 + 5 files changed, 10 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index 3bf3a2708b..12ed5f01a2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -10,6 +10,7 @@ sphinxcontrib-apidoc>=0.2.0 # BSD # Install these to generate sphinx autodocs aodhclient>=0.9.0 # Apache-2.0 gnocchiclient>=3.3.1 # Apache-2.0 +osc-placement>=1.7.0 # Apache-2.0 python-barbicanclient>=4.5.2 # Apache-2.0 python-congressclient<2000,>=1.9.0 # Apache-2.0 python-designateclient>=2.7.0 # Apache-2.0 diff --git a/doc/source/cli/plugin-commands.rst b/doc/source/cli/plugin-commands.rst index ae2e655712..5796420555 100644 --- a/doc/source/cli/plugin-commands.rst +++ b/doc/source/cli/plugin-commands.rst @@ -87,6 +87,12 @@ octavia .. list-plugins:: openstack.load_balancer.v2 :detailed: +placement +--------- + +.. list-plugins:: openstack.placement.v1 + :detailed: + rsd --- diff --git a/doc/source/contributor/plugins.rst b/doc/source/contributor/plugins.rst index effcd85ac6..d6bcbee197 100644 --- a/doc/source/contributor/plugins.rst +++ b/doc/source/contributor/plugins.rst @@ -25,6 +25,7 @@ The following is a list of projects that are an OpenStackClient plugin. - aodhclient - gnocchiclient +- osc-placement - python-barbicanclient - python-congressclient - python-designateclient diff --git a/lower-constraints.txt b/lower-constraints.txt index a84643caed..db0562e124 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -55,6 +55,7 @@ os-client-config==1.28.0 os-service-types==1.2.0 os-testr==1.0.0 osc-lib==1.14.0 +osc-placement==1.7.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.19.2 diff --git a/test-requirements.txt b/test-requirements.txt index 4db30c7fca..71adb0d1d1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -21,6 +21,7 @@ wrapt>=1.7.0 # BSD License # Install these to generate sphinx autodocs aodhclient>=0.9.0 # Apache-2.0 gnocchiclient>=3.3.1 # Apache-2.0 +osc-placement>=1.7.0 # Apache-2.0 python-barbicanclient>=4.5.2 # Apache-2.0 python-congressclient<2000,>=1.9.0 # Apache-2.0 python-designateclient>=2.7.0 # Apache-2.0 From abdec78fa331ce724008ba864841f36fde294fd9 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 21 Oct 2019 16:37:41 -0500 Subject: [PATCH 0145/1120] Remove plugin projects from test-requirements.txt Plugin projects were listed in test-requirements.txt ostensibly to pull them in for autodoc building. However, test-requirements.txt wasn't being used for that purpose -- doc/requirements.txt was, so [1] added them there. So this commit removes them from test-requirements.txt where they're unused. [1] Id9be39971110fd2eb4519a0582c9bf2514cdcacd Change-Id: Ia89888ee05e17da636ee46894232624e0178d6bc --- test-requirements.txt | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 71adb0d1d1..0f59e46faa 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -17,26 +17,3 @@ tempest>=17.1.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 bandit!=1.6.0,>=1.1.0 # Apache-2.0 wrapt>=1.7.0 # BSD License - -# Install these to generate sphinx autodocs -aodhclient>=0.9.0 # Apache-2.0 -gnocchiclient>=3.3.1 # Apache-2.0 -osc-placement>=1.7.0 # Apache-2.0 -python-barbicanclient>=4.5.2 # Apache-2.0 -python-congressclient<2000,>=1.9.0 # Apache-2.0 -python-designateclient>=2.7.0 # Apache-2.0 -python-heatclient>=1.10.0 # Apache-2.0 -python-ironicclient>=2.3.0 # Apache-2.0 -python-ironic-inspector-client>=1.5.0 # Apache-2.0 -python-karborclient>=0.6.0 # Apache-2.0 -python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 -python-muranoclient>=0.8.2 # Apache-2.0 -python-neutronclient>=6.7.0 # Apache-2.0 -python-octaviaclient>=1.3.0 # Apache-2.0 -python-rsdclient>=0.1.0 # Apache-2.0 -python-saharaclient>=1.4.0 # Apache-2.0 -python-searchlightclient>=1.0.0 #Apache-2.0 -python-senlinclient>=1.1.0 # Apache-2.0 -python-troveclient>=2.2.0 # Apache-2.0 -python-zaqarclient>=1.0.0 # Apache-2.0 -python-zunclient>=3.4.0 # Apache-2.0 From 85c83530ee63e1c66bc132d4b0c97d7f2b077ee8 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 21 Oct 2019 17:05:57 -0500 Subject: [PATCH 0146/1120] Split plugin docs per project Once [1] fixed plugin doc generation, the (single) page it produced was unusably huge. This commit splits it into one page per project. Note that there are four plugin projects that didn't have sections included: - cue - murano - tripleo - watcher These were noted in hidden rst which is preserved in the (new) index page. [1] Id9be39971110fd2eb4519a0582c9bf2514cdcacd Change-Id: I0214ddb00a5a292a46d7cfb539d6dcc540fdae79 Story: #1735016 Task: #37239 --- doc/source/_extra/.htaccess | 1 + doc/source/cli/index.rst | 2 +- doc/source/cli/plugin-commands.rst | 146 ------------------ doc/source/cli/plugin-commands/aodh.rst | 5 + doc/source/cli/plugin-commands/barbican.rst | 5 + doc/source/cli/plugin-commands/congress.rst | 5 + doc/source/cli/plugin-commands/designate.rst | 5 + doc/source/cli/plugin-commands/gnocchi.rst | 5 + doc/source/cli/plugin-commands/heat.rst | 5 + doc/source/cli/plugin-commands/index.rst | 51 ++++++ .../cli/plugin-commands/ironic-inspector.rst | 5 + doc/source/cli/plugin-commands/ironic.rst | 5 + doc/source/cli/plugin-commands/karbor.rst | 5 + doc/source/cli/plugin-commands/mistral.rst | 5 + doc/source/cli/plugin-commands/neutron.rst | 5 + doc/source/cli/plugin-commands/octavia.rst | 5 + doc/source/cli/plugin-commands/placement.rst | 5 + doc/source/cli/plugin-commands/rsd.rst | 5 + doc/source/cli/plugin-commands/sahara.rst | 5 + .../cli/plugin-commands/searchlight.rst | 5 + doc/source/cli/plugin-commands/senlin.rst | 5 + doc/source/cli/plugin-commands/trove.rst | 5 + doc/source/cli/plugin-commands/zaqar.rst | 5 + doc/source/cli/plugin-commands/zun.rst | 5 + 24 files changed, 153 insertions(+), 147 deletions(-) delete mode 100644 doc/source/cli/plugin-commands.rst create mode 100644 doc/source/cli/plugin-commands/aodh.rst create mode 100644 doc/source/cli/plugin-commands/barbican.rst create mode 100644 doc/source/cli/plugin-commands/congress.rst create mode 100644 doc/source/cli/plugin-commands/designate.rst create mode 100644 doc/source/cli/plugin-commands/gnocchi.rst create mode 100644 doc/source/cli/plugin-commands/heat.rst create mode 100644 doc/source/cli/plugin-commands/index.rst create mode 100644 doc/source/cli/plugin-commands/ironic-inspector.rst create mode 100644 doc/source/cli/plugin-commands/ironic.rst create mode 100644 doc/source/cli/plugin-commands/karbor.rst create mode 100644 doc/source/cli/plugin-commands/mistral.rst create mode 100644 doc/source/cli/plugin-commands/neutron.rst create mode 100644 doc/source/cli/plugin-commands/octavia.rst create mode 100644 doc/source/cli/plugin-commands/placement.rst create mode 100644 doc/source/cli/plugin-commands/rsd.rst create mode 100644 doc/source/cli/plugin-commands/sahara.rst create mode 100644 doc/source/cli/plugin-commands/searchlight.rst create mode 100644 doc/source/cli/plugin-commands/senlin.rst create mode 100644 doc/source/cli/plugin-commands/trove.rst create mode 100644 doc/source/cli/plugin-commands/zaqar.rst create mode 100644 doc/source/cli/plugin-commands/zun.rst diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess index d4c092b5ab..fd91901e87 100644 --- a/doc/source/_extra/.htaccess +++ b/doc/source/_extra/.htaccess @@ -20,3 +20,4 @@ redirectmatch 301 ^/python-openstackclient/([^/]+)/command-wrappers.html$ /pytho redirectmatch 301 ^/python-openstackclient/([^/]+)/developing.html$ /python-openstackclient/$1/contributor/developing.html redirectmatch 301 ^/python-openstackclient/([^/]+)/humaninterfaceguide.html$ /python-openstackclient/$1/contributor/humaninterfaceguide.html redirectmatch 301 ^/python-openstackclient/([^/]+)/plugins.html$ /python-openstackclient/$1/contributor/plugins.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/plugin-commands.html$ /python-openstackclient/$1/cli/plugin-commands/index.html diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index e9aab0afae..17e50a07d2 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -8,7 +8,7 @@ Manual Page command-list commands - plugin-commands + plugin-commands/index authentication interactive decoder diff --git a/doc/source/cli/plugin-commands.rst b/doc/source/cli/plugin-commands.rst deleted file mode 100644 index 5796420555..0000000000 --- a/doc/source/cli/plugin-commands.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. _plugin-commands: - -=============== -Plugin Commands -=============== - -.. list-plugins:: openstack.cli.extension - -aodh ----- - -.. list-plugins:: openstack.alarming.v2 - :detailed: - -barbican --------- - -.. list-plugins:: openstack.key_manager.v1 - :detailed: - -congress --------- - -.. list-plugins:: openstack.congressclient.v1 - :detailed: - -.. cue -.. # cueclient is not in global-requirements -.. # list-plugins:: openstack.mb.v1 -.. # :detailed: - -designate ---------- - -.. list-plugins:: openstack.dns.v2 - :detailed: - -gnocchi -------- -.. list-plugins:: openstack.metric.v1 - :detailed: - -heat ----- - -.. list-plugins:: openstack.orchestration.v1 - :detailed: - -ironic ------- - -.. list-plugins:: openstack.baremetal.v1 - :detailed: - -ironic-inspector ----------------- - -.. list-plugins:: openstack.baremetal_introspection.v1 - :detailed: - -karbor ------- - -.. list-plugins:: openstack.data_protection.v1 - :detailed: - -mistral -------- - -.. list-plugins:: openstack.workflow_engine.v2 - :detailed: - -.. murano -.. # the murano docs cause warnings and a broken docs build -.. # .. list-plugins:: openstack.application_catalog.v1 -.. # :detailed: - -neutron -------- - -.. list-plugins:: openstack.neutronclient.v2 - :detailed: - -octavia -------- - -.. list-plugins:: openstack.load_balancer.v2 - :detailed: - -placement ---------- - -.. list-plugins:: openstack.placement.v1 - :detailed: - -rsd ---- - -.. list-plugins:: openstack.rsd.v1 - :detailed: - -sahara ------- - -.. list-plugins:: openstack.data_processing.v1 - :detailed: - -searchlight ------------ - -.. list-plugins:: openstack.search.v1 - :detailed: - -senlin ------- - -.. list-plugins:: openstack.clustering.v1 - :detailed: - -.. tripleo -.. # tripleoclient is not in global-requirements -.. # list-plugins:: openstack.tripleoclient.v1 -.. # :detailed: - -trove ------- - -.. list-plugins:: openstack.database.v1 - :detailed: - -.. watcher -.. # watcherclient is not in global-requirements -.. # list-plugins:: openstack.infra_optim.v1 -.. # :detailed: - -zaqar ------ - -.. list-plugins:: openstack.messaging.v2 - :detailed: - -zun ---- - -.. list-plugins:: openstack.container.v1 - :detailed: diff --git a/doc/source/cli/plugin-commands/aodh.rst b/doc/source/cli/plugin-commands/aodh.rst new file mode 100644 index 0000000000..26e7bf70fa --- /dev/null +++ b/doc/source/cli/plugin-commands/aodh.rst @@ -0,0 +1,5 @@ +aodh +---- + +.. list-plugins:: openstack.alarming.v2 + :detailed: diff --git a/doc/source/cli/plugin-commands/barbican.rst b/doc/source/cli/plugin-commands/barbican.rst new file mode 100644 index 0000000000..21b9b5545b --- /dev/null +++ b/doc/source/cli/plugin-commands/barbican.rst @@ -0,0 +1,5 @@ +barbican +-------- + +.. list-plugins:: openstack.key_manager.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/congress.rst b/doc/source/cli/plugin-commands/congress.rst new file mode 100644 index 0000000000..c21e2fa399 --- /dev/null +++ b/doc/source/cli/plugin-commands/congress.rst @@ -0,0 +1,5 @@ +congress +-------- + +.. list-plugins:: openstack.congressclient.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/designate.rst b/doc/source/cli/plugin-commands/designate.rst new file mode 100644 index 0000000000..94071bf547 --- /dev/null +++ b/doc/source/cli/plugin-commands/designate.rst @@ -0,0 +1,5 @@ +designate +--------- + +.. list-plugins:: openstack.dns.v2 + :detailed: diff --git a/doc/source/cli/plugin-commands/gnocchi.rst b/doc/source/cli/plugin-commands/gnocchi.rst new file mode 100644 index 0000000000..a545a5eab2 --- /dev/null +++ b/doc/source/cli/plugin-commands/gnocchi.rst @@ -0,0 +1,5 @@ +gnocchi +------- + +.. list-plugins:: openstack.metric.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/heat.rst b/doc/source/cli/plugin-commands/heat.rst new file mode 100644 index 0000000000..3699aeaf4a --- /dev/null +++ b/doc/source/cli/plugin-commands/heat.rst @@ -0,0 +1,5 @@ +heat +---- + +.. list-plugins:: openstack.orchestration.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst new file mode 100644 index 0000000000..f6ff51bd63 --- /dev/null +++ b/doc/source/cli/plugin-commands/index.rst @@ -0,0 +1,51 @@ +.. _plugin-commands: + +=============== +Plugin Commands +=============== + +.. toctree:: + :maxdepth: 1 + + aodh + barbican + congress + designate + gnocchi + heat + ironic + ironic-inspector + karbor + mistral + neutron + octavia + placement + rsd + sahara + searchlight + senlin + trove + zaqar + zun + +.. TODO(efried): Make pages for the following once they're fixed. + +.. cue +.. # cueclient is not in global-requirements +.. # list-plugins:: openstack.mb.v1 +.. # :detailed: + +.. murano +.. # the murano docs cause warnings and a broken docs build +.. # .. list-plugins:: openstack.application_catalog.v1 +.. # :detailed: + +.. tripleo +.. # tripleoclient is not in global-requirements +.. # list-plugins:: openstack.tripleoclient.v1 +.. # :detailed: + +.. watcher +.. # watcherclient is not in global-requirements +.. # list-plugins:: openstack.infra_optim.v1 +.. # :detailed: diff --git a/doc/source/cli/plugin-commands/ironic-inspector.rst b/doc/source/cli/plugin-commands/ironic-inspector.rst new file mode 100644 index 0000000000..c149066212 --- /dev/null +++ b/doc/source/cli/plugin-commands/ironic-inspector.rst @@ -0,0 +1,5 @@ +ironic-inspector +---------------- + +.. list-plugins:: openstack.baremetal_introspection.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/ironic.rst b/doc/source/cli/plugin-commands/ironic.rst new file mode 100644 index 0000000000..032c57d286 --- /dev/null +++ b/doc/source/cli/plugin-commands/ironic.rst @@ -0,0 +1,5 @@ +ironic +------ + +.. list-plugins:: openstack.baremetal.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/karbor.rst b/doc/source/cli/plugin-commands/karbor.rst new file mode 100644 index 0000000000..aed14a6691 --- /dev/null +++ b/doc/source/cli/plugin-commands/karbor.rst @@ -0,0 +1,5 @@ +karbor +------ + +.. list-plugins:: openstack.data_protection.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/mistral.rst b/doc/source/cli/plugin-commands/mistral.rst new file mode 100644 index 0000000000..3facc506b5 --- /dev/null +++ b/doc/source/cli/plugin-commands/mistral.rst @@ -0,0 +1,5 @@ +mistral +------- + +.. list-plugins:: openstack.workflow_engine.v2 + :detailed: diff --git a/doc/source/cli/plugin-commands/neutron.rst b/doc/source/cli/plugin-commands/neutron.rst new file mode 100644 index 0000000000..6e67ae9480 --- /dev/null +++ b/doc/source/cli/plugin-commands/neutron.rst @@ -0,0 +1,5 @@ +neutron +------- + +.. list-plugins:: openstack.neutronclient.v2 + :detailed: diff --git a/doc/source/cli/plugin-commands/octavia.rst b/doc/source/cli/plugin-commands/octavia.rst new file mode 100644 index 0000000000..5384530fc8 --- /dev/null +++ b/doc/source/cli/plugin-commands/octavia.rst @@ -0,0 +1,5 @@ +octavia +------- + +.. list-plugins:: openstack.load_balancer.v2 + :detailed: diff --git a/doc/source/cli/plugin-commands/placement.rst b/doc/source/cli/plugin-commands/placement.rst new file mode 100644 index 0000000000..972818c768 --- /dev/null +++ b/doc/source/cli/plugin-commands/placement.rst @@ -0,0 +1,5 @@ +placement +--------- + +.. list-plugins:: openstack.placement.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/rsd.rst b/doc/source/cli/plugin-commands/rsd.rst new file mode 100644 index 0000000000..d7962014e5 --- /dev/null +++ b/doc/source/cli/plugin-commands/rsd.rst @@ -0,0 +1,5 @@ +rsd +--- + +.. list-plugins:: openstack.rsd.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/sahara.rst b/doc/source/cli/plugin-commands/sahara.rst new file mode 100644 index 0000000000..28ac867828 --- /dev/null +++ b/doc/source/cli/plugin-commands/sahara.rst @@ -0,0 +1,5 @@ +sahara +------ + +.. list-plugins:: openstack.data_processing.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/searchlight.rst b/doc/source/cli/plugin-commands/searchlight.rst new file mode 100644 index 0000000000..fed56f0912 --- /dev/null +++ b/doc/source/cli/plugin-commands/searchlight.rst @@ -0,0 +1,5 @@ +searchlight +----------- + +.. list-plugins:: openstack.search.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/senlin.rst b/doc/source/cli/plugin-commands/senlin.rst new file mode 100644 index 0000000000..f5f81d9e13 --- /dev/null +++ b/doc/source/cli/plugin-commands/senlin.rst @@ -0,0 +1,5 @@ +senlin +------ + +.. list-plugins:: openstack.clustering.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/trove.rst b/doc/source/cli/plugin-commands/trove.rst new file mode 100644 index 0000000000..b4575edd8a --- /dev/null +++ b/doc/source/cli/plugin-commands/trove.rst @@ -0,0 +1,5 @@ +trove +----- + +.. list-plugins:: openstack.database.v1 + :detailed: diff --git a/doc/source/cli/plugin-commands/zaqar.rst b/doc/source/cli/plugin-commands/zaqar.rst new file mode 100644 index 0000000000..3649a274d3 --- /dev/null +++ b/doc/source/cli/plugin-commands/zaqar.rst @@ -0,0 +1,5 @@ +zaqar +----- + +.. list-plugins:: openstack.messaging.v2 + :detailed: diff --git a/doc/source/cli/plugin-commands/zun.rst b/doc/source/cli/plugin-commands/zun.rst new file mode 100644 index 0000000000..56c5ff59ce --- /dev/null +++ b/doc/source/cli/plugin-commands/zun.rst @@ -0,0 +1,5 @@ +zun +--- + +.. list-plugins:: openstack.container.v1 + :detailed: From 3fd63c0021d5ddddfb0ecce5d5f38799bcb13661 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 21 Oct 2019 17:31:04 -0500 Subject: [PATCH 0147/1120] Produce complete content for plugin docs Plugin documentation previously used ``.. list-plugins::`` for each plugin command, which just produced the summary line. This might be useful if there were also a link to the complete (per-project) docs for the commands; but since we have the content available, we might as well produce the complete plugin docs inline. That's going to be most useful to a reader anyway. So this commit switches from ``list-plugins`` to ``autoprogram-cliff`` -- except for the following, whose docs break the build when this is done: - octavia - rsd - trove - zun These fixups are tracked under task #37241 under this same story. Change-Id: I2f17e203fe3da92a709884c9052c8e39ff87f4c8 Story: #1735016 Task: #37240 --- doc/source/cli/plugin-commands/aodh.rst | 3 +-- doc/source/cli/plugin-commands/barbican.rst | 3 +-- doc/source/cli/plugin-commands/congress.rst | 3 +-- doc/source/cli/plugin-commands/designate.rst | 3 +-- doc/source/cli/plugin-commands/gnocchi.rst | 3 +-- doc/source/cli/plugin-commands/heat.rst | 3 +-- doc/source/cli/plugin-commands/ironic-inspector.rst | 3 +-- doc/source/cli/plugin-commands/ironic.rst | 3 +-- doc/source/cli/plugin-commands/karbor.rst | 3 +-- doc/source/cli/plugin-commands/mistral.rst | 3 +-- doc/source/cli/plugin-commands/neutron.rst | 3 +-- doc/source/cli/plugin-commands/octavia.rst | 2 ++ doc/source/cli/plugin-commands/placement.rst | 3 +-- doc/source/cli/plugin-commands/rsd.rst | 2 ++ doc/source/cli/plugin-commands/sahara.rst | 3 +-- doc/source/cli/plugin-commands/searchlight.rst | 3 +-- doc/source/cli/plugin-commands/senlin.rst | 3 +-- doc/source/cli/plugin-commands/trove.rst | 2 ++ doc/source/cli/plugin-commands/zaqar.rst | 3 +-- doc/source/cli/plugin-commands/zun.rst | 2 ++ 20 files changed, 24 insertions(+), 32 deletions(-) diff --git a/doc/source/cli/plugin-commands/aodh.rst b/doc/source/cli/plugin-commands/aodh.rst index 26e7bf70fa..5d8b4332cf 100644 --- a/doc/source/cli/plugin-commands/aodh.rst +++ b/doc/source/cli/plugin-commands/aodh.rst @@ -1,5 +1,4 @@ aodh ---- -.. list-plugins:: openstack.alarming.v2 - :detailed: +.. autoprogram-cliff:: openstack.alarming.v2 diff --git a/doc/source/cli/plugin-commands/barbican.rst b/doc/source/cli/plugin-commands/barbican.rst index 21b9b5545b..224b274f42 100644 --- a/doc/source/cli/plugin-commands/barbican.rst +++ b/doc/source/cli/plugin-commands/barbican.rst @@ -1,5 +1,4 @@ barbican -------- -.. list-plugins:: openstack.key_manager.v1 - :detailed: +.. autoprogram-cliff:: openstack.key_manager.v1 diff --git a/doc/source/cli/plugin-commands/congress.rst b/doc/source/cli/plugin-commands/congress.rst index c21e2fa399..7a1e63b76a 100644 --- a/doc/source/cli/plugin-commands/congress.rst +++ b/doc/source/cli/plugin-commands/congress.rst @@ -1,5 +1,4 @@ congress -------- -.. list-plugins:: openstack.congressclient.v1 - :detailed: +.. autoprogram-cliff:: openstack.congressclient.v1 diff --git a/doc/source/cli/plugin-commands/designate.rst b/doc/source/cli/plugin-commands/designate.rst index 94071bf547..045072b243 100644 --- a/doc/source/cli/plugin-commands/designate.rst +++ b/doc/source/cli/plugin-commands/designate.rst @@ -1,5 +1,4 @@ designate --------- -.. list-plugins:: openstack.dns.v2 - :detailed: +.. autoprogram-cliff:: openstack.dns.v2 diff --git a/doc/source/cli/plugin-commands/gnocchi.rst b/doc/source/cli/plugin-commands/gnocchi.rst index a545a5eab2..bbc7f6c8eb 100644 --- a/doc/source/cli/plugin-commands/gnocchi.rst +++ b/doc/source/cli/plugin-commands/gnocchi.rst @@ -1,5 +1,4 @@ gnocchi ------- -.. list-plugins:: openstack.metric.v1 - :detailed: +.. autoprogram-cliff:: openstack.metric.v1 diff --git a/doc/source/cli/plugin-commands/heat.rst b/doc/source/cli/plugin-commands/heat.rst index 3699aeaf4a..025561425a 100644 --- a/doc/source/cli/plugin-commands/heat.rst +++ b/doc/source/cli/plugin-commands/heat.rst @@ -1,5 +1,4 @@ heat ---- -.. list-plugins:: openstack.orchestration.v1 - :detailed: +.. autoprogram-cliff:: openstack.orchestration.v1 diff --git a/doc/source/cli/plugin-commands/ironic-inspector.rst b/doc/source/cli/plugin-commands/ironic-inspector.rst index c149066212..7ac12618e2 100644 --- a/doc/source/cli/plugin-commands/ironic-inspector.rst +++ b/doc/source/cli/plugin-commands/ironic-inspector.rst @@ -1,5 +1,4 @@ ironic-inspector ---------------- -.. list-plugins:: openstack.baremetal_introspection.v1 - :detailed: +.. autoprogram-cliff:: openstack.baremetal_introspection.v1 diff --git a/doc/source/cli/plugin-commands/ironic.rst b/doc/source/cli/plugin-commands/ironic.rst index 032c57d286..d9254e9bd3 100644 --- a/doc/source/cli/plugin-commands/ironic.rst +++ b/doc/source/cli/plugin-commands/ironic.rst @@ -1,5 +1,4 @@ ironic ------ -.. list-plugins:: openstack.baremetal.v1 - :detailed: +.. autoprogram-cliff:: openstack.baremetal.v1 diff --git a/doc/source/cli/plugin-commands/karbor.rst b/doc/source/cli/plugin-commands/karbor.rst index aed14a6691..0e28ba5733 100644 --- a/doc/source/cli/plugin-commands/karbor.rst +++ b/doc/source/cli/plugin-commands/karbor.rst @@ -1,5 +1,4 @@ karbor ------ -.. list-plugins:: openstack.data_protection.v1 - :detailed: +.. autoprogram-cliff:: openstack.data_protection.v1 diff --git a/doc/source/cli/plugin-commands/mistral.rst b/doc/source/cli/plugin-commands/mistral.rst index 3facc506b5..5f538d29fa 100644 --- a/doc/source/cli/plugin-commands/mistral.rst +++ b/doc/source/cli/plugin-commands/mistral.rst @@ -1,5 +1,4 @@ mistral ------- -.. list-plugins:: openstack.workflow_engine.v2 - :detailed: +.. autoprogram-cliff:: openstack.workflow_engine.v2 diff --git a/doc/source/cli/plugin-commands/neutron.rst b/doc/source/cli/plugin-commands/neutron.rst index 6e67ae9480..0167931f05 100644 --- a/doc/source/cli/plugin-commands/neutron.rst +++ b/doc/source/cli/plugin-commands/neutron.rst @@ -1,5 +1,4 @@ neutron ------- -.. list-plugins:: openstack.neutronclient.v2 - :detailed: +.. autoprogram-cliff:: openstack.neutronclient.v2 diff --git a/doc/source/cli/plugin-commands/octavia.rst b/doc/source/cli/plugin-commands/octavia.rst index 5384530fc8..fb48f0ac7e 100644 --- a/doc/source/cli/plugin-commands/octavia.rst +++ b/doc/source/cli/plugin-commands/octavia.rst @@ -1,5 +1,7 @@ octavia ------- +.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed + .. list-plugins:: openstack.load_balancer.v2 :detailed: diff --git a/doc/source/cli/plugin-commands/placement.rst b/doc/source/cli/plugin-commands/placement.rst index 972818c768..203ecc2992 100644 --- a/doc/source/cli/plugin-commands/placement.rst +++ b/doc/source/cli/plugin-commands/placement.rst @@ -1,5 +1,4 @@ placement --------- -.. list-plugins:: openstack.placement.v1 - :detailed: +.. autoprogram-cliff:: openstack.placement.v1 diff --git a/doc/source/cli/plugin-commands/rsd.rst b/doc/source/cli/plugin-commands/rsd.rst index d7962014e5..9436c5ae75 100644 --- a/doc/source/cli/plugin-commands/rsd.rst +++ b/doc/source/cli/plugin-commands/rsd.rst @@ -1,5 +1,7 @@ rsd --- +.. TODO(efried): Cut over to autoprogram-cliff once rsd plugin docs build + .. list-plugins:: openstack.rsd.v1 :detailed: diff --git a/doc/source/cli/plugin-commands/sahara.rst b/doc/source/cli/plugin-commands/sahara.rst index 28ac867828..7c51756a3a 100644 --- a/doc/source/cli/plugin-commands/sahara.rst +++ b/doc/source/cli/plugin-commands/sahara.rst @@ -1,5 +1,4 @@ sahara ------ -.. list-plugins:: openstack.data_processing.v1 - :detailed: +.. autoprogram-cliff:: openstack.data_processing.v1 diff --git a/doc/source/cli/plugin-commands/searchlight.rst b/doc/source/cli/plugin-commands/searchlight.rst index fed56f0912..be934aeb36 100644 --- a/doc/source/cli/plugin-commands/searchlight.rst +++ b/doc/source/cli/plugin-commands/searchlight.rst @@ -1,5 +1,4 @@ searchlight ----------- -.. list-plugins:: openstack.search.v1 - :detailed: +.. autoprogram-cliff:: openstack.search.v1 diff --git a/doc/source/cli/plugin-commands/senlin.rst b/doc/source/cli/plugin-commands/senlin.rst index f5f81d9e13..90929058f8 100644 --- a/doc/source/cli/plugin-commands/senlin.rst +++ b/doc/source/cli/plugin-commands/senlin.rst @@ -1,5 +1,4 @@ senlin ------ -.. list-plugins:: openstack.clustering.v1 - :detailed: +.. autoprogram-cliff:: openstack.clustering.v1 diff --git a/doc/source/cli/plugin-commands/trove.rst b/doc/source/cli/plugin-commands/trove.rst index b4575edd8a..75da22726c 100644 --- a/doc/source/cli/plugin-commands/trove.rst +++ b/doc/source/cli/plugin-commands/trove.rst @@ -1,5 +1,7 @@ trove ----- +.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed + .. list-plugins:: openstack.database.v1 :detailed: diff --git a/doc/source/cli/plugin-commands/zaqar.rst b/doc/source/cli/plugin-commands/zaqar.rst index 3649a274d3..6415149029 100644 --- a/doc/source/cli/plugin-commands/zaqar.rst +++ b/doc/source/cli/plugin-commands/zaqar.rst @@ -1,5 +1,4 @@ zaqar ----- -.. list-plugins:: openstack.messaging.v2 - :detailed: +.. autoprogram-cliff:: openstack.messaging.v2 diff --git a/doc/source/cli/plugin-commands/zun.rst b/doc/source/cli/plugin-commands/zun.rst index 56c5ff59ce..40480e70ca 100644 --- a/doc/source/cli/plugin-commands/zun.rst +++ b/doc/source/cli/plugin-commands/zun.rst @@ -1,5 +1,7 @@ zun --- +.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed + .. list-plugins:: openstack.container.v1 :detailed: From 67a5654d4977998c8c767e2d5c595fba5b12164f Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 21 Oct 2019 18:08:41 -0500 Subject: [PATCH 0148/1120] Add plugin doc page for watcher This was being omitted because whenever the plugin page was produced, python-watcherclient wasn't in global-requirements. It is now, so include a page for it in the plugin docs. NOTE: We would like to use autoprogram-cliff to make the documentation complete, but that breaks the build. For now, this is better than nothing. Change-Id: I49822242b9a0c031a053d6c2fd9f644a585f4ba5 --- doc/requirements.txt | 1 + doc/source/cli/plugin-commands/index.rst | 6 +----- doc/source/cli/plugin-commands/watcher.rst | 7 +++++++ lower-constraints.txt | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 doc/source/cli/plugin-commands/watcher.rst diff --git a/doc/requirements.txt b/doc/requirements.txt index 12ed5f01a2..d603c1d4b7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -27,5 +27,6 @@ python-saharaclient>=1.4.0 # Apache-2.0 python-searchlightclient>=1.0.0 #Apache-2.0 python-senlinclient>=1.1.0 # Apache-2.0 python-troveclient>=2.2.0 # Apache-2.0 +python-watcherclient>=2.4.0 # Apache-2.0 python-zaqarclient>=1.0.0 # Apache-2.0 python-zunclient>=3.4.0 # Apache-2.0 diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst index f6ff51bd63..2c3bda3e0f 100644 --- a/doc/source/cli/plugin-commands/index.rst +++ b/doc/source/cli/plugin-commands/index.rst @@ -25,6 +25,7 @@ Plugin Commands searchlight senlin trove + watcher zaqar zun @@ -44,8 +45,3 @@ Plugin Commands .. # tripleoclient is not in global-requirements .. # list-plugins:: openstack.tripleoclient.v1 .. # :detailed: - -.. watcher -.. # watcherclient is not in global-requirements -.. # list-plugins:: openstack.infra_optim.v1 -.. # :detailed: diff --git a/doc/source/cli/plugin-commands/watcher.rst b/doc/source/cli/plugin-commands/watcher.rst new file mode 100644 index 0000000000..7f9afbdc26 --- /dev/null +++ b/doc/source/cli/plugin-commands/watcher.rst @@ -0,0 +1,7 @@ +watcher +------- + +.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed + +.. list-plugins:: openstack.infra_optim.v1 + :detailed: diff --git a/lower-constraints.txt b/lower-constraints.txt index db0562e124..6533edafc3 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -109,6 +109,7 @@ python-senlinclient==1.1.0 python-subunit==1.0.0 python-swiftclient==3.2.0 python-troveclient==2.2.0 +python-watcherclient==2.4.0 python-zaqarclient==1.0.0 python-zunclient==3.4.0 pytz==2013.6 From fcae62841f9a0ddc70a7e5be5202eea379378d70 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 22 Oct 2019 08:08:28 -0500 Subject: [PATCH 0149/1120] Link to (some) plugin doc pages We would like to use autoprogram-cliff to generate full docs inline for each plugin. But for the following projects, that breaks the build: - octavia - rsd - trove - watcher - zun For those projects, we're using list-plugins instead, because that builds, and it's better than nothing; but it only provides summaries of the commands. So with this commit, we add a link to the plugin documentation from the actual plugin project where such documentation exists, which currently is just: - octavia - watcher - zun (For rsd, I couldn't find openstack-published docs at all; for trove, published docs exist, but the osc plugin isn't documented.) Change-Id: I7c826ecef4319bead239e11b5f975302b2f24d1b Story: #1735016 Task: #37244 --- doc/source/cli/plugin-commands/octavia.rst | 3 +++ doc/source/cli/plugin-commands/watcher.rst | 3 +++ doc/source/cli/plugin-commands/zun.rst | 3 +++ doc/source/conf.py | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/doc/source/cli/plugin-commands/octavia.rst b/doc/source/cli/plugin-commands/octavia.rst index fb48f0ac7e..ce6259dd25 100644 --- a/doc/source/cli/plugin-commands/octavia.rst +++ b/doc/source/cli/plugin-commands/octavia.rst @@ -3,5 +3,8 @@ octavia .. TODO(efried): cut over to autoprogram-cliff once doc build is fixed +For more details, see the :python-octaviaclient-doc:`python-octaviaclient +plugin documentation `. + .. list-plugins:: openstack.load_balancer.v2 :detailed: diff --git a/doc/source/cli/plugin-commands/watcher.rst b/doc/source/cli/plugin-commands/watcher.rst index 7f9afbdc26..c888d3a153 100644 --- a/doc/source/cli/plugin-commands/watcher.rst +++ b/doc/source/cli/plugin-commands/watcher.rst @@ -3,5 +3,8 @@ watcher .. TODO(efried): cut over to autoprogram-cliff once doc build is fixed +For more details, see the :python-watcherclient-doc:`python-watcherclient +plugin documentation `. + .. list-plugins:: openstack.infra_optim.v1 :detailed: diff --git a/doc/source/cli/plugin-commands/zun.rst b/doc/source/cli/plugin-commands/zun.rst index 40480e70ca..69f4f62c84 100644 --- a/doc/source/cli/plugin-commands/zun.rst +++ b/doc/source/cli/plugin-commands/zun.rst @@ -3,5 +3,8 @@ zun .. TODO(efried): cut over to autoprogram-cliff once doc build is fixed +For more details, see the :python-zunclient-doc:`python-zunclient plugin +documentation `. + .. list-plugins:: openstack.container.v1 :detailed: diff --git a/doc/source/conf.py b/doc/source/conf.py index 45045002aa..92febda46d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -34,6 +34,12 @@ repository_name = 'openstack/python-openstackclient' use_storyboard = True +openstack_projects = [ + 'python-octaviaclient', + 'python-watcherclient', + 'python-zunclient', +] + # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] From d0bfef807d5597f32f8eda5ab3fae2d1f5924ecc Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 22 Oct 2019 07:35:37 -0500 Subject: [PATCH 0150/1120] Doc: launchpad => storyboard A few docs still referred to launchpad, which osc hasn't used in a couple of years. Cut over to storyboard. Change-Id: Ic9abf0fe1e52c255976bd7a019e999a8e610455e --- CONTRIBUTING.rst | 4 ++-- README.rst | 8 +++----- doc/source/index.rst | 10 +++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index de9324ec7f..dfdfe471a7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -11,6 +11,6 @@ the workflow documented at: Pull requests submitted through GitHub will be ignored. -Bugs should be filed on Launchpad, not GitHub: +Bugs should be filed on Storyboard, not GitHub or Launchpad: - https://bugs.launchpad.net/python-openstackclient + https://storyboard.openstack.org/#!/project/openstack/python-openstackclient diff --git a/README.rst b/README.rst index cd80079fab..41d0112428 100644 --- a/README.rst +++ b/README.rst @@ -24,9 +24,8 @@ language to describe operations in OpenStack. * `PyPi`_ - package installation * `Online Documentation`_ -* `Launchpad project`_ - release management -* `Blueprints`_ - feature specifications -* `Bugs`_ - issue tracking +* `Storyboard project`_ - bugs and feature requests +* `Blueprints`_ - feature specifications (historical only) * `Source`_ * `Developer`_ - getting started as a developer * `Contributing`_ - contributing code @@ -36,9 +35,8 @@ language to describe operations in OpenStack. .. _PyPi: https://pypi.org/project/python-openstackclient .. _Online Documentation: https://docs.openstack.org/python-openstackclient/latest/ -.. _Launchpad project: https://launchpad.net/python-openstackclient .. _Blueprints: https://blueprints.launchpad.net/python-openstackclient -.. _Bugs: https://storyboard.openstack.org/#!/project/975 +.. _`Storyboard project`: https://storyboard.openstack.org/#!/project/openstack/python-openstackclient .. _Source: https://opendev.org/openstack/python-openstackclient .. _Developer: https://docs.openstack.org/project-team-guide/project-setup/python.html .. _Contributing: https://docs.openstack.org/infra/manual/developers.html diff --git a/doc/source/index.rst b/doc/source/index.rst index ea4cd3e9f9..732e13872a 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -55,13 +55,13 @@ Contributing OpenStackClient utilizes all of the usual OpenStack processes and requirements for contributions. The code is hosted `on OpenStack's Git server`_. `Bug reports`_ -may be submitted to the :code:`python-openstackclient` Storyboard project -on `Launchpad`_. Code may be submitted to the -:code:`openstack/python-openstackclient` project using `Gerrit`_. -Developers may also be found in the `IRC channel`_ ``#openstack-sdks``. +may be submitted to the :code:`python-openstackclient` `Storyboard project`_. +Code may be submitted to the :code:`openstack/python-openstackclient` project +using `Gerrit`_. Developers may also be found in the `IRC channel`_ +``#openstack-sdks``. .. _`on OpenStack's Git server`: https://opendev.org/openstack/python-openstackclient/ -.. _Launchpad: https://launchpad.net/python-openstackclient +.. _`Storyboard project`: https://storyboard.openstack.org/#!/project/openstack/python-openstackclient .. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow .. _Bug reports: https://storyboard.openstack.org/#!/project/975 .. _PyPi: https://pypi.org/project/python-openstackclient From 977b0c8591e40d3f59bbc1af9adc75f2c2edb694 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 22 Oct 2019 14:18:51 -0500 Subject: [PATCH 0151/1120] Use autoprogram-cliff for remaining plugin docs Sphinx errors have been fixed in the plugin projects for octavia, rsd, trove, watcher, and zun, so we can now use autoprogram-cliff to generate the docs for those. Change-Id: Ia7790c5e86957afd0aec8f9a04ffc7aa968b4eeb Story: #1735016 Task: #37241 --- doc/requirements.txt | 10 +++++----- doc/source/cli/plugin-commands/octavia.rst | 8 +------- doc/source/cli/plugin-commands/rsd.rst | 5 +---- doc/source/cli/plugin-commands/trove.rst | 5 +---- doc/source/cli/plugin-commands/watcher.rst | 8 +------- doc/source/cli/plugin-commands/zun.rst | 8 +------- doc/source/conf.py | 7 ++----- lower-constraints.txt | 10 +++++----- 8 files changed, 17 insertions(+), 44 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index d603c1d4b7..ec692e75a0 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -21,12 +21,12 @@ python-karborclient>=0.6.0 # Apache-2.0 python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 python-muranoclient>=0.8.2 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 -python-octaviaclient>=1.3.0 # Apache-2.0 -python-rsdclient>=0.1.0 # Apache-2.0 +python-octaviaclient>=1.11.0 # Apache-2.0 +python-rsdclient>=1.0.1 # Apache-2.0 python-saharaclient>=1.4.0 # Apache-2.0 python-searchlightclient>=1.0.0 #Apache-2.0 python-senlinclient>=1.1.0 # Apache-2.0 -python-troveclient>=2.2.0 # Apache-2.0 -python-watcherclient>=2.4.0 # Apache-2.0 +python-troveclient>=3.1.0 # Apache-2.0 +python-watcherclient>=2.5.0 # Apache-2.0 python-zaqarclient>=1.0.0 # Apache-2.0 -python-zunclient>=3.4.0 # Apache-2.0 +python-zunclient>=3.6.0 # Apache-2.0 diff --git a/doc/source/cli/plugin-commands/octavia.rst b/doc/source/cli/plugin-commands/octavia.rst index ce6259dd25..640af1ace8 100644 --- a/doc/source/cli/plugin-commands/octavia.rst +++ b/doc/source/cli/plugin-commands/octavia.rst @@ -1,10 +1,4 @@ octavia ------- -.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed - -For more details, see the :python-octaviaclient-doc:`python-octaviaclient -plugin documentation `. - -.. list-plugins:: openstack.load_balancer.v2 - :detailed: +.. autoprogram-cliff:: openstack.load_balancer.v2 diff --git a/doc/source/cli/plugin-commands/rsd.rst b/doc/source/cli/plugin-commands/rsd.rst index 9436c5ae75..d28cea316f 100644 --- a/doc/source/cli/plugin-commands/rsd.rst +++ b/doc/source/cli/plugin-commands/rsd.rst @@ -1,7 +1,4 @@ rsd --- -.. TODO(efried): Cut over to autoprogram-cliff once rsd plugin docs build - -.. list-plugins:: openstack.rsd.v1 - :detailed: +.. autoprogram-cliff:: openstack.rsd.v2 diff --git a/doc/source/cli/plugin-commands/trove.rst b/doc/source/cli/plugin-commands/trove.rst index 75da22726c..43560d4e2b 100644 --- a/doc/source/cli/plugin-commands/trove.rst +++ b/doc/source/cli/plugin-commands/trove.rst @@ -1,7 +1,4 @@ trove ----- -.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed - -.. list-plugins:: openstack.database.v1 - :detailed: +.. autoprogram-cliff:: openstack.database.v1 diff --git a/doc/source/cli/plugin-commands/watcher.rst b/doc/source/cli/plugin-commands/watcher.rst index c888d3a153..9cf6dfeea0 100644 --- a/doc/source/cli/plugin-commands/watcher.rst +++ b/doc/source/cli/plugin-commands/watcher.rst @@ -1,10 +1,4 @@ watcher ------- -.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed - -For more details, see the :python-watcherclient-doc:`python-watcherclient -plugin documentation `. - -.. list-plugins:: openstack.infra_optim.v1 - :detailed: +.. autoprogram-cliff:: openstack.infra_optim.v1 diff --git a/doc/source/cli/plugin-commands/zun.rst b/doc/source/cli/plugin-commands/zun.rst index 69f4f62c84..17baac557a 100644 --- a/doc/source/cli/plugin-commands/zun.rst +++ b/doc/source/cli/plugin-commands/zun.rst @@ -1,10 +1,4 @@ zun --- -.. TODO(efried): cut over to autoprogram-cliff once doc build is fixed - -For more details, see the :python-zunclient-doc:`python-zunclient plugin -documentation `. - -.. list-plugins:: openstack.container.v1 - :detailed: +.. autoprogram-cliff:: openstack.container.v1 diff --git a/doc/source/conf.py b/doc/source/conf.py index 92febda46d..d3dfccd235 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -34,11 +34,8 @@ repository_name = 'openstack/python-openstackclient' use_storyboard = True -openstack_projects = [ - 'python-octaviaclient', - 'python-watcherclient', - 'python-zunclient', -] +# Add project 'foo' to this list to enable the :foo-doc: role +# openstack_projects = [] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] diff --git a/lower-constraints.txt b/lower-constraints.txt index 6533edafc3..201726ac65 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -101,17 +101,17 @@ python-mistralclient==3.1.0 python-muranoclient==0.8.2 python-neutronclient==6.7.0 python-novaclient==15.0.0 -python-octaviaclient==1.3.0 -python-rsdclient==0.1.0 +python-octaviaclient==1.11.0 +python-rsdclient==1.0.1 python-saharaclient==1.4.0 python-searchlightclient==1.0.0 python-senlinclient==1.1.0 python-subunit==1.0.0 python-swiftclient==3.2.0 -python-troveclient==2.2.0 -python-watcherclient==2.4.0 +python-troveclient==3.1.0 +python-watcherclient==2.5.0 python-zaqarclient==1.0.0 -python-zunclient==3.4.0 +python-zunclient==3.6.0 pytz==2013.6 PyYAML==3.12 repoze.lru==0.7 From f1d742f32adeb662a3fdf8fa3ef3bc391e71ed81 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 29 Oct 2019 15:55:11 -0500 Subject: [PATCH 0152/1120] Fix functional tests for py3 Fix various things so the functional tests will work under python3: - A hashlib.md5() can only be update()d with an encoded string in py3. - There's no dict.iteritems(), change to dict.items() (which is already an iterator). - Open temp files with 'w+' mode rather than the default 'w+b' (as an alternative to encoding all the write and expected-read payloads as bytes). - (This is a weird one) Explicitly raise SkipTest from unittest (rather than unittest2, which is where cls.skipException landed). Not sure why this is busted, but this moves the ball. Change-Id: Ic9b2b47848a600e87a3674289ae7ae8c3e091fee --- openstackclient/tests/functional/compute/v2/test_agent.py | 4 ++-- openstackclient/tests/functional/compute/v2/test_keypair.py | 4 ++-- openstackclient/tests/functional/identity/v2/common.py | 3 ++- openstackclient/tests/functional/identity/v3/common.py | 2 +- openstackclient/tests/functional/object/v1/test_object.py | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openstackclient/tests/functional/compute/v2/test_agent.py b/openstackclient/tests/functional/compute/v2/test_agent.py index 1a112e8212..25d8c868dc 100644 --- a/openstackclient/tests/functional/compute/v2/test_agent.py +++ b/openstackclient/tests/functional/compute/v2/test_agent.py @@ -21,10 +21,10 @@ class ComputeAgentTests(base.TestCase): # Generate two different md5hash MD5HASH1 = hashlib.md5() - MD5HASH1.update('agent_1') + MD5HASH1.update('agent_1'.encode('utf-8')) MD5HASH1 = MD5HASH1.hexdigest() MD5HASH2 = hashlib.md5() - MD5HASH2.update('agent_2') + MD5HASH2.update('agent_2'.encode('utf-8')) MD5HASH2 = MD5HASH2.hexdigest() def test_compute_agent_delete(self): diff --git a/openstackclient/tests/functional/compute/v2/test_keypair.py b/openstackclient/tests/functional/compute/v2/test_keypair.py index 9a88e66fca..42f334a46a 100644 --- a/openstackclient/tests/functional/compute/v2/test_keypair.py +++ b/openstackclient/tests/functional/compute/v2/test_keypair.py @@ -88,7 +88,7 @@ def test_keypair_create_public_key(self): 1) Create keypair with given public key 2) Delete keypair """ - with tempfile.NamedTemporaryFile() as f: + with tempfile.NamedTemporaryFile(mode='w+') as f: f.write(self.PUBLIC_KEY) f.flush() @@ -108,7 +108,7 @@ def test_keypair_create_private_key(self): 1) Create keypair with private key file 2) Delete keypair """ - with tempfile.NamedTemporaryFile() as f: + with tempfile.NamedTemporaryFile(mode='w+') as f: cmd_output = json.loads(self.openstack( 'keypair create -f json --private-key %s tmpkey' % f.name, )) diff --git a/openstackclient/tests/functional/identity/v2/common.py b/openstackclient/tests/functional/identity/v2/common.py index f4bc10bddd..43c0cbf273 100644 --- a/openstackclient/tests/functional/identity/v2/common.py +++ b/openstackclient/tests/functional/identity/v2/common.py @@ -11,6 +11,7 @@ # under the License. import os +import unittest import fixtures from tempest.lib.common.utils import data_utils @@ -62,7 +63,7 @@ def setUpClass(cls): # TODO(dtroyer): Actually determine if Identity v2 admin is # enabled in the target cloud. Tuens out OSC # doesn't make this easy as it should (yet). - raise cls.skipException('No Identity v2 admin endpoint?') + raise unittest.case.SkipTest('No Identity v2 admin endpoint?') @classmethod def tearDownClass(cls): diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 43b416aabe..86f090bcc0 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -360,7 +360,7 @@ def _create_dummy_registered_limit(self, add_clean_up=True): def _extract_value_from_items(self, key, items): for d in items: - for k, v in d.iteritems(): + for k, v in d.items(): if k == key: return v diff --git a/openstackclient/tests/functional/object/v1/test_object.py b/openstackclient/tests/functional/object/v1/test_object.py index 226ef8adbd..b3f23e5287 100644 --- a/openstackclient/tests/functional/object/v1/test_object.py +++ b/openstackclient/tests/functional/object/v1/test_object.py @@ -33,7 +33,7 @@ def setUp(self): self.skipTest("No object-store service present") def test_object(self): - with tempfile.NamedTemporaryFile() as f: + with tempfile.NamedTemporaryFile(mode='w+') as f: f.write('test content') f.flush() self._test_object(f.name) From 4b393681d98e6a78248f6fa36e576a9f09c4ee34 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Thu, 24 Oct 2019 13:58:12 -0400 Subject: [PATCH 0153/1120] Use SDK to get compute API extensions python-novaclient 16.0.0 removed the deprecated list_extensions module [1] so this changes the extensions command to use openstacksdk to get the compute API extensions. The functional test ExtensionTests.test_extension_list_compute ensures this works. [1] https://review.opendev.org/686516/ Change-Id: I9894bc395c0474aaa6494ac4534862efe4ea7984 Story: #2006769 Task: #37284 --- openstackclient/common/extension.py | 4 ++-- .../tests/unit/common/test_extension.py | 21 +++++++------------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index 7120661880..1ed2012c10 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -89,9 +89,9 @@ def take_action(self, parsed_args): LOG.warning(message) if parsed_args.compute or show_all: - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute try: - data += compute_client.list_extensions.show_all() + data += compute_client.extensions() except Exception: message = _("Extensions list not supported by Compute API") LOG.warning(message) diff --git a/openstackclient/tests/unit/common/test_extension.py b/openstackclient/tests/unit/common/test_extension.py index eefa814c49..87c62da499 100644 --- a/openstackclient/tests/unit/common/test_extension.py +++ b/openstackclient/tests/unit/common/test_extension.py @@ -36,13 +36,9 @@ def setUp(self): self.identity_extensions_mock = identity_client.extensions self.identity_extensions_mock.reset_mock() - compute_client = compute_fakes.FakeComputev2Client( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, - ) - self.app.client_manager.compute = compute_client - compute_client.list_extensions = mock.Mock() - self.compute_extensions_mock = compute_client.list_extensions + sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection = sdk_connection + self.compute_extensions_mock = sdk_connection.compute.extensions self.compute_extensions_mock.reset_mock() volume_client = volume_fakes.FakeVolumeClient( @@ -80,8 +76,7 @@ def setUp(self): self.identity_extensions_mock.list.return_value = [ self.identity_extension] - self.compute_extensions_mock.show_all.return_value = [ - self.compute_extension] + self.compute_extensions_mock.return_value = [self.compute_extension] self.volume_extensions_mock.show_all.return_value = [ self.volume_extension] self.network_extensions_mock.return_value = [self.network_extension] @@ -131,7 +126,7 @@ def test_extension_list_no_options(self): ) self._test_extension_list_helper(arglist, verifylist, datalist) self.identity_extensions_mock.list.assert_called_with() - self.compute_extensions_mock.show_all.assert_called_with() + self.compute_extensions_mock.assert_called_with() self.volume_extensions_mock.show_all.assert_called_with() self.network_extensions_mock.assert_called_with() @@ -178,7 +173,7 @@ def test_extension_list_long(self): ) self._test_extension_list_helper(arglist, verifylist, datalist, True) self.identity_extensions_mock.list.assert_called_with() - self.compute_extensions_mock.show_all.assert_called_with() + self.compute_extensions_mock.assert_called_with() self.volume_extensions_mock.show_all.assert_called_with() self.network_extensions_mock.assert_called_with() @@ -248,7 +243,7 @@ def test_extension_list_compute(self): self.compute_extension.description, ), ) self._test_extension_list_helper(arglist, verifylist, datalist) - self.compute_extensions_mock.show_all.assert_called_with() + self.compute_extensions_mock.assert_called_with() def test_extension_list_compute_and_network(self): arglist = [ @@ -272,7 +267,7 @@ def test_extension_list_compute_and_network(self): ), ) self._test_extension_list_helper(arglist, verifylist, datalist) - self.compute_extensions_mock.show_all.assert_called_with() + self.compute_extensions_mock.assert_called_with() self.network_extensions_mock.assert_called_with() def test_extension_list_volume(self): From 40f74816c11c2d46eb0595795ef3611c18920482 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 29 Oct 2019 08:55:51 -0500 Subject: [PATCH 0154/1120] Remove redundant OpenStackShell.prepare_to_run_command osc-lib's OpenStackShell.prepare_to_run_command has been a superset of python-openstackclient's since at least osc-lib 1.4.0. We require 1.14.0 now, so the redundant override can be removed. Change-Id: I5658e3df5af1100e139623505d0375588edae63c --- openstackclient/shell.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 7fbda1aee5..755af24d2e 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -139,32 +139,6 @@ def initialize_app(self, argv): pw_func=shell.prompt_for_password, ) - def prepare_to_run_command(self, cmd): - """Set up auth and API versions""" - - # TODO(dtroyer): Move this to osc-lib, remove entire method when 1.4.0 - # release is minimum version is in global-requirements - # NOTE(dtroyer): If auth is not required for a command, skip - # get_one_Cloud()'s validation to avoid loading plugins - validate = cmd.auth_required - - # Force skipping auth for commands that do not need it - # NOTE(dtroyer): This is here because ClientManager does not have - # visibility into the Command object to get - # auth_required. It needs to move into osc-lib - self.client_manager._auth_required = cmd.auth_required - - # Validate auth options - self.cloud = self.cloud_config.get_one_cloud( - cloud=self.options.cloud, - argparse=self.options, - validate=validate, - ) - # Push the updated args into ClientManager - self.client_manager._cli_options = self.cloud - - return super(OpenStackShell, self).prepare_to_run_command(cmd) - def main(argv=None): if argv is None: From 61ad83b57580c76a1c448e03064c4df6bcc01e87 Mon Sep 17 00:00:00 2001 From: Pierre Prinetti Date: Thu, 3 Oct 2019 15:53:39 +0200 Subject: [PATCH 0155/1120] versions: Fix 'versions show' help message The command `openstack versions show --help` shows a copy-paste nit. Change-Id: I9e4e86429ffd630c566bbdf2929e7995c9b0dbe1 Signed-off-by: Pierre Prinetti --- doc/source/cli/command-objects/versions.rst | 12 +++++++++++- openstackclient/common/versions.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/command-objects/versions.rst b/doc/source/cli/command-objects/versions.rst index 6742565265..0aa9a1b785 100644 --- a/doc/source/cli/command-objects/versions.rst +++ b/doc/source/cli/command-objects/versions.rst @@ -17,6 +17,7 @@ Show service versions: [--interface ] [--region-name ] [--service ] + [--status ] .. option:: --all-interfaces @@ -30,7 +31,7 @@ Show service versions: .. option:: --region-name - Limit results to only those from region-name + Limit results to only those from a given region. .. option:: --service @@ -38,4 +39,13 @@ Show service versions: an exact match to what is in the catalog or a known official value or alias from `service-types-authority`_. +.. option:: --status + + Limit results to only those in the given state. Valid values are: + + - SUPPORTED + - CURRENT + - DEPRECATED + - EXPERIMENTAL + .. _service-types-authority: https://service-types.openstack.org/ diff --git a/openstackclient/common/versions.py b/openstackclient/common/versions.py index 3c267bfe0a..e6781bc66f 100644 --- a/openstackclient/common/versions.py +++ b/openstackclient/common/versions.py @@ -45,12 +45,12 @@ def get_parser(self, prog_name): ) parser.add_argument( '--service', - metavar='', + metavar='', help=_('Show versions for a specific service.'), ) parser.add_argument( '--status', - metavar='', + metavar='', help=_('Show versions for a specific status.' ' [Valid values are SUPPORTED, CURRENT,' ' DEPRECATED, EXPERIMENTAL]'), From cd6c285cc6c2274e6b42cc452ba4a61a3487ca23 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 28 Oct 2019 17:27:38 -0500 Subject: [PATCH 0156/1120] neutron: autogenerate docs $namespace = openstack.network.v2 The subcommand documents for $namespace were hardcoded and thus prone to drift over time. This commit removes the hardcoded content and uses the autoprogram-cliff directive to generate them automatically from the subcommand configuration classes. This one turned out to be quite involved, because we support both neutron and nova-network. When running in a real cloud, the command classes detect whether the neutron service is present, assume nova-network if that service is not found, and only add parser options relevant to the detected service. But the docs need to present both sets of options. This was easy enough when they were hardcoded, but required a bit of additional infrastructure for generated docs. Change-Id: I426261eb1d86bcc68656aabd61f10b7f082da402 --- .../cli/command-objects/address-scope.rst | 142 +---- .../cli/command-objects/floating-ip-pool.rst | 11 +- .../floating-ip-port-forwarding.rst | 168 +----- .../cli/command-objects/floating-ip.rst | 303 +---------- .../cli/command-objects/ip-availability.rst | 56 +- .../cli/command-objects/network-agent.rst | 206 +------ .../network-auto-allocated-topology.rst | 59 +- .../network-flavor-profile.rst | 134 +---- .../cli/command-objects/network-flavor.rst | 182 +------ .../command-objects/network-meter-rule.rst | 92 +--- .../cli/command-objects/network-meter.rst | 87 +-- .../command-objects/network-qos-policy.rst | 156 +----- .../command-objects/network-qos-rule-type.rst | 26 +- .../cli/command-objects/network-qos-rule.rst | 161 +----- .../cli/command-objects/network-rbac.rst | 135 +---- .../cli/command-objects/network-segment.rst | 129 +---- .../network-service-provider.rst | 11 +- doc/source/cli/command-objects/network.rst | 510 +----------------- .../command-objects/network_segment_range.rst | 158 +----- doc/source/cli/command-objects/port.rst | 485 +---------------- doc/source/cli/command-objects/router.rst | 401 +------------- .../command-objects/security-group-rule.rst | 193 +------ .../cli/command-objects/security-group.rst | 202 +------ .../cli/command-objects/subnet-pool.rst | 303 +---------- doc/source/cli/command-objects/subnet.rst | 432 +-------------- openstackclient/identity/common.py | 8 +- openstackclient/network/common.py | 199 +++---- openstackclient/network/v2/_tag.py | 46 +- openstackclient/network/v2/floating_ip.py | 74 ++- openstackclient/network/v2/network.py | 109 ++-- openstackclient/network/v2/security_group.py | 46 +- .../network/v2/security_group_rule.py | 205 +++---- 32 files changed, 525 insertions(+), 4904 deletions(-) diff --git a/doc/source/cli/command-objects/address-scope.rst b/doc/source/cli/command-objects/address-scope.rst index 9155d09ed1..38141d5c97 100644 --- a/doc/source/cli/command-objects/address-scope.rst +++ b/doc/source/cli/command-objects/address-scope.rst @@ -7,143 +7,5 @@ to a given project and may be shared between projects. Network v2 -address scope create --------------------- - -Create new address scope - -.. program:: address scope create -.. code:: bash - - openstack address scope create - [--project [--project-domain ]] - [--ip-version ] - [--share | --no-share] - - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --ip-version - - IP version (4 or 6, default is 4) - -.. option:: --share - - Share the address scope between projects - -.. option:: --no-share - - Do not share the address scope between projects (default) - -.. _address_scope_create-name: -.. describe:: - - New address scope name - -address scope delete --------------------- - -Delete address scope(s) - -.. program:: address scope delete -.. code:: bash - - openstack address scope delete - [ ...] - -.. _address_scope_delete-address-scope: -.. describe:: - - Address scope(s) to delete (name or ID) - -address scope list ------------------- - -List address scopes - -.. program:: address scope list -.. code:: bash - - openstack address scope list - [--name ] - [--ip-version ] - [--project [--project-domain ]] - [--share | --no-share] - -.. option:: --name - - List only address scopes of given name in output - -.. option:: --ip-version - - List address scopes of given IP version networks (4 or 6) - -.. option:: --project - - List address scopes according to their project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --share - - List address scopes shared between projects - -.. option:: --no-share - - List address scopes not shared between projects - -address scope set ------------------ - -Set address scope properties - -.. program:: address scope set -.. code:: bash - - openstack address scope set - [--name ] - [--share | --no-share] - - -.. option:: --name - - Set address scope name - -.. option:: --share - - Share the address scope between projects - -.. option:: --no-share - - Do not share the address scope between projects - -.. _address_scope_set-address-scope: -.. describe:: - - Address scope to modify (name or ID) - -address scope show ------------------- - -Display address scope details - -.. program:: address scope show -.. code:: bash - - openstack address scope show - - -.. _address_scope_show-address-scope: -.. describe:: - - Address scope to display (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: address scope * diff --git a/doc/source/cli/command-objects/floating-ip-pool.rst b/doc/source/cli/command-objects/floating-ip-pool.rst index 5c8f3aa13d..ab6f83a993 100644 --- a/doc/source/cli/command-objects/floating-ip-pool.rst +++ b/doc/source/cli/command-objects/floating-ip-pool.rst @@ -4,12 +4,5 @@ floating ip pool Network v2 -floating ip pool list ---------------------- - -List pools of floating IP addresses - -.. program:: floating ip pool list -.. code:: bash - - openstack floating ip pool list +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip pool * diff --git a/doc/source/cli/command-objects/floating-ip-port-forwarding.rst b/doc/source/cli/command-objects/floating-ip-port-forwarding.rst index 5012787aaf..052ea0cc7f 100644 --- a/doc/source/cli/command-objects/floating-ip-port-forwarding.rst +++ b/doc/source/cli/command-objects/floating-ip-port-forwarding.rst @@ -4,170 +4,6 @@ floating ip port forwarding Network v2 -floating ip port forwarding create ----------------------------------- +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip port forwarding * -Create floating IP Port Forwarding - -.. program:: floating ip port forwarding create -.. code:: bash - - openstack floating ip port forwarding create - --internal-ip-address - --port - --internal-protocol-port - --external-protocol-port - --protocol - - - -.. describe:: --internal-ip-address - - The fixed IPv4 address of the network port associated - to the floating IP port forwarding - -.. describe:: --port - - The name or ID of the network port associated to the - floating IP port forwarding - -.. describe:: --internal-protocol-port - - The protocol port number of the network port fixed - IPv4 address associated to the floating IP port - forwarding - -.. describe:: --external-protocol-port - - The protocol port number of the port forwarding's - floating IP address - -.. describe:: --protocol - - The protocol used in the floating IP port forwarding, - for instance: TCP, UDP - -.. describe:: - - Floating IP that the port forwarding belongs to (IP - address or ID) - -floating ip port forwarding delete ----------------------------------- - -Delete floating IP Port Forwarding(s) - -.. program:: floating ip port forwarding delete -.. code:: bash - - openstack floating ip port forwarding delete - [ ...] - -.. describe:: - - Floating IP that the port forwarding belongs to (IP - address or ID) - -.. describe:: - - The ID of the floating IP port forwarding - -floating ip port forwarding list --------------------------------- - -List floating IP Port Forwarding(s) - -.. program:: floating ip port forwarding list -.. code:: bash - - openstack floating ip port forwarding list - [--port ] - [--external-protocol-port ] - [--protocol protocol] - - -.. option:: --port - - The ID of the network port associated to the floating - IP port forwarding - -.. option:: --protocol - - The IP protocol used in the floating IP port - forwarding - -.. describe:: - - Floating IP that the port forwarding belongs to (IP - address or ID) - -floating ip port forwarding set -------------------------------- - -Set floating IP Port Forwarding properties - -.. program:: floating ip port forwarding set -.. code:: bash - - openstack floating ip port forwarding set - [--port ] - [--internal-ip-address ] - [--internal-protocol-port ] - [--external-protocol-port ] - [--protocol ] - - - -.. option:: --port - - The ID of the network port associated to the floating - IP port forwarding - -.. option:: --internal-ip-address - - The fixed IPv4 address of the network port associated - to the floating IP port forwarding - -.. option:: --internal-protocol-port - - The TCP/UDP/other protocol port number of the network - port fixed IPv4 address associated to the floating IP - port forwarding - -.. option:: --external-protocol-port - - The TCP/UDP/other protocol port number of the port - forwarding's floating IP address - -.. option:: --protocol - - The IP protocol used in the floating IP port - forwarding - -.. describe:: - - Floating IP that the port forwarding belongs to (IP - address or ID) - -.. describe:: - - The ID of the floating IP port forwarding - -floating ip port forwarding show --------------------------------- - -Display floating IP Port Forwarding details - -.. program:: floating ip port forwarding show -.. code:: bash - - openstack floating ip show - -.. describe:: - - Floating IP that the port forwarding belongs to (IP - address or ID) - -.. describe:: - - The ID of the floating IP port forwarding diff --git a/doc/source/cli/command-objects/floating-ip.rst b/doc/source/cli/command-objects/floating-ip.rst index d122ccbea3..c781a1edb4 100644 --- a/doc/source/cli/command-objects/floating-ip.rst +++ b/doc/source/cli/command-objects/floating-ip.rst @@ -4,298 +4,23 @@ floating ip Network v2 -floating ip create ------------------- +.. NOTE(efried): have to list these out one by one; 'floating ip' pulls in + ... pool and ... port forwarding. -Create floating IP +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip create -.. program:: floating ip create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip delete - openstack floating ip create - [--subnet ] - [--port ] - [--floating-ip-address ] - [--fixed-ip-address ] - [--description ] - [--qos-policy ] - [--project [--project-domain ]] - [--tag | --no-tag] - [--dns-domain ] - [--dns-name ] - +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip list -.. option:: --subnet +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip set - Subnet on which you want to create the floating IP (name or ID) - *Network version 2 only* +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip show -.. option:: --port - - Port to be associated with the floating IP (name or ID) - *Network version 2 only* - -.. option:: --floating-ip-address - - Floating IP address - *Network version 2 only* - -.. option:: --fixed-ip-address - - Fixed IP address mapped to the floating IP - *Network version 2 only* - -.. option:: --description - - Set floating IP description - *Network version 2 only* - -.. option:: --qos-policy - - QoS policy to attach to the floating IP (name or ID) - - *Network version 2 only* - -.. option:: --project - - Owner's project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --tag - - Tag to be added to the floating IP (repeat option to set multiple tags) - - *Network version 2 only* - -.. option:: --no-tag - - No tags associated with the floating IP - - *Network version 2 only* - -.. option:: --dns-domain - - Set DNS domain for this floating IP (requires DNS integration extension). - -.. option:: --dns-name - - Set DNS name for this floating IP (requires DNS integration extension). - -.. describe:: - - Network to allocate floating IP from (name or ID) - -floating ip delete ------------------- - -Delete floating IP(s) - -.. program:: floating ip delete -.. code:: bash - - openstack floating ip delete [ ...] - -.. describe:: - - Floating IP(s) to delete (IP address or ID) - -floating ip list ----------------- - -List floating IP(s) - -.. program:: floating ip list -.. code:: bash - - openstack floating ip list - [--network ] - [--port ] - [--fixed-ip-address ] - [--long] - [--status ] - [--project [--project-domain ]] - [--router ] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --network - - List floating IP(s) according to given network (name or ID) - - *Network version 2 only* - -.. option:: --port - - List floating IP(s) according to given port (name or ID) - - *Network version 2 only* - -.. option:: --fixed-ip-address - - List floating IP(s) according to given fixed IP address - - *Network version 2 only* - -.. option:: --floating-ip-address - - List floating IP(s) according to given floating IP address - - *Network version 2 only* - -.. option:: --long - - List additional fields in output - - *Network version 2 only* - -.. option:: --status - - List floating IP(s) according to given status ('ACTIVE', 'DOWN') - - *Network version 2 only* - -.. option:: --project - - List floating IP(s) according to given project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). This can - be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --router - - List floating IP(s) according to given router (name or ID) - - *Network version 2 only* - -.. option:: --tags [,,...] - - List floating IP(s) which have all given tag(s) - - *Network version 2 only* - -.. option:: --any-tags [,,...] - - List floating IP(s) which have any given tag(s) - - *Network version 2 only* - -.. option:: --not-tags [,,...] - - Exclude floating IP(s) which have all given tag(s) - - *Network version 2 only* - -.. option:: --not-any-tags [,,...] - - Exclude floating IP(s) which have any given tag(s) - - *Network version 2 only* - -floating ip set ---------------- - -Set floating IP properties - -.. program:: floating ip set -.. code:: bash - - openstack floating ip set - [--port ] - [--fixed-ip-address ] - [--qos-policy | --no-qos-policy] - [--tag ] [--no-tag] - - -.. option:: --port - - Associate the floating IP with port (name or ID) - -.. option:: --fixed-ip-address - - Fixed IP of the port (required only if port has multiple IPs) - -.. option:: --qos-policy - - Attach QoS policy to the floating IP (name or ID) - -.. option:: --no-qos-policy - - Remove the QoS policy attached to the floating IP - -.. option:: --tag - - Tag to be added to the floating IP (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the floating IP. Specify both --tag - and --no-tag to overwrite current tags - -.. _floating_ip_set-floating-ip: -.. describe:: - - Floating IP to associate (IP address or ID) - -floating ip show ----------------- - -Display floating IP details - -.. program:: floating ip show -.. code:: bash - - openstack floating ip show - -.. describe:: - - Floating IP to display (IP address or ID) - -floating ip unset ------------------ - -Unset floating IP Properties - -.. program:: floating ip unset -.. code:: bash - - openstack floating ip unset - [--port] - [--qos-policy] - [--tag | --all-tag] - - -.. option:: --port - - Disassociate any port associated with the floating IP - -.. option:: --qos-policy - - Remove the QoS policy attached to the floating IP - -.. option:: --tag - - Tag to be removed from the floating IP - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the floating IP - -.. _floating_ip_unset-floating-ip: -.. describe:: - - Floating IP to disassociate (IP address or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip unset diff --git a/doc/source/cli/command-objects/ip-availability.rst b/doc/source/cli/command-objects/ip-availability.rst index dd39e649fe..94e60459b1 100644 --- a/doc/source/cli/command-objects/ip-availability.rst +++ b/doc/source/cli/command-objects/ip-availability.rst @@ -4,57 +4,5 @@ ip availability Network v2 -ip availability list --------------------- - -List IP availability for network - -This command retrieves information about IP availability. -Useful for admins who need a quick way to check the -IP availability for all associated networks. -List specifically returns total IP capacity and the -number of allocated IP addresses from that pool. - -.. program:: ip availability list -.. code:: bash - - openstack ip availability list - [--ip-version {4,6}] - [--project ] - -.. option:: --ip-version {4,6} - - List IP availability of given IP version networks - (default is 4) - -.. option:: --project - - List IP availability of given project - (name or ID) - -ip availability show --------------------- - -Show network IP availability details - -This command retrieves information about IP availability. -Useful for admins who need a quick way to -check the IP availability and details for a -specific network. - -This command will return information about -IP availability for the network as a whole, and -return availability information for each individual -subnet within the network as well. - - -.. program:: ip availability show -.. code:: bash - - openstack ip availability show - - -.. _ip_availability_show-network: -.. describe:: - - Show IP availability for a specific network (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: ip availability * diff --git a/doc/source/cli/command-objects/network-agent.rst b/doc/source/cli/command-objects/network-agent.rst index 9f02cb6bc8..7e721db1ea 100644 --- a/doc/source/cli/command-objects/network-agent.rst +++ b/doc/source/cli/command-objects/network-agent.rst @@ -10,207 +10,5 @@ agent is "True". Network v2 -network agent add network -------------------------- - -Add network to an agent - -.. program:: network agent add network -.. code:: bash - - openstack network agent add network - [--dhcp] - - - -.. option:: --dhcp - - Add a network to DHCP agent - -.. describe:: - - Agent to which a network is added (ID only) - -.. describe:: - - Network to be added to an agent (name or ID) - -network agent add router ------------------------- - -Add router to an agent - -.. program:: network agent add router -.. code:: bash - - openstack network agent add router - [--l3] - - - -.. option:: --l3 - - Add router to an L3 agent - -.. _network_agent_add_router-agent-id: -.. describe:: - - Agent to which a router is added (ID only) - -.. _network_agent_add_router-router: -.. describe:: - - Router to be added to an agent (name or ID) - -network agent delete --------------------- - -Delete network agent(s) - -.. program:: network agent delete -.. code:: bash - - openstack network agent delete - [ ...] - -.. _network_agent_delete-network-agent: -.. describe:: - - Network agent(s) to delete (ID only) - -network agent list ------------------- - -List network agents - -.. program:: network agent list -.. code:: bash - - openstack network agent list - [--agent-type ] - [--host ] - [--network | --router ] - [--long] - -.. option:: --agent-type - - List only agents with the specified agent type. - The supported agent types are: dhcp, open-vswitch, - linux-bridge, ofa, l3, loadbalancer, metering, - metadata, macvtap, nic. - -.. option:: --host - - List only agents running on the specified host - -.. option:: --network - - List agents hosting a network (name or ID) - -.. option:: --router - - List agents hosting this router (name or ID) - -.. option:: --long - - List additional fields in output - -network agent remove network ----------------------------- - -Remove network from an agent - -.. program:: network agent remove network -.. code:: bash - - openstack network agent remove network - [--dhcp] - - - -.. option:: --dhcp - - Remove network from DHCP agent - -.. _network_agent_remove_network-agent-id: -.. describe:: - - Agent to which a network is removed (ID only) - -.. _network_agent_remove_network-network: -.. describe:: - - Network to be removed from an agent (name or ID) - -network agent remove router ---------------------------- - -Remove router from an agent - -.. program:: network agent remove router -.. code:: bash - - openstack agent remove router - [--l3] - - - -.. option:: --l3 - - Remove router from an L3 agent - -.. _network_agent_remove_router-agent-id: -.. describe:: - - Agent from which router will be removed (ID only) - -.. _network_agent_remove_router-router: -.. describe:: - - Router to be removed from an agent (name or ID) - -network agent set ------------------ - -Set network agent properties - -.. program:: network agent set -.. code:: bash - - openstack network agent set - [--description ] - [--enable | --disable] - - -.. option:: --description - - Set network agent description - -.. option:: --enable - - Enable network agent - -.. option:: --disable - - Disable network agent - -.. _network_agent_set-network-agent: -.. describe:: - - Network agent to modify (ID only) - -network agent show ------------------- - -Display network agent details - -.. program:: network agent show -.. code:: bash - - openstack network agent show - - -.. _network_agent_show-network-agent: -.. describe:: - - Network agent to display (ID only) +.. autoprogram-cliff:: openstack.network.v2 + :command: network agent * diff --git a/doc/source/cli/command-objects/network-auto-allocated-topology.rst b/doc/source/cli/command-objects/network-auto-allocated-topology.rst index 4ed68cdae9..00e4c6b9bc 100644 --- a/doc/source/cli/command-objects/network-auto-allocated-topology.rst +++ b/doc/source/cli/command-objects/network-auto-allocated-topology.rst @@ -10,60 +10,5 @@ http://docs.openstack.org/newton/networking-guide/config-auto-allocation.html Network v2 -network auto allocated topology create --------------------------------------- - -Create the auto allocated topology for project - -.. program:: network auto allocated topology create -.. code:: bash - - openstack network auto allocated topology create - [--or-show] - [--check-resources] - [--project [--project-domain ]] - -.. option:: --or-show - - If topology exists returns the topologies information (Default). - -.. option:: --check-resources - - Validate the requirements for auto allocated topology. - Does not return a topology. - -.. option:: --project - - Return the auto allocated topology for a given project. - Default is current project. - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _network_auto_allocated_topology_create: - - -network auto allocated topology delete --------------------------------------- - -Delete auto allocated topology for project - -.. program:: network auto allocated topology delete -.. code:: bash - - openstack network auto allocated topology delete - [--project [--project-domain ]] - -.. option:: --project - - Delete auto allocated topology for a given project. - Default is the current project. - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _network_auto_allocated_topology_delete: +.. autoprogram-cliff:: openstack.network.v2 + :command: network auto allocated topology * diff --git a/doc/source/cli/command-objects/network-flavor-profile.rst b/doc/source/cli/command-objects/network-flavor-profile.rst index fdb95059b8..c5e9aa0c13 100644 --- a/doc/source/cli/command-objects/network-flavor-profile.rst +++ b/doc/source/cli/command-objects/network-flavor-profile.rst @@ -11,135 +11,5 @@ operators to create user options according to deployment needs. Network v2 -network flavor profile create ------------------------------ - -Create a new network flavor profile - -.. program:: network flavor profile create -.. code:: bash - - openstack network flavor profile create - [--project [--project-domain ]] - [--description ] - [--enable | --disable] - (--driver | --metainfo | --driver --metainfo ) - -.. option:: --project - - Owner's project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). This can - be used in case collisions between project names - exist - -.. option:: --description - - Description for the flavor profile - - *Network version 2 only* - -.. option:: --enable - - Enable the flavor profile (default) - -.. option:: --disable - - Disable the flavor profile - -.. option:: --driver - - Python module path to driver - - *Network version 2 only* - -.. option:: --metainfo - - Metainfo for the flavor profile - - *Network version 2 only* - - -network flavor profile delete ------------------------------ - -Delete network flavor profile - -.. program:: network flavor profile delete -.. code:: bash - - openstack network flavor profile delete - [ ...] - -.. describe:: - - Flavor profile(s) to delete (ID only) - -network flavor profile list ---------------------------- - -List network flavor profiles - -.. program:: network flavor profile list -.. code:: bash - - openstack network flavor profile list - -network flavor profile set --------------------------- - -Set network flavor profile properties - -.. program:: network flavor profile set -.. code:: bash - - openstack network flavor profile set - [--description ] - [--driver ] - [--enable | --disable] - [--metainfo ] - - - -.. option:: --description - - Description of the flavor profile - -.. option:: --driver - - Python module path to driver - -.. option:: --enable (Default) - - Enable the flavor profile - -.. option:: --disable - - Disable the flavor profile - -.. option:: --metainfo - - Metainfo for the flavor profile - -.. describe:: - - Flavor profile to update (ID only) - -network flavor profile show ---------------------------- - -Show network flavor profile - -.. program:: network flavor profile show -.. code:: bash - - openstack network flavor profile show - - -.. describe:: - - Flavor profile to display (ID only) +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor profile * diff --git a/doc/source/cli/command-objects/network-flavor.rst b/doc/source/cli/command-objects/network-flavor.rst index 2d23bf056a..6fe7504a0d 100644 --- a/doc/source/cli/command-objects/network-flavor.rst +++ b/doc/source/cli/command-objects/network-flavor.rst @@ -8,176 +8,26 @@ service flavors. Network v2 -network flavor add profile --------------------------- +.. NOTE(efried): have to list these out one by one; 'network flavor' pulls in + ... profile *. -Add network flavor to service profile +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor add profile -.. program:: network flavor add profile -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor create - openstack network flavor add profile - - +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor delete -.. describe:: +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor list - Flavor to which service profile is added. (Name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor remove profile -.. describe:: +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor set - Service profile to be added to flavor. (ID only) - -.. _network_flavor_add_profile: - -network flavor create ---------------------- - -Create network flavor - -.. program:: network flavor create -.. code:: bash - - openstack network flavor create - --service-type - [--description ] - [--enable | --disable] - [--project [--project-domain ]] - - -.. option:: --service-type - - Service type to which the flavor applies to: e.g. VPN. - (See openstack :ref:`\ `) (required) - -.. option:: --description - - Description for the flavor - -.. option:: --enable - - Enable the flavor (default) - -.. option:: --disable - - Disable the flavor - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). This can - be used in case collisions between project names - exist. - -.. describe:: - - Name for the flavor - -.. _network_flavor_create: - -network flavor delete ---------------------- - -Delete network flavor(s) - -.. program:: network flavor delete -.. code:: bash - - openstack network flavor delete - [ ...] - -.. describe:: - - Flavor(s) to delete (name or ID) - -.. _network_flavor_delete: - -network flavor list -------------------- - -List network flavors - -.. program:: network flavor list -.. code:: bash - - openstack network flavor list - -.. _network_flavor_list: - -network flavor remove profile ------------------------------ - -Remove network flavor from service profile - -.. program:: network flavor remove profile -.. code:: bash - - openstack network flavor remove profile - - - -.. describe:: - - Flavor from which service profile is removed. (Name or ID) - -.. describe:: - - Service profile to be removed from flavor. (ID only) - -.. _network_flavor_remove_profile: - -network flavor set ------------------- - -Set network flavor properties - -.. program:: network flavor set -.. code:: bash - - openstack network flavor set - [--name ] - [--description ] - [--enable | --disable] - - -.. option:: --name - - Set flavor name - -.. option:: --description - - Set network flavor description - -.. option:: --enable - - Enable network flavor - -.. option:: --disable - - Disable network flavor - -.. describe:: - - Flavor to update (name or ID) - -.. _network_flavor_set: - -network flavor show -------------------- - -Show network flavor - -.. program:: network flavor show -.. code:: bash - - openstack network flavor show - - -.. describe:: - - Flavor to display (name or ID) - -.. _network_flavor_show: +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor show diff --git a/doc/source/cli/command-objects/network-meter-rule.rst b/doc/source/cli/command-objects/network-meter-rule.rst index 22d50aa907..616c4bc516 100644 --- a/doc/source/cli/command-objects/network-meter-rule.rst +++ b/doc/source/cli/command-objects/network-meter-rule.rst @@ -9,93 +9,5 @@ metering extension. Network v2 -network meter rule create -------------------------- - -Create meter rule - -.. program:: network meter rule create -.. code:: bash - - openstack network meter rule create - --remote-ip-prefix - [--ingress | --egress] - [--exclude | --include] - [--project [--project-domain ]] - - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name of ID). - This can be used in case collisions between project names exist. - -.. option:: --ingress - - Rule is applied to incoming traffic (default) - -.. option:: --egress - - Rule is applied to outgoing traffic - -.. option:: --exclude - - Exclude remote_ip_prefix from count of the traffic of IP addresses - -.. option:: --include - - Include remote_ip_prefix into count of the traffic of IP addresses - (default) - -.. option:: --remote-ip-prefix - - The remote IP prefix to associate with this metering rule packet - -.. _network_meter_rule_create: -.. describe:: - - Meter to associate with this meter rule (name or ID) - - -network meter rule delete -------------------------- - -Delete meter rule(s) - -.. program:: network meter rule delete -.. code:: bash - - openstack network meter rule delete [ ...] - -.. _network_meter_rule_delete: -.. describe:: - - ID of meter rule(s) to delete - -network meter rule list ------------------------ - -List meter rules - -.. program:: network meter rule list -.. code:: bash - - openstack network meter rule list - -network meter rule show ------------------------ - -Show meter rule - -.. program:: network meter rule show -.. code:: bash - - openstack network meter rule show - -.. _network_meter_show: -.. describe:: - - Meter rule to display (ID only) +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter rule * diff --git a/doc/source/cli/command-objects/network-meter.rst b/doc/source/cli/command-objects/network-meter.rst index 6077ce9226..5752e6f419 100644 --- a/doc/source/cli/command-objects/network-meter.rst +++ b/doc/source/cli/command-objects/network-meter.rst @@ -8,84 +8,17 @@ are specific to the L3 metering extension. Network v2 -network meter create --------------------- +.. NOTE(efried): have to list these out one by one; 'network meter *' pulls in + ... rule *. -Create network meter +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter create -.. program:: network meter create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter delete - openstack network meter create - [--project [--project-domain ]] - [--description ] - [--share | --no-share] - +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter list -.. option:: --project - - Owner's project (name of ID) - - *Network version 2 only* - -.. option:: --description - - Description of meter - - *Network version 2 only* - -.. option:: --share - - Share the meter between projects - -.. option:: --no-share - - Do not share the meter between projects (Default) - -.. _network_meter_create: -.. describe:: - - New meter name - -network meter delete --------------------- - -Delete network meter(s) - -.. program:: network meter delete -.. code:: bash - - openstack network meter delete - [ ...] - -.. _network_meter_delete: -.. describe:: - - Meter(s) to delete (name or ID) - -network meter list ------------------- - -List network meters - -.. program:: network meter list -.. code:: bash - - openstack network meter list - - -network meter show ------------------- - -Show network meter - -.. program:: network meter show -.. code:: bash - - openstack network meter show - - -.. _network_meter_show: -.. describe:: - - Meter to display (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter show diff --git a/doc/source/cli/command-objects/network-qos-policy.rst b/doc/source/cli/command-objects/network-qos-policy.rst index 2e86f8f3ac..af7a104070 100644 --- a/doc/source/cli/command-objects/network-qos-policy.rst +++ b/doc/source/cli/command-objects/network-qos-policy.rst @@ -7,157 +7,5 @@ network or a port. Network v2 -network qos policy create -------------------------- - -Create new Network QoS policy - -.. program:: network qos policy create -.. code:: bash - - openstack network qos policy create - [--description ] - [--share | --no-share] - [--project ] - [--project-domain ] - [--default | --no-default] - - -.. option:: --description - - Description of the QoS policy - -.. option:: --share - - Make the QoS policy accessible by other projects - -.. option:: --no-share - - Make the QoS policy not accessible by other projects (default) - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --default - - Set this as a default network QoS policy - -.. option:: --no-default - - Set this as a non-default network QoS policy - -.. _network_qos_policy_create-name: -.. describe:: - - New QoS policy specification name - -network qos policy delete -------------------------- - -Delete Network QoS policy - -.. program:: network qos policy delete -.. code:: bash - - openstack network qos policy delete - [ ...] - -.. _network_qos_policy_delete-qos-policy: -.. describe:: - - Network QoS policy(s) to delete (name or ID) - -network qos policy list ------------------------ - -List Network QoS policies - -.. program:: network qos policy list -.. code:: bash - - openstack network qos policy list - [--project [--project-domain ]] - [--share | --no-share] - -.. option:: --project - - List qos policies according to their project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --share - - List qos policies shared between projects - -.. option:: --no-share - - List qos policies not shared between projects - -network qos policy set ----------------------- - -Set Network QoS policy properties - -.. program:: network qos policy set -.. code:: bash - - openstack network qos policy set - [--name ] - [--description ] - [--share | --no-share] - [--default | --no-default] - - -.. option:: --name - - Name of the QoS policy - -.. option:: --description - - Description of the QoS policy - -.. option:: --share - - Make the QoS policy accessible by other projects - -.. option:: --no-share - - Make the QoS policy not accessible by other projects - -.. option:: --default - - Set this as a default network QoS policy - -.. option:: --no-default - - Set this as a non-default network QoS policy - -.. _network_qos_policy_set-qos-policy: -.. describe:: - - Network QoS policy to modify (name or ID) - -network qos policy show ------------------------ - -Display Network QoS policy details - -.. program:: network qos policy show -.. code:: bash - - openstack network qos policy show - - -.. _network_qos_policy_show-qos-policy: -.. describe:: - - Network QoS policy to display (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos policy * diff --git a/doc/source/cli/command-objects/network-qos-rule-type.rst b/doc/source/cli/command-objects/network-qos-rule-type.rst index de6f2474af..197a86002f 100644 --- a/doc/source/cli/command-objects/network-qos-rule-type.rst +++ b/doc/source/cli/command-objects/network-qos-rule-type.rst @@ -7,27 +7,5 @@ used. Network v2 -network qos rule type list --------------------------- - -List Network QoS rule types - -.. program:: network qos rule type list -.. code:: bash - - openstack network qos rule type list - -network qos rule type show --------------------------- - -Display Network QoS rule type details - -.. program:: network qos rule type show -.. code:: bash - - openstack network qos rule type show - - -.. describe:: - - Name of QoS rule type (minimum-bandwidth, dscp-marking, bandwidth-limit) +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule type * diff --git a/doc/source/cli/command-objects/network-qos-rule.rst b/doc/source/cli/command-objects/network-qos-rule.rst index 1baf5dbfeb..8b715c03ab 100644 --- a/doc/source/cli/command-objects/network-qos-rule.rst +++ b/doc/source/cli/command-objects/network-qos-rule.rst @@ -9,157 +9,20 @@ rules, each of them Network v2 -network qos rule create ------------------------ +.. NOTE(efried): have to list these out one by one; 'network qos rule *' pulls + network qos rule type *. -Create new Network QoS rule +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule create -.. program:: network qos rule create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule delete - openstack network qos rule create - --type - [--max-kbps ] - [--max-burst-kbits ] - [--dscp-marks ] - [--min-kbps ] - [--ingress | --egress] - +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule list -.. option:: --type +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule set - QoS rule type (minimum-bandwidth, dscp-marking, bandwidth-limit) - -.. option:: --max-kbps - - Maximum bandwidth in kbps - -.. option:: --max-burst-kbits - - Maximum burst in kilobits, 0 means automatic - -.. option:: --dscp-mark - - DSCP mark: value can be 0, even numbers from 8-56, excluding 42, 44, 50, - 52, and 54 - -.. option:: --min-kbps - - Minimum guaranteed bandwidth in kbps - -.. option:: --ingress - - Ingress traffic direction from the project point of view - -.. option:: --egress - - Egress traffic direction from the project point of view - -.. describe:: - - QoS policy that contains the rule (name or ID) - -network qos rule delete ------------------------ - -Delete Network QoS rule - -.. program:: network qos rule delete -.. code:: bash - - openstack network qos rule delete - - - -.. describe:: - - QoS policy that contains the rule (name or ID) - -.. describe:: - - Network QoS rule to delete (ID) - -network qos rule list ---------------------- - -List Network QoS rules - -.. program:: network qos rule list -.. code:: bash - - openstack network qos rule list - - -.. describe:: - - QoS policy that contains the rule (name or ID) - -network qos rule set --------------------- - -Set Network QoS rule properties - -.. program:: network qos rule set -.. code:: bash - - openstack network qos rule set - [--max-kbps ] - [--max-burst-kbits ] - [--dscp-marks ] - [--min-kbps ] - [--ingress | --egress] - - - -.. option:: --max-kbps - - Maximum bandwidth in kbps - -.. option:: --max-burst-kbits - - Maximum burst in kilobits, 0 means automatic - -.. option:: --dscp-mark - - DSCP mark: value can be 0, even numbers from 8-56, excluding 42, 44, 50, - 52, and 54 - -.. option:: --min-kbps - - Minimum guaranteed bandwidth in kbps - -.. option:: --ingress - - Ingress traffic direction from the project point of view - -.. option:: --egress - - Egress traffic direction from the project point of view - -.. describe:: - - QoS policy that contains the rule (name or ID) - -.. describe:: - - Network QoS rule to delete (ID) - -network qos rule show ---------------------- - -Display Network QoS rule details - -.. program:: network qos rule show -.. code:: bash - - openstack network qos rule show - - - -.. describe:: - - QoS policy that contains the rule (name or ID) - -.. describe:: - - Network QoS rule to delete (ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule show diff --git a/doc/source/cli/command-objects/network-rbac.rst b/doc/source/cli/command-objects/network-rbac.rst index 22733a2cd2..d9e48554d3 100644 --- a/doc/source/cli/command-objects/network-rbac.rst +++ b/doc/source/cli/command-objects/network-rbac.rst @@ -8,136 +8,5 @@ to network resources for specific projects. Network v2 -network rbac create -------------------- - -Create network RBAC policy - -.. program:: network rbac create -.. code:: bash - - openstack network rbac create - --type - --action - [--target-project | --target-all-projects] - [--target-project-domain ] - [--project [--project-domain ]] - - -.. option:: --type - - Type of the object that RBAC policy affects ("security_group", "qos_policy" or "network") (required) - -.. option:: --action - - Action for the RBAC policy ("access_as_external" or "access_as_shared") (required) - -.. option:: --target-project - - The project to which the RBAC policy will be enforced (name or ID) - -.. option:: --target-all-projects - - Allow creating RBAC policy for all projects. - -.. option:: --target-project-domain - - Domain the target project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --project - - The owner project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _network_rbac_create-rbac-policy: -.. describe:: - - The object to which this RBAC policy affects (name or ID) - -network rbac delete -------------------- - -Delete network RBAC policy(s) - -.. program:: network rbac delete -.. code:: bash - - openstack network rbac delete - [ ...] - -.. _network_rbac_delete-rbac-policy: -.. describe:: - - RBAC policy(s) to delete (ID only) - -network rbac list ------------------ - -List network RBAC policies - -.. program:: network rbac list -.. code:: bash - - openstack network rbac list - [--type ] - [--action ] - [--long] - -.. option:: --type - - List network RBAC policies according to given object type ("security_group", "qos_policy" or "network") - -.. option:: --action - - List network RBAC policies according to given action ("access_as_external" or "access_as_shared") - -.. option:: --long - - List additional fields in output - -network rbac set ----------------- - -Set network RBAC policy properties - -.. program:: network rbac set -.. code:: bash - - openstack network rbac set - [--target-project [--target-project-domain ]] - - -.. option:: --target-project - - The project to which the RBAC policy will be enforced (name or ID) - -.. option:: --target-project-domain - - Domain the target project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _network_rbac_set-rbac-policy: -.. describe:: - - RBAC policy to be modified (ID only) - -network rbac show ------------------ - -Display network RBAC policy details - -.. program:: network rbac show -.. code:: bash - - openstack network rbac show - - -.. _network_rbac_show-rbac-policy: -.. describe:: - - RBAC policy (ID only) +.. autoprogram-cliff:: openstack.network.v2 + :command: network rbac * diff --git a/doc/source/cli/command-objects/network-segment.rst b/doc/source/cli/command-objects/network-segment.rst index d6a66aa0b9..2e9143801d 100644 --- a/doc/source/cli/command-objects/network-segment.rst +++ b/doc/source/cli/command-objects/network-segment.rst @@ -9,125 +9,20 @@ within a network may not be guaranteed. Network v2 -network segment create ----------------------- +.. NOTE(efried): have to list these out one by one; 'network segment *' pulls + ... range *. -Create new network segment +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment create -.. program:: network segment create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment delete - openstack network segment create - [--description ] - [--physical-network ] - [--segment ] - --network - --network-type - +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment list -.. option:: --description +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment set - Network segment description - -.. option:: --physical-network - - Physical network name of this network segment - -.. option:: --segment - - Segment identifier for this network segment which is - based on the network type, VLAN ID for vlan network - type and tunnel ID for geneve, gre and vxlan network - types - -.. option:: --network - - Network this network segment belongs to (name or ID) - -.. option:: --network-type - - Network type of this network segment - (flat, geneve, gre, local, vlan or vxlan) - -.. _network_segment_create-name: -.. describe:: - - New network segment name - -network segment delete ----------------------- - -Delete network segment(s) - -.. program:: network segment delete -.. code:: bash - - openstack network segment delete - [ ...] - -.. _network_segment_delete-segment: -.. describe:: - - Network segment(s) to delete (name or ID) - -network segment list --------------------- - -List network segments - -.. program:: network segment list -.. code:: bash - - openstack network segment list - [--long] - [--network ] - -.. option:: --long - - List additional fields in output - -.. option:: --network - - List network segments that belong to this network (name or ID) - -network segment set -------------------- - -Set network segment properties - -.. program:: network segment set -.. code:: bash - - openstack network segment set - [--description ] - [--name ] - - -.. option:: --description - - Set network segment description - -.. option:: --name - - Set network segment name - -.. _network_segment_set-segment: -.. describe:: - - Network segment to modify (name or ID) - -network segment show --------------------- - -Display network segment details - -.. program:: network segment show -.. code:: bash - - openstack network segment show - - -.. _network_segment_show-segment: -.. describe:: - - Network segment to display (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment show diff --git a/doc/source/cli/command-objects/network-service-provider.rst b/doc/source/cli/command-objects/network-service-provider.rst index f46073f677..52d0288dd0 100644 --- a/doc/source/cli/command-objects/network-service-provider.rst +++ b/doc/source/cli/command-objects/network-service-provider.rst @@ -9,12 +9,5 @@ Network v2 .. _network_service_provider_list: -network service provider list ------------------------------ - -List service providers - -.. program:: network service provider list -.. code:: bash - - openstack network service provider list +.. autoprogram-cliff:: openstack.network.v2 + :command: network service provider list diff --git a/doc/source/cli/command-objects/network.rst b/doc/source/cli/command-objects/network.rst index 75113f89da..626b11a928 100644 --- a/doc/source/cli/command-objects/network.rst +++ b/doc/source/cli/command-objects/network.rst @@ -11,505 +11,23 @@ provider networks. Networks can be connected via routers. Compute v2, Network v2 -network create --------------- +.. NOTE(efried): have to list these out one by one; 'network *' pulls in + ... flavor *, ... qos policy *, etc. -Create new network +.. autoprogram-cliff:: openstack.network.v2 + :command: network create -.. program:: network create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: network delete - openstack network create - [--project [--project-domain ]] - [--enable | --disable] - [--share | --no-share] - [--description ] - [--mtu ] - [--availability-zone-hint ] - [--enable-port-security | --disable-port-security] - [--external [--default | --no-default] | --internal] - [--provider-network-type ] - [--provider-physical-network ] - [--provider-segment ] - [--qos-policy ] - [--transparent-vlan | --no-transparent-vlan] - [--dns-domain ] - [--tag | --no-tag] - +.. autoprogram-cliff:: openstack.network.v2 + :command: network list -.. option:: --project +.. autoprogram-cliff:: openstack.network.v2 + :command: network set - Owner's project (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network show - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --enable - - Enable network (default) - - *Network version 2 only* - -.. option:: --disable - - Disable network - - *Network version 2 only* - -.. option:: --share - - Share the network between projects - -.. option:: --no-share - - Do not share the network between projects - -.. option:: --description - - Set network description - - *Network version 2 only* - -.. option:: --mtu - - Set network mtu - - *Network version 2 only* - -.. option:: --availability-zone-hint - - Availability Zone in which to create this network - (Network Availability Zone extension required, - repeat option to set multiple availability zones) - - *Network version 2 only* - -.. option:: --enable-port-security - - Enable port security by default for ports created on - this network (default) - - *Network version 2 only* - -.. option:: --disable-port-security - - Disable port security by default for ports created on - this network - - *Network version 2 only* - -.. option:: --subnet - - IPv4 subnet for fixed IPs (in CIDR notation) - - *Compute version 2 only* - -.. option:: --external - - Set this network as an external network - (external-net extension required) - - *Network version 2 only* - -.. option:: --internal - - Set this network as an internal network (default) - - *Network version 2 only* - -.. option:: --default - - Specify if this network should be used as - the default external network - - *Network version 2 only* - -.. option:: --no-default - - Do not use the network as the default external network - (default) - - *Network version 2 only* - -.. option:: --provider-network-type - - The physical mechanism by which the virtual network is implemented. - The supported options are: flat, geneve, gre, local, vlan, vxlan. - - *Network version 2 only* - -.. option:: --provider-physical-network - - Name of the physical network over which the virtual network is implemented - - *Network version 2 only* - -.. option:: --provider-segment - - VLAN ID for VLAN networks or Tunnel ID for GENEVE/GRE/VXLAN networks - - *Network version 2 only* - -.. option:: --qos-policy - - QoS policy to attach to this network (name or ID) - - *Network version 2 only* - -.. option:: --transparent-vlan - - Make the network VLAN transparent - - *Network version 2 only* - -.. option:: --no-transparent-vlan - - Do not make the network VLAN transparent - - *Network version 2 only* - -.. option:: --dns-domain - - Set DNS domain for this network (requires DNS integration extension). - -.. option:: --tag - - Tag to be added to the network (repeat option to set multiple tags) - - *Network version 2 only* - -.. option:: --no-tag - - No tags associated with the network - - *Network version 2 only* - -.. _network_create-name: -.. describe:: - - New network name - -network delete --------------- - -Delete network(s) - -.. program:: network delete -.. code:: bash - - openstack network delete - [ ...] - -.. _network_delete-network: -.. describe:: - - Network(s) to delete (name or ID) - -network list ------------- - -List networks - -.. program:: network list -.. code:: bash - - openstack network list - [--external | --internal] - [--long] - [--name ] - [--enable | --disable] - [--project [--project-domain ]] - [--share | --no-share] - [--status ] - [--provider-network-type ] - [--provider-physical-network ] - [--provider-segment ] - [--agent ] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --external - - List external networks - - *Network version 2 only* - -.. option:: --internal - - List internal networks - - *Network version 2 only* - -.. option:: --long - - List additional fields in output - - *Network version 2 only* - -.. option:: --name - - List networks according to their name - - *Network version 2 only* - -.. option:: --enable - - List enabled networks - - *Network version 2 only* - -.. option:: --disable - - List disabled networks - - *Network version 2 only* - -.. option:: --project - - List networks according to their project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --share - - List networks shared between projects - - *Network version 2 only* - -.. option:: --no-share - - List networks not shared between projects - - *Network version 2 only* - -.. option:: --status - - List networks according to their status - ('ACTIVE', 'BUILD', 'DOWN', 'ERROR') - -.. option:: --provider-network-type - - List networks according to their physical mechanisms. - The supported options are: flat, geneve, gre, local, vlan, vxlan. - - *Network version 2 only* - -.. option:: --provider-physical-network - - List networks according to name of the physical network - - *Network version 2 only* - -.. option:: --provider-segment - - List networks according to VLAN ID for VLAN networks - or Tunnel ID for GENEVE/GRE/VXLAN networks - - *Network version 2 only* - -.. option:: --agent - - List networks hosted by agent (ID only) - - *Network version 2 only* - -.. option:: --tags [,,...] - - List networks which have all given tag(s) - - *Network version 2 only* - -.. option:: --any-tags [,,...] - - List networks which have any given tag(s) - - *Network version 2 only* - -.. option:: --not-tags [,,...] - - Exclude networks which have all given tag(s) - - *Network version 2 only* - -.. option:: --not-any-tags [,,...] - - Exclude networks which have any given tag(s) - - *Network version 2 only* - -network set ------------ - -Set network properties - -*Network version 2 only* - -.. program:: network set -.. code:: bash - - openstack network set - [--name ] - [--enable | --disable] - [--share | --no-share] - [--description ] - [--mtu ] - [--enable-port-security | --disable-port-security] - [--external [--default | --no-default] | --internal] - [--provider-network-type ] - [--provider-physical-network ] - [--provider-segment ] - [--qos-policy | --no-qos-policy] - [--dns-domain ] - [--tag ] [--no-tag] - - -.. option:: --name - - Set network name - -.. option:: --enable - - Enable network - -.. option:: --disable - - Disable network - -.. option:: --share - - Share the network between projects - -.. option:: --no-share - - Do not share the network between projects - -.. option:: --description - - Set network description - -.. option:: --mtu - - Set network mtu - -.. option:: --enable-port-security - - Enable port security by default for ports created on - this network - -.. option:: --disable-port-security - - Disable port security by default for ports created on - this network - -.. option:: --external - - Set this network as an external network. - (external-net extension required) - -.. option:: --internal - - Set this network as an internal network - -.. option:: --default - - Set the network as the default external network - -.. option:: --no-default - - Do not use the network as the default external network. - -.. option:: --provider-network-type - - The physical mechanism by which the virtual network is implemented. - The supported options are: flat, gre, local, vlan, vxlan. - -.. option:: --provider-physical-network - - Name of the physical network over which the virtual network is implemented - -.. option:: --provider-segment - - VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN networks - -.. option:: --qos-policy - - QoS policy to attach to this network (name or ID) - -.. option:: --no-qos-policy - - Remove the QoS policy attached to this network - -.. option:: --dns-domain - - Set DNS domain for this network (requires DNS integration extension). - -.. option:: --tag - - Tag to be added to the network (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the network. Specify both --tag - and --no-tag to overwrite current tags - -.. _network_set-network: -.. describe:: - - Network to modify (name or ID) - -network show ------------- - -Display network details - -.. program:: network show -.. code:: bash - - openstack network show - - -.. _network_show-network: -.. describe:: - - Network to display (name or ID) - -network unset -------------- - -Unset network properties - -*Network version 2 only* - -.. program:: network unset -.. code:: bash - - openstack network unset - [--tag | --all-tag] - - -.. option:: --tag - - Tag to be removed from the network - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the network - -.. _network_unset-network: -.. describe:: - - Network to modify (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network unset diff --git a/doc/source/cli/command-objects/network_segment_range.rst b/doc/source/cli/command-objects/network_segment_range.rst index 71155d2224..f40007f385 100644 --- a/doc/source/cli/command-objects/network_segment_range.rst +++ b/doc/source/cli/command-objects/network_segment_range.rst @@ -10,159 +10,5 @@ administrator to control the segment ranges globally or on a per-tenant basis. Network v2 -network segment range create ----------------------------- - -Create new network segment range - -.. program:: network segment range create -.. code:: bash - - openstack network segment range create - (--private | --shared) - [--project [--project-domain ]] - --network-type - [--physical-network ] - --minimum - --maximum - - -.. option:: --private - - Network segment range is assigned specifically to the project - -.. option:: --shared - - Network segment range is shared with other projects - -.. option:: --project - - Network segment range owner (name or ID). Optional when the segment - range is shared - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --physical-network - - Physical network name of this network segment range - -.. option:: --network-type - - Network type of this network segment range - (geneve, gre, vlan or vxlan) - -.. option:: --minimum - - Minimum segment identifier for this network segment range which is based - on the network type, VLAN ID for vlan network type and tunnel ID for - geneve, gre and vxlan network types - -.. option:: --maximum - - Maximum segment identifier for this network segment range which is based - on the network type, VLAN ID for vlan network type and tunnel ID for - geneve, gre and vxlan network types - -.. _network_segment_range_create-name: -.. describe:: - - Name of new network segment range - -network segment range delete ----------------------------- - -Delete network segment range(s) - -.. program:: network segment range delete -.. code:: bash - - openstack network segment range delete - [ ...] - -.. _network_segment_range_delete-network-segment-range: -.. describe:: - - Network segment range (s) to delete (name or ID) - -network segment range list --------------------------- - -List network segment ranges - -.. program:: network segment range list -.. code:: bash - - openstack network segment range list - [--long] - [--used | --unused] - [--available | --unavailable] - -.. option:: --long - - List additional fields in output - -.. option:: --used - - List network segment ranges that have segments in use - -.. option:: --unused - - List network segment ranges that do not have segments not in use - -.. option:: --available - - List network segment ranges that have available segments - -.. option:: --unavailable - - List network segment ranges without available segments - -network segment range set -------------------------- - -Set network segment range properties - -.. program:: network segment range set -.. code:: bash - - openstack network segment range set - [--name ] - [--minimum ] - [--maximum ] - - -.. option:: --name - - Set network segment range name - -.. option:: --minimum - - Set network segment range minimum segment identifier - -.. option:: --maximum - - Set network segment range maximum segment identifier - -.. _network_segment_range_set-network-segment-range: -.. describe:: - - Network segment range to modify (name or ID) - -network segment range show --------------------------- - -Display network segment range details - -.. program:: network segment range show -.. code:: bash - - openstack network segment range show - - -.. _network_segment_range_show-network-segment-range: -.. describe:: - - Network segment range to display (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment range * diff --git a/doc/source/cli/command-objects/port.rst b/doc/source/cli/command-objects/port.rst index bc9f5dde02..3af5272eb2 100644 --- a/doc/source/cli/command-objects/port.rst +++ b/doc/source/cli/command-objects/port.rst @@ -8,486 +8,5 @@ configuration, such as the MAC and IP addresses to be used on that port. Network v2 -port create ------------ - -Create new port - -.. program:: port create -.. code:: bash - - openstack port create - --network - [--description ] - [--fixed-ip subnet=,ip-address= | --no-fixed-ip] - [--device ] - [--device-owner ] - [--vnic-type ] - [--binding-profile ] - [--host ] - [--enable | --disable] - [--enable-uplink-status-propagation | --disable-uplink-status-propagation] - [--mac-address ] - [--security-group | --no-security-group] - [--dns-domain ] - [--dns-name ] - [--allowed-address ip-address=[,mac-address=]] - [--qos-policy ] - [--project [--project-domain ]] - [--enable-port-security | --disable-port-security] - [--tag | --no-tag] - - -.. option:: --network - - Network this port belongs to (name or ID) - -.. option:: --description - - Description of this port - -.. option:: --fixed-ip subnet=,ip-address= - - Desired IP and/or subnet for this port (name or ID): - subnet=,ip-address= - (repeat option to set multiple fixed IP addresses) - -.. option:: --no-fixed-ip - - No IP or subnet for this port - -.. option:: --device - - Port device ID - -.. option:: --device-owner - - Device owner of this port. This is the entity that uses - the port (for example, network:dhcp). - -.. option:: --vnic-type - - VNIC type for this port (direct | direct-physical | macvtap | normal | baremetal | - virtio-forwarder, default: normal) - -.. option:: --binding-profile - - Custom data to be passed as binding:profile. Data may - be passed as = or JSON. - (repeat option to set multiple binding:profile data) - -.. option:: --host - - Allocate port on host ```` (ID only) - -.. option:: --enable - - Enable port (default) - -.. option:: --disable - - Disable port - -.. option:: --enable-uplink-status-propagation - - Enable uplink status propagate - -.. option:: --disable-uplink-status-propagation - - Disable uplink status propagate (default) - -.. option:: --mac-address - - MAC address of this port - -.. option:: --security-group - - Security group to associate with this port (name or ID) - (repeat option to set multiple security groups) - -.. option:: --no-security-group - - Associate no security groups with this port - -.. option:: --dns-domain - - Set DNS domain for this port - (requires dns_domain for ports extension) - -.. option:: --dns-name - - Set DNS name for this port - (requires DNS integration extension) - -.. option:: --allowed-address ip-address=[,mac-address=] - - Add allowed-address pair associated with this port: - ip-address=[,mac-address=] - (repeat option to set multiple allowed-address pairs) - -.. option:: --qos-policy - - Attach QoS policy to this port (name or ID) - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --enable-port-security - - Enable port security for this port (Default) - -.. option:: --disable-port-security - - Disable port security for this port - -.. option:: --tag - - Tag to be added to the port (repeat option to set multiple tags) - -.. option:: --no-tag - - No tags associated with the port - -.. _port_create-name: -.. describe:: - - Name of this port - -port delete ------------ - -Delete port(s) - -.. program:: port delete -.. code:: bash - - openstack port delete - [ ...] - -.. _port_delete-port: -.. describe:: - - Port(s) to delete (name or ID) - -port list ---------- - -List ports - -.. program:: port list -.. code:: bash - - openstack port list - [--device-owner ] - [--router | --server | --device-id ] - [--network ] - [--mac-address ] - [--fixed-ip subnet=,ip-address=,ip-substring=] - [--long] - [--project [--project-domain ]] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --device-owner - - List only ports with the specified device owner. This is - the entity that uses the port (for example, network:dhcp). - -.. option:: --router - - List only ports attached to this router (name or ID) - -.. option:: --server - - List only ports attached to this server (name or ID) - -.. option:: --device-id - - List only ports with the specified device ID - -.. option:: --network - - List only ports attached to this network (name or ID) - -.. option:: --mac-address - - List only ports with this MAC address - -.. option:: --fixed-ip subnet=,ip-address=,ip-substring= - - Desired IP address, IP address substring and/or subnet (name or ID) for - filtering ports: - subnet=,ip-address=,ip-substring= - (repeat option to set multiple fixed IP addresses) - -.. option:: --long - - List additional fields in output - -.. option:: --project - - List ports according to their project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --tags [,,...] - - List ports which have all given tag(s) - -.. option:: --any-tags [,,...] - - List ports which have any given tag(s) - -.. option:: --not-tags [,,...] - - Exclude ports which have all given tag(s) - -.. option:: --not-any-tags [,,...] - - Exclude ports which have any given tag(s) - -port set --------- - -Set port properties - -.. program:: port set -.. code:: bash - - openstack port set - [--description ] - [--fixed-ip subnet=,ip-address=] - [--no-fixed-ip] - [--device ] - [--device-owner ] - [--vnic-type ] - [--binding-profile ] - [--no-binding-profile] - [--host ] - [--qos-policy ] - [--enable | --disable] - [--name ] - [--mac-address ] - [--security-group ] - [--no-security-group] - [--enable-port-security | --disable-port-security] - [--dns-domain ] - [--dns-name ] - [--allowed-address ip-address=[,mac-address=]] - [--no-allowed-address] - [--data-plane-status ] - [--tag ] [--no-tag] - - -.. option:: --description - - Description of this port - -.. option:: --fixed-ip subnet=,ip-address= - - Desired IP and/or subnet for this port (name or ID): - subnet=,ip-address= - (repeat option to set multiple fixed IP addresses) - -.. option:: --no-fixed-ip - - Clear existing information of fixed IP addresses. - Specify both :option:`--fixed-ip` and :option:`--no-fixed-ip` - to overwrite the current fixed IP addresses. - -.. option:: --device - - Port device ID - -.. option:: --device-owner - - Device owner of this port. This is the entity that uses - the port (for example, network:dhcp). - -.. option:: --vnic-type - - VNIC type for this port (direct | direct-physical | macvtap | normal | baremetal | - virtio-forwarder, default: normal) - -.. option:: --binding-profile - - Custom data to be passed as binding:profile. Data may - be passed as = or JSON. - (repeat option to set multiple binding:profile data) - -.. option:: --no-binding-profile - - Clear existing information of binding:profile. - Specify both :option:`--binding-profile` and :option:`--no-binding-profile` - to overwrite the current binding:profile information. - -.. option:: --host - - Allocate port on host ```` (ID only) - -.. option:: --qos-policy - - Attach QoS policy to this port (name or ID) - -.. option:: --enable - - Enable port - -.. option:: --disable - - Disable port - -.. option:: --name - - Set port name - -.. option:: --mac-address - - Set port's MAC address (admin only) - -.. option:: --security-group - - Security group to associate with this port (name or ID) - (repeat option to set multiple security groups) - -.. option:: --no-security-group - - Clear existing security groups associated with this port - -.. option:: --enable-port-security - - Enable port security for this port - -.. option:: --disable-port-security - - Disable port security for this port - -.. option:: --dns-domain - - Set DNS domain for this port - (requires dns_domain for ports extension) - -.. option:: --dns-name - - Set DNS name for this port - (requires DNS integration extension) - -.. option:: --allowed-address ip-address=[,mac-address=] - - Add allowed-address pair associated with this port: - ip-address=[,mac-address=] - (repeat option to set multiple allowed-address pairs) - -.. option:: --no-allowed-address - - Clear existing allowed-address pairs associated - with this port. - (Specify both --allowed-address and --no-allowed-address - to overwrite the current allowed-address pairs) - -.. option:: --data-plane-status - - Set data plane status of this port (ACTIVE | DOWN). - Unset it to None with the 'port unset' command - (requires data plane status extension) - -.. option:: --tag - - Tag to be added to the port (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the port. Specify both --tag - and --no-tag to overwrite current tags - -.. _port_set-port: -.. describe:: - - Port to modify (name or ID) - -port show ---------- - -Display port details - -.. program:: port show -.. code:: bash - - openstack port show - - -.. _port_show-port: -.. describe:: - - Port to display (name or ID) - -port unset ----------- - -Unset port properties - -.. program:: port unset -.. code:: bash - - openstack port unset - [--fixed-ip subnet=,ip-address= [...]] - [--binding-profile [...]] - [--security-group [...]] - [--allowed-address ip-address=[,mac-address=] [...]] - [--qos-policy] - [--data-plane-status] - [--tag | --all-tag] - - -.. option:: --fixed-ip subnet=,ip-address= - - Desired IP and/or subnet which should be removed - from this port (name or ID): subnet=,ip-address= - (repeat option to unset multiple fixed IP addresses) - -.. option:: --binding-profile - - Desired key which should be removed from binding-profile - (repeat option to unset multiple binding:profile data) - -.. option:: --security-group - - Security group which should be removed from this port (name or ID) - (repeat option to unset multiple security groups) - -.. option:: --allowed-address ip-address=[,mac-address=] - - Desired allowed-address pair which should be removed from this port: - ip-address=[,mac-address=] - (repeat option to unset multiple allowed-address pairs) - -.. option:: --qos-policy - - Remove the QoS policy attached to the port - -.. option:: --data-plane-status - - Clear existing information of data plane status - -.. option:: --tag - - Tag to be removed from the port - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the port - -.. _port_unset-port: -.. describe:: - - Port to modify (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: port * diff --git a/doc/source/cli/command-objects/router.rst b/doc/source/cli/command-objects/router.rst index 9c9364bc75..6e8e05d7c0 100644 --- a/doc/source/cli/command-objects/router.rst +++ b/doc/source/cli/command-objects/router.rst @@ -8,402 +8,5 @@ network access for servers on project networks. Network v2 -router add port ---------------- - -Add a port to a router - -.. program:: router add port -.. code:: bash - - openstack router add port - - - -.. _router_add_port: - -.. describe:: - - Router to which port will be added (name or ID) - -.. describe:: - - Port to be added (name or ID) - -router add subnet ------------------ - -Add a subnet to a router - -.. program:: router add subnet -.. code:: bash - - openstack router add subnet - - - -.. _router_add_subnet: - -.. describe:: - - Router to which subnet will be added (name or ID) - -.. describe:: - - Subnet to be added (name or ID) - -router create -------------- - -Create new router - -.. program:: router create -.. code:: bash - - openstack router create - [--project [--project-domain ]] - [--enable | --disable] - [--distributed | --centralized] - [--ha | --no-ha] - [--description ] - [--availability-zone-hint ] - [--tag | --no-tag] - - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --enable - - Enable router (default) - -.. option:: --disable - - Disable router - -.. option:: --distributed - - Create a distributed router - - The default router type (distributed vs centralized) is determined by a - configuration setting in the OpenStack deployment. Since we are unable - to know that default wihtout attempting to actually create a router it - is suggested to use either :option:`--distributed` or :option:`--centralized` - in situations where multiple cloud deployments may be used. - -.. option:: --centralized - - Create a centralized router - - See the note in :option:`--distributed` regarding the default used when - creating a new router. - -.. option:: --ha - - Create a highly available router - -.. option:: --no-ha - - Create a legacy router - -.. option:: --description - - Set router description - -.. option:: --availability-zone-hint - - Availability Zone in which to create this router - (Router Availability Zone extension required, - repeat option to set multiple availability zones) - -.. option:: --tag - - Tag to be added to the router (repeat option to set multiple tags) - -.. option:: --no-tag - - No tags associated with the router - -.. _router_create-name: -.. describe:: - - New router name - -router delete -------------- - -Delete router(s) - -.. program:: router delete -.. code:: bash - - openstack router delete - [ ...] - -.. _router_delete-router: -.. describe:: - - Router(s) to delete (name or ID) - -router list ------------ - -List routers - -.. program:: router list -.. code:: bash - - openstack router list - [--name ] - [--enable | --disable] - [--long] - [--project [--project-domain ]] - [--agent ] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --agent - - List routers hosted by an agent (ID only) - -.. option:: --long - - List additional fields in output - -.. option:: --name - - List routers according to their name - -.. option:: --enable - - List enabled routers - -.. option:: --disable - - List disabled routers - -.. option:: --project - - List routers according to their project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --tags [,,...] - - List routers which have all given tag(s) - -.. option:: --any-tags [,,...] - - List routers which have any given tag(s) - -.. option:: --not-tags [,,...] - - Exclude routers which have all given tag(s) - -.. option:: --not-any-tags [,,...] - - Exclude routers which have any given tag(s) - -router remove port ------------------- - -Remove a port from a router - -.. program:: router remove port -.. code:: bash - - openstack router remove port - - - -.. _router_remove_port: - -.. describe:: - - Router from which port will be removed (name or ID) - -.. describe:: - - Port to be removed and deleted (name or ID) - -router remove subnet --------------------- - -Remove a subnet from a router - -.. program:: router remove subnet -.. code:: bash - - openstack router remove subnet - - - -.. _router_remove_subnet: - -.. describe:: - - Router from which subnet will be removed (name or ID) - -.. describe:: - - Subnet to be removed (name or ID) - -router set ----------- - -Set router properties - -.. program:: router set -.. code:: bash - - openstack router set - [--name ] - [--enable | --disable] - [--distributed | --centralized] - [--description ] - [--route destination=,gateway= | --no-route] - [--ha | --no-ha] - [--external-gateway [--enable-snat|--disable-snat] [--fixed-ip subnet=,ip-address=]] - [--tag ] [--no-tag] - - -.. option:: --name - - Set router name - -.. option:: --enable - - Enable router - -.. option:: --disable - - Disable router - -.. option:: --distributed - - Set router to distributed mode (disabled router only) - -.. option:: --centralized - - Set router to centralized mode (disabled router only) - -.. option:: --description - - Set router description - -.. option:: --route destination=,gateway= - - Routes associated with the router - destination: destination subnet (in CIDR notation) - gateway: nexthop IP address - (repeat option to set multiple routes) - -.. option:: --no-route - - Clear routes associated with the router. - Specify both --route and --no-route to overwrite - current value of route. - -.. option:: --ha - - Set the router as highly available (disabled router only) - -.. option:: --no-ha - - Clear high availablability attribute of the router (disabled router only) - -.. option:: --external-gateway - - External Network used as router's gateway (name or ID) - -.. option:: --enable-snat - - Enable Source NAT on external gateway - -.. option:: --disable-snat - - Disable Source NAT on external gateway - -.. option:: --fixed-ip subnet=,ip-address= - - Desired IP and/or subnet (name or ID) on external gateway: - subnet=,ip-address= - (repeat option to set multiple fixed IP addresses) - -.. option:: --tag - - Tag to be added to the router (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the router. Specify both --tag - and --no-tag to overwrite current tags - -.. _router_set-router: -.. describe:: - - Router to modify (name or ID) - -router show ------------ - -Display router details - -.. program:: router show -.. code:: bash - - openstack router show - - -.. _router_show-router: -.. describe:: - - Router to display (name or ID) - -router unset ------------- - -Unset router properties - -.. program:: router unset -.. code:: bash - - openstack router unset - [--route destination=,gateway=] - [--external-gateway] - [--tag | --all-tag] - - -.. option:: --route destination=,gateway= - - Routes to be removed from the router - destination: destination subnet (in CIDR notation) - gateway: nexthop IP address - (repeat option to unset multiple routes) - -.. option:: --external-gateway - - Remove external gateway information from the router - -.. option:: --tag - - Tag to be removed from the router - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the router - -.. _router_unset-router: -.. describe:: - - Router to modify (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: router * diff --git a/doc/source/cli/command-objects/security-group-rule.rst b/doc/source/cli/command-objects/security-group-rule.rst index 5a2d8342b3..429bcf270b 100644 --- a/doc/source/cli/command-objects/security-group-rule.rst +++ b/doc/source/cli/command-objects/security-group-rule.rst @@ -7,194 +7,5 @@ and other resources on the network. Compute v2, Network v2 -security group rule create --------------------------- - -Create a new security group rule - -.. program:: security group rule create -.. code:: bash - - openstack security group rule create - [--remote-ip | --remote-group ] - [--dst-port | [--icmp-type [--icmp-code ]]] - [--protocol ] - [--ingress | --egress] - [--ethertype ] - [--project [--project-domain ]] - [--description ] - - -.. option:: --remote-ip - - Remote IP address block (may use CIDR notation; - default for IPv4 rule: 0.0.0.0/0, - default for IPv6 rule: ::/0) - -.. option:: --remote-group - - Remote security group (name or ID) - -.. option:: --dst-port - - Destination port, may be a single port or a starting and - ending port range: 137:139. Required for IP protocols TCP - and UDP. Ignored for ICMP IP protocols. - -.. option:: --icmp-type - - ICMP type for ICMP IP protocols - - *Network version 2 only* - -.. option:: --icmp-code - - ICMP code for ICMP IP protocols - - *Network version 2 only* - -.. option:: --protocol - - IP protocol (icmp, tcp, udp; default: tcp) - - *Compute version 2* - - IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, - ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, - ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, - udp, udplite, vrrp and integer representations [0-255] - or any; default: any (all protocols)) - - *Network version 2* - -.. option:: --ingress - - Rule applies to incoming network traffic (default) - - *Network version 2 only* - -.. option:: --egress - - Rule applies to outgoing network traffic - - *Network version 2 only* - -.. option:: --ethertype - - Ethertype of network traffic - (IPv4, IPv6; default: based on IP protocol) - - *Network version 2 only* - -.. option:: --project - - Owner's project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --description - - Set security group rule description - - *Network version 2 only* - -.. describe:: - - Create rule in this security group (name or ID) - -security group rule delete --------------------------- - -Delete security group rule(s) - -.. program:: security group rule delete -.. code:: bash - - openstack security group rule delete - [ ...] - -.. describe:: - - Security group rule(s) to delete (ID only) - -security group rule list ------------------------- - -List security group rules - -.. program:: security group rule list -.. code:: bash - - openstack security group rule list - [--all-projects] - [--protocol ] - [--ethertype ] - [--ingress | --egress] - [--long] - [] - -.. option:: --all-projects - - Display information from all projects (admin only) - - *Network version 2 ignores this option and will always display information* - *for all projects (admin only).* - -.. option:: --long - - List additional fields in output - - *Compute version 2 does not have additional fields to display.* - -.. option:: --protocol - - List rules by the IP protocol (ah, dhcp, egp, esp, gre, icmp, igmp, - ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt,ipv6-opts, ipv6-route, - ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp and integer - representations [0-255] or any; default: any (all protocols)) - - *Network version 2* - -.. option:: --ethertype - - List rules by the Ethertype (IPv4 or IPv6) - - *Network version 2* - -.. option:: --ingress - - List rules applied to incoming network traffic - - *Network version 2 only* - -.. option:: --egress - - List rules applied to outgoing network traffic - - *Network version 2 only* - -.. describe:: - - List all rules in this security group (name or ID) - -security group rule show ------------------------- - -Display security group rule details - -.. program:: security group rule show -.. code:: bash - - openstack security group rule show - - -.. describe:: - - Security group rule to display (ID only) +.. autoprogram-cliff:: openstack.network.v2 + :command: security group rule * diff --git a/doc/source/cli/command-objects/security-group.rst b/doc/source/cli/command-objects/security-group.rst index 403e5fc08f..4edc199547 100644 --- a/doc/source/cli/command-objects/security-group.rst +++ b/doc/source/cli/command-objects/security-group.rst @@ -8,197 +8,23 @@ which specify the network access rules. Compute v2, Network v2 -security group create ---------------------- +.. NOTE(efried): have to list these out one by one; 'security group *' pulls in + ... rule *. -Create a new security group +.. autoprogram-cliff:: openstack.network.v2 + :command: security group create -.. program:: security group create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: security group delete - openstack security group create - [--description ] - [--project [--project-domain ]] - [--tag | --no-tag] - +.. autoprogram-cliff:: openstack.network.v2 + :command: security group list -.. option:: --description +.. autoprogram-cliff:: openstack.network.v2 + :command: security group set - Security group description +.. autoprogram-cliff:: openstack.network.v2 + :command: security group show -.. option:: --project - - Owner's project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --tag - - Tag to be added to the security group (repeat option to set multiple tags) - - *Network version 2 only* - -.. option:: --no-tag - - No tags associated with the security group - - *Network version 2 only* - -.. describe:: - - New security group name - -security group delete ---------------------- - -Delete security group(s) - -.. program:: security group delete -.. code:: bash - - openstack security group delete - [ ...] - -.. describe:: - - Security group(s) to delete (name or ID) - -security group list -------------------- - -List security groups - -.. program:: security group list -.. code:: bash - - openstack security group list - [--all-projects] - [--project [--project-domain ]] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --all-projects - - Display information from all projects (admin only) - - *Network version 2 ignores this option and will always display information* - *for all projects (admin only).* - -.. option:: --project - - List security groups according to the project (name or ID) - - *Network version 2 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Network version 2 only* - -.. option:: --tags [,,...] - - List security groups which have all given tag(s) - - *Network version 2 only* - -.. option:: --any-tags [,,...] - - List security groups which have any given tag(s) - - *Network version 2 only* - -.. option:: --not-tags [,,...] - - Exclude security groups which have all given tag(s) - - *Network version 2 only* - -.. option:: --not-any-tags [,,...] - - Exclude security groups which have any given tag(s) - - *Network version 2 only* - -security group set ------------------- - -Set security group properties - -.. program:: security group set -.. code:: bash - - openstack security group set - [--name ] - [--description ] - [--tag ] [--no-tag] - - -.. option:: --name - - New security group name - -.. option:: --description - - New security group description - -.. option:: --tag - - Tag to be added to the security group (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the security group. Specify both --tag - and --no-tag to overwrite current tags - -.. describe:: - - Security group to modify (name or ID) - -security group show -------------------- - -Display security group details - -.. program:: security group show -.. code:: bash - - openstack security group show - - -.. describe:: - - Security group to display (name or ID) - -security group unset --------------------- - -Unset security group properties - -.. program:: security group unset -.. code:: bash - - openstack security group unset - [--tag | --all-tag] - - -.. option:: --tag - - Tag to be removed from the security group - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the security group - -.. describe:: - - Security group to modify (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: security group unset diff --git a/doc/source/cli/command-objects/subnet-pool.rst b/doc/source/cli/command-objects/subnet-pool.rst index 77259a83fd..ce9649dc94 100644 --- a/doc/source/cli/command-objects/subnet-pool.rst +++ b/doc/source/cli/command-objects/subnet-pool.rst @@ -7,304 +7,5 @@ that are available for IP address allocation. Network v2 -subnet pool create ------------------- - -Create subnet pool - -.. program:: subnet pool create -.. code:: bash - - openstack subnet pool create - [--default-prefix-length ] - [--min-prefix-length ] - [--max-prefix-length ] - [--description ] - [--project [--project-domain ]] - [--address-scope ] - [--default | --no-default] - [--share | --no-share] - [--default-quota ] - [--tag | --no-tag] - --pool-prefix [...] - - -.. option:: --default-prefix-length - - Set subnet pool default prefix length - -.. option:: --min-prefix-length - - Set subnet pool minimum prefix length - -.. option:: --max-prefix-length - - Set subnet pool maximum prefix length - -.. option:: --description - - Set subnet pool description - -.. option:: --project - - Owner's project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). This can be used in case - collisions between project names exist. - -.. option:: --address-scope - - Set address scope associated with the subnet pool (name or ID), - prefixes must be unique across address scopes - -.. option:: --default - - Set this as a default subnet pool - -.. option:: --no-default - - Set this as a non-default subnet pool - -.. option:: --share - - Set this subnet pool as shared - -.. option:: --no-share - - Set this subnet pool as not shared - -.. option:: --default-quota - - Set default per-project quota for this subnet pool as the number of - IP addresses that can be allocated from the subnet pool - -.. option:: --tag - - Tag to be added to the subnet pool (repeat option to set multiple tags) - -.. option:: --no-tag - - No tags associated with the subnet pool - -.. option:: --pool-prefix - - Set subnet pool prefixes (in CIDR notation) - (repeat option to set multiple prefixes) - -.. _subnet_pool_create-name: -.. describe:: - - Name of the new subnet pool - -subnet pool delete ------------------- - -Delete subnet pool(s) - -.. program:: subnet pool delete -.. code:: bash - - openstack subnet pool delete - [ ...] - -.. _subnet_pool_delete-subnet-pool: -.. describe:: - - Subnet pool(s) to delete (name or ID) - -subnet pool list ----------------- - -List subnet pools - -.. program:: subnet pool list -.. code:: bash - - openstack subnet pool list - [--long] - [--share | --no-share] - [--default | --no-default] - [--project [--project-domain ]] - [--name ] - [--address-scope ] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --long - - List additional fields in output - -.. option:: --share - - List subnet pools shared between projects - -.. option:: --no-share - - List subnet pools not shared between projects - -.. option:: --default - - List subnet pools used as the default external subnet pool - -.. option:: --no-default - - List subnet pools not used as the default external subnet pool - -.. option:: --project - - List subnet pools according to their project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --name - - List only subnet pools of given name in output - -.. option:: --address-scope - - List only subnet pools of given address scope in output (name or ID) - -.. option:: --tags [,,...] - - List subnet pools which have all given tag(s) - -.. option:: --any-tags [,,...] - - List subnet pools which have any given tag(s) - -.. option:: --not-tags [,,...] - - Exclude subnet pools which have all given tag(s) - -.. option:: --not-any-tags [,,...] - - Exclude subnet pools which have any given tag(s) - -subnet pool set ---------------- - -Set subnet pool properties - -.. program:: subnet pool set -.. code:: bash - - openstack subnet pool set - [--name ] - [--pool-prefix [...]] - [--default-prefix-length ] - [--min-prefix-length ] - [--max-prefix-length ] - [--address-scope | --no-address-scope] - [--default | --no-default] - [--description ] - [--default-quota ] - [--tag ] [--no-tag] - - -.. option:: --name - - Set subnet pool name - -.. option:: --pool-prefix - - Set subnet pool prefixes (in CIDR notation) - (repeat option to set multiple prefixes) - -.. option:: --default-prefix-length - - Set subnet pool default prefix length - -.. option:: --min-prefix-length - - Set subnet pool minimum prefix length - -.. option:: --max-prefix-length - - Set subnet pool maximum prefix length - -.. option:: --address-scope - - Set address scope associated with the subnet pool (name or ID), - prefixes must be unique across address scopes - -.. option:: --no-address-scope - - Remove address scope associated with the subnet pool - -.. option:: --default - - Set this as a default subnet pool - -.. option:: --no-default - - Set this as a non-default subnet pool - -.. option:: --description - - Set subnet pool description - -.. option:: --default-quota - - Set default per-project quota for this subnet pool as the number of - IP addresses that can be allocated from the subnet pool - -.. option:: --tag - - Tag to be added to the subnet pool (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the subnet pool. Specify both --tag - and --no-tag to overwrite current tags - -.. _subnet_pool_set-subnet-pool: -.. describe:: - - Subnet pool to modify (name or ID) - -subnet pool show ----------------- - -Display subnet pool details - -.. program:: subnet pool show -.. code:: bash - - openstack subnet pool show - - -.. _subnet_pool_show-subnet-pool: -.. describe:: - - Subnet pool to display (name or ID) - -subnet pool unset ------------------ - -Unset subnet pool properties - -.. program:: subnet pool unset -.. code:: bash - - openstack subnet pool unset - [--tag | --all-tag] - - -.. option:: --tag - - Tag to be removed from the subnet pool - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the subnet pool - -.. _subnet_pool_unset-subnet-pool: -.. describe:: - - Subnet pool to modify (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet pool * diff --git a/doc/source/cli/command-objects/subnet.rst b/doc/source/cli/command-objects/subnet.rst index 73e656d895..488fc5a2f5 100644 --- a/doc/source/cli/command-objects/subnet.rst +++ b/doc/source/cli/command-objects/subnet.rst @@ -8,427 +8,23 @@ network. Network v2 -subnet create -------------- +.. NOTE(efried): have to list these out one by one; 'subnet *' pulls in + subnet pool *. -Create new subnet +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet create -.. program:: subnet create -.. code:: bash +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet delete - openstack subnet create - [--project [--project-domain ]] - [--subnet-pool | --use-default-subnet-pool [--prefix-length ] | --use-prefix-delegation] - [--subnet-range ] - [--allocation-pool start=,end=] - [--dhcp | --no-dhcp] - [--dns-nameserver ] - [--gateway ] - [--host-route destination=,gateway=] - [--ip-version {4,6}] - [--description ] - [--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] - [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] - [--network-segment ] - [--service-type ] - [--tag | --no-tag] - --network - +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet list -.. option:: --project +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet set - Owner's project (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet show -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --subnet-pool - - Subnet pool from which this subnet will obtain a CIDR (name or ID) - -.. option:: --use-prefix-delegation - - Use 'prefix-delegation' if IP is IPv6 format and IP would be delegated - externally - -.. option:: --use-default-subnet-pool - - Use default subnet pool for :option:`--ip-version` - -.. option:: --prefix-length - - Prefix length for subnet allocation from subnet pool - -.. option:: --subnet-range - - Subnet range in CIDR notation - (required if :option:`--subnet-pool` is not specified, optional otherwise) - -.. option:: --allocation-pool start=,end= - - Allocation pool IP addresses for this subnet e.g.: - ``start=192.168.199.2,end=192.168.199.254`` - (repeat option to add multiple IP addresses) - -.. option:: --dhcp - - Enable DHCP (default) - -.. option:: --no-dhcp - - Disable DHCP - -.. option:: --dns-nameserver - - DNS server for this subnet (repeat option to set multiple DNS servers) - -.. option:: --gateway - - Specify a gateway for the subnet. The three options are: - : Specific IP address to use as the gateway, - 'auto': Gateway address should automatically be chosen from - within the subnet itself, 'none': This subnet will not use - a gateway, e.g.: ``--gateway 192.168.9.1``, ``--gateway auto``, - ``--gateway none`` (default is 'auto'). - -.. option:: --host-route destination=,gateway= - - Additional route for this subnet e.g.: - ``destination=10.10.0.0/16,gateway=192.168.71.254`` - destination: destination subnet (in CIDR notation) - gateway: nexthop IP address - (repeat option to add multiple routes) - -.. option:: --ip-version {4,6} - - IP version (default is 4). Note that when subnet pool is specified, - IP version is determined from the subnet pool and this option - is ignored. - -.. option:: --description - - Set subnet description - -.. option:: --ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac} - - IPv6 RA (Router Advertisement) mode, - valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac] - -.. option:: --ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac} - - IPv6 address mode, valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac] - -.. option:: --network-segment - - Network segment to associate with this subnet (name or ID) - -.. option:: --service-type - - Service type for this subnet e.g.: - ``network:floatingip_agent_gateway``. - Must be a valid device owner value for a network port - (repeat option to set multiple service types) - -.. option:: --tag - - Tag to be added to the subnet (repeat option to set multiple tags) - -.. option:: --no-tag - - No tags associated with the subnet - -.. option:: --network - - Network this subnet belongs to (name or ID) - -.. _subnet_create-name: -.. describe:: - - Name of subnet to create - -subnet delete -------------- - -Delete subnet(s) - -.. program:: subnet delete -.. code:: bash - - openstack subnet delete - [ ...] - -.. _subnet_delete-subnet: -.. describe:: - - Subnet(s) to delete (name or ID) - -subnet list ------------ - -List subnets - -.. program:: subnet list -.. code:: bash - - openstack subnet list - [--long] - [--ip-version {4,6}] - [--dhcp | --no-dhcp] - [--project [--project-domain ]] - [--network ] - [--gateway ] - [--name ] - [--subnet-range ] - [--tags [,,...]] [--any-tags [,,...]] - [--not-tags [,,...]] [--not-any-tags [,,...]] - -.. option:: --long - - List additional fields in output - -.. option:: --ip-version {4, 6} - - List only subnets of given IP version in output. - Allowed values for IP version are 4 and 6. - -.. option:: --dhcp - - List subnets which have DHCP enabled - -.. option:: --no-dhcp - - List subnets which have DHCP disabled - -.. option:: --service-type - - List only subnets of a given service type in output - e.g.: ``network:floatingip_agent_gateway``. - Must be a valid device owner value for a network port - (repeat option to list multiple service types) - -.. option:: --project - - List only subnets which belong to a given project in output (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --network - - List only subnets which belong to a given network in output (name or ID) - -.. option:: --gateway - - List only subnets of given gateway IP in output - -.. option:: --name - - List only subnets of given name in output - -.. option:: --subnet-range - - List only subnets of given subnet range (in CIDR notation) in output - e.g.: ``--subnet-range 10.10.0.0/16`` - -.. option:: --tags [,,...] - - List subnets which have all given tag(s) - -.. option:: --any-tags [,,...] - - List subnets which have any given tag(s) - -.. option:: --not-tags [,,...] - - Exclude subnets which have all given tag(s) - -.. option:: --not-any-tags [,,...] - - Exclude subnets which have any given tag(s) - -subnet set ----------- - -Set subnet properties - -.. program:: subnet set -.. code:: bash - - openstack subnet set - [--allocation-pool start=,end=] - [--no-allocation-pool] - [--dhcp | --no-dhcp] - [--dns-nameserver ] - [--no-dns-nameserver] - [--gateway ] - [--network-segment ] - [--host-route destination=,gateway=] - [--no-host-route] - [--service-type ] - [--name ] - [--description ] - [--tag ] [--no-tag] - - -.. option:: --allocation-pool start=,end= - - Allocation pool IP addresses for this subnet e.g.: - ``start=192.168.199.2,end=192.168.199.254`` - (repeat option to add multiple IP addresses) - -.. option:: --no-allocation-pool - - Clear associated allocation pools from this subnet. - Specify both :option:`--allocation-pool` and :option:`--no-allocation-pool` - to overwrite the current allocation pool information. - -.. option:: --dhcp - - Enable DHCP - -.. option:: --no-dhcp - - Disable DHCP - -.. option:: --dns-nameserver - - DNS server for this subnet (repeat option to set multiple DNS servers) - -.. option:: --no-dns-nameservers - - Clear existing information of DNS servers. - Specify both :option:`--dns-nameserver` and :option:`--no-dns-nameservers` - to overwrite the current DNS server information. - -.. option:: --gateway - - Specify a gateway for the subnet. The options are: - : Specific IP address to use as the gateway, - 'none': This subnet will not use a gateway, - e.g.: ``--gateway 192.168.9.1``, ``--gateway none``. - -.. option:: --network-segment - - Network segment to associate with this subnet (name or ID). It is only - allowed to set the segment if the current value is `None`, the network - must also have only one segment and only one subnet can exist on the - network. - -.. option:: --host-route destination=,gateway= - - Additional route for this subnet e.g.: - ``destination=10.10.0.0/16,gateway=192.168.71.254`` - destination: destination subnet (in CIDR notation) - gateway: nexthop IP address - -.. option:: --no-host-route - - Clear associated host routes from this subnet. - Specify both :option:`--host-route` and :option:`--no-host-route` - to overwrite the current host route information. - -.. option:: --service-type - - Service type for this subnet e.g.: - ``network:floatingip_agent_gateway``. - Must be a valid device owner value for a network port - (repeat option to set multiple service types) - -.. option:: --description - - Set subnet description - -.. option:: --name - - Updated name of the subnet - -.. option:: --tag - - Tag to be added to the subnet (repeat option to set multiple tags) - -.. option:: --no-tag - - Clear tags associated with the subnet. Specify both --tag - and --no-tag to overwrite current tags - -.. _subnet_set-subnet: -.. describe:: - - Subnet to modify (name or ID) - - -subnet show ------------ - -Display subnet details - -.. program:: subnet show -.. code:: bash - - openstack subnet show - - -.. _subnet_show-subnet: -.. describe:: - - Subnet to display (name or ID) - -subnet unset ------------- - -Unset subnet properties - -.. program:: subnet unset -.. code:: bash - - openstack subnet unset - [--allocation-pool start=,end= [...]] - [--dns-nameserver [...]] - [--host-route destination=,gateway= [...]] - [--service-type ] - [--tag | --all-tag] - - -.. option:: --dns-nameserver - - DNS server to be removed from this subnet - (repeat option to unset multiple DNS servers) - -.. option:: --allocation-pool start=,end= - - Allocation pool IP addresses to be removed from this - subnet e.g.: ``start=192.168.199.2,end=192.168.199.254`` - (repeat option to unset multiple allocation pools) - -.. option:: --host-route destination=,gateway= - - Route to be removed from this subnet e.g.: - ``destination=10.10.0.0/16,gateway=192.168.71.254`` - destination: destination subnet (in CIDR notation) - gateway: nexthop IP address - (repeat option to unset multiple host routes) - -.. option:: --service-type - - Service type to be removed from this subnet e.g.: - ``network:floatingip_agent_gateway``. - Must be a valid device owner value for a network port - (repeat option to unset multiple service types) - -.. option:: --tag - - Tag to be removed from the subnet - (repeat option to remove multiple tags) - -.. option:: --all-tag - - Clear all tags associated with the subnet - -.. _subnet_unset-subnet: -.. describe:: - - Subnet to modify (name or ID) +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet unset diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index d125d2851d..7be2a17b72 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -233,13 +233,13 @@ def add_group_domain_option_to_parser(parser): ) -def add_project_domain_option_to_parser(parser): +def add_project_domain_option_to_parser(parser, enhance_help=lambda _h: _h): parser.add_argument( '--project-domain', metavar='', - help=_('Domain the project belongs to (name or ID). ' - 'This can be used in case collisions between project names ' - 'exist.'), + help=enhance_help(_('Domain the project belongs to (name or ID). This ' + 'can be used in case collisions between project ' + 'names exist.')), ) diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index d22b2caa3c..e68628b3f1 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -34,6 +34,10 @@ 'security_groups': 'security-groups', } +_NET_TYPE_NEUTRON = 'neutron' +_NET_TYPE_COMPUTE = 'nova-network' +_QUALIFIER_FMT = "%s\n\n*%s*" + @contextlib.contextmanager def check_missing_extension_if_error(client_manager, attrs): @@ -51,35 +55,87 @@ def check_missing_extension_if_error(client_manager, attrs): @six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeCommand(command.Command): - """Network and Compute Command +class NetDetectionMixin(object): + """Convenience methods for nova-network vs. neutron decisions. - Command class for commands that support implementation via - the network or compute endpoint. Such commands have different - implementations for take_action() and may even have different - arguments. - """ + A live environment detects which network type it is running and creates its + parser with only the options relevant to that network type. - def take_action(self, parsed_args): - if self.app.client_manager.is_network_endpoint_enabled(): - return self.take_action_network(self.app.client_manager.network, - parsed_args) - else: - return self.take_action_compute(self.app.client_manager.compute, - parsed_args) + But the command classes are used for docs builds as well, and docs must + present the options for both network types, often qualified accordingly. + """ + @property + def _network_type(self): + """Discover whether the running cloud is using neutron or nova-network. + + :return: + * ``NET_TYPE_NEUTRON`` if neutron is detected + * ``NET_TYPE_COMPUTE`` if running in a cloud but neutron is not + detected. + * ``None`` if not running in a cloud, which hopefully means we're + building docs. + """ + # Have we set it up yet for this command? + if not hasattr(self, '_net_type'): + # import pdb; pdb.set_trace() + try: + if self.app.client_manager.is_network_endpoint_enabled(): + net_type = _NET_TYPE_NEUTRON + else: + net_type = _NET_TYPE_COMPUTE + except AttributeError: + LOG.warning( + "%s: Could not detect a network type. Assuming we are " + "building docs.", self.__class__.__name__) + net_type = None + self._net_type = net_type + return self._net_type + + @property + def is_neutron(self): + return self._network_type is _NET_TYPE_NEUTRON + + @property + def is_nova_network(self): + return self._network_type is _NET_TYPE_COMPUTE + + @property + def is_docs_build(self): + return self._network_type is None + + def enhance_help_neutron(self, _help): + if self.is_docs_build: + # Why can't we say 'neutron'? + return _QUALIFIER_FMT % (_help, _("Network version 2 only")) + return _help + + def enhance_help_nova_network(self, _help): + if self.is_docs_build: + # Why can't we say 'nova-network'? + return _QUALIFIER_FMT % (_help, _("Compute version 2 only")) + return _help + + @staticmethod + def split_help(network_help, compute_help): + return ( + "*%(network_qualifier)s:*\n %(network_help)s\n\n" + "*%(compute_qualifier)s:*\n %(compute_help)s" % dict( + network_qualifier=_("Network version 2"), + network_help=network_help, + compute_qualifier=_("Compute version 2"), + compute_help=compute_help)) def get_parser(self, prog_name): LOG.debug('get_parser(%s)', prog_name) - parser = super(NetworkAndComputeCommand, self).get_parser(prog_name) + parser = super(NetDetectionMixin, self).get_parser(prog_name) parser = self.update_parser_common(parser) LOG.debug('common parser: %s', parser) - if ( - self.app is None or - self.app.client_manager.is_network_endpoint_enabled() - ): - return self.update_parser_network(parser) - else: - return self.update_parser_compute(parser) + if self.is_neutron or self.is_docs_build: + parser = self.update_parser_network(parser) + if self.is_nova_network or self.is_docs_build: + # Add nova-net options if running nova-network or building docs + parser = self.update_parser_compute(parser) + return parser def update_parser_common(self, parser): """Default is no updates to parser.""" @@ -93,17 +149,35 @@ def update_parser_compute(self, parser): """Default is no updates to parser.""" return parser - @abc.abstractmethod + def take_action(self, parsed_args): + if self.is_neutron: + return self.take_action_network(self.app.client_manager.network, + parsed_args) + elif self.is_nova_network: + return self.take_action_compute(self.app.client_manager.compute, + parsed_args) + def take_action_network(self, client, parsed_args): """Override to do something useful.""" pass - @abc.abstractmethod def take_action_compute(self, client, parsed_args): """Override to do something useful.""" pass +@six.add_metaclass(abc.ABCMeta) +class NetworkAndComputeCommand(NetDetectionMixin, command.Command): + """Network and Compute Command + + Command class for commands that support implementation via + the network or compute endpoint. Such commands have different + implementations for take_action() and may even have different + arguments. + """ + pass + + @six.add_metaclass(abc.ABCMeta) class NetworkAndComputeDelete(NetworkAndComputeCommand): """Network and Compute Delete @@ -149,7 +223,7 @@ def take_action(self, parsed_args): @six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeLister(command.Lister): +class NetworkAndComputeLister(NetDetectionMixin, command.Lister): """Network and Compute Lister Lister class for commands that support implementation via @@ -157,50 +231,11 @@ class NetworkAndComputeLister(command.Lister): implementations for take_action() and may even have different arguments. """ - - def take_action(self, parsed_args): - if self.app.client_manager.is_network_endpoint_enabled(): - return self.take_action_network(self.app.client_manager.network, - parsed_args) - else: - return self.take_action_compute(self.app.client_manager.compute, - parsed_args) - - def get_parser(self, prog_name): - LOG.debug('get_parser(%s)', prog_name) - parser = super(NetworkAndComputeLister, self).get_parser(prog_name) - parser = self.update_parser_common(parser) - LOG.debug('common parser: %s', parser) - if self.app.client_manager.is_network_endpoint_enabled(): - return self.update_parser_network(parser) - else: - return self.update_parser_compute(parser) - - def update_parser_common(self, parser): - """Default is no updates to parser.""" - return parser - - def update_parser_network(self, parser): - """Default is no updates to parser.""" - return parser - - def update_parser_compute(self, parser): - """Default is no updates to parser.""" - return parser - - @abc.abstractmethod - def take_action_network(self, client, parsed_args): - """Override to do something useful.""" - pass - - @abc.abstractmethod - def take_action_compute(self, client, parsed_args): - """Override to do something useful.""" - pass + pass @six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeShowOne(command.ShowOne): +class NetworkAndComputeShowOne(NetDetectionMixin, command.ShowOne): """Network and Compute ShowOne ShowOne class for commands that support implementation via @@ -222,35 +257,3 @@ def take_action(self, parsed_args): if exc.details: msg += ", " + six.text_type(exc.details) raise exceptions.CommandError(msg) - - def get_parser(self, prog_name): - LOG.debug('get_parser(%s)', prog_name) - parser = super(NetworkAndComputeShowOne, self).get_parser(prog_name) - parser = self.update_parser_common(parser) - LOG.debug('common parser: %s', parser) - if self.app.client_manager.is_network_endpoint_enabled(): - return self.update_parser_network(parser) - else: - return self.update_parser_compute(parser) - - def update_parser_common(self, parser): - """Default is no updates to parser.""" - return parser - - def update_parser_network(self, parser): - """Default is no updates to parser.""" - return parser - - def update_parser_compute(self, parser): - """Default is no updates to parser.""" - return parser - - @abc.abstractmethod - def take_action_network(self, client, parsed_args): - """Override to do something useful.""" - pass - - @abc.abstractmethod - def take_action_compute(self, client, parsed_args): - """Override to do something useful.""" - pass diff --git a/openstackclient/network/v2/_tag.py b/openstackclient/network/v2/_tag.py index ce39f35ec0..e1cba22ea9 100644 --- a/openstackclient/network/v2/_tag.py +++ b/openstackclient/network/v2/_tag.py @@ -22,34 +22,39 @@ def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, values.split(',')) -def add_tag_filtering_option_to_parser(parser, collection_name): +def add_tag_filtering_option_to_parser(parser, collection_name, + enhance_help=lambda _h: _h): parser.add_argument( '--tags', metavar='[,,...]', action=_CommaListAction, - help=_('List %s which have all given tag(s) ' - '(Comma-separated list of tags)') % collection_name + help=enhance_help( + _('List %s which have all given tag(s) (Comma-separated list of ' + 'tags)') % collection_name) ) parser.add_argument( '--any-tags', metavar='[,,...]', action=_CommaListAction, - help=_('List %s which have any given tag(s) ' - '(Comma-separated list of tags)') % collection_name + help=enhance_help( + _('List %s which have any given tag(s) (Comma-separated list of ' + 'tags)') % collection_name) ) parser.add_argument( '--not-tags', metavar='[,,...]', action=_CommaListAction, - help=_('Exclude %s which have all given tag(s) ' - '(Comma-separated list of tags)') % collection_name + help=enhance_help( + _('Exclude %s which have all given tag(s) (Comma-separated list ' + 'of tags)') % collection_name) ) parser.add_argument( '--not-any-tags', metavar='[,,...]', action=_CommaListAction, - help=_('Exclude %s which have any given tag(s) ' - '(Comma-separated list of tags)') % collection_name + help=enhance_help( + _('Exclude %s which have any given tag(s) (Comma-separated list ' + 'of tags)') % collection_name) ) @@ -64,37 +69,42 @@ def get_tag_filtering_args(parsed_args, args): args['not_any_tags'] = ','.join(parsed_args.not_any_tags) -def add_tag_option_to_parser_for_create(parser, resource_name): +def add_tag_option_to_parser_for_create(parser, resource_name, + enhance_help=lambda _h: _h): tag_group = parser.add_mutually_exclusive_group() tag_group.add_argument( '--tag', action='append', dest='tags', metavar='', - help=_("Tag to be added to the %s " - "(repeat option to set multiple tags)") % resource_name + help=enhance_help( + _("Tag to be added to the %s " + "(repeat option to set multiple tags)") % resource_name) ) tag_group.add_argument( '--no-tag', action='store_true', - help=_("No tags associated with the %s") % resource_name + help=enhance_help(_("No tags associated with the %s") % resource_name) ) -def add_tag_option_to_parser_for_set(parser, resource_name): +def add_tag_option_to_parser_for_set(parser, resource_name, + enhance_help=lambda _h: _h): parser.add_argument( '--tag', action='append', dest='tags', metavar='', - help=_("Tag to be added to the %s " - "(repeat option to set multiple tags)") % resource_name + help=enhance_help( + _("Tag to be added to the %s (repeat option to set multiple " + "tags)") % resource_name) ) parser.add_argument( '--no-tag', action='store_true', - help=_("Clear tags associated with the %s. Specify both " - "--tag and --no-tag to overwrite current tags") % resource_name + help=enhance_help( + _("Clear tags associated with the %s. Specify both --tag and " + "--no-tag to overwrite current tags") % resource_name) ) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index e0aa0435ed..bd43379aff 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -114,57 +114,65 @@ def update_parser_network(self, parser): parser.add_argument( '--subnet', metavar='', - help=_("Subnet on which you want to create the floating IP " - "(name or ID)") + help=self.enhance_help_neutron( + _("Subnet on which you want to create the floating IP " + "(name or ID)")) ) parser.add_argument( '--port', metavar='', - help=_("Port to be associated with the floating IP " - "(name or ID)") + help=self.enhance_help_neutron( + _("Port to be associated with the floating IP " + "(name or ID)")) ) parser.add_argument( '--floating-ip-address', metavar='', dest='floating_ip_address', - help=_("Floating IP address") + help=self.enhance_help_neutron(_("Floating IP address")) ) parser.add_argument( '--fixed-ip-address', metavar='', dest='fixed_ip_address', - help=_("Fixed IP address mapped to the floating IP") + help=self.enhance_help_neutron( + _("Fixed IP address mapped to the floating IP")) ) parser.add_argument( '--qos-policy', metavar='', - help=_("Attach QoS policy to the floating IP (name or ID)") + help=self.enhance_help_neutron( + _("Attach QoS policy to the floating IP (name or ID)")) ) parser.add_argument( '--description', metavar='', - help=_('Set floating IP description') + help=self.enhance_help_neutron(_('Set floating IP description')) ) parser.add_argument( '--project', metavar='', - help=_("Owner's project (name or ID)") + help=self.enhance_help_neutron(_("Owner's project (name or ID)")) ) parser.add_argument( '--dns-domain', metavar='', dest='dns_domain', - help=_("Set DNS domain for this floating IP") + help=self.enhance_help_neutron( + _("Set DNS domain for this floating IP")) ) parser.add_argument( '--dns-name', metavar='', dest='dns_name', - help=_("Set DNS name for this floating IP") + help=self.enhance_help_neutron( + _("Set DNS name for this floating IP")) ) - identity_common.add_project_domain_option_to_parser(parser) - _tag.add_tag_option_to_parser_for_create(parser, _('floating IP')) + identity_common.add_project_domain_option_to_parser( + parser, enhance_help=self.enhance_help_neutron) + _tag.add_tag_option_to_parser_for_create( + parser, _('floating IP'), enhance_help=self.enhance_help_neutron) return parser def take_action_network(self, client, parsed_args): @@ -217,60 +225,68 @@ def take_action_compute(self, client, parsed_args): class ListFloatingIP(common.NetworkAndComputeLister): # TODO(songminglong): Use SDK resource mapped attribute names once # the OSC minimum requirements include SDK 1.0 + _description = _("List floating IP(s)") def update_parser_network(self, parser): parser.add_argument( '--network', metavar='', - help=_("List floating IP(s) according to " - "given network (name or ID)") + help=self.enhance_help_neutron( + _("List floating IP(s) according to " + "given network (name or ID)")) ) parser.add_argument( '--port', metavar='', - help=_("List floating IP(s) according to " - "given port (name or ID)") + help=self.enhance_help_neutron( + _("List floating IP(s) according to given port (name or ID)")) ) parser.add_argument( '--fixed-ip-address', metavar='', - help=_("List floating IP(s) according to " - "given fixed IP address") + help=self.enhance_help_neutron( + _("List floating IP(s) according to given fixed IP address")) ) parser.add_argument( '--floating-ip-address', metavar='', - help=_("List floating IP(s) according to " - "given floating IP address") + help=self.enhance_help_neutron( + _("List floating IP(s) according to given floating IP " + "address")) ) parser.add_argument( '--long', action='store_true', default=False, - help=_("List additional fields in output") + help=self.enhance_help_neutron( + _("List additional fields in output")) ) parser.add_argument( '--status', metavar='', choices=['ACTIVE', 'DOWN'], - help=_("List floating IP(s) according to " - "given status ('ACTIVE', 'DOWN')") + help=self.enhance_help_neutron( + _("List floating IP(s) according to given status ('ACTIVE', " + "'DOWN')")) ) parser.add_argument( '--project', metavar='', - help=_("List floating IP(s) according to " - "given project (name or ID)") + help=self.enhance_help_neutron( + _("List floating IP(s) according to given project (name or " + "ID)")) ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--router', metavar='', - help=_("List floating IP(s) according to " - "given router (name or ID)") + help=self.enhance_help_neutron( + _("List floating IP(s) according to given router (name or " + "ID)")) ) - _tag.add_tag_filtering_option_to_parser(parser, _('floating IP')) + _tag.add_tag_filtering_option_to_parser( + parser, _('floating IP'), enhance_help=self.enhance_help_neutron) return parser diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 09f3556c43..e7031266ec 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -219,27 +219,27 @@ def update_parser_network(self, parser): '--enable', action='store_true', default=True, - help=_("Enable network (default)") + help=self.enhance_help_neutron(_("Enable network (default)")) ) admin_group.add_argument( '--disable', action='store_true', - help=_("Disable network") + help=self.enhance_help_neutron(_("Disable network")) ) parser.add_argument( '--project', metavar='', - help=_("Owner's project (name or ID)") + help=self.enhance_help_neutron(_("Owner's project (name or ID)")) ) parser.add_argument( '--description', metavar='', - help=_("Set network description") + help=self.enhance_help_neutron(_("Set network description")) ) parser.add_argument( '--mtu', metavar='', - help=_("Set network mtu") + help=self.enhance_help_neutron(_("Set network mtu")) ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( @@ -247,65 +247,76 @@ def update_parser_network(self, parser): action='append', dest='availability_zone_hints', metavar='', - help=_("Availability Zone in which to create this network " - "(Network Availability Zone extension required, " - "repeat option to set multiple availability zones)") + help=self.enhance_help_neutron( + _("Availability Zone in which to create this network " + "(Network Availability Zone extension required, " + "repeat option to set multiple availability zones)")) ) port_security_group = parser.add_mutually_exclusive_group() port_security_group.add_argument( '--enable-port-security', action='store_true', - help=_("Enable port security by default for ports created on " - "this network (default)") + help=self.enhance_help_neutron( + _("Enable port security by default for ports created on " + "this network (default)")) ) port_security_group.add_argument( '--disable-port-security', action='store_true', - help=_("Disable port security by default for ports created on " - "this network") + help=self.enhance_help_neutron( + _("Disable port security by default for ports created on " + "this network")) ) external_router_grp = parser.add_mutually_exclusive_group() external_router_grp.add_argument( '--external', action='store_true', - help=_("Set this network as an external network " - "(external-net extension required)") + help=self.enhance_help_neutron( + _("Set this network as an external network " + "(external-net extension required)")) ) external_router_grp.add_argument( '--internal', action='store_true', - help=_("Set this network as an internal network (default)") + help=self.enhance_help_neutron( + _("Set this network as an internal network (default)")) ) default_router_grp = parser.add_mutually_exclusive_group() default_router_grp.add_argument( '--default', action='store_true', - help=_("Specify if this network should be used as " - "the default external network") + help=self.enhance_help_neutron( + _("Specify if this network should be used as the default " + "external network")) ) default_router_grp.add_argument( '--no-default', action='store_true', - help=_("Do not use the network as the default external network " - "(default)") + help=self.enhance_help_neutron( + _("Do not use the network as the default external network " + "(default)")) ) parser.add_argument( '--qos-policy', metavar='', - help=_("QoS policy to attach to this network (name or ID)") + help=self.enhance_help_neutron( + _("QoS policy to attach to this network (name or ID)")) ) vlan_transparent_grp = parser.add_mutually_exclusive_group() vlan_transparent_grp.add_argument( '--transparent-vlan', action='store_true', - help=_("Make the network VLAN transparent")) + help=self.enhance_help_neutron( + _("Make the network VLAN transparent"))) vlan_transparent_grp.add_argument( '--no-transparent-vlan', action='store_true', - help=_("Do not make the network VLAN transparent")) + help=self.enhance_help_neutron( + _("Do not make the network VLAN transparent"))) _add_additional_network_options(parser) - _tag.add_tag_option_to_parser_for_create(parser, _('network')) + _tag.add_tag_option_to_parser_for_create( + parser, _('network'), enhance_help=self.enhance_help_neutron) return parser def update_parser_compute(self, parser): @@ -313,7 +324,8 @@ def update_parser_compute(self, parser): '--subnet', metavar='', required=True, - help=_("IPv4 subnet for fixed IPs (in CIDR notation)") + help=self.enhance_help_nova_network( + _("IPv4 subnet for fixed IPs (in CIDR notation)")) ) return parser @@ -376,87 +388,98 @@ def update_parser_network(self, parser): router_ext_group.add_argument( '--external', action='store_true', - help=_("List external networks") + help=self.enhance_help_neutron(_("List external networks")) ) router_ext_group.add_argument( '--internal', action='store_true', - help=_("List internal networks") + help=self.enhance_help_neutron(_("List internal networks")) ) parser.add_argument( '--long', action='store_true', - help=_("List additional fields in output") + help=self.enhance_help_neutron( + _("List additional fields in output")) ) parser.add_argument( '--name', metavar='', - help=_("List networks according to their name") + help=self.enhance_help_neutron( + _("List networks according to their name")) ) admin_state_group = parser.add_mutually_exclusive_group() admin_state_group.add_argument( '--enable', action='store_true', - help=_("List enabled networks") + help=self.enhance_help_neutron(_("List enabled networks")) ) admin_state_group.add_argument( '--disable', action='store_true', - help=_("List disabled networks") + help=self.enhance_help_neutron(_("List disabled networks")) ) parser.add_argument( '--project', metavar='', help=_("List networks according to their project (name or ID)") ) - identity_common.add_project_domain_option_to_parser(parser) + identity_common.add_project_domain_option_to_parser( + parser, enhance_help=self.enhance_help_neutron) shared_group = parser.add_mutually_exclusive_group() shared_group.add_argument( '--share', action='store_true', - help=_("List networks shared between projects") + help=self.enhance_help_neutron( + _("List networks shared between projects")) ) shared_group.add_argument( '--no-share', action='store_true', - help=_("List networks not shared between projects") + help=self.enhance_help_neutron( + _("List networks not shared between projects")) ) parser.add_argument( '--status', metavar='', choices=['ACTIVE', 'BUILD', 'DOWN', 'ERROR'], - help=_("List networks according to their status " - "('ACTIVE', 'BUILD', 'DOWN', 'ERROR')") + help=self.enhance_help_neutron( + _("List networks according to their status " + "('ACTIVE', 'BUILD', 'DOWN', 'ERROR')")) ) parser.add_argument( '--provider-network-type', metavar='', choices=['flat', 'geneve', 'gre', 'local', 'vlan', 'vxlan'], - help=_("List networks according to their physical mechanisms. " - "The supported options are: flat, geneve, gre, local, " - "vlan, vxlan.") + help=self.enhance_help_neutron( + _("List networks according to their physical mechanisms. The " + "supported options are: flat, geneve, gre, local, vlan, " + "vxlan.")) ) parser.add_argument( '--provider-physical-network', metavar='', dest='physical_network', - help=_("List networks according to name of the physical network") + help=self.enhance_help_neutron( + _("List networks according to name of the physical network")) ) parser.add_argument( '--provider-segment', metavar='', dest='segmentation_id', - help=_("List networks according to VLAN ID for VLAN networks " - "or Tunnel ID for GENEVE/GRE/VXLAN networks") + help=self.enhance_help_neutron( + _("List networks according to VLAN ID for VLAN networks or " + "Tunnel ID for GENEVE/GRE/VXLAN networks")) ) parser.add_argument( '--agent', metavar='', dest='agent_id', - help=_('List networks hosted by agent (ID only)') + help=self.enhance_help_neutron( + _('List networks hosted by agent (ID only)')) ) - _tag.add_tag_filtering_option_to_parser(parser, _('networks')) + _tag.add_tag_filtering_option_to_parser( + parser, _('networks'), enhance_help=self.enhance_help_neutron) return parser def take_action_network(self, client, parsed_args): diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 38b4e97a80..9f0ca0a1f4 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -119,10 +119,13 @@ def update_parser_network(self, parser): parser.add_argument( '--project', metavar='', - help=_("Owner's project (name or ID)") + help=self.enhance_help_neutron(_("Owner's project (name or ID)")) ) - identity_common.add_project_domain_option_to_parser(parser) - _tag.add_tag_option_to_parser_for_create(parser, _('security group')) + identity_common.add_project_domain_option_to_parser( + parser, enhance_help=self.enhance_help_neutron) + _tag.add_tag_option_to_parser_for_create( + parser, _('security group'), + enhance_help=self.enhance_help_neutron) return parser def _get_description(self, parsed_args): @@ -202,22 +205,28 @@ class ListSecurityGroup(common.NetworkAndComputeLister): _description = _("List security groups") def update_parser_network(self, parser): - # Maintain and hide the argument for backwards compatibility. - # Network will always return all projects for an admin. - parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=argparse.SUPPRESS, - ) + if not self.is_docs_build: + # Maintain and hide the argument for backwards compatibility. + # Network will always return all projects for an admin. + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=argparse.SUPPRESS, + ) + parser.add_argument( '--project', metavar='', - help=_("List security groups according to the project " - "(name or ID)") + help=self.enhance_help_neutron( + _("List security groups according to the project (name or " + "ID)")) ) - identity_common.add_project_domain_option_to_parser(parser) - _tag.add_tag_filtering_option_to_parser(parser, _('security group')) + identity_common.add_project_domain_option_to_parser( + parser, enhance_help=self.enhance_help_neutron) + _tag.add_tag_filtering_option_to_parser( + parser, _('security group'), + enhance_help=self.enhance_help_neutron) return parser def update_parser_compute(self, parser): @@ -225,7 +234,8 @@ def update_parser_compute(self, parser): '--all-projects', action='store_true', default=False, - help=_("Display information from all projects (admin only)") + help=self.enhance_help_nova_network( + _("Display information from all projects (admin only)")) ) return parser @@ -307,7 +317,9 @@ def update_parser_common(self, parser): return parser def update_parser_network(self, parser): - _tag.add_tag_option_to_parser_for_set(parser, _('security group')) + _tag.add_tag_option_to_parser_for_set( + parser, _('security group'), + enhance_help=self.enhance_help_neutron) return parser def take_action_network(self, client, parsed_args): diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 15f099b1eb..a38587fa52 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -133,109 +133,120 @@ def update_parser_common(self, parser): metavar="", help=_("Remote security group (name or ID)"), ) - return parser - def update_parser_network(self, parser): - parser.add_argument( - '--description', - metavar='', - help=_("Set security group rule description") - ) + # NOTE(efried): The --dst-port, --protocol, and --proto options exist + # for both nova-network and neutron, but differ slightly. For the sake + # of the docs build, which has to account for both variants, but only + # add each to the parser once, they are handled here rather than in the + # _network- or _compute-specific methods below. + + # --dst-port has a default for nova-net only + if self.is_nova_network: + dst_port_default = dict(default=(0, 0)) + else: + dst_port_default = {} parser.add_argument( '--dst-port', metavar='', action=parseractions.RangeAction, help=_("Destination port, may be a single port or a starting and " "ending port range: 137:139. Required for IP protocols TCP " - "and UDP. Ignored for ICMP IP protocols.") - ) - parser.add_argument( - '--icmp-type', - metavar='', - type=int, - help=_("ICMP type for ICMP IP protocols") - ) - parser.add_argument( - '--icmp-code', - metavar='', - type=int, - help=_("ICMP code for ICMP IP protocols") + "and UDP. Ignored for ICMP IP protocols."), + **dst_port_default ) + # NOTE(rtheis): Support either protocol option name for now. # However, consider deprecating and then removing --proto in # a future release. protocol_group = parser.add_mutually_exclusive_group() + # --proto[col] has choices for nova-network only + if self.is_nova_network: + proto_choices = dict(choices=['icmp', 'tcp', 'udp']) + else: + proto_choices = {} + protocol_help_compute = _("IP protocol (icmp, tcp, udp; default: tcp)") + protocol_help_network = _( + "IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, ipv6-encap, " + "ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, ipv6-route, ospf, " + "pgm, rsvp, sctp, tcp, udp, udplite, vrrp and integer " + "representations [0-255] or any; default: any (all protocols))") + if self.is_nova_network: + protocol_help = protocol_help_compute + elif self.is_neutron: + protocol_help = protocol_help_network + else: + # Docs build: compose help for both nova-network and neutron + protocol_help = self.split_help( + protocol_help_network, protocol_help_compute) + protocol_group.add_argument( '--protocol', metavar='', type=_convert_to_lowercase, - help=_("IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, " - "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " - "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " - "udp, udplite, vrrp and integer representations [0-255] " - "or any; default: any (all protocols))") + help=protocol_help, + **proto_choices ) - protocol_group.add_argument( - '--proto', - metavar='', - type=_convert_to_lowercase, - help=argparse.SUPPRESS + if not self.is_docs_build: + protocol_group.add_argument( + '--proto', + metavar='', + type=_convert_to_lowercase, + help=argparse.SUPPRESS, + **proto_choices + ) + + return parser + + def update_parser_network(self, parser): + parser.add_argument( + '--description', + metavar='', + help=self.enhance_help_neutron( + _("Set security group rule description")) + ) + parser.add_argument( + '--icmp-type', + metavar='', + type=int, + help=self.enhance_help_neutron( + _("ICMP type for ICMP IP protocols")) + ) + parser.add_argument( + '--icmp-code', + metavar='', + type=int, + help=self.enhance_help_neutron( + _("ICMP code for ICMP IP protocols")) ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( '--ingress', action='store_true', - help=_("Rule applies to incoming network traffic (default)") + help=self.enhance_help_neutron( + _("Rule applies to incoming network traffic (default)")) ) direction_group.add_argument( '--egress', action='store_true', - help=_("Rule applies to outgoing network traffic") + help=self.enhance_help_neutron( + _("Rule applies to outgoing network traffic")) ) parser.add_argument( '--ethertype', metavar='', choices=['IPv4', 'IPv6'], type=_convert_ipvx_case, - help=_("Ethertype of network traffic " - "(IPv4, IPv6; default: based on IP protocol)") + help=self.enhance_help_neutron( + _("Ethertype of network traffic " + "(IPv4, IPv6; default: based on IP protocol)")) ) parser.add_argument( '--project', metavar='', - help=_("Owner's project (name or ID)") - ) - identity_common.add_project_domain_option_to_parser(parser) - return parser - - def update_parser_compute(self, parser): - parser.add_argument( - '--dst-port', - metavar='', - default=(0, 0), - action=parseractions.RangeAction, - help=_("Destination port, may be a single port or a starting and " - "ending port range: 137:139. Required for IP protocols TCP " - "and UDP. Ignored for ICMP IP protocols.") - ) - # NOTE(rtheis): Support either protocol option name for now. - # However, consider deprecating and then removing --proto in - # a future release. - protocol_group = parser.add_mutually_exclusive_group() - protocol_group.add_argument( - '--protocol', - metavar='', - choices=['icmp', 'tcp', 'udp'], - type=_convert_to_lowercase, - help=_("IP protocol (icmp, tcp, udp; default: tcp)") - ) - protocol_group.add_argument( - '--proto', - metavar='', - choices=['icmp', 'tcp', 'udp'], - type=_convert_to_lowercase, - help=argparse.SUPPRESS + help=self.enhance_help_neutron(_("Owner's project (name or ID)")) ) + identity_common.add_project_domain_option_to_parser( + parser, enhance_help=self.enhance_help_neutron) return parser def _get_protocol(self, parsed_args, default_protocol='any'): @@ -424,47 +435,53 @@ def update_parser_common(self, parser): return parser def update_parser_network(self, parser): - # Accept but hide the argument for consistency with compute. - # Network will always return all projects for an admin. - parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=argparse.SUPPRESS - ) + if not self.is_docs_build: + # Accept but hide the argument for consistency with compute. + # Network will always return all projects for an admin. + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=argparse.SUPPRESS + ) + parser.add_argument( '--protocol', metavar='', type=_convert_to_lowercase, - help=_("List rules by the IP protocol (" - "ah, dhcp, egp, esp, gre, icmp, igmp, " - "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " - "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " - "udp, udplite, vrrp and integer representations [0-255] " - "or any; default: any (all protocols))") + help=self.enhance_help_neutron( + _("List rules by the IP protocol (ah, dhcp, egp, esp, gre, " + "icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " + "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, " + "udplite, vrrp and integer representations [0-255] or any; " + "default: any (all protocols))")) ) parser.add_argument( '--ethertype', metavar='', type=_convert_to_lowercase, - help=_("List rules by the Ethertype (IPv4 or IPv6)") + help=self.enhance_help_neutron( + _("List rules by the Ethertype (IPv4 or IPv6)")) ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( '--ingress', action='store_true', - help=_("List rules applied to incoming network traffic") + help=self.enhance_help_neutron( + _("List rules applied to incoming network traffic")) ) direction_group.add_argument( '--egress', action='store_true', - help=_("List rules applied to outgoing network traffic") + help=self.enhance_help_neutron( + _("List rules applied to outgoing network traffic")) ) parser.add_argument( '--long', action='store_true', default=False, - help=_("List additional fields in output") + help=self.enhance_help_neutron( + _("List additional fields in output")) ) return parser @@ -473,16 +490,18 @@ def update_parser_compute(self, parser): '--all-projects', action='store_true', default=False, - help=_("Display information from all projects (admin only)") - ) - # Accept but hide the argument for consistency with network. - # There are no additional fields to display at this time. - parser.add_argument( - '--long', - action='store_false', - default=False, - help=argparse.SUPPRESS + help=self.enhance_help_nova_network( + _("Display information from all projects (admin only)")) ) + if not self.is_docs_build: + # Accept but hide the argument for consistency with network. + # There are no additional fields to display at this time. + parser.add_argument( + '--long', + action='store_false', + default=False, + help=argparse.SUPPRESS + ) return parser def _get_column_headers(self, parsed_args): From c7dbe857055a04402f12dfaa3b261b0ecbf86d7c Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Fri, 1 Nov 2019 14:08:56 -0500 Subject: [PATCH 0157/1120] Update a stale doc reference to use :neutron-doc: The help page for network auto allocated topology had a link to a newton-era networking guide document that has been superseded and is now maintained in the neutron repository. This commit adds 'neutron' to the openstackdocstheme configuration so that the :neutron-doc: role works, and updates the link to point to the modern version therein. Change-Id: I5bcb40e265b22f15ff2f5ca4936160e231bb4075 --- .../cli/command-objects/network-auto-allocated-topology.rst | 4 ++-- doc/source/conf.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/command-objects/network-auto-allocated-topology.rst b/doc/source/cli/command-objects/network-auto-allocated-topology.rst index 00e4c6b9bc..436836484a 100644 --- a/doc/source/cli/command-objects/network-auto-allocated-topology.rst +++ b/doc/source/cli/command-objects/network-auto-allocated-topology.rst @@ -5,8 +5,8 @@ network auto allocated topology An **auto allocated topology** allows admins to quickly set up external connectivity for end-users. Only one auto allocated topology is allowed per project. For more information on how to set up the resources required -for auto allocated topology review the documentation at: -http://docs.openstack.org/newton/networking-guide/config-auto-allocation.html +for auto allocated topology review :neutron-doc:`the documentation +`. Network v2 diff --git a/doc/source/conf.py b/doc/source/conf.py index d3dfccd235..ff37783fb5 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -35,7 +35,9 @@ use_storyboard = True # Add project 'foo' to this list to enable the :foo-doc: role -# openstack_projects = [] +openstack_projects = [ + 'neutron', +] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] From 4c0f3bfa89dfc9f207c4f6d6dc6ded85b86fee87 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 29 Oct 2019 16:22:18 -0500 Subject: [PATCH 0158/1120] common: autogenerate docs $namespace = openstack.common The subcommand documents for $namespace were hardcoded and thus prone to drift over time. This commit removes the hardcoded content and uses the autoprogram-cliff directive to generate them automatically from the subcommand configuration classes. This incorporates a correction to `openstack versions show`: The command `openstack versions show --help` showed a copy/paste error, using for the metavar for both --service and --status. Fix. Change-Id: I7658fed40d71f4c20ee27908ade433534657cfe5 Co-Authored-By: Pierre Prinetti Co-Authored-By: Matt Riedemann --- .../cli/command-objects/availability-zone.rst | 31 +- .../cli/command-objects/configuration.rst | 23 +- doc/source/cli/command-objects/extension.rst | 52 +--- doc/source/cli/command-objects/limits.rst | 34 +-- .../cli/command-objects/project-purge.rst | 35 +-- doc/source/cli/command-objects/quota.rst | 271 +----------------- doc/source/cli/command-objects/versions.rst | 47 +-- openstackclient/common/quota.py | 42 ++- openstackclient/common/versions.py | 15 +- 9 files changed, 56 insertions(+), 494 deletions(-) diff --git a/doc/source/cli/command-objects/availability-zone.rst b/doc/source/cli/command-objects/availability-zone.rst index d4c117a0f8..bdc64f1537 100644 --- a/doc/source/cli/command-objects/availability-zone.rst +++ b/doc/source/cli/command-objects/availability-zone.rst @@ -7,32 +7,5 @@ compute and network services. Block Storage v2, Compute v2, Network v2 -availability zone list ----------------------- - -List availability zones and their status - -.. program availability zone list -.. code:: bash - - openstack availability zone list - [--compute] - [--network] - [--volume] - [--long] - -.. option:: --compute - - List compute availability zones - -.. option:: --network - - List network availability zones - -.. option:: --volume - - List volume availability zones - -.. option:: --long - - List additional fields in output +.. autoprogram-cliff:: openstack.common + :command: availability zone list diff --git a/doc/source/cli/command-objects/configuration.rst b/doc/source/cli/command-objects/configuration.rst index 6e704d2d25..22fd13e43d 100644 --- a/doc/source/cli/command-objects/configuration.rst +++ b/doc/source/cli/command-objects/configuration.rst @@ -6,24 +6,5 @@ Available for all services .. _configuration-show: -configuration show ------------------- - -Show the current openstack client configuration. This command is a little -different from other show commands because it does not take a resource name -or id to show. The command line options, such as --os-cloud, can be used to -show different configurations. - -.. program:: configuration show -.. code:: bash - - openstack configuration show - [--mask | --unmask] - -.. option:: --mask - - Attempt to mask passwords (default) - -.. option:: --unmask - - Show password in clear text +.. autoprogram-cliff:: openstack.common + :command: configuration show diff --git a/doc/source/cli/command-objects/extension.rst b/doc/source/cli/command-objects/extension.rst index 36cf418bb8..1002a5fff0 100644 --- a/doc/source/cli/command-objects/extension.rst +++ b/doc/source/cli/command-objects/extension.rst @@ -5,54 +5,6 @@ extension Many OpenStack server APIs include API extensions that enable additional functionality. -extension list --------------- -List API extensions - -.. program:: extension list -.. code:: bash - - openstack extension list - [--compute] - [--identity] - [--network] - [--volume] - [--long] - -.. option:: --compute - - List extensions for the Compute API - -.. option:: --identity - - List extensions for the Identity API - -.. option:: --network - - List extensions for the Network API - -.. option:: --volume - - List extensions for the Block Storage API - -.. option:: --long - - List additional fields in output - -extension show --------------- - -Show API extension - -.. program:: extension show -.. code:: bash - - openstack extension show - - -.. _extension_show: -.. describe:: - - Extension to display. Currently, only network extensions are supported. - (Name or Alias) +.. autoprogram-cliff:: openstack.common + :command: extension * diff --git a/doc/source/cli/command-objects/limits.rst b/doc/source/cli/command-objects/limits.rst index 9261420951..3a0f99b376 100644 --- a/doc/source/cli/command-objects/limits.rst +++ b/doc/source/cli/command-objects/limits.rst @@ -6,36 +6,6 @@ The Compute and Block Storage APIs have resource usage limits. Compute v2, Block Storage v1 -limits show ------------ -Show compute and block storage limits - -.. program:: limits show -.. code:: bash - - openstack limits show - --absolute | --rate - [--reserved] - [--project ] - [--domain ] - -.. option:: --absolute - - Show absolute limits - -.. option:: --rate - - Show rate limits - -.. option:: --reserved - - Include reservations count [only valid with :option:`--absolute`] - -.. option:: --project - - Show limits for a specific project (name or ID) [only valid with :option:`--absolute`] - -.. option:: --domain - - Domain the project belongs to (name or ID) [only valid with :option:`--absolute`] +.. autoprogram-cliff:: openstack.common + :command: limits * diff --git a/doc/source/cli/command-objects/project-purge.rst b/doc/source/cli/command-objects/project-purge.rst index 0ad0bbf969..8f10a77452 100644 --- a/doc/source/cli/command-objects/project-purge.rst +++ b/doc/source/cli/command-objects/project-purge.rst @@ -6,37 +6,6 @@ Clean resources associated with a specific project. Block Storage v1, v2; Compute v2; Image v1, v2 -project purge -------------- - -Clean resources associated with a project - -.. program:: project purge -.. code:: bash - - openstack project purge - [--dry-run] - [--keep-project] - [--auth-project | --project ] - [--project-domain ] - -.. option:: --dry-run - - List a project's resources - -.. option:: --keep-project - - Clean project resources, but don't delete the project. - -.. option:: --auth-project - - Delete resources of the project used to authenticate - -.. option:: --project - - Project to clean (name or ID) - -.. option:: --project-domain - Domain the project belongs to (name or ID). This can be - used in case collisions between project names exist. +.. autoprogram-cliff:: openstack.common + :command: project purge diff --git a/doc/source/cli/command-objects/quota.rst b/doc/source/cli/command-objects/quota.rst index 25d0188a3d..cab1265240 100644 --- a/doc/source/cli/command-objects/quota.rst +++ b/doc/source/cli/command-objects/quota.rst @@ -7,272 +7,5 @@ single object with multiple properties. Block Storage v1, v2, Compute v2, Network v2 -quota list ----------- - -List quotas for all projects with non-default quota values - -.. program:: quota list -.. code:: bash - - openstack quota list - --compute | --network | --volume - [--project ] - [--detail] - -.. option:: --network - - List network quotas - -.. option:: --compute - - List compute quotas - -.. option:: --volume - - List volume quotas - -.. option:: --project - - List quotas for this project (name or ID) - -.. option:: --detail - - Show details about quotas usage - -quota set ---------- - -Set quotas for project - -.. program:: quota set -.. code:: bash - - openstack quota set - # Compute settings - [--cores ] - [--fixed-ips ] - [--floating-ips ] - [--injected-file-size ] - [--injected-files ] - [--instances ] - [--key-pairs ] - [--properties ] - [--ram ] - [--server-groups ] - [--server-group-members ] - - # Block Storage settings - [--backups ] - [--backup-gigabytes ] - [--gigabytes ] - [--per-volume-gigabytes ] - [--snapshots ] - [--volumes ] - [--volume-type ] - - # Network settings - [--floating-ips ] - [--secgroup-rules ] - [--secgroups ] - [--networks ] - [--subnets ] - [--ports ] - [--routers ] - [--rbac-policies ] - [--subnetpools ] - - - -Set quotas for class - -.. code:: bash - - openstack quota set - --class - # Compute settings - [--cores ] - [--fixed-ips ] - [--floating-ips ] - [--injected-file-size ] - [--injected-files ] - [--instances ] - [--key-pairs ] - [--properties ] - [--ram ] - [--server-groups ] - [--server-group-members ] - - # Block Storage settings - [--backups ] - [--backup-gigabytes ] - [--gigabytes ] - [--per-volume-gigabytes ] - [--snapshots ] - [--volumes ] - - - -.. option:: --class - - Set quotas for ```` - -.. option:: --properties - - New value for the properties quota - -.. option:: --ram - - New value for the ram quota - -.. option:: --secgroup-rules - - New value for the secgroup-rules quota - -.. option:: --instances - - New value for the instances quota - -.. option:: --key-pairs - - New value for the key-pairs quota - -.. option:: --fixed-ips - - New value for the fixed-ips quota - -.. option:: --secgroups - - New value for the secgroups quota - -.. option:: --injected-file-size - - New value for the injected-file-size quota - -.. option:: --server-groups - - New value for the server-groups quota - -.. option:: --server-group-members - - New value for the server-group-members quota - -.. option:: --floating-ips - - New value for the floating-ips quota - -.. option:: --injected-files - - New value for the injected-files quota - -.. option:: --cores - - New value for the cores quota - -.. option:: --injected-path-size - - New value for the injected-path-size quota - -.. option:: --backups - - New value for the backups quota - -.. option:: --backup-gigabytes - - New value for the backup gigabytes quota - -.. option:: --gigabytes - - New value for the gigabytes quota - -.. option:: --per-volume-gigabytes - - New value for the gigabytes quota of per volume - -.. option:: --volumes - - New value for the volumes quota - -.. option:: --snapshots - - New value for the snapshots quota - -.. option:: --volume-type - - Set quotas for a specific . The supported quotas are: - gigabytes, snapshots, volumes. - -.. option:: --networks - - New value for the networks quota - -.. option:: --subnets - - New value for the subnets quota - -.. option:: --ports - - New value for the ports quota - -.. option:: --routers - - New value for the routers quota - -.. option:: --rbac-policies - - New value for the rbac-policies quota - -.. option:: --vips - - New value for the vips quota - -.. option:: --subnetpools - - New value for the subnetpools quota - -.. option:: --members - - New value for the members quota - -.. option:: --health-monitors - - New value for the health-monitors quota - -quota show ----------- - -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. - -.. program:: quota show -.. code:: bash - - openstack quota show - [--default] - [] - - -.. option:: --default - - Show default quotas for :ref:`\ ` - -.. _quota_show-project: -.. describe:: - - Show quotas for this project (name or ID) - -.. code:: bash - - openstack quota show - --class - [] - -.. option:: --class - - Show quotas for :ref:`\ ` - -.. _quota_show-class: -.. describe:: - - Show quotas for this class (name or ID) +.. autoprogram-cliff:: openstack.common + :command: quota * diff --git a/doc/source/cli/command-objects/versions.rst b/doc/source/cli/command-objects/versions.rst index 0aa9a1b785..ebebec192a 100644 --- a/doc/source/cli/command-objects/versions.rst +++ b/doc/source/cli/command-objects/versions.rst @@ -4,48 +4,5 @@ versions Get a list of every version of every service in a given cloud. -versions show -------------- - -Show service versions: - -.. program:: versions show -.. code:: bash - - openstack versions show - [--all-interfaces] - [--interface ] - [--region-name ] - [--service ] - [--status ] - -.. option:: --all-interfaces - - Return results for every interface of every service. - [Mutually exclusive with --interface] - -.. option:: --interface - - Limit results to only those on given interface. - [Default 'public'. Mutually exclusive with --all-interfaces] - -.. option:: --region-name - - Limit results to only those from a given region. - -.. option:: --service - - Limit results to only those for service. The argument should be either - an exact match to what is in the catalog or a known official value or - alias from `service-types-authority`_. - -.. option:: --status - - Limit results to only those in the given state. Valid values are: - - - SUPPORTED - - CURRENT - - DEPRECATED - - EXPERIMENTAL - -.. _service-types-authority: https://service-types.openstack.org/ +.. autoprogram-cliff:: openstack.common + :command: versions show diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index fb4e86031b..80c8749ac1 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -24,6 +24,7 @@ import six from openstackclient.i18n import _ +from openstackclient.network import common LOG = logging.getLogger(__name__) @@ -457,18 +458,37 @@ def take_action(self, parsed_args): return ((), ()) -class SetQuota(command.Command): +class SetQuota(common.NetDetectionMixin, command.Command): _description = _("Set quotas for project or class") def _build_options_list(self): - if self.app.client_manager.is_network_endpoint_enabled(): - return itertools.chain(COMPUTE_QUOTAS.items(), - VOLUME_QUOTAS.items(), - NETWORK_QUOTAS.items()) - else: - return itertools.chain(COMPUTE_QUOTAS.items(), - VOLUME_QUOTAS.items(), - NOVA_NETWORK_QUOTAS.items()) + 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(), + )] + # 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. + if self.is_docs_build: + # NOTE(efried): This takes advantage of the fact that we know the + # nova-net options are a subset of the neutron options. If that + # ever changes, this algorithm will need to be adjusted accordingly + inv_compute = set(NOVA_NETWORK_QUOTAS.values()) + for k, v in NETWORK_QUOTAS.items(): + _help = help_fmt % v + if v not in inv_compute: + # This one is unique to neutron + _help = self.enhance_help_neutron(_help) + rets.append((k, v, _help)) + elif self.is_neutron: + rets.extend( + [(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()]) + return rets def get_parser(self, prog_name): parser = super(SetQuota, self).get_parser(prog_name) @@ -484,13 +504,13 @@ def get_parser(self, prog_name): default=False, help=_('Set quotas for '), ) - for k, v in self._build_options_list(): + for k, v, h in self._build_options_list(): parser.add_argument( '--%s' % v, metavar='<%s>' % v, dest=k, type=int, - help=_('New value for the %s quota') % v, + help=h, ) parser.add_argument( '--volume-type', diff --git a/openstackclient/common/versions.py b/openstackclient/common/versions.py index e6781bc66f..3acd9f73d7 100644 --- a/openstackclient/common/versions.py +++ b/openstackclient/common/versions.py @@ -46,14 +46,21 @@ def get_parser(self, prog_name): parser.add_argument( '--service', metavar='', - help=_('Show versions for a specific service.'), + help=_('Show versions for a specific service. The argument should ' + 'be either an exact match to what is in the catalog or a ' + 'known official value or alias from ' + 'service-types-authority ' + '(https://service-types.openstack.org/)'), ) parser.add_argument( '--status', metavar='', - help=_('Show versions for a specific status.' - ' [Valid values are SUPPORTED, CURRENT,' - ' DEPRECATED, EXPERIMENTAL]'), + help=_("""Show versions for a specific status. Valid values are: + +- SUPPORTED +- CURRENT +- DEPRECATED +- EXPERIMENTAL""") ) return parser From da56b8f4cf15cc5a77249b680dd0c4d05137be9a Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 4 Nov 2019 15:48:27 -0600 Subject: [PATCH 0159/1120] openstack.cli: autogenerate docs $namespace = openstack.cli The subcommand documents for $namespace were hardcoded and thus prone to drift over time. This commit removes the hardcoded content and uses the autoprogram-cliff directive to generate them automatically from the subcommand configuration classes. Change-Id: I1f7e9d0e5748f887dbc35200c3c8b4407da43e0b --- doc/source/cli/command-objects/command.rst | 17 ++--------------- doc/source/cli/command-objects/module.rst | 16 ++-------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/doc/source/cli/command-objects/command.rst b/doc/source/cli/command-objects/command.rst index 918fd959ad..5afc4940ab 100644 --- a/doc/source/cli/command-objects/command.rst +++ b/doc/source/cli/command-objects/command.rst @@ -6,18 +6,5 @@ Internal Installed commands in the OSC process. -command list ------------- - -List recognized commands by group - -.. program:: command list -.. code:: bash - - openstack command list - [--group ] - -.. option:: --group - - Show commands filtered by a command group, for example: identity, volume, - compute, image, network and other keywords +.. autoprogram-cliff:: openstack.cli + :command: command * diff --git a/doc/source/cli/command-objects/module.rst b/doc/source/cli/command-objects/module.rst index f4b32e7589..82269f472d 100644 --- a/doc/source/cli/command-objects/module.rst +++ b/doc/source/cli/command-objects/module.rst @@ -6,17 +6,5 @@ Internal Installed Python modules in the OSC process. -module list ------------ - -List module versions - -.. program:: module list -.. code:: bash - - openstack module list - [--all] - -.. option:: --all - - Show all modules that have version information +.. autoprogram-cliff:: openstack.cli + :command: module * From c6266b5ab22b911ae7e0ca5077d0c04a23dabca9 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 4 Nov 2019 16:02:31 -0600 Subject: [PATCH 0160/1120] compute: autogenerate docs $namespace = openstack.compute.v2 The subcommand documents for $namespace were hardcoded and thus prone to drift over time. This commit removes the hardcoded content and uses the autoprogram-cliff directive to generate them automatically from the subcommand configuration classes. Many of these were already being generated. Some were missing a few sub-subcommands, so those are added. Change-Id: I1aa4b2655bafd2f6a5d83b658742f65d180eb128 --- doc/source/cli/command-objects/aggregate.rst | 179 +------------- .../cli/command-objects/compute-agent.rst | 98 +------- .../cli/command-objects/compute-service.rst | 93 +------ .../cli/command-objects/console-log.rst | 21 +- .../cli/command-objects/console-url.rst | 42 +--- doc/source/cli/command-objects/flavor.rst | 228 +----------------- doc/source/cli/command-objects/host.rst | 66 +---- .../cli/command-objects/hypervisor-stats.rst | 12 +- doc/source/cli/command-objects/hypervisor.rst | 39 +-- doc/source/cli/command-objects/keypair.rst | 71 +----- doc/source/cli/command-objects/server.rst | 4 +- doc/source/cli/command-objects/usage.rst | 49 +--- setup.cfg | 2 +- 13 files changed, 29 insertions(+), 875 deletions(-) diff --git a/doc/source/cli/command-objects/aggregate.rst b/doc/source/cli/command-objects/aggregate.rst index 2029a6c8cf..0f62ce8f43 100644 --- a/doc/source/cli/command-objects/aggregate.rst +++ b/doc/source/cli/command-objects/aggregate.rst @@ -7,180 +7,5 @@ criteria. Compute v2 -aggregate add host ------------------- - -Add host to aggregate - -.. program:: aggregate add host -.. code:: bash - - openstack aggregate add host - - - -.. _aggregate_add_host-aggregate: -.. describe:: - - Aggregate (name or ID) - -.. _aggregate_add_host-host: -.. describe:: - - Host to add to :ref:`\ ` - -aggregate create ----------------- - -Create a new aggregate - -.. program:: aggregate create -.. code:: bash - - openstack aggregate create - [--zone ] - [--property [...] ] - - -.. option:: --zone - - Availability zone name - -.. option:: --property - - Property to add to this aggregate (repeat option to set multiple properties) - -.. _aggregate_create-name: -.. describe:: - - New aggregate name - -aggregate delete ----------------- - -Delete existing aggregate(s) - -.. program:: aggregate delete -.. code:: bash - - openstack aggregate delete - [ ...] - -.. _aggregate_delete-aggregate: -.. describe:: - - Aggregate(s) to delete (name or ID) - -aggregate list --------------- - -List all aggregates - -.. program:: aggregate list -.. code:: bash - - openstack aggregate list - [--long] - -.. option:: --long - - List additional fields in output - -aggregate remove host ---------------------- - -Remove host from aggregate - -.. program:: aggregate remove host -.. code:: bash - - openstack aggregate remove host - - - -.. _aggregate_remove_host-aggregate: -.. describe:: - - Aggregate (name or ID) - -.. _aggregate_remove_host-host: -.. describe:: - - Host to remove from :ref:`\ ` - -aggregate set -------------- - -Set aggregate properties - -.. program:: aggregate set -.. code:: bash - - openstack aggregate set - [--name ] - [--zone ] - [--property [...] ] - [--no-property] - - -.. option:: --name - - Set aggregate name - -.. option:: --zone - - Set availability zone name - -.. option:: --property - - Property to set on :ref:`\ ` - (repeat option to set multiple properties) - -.. option:: --no-property - - Remove all properties from :ref:`\ ` - (specify both :option:`--property` and :option:`--no-property` to - overwrite the current properties) - -.. _aggregate_set-aggregate: -.. describe:: - - Aggregate to modify (name or ID) - -aggregate show --------------- - -Display aggregate details - -.. program:: aggregate show -.. code:: bash - - openstack aggregate show - - -.. _aggregate_show-aggregate: -.. describe:: - - Aggregate to display (name or ID) - -aggregate unset ---------------- - -Unset aggregate properties - -.. program:: aggregate unset -.. code-block:: bash - - openstack aggregate unset - [--property [...] ] - - -.. option:: --property - - Property to remove from :ref:`\ ` - (repeat option to remove multiple properties) - -.. _aggregate_unset-aggregate: -.. describe:: - - Aggregate to modify (name or ID) +.. autoprogram-cliff:: openstack.compute.v2 + :command: aggregate * diff --git a/doc/source/cli/command-objects/compute-agent.rst b/doc/source/cli/command-objects/compute-agent.rst index e8317b4825..89d7492879 100644 --- a/doc/source/cli/command-objects/compute-agent.rst +++ b/doc/source/cli/command-objects/compute-agent.rst @@ -4,99 +4,5 @@ compute agent Compute v2 -compute agent create --------------------- - -Create compute agent - -.. program:: compute agent create -.. code:: bash - - openstack compute agent create - - - -.. _compute_agent-create: -.. describe:: - - Type of OS - -.. describe:: - - Type of architecture - -.. describe:: - - Version - -.. describe:: - - URL - -.. describe:: - - MD5 hash - -.. describe:: - - Type of hypervisor - -compute agent delete --------------------- - -Delete compute agent(s) - -.. program:: compute agent delete -.. code:: bash - - openstack compute agent delete [ ...] - -.. _compute_agent-delete: -.. describe:: - - ID of agent(s) to delete - -compute agent list ------------------- - -List compute agents - -.. program:: compute agent list -.. code:: bash - - openstack compute agent list [--hypervisor ] - -.. option:: --hypervisor - - Type of hypervisor - -compute agent set ------------------ - -Set compute agent properties - -.. program:: agent set -.. code:: bash - - openstack compute agent set - [--agent-version ] - [--url ] - - -.. _compute_agent-set: -.. option:: --agent-version - - Version of the agent - -.. option:: --url - - URL of the agent - -.. option:: --md5hash - - MD5 hash of the agent - -.. describe:: - - Agent to modify (ID only) +.. autoprogram-cliff:: openstack.compute.v2 + :command: compute agent * diff --git a/doc/source/cli/command-objects/compute-service.rst b/doc/source/cli/command-objects/compute-service.rst index a76a9c26b9..ac54786e5b 100644 --- a/doc/source/cli/command-objects/compute-service.rst +++ b/doc/source/cli/command-objects/compute-service.rst @@ -4,94 +4,5 @@ compute service Compute v2 -compute service delete ----------------------- - -Delete compute service(s) - -.. program:: compute service delete -.. code:: bash - - openstack compute service delete - [ ...] - -.. _compute_service_delete-service: -.. describe:: - - Compute service(s) to delete (ID only). If using - ``--os-compute-api-version`` 2.53 or greater, the ID is a UUID which can - be retrieved by listing compute services using the same 2.53+ microversion. - -compute service list --------------------- - -List compute services - -Using ``--os-compute-api-version`` 2.53 or greater will return the ID as a -UUID value which can be used to uniquely identify the service in a multi-cell -deployment. - -.. program:: compute service list -.. code:: bash - - openstack compute service list - [--host ] - [--service ] - [--long] - -.. option:: --host - - List services on specified host (name only) - -.. option:: --service - - List only specified service binaries (name only). For example, - ``nova-compute``, ``nova-conductor``, etc. - -.. option:: --long - - List additional fields in output - -compute service set -------------------- - -Set compute service properties - -.. program:: compute service set -.. code:: bash - - openstack compute service set - [--enable | --disable] - [--disable-reason ] - [--up | --down] - - -.. option:: --enable - - Enable service - -.. option:: --disable - - Disable service - -.. option:: --disable-reason - - Reason for disabling the service (in quotes). Should be used with :option:`--disable` option. - -.. option:: --up - - Force up service. Requires ``--os-compute-api-version`` 2.11 or greater. - -.. option:: --down - - Force down service. . Requires ``--os-compute-api-version`` 2.11 or - greater. - -.. _compute_service_set-host: -.. describe:: - - Name of host - -.. describe:: - - Name of service (Binary name), for example ``nova-compute`` +.. autoprogram-cliff:: openstack.compute.v2 + :command: compute service * diff --git a/doc/source/cli/command-objects/console-log.rst b/doc/source/cli/command-objects/console-log.rst index bcb23e7009..46ef370d84 100644 --- a/doc/source/cli/command-objects/console-log.rst +++ b/doc/source/cli/command-objects/console-log.rst @@ -6,22 +6,5 @@ Server console text dump Compute v2 -console log show ----------------- - -Show server's console output - -.. program:: console log show -.. code:: bash - - openstack console log show - [--lines ] - - -.. option:: --lines - - Number of lines to display from the end of the log (default=all) - -.. describe:: - - Server to show log console log (name or ID) +.. autoprogram-cliff:: openstack.compute.v2 + :command: console log * diff --git a/doc/source/cli/command-objects/console-url.rst b/doc/source/cli/command-objects/console-url.rst index 8a5807b6a3..001ccc56c7 100644 --- a/doc/source/cli/command-objects/console-url.rst +++ b/doc/source/cli/command-objects/console-url.rst @@ -6,43 +6,5 @@ Server remote console URL Compute v2 -console url show ----------------- - -Show server's remote console URL - -.. program:: console url show -.. code:: bash - - openstack console url show - [--novnc | --xvpvnc | --spice] - [--rdp | --serial | --mks] - - -.. option:: --novnc - - Show noVNC console URL (default) - -.. option:: --xvpvnc - - Show xvpvnc console URL - -.. option:: --spice - - Show SPICE console URL - -.. option:: --rdp - - Show RDP console URL - -.. option:: --serial - - Show serial console URL - -.. option:: --mks - - Show WebMKS console URL - -.. describe:: - - Server to show URL (name or ID) +.. autoprogram-cliff:: openstack.compute.v2 + :command: console url * diff --git a/doc/source/cli/command-objects/flavor.rst b/doc/source/cli/command-objects/flavor.rst index f22463b78e..ee09b3ace7 100644 --- a/doc/source/cli/command-objects/flavor.rst +++ b/doc/source/cli/command-objects/flavor.rst @@ -4,229 +4,5 @@ flavor Compute v2 -flavor create -------------- - -Create new flavor - -.. program:: flavor create -.. code:: bash - - openstack flavor create - [--id ] - [--ram ] - [--disk ] - [--ephemeral-disk ] - [--swap ] - [--vcpus ] - [--rxtx-factor ] - [--public | --private] - [--property [...] ] - [--project ] - [--project-domain ] - [--description ] - - -.. option:: --id - - Unique flavor ID; 'auto' creates a UUID (default: auto) - -.. option:: --ram - - Memory size in MB (default 256M) - -.. option:: --disk - - Disk size in GB (default 0G) - -.. option:: --ephemeral-disk - - Ephemeral disk size in GB (default 0G) - -.. option:: --swap - - Additional swap space size in MB (default 0M) - -.. option:: --vcpus - - Number of vcpus (default 1) - -.. option:: --rxtx-factor - - RX/TX factor (default 1.0) - -.. option:: --public - - Flavor is available to other projects (default) - -.. option:: --private - - Flavor is not available to other projects - -.. option:: --property - - Property to add for this flavor (repeat option to set multiple properties) - -.. option:: --project - - Allow to access private flavor (name or ID) - (Must be used with :option:`--private` option) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --description - - Description to add for this flavor. Only available starting with - ``--os-compute-api-version 2.55``. - -.. _flavor_create-flavor-name: -.. describe:: - - New flavor name - -flavor delete -------------- - -Delete flavor(s) - -.. program:: flavor delete -.. code:: bash - - openstack flavor delete - [ ...] - -.. _flavor_delete-flavor: -.. describe:: - - Flavor(s) to delete (name or ID) - -flavor list ------------ - -List flavors - -.. program:: flavor list -.. code:: bash - - openstack flavor list - [--public | --private | --all] - [--long] - [--marker ] - [--limit ] - -.. option:: --public - - List only public flavors (default) - -.. option:: --private - - List only private flavors - -.. option:: --all - - List all flavors, whether public or private - -.. option:: --long - - List additional fields in output - -.. option:: --marker - - The last flavor ID of the previous page - -.. option:: --limit - - Maximum number of flavors to display - -flavor set ----------- - -Set flavor properties - -.. program:: flavor set -.. code:: bash - - openstack flavor set - [--no-property] - [--property [...] ] - [--project ] - [--project-domain ] - [--description ] - - -.. option:: --property - - Property to add or modify for this flavor (repeat option to set multiple properties) - -.. option:: --project - - Set flavor access to project (name or ID) (admin only) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --no-property - - Remove all properties from this flavor (specify both --no-property and --property - to remove the current properties before setting new properties.) - -.. option:: --description - - Description to set for this flavor. Only available starting with - ``--os-compute-api-version 2.55``. - -.. describe:: - - Flavor to modify (name or ID) - -flavor show ------------ - -Display flavor details - -.. program:: flavor show -.. code:: bash - - openstack flavor show - - -.. _flavor_show-flavor: -.. describe:: - - Flavor to display (name or ID) - -flavor unset ------------- - -Unset flavor properties - -.. program:: flavor unset -.. code:: bash - - openstack flavor unset - [--property [...] ] - [--project ] - [--project-domain ] - - -.. option:: --property - - Property to remove from flavor (repeat option to remove multiple properties) - -.. option:: --project - - Remove flavor access from project (name or ID) (admin only) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. describe:: - - Flavor to modify (name or ID) +.. autoprogram-cliff:: openstack.compute.v2 + :command: flavor * diff --git a/doc/source/cli/command-objects/host.rst b/doc/source/cli/command-objects/host.rst index cbf3439827..acd5a287fe 100644 --- a/doc/source/cli/command-objects/host.rst +++ b/doc/source/cli/command-objects/host.rst @@ -6,67 +6,5 @@ Compute v2 The physical computer running a hypervisor. -host list ---------- - -List hosts - -.. program:: host list -.. code:: bash - - openstack host list - [--zone ] - -.. option:: --zone - - Only return hosts in the availability zone - -host set --------- - -Set host properties - -.. program:: host set -.. code:: bash - - openstack host set - [--enable | --disable] - [--enable-maintenance | --disable-maintenance] - - -.. _host-set: -.. option:: --enable - - Enable the host - -.. option:: --disable - - Disable the host - -.. _maintenance-set: -.. option:: --enable-maintenance - - Enable maintenance mode for the host - -.. option:: --disable-maintenance - - Disable maintenance mode for the host - -.. describe:: - - Host to modify (name only) - -host show ---------- - -Display host details - -.. program:: host show -.. code:: bash - - openstack host show - - -.. describe:: - - Name of host +.. autoprogram-cliff:: openstack.compute.v2 + :command: host * diff --git a/doc/source/cli/command-objects/hypervisor-stats.rst b/doc/source/cli/command-objects/hypervisor-stats.rst index 89faf135f4..1f5768f298 100644 --- a/doc/source/cli/command-objects/hypervisor-stats.rst +++ b/doc/source/cli/command-objects/hypervisor-stats.rst @@ -4,13 +4,5 @@ hypervisor stats Compute v2 -hypervisor stats show ---------------------- - -Display hypervisor stats details - -.. program:: hypervisor stats show -.. code:: bash - - openstack hypervisor stats show - +.. autoprogram-cliff:: openstack.compute.v2 + :command: hypervisor stats * diff --git a/doc/source/cli/command-objects/hypervisor.rst b/doc/source/cli/command-objects/hypervisor.rst index 9db384a247..9ae82bf122 100644 --- a/doc/source/cli/command-objects/hypervisor.rst +++ b/doc/source/cli/command-objects/hypervisor.rst @@ -4,38 +4,11 @@ hypervisor Compute v2 -hypervisor list ---------------- +.. NOTE(efried): have to list these out one by one; 'hypervisor *' pulls in + ... stats. -List hypervisors +.. autoprogram-cliff:: openstack.compute.v2 + :command: hypervisor list -.. program:: hypervisor list -.. code:: bash - - openstack hypervisor list - [--matching ] - [--long] - -.. option:: --matching - - Filter hypervisors using substring - -.. option:: --long - - List additional fields in output - -hypervisor show ---------------- - -Display hypervisor details - -.. program:: hypervisor show -.. code:: bash - - openstack hypervisor show - - -.. _hypervisor_show-flavor: -.. describe:: - - Hypervisor to display (name or ID) +.. autoprogram-cliff:: openstack.compute.v2 + :command: hypervisor show diff --git a/doc/source/cli/command-objects/keypair.rst b/doc/source/cli/command-objects/keypair.rst index a539f0a208..f8bce3756b 100644 --- a/doc/source/cli/command-objects/keypair.rst +++ b/doc/source/cli/command-objects/keypair.rst @@ -9,72 +9,5 @@ command. Compute v2 -keypair create --------------- - -Create new public or private key for server ssh access - -.. program:: keypair create -.. code:: bash - - openstack keypair create - [--public-key | --private-key ] - - -.. option:: --public-key - - Filename for public key to add. If not used, creates a private key. - -.. option:: --private-key - - Filename for private key to save. If not used, print private key in - console. - -.. describe:: - - New public or private key name - -keypair delete --------------- - -Delete public or private key(s) - -.. program:: keypair delete -.. code:: bash - - openstack keypair delete - [ ...] - -.. describe:: - - Name of key(s) to delete (name only) - -keypair list ------------- - -List key fingerprints - -.. program:: keypair list -.. code:: bash - - openstack keypair list - -keypair show ------------- - -Display key details - -.. program:: keypair show -.. code:: bash - - openstack keypair show - [--public-key] - - -.. option:: --public-key - - Show only bare public key paired with the generated key - -.. describe:: - - Public or private key to display (name only) +.. autoprogram-cliff:: openstack.compute.v2 + :command: keypair * diff --git a/doc/source/cli/command-objects/server.rst b/doc/source/cli/command-objects/server.rst index a7fef2578c..89eb4e3710 100644 --- a/doc/source/cli/command-objects/server.rst +++ b/doc/source/cli/command-objects/server.rst @@ -23,7 +23,7 @@ Compute v2 :command: server lock .. autoprogram-cliff:: openstack.compute.v2 - :command: server migrate + :command: server migrate* .. autoprogram-cliff:: openstack.compute.v2 :command: server pause @@ -41,7 +41,7 @@ Compute v2 :command: server rescue .. autoprogram-cliff:: openstack.compute.v2 - :command: server resize + :command: server resize* .. autoprogram-cliff:: openstack.compute.v2 :command: server restore diff --git a/doc/source/cli/command-objects/usage.rst b/doc/source/cli/command-objects/usage.rst index bb13176cc8..c2bcde0610 100644 --- a/doc/source/cli/command-objects/usage.rst +++ b/doc/source/cli/command-objects/usage.rst @@ -4,50 +4,5 @@ usage Compute v2 -usage list ----------- - -List resource usage per project - -Compute API v2.40+ returns all matching entities rather than being -limited to the API server configured maximum (``CONF.api.max_limit``). - -.. program:: usage list -.. code:: bash - - openstack usage list - [--start ] - [--end ] - -.. option:: --start - - Usage range start date, ex 2012-01-20 (default: 4 weeks ago) - -.. option:: --end - - Usage range end date, ex 2012-01-20 (default: tomorrow) - -usage show ----------- - -Show resource usage for a single project - -.. program:: usage show -.. code:: bash - - openstack usage show - [--project ] - [--start ] - [--end ] - -.. option:: --project - - Name or ID of project to show usage for - -.. option:: --start - - Usage range start date, ex 2012-01-20 (default: 4 weeks ago) - -.. option:: --end - - Usage range end date, ex 2012-01-20 (default: tomorrow) +.. autoprogram-cliff:: openstack.compute.v2 + :command: usage * diff --git a/setup.cfg b/setup.cfg index d2bee32e0b..2d6422787f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -102,6 +102,7 @@ openstack.compute.v2 = server_add_volume = openstackclient.compute.v2.server:AddServerVolume server_create = openstackclient.compute.v2.server:CreateServer server_delete = openstackclient.compute.v2.server:DeleteServer + server_dump_create = openstackclient.compute.v2.server:CreateServerDump server_list = openstackclient.compute.v2.server:ListServer server_lock = openstackclient.compute.v2.server:LockServer server_migrate = openstackclient.compute.v2.server:MigrateServer @@ -129,7 +130,6 @@ openstack.compute.v2 = server_start = openstackclient.compute.v2.server:StartServer server_stop = openstackclient.compute.v2.server:StopServer server_suspend = openstackclient.compute.v2.server:SuspendServer - server_dump_create = openstackclient.compute.v2.server:CreateServerDump server_unlock = openstackclient.compute.v2.server:UnlockServer server_unpause = openstackclient.compute.v2.server:UnpauseServer server_unrescue = openstackclient.compute.v2.server:UnrescueServer From 6f07828bf044e5eda53037e986d9ae642ee499af Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 4 Nov 2019 17:42:57 -0600 Subject: [PATCH 0161/1120] Add redirect testing Adds a dependency on and invocation of the `whereto` command to validate redirects in the .htaccess file during doc builds. Change-Id: Ib6cc2953f0fd774de3c3a0c8a2bd6cff49667c14 --- doc/requirements.txt | 3 +++ doc/test/redirect-tests.txt | 18 ++++++++++++++++++ tox.ini | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 doc/test/redirect-tests.txt diff --git a/doc/requirements.txt b/doc/requirements.txt index ec692e75a0..8639f9360a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -7,6 +7,9 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.5,<2.0.0;python_version=='2.7' # BSD sphinx!=1.6.6,!=1.6.7,>=1.6.5;python_version>='3.4' # BSD sphinxcontrib-apidoc>=0.2.0 # BSD +# redirect tests in docs +whereto>=0.4.0 # Apache-2.0 + # Install these to generate sphinx autodocs aodhclient>=0.9.0 # Apache-2.0 gnocchiclient>=3.3.1 # Apache-2.0 diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt new file mode 100644 index 0000000000..9473d69225 --- /dev/null +++ b/doc/test/redirect-tests.txt @@ -0,0 +1,18 @@ +/python-openstackclient/latest/command-objects/this-is-a-test.html 301 /python-openstackclient/latest/cli/command-objects/this-is-a-test.html +/python-openstackclient/latest/authentication.html 301 /python-openstackclient/latest/cli/authentication.html +/python-openstackclient/latest/backward-incompatible.html 301 /python-openstackclient/latest/cli/backward-incompatible.html +/python-openstackclient/latest/command-list.html 301 /python-openstackclient/latest/cli/command-list.html +/python-openstackclient/latest/commands.html 301 /python-openstackclient/latest/cli/commands.html +/python-openstackclient/latest/decoder.html 301 /python-openstackclient/latest/cli/decoder.html +/python-openstackclient/latest/interactive.html 301 /python-openstackclient/latest/cli/interactive.html +/python-openstackclient/latest/plugin-commands.html 301 /python-openstackclient/latest/cli/plugin-commands.html +/python-openstackclient/latest/specs/this-is-a-test.html 301 /python-openstackclient/latest/contributor/specs/this-is-a-test.html +/python-openstackclient/latest/command-beta.html 301 /python-openstackclient/latest/contributor/command-beta.html +/python-openstackclient/latest/command-errors.html 301 /python-openstackclient/latest/contributor/command-errors.html +/python-openstackclient/latest/command-logs.html 301 /python-openstackclient/latest/contributor/command-logs.html +/python-openstackclient/latest/command-options.html 301 /python-openstackclient/latest/contributor/command-options.html +/python-openstackclient/latest/command-wrappers.html 301 /python-openstackclient/latest/contributor/command-wrappers.html +/python-openstackclient/latest/developing.html 301 /python-openstackclient/latest/contributor/developing.html +/python-openstackclient/latest/humaninterfaceguide.html 301 /python-openstackclient/latest/contributor/humaninterfaceguide.html +/python-openstackclient/latest/plugins.html 301 /python-openstackclient/latest/contributor/plugins.html +/python-openstackclient/latest/cli/plugin-commands.html 301 /python-openstackclient/latest/cli/plugin-commands/index.html diff --git a/tox.ini b/tox.ini index a973b107f7..addafa2bfe 100644 --- a/tox.ini +++ b/tox.ini @@ -118,6 +118,8 @@ deps = commands = sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html sphinx-build -a -E -W -d doc/build/doctrees -b man doc/source doc/build/man + # Validate redirects (must be done after the docs build + whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:releasenotes] basepython = python3 From e2d8dc0f1c33613c56da37758d73d63b550642c0 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 4 Nov 2019 17:55:40 -0600 Subject: [PATCH 0162/1120] Deflate .htaccess Use some regexes to reduce the number of lines in .htaccess and hopefully make it slightly clearer which groupings of files redirect to which new locations. Also collapse one redundant double-redirect. Change-Id: I65c4960856985d71076291f175df17f27a5ab8cc --- doc/source/_extra/.htaccess | 18 +++--------------- doc/test/redirect-tests.txt | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess index fd91901e87..513deddbdc 100644 --- a/doc/source/_extra/.htaccess +++ b/doc/source/_extra/.htaccess @@ -2,22 +2,10 @@ redirectmatch 301 ^/python-openstackclient/([^/]+)/command-objects/([^/.]+).html$ /python-openstackclient/$1/cli/command-objects/$2.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/authentication.html$ /python-openstackclient/$1/cli/authentication.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/backward-incompatible.html$ /python-openstackclient/$1/cli/backward-incompatible.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/command-list.html$ /python-openstackclient/$1/cli/command-list.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/commands.html$ /python-openstackclient/$1/cli/commands.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/decoder.html$ /python-openstackclient/$1/cli/decoder.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/interactive.html$ /python-openstackclient/$1/cli/interactive.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/plugin-commands.html$ /python-openstackclient/$1/cli/plugin-commands.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/(authentication|backward-incompatible|command-list|commands|decoder|interactive).html$ /python-openstackclient/$1/cli/$2.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/plugin-commands.html$ /python-openstackclient/$1/cli/plugin-commands/index.html redirectmatch 301 ^/python-openstackclient/([^/]+)/specs/([^/.]+).html$ /python-openstackclient/$1/contributor/specs/$2.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/command-beta.html$ /python-openstackclient/$1/contributor/command-beta.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/command-errors.html$ /python-openstackclient/$1/contributor/command-errors.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/command-logs.html$ /python-openstackclient/$1/contributor/command-logs.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/command-options.html$ /python-openstackclient/$1/contributor/command-options.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/command-wrappers.html$ /python-openstackclient/$1/contributor/command-wrappers.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/developing.html$ /python-openstackclient/$1/contributor/developing.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/humaninterfaceguide.html$ /python-openstackclient/$1/contributor/humaninterfaceguide.html -redirectmatch 301 ^/python-openstackclient/([^/]+)/plugins.html$ /python-openstackclient/$1/contributor/plugins.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/(command-(beta|errors|logs|options|wrappers)|developing|humaninterfaceguide|plugins).html$ /python-openstackclient/$1/contributor/$2.html redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/plugin-commands.html$ /python-openstackclient/$1/cli/plugin-commands/index.html diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt index 9473d69225..ef3a48decb 100644 --- a/doc/test/redirect-tests.txt +++ b/doc/test/redirect-tests.txt @@ -5,7 +5,7 @@ /python-openstackclient/latest/commands.html 301 /python-openstackclient/latest/cli/commands.html /python-openstackclient/latest/decoder.html 301 /python-openstackclient/latest/cli/decoder.html /python-openstackclient/latest/interactive.html 301 /python-openstackclient/latest/cli/interactive.html -/python-openstackclient/latest/plugin-commands.html 301 /python-openstackclient/latest/cli/plugin-commands.html +/python-openstackclient/latest/plugin-commands.html 301 /python-openstackclient/latest/cli/plugin-commands/index.html /python-openstackclient/latest/specs/this-is-a-test.html 301 /python-openstackclient/latest/contributor/specs/this-is-a-test.html /python-openstackclient/latest/command-beta.html 301 /python-openstackclient/latest/contributor/command-beta.html /python-openstackclient/latest/command-errors.html 301 /python-openstackclient/latest/contributor/command-errors.html From 3b409e4d0e136380042a59a421ec4c4dc5b95c18 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 5 Nov 2019 13:26:19 -0600 Subject: [PATCH 0163/1120] Refactor AggregateTests While investigating the referenced story/bug I noticed that wait_for_status in openstackclient.tests.functional.compute.v2.test_aggregate.AggregateTests was doing a lot more than it should ever need to (it probably got copied in from somewhere). The two places calling it only need to a) check the output of `openstack aggregate show`, and b) try once -- since they just got done creating the aggregate synchronously, there should never be a need to delay/retry. So this commit removes the helper method and just inlines the check. At the same time, the addCleanup(aggregate delete) directives are moved above their respective creates. This is a defensive best practice which makes sure cleanup happens even if something fails very soon after the actual back-end create (as was in fact the case with the referenced bug/story). It is unknown whether this will impact the referenced bug. Change-Id: I0d7432f13642fbccd5ca79da9c76adfcbabb5fa9 Story: 2006811 Related-Bug: #1851391 --- .../functional/compute/v2/test_aggregate.py | 60 ++++++------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index be318ae5e3..1de5309921 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -11,7 +11,6 @@ # under the License. import json -import time import uuid from openstackclient.tests.functional import base @@ -20,34 +19,14 @@ class AggregateTests(base.TestCase): """Functional tests for aggregate""" - def wait_for_status(self, check_type, check_name, desired_status, - wait=120, interval=5, failures=None): - current_status = "notset" - if failures is None: - failures = ['error'] - total_sleep = 0 - while total_sleep < wait: - output = json.loads(self.openstack( - check_type + ' show -f json ' + check_name)) - current_status = output['name'] - if (current_status == desired_status): - print('{} {} now has status {}' - .format(check_type, check_name, current_status)) - return - print('Checking {} {} Waiting for {} current status: {}' - .format(check_type, check_name, - desired_status, current_status)) - if current_status in failures: - raise Exception( - 'Current status {} of {} {} is one of failures {}' - .format(current_status, check_type, check_name, failures)) - time.sleep(interval) - total_sleep += interval - self.assertOutput(desired_status, current_status) - def test_aggregate_crud(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex + self.addCleanup( + self.openstack, + 'aggregate delete ' + name1, + fail_ok=True, + ) cmd_output = json.loads(self.openstack( 'aggregate create -f json ' + '--zone nova ' + @@ -66,14 +45,16 @@ def test_aggregate_crud(self): 'a', cmd_output['properties'] ) - self.wait_for_status('aggregate', name1, name1) + cmd_output = json.loads(self.openstack( + 'aggregate show -f json ' + name1)) + self.assertEqual(name1, cmd_output['name']) + + name2 = uuid.uuid4().hex self.addCleanup( self.openstack, - 'aggregate delete ' + name1, + 'aggregate delete ' + name2, fail_ok=True, ) - - name2 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'aggregate create -f json ' + '--zone external ' + @@ -87,15 +68,17 @@ def test_aggregate_crud(self): 'external', cmd_output['availability_zone'] ) - self.wait_for_status('aggregate', name2, name2) + cmd_output = json.loads(self.openstack( + 'aggregate show -f json ' + name2)) + self.assertEqual(name2, cmd_output['name']) + + # Test aggregate set + name3 = uuid.uuid4().hex self.addCleanup( self.openstack, - 'aggregate delete ' + name2, + 'aggregate delete ' + name3, fail_ok=True, ) - - # Test aggregate set - name3 = uuid.uuid4().hex raw_output = self.openstack( 'aggregate set ' + '--name ' + name3 + ' ' + @@ -105,11 +88,6 @@ def test_aggregate_crud(self): name1 ) self.assertOutput('', raw_output) - self.addCleanup( - self.openstack, - 'aggregate delete ' + name3, - fail_ok=True, - ) cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + @@ -196,11 +174,11 @@ def test_aggregate_add_and_remove_host(self): self.skipTest("Skip aggregates in a Nova cells v1 configuration") name = uuid.uuid4().hex + self.addCleanup(self.openstack, 'aggregate delete ' + name) self.openstack( 'aggregate create ' + name ) - self.addCleanup(self.openstack, 'aggregate delete ' + name) # Test add host cmd_output = json.loads(self.openstack( From 1c0160c8aa8482958df54e0814023c5eeed0611e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 18 Nov 2019 14:44:49 -0600 Subject: [PATCH 0164/1120] Create Volume v3 functional tests Until now-ish Volume v3 has been a pass-through to v2. In order to prepare to make the Volume v3 commands stand-alone copy the v2 functional tests to v3. This is the first of a series of reviews to completely separate Volume v2 and v3 commands. Once these are split we can begin to implement v3 microversion support and/or start using the OpenStack SDK as the REST library. Change-Id: Iefd78d8ef6bb851d7360596337a88ee8f8476767 Signed-off-by: Dean Troyer --- .../tests/functional/volume/v3/test_qos.py | 204 ++++++++++++- .../volume/v3/test_transfer_request.py | 106 ++++++- .../tests/functional/volume/v3/test_volume.py | 268 +++++++++++++++++- .../volume/v3/test_volume_snapshot.py | 240 +++++++++++++++- .../functional/volume/v3/test_volume_type.py | 224 ++++++++++++++- 5 files changed, 1031 insertions(+), 11 deletions(-) diff --git a/openstackclient/tests/functional/volume/v3/test_qos.py b/openstackclient/tests/functional/volume/v3/test_qos.py index a6290fc531..fdfa682791 100644 --- a/openstackclient/tests/functional/volume/v3/test_qos.py +++ b/openstackclient/tests/functional/volume/v3/test_qos.py @@ -10,9 +10,209 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.volume.v2 import test_qos as v2 +import json +import uuid + from openstackclient.tests.functional.volume.v3 import common -class QosTests(common.BaseVolumeTests, v2.QosTests): +class QosTests(common.BaseVolumeTests): """Functional tests for volume qos. """ + + def test_volume_qos_create_delete_list(self): + """Test create, list, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name1 + )) + self.assertEqual( + name1, + cmd_output['name'] + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name2 + )) + self.assertEqual( + name2, + cmd_output['name'] + ) + + # Test list + cmd_output = json.loads(self.openstack( + 'volume qos list -f json' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test delete multiple + del_output = self.openstack('volume qos delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_volume_qos_set_show_unset(self): + """Tests create volume qos, set, unset, show, delete""" + + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + '--consumer front-end ' + '--property Alpha=a ' + + name + )) + self.addCleanup(self.openstack, 'volume qos delete ' + name) + self.assertEqual( + name, + cmd_output['name'] + ) + + self.assertEqual( + "front-end", + cmd_output['consumer'] + ) + self.assertEqual( + {'Alpha': 'a'}, + cmd_output['properties'] + ) + + # Test volume qos set + raw_output = self.openstack( + 'volume qos set ' + + '--property Alpha=c ' + + '--property Beta=b ' + + name, + ) + self.assertOutput('', raw_output) + + # Test volume qos show + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.assertEqual( + {'Alpha': 'c', 'Beta': 'b'}, + cmd_output['properties'] + ) + + # Test volume qos unset + raw_output = self.openstack( + 'volume qos unset ' + + '--property Alpha ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.assertEqual( + {'Beta': 'b'}, + cmd_output['properties'] + ) + + def test_volume_qos_asso_disasso(self): + """Tests associate and disassociate qos with volume type""" + vol_type1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume type create -f json ' + + vol_type1 + )) + self.assertEqual( + vol_type1, + cmd_output['name'] + ) + self.addCleanup(self.openstack, 'volume type delete ' + vol_type1) + + vol_type2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume type create -f json ' + + vol_type2 + )) + self.assertEqual( + vol_type2, + cmd_output['name'] + ) + self.addCleanup(self.openstack, 'volume type delete ' + vol_type2) + + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.addCleanup(self.openstack, 'volume qos delete ' + name) + + # Test associate + raw_output = self.openstack( + 'volume qos associate ' + + name + ' ' + vol_type1 + ) + self.assertOutput('', raw_output) + raw_output = self.openstack( + 'volume qos associate ' + + name + ' ' + vol_type2 + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + types = cmd_output["associations"] + self.assertIn(vol_type1, types) + self.assertIn(vol_type2, types) + + # Test disassociate + raw_output = self.openstack( + 'volume qos disassociate ' + + '--volume-type ' + vol_type1 + + ' ' + name + ) + self.assertOutput('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + types = cmd_output["associations"] + self.assertNotIn(vol_type1, types) + self.assertIn(vol_type2, types) + + # Test disassociate --all + raw_output = self.openstack( + 'volume qos associate ' + + name + ' ' + vol_type1 + ) + self.assertOutput('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + types = cmd_output["associations"] + self.assertIn(vol_type1, types) + self.assertIn(vol_type2, types) + + raw_output = self.openstack( + 'volume qos disassociate ' + + '--all ' + name + ) + self.assertOutput('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertNotIn("associations", cmd_output.keys()) diff --git a/openstackclient/tests/functional/volume/v3/test_transfer_request.py b/openstackclient/tests/functional/volume/v3/test_transfer_request.py index f16dfafa7a..1bbfedc902 100644 --- a/openstackclient/tests/functional/volume/v3/test_transfer_request.py +++ b/openstackclient/tests/functional/volume/v3/test_transfer_request.py @@ -10,12 +10,112 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.volume.v2 import test_transfer_request \ - as v2 +import json +import uuid + from openstackclient.tests.functional.volume.v3 import common -class TransferRequestTests(common.BaseVolumeTests, v2.TransferRequestTests): +class TransferRequestTests(common.BaseVolumeTests): """Functional tests for transfer request. """ API_VERSION = '3' + + def test_volume_transfer_request_accept(self): + volume_name = uuid.uuid4().hex + xfer_name = uuid.uuid4().hex + + # create a volume + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + self.assertEqual(volume_name, cmd_output['name']) + self.addCleanup( + self.openstack, + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume delete ' + + volume_name + ) + self.wait_for_status("volume", volume_name, "available") + + # create volume transfer request for the volume + # and get the auth_key of the new transfer request + cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request create -f json ' + + ' --name ' + xfer_name + ' ' + + volume_name + )) + self.assertEqual(xfer_name, cmd_output['name']) + xfer_id = cmd_output['id'] + auth_key = cmd_output['auth_key'] + self.assertTrue(auth_key) + self.wait_for_status("volume", volume_name, "awaiting-transfer") + + # accept the volume transfer request + cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request accept -f json ' + + '--auth-key ' + auth_key + ' ' + + xfer_id + )) + self.assertEqual(xfer_name, cmd_output['name']) + self.wait_for_status("volume", volume_name, "available") + + def test_volume_transfer_request_list_show(self): + volume_name = uuid.uuid4().hex + xfer_name = uuid.uuid4().hex + + # create a volume + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + self.assertEqual(volume_name, cmd_output['name']) + self.addCleanup( + self.openstack, + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume delete ' + + volume_name + ) + self.wait_for_status("volume", volume_name, "available") + + cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request create -f json ' + + ' --name ' + xfer_name + ' ' + + volume_name + )) + self.assertEqual(xfer_name, cmd_output['name']) + xfer_id = cmd_output['id'] + auth_key = cmd_output['auth_key'] + self.assertTrue(auth_key) + self.wait_for_status("volume", volume_name, "awaiting-transfer") + + cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request list -f json' + )) + self.assertIn(xfer_name, [req['Name'] for req in cmd_output]) + + cmd_output = json.loads(self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request show -f json ' + + xfer_id + )) + self.assertEqual(xfer_name, cmd_output['name']) + + # NOTE(dtroyer): We need to delete the transfer request to allow the + # volume to be deleted. The addCleanup() route does + # not have a mechanism to wait for the volume status + # to become 'available' before attempting to delete + # the volume. + cmd_output = self.openstack( + '--os-volume-api-version ' + self.API_VERSION + ' ' + + 'volume transfer request delete ' + + xfer_id + ) + self.wait_for_status("volume", volume_name, "available") diff --git a/openstackclient/tests/functional/volume/v3/test_volume.py b/openstackclient/tests/functional/volume/v3/test_volume.py index 283b830f63..6635167dbd 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume.py +++ b/openstackclient/tests/functional/volume/v3/test_volume.py @@ -10,9 +10,273 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.volume.v2 import test_volume as v2 +import json +import uuid + from openstackclient.tests.functional.volume.v3 import common -class VolumeTests(common.BaseVolumeTests, v2.VolumeTests): +class VolumeTests(common.BaseVolumeTests): """Functional tests for volume. """ + + def test_volume_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.assertEqual( + 1, + cmd_output["size"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 2 ' + + name2 + )) + self.assertEqual( + 2, + cmd_output["size"], + ) + + self.wait_for_status("volume", name1, "available") + self.wait_for_status("volume", name2, "available") + del_output = self.openstack('volume delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_volume_list(self): + """Test create, list filter""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.addCleanup(self.openstack, 'volume delete ' + name1) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status("volume", name1, "available") + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 2 ' + + name2 + )) + self.addCleanup(self.openstack, 'volume delete ' + name2) + self.assertEqual( + 2, + cmd_output["size"], + ) + self.wait_for_status("volume", name2, "available") + raw_output = self.openstack( + 'volume set ' + + '--state error ' + + name2 + ) + self.assertOutput('', raw_output) + + # Test list --long + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '--long' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --status + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '--status error' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # TODO(qiangjiahui): Add project option to filter tests when we can + # specify volume with project + + def test_volume_set_and_unset(self): + """Tests create volume, set, unset, show, delete""" + name = uuid.uuid4().hex + new_name = name + "_" + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + '--description aaaa ' + + '--property Alpha=a ' + + name + )) + self.addCleanup(self.openstack, 'volume delete ' + new_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + self.assertEqual( + {'Alpha': 'a'}, + cmd_output["properties"], + ) + self.assertEqual( + 'false', + cmd_output["bootable"], + ) + self.wait_for_status("volume", name, "available") + + # Test volume set + raw_output = self.openstack( + 'volume set ' + + '--name ' + new_name + + ' --size 2 ' + + '--description bbbb ' + + '--no-property ' + + '--property Beta=b ' + + '--property Gamma=c ' + + '--image-property a=b ' + + '--image-property c=d ' + + '--bootable ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + self.assertEqual( + 2, + cmd_output["size"], + ) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + {'Beta': 'b', 'Gamma': 'c'}, + cmd_output["properties"], + ) + self.assertEqual( + {'a': 'b', 'c': 'd'}, + cmd_output["volume_image_metadata"], + ) + self.assertEqual( + 'true', + cmd_output["bootable"], + ) + + # Test volume unset + raw_output = self.openstack( + 'volume unset ' + + '--property Beta ' + + '--image-property a ' + + new_name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + new_name + )) + self.assertEqual( + {'Gamma': 'c'}, + cmd_output["properties"], + ) + self.assertEqual( + {'c': 'd'}, + cmd_output["volume_image_metadata"], + ) + + def test_volume_snapshot(self): + """Tests volume create from snapshot""" + + volume_name = uuid.uuid4().hex + snapshot_name = uuid.uuid4().hex + # Make a snapshot + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + self.wait_for_status("volume", volume_name, "available") + self.assertEqual( + volume_name, + cmd_output["name"], + ) + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + snapshot_name + + ' --volume ' + volume_name + )) + self.wait_for_status("volume snapshot", snapshot_name, "available") + + name = uuid.uuid4().hex + # Create volume from snapshot + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--snapshot ' + snapshot_name + + ' ' + name + )) + self.addCleanup(self.openstack, 'volume delete ' + name) + self.addCleanup(self.openstack, 'volume delete ' + volume_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.wait_for_status("volume", name, "available") + + # Delete snapshot + raw_output = self.openstack( + 'volume snapshot delete ' + snapshot_name) + self.assertOutput('', raw_output) + # Deleting snapshot may take time. If volume snapshot still exists when + # a parent volume delete is requested, the volume deletion will fail. + self.wait_for_delete('volume snapshot', snapshot_name) + + def test_volume_list_backward_compatibility(self): + """Test backward compatibility of list command""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.addCleanup(self.openstack, 'volume delete ' + name1) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status("volume", name1, "available") + + # Test list -c "Display Name" + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '-c "Display Name"' + )) + for each_volume in cmd_output: + self.assertIn('Display Name', each_volume) + + # Test list -c "Name" + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '-c "Name"' + )) + for each_volume in cmd_output: + self.assertIn('Name', each_volume) diff --git a/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py index 28eee6d24a..edfdafb6bd 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py @@ -10,9 +10,245 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.volume.v2 import test_volume_snapshot as v2 # noqa +import json +import uuid + from openstackclient.tests.functional.volume.v3 import common -class VolumeSnapshotTests(common.BaseVolumeTests, v2.VolumeSnapshotTests): +class VolumeSnapshotTests(common.BaseVolumeTests): """Functional tests for volume snapshot. """ + + VOLLY = uuid.uuid4().hex + + @classmethod + def setUpClass(cls): + super(VolumeSnapshotTests, cls).setUpClass() + # create a volume for all tests to create snapshot + cmd_output = json.loads(cls.openstack( + 'volume create -f json ' + + '--size 1 ' + + cls.VOLLY + )) + cls.wait_for_status('volume', cls.VOLLY, 'available') + cls.VOLUME_ID = cmd_output['id'] + + @classmethod + def tearDownClass(cls): + try: + cls.wait_for_status('volume', cls.VOLLY, 'available') + raw_output = cls.openstack( + 'volume delete --force ' + cls.VOLLY) + cls.assertOutput('', raw_output) + finally: + super(VolumeSnapshotTests, cls).tearDownClass() + + def test_volume_snapshot_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name1 + + ' --volume ' + self.VOLLY + )) + self.assertEqual( + name1, + cmd_output["name"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name2 + + ' --volume ' + self.VOLLY + )) + self.assertEqual( + name2, + cmd_output["name"], + ) + + self.wait_for_status('volume snapshot', name1, 'available') + self.wait_for_status('volume snapshot', name2, 'available') + + del_output = self.openstack( + 'volume snapshot delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + self.wait_for_delete('volume snapshot', name1) + self.wait_for_delete('volume snapshot', name2) + + def test_volume_snapshot_list(self): + """Test create, list filter""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name1 + + ' --volume ' + self.VOLLY + )) + self.addCleanup(self.wait_for_delete, 'volume snapshot', name1) + self.addCleanup(self.openstack, 'volume snapshot delete ' + name1) + self.assertEqual( + name1, + cmd_output["name"], + ) + self.assertEqual( + self.VOLUME_ID, + cmd_output["volume_id"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status('volume snapshot', name1, 'available') + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name2 + + ' --volume ' + self.VOLLY + )) + self.addCleanup(self.wait_for_delete, 'volume snapshot', name2) + self.addCleanup(self.openstack, 'volume snapshot delete ' + name2) + self.assertEqual( + name2, + cmd_output["name"], + ) + self.assertEqual( + self.VOLUME_ID, + cmd_output["volume_id"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status('volume snapshot', name2, 'available') + raw_output = self.openstack( + 'volume snapshot set ' + + '--state error ' + + name2 + ) + self.assertOutput('', raw_output) + + # Test list --long, --status + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--long ' + + '--status error' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # Test list --volume + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--volume ' + self.VOLLY + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--name ' + name1 + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + def test_volume_snapshot_set(self): + """Test create, set, unset, show, delete volume snapshot""" + name = uuid.uuid4().hex + new_name = name + "_" + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + '--volume ' + self.VOLLY + + ' --description aaaa ' + + '--property Alpha=a ' + + name + )) + self.addCleanup(self.wait_for_delete, 'volume snapshot', new_name) + self.addCleanup(self.openstack, 'volume snapshot delete ' + new_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + self.assertEqual( + {'Alpha': 'a'}, + cmd_output["properties"], + ) + self.wait_for_status('volume snapshot', name, 'available') + + # Test volume snapshot set + raw_output = self.openstack( + 'volume snapshot set ' + + '--name ' + new_name + + ' --description bbbb ' + + '--property Alpha=c ' + + '--property Beta=b ' + + name, + ) + self.assertOutput('', raw_output) + + # Show snapshot set result + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + {'Alpha': 'c', 'Beta': 'b'}, + cmd_output["properties"], + ) + + # Test volume snapshot unset + raw_output = self.openstack( + 'volume snapshot unset ' + + '--property Alpha ' + + new_name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertEqual( + {'Beta': 'b'}, + cmd_output["properties"], + ) + + # Test volume snapshot set --no-property + raw_output = self.openstack( + 'volume snapshot set ' + + '--no-property ' + + new_name, + ) + self.assertOutput('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertNotIn( + {'Beta': 'b'}, + cmd_output["properties"], + ) diff --git a/openstackclient/tests/functional/volume/v3/test_volume_type.py b/openstackclient/tests/functional/volume/v3/test_volume_type.py index eb66515ed1..79d4096998 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_type.py @@ -10,9 +10,229 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.volume.v2 import test_volume_type as v2 +import json +import time +import uuid + from openstackclient.tests.functional.volume.v3 import common -class VolumeTypeTests(common.BaseVolumeTests, v2.VolumeTypeTests): +class VolumeTypeTests(common.BaseVolumeTests): """Functional tests for volume type. """ + + def test_volume_type_create_list(self): + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume type create -f json --private ' + + name, + )) + self.addCleanup( + self.openstack, + 'volume type delete ' + name, + ) + self.assertEqual(name, cmd_output['name']) + + cmd_output = json.loads(self.openstack( + 'volume type show -f json %s' % name + )) + self.assertEqual(name, cmd_output['name']) + + cmd_output = json.loads(self.openstack('volume type list -f json')) + self.assertIn(name, [t['Name'] for t in cmd_output]) + + cmd_output = json.loads(self.openstack( + 'volume type list -f json --default' + )) + self.assertEqual(1, len(cmd_output)) + self.assertEqual('lvmdriver-1', cmd_output[0]['Name']) + + def test_volume_type_set_unset_properties(self): + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume type create -f json --private ' + + name, + )) + self.addCleanup( + self.openstack, + 'volume type delete ' + name + ) + self.assertEqual(name, cmd_output['name']) + + raw_output = self.openstack( + 'volume type set --property a=b --property c=d %s' % name + ) + self.assertEqual("", raw_output) + cmd_output = json.loads(self.openstack( + 'volume type show -f json %s' % name + )) + self.assertEqual({'a': 'b', 'c': 'd'}, cmd_output['properties']) + + raw_output = self.openstack( + 'volume type unset --property a %s' % name + ) + self.assertEqual("", raw_output) + cmd_output = json.loads(self.openstack( + 'volume type show -f json %s' % name + )) + self.assertEqual({'c': 'd'}, cmd_output['properties']) + + def test_volume_type_set_unset_multiple_properties(self): + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume type create -f json --private ' + + name, + )) + self.addCleanup( + self.openstack, + 'volume type delete ' + name + ) + self.assertEqual(name, cmd_output['name']) + + raw_output = self.openstack( + 'volume type set --property a=b --property c=d %s' % name + ) + self.assertEqual("", raw_output) + cmd_output = json.loads(self.openstack( + 'volume type show -f json %s' % name + )) + self.assertEqual({'a': 'b', 'c': 'd'}, cmd_output['properties']) + + raw_output = self.openstack( + 'volume type unset --property a --property c %s' % name + ) + self.assertEqual("", raw_output) + cmd_output = json.loads(self.openstack( + 'volume type show -f json %s' % name + )) + self.assertEqual({}, cmd_output['properties']) + + def test_volume_type_set_unset_project(self): + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume type create -f json --private ' + + name, + )) + self.addCleanup( + self.openstack, + 'volume type delete ' + name + ) + self.assertEqual(name, cmd_output['name']) + + raw_output = self.openstack( + 'volume type set --project admin %s' % name + ) + self.assertEqual("", raw_output) + + raw_output = self.openstack( + 'volume type unset --project admin %s' % name + ) + self.assertEqual("", raw_output) + + def test_multi_delete(self): + vol_type1 = uuid.uuid4().hex + vol_type2 = uuid.uuid4().hex + self.openstack('volume type create %s' % vol_type1) + time.sleep(5) + self.openstack('volume type create %s' % vol_type2) + time.sleep(5) + cmd = 'volume type delete %s %s' % (vol_type1, vol_type2) + raw_output = self.openstack(cmd) + self.assertOutput('', raw_output) + + # NOTE: Add some basic funtional tests with the old format to + # make sure the command works properly, need to change + # these to new test format when beef up all tests for + # volume tye commands. + def test_encryption_type(self): + name = uuid.uuid4().hex + encryption_type = uuid.uuid4().hex + # test create new encryption type + cmd_output = json.loads(self.openstack( + 'volume type create -f json ' + '--encryption-provider LuksEncryptor ' + '--encryption-cipher aes-xts-plain64 ' + '--encryption-key-size 128 ' + '--encryption-control-location front-end ' + + encryption_type)) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) + # test show encryption type + cmd_output = json.loads(self.openstack( + 'volume type show -f json --encryption-type ' + encryption_type)) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) + # test list encryption type + cmd_output = json.loads(self.openstack( + 'volume type list -f json --encryption-type')) + encryption_output = [t['Encryption'] for t in cmd_output + if t['Name'] == encryption_type][0] + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, encryption_output[attr]) + # test set existing encryption type + raw_output = self.openstack( + 'volume type set ' + '--encryption-key-size 256 ' + '--encryption-control-location back-end ' + + encryption_type) + self.assertEqual('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume type show -f json --encryption-type ' + encryption_type)) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 256, + 'control_location': 'back-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) + # test set new encryption type + cmd_output = json.loads(self.openstack( + 'volume type create -f json --private ' + + name, + )) + self.addCleanup( + self.openstack, + 'volume type delete ' + name, + ) + self.assertEqual(name, cmd_output['name']) + + raw_output = self.openstack( + 'volume type set ' + '--encryption-provider LuksEncryptor ' + '--encryption-cipher aes-xts-plain64 ' + '--encryption-key-size 128 ' + '--encryption-control-location front-end ' + + name) + self.assertEqual('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume type show -f json --encryption-type ' + name + )) + expected = {'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end'} + for attr, value in expected.items(): + self.assertEqual(value, cmd_output['encryption'][attr]) + # test unset encryption type + raw_output = self.openstack( + 'volume type unset --encryption-type ' + name + ) + self.assertEqual('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume type show -f json --encryption-type ' + name + )) + self.assertEqual({}, cmd_output['encryption']) + # test delete encryption type + raw_output = self.openstack('volume type delete ' + encryption_type) + self.assertEqual('', raw_output) From 874a726f522dae3a08832f32230e2590a787d45c Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Wed, 31 Jul 2019 12:24:04 +0800 Subject: [PATCH 0165/1120] Microversion 2.79: Add delete_on_termination to volume-attach API Added ``--disable-delete-on-termination`` and ``--enable-delete-on-termination`` options to the ``openstack server add volume`` command that enables users to mark whether to delete the attached volume when the server is destroyed. Depends-On: https://review.opendev.org/#/c/681267/ Part of blueprint support-delete-on-termination-in-server-attach-volume Change-Id: I6b5cd54b82a1135335a71b9768a1a2c2012f755b --- lower-constraints.txt | 2 +- openstackclient/compute/v2/server.py | 41 ++++- .../tests/unit/compute/v2/test_server.py | 172 ++++++++++++++++++ ...store-shelved-server-9179045f04815bbb.yaml | 9 + requirements.txt | 2 +- 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-9179045f04815bbb.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 201726ac65..ad911e771d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -100,7 +100,7 @@ python-mimeparse==1.6.0 python-mistralclient==3.1.0 python-muranoclient==0.8.2 python-neutronclient==6.7.0 -python-novaclient==15.0.0 +python-novaclient==15.1.0 python-octaviaclient==1.11.0 python-rsdclient==1.0.1 python-saharaclient==1.4.0 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index fb7596a9fd..414abb626a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -446,6 +446,21 @@ def get_parser(self, prog_name): metavar='', help=_('Server internal device name for volume'), ) + termination_group = parser.add_mutually_exclusive_group() + termination_group.add_argument( + '--enable-delete-on-termination', + action='store_true', + help=_("Specify if the attached volume should be deleted when " + "the server is destroyed. (Supported with " + "``--os-compute-api-version`` 2.79 or greater.)"), + ) + termination_group.add_argument( + '--disable-delete-on-termination', + action='store_true', + help=_("Specify if the attached volume should not be deleted " + "when the server is destroyed. (Supported with " + "``--os-compute-api-version`` 2.79 or greater.)"), + ) return parser def take_action(self, parsed_args): @@ -461,10 +476,34 @@ def take_action(self, parsed_args): parsed_args.volume, ) + support_set_delete_on_termination = (compute_client.api_version >= + api_versions.APIVersion('2.79')) + + if not support_set_delete_on_termination: + if parsed_args.enable_delete_on_termination: + msg = _('--os-compute-api-version 2.79 or greater ' + 'is required to support the ' + '--enable-delete-on-termination option.') + raise exceptions.CommandError(msg) + if parsed_args.disable_delete_on_termination: + msg = _('--os-compute-api-version 2.79 or greater ' + 'is required to support the ' + '--disable-delete-on-termination option.') + raise exceptions.CommandError(msg) + + kwargs = { + "device": parsed_args.device + } + + if parsed_args.enable_delete_on_termination: + kwargs['delete_on_termination'] = True + if parsed_args.disable_delete_on_termination: + kwargs['delete_on_termination'] = False + compute_client.volumes.create_server_volume( server.id, volume.id, - parsed_args.device, + **kwargs ) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5c98188a76..c70d6d72d0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -43,6 +43,10 @@ def setUp(self): self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() + # Get a shortcut to the compute client volumeManager Mock + self.servers_volumes_mock = self.app.client_manager.compute.volumes + self.servers_volumes_mock.reset_mock() + # Get a shortcut to the compute client FlavorManager Mock self.flavors_mock = self.app.client_manager.compute.flavors self.flavors_mock.reset_mock() @@ -457,6 +461,174 @@ def test_server_add_port_no_neutron(self): self.find_port.assert_not_called() +class TestServerVolume(TestServer): + + def setUp(self): + super(TestServerVolume, self).setUp() + + self.volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = self.volume + + self.methods = { + 'create_server_volume': None, + } + + # Get the command object to test + self.cmd = server.AddServerVolume(self.app, None) + + def test_server_add_volume(self): + servers = self.setup_servers_mock(count=1) + arglist = [ + '--device', '/dev/sdb', + servers[0].id, + self.volume.id, + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('device', '/dev/sdb'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.create_server_volume.assert_called_once_with( + servers[0].id, self.volume.id, device='/dev/sdb') + self.assertIsNone(result) + + +class TestServerVolumeV279(TestServerVolume): + + def test_server_add_volume_with_enable_delete_on_termination(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.79') + + servers = self.setup_servers_mock(count=1) + arglist = [ + '--enable-delete-on-termination', + '--device', '/dev/sdb', + servers[0].id, + self.volume.id, + ] + + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('device', '/dev/sdb'), + ('enable_delete_on_termination', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.create_server_volume.assert_called_once_with( + servers[0].id, self.volume.id, + device='/dev/sdb', delete_on_termination=True) + self.assertIsNone(result) + + def test_server_add_volume_with_disable_delete_on_termination(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.79') + + servers = self.setup_servers_mock(count=1) + arglist = [ + '--disable-delete-on-termination', + '--device', '/dev/sdb', + servers[0].id, + self.volume.id, + ] + + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('device', '/dev/sdb'), + ('disable_delete_on_termination', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.create_server_volume.assert_called_once_with( + servers[0].id, self.volume.id, + device='/dev/sdb', delete_on_termination=False) + self.assertIsNone(result) + + def test_server_add_volume_with_enable_delete_on_termination_pre_v279( + self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.78') + + servers = self.setup_servers_mock(count=1) + arglist = [ + servers[0].id, + self.volume.id, + '--enable-delete-on-termination', + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('enable_delete_on_termination', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('--os-compute-api-version 2.79 or greater is required', + str(ex)) + + def test_server_add_volume_with_disable_delete_on_termination_pre_v279( + self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.78') + + servers = self.setup_servers_mock(count=1) + arglist = [ + servers[0].id, + self.volume.id, + '--disable-delete-on-termination', + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('disable_delete_on_termination', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('--os-compute-api-version 2.79 or greater is required', + str(ex)) + + def test_server_add_volume_with_disable_and_enable_delete_on_termination( + self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.79') + + servers = self.setup_servers_mock(count=1) + arglist = [ + '--enable-delete-on-termination', + '--disable-delete-on-termination', + '--device', '/dev/sdb', + servers[0].id, + self.volume.id, + ] + + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('device', '/dev/sdb'), + ('enable_delete_on_termination', True), + ('disable_delete_on_termination', True), + ] + ex = self.assertRaises(utils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + self.assertIn('Argument parse failed', str(ex)) + + class TestServerAddNetwork(TestServer): def setUp(self): diff --git a/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-9179045f04815bbb.yaml b/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-9179045f04815bbb.yaml new file mode 100644 index 0000000000..ec207eac0c --- /dev/null +++ b/releasenotes/notes/bp-support-specifying-az-when-restore-shelved-server-9179045f04815bbb.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add ``--disable-delete-on-termination`` and + ``--enable-delete-on-termination`` options to the ``server add volume`` + command to indicate when to delete the attached volume when the server + is deleted. + Note that it requires ``--os-compute-api-version 2.79`` or greater. + [Blueprint support-specifying-az-when-restore-shelved-server ``_] diff --git a/requirements.txt b/requirements.txt index 67139bbdbc..eea51e5163 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.17.0 # Apache-2.0 -python-novaclient>=15.0.0 # Apache-2.0 +python-novaclient>=15.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From 5b3a827a1ff80e4b51c7ede44b84bf640d5b6380 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Wed, 18 Sep 2019 11:58:12 -0400 Subject: [PATCH 0166/1120] Provide stderr in exception when check_parser fails For negative tests that are asserting an argparse failure it would be useful to assert the specific reason for the failure in the test rather than just getting an exception, especially to avoid false positives in the tests when what is being tested and failing isn't the actual expected reason for the failure. This wraps the check_parser code that parses the args and mocks sys.stderr so we can trap that output and put it in the exception message that gets raised to the test. As a result, we can tighten up a test that was passing before for the wrong reason [1]. [1] https://review.opendev.org/#/c/673725/12/openstackclient/tests/unit/compute/v2/test_server.py@605 Change-Id: I0f1dc1215bdfb3eba98ccaf66a0041d220b93812 --- openstackclient/tests/unit/compute/v2/test_server.py | 3 ++- openstackclient/tests/unit/utils.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c70d6d72d0..c2bac2771f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -626,7 +626,8 @@ def test_server_add_volume_with_disable_and_enable_delete_on_termination( ex = self.assertRaises(utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) - self.assertIn('Argument parse failed', str(ex)) + self.assertIn('argument --disable-delete-on-termination: not allowed ' + 'with argument --enable-delete-on-termination', str(ex)) class TestServerAddNetwork(TestServer): diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index c15d8bbfe2..8df81a50eb 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -17,6 +17,7 @@ import os import fixtures +from six.moves import StringIO import testtools from cliff import columns as cliff_columns @@ -72,10 +73,13 @@ def setUp(self): def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') - try: - parsed_args = cmd_parser.parse_args(args) - except SystemExit: - raise ParserException("Argument parse failed") + stderr = StringIO() + with fixtures.MonkeyPatch('sys.stderr', stderr): + try: + parsed_args = cmd_parser.parse_args(args) + except SystemExit: + raise ParserException("Argument parse failed: %s" % + stderr.getvalue()) for av in verify_args: attr, value = av if attr: From 509ca3ed36b4ef512a47ff8d39c9df751084015a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89douard=20Thuleau?= Date: Wed, 4 Dec 2019 08:21:50 +0100 Subject: [PATCH 0167/1120] Fix router create/show if extraroute not supported If neutron does not support extraroute l3 extension, the route column formatter fails. Change-Id: I7b89c4f818865073947e0850e86c18d0d2415a51 --- openstackclient/network/v2/router.py | 2 +- .../tests/unit/network/v2/test_router.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 02da50c38e..5d85e485a0 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -49,7 +49,7 @@ def human_readable(self): class RoutesColumn(cliff_columns.FormattableColumn): def human_readable(self): # Map the route keys to match --route option. - for route in self._value: + for route in self._value or []: if 'nexthop' in route: route['gateway'] = route.pop('nexthop') return utils.format_list_of_dicts(self._value) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 079b9746ae..500cfbe5a3 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -1285,6 +1285,24 @@ def test_show_no_ha_no_distributed(self): self.assertNotIn("is_distributed", columns) self.assertNotIn("is_ha", columns) + def test_show_no_extra_route_extension(self): + _router = network_fakes.FakeRouter.create_one_router({'routes': None}) + + arglist = [ + _router.name, + ] + verifylist = [ + ('router', _router.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object( + self.network, "find_router", return_value=_router): + columns, data = self.cmd.take_action(parsed_args) + + self.assertIn("routes", columns) + self.assertIsNone(list(data)[columns.index('routes')].human_readable()) + class TestUnsetRouter(TestRouter): From 924627678d9bb1e9cdf84b524fb69dd37f4345ec Mon Sep 17 00:00:00 2001 From: Daniel Bengtsson Date: Thu, 21 Nov 2019 14:20:42 +0100 Subject: [PATCH 0168/1120] Stop testing python 2 in tox and zuul. Remove python 2 from envlist parameter. Check the link: https://etherpad.openstack.org/p/drop-python2-support The plan is drop the python 2 support from OpenStack in Ussuri release. Remove the zuul jobs. Note that the (non-voting) openstackclient-check-plugins job is still running under py2 at this time. That will need to be fixed in the python/openstackclient repository where the job is defined. Change-Id: I3148db053b9ef0fcf7dc88e5cc075d974c93d819 --- .zuul.yaml | 29 +---------------------------- tox.ini | 13 ++++++------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 574734659e..15f719c356 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -5,28 +5,6 @@ Run unit tests for OpenStackClient with master branch of important libs. Takes advantage of the base tox job's install-siblings feature. - required-projects: - - openstack/cliff - - openstack/keystoneauth - - openstack/openstacksdk - - openstack/os-client-config - - openstack/osc-lib - - openstack/python-openstackclient - vars: - tox_envlist: py27 - # Set work dir to openstackclient so that if it's triggered by one of the - # other repos the tests will run in the same place - zuul_work_dir: src/opendev.org/openstack/python-openstackclient - -- job: - name: osc-tox-py27-tips - parent: openstack-tox-py27 - description: | - Run unit tests for OpenStackClient with master branch of important libs. - - Takes advantage of the base tox job's install-siblings feature. - # The job only tests the latest and shouldn't be run on the stable branches - branches: ^(?!stable) required-projects: - openstack/cliff - openstack/keystoneauth @@ -154,7 +132,6 @@ - openstack/python-openstackclient vars: devstack_localrc: - USE_PYTHON3: true LIBS_FROM_GIT: python-openstackclient,openstacksdk,osc-lib,os-client-config # This is insufficient, but leaving it here as a reminder of what may # someday be all we need to make this work @@ -179,11 +156,9 @@ name: osc-tox-unit-tips check: jobs: - - osc-tox-py27-tips - osc-tox-py36-tips gate: jobs: - - osc-tox-py27-tips - osc-tox-py36-tips - project: @@ -192,12 +167,10 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python-jobs - - openstack-python3-train-jobs + - openstack-python3-ussuri-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 - - lib-forward-testing - lib-forward-testing-python3 check: jobs: diff --git a/tox.ini b/tox.ini index addafa2bfe..f15529cdc6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,15 @@ [tox] minversion = 2.3 -envlist = py27,py37,pep8 +envlist = py37,pep8 skipdist = True +# Automatic envs (pyXX) will only use the python version appropriate to that +# env and ignore basepython inherited from [testenv] if we set +# ignore_basepython_conflict. +ignore_basepython_conflict = True [testenv] usedevelop = True +basepython = python3 install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_STDOUT_CAPTURE=1 @@ -24,7 +29,6 @@ commands = {toxinidir}/tools/fast8.sh [testenv:pep8] -basepython = python3 commands = flake8 bandit -r openstackclient -x tests -s B105,B106,B107,B401,B404,B603,B606,B607,B110,B605,B101 @@ -86,7 +90,6 @@ commands = stestr run {posargs} [testenv:venv] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt @@ -94,7 +97,6 @@ deps = commands = {posargs} [testenv:cover] -basepython = python3 setenv = VIRTUAL_ENV={envdir} PYTHON=coverage run --source openstackclient --parallel-mode @@ -110,7 +112,6 @@ commands = oslo_debug_helper -t openstackclient/tests {posargs} [testenv:docs] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt @@ -122,7 +123,6 @@ commands = whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:releasenotes] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt @@ -140,7 +140,6 @@ import-order-style = pep8 application_import_names = openstackclient [testenv:lower-constraints] -basepython = python3 deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt From 69870ae439f18863979949f5728543a0fb1cbe83 Mon Sep 17 00:00:00 2001 From: Bram Verschueren Date: Fri, 13 Dec 2019 11:13:54 +0100 Subject: [PATCH 0169/1120] Fix faulthy state argument choice The correct state name for a failing volume snapshot deletion is 'error_deleting' instead of 'error-deleting'. [1] [1] https://opendev.org/openstack/cinder/src/commit/89d6a5042fcb2ede5a0b1112d72fae805ea52fcd/cinder/objects/fields.py#L126 Task: #37844 Story: #2007037 Change-Id: Ia99900ece4f1cd29769b22ddaa3965789d719556 --- .../cli/command-objects/volume-snapshot.rst | 2 +- .../volume/v2/test_volume_snapshot.py | 18 ++++++++++++++++++ openstackclient/volume/v1/volume_snapshot.py | 4 ++-- .../volume/v2/consistency_group_snapshot.py | 2 +- openstackclient/volume/v2/volume_snapshot.py | 6 +++--- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/doc/source/cli/command-objects/volume-snapshot.rst b/doc/source/cli/command-objects/volume-snapshot.rst index 30cc77cc57..21a8937018 100644 --- a/doc/source/cli/command-objects/volume-snapshot.rst +++ b/doc/source/cli/command-objects/volume-snapshot.rst @@ -115,7 +115,7 @@ List volume snapshots .. option:: --status Filters results by a status. - ('available', 'error', 'creating', 'deleting' or 'error-deleting') + ('available', 'error', 'creating', 'deleting' or 'error_deleting') .. option:: --name diff --git a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py index 8d32d99731..4977a73ebc 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py @@ -121,6 +121,24 @@ def test_volume_snapshot_list(self): cmd_output["size"], ) self.wait_for_status('volume snapshot', name2, 'available') + + raw_output = self.openstack( + 'volume snapshot set ' + + '--state error_deleting ' + + name2 + ) + self.assertOutput('', raw_output) + + # Test list --long, --status + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--long ' + + '--status error_deleting' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + raw_output = self.openstack( 'volume snapshot set ' + '--state error ' + diff --git a/openstackclient/volume/v1/volume_snapshot.py b/openstackclient/volume/v1/volume_snapshot.py index 50f81771a0..bfe249f526 100644 --- a/openstackclient/volume/v1/volume_snapshot.py +++ b/openstackclient/volume/v1/volume_snapshot.py @@ -175,10 +175,10 @@ def get_parser(self, prog_name): '--status', metavar='', choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_("Filters results by a status. " "('available', 'error', 'creating', 'deleting'" - " or 'error-deleting')") + " or 'error_deleting')") ) parser.add_argument( '--volume', diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py index 540deb0187..3cba4ecaa9 100644 --- a/openstackclient/volume/v2/consistency_group_snapshot.py +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -129,7 +129,7 @@ def get_parser(self, prog_name): '--status', metavar="", choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_('Filters results by a status ("available", "error", ' '"creating", "deleting" or "error_deleting")') ) diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index 2b26ae323b..6cbd1156ad 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -230,10 +230,10 @@ def get_parser(self, prog_name): '--status', metavar='', choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_("Filters results by a status. " "('available', 'error', 'creating', 'deleting'" - " or 'error-deleting')") + " or 'error_deleting')") ) parser.add_argument( '--volume', @@ -345,7 +345,7 @@ def get_parser(self, prog_name): '--state', metavar='', choices=['available', 'error', 'creating', 'deleting', - 'error-deleting'], + 'error_deleting'], help=_('New snapshot state. ("available", "error", "creating", ' '"deleting", or "error_deleting") (admin only) ' '(This option simply changes the state of the snapshot ' From f5384ae16a24cdd54149fa21827d14b8b8983d4f Mon Sep 17 00:00:00 2001 From: KeithMnemonic Date: Thu, 24 Oct 2019 14:39:50 -0400 Subject: [PATCH 0170/1120] Fix openstack server list --deleted --marker option This patch removes using the "name" option for a marker when --deleted is also used. The find_resource() function that is being called does not correctly handle using the marker as the "name" in the search when also using deleted=True. One simple way to fix this is force the marker to only be an ID when --deleted is used. This is how the nova client works. Using the --deleted option is available to users with the admin role by default. If you're an admin listing --deleted servers with a marker by name, find_resource() is going to fail to find it since it doesn't apply the --deleted filter to find_resource(). The find_resource() function is trying to find the marker server by name if it's not found by id, and to find it by name it's listing servers with the given marker as the name, but not applying the --deleted filter so it doesn't get back any results. In the story it was suggested modifying find_resource to include the deleted query param when it's specified on the command line but that didn't work because it still results in something like this: http://192.168.1.123/compute/v2.1/servers?deleted=True&name=4cecd49f-bc25-4a7e-826e-4aea6f9267d9 It seems like there are bugs in find_resource(). Restricting the marker to be the server ID when listing deleted servers is probably OK since if you're using --deleted you're an admin and you could be listing across all projects and if you're filtering by a server across all projects anyway (not that you have to, I'm just saying if you are), or even showing a server in another project, you have to do it by id rather than name because find_resource() won't find the server in another project by name, only ID. story: 2006761 Task: 37258 Change-Id: Ib878982b1d469212ca3483dcfaf407a8e1d2b417 --- openstackclient/compute/v2/server.py | 15 +++++-- .../functional/compute/v2/test_server.py | 43 +++++++++++++++++++ .../notes/bug-2006761-9041d1b25e845cfb.yaml | 8 ++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/bug-2006761-9041d1b25e845cfb.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 414abb626a..84061a53c9 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1242,7 +1242,8 @@ def get_parser(self, prog_name): default=None, help=_('The last server of the previous page. Display ' 'list of servers after marker. Display all servers if not ' - 'specified. (name or ID)') + 'specified. When used with ``--deleted``, the marker must ' + 'be an ID, otherwise a name or ID can be used.'), ) parser.add_argument( '--limit', @@ -1450,9 +1451,17 @@ def take_action(self, parsed_args): mixed_case_fields = [] marker_id = None + if parsed_args.marker: - marker_id = utils.find_resource(compute_client.servers, - parsed_args.marker).id + # Check if both "--marker" and "--deleted" are used. + # In that scenario a lookup is not needed as the marker + # needs to be an ID, because find_resource does not + # handle deleted resources + if parsed_args.deleted: + marker_id = parsed_args.marker + else: + marker_id = utils.find_resource(compute_client.servers, + parsed_args.marker).id data = compute_client.servers.list(search_opts=search_opts, marker=marker_id, diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 2bca70d0ec..6e080e9ba2 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -63,6 +63,49 @@ def test_server_list(self): self.assertNotIn(name1, col_name) self.assertIn(name2, col_name) + def test_server_list_with_marker_and_deleted(self): + """Test server list with deleted and marker""" + cmd_output = self.server_create(cleanup=False) + name1 = cmd_output['name'] + cmd_output = self.server_create(cleanup=False) + name2 = cmd_output['name'] + id2 = cmd_output['id'] + self.wait_for_status(name1, "ACTIVE") + self.wait_for_status(name2, "ACTIVE") + + # Test list --marker with ID + cmd_output = json.loads(self.openstack( + 'server list -f json --marker ' + id2 + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + + # Test list --marker with Name + cmd_output = json.loads(self.openstack( + 'server list -f json --marker ' + name2 + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + + self.openstack('server delete --wait ' + name1) + self.openstack('server delete --wait ' + name2) + + # Test list --deleted --marker with ID + cmd_output = json.loads(self.openstack( + 'server list -f json --deleted --marker ' + id2 + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + + # Test list --deleted --marker with Name + try: + cmd_output = json.loads(self.openstack( + 'server list -f json --deleted --marker ' + name2 + )) + except exceptions.CommandFailed as e: + self.assertIn('marker [%s] not found (HTTP 400)' % (name2), + e.stderr.decode('utf-8')) + def test_server_list_with_changes_before(self): """Test server list. diff --git a/releasenotes/notes/bug-2006761-9041d1b25e845cfb.yaml b/releasenotes/notes/bug-2006761-9041d1b25e845cfb.yaml new file mode 100644 index 0000000000..e647cf2114 --- /dev/null +++ b/releasenotes/notes/bug-2006761-9041d1b25e845cfb.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixes the "No server with a name or ID of 'id' exists" error when running + ``server list --deleted --marker``. The fix removes using a name for + the marker when both ``--deleted`` and ``--marker`` are used. In + this scenario an ID must be supplied for the marker. + [Story `2006761 `_] From 32080f7a428f7a56363e43563d0d316b04219f43 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 7 Jan 2020 09:22:30 -0500 Subject: [PATCH 0171/1120] Bump tox minversion ignore_basepython_conflict was introduced in tox 3.1.0. Change-Id: I7a6049b6a4fd3ee376a3478e94837c0afe89d4df --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f15529cdc6..3a7ed09794 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 2.3 +minversion = 3.1 envlist = py37,pep8 skipdist = True # Automatic envs (pyXX) will only use the python version appropriate to that From 780d9b49a0f9a1bef5007e3e75275a1d48a96b13 Mon Sep 17 00:00:00 2001 From: Alex Katz Date: Sun, 5 Jan 2020 14:08:04 +0200 Subject: [PATCH 0172/1120] Show correct name for resource with quota set to zero In case quota for the resource is set to zero "openstack quota show" command will not map the resource name according to one of the following dicts: - COMPUTE_QUOTAS - NOVA_NETWORK_QUOTAS - VOLUME_QUOTAS - NETWORK_QUOTAS For example: $ openstack quota set --secgroups 10 admin $ openstack quota show admin -f json|egrep "(secgroups|security_groups)" "secgroups": 10, $ openstack quota set --secgroups 0 admin $ openstack quota show admin -f json|egrep "(secgroups|security_groups)" "security_groups": 0, Change-Id: I94ed9e6b41b1cc692297c01e6c7582998dcacfda --- openstackclient/common/quota.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 80c8749ac1..ef21e69624 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -650,7 +650,7 @@ def take_action(self, parsed_args): for k, v in itertools.chain( COMPUTE_QUOTAS.items(), NOVA_NETWORK_QUOTAS.items(), VOLUME_QUOTAS.items(), NETWORK_QUOTAS.items()): - if not k == v and info.get(k): + if not k == v and info.get(k) is not None: info[v] = info[k] info.pop(k) From d15bbada73f81136c966007d9c564dd6cfb2fd9c Mon Sep 17 00:00:00 2001 From: lihaijing Date: Fri, 7 Jul 2017 11:48:48 +0800 Subject: [PATCH 0173/1120] Replace six.iteritems() with .items() 1. As mentioned in [1], we should avoid using six.iteritems to achieve iterators. We can use dict.items instead, as it will return iterators in PY3 as well. And dict.items/keys will more readable. 2. In py2, the performance about list should be negligible, see the link [2]. [1] https://wiki.openstack.org/wiki/Python3 [2] http://lists.openstack.org/pipermail/openstack-dev/2015-June/066391.html Co-Authored-By: Akihiro Motoki Change-Id: I4b9edb326444264c0f6c4ad281acaac356a07e85 Implements: blueprint replace-iteritems-with-items --- HACKING.rst | 3 +-- openstackclient/api/object_store_v1.py | 5 ++--- openstackclient/common/availability_zone.py | 5 ++--- openstackclient/common/configuration.py | 5 ++--- openstackclient/common/module.py | 3 +-- openstackclient/common/quota.py | 3 +-- openstackclient/compute/v2/agent.py | 3 +-- openstackclient/compute/v2/aggregate.py | 9 ++++----- openstackclient/compute/v2/console.py | 3 +-- openstackclient/compute/v2/flavor.py | 5 ++--- openstackclient/compute/v2/hypervisor.py | 3 +-- openstackclient/compute/v2/hypervisor_stats.py | 3 +-- openstackclient/compute/v2/keypair.py | 5 ++--- openstackclient/compute/v2/server.py | 6 +++--- openstackclient/compute/v2/server_backup.py | 3 +-- openstackclient/compute/v2/server_event.py | 3 +-- openstackclient/compute/v2/server_image.py | 3 +-- openstackclient/compute/v2/usage.py | 3 +-- openstackclient/identity/v2_0/catalog.py | 3 +-- openstackclient/identity/v2_0/ec2creds.py | 5 ++--- openstackclient/identity/v2_0/endpoint.py | 5 ++--- openstackclient/identity/v2_0/project.py | 5 ++--- openstackclient/identity/v2_0/role.py | 7 +++---- openstackclient/identity/v2_0/service.py | 9 ++++----- openstackclient/identity/v2_0/token.py | 3 +-- openstackclient/identity/v2_0/user.py | 5 ++--- openstackclient/identity/v3/application_credential.py | 5 ++--- openstackclient/identity/v3/catalog.py | 3 +-- openstackclient/identity/v3/consumer.py | 5 ++--- openstackclient/identity/v3/credential.py | 5 ++--- openstackclient/identity/v3/domain.py | 5 ++--- openstackclient/identity/v3/ec2creds.py | 5 ++--- openstackclient/identity/v3/endpoint.py | 5 ++--- openstackclient/identity/v3/endpoint_group.py | 5 ++--- openstackclient/identity/v3/federation_protocol.py | 7 +++---- openstackclient/identity/v3/group.py | 5 ++--- openstackclient/identity/v3/identity_provider.py | 5 ++--- openstackclient/identity/v3/implied_role.py | 3 +-- openstackclient/identity/v3/limit.py | 7 +++---- openstackclient/identity/v3/mapping.py | 5 ++--- openstackclient/identity/v3/policy.py | 5 ++--- openstackclient/identity/v3/project.py | 5 ++--- openstackclient/identity/v3/region.py | 5 ++--- openstackclient/identity/v3/registered_limit.py | 7 +++---- openstackclient/identity/v3/role.py | 5 ++--- openstackclient/identity/v3/service.py | 5 ++--- openstackclient/identity/v3/service_provider.py | 5 ++--- openstackclient/identity/v3/token.py | 9 ++++----- openstackclient/identity/v3/trust.py | 5 ++--- openstackclient/identity/v3/user.py | 5 ++--- openstackclient/image/v1/image.py | 5 ++--- openstackclient/image/v2/image.py | 10 +++++----- openstackclient/network/sdk_utils.py | 4 +--- openstackclient/network/v2/network_segment_range.py | 4 ++-- openstackclient/network/v2/security_group.py | 3 +-- openstackclient/network/v2/security_group_rule.py | 3 +-- openstackclient/object/v1/account.py | 3 +-- openstackclient/object/v1/container.py | 3 +-- openstackclient/object/v1/object.py | 3 +-- .../tests/unit/common/test_availability_zone.py | 6 ++---- openstackclient/tests/unit/fakes.py | 4 ++-- openstackclient/volume/v1/qos_specs.py | 5 ++--- openstackclient/volume/v1/volume.py | 5 ++--- openstackclient/volume/v1/volume_backup.py | 5 ++--- openstackclient/volume/v1/volume_snapshot.py | 5 ++--- openstackclient/volume/v1/volume_transfer_request.py | 7 +++---- openstackclient/volume/v1/volume_type.py | 5 ++--- openstackclient/volume/v2/backup_record.py | 5 ++--- openstackclient/volume/v2/consistency_group.py | 5 ++--- .../volume/v2/consistency_group_snapshot.py | 5 ++--- openstackclient/volume/v2/qos_specs.py | 5 ++--- openstackclient/volume/v2/volume.py | 5 ++--- openstackclient/volume/v2/volume_backup.py | 7 +++---- openstackclient/volume/v2/volume_snapshot.py | 5 ++--- openstackclient/volume/v2/volume_transfer_request.py | 7 +++---- openstackclient/volume/v2/volume_type.py | 5 ++--- 76 files changed, 148 insertions(+), 222 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 61803e9a4d..52e96caa93 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -93,8 +93,7 @@ OpenStackClient strives to be Python 3.3 compatible. Common guidelines: * Convert print statements to functions: print statements should be converted to an appropriate log or other output mechanism. -* Use six where applicable: x.iteritems is converted to six.iteritems(x) - for example. +* Prefer to x.items() over six.iteritems(x). Running Tests ------------- diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index d1e5dfaf45..44ff7a01fb 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -19,7 +19,6 @@ import sys from osc_lib import utils -import six from six.moves import urllib from openstackclient.api import api @@ -559,7 +558,7 @@ def _set_properties(self, properties, header_tag): log = logging.getLogger(__name__ + '._set_properties') headers = {} - for k, v in six.iteritems(properties): + for k, v in properties.items(): if not utils.is_ascii(k) or not utils.is_ascii(v): log.error('Cannot set property %s to non-ascii value', k) continue @@ -572,7 +571,7 @@ def _get_properties(self, headers, header_tag): # Add in properties as a top level key, this is consistent with other # OSC commands properties = {} - for k, v in six.iteritems(headers): + for k, v in headers.items(): if k.lower().startswith(header_tag): properties[k[len(header_tag):]] = v return properties diff --git a/openstackclient/common/availability_zone.py b/openstackclient/common/availability_zone.py index b2385ef743..3b2fa848a0 100644 --- a/openstackclient/common/availability_zone.py +++ b/openstackclient/common/availability_zone.py @@ -19,7 +19,6 @@ from novaclient import exceptions as nova_exceptions from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -47,11 +46,11 @@ def _xform_compute_availability_zone(az, include_extra): return result if hasattr(az, 'hosts') and az.hosts: - for host, services in six.iteritems(az.hosts): + for host, services in az.hosts.items(): host_info = copy.deepcopy(zone_info) host_info['host_name'] = host - for svc, state in six.iteritems(services): + for svc, state in services.items(): info = copy.deepcopy(host_info) info['service_name'] = svc info['service_status'] = '%s %s %s' % ( diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 53b30d5fdd..49ef0e05ca 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -15,7 +15,6 @@ from keystoneauth1.loading import base from osc_lib.command import command -import six from openstackclient.i18n import _ @@ -59,9 +58,9 @@ def take_action(self, parsed_args): if o.secret ] - for key, value in six.iteritems(info.pop('auth', {})): + for key, value in info.pop('auth', {}).items(): if parsed_args.mask and key.lower() in secret_opts: value = REDACTED info['auth.' + key] = value - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 20497f2131..f55fdce048 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -113,4 +112,4 @@ def take_action(self, parsed_args): # Catch all exceptions, just skip it pass - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 80c8749ac1..c7b3ad0707 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.network import common @@ -663,4 +662,4 @@ def take_action(self, parsed_args): project_name = project_info['name'] info['project_name'] = project_name - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index 151dcc1e82..3feb99ec83 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -77,7 +76,7 @@ def take_action(self, parsed_args): parsed_args.hypervisor ) agent = compute_client.agents.create(*args)._info.copy() - return zip(*sorted(six.iteritems(agent))) + return zip(*sorted(agent.items())) class DeleteAgent(command.Command): diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 3834de1f0b..599659a3e2 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -68,7 +67,7 @@ def take_action(self, parsed_args): 'properties': format_columns.DictColumn(info.pop('metadata')), }, ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class CreateAggregate(command.ShowOne): @@ -125,7 +124,7 @@ def take_action(self, parsed_args): 'properties': properties, }, ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteAggregate(command.Command): @@ -255,7 +254,7 @@ def take_action(self, parsed_args): 'properties': format_columns.DictColumn(info.pop('metadata')), }, ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class SetAggregate(command.Command): @@ -372,7 +371,7 @@ def take_action(self, parsed_args): info = {} info.update(data._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class UnsetAggregate(command.Command): diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index b2f7288f40..110b21b81e 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -18,7 +18,6 @@ from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -138,4 +137,4 @@ def take_action(self, parsed_args): # handle for different microversion API. console_data = data.get('remote_console', data.get('console')) info.update(console_data) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 4f1e48af06..42649db504 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -22,7 +22,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -195,7 +194,7 @@ def take_action(self, parsed_args): flavor_info.pop("links") flavor_info['properties'] = utils.format_dict(flavor.get_keys()) - return zip(*sorted(six.iteritems(flavor_info))) + return zip(*sorted(flavor_info.items())) class DeleteFlavor(command.Command): @@ -447,7 +446,7 @@ def take_action(self, parsed_args): flavor['properties'] = utils.format_dict(resource_flavor.get_keys()) - return zip(*sorted(six.iteritems(flavor))) + return zip(*sorted(flavor.items())) class UnsetFlavor(command.Command): diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 0d367fee86..7f110028b6 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -20,7 +20,6 @@ from novaclient import exceptions as nova_exceptions from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -126,4 +125,4 @@ def take_action(self, parsed_args): hypervisor["service_host"] = hypervisor["service"]["host"] del hypervisor["service"] - return zip(*sorted(six.iteritems(hypervisor))) + return zip(*sorted(hypervisor.items())) diff --git a/openstackclient/compute/v2/hypervisor_stats.py b/openstackclient/compute/v2/hypervisor_stats.py index b0413005b6..4493e08018 100644 --- a/openstackclient/compute/v2/hypervisor_stats.py +++ b/openstackclient/compute/v2/hypervisor_stats.py @@ -15,7 +15,6 @@ """Hypervisor Stats action implementations""" from osc_lib.command import command -import six from openstackclient.i18n import _ @@ -27,4 +26,4 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute hypervisor_stats = compute_client.hypervisors.statistics().to_dict() - return zip(*sorted(six.iteritems(hypervisor_stats))) + return zip(*sorted(hypervisor_stats.items())) diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 851cced0e9..2b365cebff 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -101,7 +100,7 @@ def take_action(self, parsed_args): del info['public_key'] if 'private_key' in info: del info['private_key'] - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) else: sys.stdout.write(keypair.private_key) return ({}, {}) @@ -184,7 +183,7 @@ def take_action(self, parsed_args): info.update(keypair._info) if not parsed_args.public_key: del info['public_key'] - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) else: # NOTE(dtroyer): a way to get the public key in a similar form # as the private key in the create command diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 84061a53c9..b5c420fe87 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1069,7 +1069,7 @@ def _match_image(image_api, wanted_properties): raise SystemExit details = _prep_server_detail(compute_client, image_client, server) - return zip(*sorted(six.iteritems(details))) + return zip(*sorted(details.items())) class CreateServerDump(command.Command): @@ -1967,7 +1967,7 @@ def _show_progress(progress): details = _prep_server_detail(compute_client, image_client, server, refresh=False) - return zip(*sorted(six.iteritems(details))) + return zip(*sorted(details.items())) class RemoveFixedIP(command.Command): @@ -2537,7 +2537,7 @@ def take_action(self, parsed_args): self.app.client_manager.image, server, refresh=False) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) class SshServer(command.Command): diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index a79f5f7039..1d560dc0c7 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -19,7 +19,6 @@ from osc_lib import exceptions from osc_lib import utils from oslo_utils import importutils -import six from openstackclient.i18n import _ @@ -129,4 +128,4 @@ def _show_progress(progress): ] ) info = image_module._format_image(image) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py index 6d33d02d50..4fcc913614 100644 --- a/openstackclient/compute/v2/server_event.py +++ b/openstackclient/compute/v2/server_event.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -122,4 +121,4 @@ def take_action(self, parsed_args): action_detail = compute_client.instance_action.get( server_id, parsed_args.request_id) - return zip(*sorted(six.iteritems(action_detail.to_dict()))) + return zip(*sorted(action_detail.to_dict().items())) diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index 3bc5d94aab..b93cd4d887 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -21,7 +21,6 @@ from osc_lib import exceptions from osc_lib import utils from oslo_utils import importutils -import six from openstackclient.i18n import _ @@ -109,4 +108,4 @@ def _show_progress(progress): ] ) info = image_module._format_image(image) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index f84cd61dfb..307c238afe 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -21,7 +21,6 @@ from novaclient import api_versions from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -236,4 +235,4 @@ def take_action(self, parsed_args): info['Disk GB-Hours'] = ( float("%.2f" % usage.total_local_gb_usage) if hasattr(usage, "total_local_gb_usage") else None) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 5d1e30626f..ccedbf3312 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -102,4 +101,4 @@ def take_action(self, parsed_args): LOG.error(_('service %s not found\n'), parsed_args.service) return ((), ()) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 0bc48322bc..f712bf4584 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -82,7 +81,7 @@ def take_action(self, parsed_args): {'project_id': info.pop('tenant_id')} ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteEC2Creds(command.Command): @@ -206,4 +205,4 @@ def take_action(self, parsed_args): {'project_id': info.pop('tenant_id')} ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 1628e48813..57906ddff6 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -76,7 +75,7 @@ def take_action(self, parsed_args): info.update(endpoint._info) info['service_name'] = service.name info['service_type'] = service.type - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteEndpoint(command.Command): @@ -178,4 +177,4 @@ def take_action(self, parsed_args): info.update(match._info) info['service_name'] = service.name info['service_type'] = service.type - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index e0018860e2..f431c02144 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -100,7 +99,7 @@ def take_action(self, parsed_args): # TODO(stevemar): Remove the line below when we support multitenancy project._info.pop('parent_id', None) - return zip(*sorted(six.iteritems(project._info))) + return zip(*sorted(project._info.items())) class DeleteProject(command.Command): @@ -299,7 +298,7 @@ def take_action(self, parsed_args): properties[k] = v info['properties'] = format_columns.DictColumn(properties) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class UnsetProject(command.Command): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index e9fe50fa4a..5c53fbcd5c 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -69,7 +68,7 @@ def take_action(self, parsed_args): info = {} info.update(role._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class CreateRole(command.ShowOne): @@ -105,7 +104,7 @@ def take_action(self, parsed_args): info = {} info.update(role._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteRole(command.Command): @@ -217,4 +216,4 @@ def take_action(self, parsed_args): info = {} info.update(role._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 653de8eb47..afc0b3d7ad 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -65,7 +64,7 @@ def take_action(self, parsed_args): info = {} info.update(service._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteService(command.Command): @@ -153,11 +152,11 @@ def take_action(self, parsed_args): if parsed_args.catalog: endpoints = auth_ref.service_catalog.get_endpoints( service_type=parsed_args.service) - for (service, service_endpoints) in six.iteritems(endpoints): + for (service, service_endpoints) in endpoints.items(): if service_endpoints: info = {"type": service} info.update(service_endpoints[0]) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) msg = _("No service catalog with a type, name or ID of '%s' " "exists.") % (parsed_args.service) @@ -166,4 +165,4 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) info = {} info.update(service._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index 3b08b4750d..205e15d30b 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -17,7 +17,6 @@ from osc_lib.command import command from osc_lib import exceptions -import six from openstackclient.i18n import _ @@ -49,7 +48,7 @@ def take_action(self, parsed_args): data['project_id'] = auth_ref.project_id if auth_ref.user_id: data['user_id'] = auth_ref.user_id - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) class RevokeToken(command.Command): diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 0675877b9b..8dac093ef4 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -154,7 +153,7 @@ def take_action(self, parsed_args): info = {} info.update(user._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteUser(command.Command): @@ -418,4 +417,4 @@ def take_action(self, parsed_args): {'project_id': info.pop('tenant_id')} ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v3/application_credential.py b/openstackclient/identity/v3/application_credential.py index 747fa20ed1..ea0b30cdbb 100644 --- a/openstackclient/identity/v3/application_credential.py +++ b/openstackclient/identity/v3/application_credential.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -123,7 +122,7 @@ def take_action(self, parsed_args): msg = ' '.join(r['name'] for r in roles) application_credential._info['roles'] = msg - return zip(*sorted(six.iteritems(application_credential._info))) + return zip(*sorted(application_credential._info.items())) class DeleteApplicationCredential(command.Command): @@ -217,4 +216,4 @@ def take_action(self, parsed_args): msg = ' '.join(r['name'] for r in roles) app_cred._info['roles'] = msg - return zip(*sorted(six.iteritems(app_cred._info))) + return zip(*sorted(app_cred._info.items())) diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index 59430c4ce0..d1f7d31909 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -97,4 +96,4 @@ def take_action(self, parsed_args): LOG.error(_('service %s not found\n'), parsed_args.service) return ((), ()) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index 6dd24dccf1..2f925aba87 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -46,7 +45,7 @@ def take_action(self, parsed_args): parsed_args.description ) consumer._info.pop('links', None) - return zip(*sorted(six.iteritems(consumer._info))) + return zip(*sorted(consumer._info.items())) class DeleteConsumer(command.Command): @@ -142,4 +141,4 @@ def take_action(self, parsed_args): identity_client.oauth1.consumers, parsed_args.consumer) consumer._info.pop('links', None) - return zip(*sorted(six.iteritems(consumer._info))) + return zip(*sorted(consumer._info.items())) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index 981f940aae..bf48df83d5 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -74,7 +73,7 @@ def take_action(self, parsed_args): project=project) credential._info.pop('links') - return zip(*sorted(six.iteritems(credential._info))) + return zip(*sorted(credential._info.items())) class DeleteCredential(command.Command): @@ -225,4 +224,4 @@ def take_action(self, parsed_args): parsed_args.credential) credential._info.pop('links') - return zip(*sorted(six.iteritems(credential._info))) + return zip(*sorted(credential._info.items())) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 064624ab45..dbcc97f6d2 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -85,7 +84,7 @@ def take_action(self, parsed_args): raise domain._info.pop('links') - return zip(*sorted(six.iteritems(domain._info))) + return zip(*sorted(domain._info.items())) class DeleteDomain(command.Command): @@ -206,4 +205,4 @@ def take_action(self, parsed_args): domain_str) domain._info.pop('links') - return zip(*sorted(six.iteritems(domain._info))) + return zip(*sorted(domain._info.items())) diff --git a/openstackclient/identity/v3/ec2creds.py b/openstackclient/identity/v3/ec2creds.py index 44e9a2c778..921b9168b0 100644 --- a/openstackclient/identity/v3/ec2creds.py +++ b/openstackclient/identity/v3/ec2creds.py @@ -17,7 +17,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -108,7 +107,7 @@ def take_action(self, parsed_args): {'project_id': info.pop('tenant_id')} ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteEC2Creds(command.Command): @@ -209,4 +208,4 @@ def take_action(self, parsed_args): {'project_id': info.pop('tenant_id')} ) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 858b50363b..a3bd2683ee 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -131,7 +130,7 @@ def take_action(self, parsed_args): info.update(endpoint._info) info['service_name'] = get_service_name(service) info['service_type'] = service.type - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteEndpoint(command.Command): @@ -389,4 +388,4 @@ def take_action(self, parsed_args): info.update(endpoint._info) info['service_name'] = get_service_name(service) info['service_type'] = service.type - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py index 66bd164d0d..cbe27edb4b 100644 --- a/openstackclient/identity/v3/endpoint_group.py +++ b/openstackclient/identity/v3/endpoint_group.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -129,7 +128,7 @@ def take_action(self, parsed_args): info = {} endpoint_group._info.pop('links') info.update(endpoint_group._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteEndpointGroup(command.Command): @@ -321,4 +320,4 @@ def take_action(self, parsed_args): info = {} endpoint_group._info.pop('links') info.update(endpoint_group._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 6429d934c1..0929469e7e 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -68,7 +67,7 @@ def take_action(self, parsed_args): info['identity_provider'] = parsed_args.identity_provider info['mapping'] = info.pop('mapping_id') info.pop('links', None) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteProtocol(command.Command): @@ -175,7 +174,7 @@ def take_action(self, parsed_args): # user. info['identity_provider'] = parsed_args.identity_provider info['mapping'] = info.pop('mapping_id') - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class ShowProtocol(command.ShowOne): @@ -205,4 +204,4 @@ def take_action(self, parsed_args): info = dict(protocol._info) info['mapping'] = info.pop('mapping_id') info.pop('links', None) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 02eeadd63f..46c3142cdd 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -182,7 +181,7 @@ def take_action(self, parsed_args): raise group._info.pop('links') - return zip(*sorted(six.iteritems(group._info))) + return zip(*sorted(group._info.items())) class DeleteGroup(command.Command): @@ -405,4 +404,4 @@ def take_action(self, parsed_args): domain_name_or_id=parsed_args.domain) group._info.pop('links') - return zip(*sorted(six.iteritems(group._info))) + return zip(*sorted(group._info.items())) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index b331518213..2b2d9d11bd 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -106,7 +105,7 @@ def take_action(self, parsed_args): idp._info.pop('links', None) remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) idp._info['remote_ids'] = remote_ids - return zip(*sorted(six.iteritems(idp._info))) + return zip(*sorted(idp._info.items())) class DeleteIdentityProvider(command.Command): @@ -248,4 +247,4 @@ def take_action(self, parsed_args): idp._info.pop('links', None) remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) idp._info['remote_ids'] = remote_ids - return zip(*sorted(six.iteritems(idp._info))) + return zip(*sorted(idp._info.items())) diff --git a/openstackclient/identity/v3/implied_role.py b/openstackclient/identity/v3/implied_role.py index 4e3df88ac1..054f30285c 100644 --- a/openstackclient/identity/v3/implied_role.py +++ b/openstackclient/identity/v3/implied_role.py @@ -18,7 +18,6 @@ import logging from osc_lib.command import command -import six from openstackclient.i18n import _ @@ -75,7 +74,7 @@ def take_action(self, parsed_args): prior_role_id, implied_role_id) response._info.pop('links', None) return zip(*sorted([(k, v['id']) - for k, v in six.iteritems(response._info)])) + for k, v in response._info.items()])) class DeleteImpliedRole(command.Command): diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py index f2af81e9f0..b155cbd863 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as common_utils @@ -102,7 +101,7 @@ def take_action(self, parsed_args): ) limit._info.pop('links', None) - return zip(*sorted(six.iteritems(limit._info))) + return zip(*sorted(limit._info.items())) class ListLimit(command.Lister): @@ -198,7 +197,7 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity limit = identity_client.limits.get(parsed_args.limit_id) limit._info.pop('links', None) - return zip(*sorted(six.iteritems(limit._info))) + return zip(*sorted(limit._info.items())) class SetLimit(command.ShowOne): @@ -236,7 +235,7 @@ def take_action(self, parsed_args): limit._info.pop('links', None) - return zip(*sorted(six.iteritems(limit._info))) + return zip(*sorted(limit._info.items())) class DeleteLimit(command.Command): diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index e729c410c8..7d40a2b7f9 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -107,7 +106,7 @@ def take_action(self, parsed_args): rules=rules) mapping._info.pop('links', None) - return zip(*sorted(six.iteritems(mapping._info))) + return zip(*sorted(mapping._info.items())) class DeleteMapping(command.Command): @@ -202,4 +201,4 @@ def take_action(self, parsed_args): mapping = identity_client.federation.mappings.get(parsed_args.mapping) mapping._info.pop('links', None) - return zip(*sorted(six.iteritems(mapping._info))) + return zip(*sorted(mapping._info.items())) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 3b6441959b..45674210f0 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -57,7 +56,7 @@ def take_action(self, parsed_args): policy._info.pop('links') policy._info.update({'rules': policy._info.pop('blob')}) - return zip(*sorted(six.iteritems(policy._info))) + return zip(*sorted(policy._info.items())) class DeletePolicy(command.Command): @@ -176,4 +175,4 @@ def take_action(self, parsed_args): policy._info.pop('links') policy._info.update({'rules': policy._info.pop('blob')}) - return zip(*sorted(six.iteritems(policy._info))) + return zip(*sorted(policy._info.items())) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 073fb6df11..9ecc70ef4e 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -22,7 +22,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -124,7 +123,7 @@ def take_action(self, parsed_args): raise project._info.pop('links') - return zip(*sorted(six.iteritems(project._info))) + return zip(*sorted(project._info.items())) class DeleteProject(command.Command): @@ -401,4 +400,4 @@ def take_action(self, parsed_args): subtree_as_ids=parsed_args.children) project._info.pop('links') - return zip(*sorted(six.iteritems(project._info))) + return zip(*sorted(project._info.items())) diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index 69c8b5066a..20ee073c3b 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -62,7 +61,7 @@ def take_action(self, parsed_args): region._info['region'] = region._info.pop('id') region._info['parent_region'] = region._info.pop('parent_region_id') region._info.pop('links', None) - return zip(*sorted(six.iteritems(region._info))) + return zip(*sorted(region._info.items())) class DeleteRegion(command.Command): @@ -181,4 +180,4 @@ def take_action(self, parsed_args): region._info['region'] = region._info.pop('id') region._info['parent_region'] = region._info.pop('parent_region_id') region._info.pop('links', None) - return zip(*sorted(six.iteritems(region._info))) + return zip(*sorted(region._info.items())) diff --git a/openstackclient/identity/v3/registered_limit.py b/openstackclient/identity/v3/registered_limit.py index 9366ec1efc..53117c71ae 100644 --- a/openstackclient/identity/v3/registered_limit.py +++ b/openstackclient/identity/v3/registered_limit.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as common_utils @@ -92,7 +91,7 @@ def take_action(self, parsed_args): ) registered_limit._info.pop('links', None) - return zip(*sorted(six.iteritems(registered_limit._info))) + return zip(*sorted(registered_limit._info.items())) class DeleteRegisteredLimit(command.Command): @@ -275,7 +274,7 @@ def take_action(self, parsed_args): ) registered_limit._info.pop('links', None) - return zip(*sorted(six.iteritems(registered_limit._info))) + return zip(*sorted(registered_limit._info.items())) class ShowRegisteredLimit(command.ShowOne): @@ -296,4 +295,4 @@ def take_action(self, parsed_args): parsed_args.registered_limit_id ) registered_limit._info.pop('links', None) - return zip(*sorted(six.iteritems(registered_limit._info))) + return zip(*sorted(registered_limit._info.items())) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 0eeddd37fb..986f823fcf 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -21,7 +21,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -211,7 +210,7 @@ def take_action(self, parsed_args): raise role._info.pop('links') - return zip(*sorted(six.iteritems(role._info))) + return zip(*sorted(role._info.items())) class DeleteRole(command.Command): @@ -403,4 +402,4 @@ def take_action(self, parsed_args): domain_id=domain_id) role._info.pop('links') - return zip(*sorted(six.iteritems(role._info))) + return zip(*sorted(role._info.items())) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index ac8d8d9eef..9dc6696251 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -77,7 +76,7 @@ def take_action(self, parsed_args): ) service._info.pop('links') - return zip(*sorted(six.iteritems(service._info))) + return zip(*sorted(service._info.items())) class DeleteService(command.Command): @@ -218,4 +217,4 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) service._info.pop('links') - return zip(*sorted(six.iteritems(service._info))) + return zip(*sorted(service._info.items())) diff --git a/openstackclient/identity/v3/service_provider.py b/openstackclient/identity/v3/service_provider.py index bb2d9917b4..e106c787b4 100644 --- a/openstackclient/identity/v3/service_provider.py +++ b/openstackclient/identity/v3/service_provider.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -83,7 +82,7 @@ def take_action(self, parsed_args): sp_url=parsed_args.service_provider_url) sp._info.pop('links', None) - return zip(*sorted(six.iteritems(sp._info))) + return zip(*sorted(sp._info.items())) class DeleteServiceProvider(command.Command): @@ -211,4 +210,4 @@ def take_action(self, parsed_args): id=parsed_args.service_provider) service_provider._info.pop('links', None) - return zip(*sorted(six.iteritems(service_provider._info))) + return zip(*sorted(service_provider._info.items())) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 1933ecad65..f14dd8bc3e 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -62,7 +61,7 @@ def take_action(self, parsed_args): parsed_args.request_key, roles) - return zip(*sorted(six.iteritems(verifier_pin._info))) + return zip(*sorted(verifier_pin._info.items())) class CreateAccessToken(command.ShowOne): @@ -108,7 +107,7 @@ def take_action(self, parsed_args): parsed_args.consumer_key, parsed_args.consumer_secret, parsed_args.request_key, parsed_args.request_secret, parsed_args.verifier) - return zip(*sorted(six.iteritems(access_token._info))) + return zip(*sorted(access_token._info.items())) class CreateRequestToken(command.ShowOne): @@ -160,7 +159,7 @@ def take_action(self, parsed_args): parsed_args.consumer_key, parsed_args.consumer_secret, project.id) - return zip(*sorted(six.iteritems(request_token._info))) + return zip(*sorted(request_token._info.items())) class IssueToken(command.ShowOne): @@ -198,7 +197,7 @@ def take_action(self, parsed_args): # deployment system. When that happens, this will have to relay # scope information and IDs like we do for projects and domains. data['system'] = 'all' - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) class RevokeToken(command.Command): diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index 155063bb39..cd3a65d0da 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -136,7 +135,7 @@ def take_action(self, parsed_args): msg = ' '.join(r['name'] for r in roles) trust._info['roles'] = msg - return zip(*sorted(six.iteritems(trust._info))) + return zip(*sorted(trust._info.items())) class DeleteTrust(command.Command): @@ -213,4 +212,4 @@ def take_action(self, parsed_args): msg = ' '.join(r['name'] for r in roles) trust._info['roles'] = msg - return zip(*sorted(six.iteritems(trust._info))) + return zip(*sorted(trust._info.items())) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 5f4fb544fe..ca85c5d8a8 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -22,7 +22,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -135,7 +134,7 @@ def take_action(self, parsed_args): raise user._info.pop('links') - return zip(*sorted(six.iteritems(user._info))) + return zip(*sorted(user._info.items())) class DeleteUser(command.Command): @@ -486,4 +485,4 @@ def take_action(self, parsed_args): user_str) user._info.pop('links') - return zip(*sorted(six.iteritems(user._info))) + return zip(*sorted(user._info.items())) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index c2dab3eec3..a711a1288b 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -28,7 +28,6 @@ from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -271,7 +270,7 @@ def take_action(self, parsed_args): info.update(image._info) info['properties'] = format_columns.DictColumn( info.get('properties', {})) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteImage(command.Command): @@ -718,4 +717,4 @@ def take_action(self, parsed_args): info['size'] = utils.format_size(info['size']) info['properties'] = format_columns.DictColumn( info.get('properties', {})) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 2b10c3adda..feeb256744 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -110,7 +110,7 @@ def take_action(self, parsed_args): project_id, ) - return zip(*sorted(six.iteritems(image_member))) + return zip(*sorted(image_member.items())) class CreateImage(command.ShowOne): @@ -292,7 +292,7 @@ def take_action(self, parsed_args): # properties should get flattened into the general kwargs if getattr(parsed_args, 'properties', None): - for k, v in six.iteritems(parsed_args.properties): + for k, v in parsed_args.properties.items(): kwargs[k] = str(v) # Handle exclusive booleans with care @@ -417,7 +417,7 @@ def take_action(self, parsed_args): if not info: info = _format_image(image) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class DeleteImage(command.Command): @@ -969,7 +969,7 @@ def take_action(self, parsed_args): # Properties should get flattened into the general kwargs if getattr(parsed_args, 'properties', None): - for k, v in six.iteritems(parsed_args.properties): + for k, v in parsed_args.properties.items(): kwargs[k] = str(v) # Handle exclusive booleans with care @@ -1066,7 +1066,7 @@ def take_action(self, parsed_args): image['size'] = utils.format_size(image['size']) info = _format_image(image) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(info.items())) class UnsetImage(command.Command): diff --git a/openstackclient/network/sdk_utils.py b/openstackclient/network/sdk_utils.py index 9f0856175d..af9c74f944 100644 --- a/openstackclient/network/sdk_utils.py +++ b/openstackclient/network/sdk_utils.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - def get_osc_show_columns_for_sdk_resource( sdk_resource, @@ -44,7 +42,7 @@ def get_osc_show_columns_for_sdk_resource( for col_name in invisible_columns: if col_name in display_columns: display_columns.remove(col_name) - for sdk_attr, osc_attr in six.iteritems(osc_column_map): + for sdk_attr, osc_attr in osc_column_map.items(): if sdk_attr in display_columns: attr_map[osc_attr] = sdk_attr display_columns.remove(sdk_attr) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index f03bcc1c0b..2cdae642e5 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -61,7 +61,7 @@ def _is_prop_empty(columns, props, prop_name): def _exchange_dict_keys_with_values(orig_dict): updated_dict = dict() - for k, v in six.iteritems(orig_dict): + for k, v in orig_dict.items(): k = [k] if not updated_dict.get(v): updated_dict[v] = k @@ -80,7 +80,7 @@ def _update_available_from_props(columns, props): def _update_used_from_props(columns, props): index_used = columns.index('used') updated_used = _exchange_dict_keys_with_values(props[index_used]) - for k, v in six.iteritems(updated_used): + for k, v in updated_used.items(): updated_used[k] = list(_get_ranges(v)) props = _hack_tuple_value_update_by_index( props, index_used, updated_used) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 9f0ca0a1f4..24f71ab678 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -19,7 +19,6 @@ from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -34,7 +33,7 @@ def _format_network_security_group_rules(sg_rules): # rules, trim keys with caller known (e.g. security group and tenant ID) # or empty values. for sg_rule in sg_rules: - empty_keys = [k for k, v in six.iteritems(sg_rule) if not v] + empty_keys = [k for k, v in sg_rule.items() if not v] for key in empty_keys: sg_rule.pop(key) sg_rule.pop('security_group_id', None) diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index a38587fa52..f48478ea74 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -20,7 +20,6 @@ from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -39,7 +38,7 @@ def _format_security_group_rule_show(obj): data = network_utils.transform_compute_security_group_rule(obj) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) def _format_network_port_range(rule): diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py index 95be8132c9..d6bc9fd780 100644 --- a/openstackclient/object/v1/account.py +++ b/openstackclient/object/v1/account.py @@ -16,7 +16,6 @@ from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command -import six from openstackclient.i18n import _ @@ -50,7 +49,7 @@ def take_action(self, parsed_args): if 'properties' in data: data['properties'] = format_columns.DictColumn( data.pop('properties')) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) class UnsetAccount(command.Command): diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 02e8d27780..47ca5bc1da 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -21,7 +21,6 @@ from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -233,7 +232,7 @@ def take_action(self, parsed_args): if 'properties' in data: data['properties'] = format_columns.DictColumn(data['properties']) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) class UnsetContainer(command.Command): diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 3747e19e47..01e537eef7 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -22,7 +22,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -287,7 +286,7 @@ def take_action(self, parsed_args): if 'properties' in data: data['properties'] = format_columns.DictColumn(data['properties']) - return zip(*sorted(six.iteritems(data))) + return zip(*sorted(data.items())) class UnsetObject(command.Command): diff --git a/openstackclient/tests/unit/common/test_availability_zone.py b/openstackclient/tests/unit/common/test_availability_zone.py index 6c7adc43b6..49e5904dbd 100644 --- a/openstackclient/tests/unit/common/test_availability_zone.py +++ b/openstackclient/tests/unit/common/test_availability_zone.py @@ -13,8 +13,6 @@ import mock -import six - from openstackclient.common import availability_zone from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit import fakes @@ -31,8 +29,8 @@ def _build_compute_az_datalist(compute_az, long_datalist=False): 'available', ) else: - for host, services in six.iteritems(compute_az.hosts): - for service, state in six.iteritems(services): + for host, services in compute_az.hosts.items(): + for service, state in services.items(): datalist += ( compute_az.zoneName, 'available', diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index bca457e457..59cbbe1055 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -200,7 +200,7 @@ def __init__(self, manager=None, info=None, loaded=False, methods=None): self._loaded = loaded def _add_details(self, info): - for (k, v) in six.iteritems(info): + for (k, v) in info.items(): setattr(self, k, v) def _add_methods(self, methods): @@ -211,7 +211,7 @@ def _add_methods(self, methods): @value. When users access the attribute with (), @value will be returned, which looks like a function call. """ - for (name, ret) in six.iteritems(methods): + for (name, ret) in methods.items(): method = mock.Mock(return_value=ret) setattr(self, name, method) diff --git a/openstackclient/volume/v1/qos_specs.py b/openstackclient/volume/v1/qos_specs.py index 0b6a7fa0ca..79dff1c677 100644 --- a/openstackclient/volume/v1/qos_specs.py +++ b/openstackclient/volume/v1/qos_specs.py @@ -22,7 +22,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -99,7 +98,7 @@ def take_action(self, parsed_args): {'properties': format_columns.DictColumn(qos_spec._info.pop('specs'))} ) - return zip(*sorted(six.iteritems(qos_spec._info))) + return zip(*sorted(qos_spec._info.items())) class DeleteQos(command.Command): @@ -273,7 +272,7 @@ def take_action(self, parsed_args): {'properties': format_columns.DictColumn(qos_spec._info.pop('specs'))}) - return zip(*sorted(six.iteritems(qos_spec._info))) + return zip(*sorted(qos_spec._info.items())) class UnsetQos(command.Command): diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 36c7ef8985..460bd85a8b 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -25,7 +25,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -253,7 +252,7 @@ def take_action(self, parsed_args): volume._info, parsed_args.columns, {'display_name': 'name'} ) - return zip(*sorted(six.iteritems(volume_info))) + return zip(*sorted(volume_info.items())) class DeleteVolume(command.Command): @@ -614,7 +613,7 @@ def take_action(self, parsed_args): volume._info, parsed_args.columns, {'display_name': 'name'} ) - return zip(*sorted(six.iteritems(volume_info))) + return zip(*sorted(volume_info.items())) class UnsetVolume(command.Command): diff --git a/openstackclient/volume/v1/volume_backup.py b/openstackclient/volume/v1/volume_backup.py index 2daa23cb92..1a83a3c040 100644 --- a/openstackclient/volume/v1/volume_backup.py +++ b/openstackclient/volume/v1/volume_backup.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -98,7 +97,7 @@ def take_action(self, parsed_args): ) backup._info.pop('links') - return zip(*sorted(six.iteritems(backup._info))) + return zip(*sorted(backup._info.items())) class DeleteVolumeBackup(command.Command): @@ -263,4 +262,4 @@ def take_action(self, parsed_args): backup = utils.find_resource(volume_client.backups, parsed_args.backup) backup._info.pop('links') - return zip(*sorted(six.iteritems(backup._info))) + return zip(*sorted(backup._info.items())) diff --git a/openstackclient/volume/v1/volume_snapshot.py b/openstackclient/volume/v1/volume_snapshot.py index 50f81771a0..966db48ff1 100644 --- a/openstackclient/volume/v1/volume_snapshot.py +++ b/openstackclient/volume/v1/volume_snapshot.py @@ -25,7 +25,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -110,7 +109,7 @@ def take_action(self, parsed_args): format_columns.DictColumn(snapshot._info.pop('metadata'))} ) - return zip(*sorted(six.iteritems(snapshot._info))) + return zip(*sorted(snapshot._info.items())) class DeleteVolumeSnapshot(command.Command): @@ -342,7 +341,7 @@ def take_action(self, parsed_args): format_columns.DictColumn(snapshot._info.pop('metadata'))} ) - return zip(*sorted(six.iteritems(snapshot._info))) + return zip(*sorted(snapshot._info.items())) class UnsetVolumeSnapshot(command.Command): diff --git a/openstackclient/volume/v1/volume_transfer_request.py b/openstackclient/volume/v1/volume_transfer_request.py index 6f79658e5a..971b9ab592 100644 --- a/openstackclient/volume/v1/volume_transfer_request.py +++ b/openstackclient/volume/v1/volume_transfer_request.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -67,7 +66,7 @@ def take_action(self, parsed_args): ) transfer_accept._info.pop("links", None) - return zip(*sorted(six.iteritems(transfer_accept._info))) + return zip(*sorted(transfer_accept._info.items())) class CreateTransferRequest(command.ShowOne): @@ -99,7 +98,7 @@ def take_action(self, parsed_args): ) volume_transfer_request._info.pop("links", None) - return zip(*sorted(six.iteritems(volume_transfer_request._info))) + return zip(*sorted(volume_transfer_request._info.items())) class DeleteTransferRequest(command.Command): @@ -189,4 +188,4 @@ def take_action(self, parsed_args): ) volume_transfer_request._info.pop("links", None) - return zip(*sorted(six.iteritems(volume_transfer_request._info))) + return zip(*sorted(volume_transfer_request._info.items())) diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index e744e92fdb..4f015d13f6 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -24,7 +24,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -162,7 +161,7 @@ def take_action(self, parsed_args): {'encryption': format_columns.DictColumn(encryption._info)}) volume_type._info.pop("os-volume-type-access:is_public", None) - return zip(*sorted(six.iteritems(volume_type._info))) + return zip(*sorted(volume_type._info.items())) class DeleteVolumeType(command.Command): @@ -388,7 +387,7 @@ def take_action(self, parsed_args): LOG.error(_("Failed to display the encryption information " "of this volume type: %s"), e) volume_type._info.pop("os-volume-type-access:is_public", None) - return zip(*sorted(six.iteritems(volume_type._info))) + return zip(*sorted(volume_type._info.items())) class UnsetVolumeType(command.Command): diff --git a/openstackclient/volume/v2/backup_record.py b/openstackclient/volume/v2/backup_record.py index f491803272..64ff4f67ad 100644 --- a/openstackclient/volume/v2/backup_record.py +++ b/openstackclient/volume/v2/backup_record.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import utils -import six from openstackclient.i18n import _ @@ -51,7 +50,7 @@ def take_action(self, parsed_args): backup_data['Backup Service'] = backup_data.pop('backup_service') backup_data['Metadata'] = backup_data.pop('backup_url') - return zip(*sorted(six.iteritems(backup_data))) + return zip(*sorted(backup_data.items())) class ImportBackupRecord(command.ShowOne): @@ -79,4 +78,4 @@ def take_action(self, parsed_args): parsed_args.backup_service, parsed_args.backup_metadata) backup_data.pop('links', None) - return zip(*sorted(six.iteritems(backup_data))) + return zip(*sorted(backup_data.items())) diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 26dd8ffca1..c50a1b5bb2 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -161,7 +160,7 @@ def take_action(self, parsed_args): ) ) - return zip(*sorted(six.iteritems(consistency_group._info))) + return zip(*sorted(consistency_group._info.items())) class DeleteConsistencyGroup(command.Command): @@ -335,4 +334,4 @@ def take_action(self, parsed_args): consistency_group = utils.find_resource( volume_client.consistencygroups, parsed_args.consistency_group) - return zip(*sorted(six.iteritems(consistency_group._info))) + return zip(*sorted(consistency_group._info.items())) diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py index 540deb0187..3df66e6962 100644 --- a/openstackclient/volume/v2/consistency_group_snapshot.py +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -68,7 +67,7 @@ def take_action(self, parsed_args): description=parsed_args.description, ) - return zip(*sorted(six.iteritems(consistency_group_snapshot._info))) + return zip(*sorted(consistency_group_snapshot._info.items())) class DeleteConsistencyGroupSnapshot(command.Command): @@ -187,4 +186,4 @@ def take_action(self, parsed_args): consistency_group_snapshot = utils.find_resource( volume_client.cgsnapshots, parsed_args.consistency_group_snapshot) - return zip(*sorted(six.iteritems(consistency_group_snapshot._info))) + return zip(*sorted(consistency_group_snapshot._info.items())) diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index 3037d34ae8..e6e6b9f8b7 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -22,7 +22,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -100,7 +99,7 @@ def take_action(self, parsed_args): {'properties': format_columns.DictColumn(qos_spec._info.pop('specs'))} ) - return zip(*sorted(six.iteritems(qos_spec._info))) + return zip(*sorted(qos_spec._info.items())) class DeleteQos(command.Command): @@ -275,7 +274,7 @@ def take_action(self, parsed_args): {'properties': format_columns.DictColumn(qos_spec._info.pop('specs'))}) - return zip(*sorted(six.iteritems(qos_spec._info))) + return zip(*sorted(qos_spec._info.items())) class UnsetQos(command.Command): diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 17ccd3d31e..4dde1340a7 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -25,7 +25,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -252,7 +251,7 @@ def take_action(self, parsed_args): } ) volume._info.pop("links", None) - return zip(*sorted(six.iteritems(volume._info))) + return zip(*sorted(volume._info.items())) class DeleteVolume(command.Command): @@ -751,7 +750,7 @@ def take_action(self, parsed_args): # Remove key links from being displayed volume._info.pop("links", None) - return zip(*sorted(six.iteritems(volume._info))) + return zip(*sorted(volume._info.items())) class UnsetVolume(command.Command): diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index 4d0d54c1b5..c336f6c9ac 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -120,7 +119,7 @@ def take_action(self, parsed_args): snapshot_id=snapshot_id, ) backup._info.pop("links", None) - return zip(*sorted(six.iteritems(backup._info))) + return zip(*sorted(backup._info.items())) class DeleteVolumeBackup(command.Command): @@ -289,7 +288,7 @@ def take_action(self, parsed_args): parsed_args.volume) backup = volume_client.restores.restore(backup.id, destination_volume.id) - return zip(*sorted(six.iteritems(backup._info))) + return zip(*sorted(backup._info.items())) class SetVolumeBackup(command.Command): @@ -371,4 +370,4 @@ def take_action(self, parsed_args): backup = utils.find_resource(volume_client.backups, parsed_args.backup) backup._info.pop("links", None) - return zip(*sorted(six.iteritems(backup._info))) + return zip(*sorted(backup._info.items())) diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index 2b26ae323b..edacf68364 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -24,7 +24,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -140,7 +139,7 @@ def take_action(self, parsed_args): {'properties': format_columns.DictColumn(snapshot._info.pop('metadata'))} ) - return zip(*sorted(six.iteritems(snapshot._info))) + return zip(*sorted(snapshot._info.items())) class DeleteVolumeSnapshot(command.Command): @@ -426,7 +425,7 @@ def take_action(self, parsed_args): {'properties': format_columns.DictColumn(snapshot._info.pop('metadata'))} ) - return zip(*sorted(six.iteritems(snapshot._info))) + return zip(*sorted(snapshot._info.items())) class UnsetVolumeSnapshot(command.Command): diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index 4c4741bc30..2a1ace1f42 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -19,7 +19,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ @@ -64,7 +63,7 @@ def take_action(self, parsed_args): ) transfer_accept._info.pop("links", None) - return zip(*sorted(six.iteritems(transfer_accept._info))) + return zip(*sorted(transfer_accept._info.items())) class CreateTransferRequest(command.ShowOne): @@ -96,7 +95,7 @@ def take_action(self, parsed_args): ) volume_transfer_request._info.pop("links", None) - return zip(*sorted(six.iteritems(volume_transfer_request._info))) + return zip(*sorted(volume_transfer_request._info.items())) class DeleteTransferRequest(command.Command): @@ -186,4 +185,4 @@ def take_action(self, parsed_args): ) volume_transfer_request._info.pop("links", None) - return zip(*sorted(six.iteritems(volume_transfer_request._info))) + return zip(*sorted(volume_transfer_request._info.items())) diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 54b1f49719..483e6dd3b5 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -235,7 +234,7 @@ def take_action(self, parsed_args): {'encryption': format_columns.DictColumn(encryption._info)}) volume_type._info.pop("os-volume-type-access:is_public", None) - return zip(*sorted(six.iteritems(volume_type._info))) + return zip(*sorted(volume_type._info.items())) class DeleteVolumeType(command.Command): @@ -553,7 +552,7 @@ def take_action(self, parsed_args): LOG.error(_("Failed to display the encryption information " "of this volume type: %s"), e) volume_type._info.pop("os-volume-type-access:is_public", None) - return zip(*sorted(six.iteritems(volume_type._info))) + return zip(*sorted(volume_type._info.items())) class UnsetVolumeType(command.Command): From 90ca67bdae937244bcb0f3ee9bd89e89dcf128d9 Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 10 Jan 2020 11:43:07 -0600 Subject: [PATCH 0174/1120] Raise hacking to more recent 2.0.0 Change-Id: I3cf36ed4d8fb5d003acae762820a8d80f75a11e9 Signed-off-by: Sean McGinnis --- lower-constraints.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index ad911e771d..6bc10e57bc 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -29,7 +29,7 @@ gitdb==0.6.4 GitPython==1.0.1 gnocchiclient==3.3.1 greenlet==0.4.10 -hacking==1.1.0 +hacking==2.0.0 httplib2==0.9.1 idna==2.6 iso8601==0.1.11 diff --git a/test-requirements.txt b/test-requirements.txt index 0f59e46faa..15b3e9c495 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # 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. -hacking>=1.1.0,<1.2.0 # Apache-2.0 +hacking>=2.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD flake8-import-order==0.13 # LGPLv3 From 69db9fe73c81bc6341c37948365163902b6437ee Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 10 Jan 2020 14:21:47 -0600 Subject: [PATCH 0175/1120] Raise flake8-import-order version to latest We had this library capped at a release that is a few years old. Now that we have dropped py2 testing, we can pick up the latest version. This uncovered a few things to clean up. Mostly the fact that mock is now a part of the StdLib unittest since Python 3.3. Change-Id: I27484dd4c25378413ff16e97a35a1a46062357bc Signed-off-by: Sean McGinnis --- openstackclient/api/api.py | 3 +-- openstackclient/tests/unit/api/test_object_store_v1.py | 2 +- openstackclient/tests/unit/common/test_availability_zone.py | 2 +- openstackclient/tests/unit/common/test_command.py | 2 +- openstackclient/tests/unit/common/test_configuration.py | 2 +- openstackclient/tests/unit/common/test_extension.py | 2 +- openstackclient/tests/unit/common/test_logs.py | 3 +-- openstackclient/tests/unit/common/test_module.py | 2 +- openstackclient/tests/unit/common/test_project_purge.py | 2 +- openstackclient/tests/unit/common/test_quota.py | 2 +- openstackclient/tests/unit/compute/v2/fakes.py | 2 +- openstackclient/tests/unit/compute/v2/test_agent.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_aggregate.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_console.py | 2 +- openstackclient/tests/unit/compute/v2/test_flavor.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_host.py | 2 +- openstackclient/tests/unit/compute/v2/test_keypair.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_server.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_server_backup.py | 2 +- openstackclient/tests/unit/compute/v2/test_server_group.py | 2 +- openstackclient/tests/unit/compute/v2/test_server_image.py | 2 +- openstackclient/tests/unit/compute/v2/test_service.py | 5 +++-- openstackclient/tests/unit/compute/v2/test_usage.py | 2 +- openstackclient/tests/unit/fakes.py | 2 +- openstackclient/tests/unit/identity/v2_0/fakes.py | 2 +- openstackclient/tests/unit/identity/v2_0/test_catalog.py | 2 +- openstackclient/tests/unit/identity/v2_0/test_project.py | 2 +- openstackclient/tests/unit/identity/v2_0/test_role.py | 2 +- .../tests/unit/identity/v2_0/test_role_assignment.py | 2 +- openstackclient/tests/unit/identity/v2_0/test_token.py | 2 +- openstackclient/tests/unit/identity/v2_0/test_user.py | 2 +- openstackclient/tests/unit/identity/v3/fakes.py | 2 +- .../tests/unit/identity/v3/test_application_credential.py | 2 +- openstackclient/tests/unit/identity/v3/test_catalog.py | 2 +- openstackclient/tests/unit/identity/v3/test_credential.py | 4 ++-- .../tests/unit/identity/v3/test_endpoint_group.py | 2 +- openstackclient/tests/unit/identity/v3/test_group.py | 4 ++-- .../tests/unit/identity/v3/test_identity_provider.py | 3 +-- openstackclient/tests/unit/identity/v3/test_mappings.py | 2 +- openstackclient/tests/unit/identity/v3/test_project.py | 4 ++-- openstackclient/tests/unit/identity/v3/test_role.py | 2 +- .../tests/unit/identity/v3/test_role_assignment.py | 3 +-- openstackclient/tests/unit/identity/v3/test_token.py | 2 +- openstackclient/tests/unit/identity/v3/test_trust.py | 2 +- openstackclient/tests/unit/identity/v3/test_user.py | 2 +- openstackclient/tests/unit/image/v1/fakes.py | 3 +-- openstackclient/tests/unit/image/v1/test_image.py | 3 +-- openstackclient/tests/unit/image/v2/fakes.py | 2 +- openstackclient/tests/unit/image/v2/test_image.py | 2 +- openstackclient/tests/unit/integ/cli/test_shell.py | 2 +- openstackclient/tests/unit/network/test_common.py | 2 +- openstackclient/tests/unit/network/v2/fakes.py | 3 +-- openstackclient/tests/unit/network/v2/test_address_scope.py | 4 ++-- .../tests/unit/network/v2/test_floating_ip_compute.py | 4 ++-- .../tests/unit/network/v2/test_floating_ip_network.py | 4 ++-- .../tests/unit/network/v2/test_floating_ip_pool_compute.py | 2 +- .../unit/network/v2/test_floating_ip_port_forwarding.py | 4 ++-- .../tests/unit/network/v2/test_ip_availability.py | 2 +- openstackclient/tests/unit/network/v2/test_network.py | 5 ++--- openstackclient/tests/unit/network/v2/test_network_agent.py | 4 ++-- .../unit/network/v2/test_network_auto_allocated_topology.py | 2 +- .../tests/unit/network/v2/test_network_compute.py | 4 ++-- openstackclient/tests/unit/network/v2/test_network_flavor.py | 2 +- .../tests/unit/network/v2/test_network_flavor_profile.py | 2 +- openstackclient/tests/unit/network/v2/test_network_meter.py | 4 ++-- .../tests/unit/network/v2/test_network_meter_rule.py | 4 ++-- .../tests/unit/network/v2/test_network_qos_policy.py | 4 ++-- .../tests/unit/network/v2/test_network_qos_rule.py | 2 +- .../tests/unit/network/v2/test_network_qos_rule_type.py | 2 +- openstackclient/tests/unit/network/v2/test_network_rbac.py | 4 ++-- .../tests/unit/network/v2/test_network_segment.py | 4 ++-- .../tests/unit/network/v2/test_network_segment_range.py | 4 ++-- .../tests/unit/network/v2/test_network_service_provider.py | 2 +- openstackclient/tests/unit/network/v2/test_port.py | 4 ++-- openstackclient/tests/unit/network/v2/test_router.py | 4 ++-- .../tests/unit/network/v2/test_security_group_compute.py | 4 ++-- .../tests/unit/network/v2/test_security_group_network.py | 4 ++-- .../unit/network/v2/test_security_group_rule_compute.py | 4 ++-- .../unit/network/v2/test_security_group_rule_network.py | 4 ++-- openstackclient/tests/unit/network/v2/test_subnet.py | 4 ++-- openstackclient/tests/unit/network/v2/test_subnet_pool.py | 5 ++--- openstackclient/tests/unit/object/v1/fakes.py | 3 +-- openstackclient/tests/unit/object/v1/test_container.py | 3 +-- openstackclient/tests/unit/object/v1/test_object.py | 3 +-- openstackclient/tests/unit/object/v1/test_object_all.py | 2 +- openstackclient/tests/unit/test_shell.py | 2 +- openstackclient/tests/unit/utils.py | 3 +-- openstackclient/tests/unit/volume/test_find_resource.py | 2 +- openstackclient/tests/unit/volume/v1/fakes.py | 3 +-- openstackclient/tests/unit/volume/v1/test_qos_specs.py | 5 ++--- .../tests/unit/volume/v1/test_transfer_request.py | 4 ++-- openstackclient/tests/unit/volume/v1/test_type.py | 4 ++-- openstackclient/tests/unit/volume/v1/test_volume.py | 5 ++--- openstackclient/tests/unit/volume/v1/test_volume_backup.py | 4 ++-- openstackclient/tests/unit/volume/v2/fakes.py | 3 +-- .../tests/unit/volume/v2/test_consistency_group.py | 4 ++-- .../tests/unit/volume/v2/test_consistency_group_snapshot.py | 2 +- openstackclient/tests/unit/volume/v2/test_qos_specs.py | 5 ++--- .../tests/unit/volume/v2/test_transfer_request.py | 4 ++-- openstackclient/tests/unit/volume/v2/test_type.py | 4 ++-- openstackclient/tests/unit/volume/v2/test_volume.py | 5 ++--- openstackclient/tests/unit/volume/v2/test_volume_backup.py | 4 ++-- test-requirements.txt | 2 +- 103 files changed, 145 insertions(+), 163 deletions(-) diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 7e2fe38f0e..d4772f94c3 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -13,11 +13,10 @@ """Base API Library""" -import simplejson as json - from keystoneauth1 import exceptions as ks_exceptions from keystoneauth1 import session as ks_session from osc_lib import exceptions +import simplejson as json from openstackclient.i18n import _ diff --git a/openstackclient/tests/unit/api/test_object_store_v1.py b/openstackclient/tests/unit/api/test_object_store_v1.py index 74b62493db..1c55be7192 100644 --- a/openstackclient/tests/unit/api/test_object_store_v1.py +++ b/openstackclient/tests/unit/api/test_object_store_v1.py @@ -13,7 +13,7 @@ """Object Store v1 API Library Tests""" -import mock +from unittest import mock from keystoneauth1 import session from requests_mock.contrib import fixture diff --git a/openstackclient/tests/unit/common/test_availability_zone.py b/openstackclient/tests/unit/common/test_availability_zone.py index 49e5904dbd..8733b51014 100644 --- a/openstackclient/tests/unit/common/test_availability_zone.py +++ b/openstackclient/tests/unit/common/test_availability_zone.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.common import availability_zone from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes diff --git a/openstackclient/tests/unit/common/test_command.py b/openstackclient/tests/unit/common/test_command.py index 6ddb7c1220..4fde5301d5 100644 --- a/openstackclient/tests/unit/common/test_command.py +++ b/openstackclient/tests/unit/common/test_command.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from osc_lib.command import command from osc_lib import exceptions diff --git a/openstackclient/tests/unit/common/test_configuration.py b/openstackclient/tests/unit/common/test_configuration.py index e10522b99c..bdd3debff5 100644 --- a/openstackclient/tests/unit/common/test_configuration.py +++ b/openstackclient/tests/unit/common/test_configuration.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.common import configuration from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/common/test_extension.py b/openstackclient/tests/unit/common/test_extension.py index 87c62da499..5093cbbb09 100644 --- a/openstackclient/tests/unit/common/test_extension.py +++ b/openstackclient/tests/unit/common/test_extension.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.common import extension from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes diff --git a/openstackclient/tests/unit/common/test_logs.py b/openstackclient/tests/unit/common/test_logs.py index 421234d62c..0e7105619d 100644 --- a/openstackclient/tests/unit/common/test_logs.py +++ b/openstackclient/tests/unit/common/test_logs.py @@ -15,8 +15,7 @@ # or Jun 2017. import logging - -import mock +from unittest import mock from osc_lib import logs diff --git a/openstackclient/tests/unit/common/test_module.py b/openstackclient/tests/unit/common/test_module.py index 2491d63965..d2e8293fa3 100644 --- a/openstackclient/tests/unit/common/test_module.py +++ b/openstackclient/tests/unit/common/test_module.py @@ -15,7 +15,7 @@ """Test module module""" -import mock +from unittest import mock from openstackclient.common import module as osc_module from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/common/test_project_purge.py b/openstackclient/tests/unit/common/test_project_purge.py index 6e8ce188f7..adc48ce26f 100644 --- a/openstackclient/tests/unit/common/test_project_purge.py +++ b/openstackclient/tests/unit/common/test_project_purge.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from osc_lib import exceptions diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 297452a2dc..bd59ca77fe 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -11,8 +11,8 @@ # under the License. import copy +from unittest import mock -import mock from osc_lib import exceptions from openstackclient.common import quota diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 7357e1430a..6e12f735d4 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -14,9 +14,9 @@ # import copy +from unittest import mock import uuid -import mock from novaclient import api_versions from openstackclient.api import compute_v2 diff --git a/openstackclient/tests/unit/compute/v2/test_agent.py b/openstackclient/tests/unit/compute/v2/test_agent.py index 169940e29c..c6d4f2b655 100644 --- a/openstackclient/tests/unit/compute/v2/test_agent.py +++ b/openstackclient/tests/unit/compute/v2/test_agent.py @@ -13,8 +13,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index 0937047cf0..cd0c1525ee 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -13,8 +13,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/compute/v2/test_console.py b/openstackclient/tests/unit/compute/v2/test_console.py index 3c708aaee3..99a14f048d 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.compute.v2 import console from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index a112fc1fae..fe7ce17481 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -13,8 +13,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call import novaclient from osc_lib import exceptions diff --git a/openstackclient/tests/unit/compute/v2/test_host.py b/openstackclient/tests/unit/compute/v2/test_host.py index 244da41317..4e1b5ad15b 100644 --- a/openstackclient/tests/unit/compute/v2/test_host.py +++ b/openstackclient/tests/unit/compute/v2/test_host.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.compute.v2 import host from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 0e5fb14324..1f3f56f989 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -13,10 +13,10 @@ # under the License. # +from unittest import mock +from unittest.mock import call import uuid -import mock -from mock import call from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c2bac2771f..b3be514724 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -16,9 +16,9 @@ import collections import copy import getpass +from unittest import mock +from unittest.mock import call -import mock -from mock import call from novaclient import api_versions from openstack import exceptions as sdk_exceptions from osc_lib import exceptions diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 24a94531a4..7dd459d823 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index dc924e2421..9cd876ead1 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 02e43129ed..f9d7b10e75 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -import mock +from unittest import mock from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/compute/v2/test_service.py b/openstackclient/tests/unit/compute/v2/test_service.py index 0d663b2e67..7a03683364 100644 --- a/openstackclient/tests/unit/compute/v2/test_service.py +++ b/openstackclient/tests/unit/compute/v2/test_service.py @@ -13,8 +13,9 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call + from novaclient import api_versions from osc_lib import exceptions import six diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py index 76dcc963fb..c08710256b 100644 --- a/openstackclient/tests/unit/compute/v2/test_usage.py +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -12,8 +12,8 @@ # import datetime +from unittest import mock -import mock from novaclient import api_versions from openstackclient.compute.v2 import usage diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index 59cbbe1055..e5476f06bb 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -15,9 +15,9 @@ import json import sys +from unittest import mock from keystoneauth1 import fixture -import mock import requests import six diff --git a/openstackclient/tests/unit/identity/v2_0/fakes.py b/openstackclient/tests/unit/identity/v2_0/fakes.py index 5db9422275..bd76a784a0 100644 --- a/openstackclient/tests/unit/identity/v2_0/fakes.py +++ b/openstackclient/tests/unit/identity/v2_0/fakes.py @@ -14,11 +14,11 @@ # import copy +from unittest import mock import uuid from keystoneauth1 import access from keystoneauth1 import fixture -import mock from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils diff --git a/openstackclient/tests/unit/identity/v2_0/test_catalog.py b/openstackclient/tests/unit/identity/v2_0/test_catalog.py index 362dec0885..1735507469 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_catalog.py +++ b/openstackclient/tests/unit/identity/v2_0/test_catalog.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.identity.v2_0 import catalog from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index 7af7b3946e..cd8c825d15 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import format_columns diff --git a/openstackclient/tests/unit/identity/v2_0/test_role.py b/openstackclient/tests/unit/identity/v2_0/test_role.py index 643d77f600..423884d9f5 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions diff --git a/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py b/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py index 733fda6cb1..3e1231aa30 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py @@ -12,8 +12,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from openstackclient.identity.v2_0 import role_assignment diff --git a/openstackclient/tests/unit/identity/v2_0/test_token.py b/openstackclient/tests/unit/identity/v2_0/test_token.py index dd7f4f4ab4..c079ce6796 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_token.py +++ b/openstackclient/tests/unit/identity/v2_0/test_token.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.identity.v2_0 import token from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index 0a0d4b3651..4308b05d05 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index e9ff068944..c394ab8237 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -15,11 +15,11 @@ import copy import datetime +from unittest import mock import uuid from keystoneauth1 import access from keystoneauth1 import fixture -import mock from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/identity/v3/test_application_credential.py b/openstackclient/tests/unit/identity/v3/test_application_credential.py index e7c8ede826..163aae9db9 100644 --- a/openstackclient/tests/unit/identity/v3/test_application_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_application_credential.py @@ -14,8 +14,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/identity/v3/test_catalog.py b/openstackclient/tests/unit/identity/v3/test_catalog.py index ba076dbddf..3630ccb6a3 100644 --- a/openstackclient/tests/unit/identity/v3/test_catalog.py +++ b/openstackclient/tests/unit/identity/v3/test_catalog.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.identity.v3 import catalog from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes diff --git a/openstackclient/tests/unit/identity/v3/test_credential.py b/openstackclient/tests/unit/identity/v3/test_credential.py index de0306dd68..40596d5874 100644 --- a/openstackclient/tests/unit/identity/v3/test_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_credential.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint_group.py b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py index 6e9da9c78e..c081fa1f11 100644 --- a/openstackclient/tests/unit/identity/v3/test_endpoint_group.py +++ b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.identity.v3 import endpoint_group from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes diff --git a/openstackclient/tests/unit/identity/v3/test_group.py b/openstackclient/tests/unit/identity/v3/test_group.py index 81722631b3..04ba0dbe03 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index 0163c6c8d4..a419a9bcb1 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -13,8 +13,7 @@ # under the License. import copy - -import mock +from unittest import mock from openstackclient.identity.v3 import identity_provider from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/identity/v3/test_mappings.py b/openstackclient/tests/unit/identity/v3/test_mappings.py index 1d8e77d9e8..184bd2a265 100644 --- a/openstackclient/tests/unit/identity/v3/test_mappings.py +++ b/openstackclient/tests/unit/identity/v3/test_mappings.py @@ -13,8 +13,8 @@ # under the License. import copy +from unittest import mock -import mock from osc_lib import exceptions from openstackclient.identity.v3 import mapping diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index db27fedcdf..466bea1867 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -13,8 +13,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 99f3a2de74..ead2cb58bb 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -14,8 +14,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/identity/v3/test_role_assignment.py b/openstackclient/tests/unit/identity/v3/test_role_assignment.py index bff6c56df1..7d38d360b0 100644 --- a/openstackclient/tests/unit/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/unit/identity/v3/test_role_assignment.py @@ -12,8 +12,7 @@ # import copy - -import mock +from unittest import mock from openstackclient.identity.v3 import role_assignment from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/identity/v3/test_token.py b/openstackclient/tests/unit/identity/v3/test_token.py index 7321909f47..adb491b30f 100644 --- a/openstackclient/tests/unit/identity/v3/test_token.py +++ b/openstackclient/tests/unit/identity/v3/test_token.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.identity.v3 import token from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index 1355b9089b..d8cfc59fe8 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -12,8 +12,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 920ee95051..4b14bca05a 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -14,8 +14,8 @@ # import contextlib +from unittest import mock -import mock from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index bbec00fc06..de23223572 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -14,10 +14,9 @@ # import copy +from unittest import mock import uuid -import mock - from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 0997d76546..970b36c68a 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -14,8 +14,7 @@ # import copy - -import mock +from unittest import mock from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index f69a2bc311..655ae341c6 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -15,10 +15,10 @@ import copy import random +from unittest import mock import uuid from glanceclient.v2 import schemas -import mock from osc_lib.cli import format_columns import warlock diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 748a61aaf4..78d857e269 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -14,10 +14,10 @@ # import copy +from unittest import mock from glanceclient.common import utils as glanceclient_utils from glanceclient.v2 import schemas -import mock from osc_lib.cli import format_columns from osc_lib import exceptions import warlock diff --git a/openstackclient/tests/unit/integ/cli/test_shell.py b/openstackclient/tests/unit/integ/cli/test_shell.py index 2598517113..0c98a12942 100644 --- a/openstackclient/tests/unit/integ/cli/test_shell.py +++ b/openstackclient/tests/unit/integ/cli/test_shell.py @@ -11,9 +11,9 @@ # under the License. import copy +from unittest import mock import fixtures -import mock from osc_lib.tests import utils as osc_lib_utils from openstackclient import shell diff --git a/openstackclient/tests/unit/network/test_common.py b/openstackclient/tests/unit/network/test_common.py index 3a20687824..cde321aa23 100644 --- a/openstackclient/tests/unit/network/test_common.py +++ b/openstackclient/tests/unit/network/test_common.py @@ -12,8 +12,8 @@ # import argparse +from unittest import mock -import mock import openstack from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 35f0b1a59c..5809b225e1 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -15,10 +15,9 @@ import copy from random import choice from random import randint +from unittest import mock import uuid -import mock - from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit import utils diff --git a/openstackclient/tests/unit/network/v2/test_address_scope.py b/openstackclient/tests/unit/network/v2/test_address_scope.py index 400671881f..17f13e8351 100644 --- a/openstackclient/tests/unit/network/v2/test_address_scope.py +++ b/openstackclient/tests/unit/network/v2/test_address_scope.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py b/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py index df47e63e91..18212cf707 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py index cbd4da38ca..a98051e769 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py b/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py index 591f58ca4e..3dd99362c1 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from openstackclient.network.v2 import floating_ip_pool from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes 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 b51158be37..ea6cdd266f 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 @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index 21508a8df1..9a712704c6 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -11,7 +11,7 @@ # under the License. # -import mock +from unittest import mock from osc_lib.cli import format_columns diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 5c97c363f5..5f8eed6702 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -12,9 +12,8 @@ # import random - -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 8500d08ea6..3181ee78a1 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py index 1a231160dd..e9687a70d3 100644 --- a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py +++ b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstackclient.network.v2 import network_auto_allocated_topology from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes diff --git a/openstackclient/tests/unit/network/v2/test_network_compute.py b/openstackclient/tests/unit/network/v2/test_network_compute.py index c649401c41..89330fffe9 100644 --- a/openstackclient/tests/unit/network/v2/test_network_compute.py +++ b/openstackclient/tests/unit/network/v2/test_network_compute.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor.py b/openstackclient/tests/unit/network/v2/test_network_flavor.py index 896a172530..010f53d33c 100644 --- a/openstackclient/tests/unit/network/v2/test_network_flavor.py +++ b/openstackclient/tests/unit/network/v2/test_network_flavor.py @@ -14,7 +14,7 @@ # under the License. # -import mock +from unittest import mock from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py b/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py index 91683241c2..fcf24da9f2 100644 --- a/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py +++ b/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_meter.py b/openstackclient/tests/unit/network/v2/test_network_meter.py index 2b96f7a680..4fadcfe1f8 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py index af481793d7..8f8922c020 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py index e7239932a5..d6a78410d4 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py index 5b54d318ac..217e481ea7 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py index 80c52bf72e..08a83fabda 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstackclient.network.v2 import network_qos_rule_type as _qos_rule_type from openstackclient.tests.unit.network.v2 import fakes as network_fakes diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index 96440091c1..078188ceb5 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_segment.py b/openstackclient/tests/unit/network/v2/test_network_segment.py index 0639766d26..6cd948e3bb 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index 22e25df133..89a0c223ce 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -14,8 +14,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_network_service_provider.py b/openstackclient/tests/unit/network/v2/test_network_service_provider.py index 5ba85ddb9a..5e4ddea6cf 100644 --- a/openstackclient/tests/unit/network/v2/test_network_service_provider.py +++ b/openstackclient/tests/unit/network/v2/test_network_service_provider.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from openstackclient.network.v2 import network_service_provider \ as service_provider diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index c30d682f59..d64cc79d56 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -12,9 +12,9 @@ # import argparse +from unittest import mock +from unittest.mock import call -import mock -from mock import call from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 500cfbe5a3..38861b0ad5 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_security_group_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_compute.py index df36006899..b4ddcf803d 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_compute.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index 57698ec586..14d5751436 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py index cf5261b284..5720e30584 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index 49c3d5dbec..0a9522b0bc 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 9903b0423a..e71e1dd6ed 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -11,8 +11,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 2271c08944..eb454646de 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -12,9 +12,8 @@ # import argparse - -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/object/v1/fakes.py b/openstackclient/tests/unit/object/v1/fakes.py index 5d65d1066f..0ed791a5bc 100644 --- a/openstackclient/tests/unit/object/v1/fakes.py +++ b/openstackclient/tests/unit/object/v1/fakes.py @@ -13,9 +13,8 @@ # under the License. # -import six - from keystoneauth1 import session +import six from openstackclient.api import object_store_v1 as object_store from openstackclient.tests.unit import utils diff --git a/openstackclient/tests/unit/object/v1/test_container.py b/openstackclient/tests/unit/object/v1/test_container.py index 39e2d80f4a..7d3cc8d840 100644 --- a/openstackclient/tests/unit/object/v1/test_container.py +++ b/openstackclient/tests/unit/object/v1/test_container.py @@ -14,8 +14,7 @@ # import copy - -import mock +from unittest import mock from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container diff --git a/openstackclient/tests/unit/object/v1/test_object.py b/openstackclient/tests/unit/object/v1/test_object.py index b629937351..fc3073c8bc 100644 --- a/openstackclient/tests/unit/object/v1/test_object.py +++ b/openstackclient/tests/unit/object/v1/test_object.py @@ -14,8 +14,7 @@ # import copy - -import mock +from unittest import mock from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as obj diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py index 08a7534d06..dd587142fa 100644 --- a/openstackclient/tests/unit/object/v1/test_object_all.py +++ b/openstackclient/tests/unit/object/v1/test_object_all.py @@ -12,8 +12,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from requests_mock.contrib import fixture import six diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py index 31675c47e6..94f4f44d24 100644 --- a/openstackclient/tests/unit/test_shell.py +++ b/openstackclient/tests/unit/test_shell.py @@ -15,8 +15,8 @@ import os import sys +from unittest import mock -import mock from osc_lib.tests import utils as osc_lib_test_utils from oslo_utils import importutils import wrapt diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index 8df81a50eb..4f1bc46a98 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -16,12 +16,11 @@ import os +from cliff import columns as cliff_columns import fixtures from six.moves import StringIO import testtools -from cliff import columns as cliff_columns - from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/volume/test_find_resource.py b/openstackclient/tests/unit/volume/test_find_resource.py index 60591eff7f..208f55b94e 100644 --- a/openstackclient/tests/unit/volume/test_find_resource.py +++ b/openstackclient/tests/unit/volume/test_find_resource.py @@ -13,7 +13,7 @@ # under the License. # -import mock +from unittest import mock from cinderclient.v3 import volume_snapshots from cinderclient.v3 import volumes diff --git a/openstackclient/tests/unit/volume/v1/fakes.py b/openstackclient/tests/unit/volume/v1/fakes.py index de9c724f1e..adb775edf2 100644 --- a/openstackclient/tests/unit/volume/v1/fakes.py +++ b/openstackclient/tests/unit/volume/v1/fakes.py @@ -15,10 +15,9 @@ import copy import random +from unittest import mock import uuid -import mock - from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.unit import utils diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index 11dc80844a..83c533b6f0 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -14,9 +14,8 @@ # import copy - -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/volume/v1/test_transfer_request.py b/openstackclient/tests/unit/volume/v1/test_transfer_request.py index 680561d522..333bf52688 100644 --- a/openstackclient/tests/unit/volume/v1/test_transfer_request.py +++ b/openstackclient/tests/unit/volume/v1/test_transfer_request.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index beff83364c..8bee574795 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index c415455534..25cdf92a43 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -14,9 +14,8 @@ # import argparse - -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/volume/v1/test_volume_backup.py b/openstackclient/tests/unit/volume/v1/test_volume_backup.py index 20be6e46d7..20aadcd344 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_volume_backup.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 5f976b0625..5f18990e96 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -14,10 +14,9 @@ import copy import random +from unittest import mock import uuid -import mock - from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index d238818245..c3bd71e325 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions 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 3bfe93df04..2202b85b98 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py @@ -12,7 +12,7 @@ # under the License. # -from mock import call +from unittest.mock import call from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import consistency_group_snapshot diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index 454747f5aa..073ec5709b 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -14,9 +14,8 @@ # import copy - -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/volume/v2/test_transfer_request.py b/openstackclient/tests/unit/volume/v2/test_transfer_request.py index 1ea6648f6a..c9dce3cab0 100644 --- a/openstackclient/tests/unit/volume/v2/test_transfer_request.py +++ b/openstackclient/tests/unit/volume/v2/test_transfer_request.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions from osc_lib import utils diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 17915928dc..f13d08517a 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 332b50a78f..5d41b3a180 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -13,9 +13,8 @@ # import argparse - -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib.cli import format_columns from osc_lib import exceptions diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index 30c7915dd9..4e1f7ee135 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -12,8 +12,8 @@ # under the License. # -import mock -from mock import call +from unittest import mock +from unittest.mock import call from osc_lib import exceptions from osc_lib import utils diff --git a/test-requirements.txt b/test-requirements.txt index 0f59e46faa..d0bce4405c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ hacking>=1.1.0,<1.2.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -flake8-import-order==0.13 # LGPLv3 +flake8-import-order>=0.13 # LGPLv3 mock>=2.0.0 # BSD oslotest>=3.2.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 From 68aa35f35f21476085e25ad2e3da51a1961948e4 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Jan 2020 11:13:42 -0600 Subject: [PATCH 0176/1120] Add unit tests and release note for dns_publish_fixed_ip Follow-up to https://review.opendev.org/#/c/679834/ which added the options and lacked both a release note and minimal option-handling unit tests. Change-Id: Ibb2820add9b2fedaf5a8b1a77babf043f6641724 Signed-off-by: Dean Troyer --- .../tests/unit/network/v2/test_subnet.py | 38 +++++++++++++++++++ .../notes/bug-1784879-9b632174d4af853f.yaml | 8 ++++ 2 files changed, 46 insertions(+) create mode 100644 releasenotes/notes/bug-1784879-9b632174d4af853f.yaml diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 9903b0423a..d34be18cb2 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -460,6 +460,44 @@ def test_create_with_description(self): self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) + def _test_create_with_dns(self, publish_dns=True): + arglist = [ + "--subnet-range", self._subnet.cidr, + "--network", self._subnet.network_id, + self._subnet.name, + ] + if publish_dns: + arglist += ['--dns-publish-fixed-ip'] + else: + arglist += ['--no-dns-publish-fixed-ip'] + verifylist = [ + ('name', self._subnet.name), + ('subnet_range', self._subnet.cidr), + ('network', self._subnet.network_id), + ('ip_version', self._subnet.ip_version), + ('gateway', 'auto'), + ] + verifylist.append(('dns_publish_fixed_ip', publish_dns)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_subnet.assert_called_once_with( + cidr=self._subnet.cidr, + ip_version=self._subnet.ip_version, + name=self._subnet.name, + network_id=self._subnet.network_id, + dns_publish_fixed_ip=publish_dns, + ) + self.assertEqual(self.columns, columns) + self.assertItemEqual(self.data, data) + + def test_create_with_dns(self): + self._test_create_with_dns(publish_dns=True) + + def test_create_with_no_dns(self): + self._test_create_with_dns(publish_dns=False) + def _test_create_with_tag(self, add_tags=True): arglist = [ "--subnet-range", self._subnet.cidr, diff --git a/releasenotes/notes/bug-1784879-9b632174d4af853f.yaml b/releasenotes/notes/bug-1784879-9b632174d4af853f.yaml new file mode 100644 index 0000000000..67085731d0 --- /dev/null +++ b/releasenotes/notes/bug-1784879-9b632174d4af853f.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``--dns-publish-fixed-ip`` and ``--no-dns-publish-fixed-ip`` + options to ``create subnet`` and ``set subnet`` commands to + control the publishing of fixed IPs in DNS when the + ``subnet_dns_publish_fixed_ip`` Neutron extension is enabled. + [Bug `1784879 `_] From db29e28b7c1a6ef737f0c4cd459906379f59b252 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Mon, 3 Jun 2019 14:37:41 -0700 Subject: [PATCH 0177/1120] Switch to using osc_lib.utils.tags This patch updates the network modules to use the new osc_lib.utils.tags module and removes the in tree _tag.py version. A previous patch[1] moves the _tag.py code to osc-lib to allow other projects to leverage the code. [1] https://review.opendev.org/662859 Change-Id: Id0c34029e327de50c5fd2732bae5fbf45bbd16ee --- lower-constraints.txt | 2 +- openstackclient/network/v2/_tag.py | 144 ------------------- openstackclient/network/v2/floating_ip.py | 2 +- openstackclient/network/v2/network.py | 2 +- openstackclient/network/v2/port.py | 2 +- openstackclient/network/v2/router.py | 2 +- openstackclient/network/v2/security_group.py | 2 +- openstackclient/network/v2/subnet.py | 2 +- openstackclient/network/v2/subnet_pool.py | 2 +- requirements.txt | 2 +- 10 files changed, 9 insertions(+), 153 deletions(-) delete mode 100644 openstackclient/network/v2/_tag.py diff --git a/lower-constraints.txt b/lower-constraints.txt index 6bc10e57bc..91a543a617 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -54,7 +54,7 @@ openstacksdk==0.17.0 os-client-config==1.28.0 os-service-types==1.2.0 os-testr==1.0.0 -osc-lib==1.14.0 +osc-lib==2.0.0 osc-placement==1.7.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 diff --git a/openstackclient/network/v2/_tag.py b/openstackclient/network/v2/_tag.py deleted file mode 100644 index e1cba22ea9..0000000000 --- a/openstackclient/network/v2/_tag.py +++ /dev/null @@ -1,144 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -import argparse - -from openstackclient.i18n import _ - - -class _CommaListAction(argparse.Action): - - def __call__(self, parser, namespace, values, option_string=None): - setattr(namespace, self.dest, values.split(',')) - - -def add_tag_filtering_option_to_parser(parser, collection_name, - enhance_help=lambda _h: _h): - parser.add_argument( - '--tags', - metavar='[,,...]', - action=_CommaListAction, - help=enhance_help( - _('List %s which have all given tag(s) (Comma-separated list of ' - 'tags)') % collection_name) - ) - parser.add_argument( - '--any-tags', - metavar='[,,...]', - action=_CommaListAction, - help=enhance_help( - _('List %s which have any given tag(s) (Comma-separated list of ' - 'tags)') % collection_name) - ) - parser.add_argument( - '--not-tags', - metavar='[,,...]', - action=_CommaListAction, - help=enhance_help( - _('Exclude %s which have all given tag(s) (Comma-separated list ' - 'of tags)') % collection_name) - ) - parser.add_argument( - '--not-any-tags', - metavar='[,,...]', - action=_CommaListAction, - help=enhance_help( - _('Exclude %s which have any given tag(s) (Comma-separated list ' - 'of tags)') % collection_name) - ) - - -def get_tag_filtering_args(parsed_args, args): - if parsed_args.tags: - args['tags'] = ','.join(parsed_args.tags) - if parsed_args.any_tags: - args['any_tags'] = ','.join(parsed_args.any_tags) - if parsed_args.not_tags: - args['not_tags'] = ','.join(parsed_args.not_tags) - if parsed_args.not_any_tags: - args['not_any_tags'] = ','.join(parsed_args.not_any_tags) - - -def add_tag_option_to_parser_for_create(parser, resource_name, - enhance_help=lambda _h: _h): - tag_group = parser.add_mutually_exclusive_group() - tag_group.add_argument( - '--tag', - action='append', - dest='tags', - metavar='', - help=enhance_help( - _("Tag to be added to the %s " - "(repeat option to set multiple tags)") % resource_name) - ) - tag_group.add_argument( - '--no-tag', - action='store_true', - help=enhance_help(_("No tags associated with the %s") % resource_name) - ) - - -def add_tag_option_to_parser_for_set(parser, resource_name, - enhance_help=lambda _h: _h): - parser.add_argument( - '--tag', - action='append', - dest='tags', - metavar='', - help=enhance_help( - _("Tag to be added to the %s (repeat option to set multiple " - "tags)") % resource_name) - ) - parser.add_argument( - '--no-tag', - action='store_true', - help=enhance_help( - _("Clear tags associated with the %s. Specify both --tag and " - "--no-tag to overwrite current tags") % resource_name) - ) - - -def update_tags_for_set(client, obj, parsed_args): - if parsed_args.no_tag: - tags = set() - else: - tags = set(obj.tags or []) - if parsed_args.tags: - tags |= set(parsed_args.tags) - if set(obj.tags or []) != tags: - client.set_tags(obj, list(tags)) - - -def add_tag_option_to_parser_for_unset(parser, resource_name): - tag_group = parser.add_mutually_exclusive_group() - tag_group.add_argument( - '--tag', - action='append', - dest='tags', - metavar='', - help=_("Tag to be removed from the %s " - "(repeat option to remove multiple tags)") % resource_name) - tag_group.add_argument( - '--all-tag', - action='store_true', - help=_("Clear all tags associated with the %s") % resource_name) - - -def update_tags_for_unset(client, obj, parsed_args): - tags = set(obj.tags) - if parsed_args.all_tag: - tags = set() - if parsed_args.tags: - tags -= set(parsed_args.tags) - if set(obj.tags) != tags: - client.set_tags(obj, list(tags)) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index bd43379aff..4525913f52 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -16,12 +16,12 @@ from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common from openstackclient.network import sdk_utils -from openstackclient.network.v2 import _tag _formatters = { diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index e7031266ec..00cb782b73 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -17,12 +17,12 @@ from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common from openstackclient.network import sdk_utils -from openstackclient.network.v2 import _tag class AdminStateColumn(cliff_columns.FormattableColumn): diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 4d7e518975..a22bcafb56 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -24,12 +24,12 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common from openstackclient.network import sdk_utils -from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 5d85e485a0..464dbbec78 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -23,11 +23,11 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils -from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 24f71ab678..f8153fa8b2 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -19,13 +19,13 @@ from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common from openstackclient.network import sdk_utils from openstackclient.network import utils as network_utils -from openstackclient.network.v2 import _tag def _format_network_security_group_rules(sg_rules): diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index f282f42b06..f684406558 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -22,11 +22,11 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils -from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index d5a15475ef..2750574a97 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -20,11 +20,11 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from osc_lib.utils import tags as _tag from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils -from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) diff --git a/requirements.txt b/requirements.txt index eea51e5163..08777b5fef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 keystoneauth1>=3.6.2 # Apache-2.0 openstacksdk>=0.17.0 # Apache-2.0 -osc-lib>=1.14.0 # Apache-2.0 +osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 From 70ab3f9dd56a638cdff516ca85baa5ebd64c888b Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Wed, 21 Aug 2019 17:38:29 -0700 Subject: [PATCH 0178/1120] Add support for app cred access rules This commit introduces the --access-rules option for 'application credential create' as well as new 'access rule' commands for listing, showing, and deleting access rules. bp whitelist-extension-for-app-creds Change-Id: I04834b2874ec2a70da456a380b5bef03a392effa --- .../cli/command-objects/access-rules.rst | 61 ++++++ .../application-credentials.rst | 7 + lower-constraints.txt | 2 +- openstackclient/identity/v3/access_rule.py | 118 ++++++++++++ .../identity/v3/application_credential.py | 27 +++ .../tests/unit/identity/v3/fakes.py | 33 +++- .../unit/identity/v3/test_access_rule.py | 174 ++++++++++++++++++ .../v3/test_application_credential.py | 125 ++++++++++++- ...ension-for-app-creds-9afd5009b374190b.yaml | 6 + requirements.txt | 2 +- setup.cfg | 4 + 11 files changed, 548 insertions(+), 11 deletions(-) create mode 100644 doc/source/cli/command-objects/access-rules.rst create mode 100644 openstackclient/identity/v3/access_rule.py create mode 100644 openstackclient/tests/unit/identity/v3/test_access_rule.py create mode 100644 releasenotes/notes/bp-whitelist-extension-for-app-creds-9afd5009b374190b.yaml diff --git a/doc/source/cli/command-objects/access-rules.rst b/doc/source/cli/command-objects/access-rules.rst new file mode 100644 index 0000000000..bc8458283f --- /dev/null +++ b/doc/source/cli/command-objects/access-rules.rst @@ -0,0 +1,61 @@ +=========== +access rule +=========== + +Identity v3 + +Access rules are fine-grained permissions for application credentials. An access +rule comprises of a service type, a request path, and a request method. Access +rules may only be created as attributes of application credentials, but they may +be viewed and deleted independently. + + +access rule delete +------------------ + +Delete access rule(s) + +.. program:: access rule delete +.. code:: bash + + openstack access rule delete [ ...] + +.. describe:: + + Access rule(s) to delete (ID) + +access rule list +---------------- + +List access rules + +.. program:: access rule list +.. code:: bash + + openstack access rule list + [--user ] + [--user-domain ] + +.. option:: --user + + User whose access rules to list (name or ID). If not provided, looks up the + current user's access rules. + +.. option:: --user-domain + + Domain the user belongs to (name or ID). This can be + used in case collisions between user names exist. + +access rule show +--------------------------- + +Display access rule details + +.. program:: access rule show +.. code:: bash + + openstack access rule show + +.. describe:: + + Access rule to display (ID) diff --git a/doc/source/cli/command-objects/application-credentials.rst b/doc/source/cli/command-objects/application-credentials.rst index 2a1fbff25e..047f5ab661 100644 --- a/doc/source/cli/command-objects/application-credentials.rst +++ b/doc/source/cli/command-objects/application-credentials.rst @@ -22,6 +22,7 @@ Create new application credential [--expiration ] [--description ] [--restricted|--unrestricted] + [--access-rules ] .. option:: --secret @@ -52,6 +53,12 @@ Create new application credential Prohibit application credential from creating and deleting other application credentials and trusts (this is the default behavior) +.. option:: --access-rules + + Either a string or file path containing a JSON-formatted list of access + rules, each containing a request method, path, and service, for example + '[{"method": "GET", "path": "/v2.1/servers", "service": "compute"}]' + .. describe:: Name of the application credential diff --git a/lower-constraints.txt b/lower-constraints.txt index 91a543a617..0e00a11488 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -95,7 +95,7 @@ python-heatclient==1.10.0 python-ironic-inspector-client==1.5.0 python-ironicclient==2.3.0 python-karborclient==0.6.0 -python-keystoneclient==3.17.0 +python-keystoneclient==3.22.0 python-mimeparse==1.6.0 python-mistralclient==3.1.0 python-muranoclient==0.8.2 diff --git a/openstackclient/identity/v3/access_rule.py b/openstackclient/identity/v3/access_rule.py new file mode 100644 index 0000000000..d96b44daf9 --- /dev/null +++ b/openstackclient/identity/v3/access_rule.py @@ -0,0 +1,118 @@ +# Copyright 2019 SUSE LLC +# +# 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. +# + +"""Identity v3 Access Rule action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common + + +LOG = logging.getLogger(__name__) + + +class DeleteAccessRule(command.Command): + _description = _("Delete access rule(s)") + + def get_parser(self, prog_name): + parser = super(DeleteAccessRule, self).get_parser(prog_name) + parser.add_argument( + 'access_rule', + metavar='', + nargs="+", + help=_('Application credentials(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + errors = 0 + for ac in parsed_args.access_rule: + try: + access_rule = utils.find_resource( + identity_client.access_rules, ac) + identity_client.access_rules.delete(access_rule.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete access rule with " + "ID '%(ac)s': %(e)s"), + {'ac': ac, 'e': e}) + + if errors > 0: + total = len(parsed_args.access_rule) + msg = (_("%(errors)s of %(total)s access rules failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListAccessRule(command.Lister): + _description = _("List access rules") + + def get_parser(self, prog_name): + parser = super(ListAccessRule, self).get_parser(prog_name) + parser.add_argument( + '--user', + metavar='', + help=_('User whose access rules to list (name or ID)'), + ) + common.add_user_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + if parsed_args.user: + user_id = common.find_user(identity_client, + parsed_args.user, + parsed_args.user_domain).id + else: + user_id = None + + columns = ('ID', 'Service', 'Method', 'Path') + data = identity_client.access_rules.list( + user=user_id) + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class ShowAccessRule(command.ShowOne): + _description = _("Display access rule details") + + def get_parser(self, prog_name): + parser = super(ShowAccessRule, self).get_parser(prog_name) + parser.add_argument( + 'access_rule', + metavar='', + help=_('Application credential to display (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + access_rule = utils.find_resource(identity_client.access_rules, + parsed_args.access_rule) + + access_rule._info.pop('links', None) + + return zip(*sorted(six.iteritems(access_rule._info))) diff --git a/openstackclient/identity/v3/application_credential.py b/openstackclient/identity/v3/application_credential.py index ea0b30cdbb..a208985624 100644 --- a/openstackclient/identity/v3/application_credential.py +++ b/openstackclient/identity/v3/application_credential.py @@ -16,6 +16,7 @@ """Identity v3 Application Credential action implementations""" import datetime +import json import logging from osc_lib.command import command @@ -79,6 +80,17 @@ def get_parser(self, prog_name): ' other application credentials and trusts (this is the' ' default behavior)'), ) + parser.add_argument( + '--access-rules', + metavar='', + help=_('Either a string or file path containing a JSON-formatted ' + 'list of access rules, each containing a request method, ' + 'path, and service, for example ' + '\'[{"method": "GET", ' + '"path": "/v2.1/servers", ' + '"service": "compute"}]\''), + + ) return parser def take_action(self, parsed_args): @@ -105,6 +117,20 @@ def take_action(self, parsed_args): else: unrestricted = parsed_args.unrestricted + if parsed_args.access_rules: + try: + access_rules = json.loads(parsed_args.access_rules) + except ValueError: + try: + with open(parsed_args.access_rules) as f: + access_rules = json.load(f) + except IOError: + raise exceptions.CommandError( + _("Access rules is not valid JSON string or file does" + " not exist.")) + else: + access_rules = None + app_cred_manager = identity_client.application_credentials application_credential = app_cred_manager.create( parsed_args.name, @@ -113,6 +139,7 @@ def take_action(self, parsed_args): description=parsed_args.description, secret=parsed_args.secret, unrestricted=unrestricted, + access_rules=access_rules, ) application_credential._info.pop('links', None) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index c394ab8237..fc4a48e37f 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -470,6 +470,14 @@ app_cred_expires = datetime.datetime(2022, 1, 1, 0, 0) app_cred_expires_str = app_cred_expires.strftime('%Y-%m-%dT%H:%M:%S%z') app_cred_secret = 'moresecuresecret' +app_cred_access_rules = ( + '[{"path": "/v2.1/servers", "method": "GET", "service": "compute"}]' +) +app_cred_access_rules_path = '/tmp/access_rules.json' +access_rule_id = 'access-rule-id' +access_rule_service = 'compute' +access_rule_path = '/v2.1/servers' +access_rule_method = 'GET' APP_CRED_BASIC = { 'id': app_cred_id, 'name': app_cred_name, @@ -478,7 +486,8 @@ 'description': None, 'expires_at': None, 'unrestricted': False, - 'secret': app_cred_secret + 'secret': app_cred_secret, + 'access_rules': None } APP_CRED_OPTIONS = { 'id': app_cred_id, @@ -488,7 +497,25 @@ 'description': app_cred_description, 'expires_at': app_cred_expires_str, 'unrestricted': False, - 'secret': app_cred_secret + 'secret': app_cred_secret, + 'access_rules': None, +} +ACCESS_RULE = { + 'id': access_rule_id, + 'service': access_rule_service, + 'path': access_rule_path, + 'method': access_rule_method, +} +APP_CRED_ACCESS_RULES = { + 'id': app_cred_id, + 'name': app_cred_name, + 'project_id': project_id, + 'roles': app_cred_role, + 'description': None, + 'expires_at': None, + 'unrestricted': False, + 'secret': app_cred_secret, + 'access_rules': app_cred_access_rules } registered_limit_id = 'registered-limit-id' @@ -625,6 +652,8 @@ def __init__(self, **kwargs): self.application_credentials = mock.Mock() self.application_credentials.resource_class = fakes.FakeResource(None, {}) + self.access_rules = mock.Mock() + self.access_rules.resource_class = fakes.FakeResource(None, {}) self.inference_rules = mock.Mock() self.inference_rules.resource_class = fakes.FakeResource(None, {}) self.registered_limits = mock.Mock() diff --git a/openstackclient/tests/unit/identity/v3/test_access_rule.py b/openstackclient/tests/unit/identity/v3/test_access_rule.py new file mode 100644 index 0000000000..f8b6093a6c --- /dev/null +++ b/openstackclient/tests/unit/identity/v3/test_access_rule.py @@ -0,0 +1,174 @@ +# Copyright 2019 SUSE LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +import mock +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.identity.v3 import access_rule +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +class TestAccessRule(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestAccessRule, self).setUp() + + identity_manager = self.app.client_manager.identity + self.access_rules_mock = identity_manager.access_rules + self.access_rules_mock.reset_mock() + self.roles_mock = identity_manager.roles + self.roles_mock.reset_mock() + + +class TestAccessRuleDelete(TestAccessRule): + + def setUp(self): + super(TestAccessRuleDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.access_rules_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ACCESS_RULE), + loaded=True, + ) + self.access_rules_mock.delete.return_value = None + + # Get the command object to test + self.cmd = access_rule.DeleteAccessRule( + self.app, None) + + def test_access_rule_delete(self): + arglist = [ + identity_fakes.access_rule_id, + ] + verifylist = [ + ('access_rule', [identity_fakes.access_rule_id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.access_rules_mock.delete.assert_called_with( + identity_fakes.access_rule_id, + ) + self.assertIsNone(result) + + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_access_rules_with_exception(self, find_mock): + find_mock.side_effect = [self.access_rules_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.access_rule_id, + 'nonexistent_access_rule', + ] + verifylist = [ + ('access_rule', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 access rules failed to' + ' delete.', str(e)) + + find_mock.assert_any_call(self.access_rules_mock, + identity_fakes.access_rule_id) + find_mock.assert_any_call(self.access_rules_mock, + 'nonexistent_access_rule') + + self.assertEqual(2, find_mock.call_count) + self.access_rules_mock.delete.assert_called_once_with( + identity_fakes.access_rule_id) + + +class TestAccessRuleList(TestAccessRule): + + def setUp(self): + super(TestAccessRuleList, self).setUp() + + self.access_rules_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ACCESS_RULE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = access_rule.ListAccessRule(self.app, None) + + def test_access_rule_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.access_rules_mock.list.assert_called_with(user=None) + + collist = ('ID', 'Service', 'Method', 'Path') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.access_rule_id, + identity_fakes.access_rule_service, + identity_fakes.access_rule_method, + identity_fakes.access_rule_path, + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestAccessRuleShow(TestAccessRule): + + def setUp(self): + super(TestAccessRuleShow, self).setUp() + + self.access_rules_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ACCESS_RULE), + loaded=True, + ) + + # Get the command object to test + self.cmd = access_rule.ShowAccessRule(self.app, None) + + def test_access_rule_show(self): + arglist = [ + identity_fakes.access_rule_id, + ] + verifylist = [ + ('access_rule', identity_fakes.access_rule_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.access_rules_mock.get.assert_called_with( + identity_fakes.access_rule_id) + + collist = ('id', 'method', 'path', 'service') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.access_rule_id, + identity_fakes.access_rule_method, + identity_fakes.access_rule_path, + identity_fakes.access_rule_service, + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_application_credential.py b/openstackclient/tests/unit/identity/v3/test_application_credential.py index 163aae9db9..24bafc9f5f 100644 --- a/openstackclient/tests/unit/identity/v3/test_application_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_application_credential.py @@ -14,6 +14,7 @@ # import copy +import json from unittest import mock from osc_lib import exceptions @@ -79,16 +80,18 @@ def test_application_credential_create_basic(self): 'expires_at': None, 'description': None, 'unrestricted': False, + 'access_rules': None, } self.app_creds_mock.create.assert_called_with( name, **kwargs ) - collist = ('description', 'expires_at', 'id', 'name', 'project_id', - 'roles', 'secret', 'unrestricted') + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') self.assertEqual(collist, columns) datalist = ( + None, None, None, identity_fakes.app_cred_id, @@ -135,17 +138,19 @@ def test_application_credential_create_with_options(self): 'roles': [identity_fakes.role_id], 'expires_at': identity_fakes.app_cred_expires, 'description': 'credential for testing', - 'unrestricted': False + 'unrestricted': False, + 'access_rules': None, } self.app_creds_mock.create.assert_called_with( name, **kwargs ) - collist = ('description', 'expires_at', 'id', 'name', 'project_id', - 'roles', 'secret', 'unrestricted') + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') self.assertEqual(collist, columns) datalist = ( + None, identity_fakes.app_cred_description, identity_fakes.app_cred_expires_str, identity_fakes.app_cred_id, @@ -157,6 +162,111 @@ def test_application_credential_create_with_options(self): ) self.assertEqual(datalist, data) + def test_application_credential_create_with_access_rules_string(self): + name = identity_fakes.app_cred_name + self.app_creds_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.APP_CRED_ACCESS_RULES), + loaded=True, + ) + + arglist = [ + name, + '--access-rules', identity_fakes.app_cred_access_rules, + ] + verifylist = [ + ('name', identity_fakes.app_cred_name), + ('access_rules', identity_fakes.app_cred_access_rules), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'secret': None, + 'roles': [], + 'expires_at': None, + 'description': None, + 'unrestricted': False, + 'access_rules': json.loads(identity_fakes.app_cred_access_rules) + } + self.app_creds_mock.create.assert_called_with( + name, + **kwargs + ) + + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.app_cred_access_rules, + None, + None, + identity_fakes.app_cred_id, + identity_fakes.app_cred_name, + identity_fakes.project_id, + identity_fakes.role_name, + identity_fakes.app_cred_secret, + False, + ) + self.assertEqual(datalist, data) + + @mock.patch('openstackclient.identity.v3.application_credential.json.load') + @mock.patch('openstackclient.identity.v3.application_credential.open') + def test_application_credential_create_with_access_rules_file( + self, _, mock_json_load): + mock_json_load.return_value = identity_fakes.app_cred_access_rules + + name = identity_fakes.app_cred_name + self.app_creds_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.APP_CRED_ACCESS_RULES), + loaded=True, + ) + + arglist = [ + name, + '--access-rules', identity_fakes.app_cred_access_rules_path, + ] + verifylist = [ + ('name', identity_fakes.app_cred_name), + ('access_rules', identity_fakes.app_cred_access_rules_path), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'secret': None, + 'roles': [], + 'expires_at': None, + 'description': None, + 'unrestricted': False, + 'access_rules': identity_fakes.app_cred_access_rules + } + self.app_creds_mock.create.assert_called_with( + name, + **kwargs + ) + + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.app_cred_access_rules, + None, + None, + identity_fakes.app_cred_id, + identity_fakes.app_cred_name, + identity_fakes.project_id, + identity_fakes.role_name, + identity_fakes.app_cred_secret, + False, + ) + self.assertEqual(datalist, data) + class TestApplicationCredentialDelete(TestApplicationCredential): @@ -293,10 +403,11 @@ def test_application_credential_show(self): self.app_creds_mock.get.assert_called_with(identity_fakes.app_cred_id) - collist = ('description', 'expires_at', 'id', 'name', 'project_id', - 'roles', 'secret', 'unrestricted') + collist = ('access_rules', 'description', 'expires_at', 'id', 'name', + 'project_id', 'roles', 'secret', 'unrestricted') self.assertEqual(collist, columns) datalist = ( + None, None, None, identity_fakes.app_cred_id, diff --git a/releasenotes/notes/bp-whitelist-extension-for-app-creds-9afd5009b374190b.yaml b/releasenotes/notes/bp-whitelist-extension-for-app-creds-9afd5009b374190b.yaml new file mode 100644 index 0000000000..afa6e212e0 --- /dev/null +++ b/releasenotes/notes/bp-whitelist-extension-for-app-creds-9afd5009b374190b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + [`blueprint whitelist-extension-for-app-creds `_] + Added support for creating access rules as an attribute of application + credentials as well as for listing, showing, and deleting access rules. diff --git a/requirements.txt b/requirements.txt index 08777b5fef..f7e2cecadb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,6 @@ osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 -python-keystoneclient>=3.17.0 # Apache-2.0 +python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=15.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 2d6422787f..fcc9700715 100644 --- a/setup.cfg +++ b/setup.cfg @@ -197,6 +197,10 @@ openstack.identity.v2 = openstack.identity.v3 = access_token_create = openstackclient.identity.v3.token:CreateAccessToken + access_rule_delete = openstackclient.identity.v3.access_rule:DeleteAccessRule + access_rule_list = openstackclient.identity.v3.access_rule:ListAccessRule + access_rule_show = openstackclient.identity.v3.access_rule:ShowAccessRule + application_credential_create = openstackclient.identity.v3.application_credential:CreateApplicationCredential application_credential_delete = openstackclient.identity.v3.application_credential:DeleteApplicationCredential application_credential_list = openstackclient.identity.v3.application_credential:ListApplicationCredential From 99b0b073922fb64ca4d0f6a5c44a28bc339b4312 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Tue, 21 Jan 2020 15:08:46 -0800 Subject: [PATCH 0179/1120] Fix copypaste errors in access rule command Access rules are access rules, not application credentials. Change-Id: I74d05f11ec186283e5a86d92dcbfe4eb24130eee --- openstackclient/identity/v3/access_rule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/v3/access_rule.py b/openstackclient/identity/v3/access_rule.py index d96b44daf9..65e78be1d0 100644 --- a/openstackclient/identity/v3/access_rule.py +++ b/openstackclient/identity/v3/access_rule.py @@ -38,7 +38,7 @@ def get_parser(self, prog_name): 'access_rule', metavar='', nargs="+", - help=_('Application credentials(s) to delete (name or ID)'), + help=_('Access rule(s) to delete (name or ID)'), ) return parser @@ -104,7 +104,7 @@ def get_parser(self, prog_name): parser.add_argument( 'access_rule', metavar='', - help=_('Application credential to display (name or ID)'), + help=_('Access rule to display (name or ID)'), ) return parser From cb265774ac33d6fe09f6f9e2bb5052f132662d0d Mon Sep 17 00:00:00 2001 From: Georgina Shippey Date: Tue, 21 Jan 2020 12:30:09 +0000 Subject: [PATCH 0180/1120] Incorrect title for service provider Mistakenly changed to identity provider. Change-Id: I0841a6e5ebd6a27a5375a54c56fc194dff65b370 --- doc/source/cli/command-objects/service-provider.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/cli/command-objects/service-provider.rst b/doc/source/cli/command-objects/service-provider.rst index a92c3b208c..b7a282cba5 100644 --- a/doc/source/cli/command-objects/service-provider.rst +++ b/doc/source/cli/command-objects/service-provider.rst @@ -1,6 +1,6 @@ -================= -identity provider -================= +================ +service provider +================ A **service provider** is used by the Identity service's OS-FEDERATION extension. It is used by to register another OpenStack Identity service. From 27b16df7f87c096e8366219ed76ed9467cdcb018 Mon Sep 17 00:00:00 2001 From: Kendall Nelson Date: Mon, 27 Jan 2020 21:51:18 -0800 Subject: [PATCH 0181/1120] Remove mention of meetings from docs Since the OpenStackClient team doesn't meet anymore, I removed the mention of meetings and added mention of the IRC channel in the communication section of the docs.. Change-Id: Iefa95878f95bf84bd9fc22ea4c914effc30dffa7 --- doc/source/contributor/developing.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index 721a016a03..bd9197d7fc 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -5,12 +5,11 @@ Developing with OpenStackClient Communication ------------- -Meetings -======== -The OpenStackClient team meets regularly on every Thursday. For details -please refer to the `OpenStack IRC meetings`_ page. - -.. _`OpenStack IRC meetings`: http://eavesdrop.openstack.org/#OpenStackClient_Team_Meeting +IRC Channel +=========== +The OpenStackClient team doesn't have regular meetings so if you have +questions or anything you want to discuss, come to our channel: +#openstack-sdks Testing ------- From ea27ebb0f918db9eab2f5751a1b065818faa0e6d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 27 Sep 2019 12:19:29 +0100 Subject: [PATCH 0182/1120] Stop silently ignoring invalid 'server create --hint' options The '--hint' option for 'server create' expects a key-value pair like so: openstack server create --hint group=245e1dfe-2d0e-4139-80a9-fce124948896 ... However, the command doesn't complain if this isn't the case, meaning typos like the below aren't indicated to the user: openstack server create --hint 245e1dfe-2d0e-4139-80a9-fce124948896 Due to how we'd implemented this here, this ultimately results in us POSTing the following as part of the body to 'os-servers': { ... "OS-SCH-HNT:scheduler_hints": { "245e1dfe-2d0e-4139-80a9-fce124948896": null } ... } Which is unfortunately allowed and ignored by nova due to the use of 'additionalProperties' in the schema [1] Do what we do for loads of other options and explicitly fail on invalid values. This involves adding a new argparse action since none of those defined in osc-lib work for us. This is included here to ease backporting of the fix but will be moved to osc-lib in a future patch. [1] https://github.com/openstack/nova/blob/19.0.0/nova/api/openstack/compute/schemas/servers.py#L142-L146 Change-Id: I9e96d2978912c8dfeadae4a782c481a17cd7e348 Signed-off-by: Stephen Finucane Story: #2006628 Task: #36840 Related-Bug: #1845322 --- openstackclient/compute/v2/server.py | 48 ++++++++++++++----- .../tests/unit/compute/v2/test_server.py | 25 +++++++++- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b5c420fe87..a4cca661b6 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -201,6 +201,36 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): return info +# TODO(stephenfin): Migrate this to osc-lib +class KeyValueAppendAction(argparse.Action): + """A custom action to parse arguments as key=value pairs + + Ensures that ``dest`` is a dict and values are lists of strings. + """ + + 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, {}) + + # Add value if an assignment else remove it + if '=' in values: + key, value = values.split('=', 1) + # NOTE(qtang): Prevent null key setting in property + if '' == key: + msg = _("Property key must be specified: %s") + raise argparse.ArgumentTypeError(msg % str(values)) + + dest = getattr(namespace, self.dest) + if key in dest: + dest[key].append(value) + else: + dest[key] = [value] + else: + msg = _("Expected 'key=value' type, but got: %s") + raise argparse.ArgumentTypeError(msg % str(values)) + + class AddFixedIP(command.Command): _description = _("Add fixed IP address to server") @@ -689,8 +719,8 @@ def get_parser(self, prog_name): parser.add_argument( '--hint', metavar='', - action='append', - default=[], + action=KeyValueAppendAction, + default={}, help=_('Hints for the scheduler (optional extension)'), ) parser.add_argument( @@ -986,16 +1016,12 @@ def _match_image(image_api, wanted_properties): security_group_names.append(sg['name']) hints = {} - for hint in parsed_args.hint: - key, _sep, value = hint.partition('=') - # NOTE(vish): multiple copies of the same hint will - # result in a list of values - if key in hints: - if isinstance(hints[key], six.string_types): - hints[key] = [hints[key]] - hints[key] += [value] + for key, values in parsed_args.hint.items(): + # only items with multiple values will result in a list + if len(values) == 1: + hints[key] = values[0] else: - hints[key] = value + hints[key] = values # What does a non-boolean value for config-drive do? # --config-drive argument is either a volume id or diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index b3be514724..27eefd8586 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -860,7 +860,7 @@ def test_server_create_with_options(self): ('key_name', 'keyname'), ('property', {'Beta': 'b'}), ('security_group', ['securitygroup']), - ('hint', ['a=b', 'a=c']), + ('hint', {'a': ['b', 'c']}), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2060,6 +2060,29 @@ def test_server_create_image_property_missed(self): self.cmd.take_action, parsed_args) + def test_server_create_invalid_hint(self): + # Not a key-value pair + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hint', 'a0cf03a5-d921-4877-bb5c-86d26cf818e1', + self.new_server.name, + ] + self.assertRaises(argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) + + # Empty key + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hint', '=a0cf03a5-d921-4877-bb5c-86d26cf818e1', + self.new_server.name, + ] + self.assertRaises(argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) + def test_server_create_with_description_api_newer(self): # Description is supported for nova api version 2.19 or above From cefa571d4b3162ab1da194573f85cce8bbdacc14 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 27 Sep 2019 12:59:48 +0100 Subject: [PATCH 0183/1120] Use 'KeyValueAppendAction' from osc-lib Does what it says on the tin. This action was added to osc-lib in change If73cab759fa09bddf1ff519923c5972c3b2052b1. Change-Id: I51efaa096bb26e297d99634c5d9cca34c0919074 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 32 +--------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a4cca661b6..5cc73284a2 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -201,36 +201,6 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): return info -# TODO(stephenfin): Migrate this to osc-lib -class KeyValueAppendAction(argparse.Action): - """A custom action to parse arguments as key=value pairs - - Ensures that ``dest`` is a dict and values are lists of strings. - """ - - 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, {}) - - # Add value if an assignment else remove it - if '=' in values: - key, value = values.split('=', 1) - # NOTE(qtang): Prevent null key setting in property - if '' == key: - msg = _("Property key must be specified: %s") - raise argparse.ArgumentTypeError(msg % str(values)) - - dest = getattr(namespace, self.dest) - if key in dest: - dest[key].append(value) - else: - dest[key] = [value] - else: - msg = _("Expected 'key=value' type, but got: %s") - raise argparse.ArgumentTypeError(msg % str(values)) - - class AddFixedIP(command.Command): _description = _("Add fixed IP address to server") @@ -719,7 +689,7 @@ def get_parser(self, prog_name): parser.add_argument( '--hint', metavar='', - action=KeyValueAppendAction, + action=parseractions.KeyValueAppendAction, default={}, help=_('Hints for the scheduler (optional extension)'), ) From e6e4b73efa01281002930e0806b765200f38e7c8 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 4 Oct 2019 14:42:41 +0200 Subject: [PATCH 0184/1120] Complete "Drop python2 support" goal We stopped testing python2, but there are more things to be cleanup. - Remove python2 entries from setup.cfg - Add releasenotes - remove universal wheel since this is only python 3 now Change-Id: Ie2bbb4d34b8411939ad5cfd750fc76c933779542 --- doc/requirements.txt | 3 +-- releasenotes/notes/drop-py2-421c90fbdf18dbc2.yaml | 7 +++++++ setup.cfg | 5 ----- 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/drop-py2-421c90fbdf18dbc2.yaml diff --git a/doc/requirements.txt b/doc/requirements.txt index 8639f9360a..5cfc337b13 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,8 +3,7 @@ # process, which may cause wedges in the gate later. openstackdocstheme>=1.23.2 # Apache-2.0 reno>=2.5.0 # Apache-2.0 -sphinx!=1.6.6,!=1.6.7,>=1.6.5,<2.0.0;python_version=='2.7' # BSD -sphinx!=1.6.6,!=1.6.7,>=1.6.5;python_version>='3.4' # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.5 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD # redirect tests in docs diff --git a/releasenotes/notes/drop-py2-421c90fbdf18dbc2.yaml b/releasenotes/notes/drop-py2-421c90fbdf18dbc2.yaml new file mode 100644 index 0000000000..74c5d8b530 --- /dev/null +++ b/releasenotes/notes/drop-py2-421c90fbdf18dbc2.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + Python 2.7 support has been dropped. The last release of + python-openstackclient to support Python 2.7 is OpenStack Train. The + minimum version of Python now supported by python-openstackclient is + Python 3.6. diff --git a/setup.cfg b/setup.cfg index fcc9700715..60caf5db1e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,8 +13,6 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 @@ -720,9 +718,6 @@ openstack.volume.v3 = [upload_sphinx] upload-dir = doc/build/html -[wheel] -universal = 1 - [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg From 2745b178a43f51ba837c485eed08c8ef6011a9ea Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 9 Jan 2020 17:52:37 +0000 Subject: [PATCH 0185/1120] Add qos_network_policy_id to network port tests Added "qos_network_policy_id" to "port show" command. Because this is just a read-only parameter and is read from the SDK port definition, this patch only modifies the corresponding tests. This patch is adding this new parameter to the test bench. Change-Id: Ice7423e0e0b98a39cc36622b70eae5a8493a037c Closes-Bug: #1851362 --- openstackclient/tests/unit/network/v2/fakes.py | 1 + openstackclient/tests/unit/network/v2/test_port.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 35f0b1a59c..e2e09cbadf 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -639,6 +639,7 @@ def create_one_port(attrs=None): 'security_group_ids': [], 'status': 'ACTIVE', 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'qos_network_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'tags': [], 'uplink_status_propagation': False, diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index c30d682f59..ec063b17a4 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -61,6 +61,7 @@ def _get_common_cols_data(fake_port): 'network_id', 'port_security_enabled', 'project_id', + 'qos_network_policy_id', 'qos_policy_id', 'security_group_ids', 'status', @@ -91,6 +92,7 @@ def _get_common_cols_data(fake_port): fake_port.network_id, fake_port.port_security_enabled, fake_port.project_id, + fake_port.qos_network_policy_id, fake_port.qos_policy_id, format_columns.ListColumn(fake_port.security_group_ids), fake_port.status, From d6022f96dfd608b83a4ff41483336f024aeacb16 Mon Sep 17 00:00:00 2001 From: Simon Merrick Date: Tue, 18 Feb 2020 12:48:02 +1300 Subject: [PATCH 0186/1120] Add storage policy option to create container command + Add CLI option to specify swift storage policy + Add CLI flag to specify container uses public read ACLS + Show storage policy in container show data Change-Id: I08ffa0d98bd39d467aa415771675f59bd77768ff --- openstackclient/api/object_store_v1.py | 27 ++++++- openstackclient/object/v1/container.py | 12 ++++ .../tests/unit/api/test_object_store_v1.py | 2 + .../unit/object/v1/test_container_all.py | 72 +++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 44ff7a01fb..c8514a5723 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -24,6 +24,11 @@ from openstackclient.api import api +GLOBAL_READ_ACL = ".r:*" +LIST_CONTENTS_ACL = ".rlistings" +PUBLIC_CONTAINER_ACLS = [GLOBAL_READ_ACL, LIST_CONTENTS_ACL] + + class APIv1(api.BaseAPI): """Object Store v1 API""" @@ -33,15 +38,32 @@ def __init__(self, **kwargs): def container_create( self, container=None, + public=False, + storage_policy=None ): """Create a container :param string container: name of container to create + :param bool public: + Boolean value indicating if the container should be publicly + readable. Defaults to False. + :param string storage_policy: + Name of the a specific storage policy to use. If not specified + swift will use its default storage policy. :returns: dict of returned headers """ - response = self.create(urllib.parse.quote(container), method='PUT') + + headers = {} + if public: + headers['x-container-read'] = ",".join(PUBLIC_CONTAINER_ACLS) + if storage_policy is not None: + headers['x-storage-policy'] = storage_policy + + response = self.create( + urllib.parse.quote(container), method='PUT', headers=headers) + data = { 'account': self._find_account_id(), 'container': container, @@ -173,7 +195,8 @@ def container_show( 'object_count': response.headers.get( 'x-container-object-count' ), - 'bytes_used': response.headers.get('x-container-bytes-used') + 'bytes_used': response.headers.get('x-container-bytes-used'), + 'storage_policy': response.headers.get('x-storage-policy'), } if 'x-container-read' in response.headers: diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 47ca5bc1da..917e41c02f 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -33,6 +33,16 @@ class CreateContainer(command.Lister): def get_parser(self, prog_name): parser = super(CreateContainer, self).get_parser(prog_name) + parser.add_argument( + '--public', + action='store_true', + default=False, + help="Make the container publicly accessible" + ) + parser.add_argument( + '--storage-policy', + help="Specify a particular storage policy to use." + ) parser.add_argument( 'containers', metavar='', @@ -51,6 +61,8 @@ def take_action(self, parsed_args): ' is 256'), len(container)) data = self.app.client_manager.object_store.container_create( container=container, + public=parsed_args.public, + storage_policy=parsed_args.storage_policy ) results.append(data) diff --git a/openstackclient/tests/unit/api/test_object_store_v1.py b/openstackclient/tests/unit/api/test_object_store_v1.py index 1c55be7192..96c68d5a1e 100644 --- a/openstackclient/tests/unit/api/test_object_store_v1.py +++ b/openstackclient/tests/unit/api/test_object_store_v1.py @@ -151,12 +151,14 @@ def test_container_show(self): 'X-Container-Meta-Owner': FAKE_ACCOUNT, 'x-container-object-count': '1', 'x-container-bytes-used': '577', + 'x-storage-policy': 'o1--sr-r3' } resp = { 'account': FAKE_ACCOUNT, 'container': 'qaz', 'object_count': '1', 'bytes_used': '577', + 'storage_policy': 'o1--sr-r3', 'properties': {'Owner': FAKE_ACCOUNT}, } self.requests_mock.register_uri( diff --git a/openstackclient/tests/unit/object/v1/test_container_all.py b/openstackclient/tests/unit/object/v1/test_container_all.py index 58c90e36d5..654cfbc740 100644 --- a/openstackclient/tests/unit/object/v1/test_container_all.py +++ b/openstackclient/tests/unit/object/v1/test_container_all.py @@ -70,6 +70,75 @@ def test_object_create_container_single(self): )] self.assertEqual(datalist, list(data)) + def test_object_create_container_storage_policy(self): + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/ernie', + headers={ + 'x-trans-id': '314159', + 'x-storage-policy': 'o1--sr-r3' + }, + status_code=200, + ) + + arglist = [ + 'ernie', + '--storage-policy', + 'o1--sr-r3' + ] + verifylist = [ + ('containers', ['ernie']), + ('storage_policy', 'o1--sr-r3') + ] + 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, columns) + datalist = [( + object_fakes.ACCOUNT_ID, + 'ernie', + '314159', + )] + self.assertEqual(datalist, list(data)) + + def test_object_create_container_public(self): + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/ernie', + headers={ + 'x-trans-id': '314159', + 'x-container-read': '.r:*,.rlistings' + }, + status_code=200, + ) + + arglist = [ + 'ernie', + '--public' + ] + verifylist = [ + ('containers', ['ernie']), + ('public', True) + ] + 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, columns) + datalist = [( + object_fakes.ACCOUNT_ID, + 'ernie', + '314159', + )] + self.assertEqual(datalist, list(data)) + def test_object_create_container_more(self): self.requests_mock.register_uri( 'PUT', @@ -300,6 +369,7 @@ def test_object_show_container(self): 'x-container-write': 'wsx', 'x-container-sync-to': 'edc', 'x-container-sync-key': 'rfv', + 'x-storage-policy': 'o1--sr-r3' } self.requests_mock.register_uri( 'HEAD', @@ -327,6 +397,7 @@ def test_object_show_container(self): 'container', 'object_count', 'read_acl', + 'storage_policy', 'sync_key', 'sync_to', 'write_acl', @@ -338,6 +409,7 @@ def test_object_show_container(self): 'ernie', '42', 'qaz', + 'o1--sr-r3', 'rfv', 'edc', 'wsx', From 0699df95c8a6c177940ef2abbc97f21ad4b86ebf Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 3 Mar 2020 10:55:06 -0600 Subject: [PATCH 0187/1120] Add bindep file We're missing one of these, which means starting from a bare node it's not completely possible to install openstackclient based only on bindep and pip. Add one that allows building and installing. Change-Id: I7b297bb1485773df3d5d1cc3ba78b0b9af4b2d00 --- bindep.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bindep.txt diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 0000000000..c5ae6c6097 --- /dev/null +++ b/bindep.txt @@ -0,0 +1,10 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see https://docs.openstack.org/infra/bindep/ for additional information. + +gcc [compile test] +libffi-devel [platform:rpm] +libffi-dev [compile test platform:dpkg] +libffi6 [platform:dpkg] +libssl-dev [compile test platform:dpkg] +python3-dev [compile test platform:dpkg] +python3-devel [compile test platform:rpm] From e410e61d204d09eeb7bcf0a33e3858ef7b110dd3 Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Tue, 25 Feb 2020 12:52:01 +1100 Subject: [PATCH 0188/1120] Always display direction for security group rules The --long option is still accepted but is now ignored. Change-Id: I23dd9fa7cff310ee9a62ce32b843b822b93b7548 Story: #2007323 --- .../network/v2/security_group_rule.py | 17 ++++++++++------- .../v2/test_security_group_rule_compute.py | 1 + .../v2/test_security_group_rule_network.py | 16 ++++++++-------- ...-direction-for-sg-rule-130efc39bf67d79a.yaml | 10 ++++++++++ 4 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index f48478ea74..150de1667b 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -480,7 +480,7 @@ def update_parser_network(self, parser): action='store_true', default=False, help=self.enhance_help_neutron( - _("List additional fields in output")) + _("**Deprecated** This argument is no longer needed")) ) return parser @@ -510,15 +510,19 @@ def _get_column_headers(self, parsed_args): 'Ethertype', 'IP Range', 'Port Range', + 'Direction', + 'Remote Security Group', ) - if parsed_args.long: - column_headers = column_headers + ('Direction',) - column_headers = column_headers + ('Remote Security Group',) if parsed_args.group is None: column_headers = column_headers + ('Security Group',) return column_headers def take_action_network(self, client, parsed_args): + if parsed_args.long: + self.log.warning(_( + "The --long option has been deprecated and is no longer needed" + )) + column_headers = self._get_column_headers(parsed_args) columns = ( 'id', @@ -526,10 +530,9 @@ def take_action_network(self, client, parsed_args): 'ether_type', 'remote_ip_prefix', 'port_range', + 'direction', + 'remote_group_id', ) - if parsed_args.long: - columns = columns + ('direction',) - columns = columns + ('remote_group_id',) # Get the security group rules using the requested query. query = {} diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py index 5720e30584..b7e38afb18 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py @@ -362,6 +362,7 @@ class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): 'Ethertype', 'IP Range', 'Port Range', + 'Direction', 'Remote Security Group', ) expected_columns_no_group = \ diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index 0a9522b0bc..0141161134 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -870,7 +870,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rules = [_security_group_rule_tcp, _security_group_rule_icmp] - expected_columns_with_group_and_long = ( + expected_columns_with_group = ( 'ID', 'IP Protocol', 'Ethertype', @@ -885,14 +885,15 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'Ethertype', 'IP Range', 'Port Range', + 'Direction', 'Remote Security Group', 'Security Group', ) - expected_data_with_group_and_long = [] + expected_data_with_group = [] expected_data_no_group = [] for _security_group_rule in _security_group_rules: - expected_data_with_group_and_long.append(( + expected_data_with_group.append(( _security_group_rule.id, _security_group_rule.protocol, _security_group_rule.ether_type, @@ -909,6 +910,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rule.remote_ip_prefix, security_group_rule._format_network_port_range( _security_group_rule), + _security_group_rule.direction, _security_group_rule.remote_group_id, _security_group_rule.security_group_id, )) @@ -935,14 +937,12 @@ def test_list_default(self): self.assertEqual(self.expected_columns_no_group, columns) self.assertEqual(self.expected_data_no_group, list(data)) - def test_list_with_group_and_long(self): + def test_list_with_group(self): self._security_group_rule_tcp.port_range_min = 80 arglist = [ - '--long', self._security_group.id, ] verifylist = [ - ('long', True), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -952,8 +952,8 @@ def test_list_with_group_and_long(self): self.network.security_group_rules.assert_called_once_with(**{ 'security_group_id': self._security_group.id, }) - self.assertEqual(self.expected_columns_with_group_and_long, columns) - self.assertEqual(self.expected_data_with_group_and_long, list(data)) + self.assertEqual(self.expected_columns_with_group, columns) + self.assertEqual(self.expected_data_with_group, list(data)) def test_list_with_ignored_options(self): self._security_group_rule_tcp.port_range_min = 80 diff --git a/releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml b/releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml new file mode 100644 index 0000000000..70dd67504c --- /dev/null +++ b/releasenotes/notes/always-show-direction-for-sg-rule-130efc39bf67d79a.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + By default listing security group rules now shows the direction. + The ``--long`` argument is now redundant and is now ignored as it + was only used to display the direction. +deprecations: + - | + Deprecate the ``--long`` option for the ``security group list`` + command. This is no longer needed to display all columns. From 001796faa26e3edcfb758e448857fd20f5a84158 Mon Sep 17 00:00:00 2001 From: yangxi Date: Wed, 10 Jan 2018 15:32:53 +0800 Subject: [PATCH 0189/1120] Change 'Volume' to 'Block Storage' In volume*.rst files, 'Volume' should be instead of 'Block Storage'. Change-Id: Iafc8bfa19e87edf1ffad2340c75e9d867042cae5 --- doc/source/cli/command-objects/volume-backend.rst | 2 +- doc/source/cli/command-objects/volume-backup.rst | 2 +- doc/source/cli/command-objects/volume-host.rst | 2 +- doc/source/cli/command-objects/volume-service.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/cli/command-objects/volume-backend.rst b/doc/source/cli/command-objects/volume-backend.rst index 0285d61b84..4766ecabb2 100644 --- a/doc/source/cli/command-objects/volume-backend.rst +++ b/doc/source/cli/command-objects/volume-backend.rst @@ -2,7 +2,7 @@ volume backend ============== -Volume v2 +Block Storage v2 .. autoprogram-cliff:: openstack.volume.v2 :command: volume backend * diff --git a/doc/source/cli/command-objects/volume-backup.rst b/doc/source/cli/command-objects/volume-backup.rst index 632e215e2b..1c26921197 100644 --- a/doc/source/cli/command-objects/volume-backup.rst +++ b/doc/source/cli/command-objects/volume-backup.rst @@ -2,7 +2,7 @@ volume backup ============= -Volume v1, v2 +Block Storage v1, v2 .. autoprogram-cliff:: openstack.volume.v2 :command: volume backup * diff --git a/doc/source/cli/command-objects/volume-host.rst b/doc/source/cli/command-objects/volume-host.rst index 1e513cb716..350d6dec7c 100644 --- a/doc/source/cli/command-objects/volume-host.rst +++ b/doc/source/cli/command-objects/volume-host.rst @@ -2,7 +2,7 @@ volume host =========== -Volume v2 +Block Storage v2 volume host failover -------------------- diff --git a/doc/source/cli/command-objects/volume-service.rst b/doc/source/cli/command-objects/volume-service.rst index 2ad23240d3..0499fb9062 100644 --- a/doc/source/cli/command-objects/volume-service.rst +++ b/doc/source/cli/command-objects/volume-service.rst @@ -2,7 +2,7 @@ volume service ============== -Volume v1, v2 +Block Storage v1, v2 volume service list ------------------- From 962efd949feb461283a9bb4a668fbd310f80ba40 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Tue, 30 Jan 2018 19:11:40 +0000 Subject: [PATCH 0190/1120] Disallow setting default on internal network The ``--default`` option should be only used for external network. Default internal network is not currently supported so we disallow it for now. Change-Id: Ia9d39b40e1e041d7bda0f6a27d058e382b572e1a Closes-Bug: #1745658 --- openstackclient/network/v2/network.py | 7 +++- .../tests/unit/network/v2/test_network.py | 33 +++++++++++++++++++ ...-on-internal-network-824fdea1a900891c.yaml | 9 +++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 00cb782b73..3f579b6d52 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -16,6 +16,7 @@ from cliff import columns as cliff_columns from osc_lib.cli import format_columns from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag @@ -125,6 +126,9 @@ def _get_attrs_network(client_manager, parsed_args): attrs['is_default'] = False if parsed_args.default: attrs['is_default'] = True + if attrs.get('is_default') and not attrs.get('router:external'): + msg = _("Cannot set default for internal network") + raise exceptions.CommandError(msg) # Update Provider network options if parsed_args.provider_network_type: attrs['provider:network_type'] = parsed_args.provider_network_type @@ -702,7 +706,8 @@ def get_parser(self, prog_name): default_router_grp.add_argument( '--default', action='store_true', - help=_("Set the network as the default external network") + help=_("Set the network as the default external network " + "(cannot be used with internal network).") ) default_router_grp.add_argument( '--no-default', diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 5f8eed6702..45d6008b5b 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -278,6 +278,24 @@ def test_create_with_tags(self): def test_create_with_no_tag(self): self._test_create_with_tag(add_tags=False) + def test_create_default_internal(self): + arglist = [ + self._network.name, + "--default", + ] + verifylist = [ + ('name', self._network.name), + ('enable', True), + ('share', None), + ('project', None), + ('external', False), + ('default', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestCreateNetworkIdentityV2(TestNetwork): @@ -1025,6 +1043,21 @@ def test_set_with_tags(self): def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) + def test_set_default_internal(self): + arglist = [ + self._network.name, + '--internal', + '--default', + ] + verifylist = [ + ('internal', True), + ('default', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestShowNetwork(TestNetwork): diff --git a/releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml b/releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml new file mode 100644 index 0000000000..baf4efe91d --- /dev/null +++ b/releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + For ``network create`` the + `--default`` option should be only used for external networks. + After this release, we enforce this scenario. If a users attempts + to create an internal default network or update a network to be + internal default, the command will be denied. + [Bug `1745658 `_] From 519296d762aceed6a9b3b3c206725d03fa3c2a20 Mon Sep 17 00:00:00 2001 From: huangshan Date: Fri, 11 May 2018 10:54:11 +0800 Subject: [PATCH 0191/1120] Update http links in docs This patch is proposed according to the Direction 10 of doc migration(https://etherpad.openstack.org/p/doc-migration-tracking). Change-Id: I0061bf788a8da89da0077db63f6cecf2ead0d0be --- CONTRIBUTING.rst | 4 ++-- doc/source/index.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index dfdfe471a7..69ecd79cad 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,13 +1,13 @@ If you would like to contribute to the development of OpenStack, you must follow the steps documented at: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. diff --git a/doc/source/index.rst b/doc/source/index.rst index 732e13872a..7675d6c379 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,7 +29,7 @@ Release Notes .. toctree:: :maxdepth: 1 - Release Notes + Release Notes Contributor Documentation ------------------------- From 711b9c9405c55ca8bb5649e2b56202845356e049 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Mon, 2 Mar 2020 16:40:15 +0000 Subject: [PATCH 0192/1120] Add "fields" parameter to ListSecurityGroup query This new query parameter will allow to send a query sending the "fields" parameter. This "fields" parameter contains the needed API fields, translated into OVO fields in Neutron server, that require to be retrieved from the DB. As commented in the related bug, the OSC "list" command only prints five parameters, none of them the security group rules. In systems with a reasonable amount of security groups, skipping the unnecessary rule load can save a lot of time. Depends-On: https://review.opendev.org/#/c/710820/ Change-Id: I16f48e292997d029d68f66365db949b9f4b5a0c8 Closes-Bug: #1865223 --- openstackclient/network/v2/security_group.py | 4 +++- .../network/v2/test_security_group_network.py | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index f8153fa8b2..2033af1432 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -202,6 +202,7 @@ def take_action_compute(self, client, parsed_args): # the OSC minimum requirements include SDK 1.0. class ListSecurityGroup(common.NetworkAndComputeLister): _description = _("List security groups") + FIELDS_TO_RETRIEVE = ['id', 'name', 'description', 'project_id', 'tags'] def update_parser_network(self, parser): if not self.is_docs_build: @@ -251,7 +252,8 @@ def take_action_network(self, client, parsed_args): filters['project_id'] = project_id _tag.get_tag_filtering_args(parsed_args, filters) - data = client.security_groups(**filters) + data = client.security_groups(fields=self.FIELDS_TO_RETRIEVE, + **filters) columns = ( "ID", diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index 14d5751436..67908fa8bf 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -285,7 +285,8 @@ def test_security_group_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) - self.network.security_groups.assert_called_once_with() + self.network.security_groups.assert_called_once_with( + fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -300,7 +301,8 @@ def test_security_group_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) - self.network.security_groups.assert_called_once_with() + self.network.security_groups.assert_called_once_with( + fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -316,7 +318,9 @@ def test_security_group_list_project(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = { + 'tenant_id': project.id, 'project_id': project.id, + 'fields': security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE} self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -336,7 +340,9 @@ def test_security_group_list_project_domain(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = { + 'tenant_id': project.id, 'project_id': project.id, + 'fields': security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE} self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -362,7 +368,8 @@ def test_list_with_tag_options(self): **{'tags': 'red,blue', 'any_tags': 'red,green', 'not_tags': 'orange,yellow', - 'not_any_tags': 'black,white'} + 'not_any_tags': 'black,white', + 'fields': security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) From 3e83e7471b57ed1a2c29a5402059e21da6db0666 Mon Sep 17 00:00:00 2001 From: Jose Castro Leon Date: Thu, 12 Mar 2020 14:43:18 +0100 Subject: [PATCH 0193/1120] Allow os quota list query to filter by project In the os quota list command, project parameter is completely ignored ending up in a request to all projects and then all quotas. This patch enables back the parameter and does a single call to quotas if specified. Change-Id: Ie17c256e2bdc307dcd94ad5be7abdbffa776d369 Story: 2007422 Task: 39043 --- openstackclient/common/quota.py | 13 +++- .../tests/unit/common/test_quota.py | 69 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 37437344dc..71b8ea61f2 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -274,9 +274,18 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - projects = self.app.client_manager.identity.projects.list() result = [] - project_ids = [getattr(p, 'id', '') for p in projects] + project_ids = [] + if parsed_args.project is None: + for p in self.app.client_manager.identity.projects.list(): + project_ids.append(getattr(p, 'id', '')) + else: + identity_client = self.app.client_manager.identity + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + project_ids.append(getattr(project, 'id', '')) if parsed_args.compute: if parsed_args.detail: diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index bd59ca77fe..3fff062b4c 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -392,6 +392,29 @@ def test_quota_list_compute_no_project_5xx(self): parsed_args, ) + def test_quota_list_compute_by_project(self): + # Two projects with non-default quotas + self.compute.quotas.get = mock.Mock( + side_effect=self.compute_quotas, + ) + + arglist = [ + '--compute', + '--project', self.projects[0].name, + ] + verifylist = [ + ('compute', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(self.compute_column_header, columns) + self.assertEqual(self.compute_reference_data, ret_quotas[0]) + self.assertEqual(1, len(ret_quotas)) + def test_quota_list_network(self): # Two projects with non-default quotas self.network.get_quota = mock.Mock( @@ -461,6 +484,29 @@ def test_quota_list_network_no_project(self): self.assertEqual(self.network_reference_data, ret_quotas[0]) self.assertEqual(1, len(ret_quotas)) + def test_quota_list_network_by_project(self): + # Two projects with non-default quotas + self.network.get_quota = mock.Mock( + side_effect=self.network_quotas, + ) + + arglist = [ + '--network', + '--project', self.projects[0].name, + ] + verifylist = [ + ('network', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(self.network_column_header, columns) + self.assertEqual(self.network_reference_data, ret_quotas[0]) + self.assertEqual(1, len(ret_quotas)) + def test_quota_list_volume(self): # Two projects with non-default quotas self.volume.quotas.get = mock.Mock( @@ -530,6 +576,29 @@ def test_quota_list_volume_no_project(self): self.assertEqual(self.volume_reference_data, ret_quotas[0]) self.assertEqual(1, len(ret_quotas)) + def test_quota_list_volume_by_project(self): + # Two projects with non-default quotas + self.volume.quotas.get = mock.Mock( + side_effect=self.volume_quotas, + ) + + arglist = [ + '--volume', + '--project', self.projects[0].name, + ] + verifylist = [ + ('volume', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + ret_quotas = list(data) + + self.assertEqual(self.volume_column_header, columns) + self.assertEqual(self.volume_reference_data, ret_quotas[0]) + self.assertEqual(1, len(ret_quotas)) + class TestQuotaSet(TestQuota): From 8c47b67e8337c05a6e14d717452a4438aa17167f Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 4 Mar 2020 08:56:08 -0600 Subject: [PATCH 0194/1120] Build utility image for using osc python-openstackclient currently has a non-zero number of dependencies, so for admins who would like to run it on laptops or similar it can get tricky. In opendev, for instance, admins have it installed into a venv on a jump host, but it's really wonky to keep up with. Use the opendev/python-builder opendev/python-base pair to make a minimal image that contains an install of python-openstackclient and publish it to the osclient org on dockerhub. There is an overall policy against having binary artifacts such as this appear to be official deliverables of the OpenStack project, which this is not. It's also only publishing images based on master, so no warranties should be implied. But if this makes life easier for a user somewhere, cool. Change-Id: I9a8bfc27c127e92b6856cb6a3e45b32c818db16c --- .zuul.yaml | 58 +++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 26 +++++++++++++++++++ examples/openstack.sh | 33 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 Dockerfile create mode 100755 examples/openstack.sh diff --git a/.zuul.yaml b/.zuul.yaml index 15f719c356..26ca146ff6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -152,6 +152,59 @@ tox_envlist: functional tox_install_siblings: true +- secret: + name: osc-dockerhub + data: + username: osclientzuul + password: !encrypted/pkcs1-oaep + - qQ0O7bXUWBhkygjSKcPHogWvR2ax67EgHZcYd27zgg6KvpdK9GsNTRTIeD5yeBb9Dzr/K + RcAf+0pQT3fRIsKyEx2odHNevGpePjmUZENd5vHTIvTuZWq+X5ehpXgkEYvw3jwYJg78F + ids1igEaHsE86OMHjWauyc1QUzYfwkf+ziK7TIOZ6RpVRHgq5Bf9S+Hz/QnVdxOLaIlO0 + VC/bchKX/36vOQKd20KkNhBQAnUlDBQWMnZocvZKZYtkDs2w2vqlnUPRlzEppBWm5Yae6 + 5acyIHEEAIbECd/wC/OT8YndoeOUiqOZY0uSWtv4JgEKl6AexP+54VxPrsz7LayRMDJ4B + jVCZK6y1sss9mF6mNXvZipPEVgklGcGM76GfGdqTeuQ3i8CqaKmCTBo1IKlEmcslXR/5T + vjibWzvNHPpFcpYEEM6GLGg2K6nja1MCE1s/L76pN3FtxCZHdl8rZXU+mJH37uQk9zvdR + Y6qtWJ+3o5sbgYfjgdp/nPs1xXMUvuG83qykuzYgtOYvlEw51eqwd2SPXd3op/KApAhKR + Zlu8fBUkm/FyXToOpCl0s/eR4w1d+Spv0A+UhrS5pmV18+NlpNs0Krj5wS9KWMUIec0ae + opgPkQrFfj/zD45rrIUJRzT+alZlZeK+WQfeNOXt2i6MLtOPesHMukTc6ksXtA= + +- job: + name: osc-build-image + parent: opendev-build-docker-image + description: Build Docker images. + allowed-projects: openstack/python-openstackclient + requires: + - python-builder-container-image + - python-base-container-image + provides: osc-container-image + vars: &osc_image_vars + docker_images: + - context: . + repository: osclient/python-openstackclient + +- job: + name: osc-upload-image + parent: opendev-upload-docker-image + description: Build Docker images and upload to Docker Hub. + allowed-projects: openstack/python-openstackclient + requires: + - python-builder-container-image + - python-base-container-image + provides: osc-container-image + vars: *osc_image_vars + secrets: &osc_image_secrets + - name: docker_credentials + secret: osc-dockerhub + pass-to-parent: true + +- job: + name: osc-promote-image + parent: opendev-promote-docker-image + allowed-projects: openstack/python-openstackclient + description: Promote previously uploaded Docker images. + vars: *osc_image_vars + secrets: *osc_image_secrets + - project-template: name: osc-tox-unit-tips check: @@ -174,6 +227,7 @@ - lib-forward-testing-python3 check: jobs: + - osc-build-image - osc-functional-devstack # - osc-functional-devstack-n-net: # voting: false @@ -187,4 +241,8 @@ branches: ^(?!stable) gate: jobs: + - osc-upload-image - osc-functional-devstack + promote: + jobs: + - osc-promote-image diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..9ff8084c30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Copyright (c) 2020 Red Hat, 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 docker.io/opendevorg/python-builder as builder + +COPY . /tmp/src +RUN assemble + +FROM docker.io/opendevorg/python-base + +COPY --from=builder /output/ /output +RUN /output/install-from-bindep + +CMD ["/usr/local/bin/openstack"] diff --git a/examples/openstack.sh b/examples/openstack.sh new file mode 100755 index 0000000000..aa35acfeb8 --- /dev/null +++ b/examples/openstack.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Copyright (c) 2020 Red Hat, 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. + +# This is an example script that can be installed somewhere, such as +# /usr/local/bin/openstack, that will allow using python-openstackclient +# via the container image instead of via a direct installation. It +# bind-mounts in clouds.yaml files, so it should behave like a directly +# installed osc. + +if type podman 2>/dev/null ; then + RUNTIME=podman +else + RUNTIME=docker +fi + +exec $RUNTIME run -it --rm \ + -v/etc/openstack:/etc/openstack \ + -v$HOME/.config/openstack:/root/.config/openstack \ + osclient/openstackclient \ + openstack $@ From bf2beb9e868175adb775ec253ef4a929ac557975 Mon Sep 17 00:00:00 2001 From: Daniel Bengtsson Date: Fri, 15 Nov 2019 10:06:35 +0100 Subject: [PATCH 0195/1120] Stop configuring install_command in tox and stop use pip. Currently, we are overriding 'install_command' to use 'pip'. This is considered poor behavior and 'python -m pip' should be used instead: https://snarky.ca/why-you-should-use-python-m-pip/ It turns out that this is the the default value provided by tox: https://tox.readthedocs.io/en/latest/config.html#conf-install_command So we can remove the line and simply use the default value. Use the right way when it's necessary. Change-Id: I410173d5fdcd8c592d98eed2f48b98e06299e8b3 --- lower-constraints.txt | 2 +- requirements.txt | 2 +- tox.ini | 31 +++++++++++++++---------------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 0e00a11488..5fdd4dd5fc 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.6.2 +keystoneauth1==3.14.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 diff --git a/requirements.txt b/requirements.txt index f7e2cecadb..aaea495e14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 -keystoneauth1>=3.6.2 # Apache-2.0 +keystoneauth1>=3.14.0 # Apache-2.0 openstacksdk>=0.17.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 3a7ed09794..3b4b66a0fe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.1 +minversion = 3.2.0 envlist = py37,pep8 skipdist = True # Automatic envs (pyXX) will only use the python version appropriate to that @@ -10,9 +10,7 @@ ignore_basepython_conflict = True [testenv] usedevelop = True basepython = python3 -install_command = pip install {opts} {packages} -setenv = VIRTUAL_ENV={envdir} - OS_STDOUT_CAPTURE=1 +setenv = OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = @@ -24,6 +22,7 @@ whitelist_externals = stestr [testenv:fast8] # Use same environment directory as pep8 env to save space and install time +setenv = VIRTUAL_ENV={envdir} envdir = {toxworkdir}/pep8 commands = {toxinidir}/tools/fast8.sh @@ -62,12 +61,12 @@ commands = [testenv:unit-tips] commands = - pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" - pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" - pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" - pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" - pip install -q -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" - pip freeze + python -m pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" + python -m pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" + python -m pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" + python -m pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" + pythom -m pip install -q -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" + python -m pip freeze stestr run {posargs} whitelist_externals = stestr @@ -81,12 +80,12 @@ commands = setenv = OS_TEST_PATH=./openstackclient/tests/functional passenv = OS_* commands = - pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" - pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" - pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" - pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" - pip install -q -U -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" - pip freeze + python -m pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" + python -m pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" + python -m pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" + python -m pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" + python -m pip install -q -U -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" + python -m pip freeze stestr run {posargs} [testenv:venv] From d2826e89e99ba63335a468e50e49db2fad6ab337 Mon Sep 17 00:00:00 2001 From: Daniel Strong Date: Wed, 18 Mar 2020 15:51:55 +0000 Subject: [PATCH 0196/1120] Allow setting floating IP description Change-Id: If664bfe3c9fdcb69c7046eb16c5d32602d1b3262 Story: 2007439 Task: 39094 --- openstackclient/network/v2/floating_ip.py | 8 ++++++ .../network/v2/test_floating_ip_network.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index 4525913f52..f3e3e5c44c 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -412,6 +412,11 @@ def get_parser(self, prog_name): help=_("Fixed IP of the port " "(required only if port has multiple IPs)") ) + parser.add_argument( + '--description', + metavar='', + help=_('Set floating IP description') + ) qos_policy_group = parser.add_mutually_exclusive_group() qos_policy_group.add_argument( '--qos-policy', @@ -443,6 +448,9 @@ def take_action(self, parsed_args): if parsed_args.fixed_ip_address: attrs['fixed_ip_address'] = parsed_args.fixed_ip_address + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.qos_policy: attrs['qos_policy_id'] = client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False).id diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py index a98051e769..dbcd5c9782 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py @@ -776,6 +776,32 @@ def test_fixed_ip_option(self): self.network.update_ip.assert_called_once_with( self.floating_ip, **attrs) + def test_description_option(self): + arglist = [ + self.floating_ip.id, + '--port', self.floating_ip.port_id, + '--description', self.floating_ip.description, + ] + verifylist = [ + ('floating_ip', self.floating_ip.id), + ('port', self.floating_ip.port_id), + ('description', self.floating_ip.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + attrs = { + 'port_id': self.floating_ip.port_id, + 'description': self.floating_ip.description, + } + self.network.find_ip.assert_called_once_with( + self.floating_ip.id, + ignore_missing=False, + ) + self.network.update_ip.assert_called_once_with( + self.floating_ip, **attrs) + def test_qos_policy_option(self): qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() self.network.find_qos_policy = mock.Mock(return_value=qos_policy) From 332457bc875401ba990a50c2d7ed14f49ea4e462 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 19 Mar 2020 16:17:10 -0500 Subject: [PATCH 0197/1120] Update image building jobs We're failing on promote but not upload. That's weird. Make sure the secret is appropriately encoded, and copy what zuul is doing. Also make promote a zero-node job. Change-Id: Ifcb5b4fe2486087a5ca1ff9609f7bf09ef026974 --- .zuul.yaml | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 26ca146ff6..3e8478d62a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -157,16 +157,16 @@ data: username: osclientzuul password: !encrypted/pkcs1-oaep - - qQ0O7bXUWBhkygjSKcPHogWvR2ax67EgHZcYd27zgg6KvpdK9GsNTRTIeD5yeBb9Dzr/K - RcAf+0pQT3fRIsKyEx2odHNevGpePjmUZENd5vHTIvTuZWq+X5ehpXgkEYvw3jwYJg78F - ids1igEaHsE86OMHjWauyc1QUzYfwkf+ziK7TIOZ6RpVRHgq5Bf9S+Hz/QnVdxOLaIlO0 - VC/bchKX/36vOQKd20KkNhBQAnUlDBQWMnZocvZKZYtkDs2w2vqlnUPRlzEppBWm5Yae6 - 5acyIHEEAIbECd/wC/OT8YndoeOUiqOZY0uSWtv4JgEKl6AexP+54VxPrsz7LayRMDJ4B - jVCZK6y1sss9mF6mNXvZipPEVgklGcGM76GfGdqTeuQ3i8CqaKmCTBo1IKlEmcslXR/5T - vjibWzvNHPpFcpYEEM6GLGg2K6nja1MCE1s/L76pN3FtxCZHdl8rZXU+mJH37uQk9zvdR - Y6qtWJ+3o5sbgYfjgdp/nPs1xXMUvuG83qykuzYgtOYvlEw51eqwd2SPXd3op/KApAhKR - Zlu8fBUkm/FyXToOpCl0s/eR4w1d+Spv0A+UhrS5pmV18+NlpNs0Krj5wS9KWMUIec0ae - opgPkQrFfj/zD45rrIUJRzT+alZlZeK+WQfeNOXt2i6MLtOPesHMukTc6ksXtA= + - n764ECFMGlEna6S5ezNyvW5nmq8IZCBts/7QRzdo2tWLQMp/mNFoaQensd797Ra3NLaS7 + NzCFJwGQvgWF6hJJIUfnf3h2+RecCwHahLN4r95RtjhAltARzHSZVDCeNXhiRJqSDS4Qc + kmXR2NTNfz8kkWUhnWNVjaBhYdMk0LnqZjQCxiCaR+eNdmeecWlSuJXg0Uz6vObLNfGHO + KxV3RiGc4J0AATYpYFEpC/SyPbBk0pJv6JWJb7nNIe0CEVW/7hkCfA6o3hQ5PNAswn5ZP + sp/L8NdoRQe/fEWOm/9K2lZqQehEj6SKsk6jkx3Wiy5stqFcGfafrxWcfQoQWpKHY5TeP + R9U0jJy+ipnhfnm0flBIBt9XHYykrTuFwp5QVdRhRRQDwg5RZBX+VmaBeSQlS2Z0oJmCX + PXFQmUDfnoU5go0BALlXDdy1sYE+SrQH4Eydw+hgf2oDFh+EkdhXMFburxnU8B7t4ey14 + EM1W4BdOBUgeI4fa/92BP6ipgUFvcJu19FYTdg4v7NZ/ApnwZnZ5KC4eYlDaKNQiPQUmW + pFJrnxxYXeDgmiXij8mCCgo8KEGvPCKHAghZ14iBCaWqvniLXuOSkFI1gYU+llg4i2jAp + ts3GfQqBe8jfROGPMexVuonqZxZBxvWmIgDsAaqAeJCtykS1xeIiAMtA8rNl40= - job: name: osc-build-image @@ -191,19 +191,24 @@ - python-builder-container-image - python-base-container-image provides: osc-container-image - vars: *osc_image_vars - secrets: &osc_image_secrets + secrets: - name: docker_credentials secret: osc-dockerhub pass-to-parent: true + vars: *osc_image_vars - job: name: osc-promote-image parent: opendev-promote-docker-image allowed-projects: openstack/python-openstackclient description: Promote previously uploaded Docker images. + secrets: + - name: docker_credentials + secret: osc-dockerhub + pass-to-parent: true + nodeset: + nodes: [] vars: *osc_image_vars - secrets: *osc_image_secrets - project-template: name: osc-tox-unit-tips From aac4f8c4f190d1f56736761e2fa811a76cc4991b Mon Sep 17 00:00:00 2001 From: Bence Romsics Date: Fri, 20 Mar 2020 11:52:29 +0100 Subject: [PATCH 0198/1120] Bump lower constraint of MarkupSafe setuptools 46.0.0's drop of the Features feature broke python-openstackclient's lower-constraints job on master via the MarkupSafe package. Bump the lower constraint of MarkupSafe to fix lower-constraints on master. Change-Id: Ib0a6f94a6611b221efbf76f6f25b55c43782546f --- lower-constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 5fdd4dd5fc..d29de0ab99 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -41,7 +41,7 @@ jsonschema==2.6.0 keystoneauth1==3.14.0 kombu==4.0.0 linecache2==1.0.0 -MarkupSafe==1.0 +MarkupSafe==1.1.0 mccabe==0.2.1 mock==2.0.0 monotonic==0.6 From 42abde330eb830aff7e1b63919df2d53e607ce4d Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 23 Mar 2020 08:08:52 -0500 Subject: [PATCH 0199/1120] Change dockerhub password Changed it dockerhub side. Change-Id: I1befae9622fc1ef72cd77cfd5792aad3fa231a6a --- .zuul.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 3e8478d62a..742de7af90 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -157,16 +157,16 @@ data: username: osclientzuul password: !encrypted/pkcs1-oaep - - n764ECFMGlEna6S5ezNyvW5nmq8IZCBts/7QRzdo2tWLQMp/mNFoaQensd797Ra3NLaS7 - NzCFJwGQvgWF6hJJIUfnf3h2+RecCwHahLN4r95RtjhAltARzHSZVDCeNXhiRJqSDS4Qc - kmXR2NTNfz8kkWUhnWNVjaBhYdMk0LnqZjQCxiCaR+eNdmeecWlSuJXg0Uz6vObLNfGHO - KxV3RiGc4J0AATYpYFEpC/SyPbBk0pJv6JWJb7nNIe0CEVW/7hkCfA6o3hQ5PNAswn5ZP - sp/L8NdoRQe/fEWOm/9K2lZqQehEj6SKsk6jkx3Wiy5stqFcGfafrxWcfQoQWpKHY5TeP - R9U0jJy+ipnhfnm0flBIBt9XHYykrTuFwp5QVdRhRRQDwg5RZBX+VmaBeSQlS2Z0oJmCX - PXFQmUDfnoU5go0BALlXDdy1sYE+SrQH4Eydw+hgf2oDFh+EkdhXMFburxnU8B7t4ey14 - EM1W4BdOBUgeI4fa/92BP6ipgUFvcJu19FYTdg4v7NZ/ApnwZnZ5KC4eYlDaKNQiPQUmW - pFJrnxxYXeDgmiXij8mCCgo8KEGvPCKHAghZ14iBCaWqvniLXuOSkFI1gYU+llg4i2jAp - ts3GfQqBe8jfROGPMexVuonqZxZBxvWmIgDsAaqAeJCtykS1xeIiAMtA8rNl40= + - X666jS/g43U4ykLzuQSNhl9XOVpKViT1bMQb/YJgTHj8AzigONu3+4BrxItS7mqwp9AWp + YtNHZ0Dju3piNw07ulnsOAYHpBcz+zFK5UzVMXzkHk5vu1W0WWYLVayU4HJKliAwPmNii + J3itt196sQ6IhIe/Hamcm715rP9cQd50w32lvAV4RNXoc4Gq7ZzOIzyA80UEl6HY7jiR9 + WYFI4PoCrmNxKKV8YAaH1OlJW71WF0O+77+oostKwFG35bxIniUl1ZinxOgjC2va8j5nT + KKgVgQdjwjuMOXEA6iiLewAo39P+nL/81AheDGQNex6x1oJuVRklQmiznbQI3knTOeJIh + ncRJuE9RfS0xuR5J45WOdKWVjIYpMkTXF+5Kn/pEtNelA2ADzaloGidNxEOvOD74vVf68 + mtZ1kffQi6d7yq57m0ZJ8WwTEo5g8Z+tmS40U2vdZ0vN5c/keGJisKbOpwavloP9owize + U9EgIAMIT6LE/fJWPGGugMR57z7gnCNiq970crTjWvsP1yFGvo/FQiUpplv5ni6C+s9jq + 1rba4Ya0uxQa2r5zjFXNJ52zGoRVlpjSsfpEtvCmTzfF5p0fa2aSyDs8ZOjaiDCVSmTPn + SE9rutyH0XO7CP9RFetxirfgSQ8ZDpUmXpfKaTGa0glIfjmmf4yyaaJRKOkubg= - job: name: osc-build-image From 27da238da2f9a8b35082d06624313a844cb6cc6f Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Wed, 26 Feb 2020 12:24:52 +0000 Subject: [PATCH 0200/1120] Fix network segment range "_get_ranges" function This function should return an ordered set of ranges based on an unordered list of numbers (int or str). Change-Id: I918c8befc51236cc33d96a5c88fb6eafdd143e9c Story: 2007341 Task: 38878 --- .../network/v2/network_segment_range.py | 3 +-- .../unit/network/v2/test_network_segment_range.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index 2cdae642e5..b38c72c248 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -23,7 +23,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -42,7 +41,7 @@ def _get_columns(item): def _get_ranges(item): - item = [int(i) if isinstance(i, six.string_types) else i for i in item] + item = sorted([int(i) for i in item]) for a, b in itertools.groupby(enumerate(item), lambda xy: xy[1] - xy[0]): b = list(b) yield "%s-%s" % (b[0][1], b[-1][1]) if b[0][1] != b[-1][1] else \ diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index 89a0c223ce..b60f1710b7 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -24,6 +24,20 @@ from openstackclient.tests.unit import utils as tests_utils +class TestAuxiliaryFunctions(tests_utils.TestCase): + + def test__get_ranges(self): + input_reference = [ + ([1, 2, 3, 4, 5, 6, 7], ['1-7']), + ([1, 2, 5, 4, 3, 6, 7], ['1-7']), + ([1, 2, 4, 3, 7, 6], ['1-4', '6-7']), + ([1, 2, 4, 3, '13', 12, '7', '6'], ['1-4', '6-7', '12-13']) + ] + for input, reference in input_reference: + self.assertEqual(reference, + list(network_segment_range._get_ranges(input))) + + class TestNetworkSegmentRange(network_fakes.TestNetworkV2): def setUp(self): From 9d96a13a97f0da0ae178433140e8aee8d834c6a1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 23 Mar 2020 12:55:11 -0500 Subject: [PATCH 0201/1120] Remove trailing newline from dockerhub secret When doing encrypt_secret, one should use echo -n not just echo. Change-Id: Iefbf0f13cd349b05de910f95b9467877cb53e46b --- .zuul.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 742de7af90..7a7e264ca7 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -157,16 +157,16 @@ data: username: osclientzuul password: !encrypted/pkcs1-oaep - - X666jS/g43U4ykLzuQSNhl9XOVpKViT1bMQb/YJgTHj8AzigONu3+4BrxItS7mqwp9AWp - YtNHZ0Dju3piNw07ulnsOAYHpBcz+zFK5UzVMXzkHk5vu1W0WWYLVayU4HJKliAwPmNii - J3itt196sQ6IhIe/Hamcm715rP9cQd50w32lvAV4RNXoc4Gq7ZzOIzyA80UEl6HY7jiR9 - WYFI4PoCrmNxKKV8YAaH1OlJW71WF0O+77+oostKwFG35bxIniUl1ZinxOgjC2va8j5nT - KKgVgQdjwjuMOXEA6iiLewAo39P+nL/81AheDGQNex6x1oJuVRklQmiznbQI3knTOeJIh - ncRJuE9RfS0xuR5J45WOdKWVjIYpMkTXF+5Kn/pEtNelA2ADzaloGidNxEOvOD74vVf68 - mtZ1kffQi6d7yq57m0ZJ8WwTEo5g8Z+tmS40U2vdZ0vN5c/keGJisKbOpwavloP9owize - U9EgIAMIT6LE/fJWPGGugMR57z7gnCNiq970crTjWvsP1yFGvo/FQiUpplv5ni6C+s9jq - 1rba4Ya0uxQa2r5zjFXNJ52zGoRVlpjSsfpEtvCmTzfF5p0fa2aSyDs8ZOjaiDCVSmTPn - SE9rutyH0XO7CP9RFetxirfgSQ8ZDpUmXpfKaTGa0glIfjmmf4yyaaJRKOkubg= + - LbIZjJiVstRVXMpoLQ3+/JcNB6lKVUWJXXo5+Outf+PKAaO7mNnv8XLiFMKnJ6ftopLyu + hWbX9rA+NddvplLQkf1xxkh7QBBU8PToLr58quI2SENUclt4tpjxbZfZu451kFSNJvNvR + E58cHHpfJZpyRnS2htXmN/Qy24gbV2w7CQxSZD2YhlcrerD8uQ8rWEnlY1wcJEaEGomtS + ZTGxsdK2TsZC2cd4b7TG7+xbl2i+hjADzwSQAgUzlLlwuG71667+IWk4SOZ7OycJTv9NN + ZTak8+CGfiMKdmsxZ1Z8uD7DC+RIklDjMWyly6zuhWzfhOmsmU0CesR50moodRUvbK79p + NZM8u0hBex5cl2EpUEwJL/FSPJXUhDMPoMoTZT/SAuXf25R9eZ9JGrKsIAlmVhpl8ifoE + 8TpPyvIHGS3YelTQjhqOX0wGb9T4ZauQCcI5Ajzy9NuCTyD9xxme9OX1zz7gMACRnVHvz + q7U7Ue90MnmGH6E2SgKjIZhyzy9Efwb7JUvH1Zb3hlrjCjEhwi9MV5FnABTEeXyYwE10s + 3o/KZg2zvdWkVG6x0dEkjpoQaNuaB7T2Na7Sm421n/z3LCzhiQGuTUjENnL6cMEtuA6Pp + BfI5+Qlg7HMwkBXNB73EPfWHzbCR3VNrzGYTy9FvhGud0/cXsuBXgps4WH63ic= - job: name: osc-build-image From 60e7c51df4cf061ebbb435a959ad63c7d3a296bf Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 13 Sep 2019 18:03:15 +0200 Subject: [PATCH 0202/1120] Switch image to use SDK This is a work to switch OSC from using glanceclient to OpenStackSDK. With this change only v2 is using OpenStackSDK. V1 is still using glanceclient and will be switched in a separate change. Remove the direct depend on keystoneauth- let that flow through openstacksdk. Depends-on: https://review.opendev.org/#/c/698972 Change-Id: I36f292fb70c98f6e558f58be55d533d979c47ca7 --- lower-constraints.txt | 6 +- openstackclient/common/sdk_utils.py | 60 ++ openstackclient/compute/v2/server.py | 31 +- openstackclient/compute/v2/server_backup.py | 7 +- openstackclient/compute/v2/server_image.py | 7 +- openstackclient/image/client.py | 76 +-- openstackclient/image/v2/image.py | 274 +++++---- .../tests/unit/compute/v2/test_server.py | 93 ++- .../unit/compute/v2/test_server_backup.py | 54 +- .../unit/compute/v2/test_server_image.py | 19 +- openstackclient/tests/unit/image/v2/fakes.py | 24 +- .../tests/unit/image/v2/test_image.py | 547 +++++++++--------- .../tests/unit/volume/v2/test_volume.py | 8 +- openstackclient/volume/v2/volume.py | 5 +- .../use_sdk_for_image-f49d2df38e7d9f81.yaml | 4 + requirements.txt | 3 +- 16 files changed, 660 insertions(+), 558 deletions(-) create mode 100644 openstackclient/common/sdk_utils.py create mode 100644 releasenotes/notes/use_sdk_for_image-f49d2df38e7d9f81.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 5fdd4dd5fc..1754ed7780 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.14.0 +keystoneauth1==3.16.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 @@ -50,9 +50,9 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.17.0 +openstacksdk==0.36.0 os-client-config==1.28.0 -os-service-types==1.2.0 +os-service-types==1.7.0 os-testr==1.0.0 osc-lib==2.0.0 osc-placement==1.7.0 diff --git a/openstackclient/common/sdk_utils.py b/openstackclient/common/sdk_utils.py new file mode 100644 index 0000000000..9f0856175d --- /dev/null +++ b/openstackclient/common/sdk_utils.py @@ -0,0 +1,60 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + + +def get_osc_show_columns_for_sdk_resource( + sdk_resource, + osc_column_map, + invisible_columns=None +): + """Get and filter the display and attribute columns for an SDK resource. + + Common utility function for preparing the output of an OSC show command. + Some of the columns may need to get renamed, others made invisible. + + :param sdk_resource: An SDK resource + :param osc_column_map: A hash of mappings for display column names + :param invisible_columns: A list of invisible column names + + :returns: Two tuples containing the names of the display and attribute + columns + """ + + if getattr(sdk_resource, 'allow_get', None) is not None: + resource_dict = sdk_resource.to_dict( + body=True, headers=False, ignore_none=False) + else: + resource_dict = sdk_resource + + # Build the OSC column names to display for the SDK resource. + attr_map = {} + display_columns = list(resource_dict.keys()) + invisible_columns = [] if invisible_columns is None else invisible_columns + for col_name in invisible_columns: + if col_name in display_columns: + display_columns.remove(col_name) + for sdk_attr, osc_attr in six.iteritems(osc_column_map): + if sdk_attr in display_columns: + attr_map[osc_attr] = sdk_attr + display_columns.remove(sdk_attr) + if osc_attr not in display_columns: + display_columns.append(osc_attr) + sorted_display_columns = sorted(display_columns) + + # Build the SDK attribute names for the OSC column names. + attr_columns = [] + for column in sorted_display_columns: + new_column = attr_map[column] if column in attr_map else column + attr_columns.append(new_column) + return tuple(sorted_display_columns), tuple(attr_columns) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 5cc73284a2..8be78049c8 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -143,7 +143,7 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): if image_info: image_id = image_info.get('id', '') try: - image = utils.find_resource(image_client.images, image_id) + image = image_client.get_image(image_id) info['image'] = "%s (%s)" % (image.name, image_id) except Exception: info['image'] = image_id @@ -735,10 +735,8 @@ def _show_progress(progress): # Lookup parsed_args.image image = None if parsed_args.image: - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image( + parsed_args.image, ignore_missing=False) if not image and parsed_args.image_property: def emit_duplicated_warning(img, image_property): @@ -749,7 +747,7 @@ def emit_duplicated_warning(img, image_property): 'chosen_one': img_uuid_list[0]}) def _match_image(image_api, wanted_properties): - image_list = image_api.image_list() + image_list = image_api.images() images_matched = [] for img in image_list: img_dict = {} @@ -768,7 +766,7 @@ def _match_image(image_api, wanted_properties): return [] return images_matched - images = _match_image(image_client.api, parsed_args.image_property) + images = _match_image(image_client, parsed_args.image_property) if len(images) > 1: emit_duplicated_warning(images, parsed_args.image_property) @@ -890,8 +888,8 @@ def _match_image(image_api, wanted_properties): # one specified by --image, then the compute service will # create a volume from the image and attach it to the # server as a non-root volume. - image_id = utils.find_resource( - image_client.images, dev_map[0]).id + image_id = image_client.find_image(dev_map[0], + ignore_missing=False).id mapping['uuid'] = image_id # 3. append size and delete_on_termination if exist if len(dev_map) > 2 and dev_map[2]: @@ -1324,8 +1322,8 @@ def take_action(self, parsed_args): # image name is given, map it to ID. image_id = None if parsed_args.image: - image_id = utils.find_resource(image_client.images, - parsed_args.image).id + image_id = image_client.find_image(parsed_args.image, + ignore_missing=False).id search_opts = { 'reservation_id': parsed_args.reservation_id, @@ -1476,12 +1474,12 @@ def take_action(self, parsed_args): (s.image.get('id') for s in data if s.image))): try: - images[i_id] = image_client.images.get(i_id) + images[i_id] = image_client.get_image(i_id) except Exception: pass else: try: - images_list = image_client.images.list() + images_list = image_client.images() for i in images_list: images[i.id] = i except Exception: @@ -1925,7 +1923,7 @@ def _show_progress(progress): # If parsed_args.image is not set, default to the currently used one. image_id = parsed_args.image or server.to_dict().get( 'image', {}).get('id') - image = utils.find_resource(image_client.images, image_id) + image = image_client.get_image(image_id) kwargs = {} if parsed_args.property: @@ -2195,10 +2193,7 @@ def take_action(self, parsed_args): image = None if parsed_args.image: - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image) utils.find_resource( compute_client.servers, diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index 1d560dc0c7..a5d43fc6f8 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -100,14 +100,11 @@ def _show_progress(progress): ) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - backup_name, - ) + image = image_client.find_image(backup_name, ignore_missing=False) if parsed_args.wait: if utils.wait_for_status( - image_client.images.get, + image_client.get_image, image.id, callback=_show_progress, ): diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index b93cd4d887..fea87af81f 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -79,14 +79,11 @@ def _show_progress(progress): ) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - image_id, - ) + image = image_client.find_image(image_id) if parsed_args.wait: if utils.wait_for_status( - image_client.images.get, + image_client.get_image, image_id, callback=_show_progress, ): diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index b67c291fd0..15bea17eb6 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -27,7 +27,7 @@ API_NAME = "image" API_VERSIONS = { "1": "glanceclient.v1.client.Client", - "2": "glanceclient.v2.client.Client", + "2": "openstack.connection.Connection", } IMAGE_API_TYPE = 'image' @@ -38,44 +38,52 @@ def make_client(instance): - """Returns an image service client""" - image_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS) - LOG.debug('Instantiating image client: %s', image_client) - - endpoint = instance.get_endpoint_for_service_type( - API_NAME, - region_name=instance.region_name, - interface=instance.interface, - ) - - client = image_client( - endpoint, - token=instance.auth.get_token(instance.session), - cacert=instance.cacert, - insecure=not instance.verify, - ) - # Create the low-level API - - image_api = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - IMAGE_API_VERSIONS) - LOG.debug('Instantiating image api: %s', image_api) - - client.api = image_api( - session=instance.session, - endpoint=instance.get_endpoint_for_service_type( - IMAGE_API_TYPE, + if instance._api_version[API_NAME] != '1': + LOG.debug( + 'Image client initialized using OpenStack SDK: %s', + instance.sdk_connection.image, + ) + return instance.sdk_connection.image + else: + """Returns an image service client""" + image_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + LOG.debug('Instantiating image client: %s', image_client) + + endpoint = instance.get_endpoint_for_service_type( + API_NAME, region_name=instance.region_name, interface=instance.interface, ) - ) - return client + client = image_client( + endpoint, + token=instance.auth.get_token(instance.session), + cacert=instance.cacert, + insecure=not instance.verify, + ) + + # Create the low-level API + + image_api = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + IMAGE_API_VERSIONS) + LOG.debug('Instantiating image api: %s', image_api) + + client.api = image_api( + session=instance.session, + endpoint=instance.get_endpoint_for_service_type( + IMAGE_API_TYPE, + region_name=instance.region_name, + interface=instance.interface, + ) + ) + + return client def build_option_parser(parser): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index feeb256744..412a16cc1c 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -18,8 +18,9 @@ import argparse from base64 import b64encode import logging +import os +import sys -from glanceclient.common import utils as gc_utils from openstack.image import image_signer from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns @@ -27,11 +28,16 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six +from openstackclient.common import sdk_utils from openstackclient.i18n import _ from openstackclient.identity import common +if os.name == "nt": + import msvcrt +else: + msvcrt = None + CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"] DEFAULT_CONTAINER_FORMAT = 'bare' @@ -44,7 +50,7 @@ LOG = logging.getLogger(__name__) -def _format_image(image): +def _format_image(image, human_readable=False): """Format an image to make it more consistent with OSC operations.""" info = {} @@ -56,15 +62,25 @@ def _format_image(image): 'min_disk', 'protected', 'id', 'file', 'checksum', 'owner', 'virtual_size', 'min_ram', 'schema'] + # TODO(gtema/anybody): actually it should be possible to drop this method, + # since SDK already delivers a proper object + image = image.to_dict(ignore_none=True, original_names=True) + # split out the usual key and the properties which are top-level - for key in six.iterkeys(image): + for key in image: if key in fields_to_show: info[key] = image.get(key) elif key == 'tags': continue # handle this later - else: + elif key == 'properties': + # NOTE(gtema): flatten content of properties + properties.update(image.get(key)) + elif key != 'location': properties[key] = image.get(key) + if human_readable: + info['size'] = utils.format_size(image['size']) + # format the tags if they are there info['tags'] = format_columns.ListColumn(image.get('tags')) @@ -75,6 +91,51 @@ def _format_image(image): return info +_formatters = { + 'tags': format_columns.ListColumn, +} + + +def _get_member_columns(item): + # Trick sdk_utils to return URI attribute + column_map = { + 'image_id': 'image_id' + } + hidden_columns = ['id', 'location', 'name'] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item.to_dict(), column_map, hidden_columns) + + +def get_data_file(args): + if args.file: + return (open(args.file, 'rb'), args.file) + 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) + + class AddProjectToImage(command.ShowOne): _description = _("Associate project with image") @@ -101,16 +162,18 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain).id - image_id = utils.find_resource( - image_client.images, - parsed_args.image).id + image = image_client.find_image(parsed_args.image, + ignore_missing=False) - image_member = image_client.image_members.create( - image_id, - project_id, + obj = image_client.add_member( + image=image.id, + member_id=project_id, ) - return zip(*sorted(image_member.items())) + display_columns, columns = _get_member_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) class CreateImage(command.ShowOne): @@ -302,9 +365,9 @@ def take_action(self, parsed_args): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['visibility'] = 'public' if parsed_args.private: @@ -314,24 +377,30 @@ def take_action(self, parsed_args): if parsed_args.shared: kwargs['visibility'] = 'shared' if parsed_args.project: - kwargs['owner'] = common.find_project( + kwargs['owner_id'] = common.find_project( identity_client, parsed_args.project, parsed_args.project_domain, ).id # open the file first to ensure any failures are handled before the - # image is created - fp = gc_utils.get_data_file(parsed_args) + # 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) info = {} + if fp is not None and parsed_args.volume: raise exceptions.CommandError(_("Uploading data and using " "container are not allowed at " "the same time")) - if fp is None and parsed_args.file: LOG.warning(_("Failed to get an image file.")) return {}, {} + elif fname: + kwargs['filename'] = fname + 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: @@ -361,8 +430,8 @@ def take_action(self, parsed_args): sign_key_path, password=pw) except Exception: - msg = (_("Error during sign operation: private key could " - "not be loaded.")) + msg = (_("Error during sign operation: private key " + "could not be loaded.")) raise exceptions.CommandError(msg) signature = signer.generate_signature(fp) @@ -371,7 +440,8 @@ def take_action(self, parsed_args): kwargs['img_signature_certificate_uuid'] = sign_cert_id kwargs['img_signature_hash_method'] = signer.hash_method if signer.padding_method: - kwargs['img_signature_key_type'] = signer.padding_method + kwargs['img_signature_key_type'] = \ + signer.padding_method # If a volume is specified. if parsed_args.volume: @@ -393,26 +463,7 @@ def take_action(self, parsed_args): except TypeError: info['volume_type'] = None else: - image = image_client.images.create(**kwargs) - - if fp is not None: - with fp: - try: - image_client.images.upload(image.id, fp) - except Exception: - # If the upload fails for some reason attempt to remove the - # dangling queued image made by the create() call above but - # only if the user did not specify an id which indicates - # the Image already exists and should be left alone. - try: - if 'id' not in kwargs: - image_client.images.delete(image.id) - except Exception: - pass # we don't care about this one - raise # now, throw the upload exception again - - # update the image after the data has been uploaded - image = image_client.images.get(image.id) + image = image_client.create_image(**kwargs) if not info: info = _format_image(image) @@ -439,11 +490,9 @@ def take_action(self, parsed_args): image_client = self.app.client_manager.image for image in parsed_args.images: try: - image_obj = utils.find_resource( - image_client.images, - image, - ) - image_client.images.delete(image_obj.id) + image_obj = image_client.find_image(image, + ignore_missing=False) + image_client.delete_image(image_obj.id) except Exception as e: del_result += 1 LOG.error(_("Failed to delete image with name or " @@ -569,18 +618,17 @@ def take_action(self, parsed_args): kwargs = {} if parsed_args.public: - kwargs['public'] = True + kwargs['visibility'] = 'public' if parsed_args.private: - kwargs['private'] = True + kwargs['visibility'] = 'private' if parsed_args.community: - kwargs['community'] = True + kwargs['visibility'] = 'community' if parsed_args.shared: - kwargs['shared'] = True + kwargs['visibility'] = 'shared' if parsed_args.limit: kwargs['limit'] = parsed_args.limit if parsed_args.marker: - kwargs['marker'] = utils.find_resource(image_client.images, - parsed_args.marker).id + kwargs['marker'] = image_client.find_image(parsed_args.marker).id if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.status: @@ -599,8 +647,8 @@ def take_action(self, parsed_args): 'Checksum', 'Status', 'visibility', - 'protected', - 'owner', + 'is_protected', + 'owner_id', 'tags', ) column_headers = ( @@ -621,24 +669,10 @@ def take_action(self, parsed_args): column_headers = columns # List of image data received - data = [] - limit = None if 'limit' in kwargs: - limit = kwargs['limit'] - if 'marker' in kwargs: - data = image_client.api.image_list(**kwargs) - else: - # No pages received yet, so start the page marker at None. - marker = None - while True: - page = image_client.api.image_list(marker=marker, **kwargs) - if not page: - break - data.extend(page) - # Set the marker to the id of the last item we received - marker = page[-1]['id'] - if limit: - break + # Disable automatic pagination in SDK + kwargs['paginated'] = False + data = list(image_client.images(**kwargs)) if parsed_args.property: for attr, value in parsed_args.property.items(): @@ -653,12 +687,10 @@ def take_action(self, parsed_args): return ( column_headers, - (utils.get_dict_properties( + (utils.get_item_properties( s, columns, - formatters={ - 'tags': format_columns.ListColumn, - }, + formatters=_formatters, ) for s in data) ) @@ -684,11 +716,9 @@ def take_action(self, parsed_args): "Status" ) - image_id = utils.find_resource( - image_client.images, - parsed_args.image).id + image_id = image_client.find_image(parsed_args.image).id - data = image_client.image_members.list(image_id) + data = image_client.members(image=image_id) return (columns, (utils.get_item_properties( @@ -722,11 +752,12 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain).id - image_id = utils.find_resource( - image_client.images, - parsed_args.image).id + image = image_client.find_image(parsed_args.image, + ignore_missing=False) - image_client.image_members.delete(image_id, project_id) + image_client.remove_member( + member=project_id, + image=image.id) class SaveImage(command.Command): @@ -748,19 +779,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - data = image_client.images.data(image.id) + image = image_client.find_image(parsed_args.image) - if data.wrapped is None: - msg = _('Image %s has no data.') % image.id - LOG.error(msg) - self.app.stdout.write(msg + '\n') - raise SystemExit - - gc_utils.save_image(data, parsed_args.file) + image_client.download_image(image.id, output=parsed_args.file) class SetImage(command.Command): @@ -979,9 +1000,9 @@ def take_action(self, parsed_args): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['visibility'] = 'public' if parsed_args.private: @@ -997,17 +1018,20 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain, ).id - kwargs['owner'] = project_id + kwargs['owner_id'] = project_id + + image = image_client.find_image(parsed_args.image, + ignore_missing=False) - image = utils.find_resource( - image_client.images, parsed_args.image) + # image = utils.find_resource( + # image_client.images, parsed_args.image) activation_status = None if parsed_args.deactivate: - image_client.images.deactivate(image.id) + image_client.deactivate_image(image.id) activation_status = "deactivated" if parsed_args.activate: - image_client.images.reactivate(image.id) + image_client.reactivate_image(image.id) activation_status = "activated" membership_group_args = ('accept', 'reject', 'pending') @@ -1022,15 +1046,15 @@ def take_action(self, parsed_args): # most one item in the membership_status list. if membership_status[0] != 'pending': membership_status[0] += 'ed' # Glance expects the past form - image_client.image_members.update( - image.id, project_id, membership_status[0]) + image_client.update_member( + image=image.id, member=project_id, status=membership_status[0]) if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) try: - image = image_client.images.update(image.id, **kwargs) + image = image_client.update_image(image.id, **kwargs) except Exception: if activation_status is not None: LOG.info(_("Image %(id)s was %(status)s."), @@ -1058,14 +1082,11 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - if parsed_args.human_readable: - image['size'] = utils.format_size(image['size']) - info = _format_image(image) + image = image_client.find_image(parsed_args.image, + ignore_missing=False) + + info = _format_image(image, parsed_args.human_readable) return zip(*sorted(info.items())) @@ -1101,10 +1122,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image, + ignore_missing=False) kwargs = {} tagret = 0 @@ -1112,7 +1131,7 @@ def take_action(self, parsed_args): if parsed_args.tags: for k in parsed_args.tags: try: - image_client.image_tags.delete(image.id, k) + image_client.remove_tag(image.id, k) except Exception: LOG.error(_("tag unset failed, '%s' is a " "nonexistent tag "), k) @@ -1120,13 +1139,26 @@ def take_action(self, parsed_args): if parsed_args.properties: for k in parsed_args.properties: - if k not in image: + if k in image: + kwargs[k] = None + elif k in image.properties: + # Since image is an "evil" object from SDK POV we need to + # pass modified properties object, so that SDK can figure + # out, what was changed inside + # NOTE: ping gtema to improve that in SDK + new_props = kwargs.get('properties', + image.get('properties').copy()) + new_props.pop(k, None) + kwargs['properties'] = new_props + else: LOG.error(_("property unset failed, '%s' is a " "nonexistent property "), k) propret += 1 - image_client.images.update( - image.id, - parsed_args.properties, + + # We must give to update a current image for the reference on what + # has changed + image_client.update_image( + image, **kwargs) tagtotal = len(parsed_args.tags) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 27eefd8586..8ec8217dfe 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -55,6 +55,12 @@ def setUp(self): self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() + self.find_image_mock = self.app.client_manager.image.find_image + self.find_image_mock.reset_mock() + + self.get_image_mock = self.app.client_manager.image.get_image + self.get_image_mock.reset_mock() + # Get a shortcut to the volume client VolumeManager Mock self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() @@ -770,7 +776,8 @@ def setUp(self): self.servers_mock.create.return_value = self.new_server self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + self.find_image_mock.return_value = self.image + self.get_image_mock.return_value = self.image self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor @@ -1916,19 +1923,13 @@ def test_server_create_image_property(self): ('config_drive', False), ('server_name', self.new_server.name), ] - _image = image_fakes.FakeImage.create_one_image() # create a image_info as the side_effect of the fake image_list() image_info = { - 'id': _image.id, - 'name': _image.name, - 'owner': _image.owner, 'hypervisor_type': 'qemu', } - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [image_info], [], - ] - self.app.client_manager.image.api = self.api_mock + + _image = image_fakes.FakeImage.create_one_image(image_info) + self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1953,7 +1954,7 @@ def test_server_create_image_property(self): # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( self.new_server.name, - image_info, + _image, self.flavor, **kwargs ) @@ -1977,20 +1978,13 @@ def test_server_create_image_property_multi(self): ('config_drive', False), ('server_name', self.new_server.name), ] - _image = image_fakes.FakeImage.create_one_image() # create a image_info as the side_effect of the fake image_list() image_info = { - 'id': _image.id, - 'name': _image.name, - 'owner': _image.owner, 'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide', } - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [image_info], [], - ] - self.app.client_manager.image.api = self.api_mock + _image = image_fakes.FakeImage.create_one_image(image_info) + self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -2015,7 +2009,7 @@ def test_server_create_image_property_multi(self): # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( self.new_server.name, - image_info, + _image, self.flavor, **kwargs ) @@ -2039,20 +2033,14 @@ def test_server_create_image_property_missed(self): ('config_drive', False), ('server_name', self.new_server.name), ] - _image = image_fakes.FakeImage.create_one_image() # create a image_info as the side_effect of the fake image_list() image_info = { - 'id': _image.id, - 'name': _image.name, - 'owner': _image.owner, 'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide', } - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [image_info], [], - ] - self.app.client_manager.image.api = self.api_mock + + _image = image_fakes.FakeImage.create_one_image(image_info) + self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -2585,7 +2573,10 @@ def setUp(self): self.servers_mock.list.return_value = self.servers self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + + # self.images_mock.return_value = [self.image] + self.find_image_mock.return_value = self.image + self.get_image_mock.return_value = self.image self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor @@ -2599,7 +2590,7 @@ def setUp(self): self.data_no_name_lookup = [] Image = collections.namedtuple('Image', 'id name') - self.images_mock.list.return_value = [ + self.images_mock.return_value = [ Image(id=s.image['id'], name=self.image.name) # Image will be an empty string if boot-from-volume for s in self.servers if s.image @@ -2662,11 +2653,11 @@ def test_server_list_no_option(self): columns, data = self.cmd.take_action(parsed_args) self.servers_mock.list.assert_called_with(**self.kwargs) - self.images_mock.list.assert_called() + self.images_mock.assert_called() self.flavors_mock.list.assert_called() # we did not pass image or flavor, so gets on those must be absent self.assertFalse(self.flavors_mock.get.call_count) - self.assertFalse(self.images_mock.get.call_count) + self.assertFalse(self.get_image_mock.call_count) self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) @@ -2753,7 +2744,7 @@ def test_server_list_name_lookup_one_by_one(self): 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.get.assert_called() + self.get_image_mock.assert_called() self.flavors_mock.get.assert_called() self.assertEqual(self.columns, columns) @@ -2771,7 +2762,8 @@ def test_server_list_with_image(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_any_call(self.image.id) + self.find_image_mock.assert_called_with(self.image.id, + ignore_missing=False) self.search_opts['image'] = self.image.id self.servers_mock.list.assert_called_with(**self.kwargs) @@ -3558,7 +3550,7 @@ def setUp(self): # Return value for utils.find_resource for image self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + self.get_image_mock.return_value = self.image # Fake the rebuilt new server. attrs = { @@ -3598,7 +3590,7 @@ def test_rebuild_with_current_image(self): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) def test_rebuild_with_current_image_and_password(self): @@ -3617,7 +3609,7 @@ def test_rebuild_with_current_image_and_password(self): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) def test_rebuild_with_description_api_older(self): @@ -3665,7 +3657,7 @@ def test_rebuild_with_description_api_newer(self): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None, description=description) @@ -3694,7 +3686,7 @@ def test_rebuild_with_wait_ok(self, mock_wait_for_status): ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) @@ -3718,7 +3710,7 @@ def test_rebuild_with_wait_fails(self, mock_wait_for_status): ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) def test_rebuild_with_property(self): @@ -3738,7 +3730,7 @@ def test_rebuild_with_property(self): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with( self.image, None, meta=expected_property) @@ -3767,7 +3759,7 @@ def test_rebuild_with_keypair_name(self): key_name=self.server.key_name, ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(*args, **kwargs) def test_rebuild_with_keypair_name_older_version(self): @@ -3814,7 +3806,7 @@ def test_rebuild_with_keypair_unset(self): key_name=None, ) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(self.image.id) + self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(*args, **kwargs) def test_rebuild_with_key_name_and_unset(self): @@ -3872,7 +3864,7 @@ def setUp(self): # Return value for utils.find_resource for image self.image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = self.image + self.get_image_mock.return_value = self.image new_server = compute_fakes.FakeServer.create_one_server() attrs = { @@ -3913,7 +3905,7 @@ def test_rescue_with_current_image(self): def test_rescue_with_new_image(self): new_image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = new_image + self.find_image_mock.return_value = new_image arglist = [ '--image', new_image.id, self.server.id, @@ -3928,7 +3920,7 @@ def test_rescue_with_new_image(self): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.images_mock.get.assert_called_with(new_image.id) + self.find_image_mock.assert_called_with(new_image.id) self.server.rescue.assert_called_with(image=new_image, password=None) def test_rescue_with_current_image_and_password(self): @@ -4679,7 +4671,7 @@ def setUp(self): # This is the return value for utils.find_resource() self.servers_mock.get.return_value = self.server - self.images_mock.get.return_value = self.image + self.get_image_mock.return_value = self.image self.flavors_mock.get.return_value = self.flavor # Get the command object to test @@ -5140,7 +5132,8 @@ def test_prep_server_detail(self, find_resource): 'links': u'http://xxx.yyy.com', } _server = compute_fakes.FakeServer.create_one_server(attrs=server_info) - find_resource.side_effect = [_server, _image, _flavor] + find_resource.side_effect = [_server, _flavor] + self.get_image_mock.return_value = _image # Prepare result data. info = { diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 7dd459d823..5cdc20800a 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -32,8 +32,8 @@ def setUp(self): self.servers_mock.reset_mock() # Get a shortcut to the image client ImageManager Mock - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.images_mock = self.app.client_manager.image + self.images_mock.find_image.reset_mock() # Set object attributes to be tested. Could be overwritten in subclass. self.attrs = {} @@ -60,15 +60,18 @@ class TestServerBackupCreate(TestServerBackup): # Just return whatever Image is testing with these days def image_columns(self, image): - columnlist = tuple(sorted(image.keys())) + # columnlist = tuple(sorted(image.keys())) + columnlist = ( + 'id', 'name', 'owner', 'protected', 'status', 'tags', 'visibility' + ) return columnlist def image_data(self, image): datalist = ( image['id'], image['name'], - image['owner'], - image['protected'], + image['owner_id'], + image['is_protected'], 'active', format_columns.ListColumn(image.get('tags')), image['visibility'], @@ -102,7 +105,8 @@ def setup_images_mock(self, count, servers=None): count=count, ) - self.images_mock.get = mock.Mock(side_effect=images) + # self.images_mock.get = mock.Mock(side_effect=images) + self.images_mock.find_image = mock.Mock(side_effect=images) return images def test_server_backup_defaults(self): @@ -174,16 +178,18 @@ def test_server_backup_create_options(self): @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_backup_wait_fail(self, mock_wait_for_status): servers = self.setup_servers_mock(count=1) - images = image_fakes.FakeImage.create_images( - attrs={ - 'name': servers[0].name, - 'status': 'active', - }, - count=5, - ) - - self.images_mock.get = mock.Mock( - side_effect=images, + images = self.setup_images_mock(count=1, servers=servers) +# images = image_fakes.FakeImage.create_images( +# attrs={ +# 'name': servers[0].name, +# 'status': 'active', +# }, +# count=1, +# ) +# +# self.images_mock.find_image.return_value = images[0] + self.images_mock.get_image = mock.Mock( + side_effect=images[0], ) arglist = [ @@ -215,7 +221,7 @@ def test_server_backup_wait_fail(self, mock_wait_for_status): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) @@ -223,16 +229,10 @@ def test_server_backup_wait_fail(self, mock_wait_for_status): @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_server_backup_wait_ok(self, mock_wait_for_status): servers = self.setup_servers_mock(count=1) - images = image_fakes.FakeImage.create_images( - attrs={ - 'name': servers[0].name, - 'status': 'active', - }, - count=5, - ) + images = self.setup_images_mock(count=1, servers=servers) - self.images_mock.get = mock.Mock( - side_effect=images, + self.images_mock.get_image = mock.Mock( + side_effect=images[0], ) arglist = [ @@ -263,7 +263,7 @@ def test_server_backup_wait_ok(self, mock_wait_for_status): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index f9d7b10e75..1cec5b685f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -31,8 +31,8 @@ def setUp(self): self.servers_mock.reset_mock() # Get a shortcut to the image client ImageManager Mock - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.images_mock = self.app.client_manager.image + self.images_mock.find_image.reset_mock() # Set object attributes to be tested. Could be overwritten in subclass. self.attrs = {} @@ -58,15 +58,18 @@ def setup_servers_mock(self, count): class TestServerImageCreate(TestServerImage): def image_columns(self, image): - columnlist = tuple(sorted(image.keys())) + # columnlist = tuple(sorted(image.keys())) + columnlist = ( + 'id', 'name', 'owner', 'protected', 'status', 'tags', 'visibility' + ) return columnlist def image_data(self, image): datalist = ( image['id'], image['name'], - image['owner'], - image['protected'], + image['owner_id'], + image['is_protected'], 'active', format_columns.ListColumn(image.get('tags')), image['visibility'], @@ -100,7 +103,7 @@ def setup_images_mock(self, count, servers=None): count=count, ) - self.images_mock.get = mock.Mock(side_effect=images) + self.images_mock.find_image = mock.Mock(side_effect=images) self.servers_mock.create_image = mock.Mock( return_value=images[0].id, ) @@ -188,7 +191,7 @@ def test_server_create_image_wait_fail(self, mock_wait_for_status): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) @@ -220,7 +223,7 @@ def test_server_create_image_wait_ok(self, mock_wait_for_status): ) mock_wait_for_status.assert_called_once_with( - self.images_mock.get, + self.images_mock.get_image, images[0].id, callback=mock.ANY ) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 655ae341c6..516d563001 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -18,9 +18,9 @@ from unittest import mock import uuid -from glanceclient.v2 import schemas +from openstack.image.v2 import image +from openstack.image.v2 import member from osc_lib.cli import format_columns -import warlock from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -154,6 +154,12 @@ def __init__(self, **kwargs): self.image_members.resource_class = fakes.FakeResource(None, {}) self.image_tags = mock.Mock() self.image_tags.resource_class = fakes.FakeResource(None, {}) + + self.find_image = mock.Mock() + self.find_image.resource_class = fakes.FakeResource(None, {}) + + self.get_image = mock.Mock() + self.get_image.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.version = 2.0 @@ -197,8 +203,8 @@ def create_one_image(attrs=None): image_info = { 'id': str(uuid.uuid4()), 'name': 'image-name' + uuid.uuid4().hex, - 'owner': 'image-owner' + uuid.uuid4().hex, - 'protected': bool(random.choice([0, 1])), + 'owner_id': 'image-owner' + uuid.uuid4().hex, + 'is_protected': bool(random.choice([0, 1])), 'visibility': random.choice(['public', 'private']), 'tags': [uuid.uuid4().hex for r in range(2)], } @@ -206,13 +212,7 @@ def create_one_image(attrs=None): # Overwrite default attributes if there are some attributes set image_info.update(attrs) - # Set up the schema - model = warlock.model_factory( - IMAGE_schema, - schemas.SchemaBasedModel, - ) - - return model(**image_info) + return image.Image(**image_info) @staticmethod def create_images(attrs=None, count=2): @@ -307,6 +307,8 @@ def create_one_image_member(attrs=None): # Overwrite default attributes if there are some attributes set image_member_info.update(attrs) + return member.Member(**image_member_info) + image_member = fakes.FakeModel( copy.deepcopy(image_member_info)) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 78d857e269..a021cfc7fc 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -14,13 +14,14 @@ # import copy +import io +import os +import tempfile from unittest import mock -from glanceclient.common import utils as glanceclient_utils -from glanceclient.v2 import schemas +from openstack import exceptions as sdk_exceptions from osc_lib.cli import format_columns from osc_lib import exceptions -import warlock from openstackclient.image.v2 import image from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -33,12 +34,16 @@ def setUp(self): super(TestImage, self).setUp() # Get shortcuts to the Mocks in image client - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + # SDK proxy mock + self.app.client_manager.image = mock.Mock() + self.client = self.app.client_manager.image + + self.client.remove_member = mock.Mock() + + self.client.create_image = mock.Mock() + self.client.update_image = mock.Mock() self.image_members_mock = self.app.client_manager.image.image_members - self.image_members_mock.reset_mock() self.image_tags_mock = self.app.client_manager.image.image_tags - self.image_tags_mock.reset_mock() # Get shortcut to the Mocks in identity client self.project_mock = self.app.client_manager.identity.projects @@ -49,9 +54,6 @@ def setUp(self): def setup_images_mock(self, count): images = image_fakes.FakeImage.create_images(count=count) - self.images_mock.get = image_fakes.FakeImage.get_images( - images, - 0) return images @@ -64,26 +66,22 @@ def setUp(self): super(TestImageCreate, self).setUp() self.new_image = image_fakes.FakeImage.create_one_image() - self.images_mock.create.return_value = self.new_image + self.client.create_image.return_value = self.new_image self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain - # This is the return value for utils.find_resource() - self.images_mock.get.return_value = copy.deepcopy( - self.new_image - ) - self.images_mock.update.return_value = self.new_image + self.client.update_image.return_value = self.new_image + + (self.expected_columns, self.expected_data) = zip( + *sorted(image._format_image(self.new_image).items())) # Get the command object to test self.cmd = image.CreateImage(self.app, None) - def test_image_reserve_no_options(self): - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch("sys.stdin", side_effect=[None]) + def test_image_reserve_no_options(self, raw_input): arglist = [ self.new_image.name ] @@ -100,45 +98,34 @@ def test_image_reserve_no_options(self): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) - - self.images_mock.upload.assert_called_with( - mock.ANY, mock.ANY, - ) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual( - image_fakes.FakeImage.get_image_columns(self.new_image), + self.expected_columns, columns) self.assertItemEqual( - image_fakes.FakeImage.get_image_data(self.new_image), + self.expected_data, data) - @mock.patch('glanceclient.common.utils.get_data_file', name='Open') - def test_image_reserve_options(self, mock_open): - mock_file = mock.MagicMock(name='File') - mock_open.return_value = mock_file - mock_open.read.return_value = None - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_reserve_options(self, raw_input): arglist = [ '--container-format', 'ovf', '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', ('--protected' - if self.new_image.protected else '--unprotected'), + if self.new_image.is_protected else '--unprotected'), ('--private' if self.new_image.visibility == 'private' else '--public'), - '--project', self.new_image.owner, + '--project', self.new_image.owner_id, '--project-domain', self.domain.id, self.new_image.name, ] @@ -147,11 +134,11 @@ def test_image_reserve_options(self, mock_open): ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), - ('protected', self.new_image.protected), - ('unprotected', not self.new_image.protected), + ('protected', self.new_image.is_protected), + ('unprotected', not self.new_image.is_protected), ('public', self.new_image.visibility == 'public'), ('private', self.new_image.visibility == 'private'), - ('project', self.new_image.owner), + ('project', self.new_image.owner_id), ('project_domain', self.domain.id), ('name', self.new_image.name), ] @@ -163,29 +150,22 @@ def test_image_reserve_options(self, mock_open): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format='ovf', disk_format='ami', min_disk=10, min_ram=4, - owner=self.project.id, - protected=self.new_image.protected, + owner_id=self.project.id, + is_protected=self.new_image.is_protected, visibility=self.new_image.visibility, ) - # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) - - self.images_mock.upload.assert_called_with( - mock.ANY, mock.ANY, - ) - self.assertEqual( - image_fakes.FakeImage.get_image_columns(self.new_image), + self.expected_columns, columns) self.assertItemEqual( - image_fakes.FakeImage.get_image_data(self.new_image), + self.expected_data, data) def test_image_create_with_unexist_project(self): @@ -222,21 +202,15 @@ def test_image_create_with_unexist_project(self): parsed_args, ) - @mock.patch('glanceclient.common.utils.get_data_file', name='Open') - def test_image_create_file(self, mock_open): - mock_file = mock.MagicMock(name='File') - mock_open.return_value = mock_file - mock_open.read.return_value = ( - image_fakes.FakeImage.get_image_data(self.new_image)) - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + def test_image_create_file(self): + imagefile = tempfile.NamedTemporaryFile(delete=False) + imagefile.write(b'\0') + imagefile.close() arglist = [ - '--file', 'filer', + '--file', imagefile.name, ('--unprotected' - if not self.new_image.protected else '--protected'), + if not self.new_image.is_protected else '--protected'), ('--public' if self.new_image.visibility == 'public' else '--private'), '--property', 'Alpha=1', @@ -246,9 +220,9 @@ def test_image_create_file(self, mock_open): self.new_image.name, ] verifylist = [ - ('file', 'filer'), - ('protected', self.new_image.protected), - ('unprotected', not self.new_image.protected), + ('file', imagefile.name), + ('protected', self.new_image.is_protected), + ('unprotected', not self.new_image.is_protected), ('public', self.new_image.visibility == 'public'), ('private', self.new_image.visibility == 'private'), ('properties', {'Alpha': '1', 'Beta': '2'}), @@ -263,29 +237,23 @@ def test_image_create_file(self, mock_open): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, - protected=self.new_image.protected, + is_protected=self.new_image.is_protected, visibility=self.new_image.visibility, Alpha='1', Beta='2', tags=self.new_image.tags, - ) - - # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) - - self.images_mock.upload.assert_called_with( - mock.ANY, mock.ANY, + filename=imagefile.name ) self.assertEqual( - image_fakes.FakeImage.get_image_columns(self.new_image), + self.expected_columns, columns) self.assertItemEqual( - image_fakes.FakeImage.get_image_data(self.new_image), + self.expected_data, data) def test_image_create_dead_options(self): @@ -315,25 +283,31 @@ class TestAddProjectToImage(TestImage): ) columns = ( + 'created_at', 'image_id', 'member_id', + 'schema', 'status', + 'updated_at' ) datalist = ( + new_member.created_at, _image.id, new_member.member_id, + new_member.schema, new_member.status, + new_member.updated_at ) def setUp(self): super(TestAddProjectToImage, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image + self.client.find_image.return_value = self._image # Update the image_id in the MEMBER dict - self.image_members_mock.create.return_value = self.new_member + self.client.add_member.return_value = self.new_member self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain # Get the command object to test @@ -354,9 +328,9 @@ def test_add_project_to_image_no_option(self): # 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.image_members_mock.create.assert_called_with( - self._image.id, - self.project.id + self.client.add_member.assert_called_with( + image=self._image.id, + member_id=self.project.id ) self.assertEqual(self.columns, columns) @@ -379,10 +353,11 @@ def test_add_project_to_image_with_option(self): # 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.image_members_mock.create.assert_called_with( - self._image.id, - self.project.id + self.client.add_member.assert_called_with( + image=self._image.id, + member_id=self.project.id ) + self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -392,7 +367,7 @@ class TestImageDelete(TestImage): def setUp(self): super(TestImageDelete, self).setUp() - self.images_mock.delete.return_value = None + self.client.delete_image.return_value = None # Get the command object to test self.cmd = image.DeleteImage(self.app, None) @@ -408,9 +383,11 @@ def test_image_delete_no_options(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.client.find_image.side_effect = images + result = self.cmd.take_action(parsed_args) - self.images_mock.delete.assert_called_with(images[0].id) + self.client.delete_image.assert_called_with(images[0].id) self.assertIsNone(result) def test_image_delete_multi_images(self): @@ -422,10 +399,12 @@ def test_image_delete_multi_images(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.client.find_image.side_effect = images + result = self.cmd.take_action(parsed_args) calls = [mock.call(i.id) for i in images] - self.images_mock.delete.assert_has_calls(calls) + self.client.delete_image.assert_has_calls(calls) self.assertIsNone(result) def test_image_delete_multi_images_exception(self): @@ -449,15 +428,15 @@ def test_image_delete_multi_images_exception(self): ret_find = [ images[0], images[1], - exceptions.NotFound('404'), + sdk_exceptions.ResourceNotFound() ] - self.images_mock.get = Exception() - self.images_mock.find.side_effect = ret_find + self.client.find_image.side_effect = ret_find + self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) calls = [mock.call(i.id) for i in images] - self.images_mock.delete.assert_has_calls(calls) + self.client.delete_image.assert_has_calls(calls) class TestImageList(TestImage): @@ -473,17 +452,17 @@ class TestImageList(TestImage): datalist = ( _image.id, _image.name, - '', + None, ), def setUp(self): super(TestImageList, self).setUp() self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ + self.api_mock.side_effect = [ [self._image], [], ] - self.app.client_manager.image.api = self.api_mock + self.client.images = self.api_mock # Get the command object to test self.cmd = image.ListImage(self.app, None) @@ -503,8 +482,8 @@ def test_image_list_no_options(self): # 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.api_mock.image_list.assert_called_with( - marker=self._image.id, + self.client.images.assert_called_with( + # marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -527,9 +506,8 @@ def test_image_list_public_option(self): # 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.api_mock.image_list.assert_called_with( - public=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='public', ) self.assertEqual(self.columns, columns) @@ -552,9 +530,8 @@ def test_image_list_private_option(self): # 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.api_mock.image_list.assert_called_with( - private=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='private', ) self.assertEqual(self.columns, columns) @@ -577,9 +554,8 @@ def test_image_list_community_option(self): # 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.api_mock.image_list.assert_called_with( - community=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='community', ) self.assertEqual(self.columns, columns) @@ -602,9 +578,8 @@ def test_image_list_shared_option(self): # 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.api_mock.image_list.assert_called_with( - shared=True, - marker=self._image.id, + self.client.images.assert_called_with( + visibility='shared', ) self.assertEqual(self.columns, columns) @@ -629,10 +604,9 @@ def test_image_list_shared_member_status_option(self): # 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.api_mock.image_list.assert_called_with( - shared=True, + self.client.images.assert_called_with( + visibility='shared', member_status='all', - marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -666,8 +640,7 @@ def test_image_list_long_option(self): # 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.api_mock.image_list.assert_called_with( - marker=self._image.id, + self.client.images.assert_called_with( ) collist = ( @@ -688,14 +661,14 @@ def test_image_list_long_option(self): datalist = (( self._image.id, self._image.name, - '', - '', - '', - '', - '', + None, + None, + None, + None, + None, self._image.visibility, - self._image.protected, - self._image.owner, + self._image.is_protected, + self._image.owner_id, format_columns.ListColumn(self._image.tags), ), ) self.assertListItemEqual(datalist, tuple(data)) @@ -716,8 +689,7 @@ def test_image_list_property_option(self, sf_mock): # 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.api_mock.image_list.assert_called_with( - marker=self._image.id, + self.client.images.assert_called_with( ) sf_mock.assert_called_with( [self._image], @@ -741,8 +713,7 @@ def test_image_list_sort_option(self, si_mock): # 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.api_mock.image_list.assert_called_with( - marker=self._image.id, + self.client.images.assert_called_with( ) si_mock.assert_called_with( [self._image], @@ -763,8 +734,10 @@ def test_image_list_limit_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - limit=ret_limit, marker=None + self.client.images.assert_called_with( + limit=ret_limit, + paginated=False + # marker=None ) self.assertEqual(self.columns, columns) @@ -775,8 +748,9 @@ def test_image_list_marker_option(self, fr_mock): # tangchen: Since image_fakes.IMAGE is a dict, it cannot offer a .id # operation. Will fix this by using FakeImage class instead # of IMAGE dict. - fr_mock.return_value = mock.Mock() - fr_mock.return_value.id = image_fakes.image_id + self.client.find_image = mock.Mock(return_value=self._image) +# fr_mock.return_value = mock.Mock() +# fr_mock.return_value.id = image_fakes.image_id arglist = [ '--marker', image_fakes.image_name, @@ -787,10 +761,12 @@ def test_image_list_marker_option(self, fr_mock): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - marker=image_fakes.image_id, + self.client.images.assert_called_with( + marker=self._image.id, ) + self.client.find_image.assert_called_with(image_fakes.image_name) + def test_image_list_name_option(self): arglist = [ '--name', 'abc', @@ -801,8 +777,9 @@ def test_image_list_name_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - name='abc', marker=self._image.id + self.client.images.assert_called_with( + name='abc', + # marker=self._image.id ) def test_image_list_status_option(self): @@ -815,8 +792,8 @@ def test_image_list_status_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - status='active', marker=self._image.id + self.client.images.assert_called_with( + status='active' ) def test_image_list_tag_option(self): @@ -829,8 +806,8 @@ def test_image_list_tag_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with( - tag='abc', marker=self._image.id + self.client.images.assert_called_with( + tag='abc' ) @@ -849,17 +826,17 @@ class TestListImageProjects(TestImage): "Status" ) - datalist = (( + datalist = [( _image.id, member.member_id, member.status, - )) + )] def setUp(self): super(TestListImageProjects, self).setUp() - self.images_mock.get.return_value = self._image - self.image_members_mock.list.return_value = self.datalist + self.client.find_image.return_value = self._image + self.client.members.return_value = [self.member] self.cmd = image.ListImageProjects(self.app, None) @@ -874,10 +851,10 @@ def test_image_member_list(self): columns, data = self.cmd.take_action(parsed_args) - self.image_members_mock.list.assert_called_with(self._image.id) + self.client.members.assert_called_with(image=self._image.id) self.assertEqual(self.columns, columns) - self.assertEqual(len(self.datalist), len(tuple(data))) + self.assertEqual(self.datalist, list(data)) class TestRemoveProjectImage(TestImage): @@ -890,11 +867,11 @@ def setUp(self): self._image = image_fakes.FakeImage.create_one_image() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image + self.client.find_image.return_value = self._image self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain - self.image_members_mock.delete.return_value = None + self.client.remove_member.return_value = None # Get the command object to test self.cmd = image.RemoveProjectImage(self.app, None) @@ -911,9 +888,13 @@ def test_remove_project_image_no_options(self): result = self.cmd.take_action(parsed_args) - self.image_members_mock.delete.assert_called_with( + self.client.find_image.assert_called_with( self._image.id, - self.project.id, + ignore_missing=False) + + self.client.remove_member.assert_called_with( + member=self.project.id, + image=self._image.id, ) self.assertIsNone(result) @@ -932,9 +913,9 @@ def test_remove_project_image_with_options(self): result = self.cmd.take_action(parsed_args) - self.image_members_mock.delete.assert_called_with( - self._image.id, - self.project.id, + self.client.remove_member.assert_called_with( + member=self.project.id, + image=self._image.id, ) self.assertIsNone(result) @@ -943,21 +924,16 @@ class TestImageSet(TestImage): project = identity_fakes.FakeProject.create_one_project() domain = identity_fakes.FakeDomain.create_one_domain() + _image = image_fakes.FakeImage.create_one_image({'tags': []}) def setUp(self): super(TestImageSet, self).setUp() - # Set up the schema - self.model = warlock.model_factory( - image_fakes.IMAGE_schema, - schemas.SchemaBasedModel, - ) self.project_mock.get.return_value = self.project self.domain_mock.get.return_value = self.domain - self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) - self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) + self.client.find_image.return_value = self._image self.app.client_manager.auth_ref = mock.Mock( project_id=self.project.id, @@ -986,38 +962,38 @@ def test_image_set_membership_option_accept(self): attrs={'image_id': image_fakes.image_id, 'member_id': self.project.id} ) - self.image_members_mock.update.return_value = membership + self.client.update_member.return_value = membership arglist = [ '--accept', - image_fakes.image_id, + self._image.id, ] verifylist = [ ('accept', True), ('reject', False), ('pending', False), - ('image', image_fakes.image_id) + ('image', self._image.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.image_members_mock.update.assert_called_once_with( - image_fakes.image_id, - self.app.client_manager.auth_ref.project_id, - 'accepted', + self.client.update_member.assert_called_once_with( + image=self._image.id, + member=self.app.client_manager.auth_ref.project_id, + status='accepted', ) # Assert that the 'update image" route is also called, in addition to # the 'update membership' route. - self.images_mock.update.assert_called_with(image_fakes.image_id) + self.client.update_image.assert_called_with(self._image.id) def test_image_set_membership_option_reject(self): membership = image_fakes.FakeImage.create_one_image_member( attrs={'image_id': image_fakes.image_id, 'member_id': self.project.id} ) - self.image_members_mock.update.return_value = membership + self.client.update_member.return_value = membership arglist = [ '--reject', @@ -1033,22 +1009,22 @@ def test_image_set_membership_option_reject(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.image_members_mock.update.assert_called_once_with( - image_fakes.image_id, - self.app.client_manager.auth_ref.project_id, - 'rejected', + self.client.update_member.assert_called_once_with( + image=self._image.id, + member=self.app.client_manager.auth_ref.project_id, + status='rejected', ) # Assert that the 'update image" route is also called, in addition to # the 'update membership' route. - self.images_mock.update.assert_called_with(image_fakes.image_id) + self.client.update_image.assert_called_with(self._image.id) def test_image_set_membership_option_pending(self): membership = image_fakes.FakeImage.create_one_image_member( attrs={'image_id': image_fakes.image_id, 'member_id': self.project.id} ) - self.image_members_mock.update.return_value = membership + self.client.update_member.return_value = membership arglist = [ '--pending', @@ -1064,15 +1040,15 @@ def test_image_set_membership_option_pending(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.image_members_mock.update.assert_called_once_with( - image_fakes.image_id, - self.app.client_manager.auth_ref.project_id, - 'pending', + self.client.update_member.assert_called_once_with( + image=self._image.id, + member=self.app.client_manager.auth_ref.project_id, + status='pending', ) # Assert that the 'update image" route is also called, in addition to # the 'update membership' route. - self.images_mock.update.assert_called_with(image_fakes.image_id) + self.client.update_image.assert_called_with(self._image.id) def test_image_set_options(self): arglist = [ @@ -1083,7 +1059,7 @@ def test_image_set_options(self): '--disk-format', 'vmdk', '--project', self.project.name, '--project-domain', self.domain.id, - image_fakes.image_id, + self._image.id, ] verifylist = [ ('name', 'new-name'), @@ -1093,7 +1069,7 @@ def test_image_set_options(self): ('disk_format', 'vmdk'), ('project', self.project.name), ('project_domain', self.domain.id), - ('image', image_fakes.image_id), + ('image', self._image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1101,15 +1077,15 @@ def test_image_set_options(self): kwargs = { 'name': 'new-name', - 'owner': self.project.id, + 'owner_id': self.project.id, 'min_disk': 2, 'min_ram': 4, 'container_format': 'ovf', 'disk_format': 'vmdk', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, **kwargs) + self.client.update_image.assert_called_with( + self._image.id, **kwargs) self.assertIsNone(result) def test_image_set_with_unexist_project(self): @@ -1148,12 +1124,12 @@ def test_image_set_bools1(self): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': True, + 'is_protected': True, 'visibility': 'private', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1176,12 +1152,12 @@ def test_image_set_bools2(self): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': False, + 'is_protected': False, 'visibility': 'public', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1205,8 +1181,8 @@ def test_image_set_properties(self): 'Beta': '2', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1243,8 +1219,8 @@ def test_image_set_fake_properties(self): 'ramdisk_id': 'xyzpdq', } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1266,8 +1242,8 @@ def test_image_set_tag(self): 'tags': ['test-tag'], } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1290,12 +1266,12 @@ def test_image_set_activate(self): 'tags': ['test-tag'], } - self.images_mock.reactivate.assert_called_with( - image_fakes.image_id, + self.client.reactivate_image.assert_called_with( + self._image.id, ) # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1318,20 +1294,20 @@ def test_image_set_deactivate(self): 'tags': ['test-tag'], } - self.images_mock.deactivate.assert_called_with( - image_fakes.image_id, + self.client.deactivate_image.assert_called_with( + self._image.id, ) # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) def test_image_set_tag_merge(self): - old_image = copy.copy(image_fakes.IMAGE) + old_image = self._image old_image['tags'] = ['old1', 'new2'] - self.images_mock.get.return_value = self.model(**old_image) + self.client.find_image.return_value = old_image arglist = [ '--tag', 'test-tag', image_fakes.image_name, @@ -1348,16 +1324,16 @@ def test_image_set_tag_merge(self): 'tags': ['old1', 'new2', 'test-tag'], } # ImageManager.update(image, **kwargs) - a, k = self.images_mock.update.call_args - self.assertEqual(image_fakes.image_id, a[0]) + a, k = self.client.update_image.call_args + self.assertEqual(self._image.id, a[0]) self.assertIn('tags', k) self.assertEqual(set(kwargs['tags']), set(k['tags'])) self.assertIsNone(result) def test_image_set_tag_merge_dupe(self): - old_image = copy.copy(image_fakes.IMAGE) + old_image = self._image old_image['tags'] = ['old1', 'new2'] - self.images_mock.get.return_value = self.model(**old_image) + self.client.find_image.return_value = old_image arglist = [ '--tag', 'old1', image_fakes.image_name, @@ -1374,8 +1350,8 @@ def test_image_set_tag_merge_dupe(self): 'tags': ['new2', 'old1'], } # ImageManager.update(image, **kwargs) - a, k = self.images_mock.update.call_args - self.assertEqual(image_fakes.image_id, a[0]) + a, k = self.client.update_image.call_args + self.assertEqual(self._image.id, a[0]) self.assertIn('tags', k) self.assertEqual(set(kwargs['tags']), set(k['tags'])) self.assertIsNone(result) @@ -1416,8 +1392,8 @@ def test_image_set_numeric_options_to_zero(self): 'min_ram': 0, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, + self.client.update_image.assert_called_with( + self._image.id, **kwargs ) self.assertIsNone(result) @@ -1428,16 +1404,25 @@ class TestImageShow(TestImage): new_image = image_fakes.FakeImage.create_one_image( attrs={'size': 1000}) + _data = image_fakes.FakeImage.create_one_image() + + columns = ( + 'id', 'name', 'owner', 'protected', 'tags', 'visibility' + ) + + data = ( + _data.id, + _data.name, + _data.owner_id, + _data.is_protected, + format_columns.ListColumn(_data.tags), + _data.visibility + ) + def setUp(self): super(TestImageShow, self).setUp() - # Set up the schema - self.model = warlock.model_factory( - image_fakes.IMAGE_schema, - schemas.SchemaBasedModel, - ) - - self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) + self.client.find_image = mock.Mock(return_value=self._data) # Get the command object to test self.cmd = image.ShowImage(self.app, None) @@ -1455,15 +1440,16 @@ def test_image_show(self): # 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.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( image_fakes.image_id, + ignore_missing=False ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertItemEqual(image_fakes.IMAGE_SHOW_data, data) + self.assertEqual(self.columns, columns) + self.assertItemEqual(self.data, data) def test_image_show_human_readable(self): - self.images_mock.get.return_value = self.new_image + self.client.find_image.return_value = self.new_image arglist = [ '--human-readable', self.new_image.id, @@ -1478,8 +1464,9 @@ def test_image_show_human_readable(self): # 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.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( self.new_image.id, + ignore_missing=False ) size_index = columns.index('size') @@ -1491,19 +1478,15 @@ class TestImageUnset(TestImage): attrs = {} attrs['tags'] = ['test'] attrs['prop'] = 'test' + attrs['prop2'] = 'fake' image = image_fakes.FakeImage.create_one_image(attrs) def setUp(self): super(TestImageUnset, self).setUp() - # Set up the schema - self.model = warlock.model_factory( - image_fakes.IMAGE_schema, - schemas.SchemaBasedModel, - ) - - self.images_mock.get.return_value = self.image - self.image_tags_mock.delete.return_value = self.image + self.client.find_image.return_value = self.image + self.client.remove_tag.return_value = self.image + self.client.update_image.return_value = self.image # Get the command object to test self.cmd = image.UnsetImage(self.app, None) @@ -1535,7 +1518,7 @@ def test_image_unset_tag_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.image_tags_mock.delete.assert_called_with( + self.client.remove_tag.assert_called_with( self.image.id, 'test' ) self.assertIsNone(result) @@ -1555,9 +1538,9 @@ def test_image_unset_property_option(self): result = self.cmd.take_action(parsed_args) kwargs = {} - self.images_mock.update.assert_called_with( - self.image.id, - parsed_args.properties, + self.client.update_image.assert_called_with( + self.image, + properties={'prop2': 'fake'}, **kwargs) self.assertIsNone(result) @@ -1579,12 +1562,12 @@ def test_image_unset_mixed_option(self): result = self.cmd.take_action(parsed_args) kwargs = {} - self.images_mock.update.assert_called_with( - self.image.id, - parsed_args.properties, + self.client.update_image.assert_called_with( + self.image, + properties={'prop2': 'fake'}, **kwargs) - self.image_tags_mock.delete.assert_called_with( + self.client.remove_tag.assert_called_with( self.image.id, 'test' ) self.assertIsNone(result) @@ -1597,18 +1580,13 @@ class TestImageSave(TestImage): def setUp(self): super(TestImageSave, self).setUp() - # Generate a request id - self.resp = mock.MagicMock() - self.resp.headers['x-openstack-request-id'] = 'req_id' + self.client.find_image.return_value = self.image + self.client.download_image.return_value = self.image # Get the command object to test self.cmd = image.SaveImage(self.app, None) def test_save_data(self): - req_id_proxy = glanceclient_utils.RequestIdProxy( - ['some_data', self.resp] - ) - self.images_mock.data.return_value = req_id_proxy arglist = ['--file', '/path/to/file', self.image.id] @@ -1618,23 +1596,58 @@ def test_save_data(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch('glanceclient.common.utils.save_image') as mocked_save: - self.cmd.take_action(parsed_args) - mocked_save.assert_called_once_with(req_id_proxy, '/path/to/file') + self.cmd.take_action(parsed_args) - def test_save_no_data(self): - req_id_proxy = glanceclient_utils.RequestIdProxy( - [None, self.resp] - ) - self.images_mock.data.return_value = req_id_proxy + self.client.download_image.assert_called_once_with( + self.image.id, + output='/path/to/file') - arglist = ['--file', '/path/to/file', self.image.id] - verifylist = [ - ('file', '/path/to/file'), - ('image', self.image.id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) +class TestImageGetData(TestImage): + + def setUp(self): + super(TestImageGetData, self).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") + + with mock.patch('sys.stdin') as stdin: + stdin.return_value = f + stdin.isatty.return_value = False + stdin.buffer = f + + (test_fd, test_name) = image.get_data_file(self.args) + + # 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 + + f = 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 + + (test_fd, test_fname) = image.get_data_file(self.args) - # Raise SystemExit if no data was provided. - self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + self.assertIsNone(test_fd) + self.assertIsNone(test_fname) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 5d41b3a180..4e204ad1b2 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -41,8 +41,8 @@ def setUp(self): self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.find_image_mock = self.app.client_manager.image.find_image + self.find_image_mock.reset_mock() self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock.reset_mock() @@ -222,7 +222,7 @@ def test_volume_create_properties(self): def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = image + self.find_image_mock.return_value = image arglist = [ '--image', image.id, @@ -260,7 +260,7 @@ def test_volume_create_image_id(self): def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() - self.images_mock.get.return_value = image + self.find_image_mock.return_value = image arglist = [ '--image', image.name, diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 4dde1340a7..1e0cb183fc 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -193,9 +193,8 @@ def take_action(self, parsed_args): image = None if parsed_args.image: - image = utils.find_resource( - image_client.images, - parsed_args.image).id + image = image_client.find_image(parsed_args.image, + ignore_missing=False).id size = parsed_args.size diff --git a/releasenotes/notes/use_sdk_for_image-f49d2df38e7d9f81.yaml b/releasenotes/notes/use_sdk_for_image-f49d2df38e7d9f81.yaml new file mode 100644 index 0000000000..ba10f388c7 --- /dev/null +++ b/releasenotes/notes/use_sdk_for_image-f49d2df38e7d9f81.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Image service for v2 is switched from using glanceclient to OpenStackSDK. diff --git a/requirements.txt b/requirements.txt index aaea495e14..bf2a0590f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,7 @@ six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 -keystoneauth1>=3.14.0 # Apache-2.0 -openstacksdk>=0.17.0 # Apache-2.0 +openstacksdk>=0.36.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 From cdac869412a48ba0e33542ff7859bcc5ccc0544e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 24 Mar 2020 07:23:00 -0500 Subject: [PATCH 0203/1120] Honor endpoint override from config for volume I'm guessing we should do this for everyone, but we have volume on the brain right now. Rackspace is in the weird situation where they do support v2 but only have v1 in the catalog (wut) So we need to override the block-storage enpdoint by config. To do that, we need to actually honor the config setting over here in OSC. NOTE: We need to systemically overhaul how we're injesting config over here - because there's too much variation. But we can leave that for another day. Story: 2007459 Task: 39137 Change-Id: Ifddf1ddd5abaa768ab18049c09d18bc269f3a4f5 --- openstackclient/volume/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index fdd1794be5..1fbfaaee78 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -67,11 +67,15 @@ def make_client(instance): # Remember interface only if it is set kwargs = utils.build_kwargs_dict('endpoint_type', instance.interface) + endpoint_override = instance.sdk_connection.config.get_endpoint( + 'block-storage') + client = volume_client( session=instance.session, extensions=extensions, http_log_debug=http_log_debug, region_name=instance.region_name, + endpoint_override=endpoint_override, **kwargs ) From 768a64aac5bd14f56c142faeec1793aac91947cb Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 17 Dec 2019 14:43:21 +0100 Subject: [PATCH 0204/1120] Complete switch from glanceclient to SDK for image service In https://review.opendev.org/#/c/650374/ a work has been started to switch image service support from glanceclient with all it's dependencies to the SDK version. With this change version 1 (anyway deprecated since ages) is also being switched to SDK. Change-Id: Ic391500af02a73d81d64a9e9113cca85c9e24390 --- openstackclient/image/client.py | 58 +----- openstackclient/image/v1/image.py | 138 +++++++------- openstackclient/tests/unit/image/v1/fakes.py | 10 +- .../tests/unit/image/v1/test_image.py | 173 +++++++----------- ...omplete_image_switch-203e0b3105a54674.yaml | 4 + requirements.txt | 1 - 6 files changed, 155 insertions(+), 229 deletions(-) create mode 100644 releasenotes/notes/complete_image_switch-203e0b3105a54674.yaml diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 15bea17eb6..9a0d7bacb1 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -26,64 +26,18 @@ API_VERSION_OPTION = 'os_image_api_version' API_NAME = "image" API_VERSIONS = { - "1": "glanceclient.v1.client.Client", + "1": "openstack.connection.Connection", "2": "openstack.connection.Connection", } -IMAGE_API_TYPE = 'image' -IMAGE_API_VERSIONS = { - '1': 'openstackclient.api.image_v1.APIv1', - '2': 'openstackclient.api.image_v2.APIv2', -} - def make_client(instance): - if instance._api_version[API_NAME] != '1': - LOG.debug( - 'Image client initialized using OpenStack SDK: %s', - instance.sdk_connection.image, - ) - return instance.sdk_connection.image - else: - """Returns an image service client""" - image_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS) - LOG.debug('Instantiating image client: %s', image_client) - - endpoint = instance.get_endpoint_for_service_type( - API_NAME, - region_name=instance.region_name, - interface=instance.interface, - ) - - client = image_client( - endpoint, - token=instance.auth.get_token(instance.session), - cacert=instance.cacert, - insecure=not instance.verify, - ) - - # Create the low-level API - - image_api = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - IMAGE_API_VERSIONS) - LOG.debug('Instantiating image api: %s', image_api) - - client.api = image_api( - session=instance.session, - endpoint=instance.get_endpoint_for_service_type( - IMAGE_API_TYPE, - region_name=instance.region_name, - interface=instance.interface, - ) - ) - - return client + LOG.debug( + 'Image client initialized using OpenStack SDK: %s', + instance.sdk_connection.image, + ) + return instance.sdk_connection.image def build_option_parser(parser): diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index a711a1288b..cf1d68171a 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -22,13 +22,13 @@ import sys from cliff import columns as cliff_columns -from glanceclient.common import utils as gc_utils from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils +from openstackclient.common import sdk_utils from openstackclient.i18n import _ if os.name == "nt": @@ -47,6 +47,36 @@ LOG = logging.getLogger(__name__) +def _get_columns(item): + # Trick sdk_utils to return URI attribute + column_map = { + 'is_protected': 'protected', + 'owner_id': 'owner' + } + hidden_columns = ['location', 'checksum', + 'copy_from', 'created_at', 'status', 'updated_at'] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item.to_dict(), column_map, hidden_columns) + + +_formatters = { +} + + +class HumanReadableSizeColumn(cliff_columns.FormattableColumn): + def human_readable(self): + """Return a formatted visibility string + + :rtype: + A string formatted to public/private + """ + + if self._value: + return utils.format_size(self._value) + else: + return '' + + class VisibilityColumn(cliff_columns.FormattableColumn): def human_readable(self): """Return a formatted visibility string @@ -210,7 +240,7 @@ def take_action(self, parsed_args): # Special case project option back to API attribute name 'owner' val = getattr(parsed_args, 'project', None) if val: - kwargs['owner'] = val + kwargs['owner_id'] = val # Handle exclusive booleans with care # Avoid including attributes in kwargs if an option is not @@ -219,9 +249,9 @@ def take_action(self, parsed_args): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['is_public'] = True if parsed_args.private: @@ -250,27 +280,35 @@ def take_action(self, parsed_args): kwargs["data"] = io.open(parsed_args.file, "rb") else: # Read file from stdin - if sys.stdin.isatty() is not True: + if not sys.stdin.isatty(): if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - # Send an open file handle to glanceclient so it will - # do a chunked transfer - kwargs["data"] = sys.stdin + if hasattr(sys.stdin, 'buffer'): + kwargs['data'] = sys.stdin.buffer + else: + kwargs["data"] = sys.stdin if not parsed_args.volume: # Wrap the call to catch exceptions in order to close files try: - image = image_client.images.create(**kwargs) + image = image_client.create_image(**kwargs) finally: # Clean up open files - make sure data isn't a string if ('data' in kwargs and hasattr(kwargs['data'], 'close') and kwargs['data'] != sys.stdin): kwargs['data'].close() + if image: + display_columns, columns = _get_columns(image) + _formatters['properties'] = format_columns.DictColumn + data = utils.get_item_properties(image, columns, + formatters=_formatters) + return (display_columns, data) + elif info: info.update(image._info) info['properties'] = format_columns.DictColumn( info.get('properties', {})) - return zip(*sorted(info.items())) + return zip(*sorted(info.items())) class DeleteImage(command.Command): @@ -289,11 +327,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): image_client = self.app.client_manager.image for image in parsed_args.images: - image_obj = utils.find_resource( - image_client.images, - image, - ) - image_client.images.delete(image_obj.id) + image_obj = image_client.find_image(image) + image_client.delete_image(image_obj.id) class ListImage(command.Lister): @@ -359,15 +394,9 @@ def take_action(self, parsed_args): kwargs = {} if parsed_args.public: - kwargs['public'] = True + kwargs['is_public'] = True if parsed_args.private: - kwargs['private'] = True - # Note: We specifically need to do that below to get the 'status' - # column. - # - # Always set kwargs['detailed'] to True, and then filter the columns - # according to whether the --long option is specified or not. - kwargs['detailed'] = True + kwargs['is_private'] = True if parsed_args.long: columns = ( @@ -379,8 +408,8 @@ def take_action(self, parsed_args): 'Checksum', 'Status', 'is_public', - 'protected', - 'owner', + 'is_protected', + 'owner_id', 'properties', ) column_headers = ( @@ -401,16 +430,7 @@ def take_action(self, parsed_args): column_headers = columns # List of image data received - data = [] - # No pages received yet, so start the page marker at None. - marker = None - while True: - page = image_client.api.image_list(marker=marker, **kwargs) - if not page: - break - data.extend(page) - # Set the marker to the id of the last item we received - marker = page[-1]['id'] + data = list(image_client.images(**kwargs)) if parsed_args.property: # NOTE(dtroyer): coerce to a list to subscript it in py3 @@ -426,7 +446,7 @@ def take_action(self, parsed_args): return ( column_headers, - (utils.get_dict_properties( + (utils.get_item_properties( s, columns, formatters={ @@ -456,13 +476,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - data = image_client.images.data(image) + image = image_client.find_image(parsed_args.image) - gc_utils.save_image(data, parsed_args.file) + image_client.download_image(image.id, output=parsed_args.file) class SetImage(command.Command): @@ -621,22 +637,17 @@ def take_action(self, parsed_args): # to do nothing when no options are present as opposed to always # setting a default. if parsed_args.protected: - kwargs['protected'] = True + kwargs['is_protected'] = True if parsed_args.unprotected: - kwargs['protected'] = False + kwargs['is_protected'] = False if parsed_args.public: kwargs['is_public'] = True if parsed_args.private: kwargs['is_public'] = False - if parsed_args.force: - kwargs['force'] = True # Wrap the call to catch exceptions in order to close files try: - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image) if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: @@ -666,9 +677,10 @@ def take_action(self, parsed_args): if parsed_args.stdin: if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - # Send an open file handle to glanceclient so it - # will do a chunked transfer - kwargs["data"] = sys.stdin + if hasattr(sys.stdin, 'buffer'): + kwargs['data'] = sys.stdin.buffer + else: + kwargs["data"] = sys.stdin else: LOG.warning(_('Use --stdin to enable read image ' 'data from standard input')) @@ -677,7 +689,7 @@ def take_action(self, parsed_args): image.properties.update(kwargs['properties']) kwargs['properties'] = image.properties - image = image_client.images.update(image.id, **kwargs) + image = image_client.update_image(image.id, **kwargs) finally: # Clean up open files - make sure data isn't a string if ('data' in kwargs and hasattr(kwargs['data'], 'close') and @@ -705,16 +717,12 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) + image = image_client.find_image(parsed_args.image) - info = {} - info.update(image._info) if parsed_args.human_readable: - if 'size' in info: - info['size'] = utils.format_size(info['size']) - info['properties'] = format_columns.DictColumn( - info.get('properties', {})) - return zip(*sorted(info.items())) + _formatters['size'] = HumanReadableSizeColumn + display_columns, columns = _get_columns(image) + _formatters['properties'] = format_columns.DictColumn + data = utils.get_item_properties(image, columns, + formatters=_formatters) + return (display_columns, data) diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index de23223572..add3978d1d 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -13,10 +13,11 @@ # under the License. # -import copy from unittest import mock import uuid +from openstack.image.v1 import image + from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes @@ -111,13 +112,10 @@ def create_one_image(attrs=None): 'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}, + 'status': 'status' + uuid.uuid4().hex } # Overwrite default attributes if there are some attributes set image_info.update(attrs) - image = fakes.FakeResource( - info=copy.deepcopy(image_info), - loaded=True) - - return image + return image.Image(**image_info) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 970b36c68a..2f190a7a3c 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -17,7 +17,6 @@ from unittest import mock from osc_lib.cli import format_columns -from osc_lib import exceptions from openstackclient.image.v1 import image from openstackclient.tests.unit import fakes @@ -29,9 +28,8 @@ class TestImage(image_fakes.TestImagev1): def setUp(self): super(TestImage, self).setUp() - # Get a shortcut to the ServerManager Mock - self.images_mock = self.app.client_manager.image.images - self.images_mock.reset_mock() + self.app.client_manager.image = mock.Mock() + self.client = self.app.client_manager.image class TestImageCreate(TestImage): @@ -48,6 +46,7 @@ class TestImageCreate(TestImage): 'owner', 'properties', 'protected', + 'size' ) data = ( new_image.container_format, @@ -57,28 +56,24 @@ class TestImageCreate(TestImage): new_image.min_disk, new_image.min_ram, new_image.name, - new_image.owner, + new_image.owner_id, format_columns.DictColumn(new_image.properties), - new_image.protected, + new_image.is_protected, + new_image.size ) def setUp(self): super(TestImageCreate, self).setUp() - self.images_mock.create.return_value = self.new_image - # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self.new_image - self.images_mock.update.return_value = self.new_image + self.client.create_image = mock.Mock(return_value=self.new_image) + self.client.find_image = mock.Mock(return_value=self.new_image) + self.client.update_image = mock.Mock(return_image=self.new_image) # Get the command object to test self.cmd = image.CreateImage(self.app, None) - def test_image_reserve_no_options(self): - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - 'get.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_reserve_no_options(self, raw_input): arglist = [ self.new_image.name, ] @@ -95,25 +90,20 @@ def test_image_reserve_no_options(self): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, - disk_format=image.DEFAULT_DISK_FORMAT, - data=mock.ANY, + disk_format=image.DEFAULT_DISK_FORMAT ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) - def test_image_reserve_options(self): - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - 'get.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_reserve_options(self, raw_input): arglist = [ '--container-format', 'ovf', '--disk-format', 'ami', @@ -144,20 +134,19 @@ def test_image_reserve_options(self): columns, data = self.cmd.take_action(parsed_args) # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format='ovf', disk_format='ami', min_disk=10, min_ram=4, - protected=True, + is_protected=True, is_public=False, - owner='q', - data=mock.ANY, + owner_id='q', ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -167,11 +156,6 @@ def test_image_create_file(self, mock_open): mock_file = mock.Mock(name='File') mock_open.return_value = mock_file mock_open.read.return_value = self.data - mock_exception = { - 'find.side_effect': exceptions.CommandError('x'), - 'get.side_effect': exceptions.CommandError('x'), - } - self.images_mock.configure_mock(**mock_exception) arglist = [ '--file', 'filer', @@ -203,15 +187,12 @@ def test_image_create_file(self, mock_open): # Ensure the input file is closed mock_file.close.assert_called_with() - # ImageManager.get(name) not to be called since update action exists - self.images_mock.get.assert_not_called() - # ImageManager.create(name=, **) - self.images_mock.create.assert_called_with( + self.client.create_image.assert_called_with( name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, - protected=False, + is_protected=False, is_public=True, properties={ 'Alpha': '1', @@ -221,7 +202,7 @@ def test_image_create_file(self, mock_open): ) # Verify update() was not called, if it was show the args - self.assertEqual(self.images_mock.update.call_args_list, []) + self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -235,8 +216,8 @@ def setUp(self): super(TestImageDelete, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image - self.images_mock.delete.return_value = None + self.client.find_image = mock.Mock(return_value=self._image) + self.client.delete_image = mock.Mock(return_value=None) # Get the command object to test self.cmd = image.DeleteImage(self.app, None) @@ -252,7 +233,7 @@ def test_image_delete_no_options(self): result = self.cmd.take_action(parsed_args) - self.images_mock.delete.assert_called_with(self._image.id) + self.client.delete_image.assert_called_with(self._image.id) self.assertIsNone(result) @@ -269,7 +250,7 @@ class TestImageList(TestImage): ( _image.id, _image.name, - '', + _image.status ), ) @@ -277,13 +258,13 @@ class TestImageList(TestImage): info = { 'id': _image.id, 'name': _image.name, - 'owner': _image.owner, + 'owner': _image.owner_id, 'container_format': _image.container_format, 'disk_format': _image.disk_format, 'min_disk': _image.min_disk, 'min_ram': _image.min_ram, 'is_public': _image.is_public, - 'protected': _image.protected, + 'protected': _image.is_protected, 'properties': _image.properties, } image_info = copy.deepcopy(info) @@ -291,11 +272,10 @@ class TestImageList(TestImage): def setUp(self): super(TestImageList, self).setUp() - self.api_mock = mock.Mock() - self.api_mock.image_list.side_effect = [ - [self.image_info], [], + self.client.images = mock.Mock() + self.client.images.side_effect = [ + [self._image], [], ] - self.app.client_manager.image.api = self.api_mock # Get the command object to test self.cmd = image.ListImage(self.app, None) @@ -313,10 +293,7 @@ def test_image_list_no_options(self): # 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.api_mock.image_list.assert_called_with( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -336,10 +313,8 @@ def test_image_list_public_option(self): # 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.api_mock.image_list.assert_called_with( - detailed=True, - public=True, - marker=self._image.id, + self.client.images.assert_called_with( + is_public=True, ) self.assertEqual(self.columns, columns) @@ -360,10 +335,8 @@ def test_image_list_private_option(self): # 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.api_mock.image_list.assert_called_with( - detailed=True, - private=True, - marker=self._image.id, + self.client.images.assert_called_with( + is_private=True, ) self.assertEqual(self.columns, columns) @@ -382,10 +355,7 @@ def test_image_list_long_option(self): # 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.api_mock.image_list.assert_called_with( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() collist = ( 'ID', @@ -405,14 +375,14 @@ def test_image_list_long_option(self): datalist = (( self._image.id, self._image.name, - '', - '', - '', - '', - '', - image.VisibilityColumn(True), - False, - self._image.owner, + self._image.disk_format, + self._image.container_format, + self._image.size, + self._image.checksum, + self._image.status, + image.VisibilityColumn(self._image.is_public), + self._image.is_protected, + self._image.owner_id, format_columns.DictColumn( {'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}), ), ) @@ -436,12 +406,9 @@ def test_image_list_property_option(self, sf_mock): # 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.api_mock.image_list.assert_called_with( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() sf_mock.assert_called_with( - [self.image_info], + [self._image], attr='a', value='1', property_field='properties', @@ -453,7 +420,7 @@ def test_image_list_property_option(self, sf_mock): @mock.patch('osc_lib.utils.sort_items') def test_image_list_sort_option(self, si_mock): si_mock.side_effect = [ - [self.image_info], [], + [self._image], [], ] arglist = ['--sort', 'name:asc'] @@ -464,12 +431,9 @@ def test_image_list_sort_option(self, si_mock): # 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.api_mock.image_list.assert_called_with( - detailed=True, - marker=self._image.id, - ) + self.client.images.assert_called_with() si_mock.assert_called_with( - [self.image_info], + [self._image], 'name:asc' ) @@ -485,8 +449,8 @@ def setUp(self): super(TestImageSet, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = self._image - self.images_mock.update.return_value = self._image + self.client.find_image = mock.Mock(return_value=self._image) + self.client.update_image = mock.Mock(return_value=self._image) # Get the command object to test self.cmd = image.SetImage(self.app, None) @@ -502,8 +466,7 @@ def test_image_set_no_options(self): result = self.cmd.take_action(parsed_args) - self.images_mock.update.assert_called_with(self._image.id, - **{}) + self.client.update_image.assert_called_with(self._image.id, **{}) self.assertIsNone(result) def test_image_set_options(self): @@ -541,7 +504,7 @@ def test_image_set_options(self): 'size': 35165824 } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -565,11 +528,11 @@ def test_image_set_bools1(self): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': True, + 'is_protected': True, 'is_public': False, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -593,11 +556,11 @@ def test_image_set_bools2(self): result = self.cmd.take_action(parsed_args) kwargs = { - 'protected': False, + 'is_protected': False, 'is_public': True, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -625,7 +588,7 @@ def test_image_set_properties(self): }, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -683,7 +646,7 @@ def test_image_update_volume(self): '', ) # ImageManager.update(image_id, remove_props=, **) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, name='updated_image', volume='volly', @@ -710,7 +673,7 @@ def test_image_set_numeric_options_to_zero(self): 'min_ram': 0, } # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( + self.client.update_image.assert_called_with( self._image.id, **kwargs ) @@ -742,16 +705,16 @@ class TestImageShow(TestImage): _image.min_disk, _image.min_ram, _image.name, - _image.owner, + _image.owner_id, format_columns.DictColumn(_image.properties), - _image.protected, + _image.is_protected, _image.size, ) def setUp(self): super(TestImageShow, self).setUp() - self.images_mock.get.return_value = self._image + self.client.find_image = mock.Mock(return_value=self._image) # Get the command object to test self.cmd = image.ShowImage(self.app, None) @@ -769,7 +732,7 @@ def test_image_show(self): # 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.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( self._image.id, ) @@ -791,9 +754,9 @@ def test_image_show_human_readable(self): # 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.images_mock.get.assert_called_with( + self.client.find_image.assert_called_with( self._image.id, ) size_index = columns.index('size') - self.assertEqual(data[size_index], '2K') + self.assertEqual(data[size_index].human_readable(), '2K') diff --git a/releasenotes/notes/complete_image_switch-203e0b3105a54674.yaml b/releasenotes/notes/complete_image_switch-203e0b3105a54674.yaml new file mode 100644 index 0000000000..6878d1d984 --- /dev/null +++ b/releasenotes/notes/complete_image_switch-203e0b3105a54674.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Complete switch from glanceclient to the SDK for image service. diff --git a/requirements.txt b/requirements.txt index bf2a0590f3..b17b6a5593 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ openstacksdk>=0.36.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 -python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=15.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From 042be7c7fe89f5a1a190af90d5980205d995941b Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Thu, 12 Mar 2020 20:35:49 +0200 Subject: [PATCH 0205/1120] Don't look up project by id if given id There is a much deeper and systemic issue going on here, but let's start with fixing the immediate issue which is that adding a project to an image fails trying to look up project information even if the user passes the project id by id. _is_uuid_like from sdk isn't perfect, but it'll be good enough for this. Change-Id: I541416d737b961c56aa2f584c172528632fd5537 --- openstackclient/image/v2/image.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 412a16cc1c..53ce560dcd 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -21,6 +21,7 @@ import os import sys +import openstack.cloud._utils from openstack.image import image_signer from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns @@ -158,9 +159,13 @@ def take_action(self, parsed_args): image_client = self.app.client_manager.image identity_client = self.app.client_manager.identity - project_id = common.find_project(identity_client, - parsed_args.project, - parsed_args.project_domain).id + if openstack.cloud._utils._is_uuid_like(parsed_args.project): + project_id = parsed_args.project + else: + project_id = common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain).id image = image_client.find_image(parsed_args.image, ignore_missing=False) From f03cb68ad89dcc856fdad1a46fee36b7d68c5ba2 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Mon, 17 Feb 2020 17:10:04 +0200 Subject: [PATCH 0206/1120] Add 'address_scope' type support to network rbac commands Change-Id: I6a4b7219934805c1bbd1e88fcc670ae231d9ac37 Partial-Bug: #1862968 Depends-On: https://review.opendev.org/709122 --- lower-constraints.txt | 1 + openstackclient/network/v2/network_rbac.py | 17 +++-- .../unit/network/v2/test_network_rbac.py | 70 ++++++------------- ...ac-add-address-scope-7f6409ab70d36306.yaml | 4 ++ test-requirements.txt | 1 + 5 files changed, 41 insertions(+), 52 deletions(-) create mode 100644 releasenotes/notes/rbac-add-address-scope-7f6409ab70d36306.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 1fa30674e1..dac6922bbf 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -11,6 +11,7 @@ cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 cryptography==2.1 +ddt==1.0.1 debtcollector==1.2.0 decorator==3.4.0 deprecation==1.0 diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 1781193f61..0d6e07927b 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -58,6 +58,10 @@ def _get_attrs(client_manager, parsed_args): object_id = network_client.find_security_group( parsed_args.rbac_object, ignore_missing=False).id + if parsed_args.type == 'address_scope': + object_id = network_client.find_address_scope( + parsed_args.rbac_object, + ignore_missing=False).id attrs['object_id'] = object_id identity_client = client_manager.identity @@ -97,9 +101,11 @@ def get_parser(self, prog_name): '--type', metavar="", required=True, - choices=['security_group', 'qos_policy', 'network'], + choices=['address_scope', 'security_group', + 'qos_policy', 'network'], help=_('Type of the object that RBAC policy ' - 'affects ("security_group", "qos_policy" or "network")') + 'affects ("address_scope", "security_group", ' + '"qos_policy" or "network")') ) parser.add_argument( '--action', @@ -188,10 +194,11 @@ def get_parser(self, prog_name): parser.add_argument( '--type', metavar='', - choices=['security_group', 'qos_policy', 'network'], + choices=['address_scope', 'security_group', + 'qos_policy', 'network'], help=_('List network RBAC policies according to ' - 'given object type ("security_group", "qos_policy" ' - 'or "network")') + 'given object type ("address_scope", "security_group", ' + '"qos_policy" or "network")') ) parser.add_argument( '--action', diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index 078188ceb5..b3f8de4050 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -14,6 +14,7 @@ from unittest import mock from unittest.mock import call +import ddt from osc_lib import exceptions from openstackclient.network.v2 import network_rbac @@ -33,11 +34,13 @@ def setUp(self): self.projects_mock = self.app.client_manager.identity.projects +@ddt.ddt class TestCreateNetworkRBAC(TestNetworkRBAC): network_object = network_fakes.FakeNetwork.create_one_network() qos_object = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() sg_object = network_fakes.FakeNetworkSecGroup.create_one_security_group() + as_object = network_fakes.FakeAddressScope.create_one_address_scope() project = identity_fakes_v3.FakeProject.create_one_project() rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac( attrs={'tenant_id': project.id, @@ -77,6 +80,8 @@ def setUp(self): return_value=self.qos_object) self.network.find_security_group = mock.Mock( return_value=self.sg_object) + self.network.find_address_scope = mock.Mock( + return_value=self.as_object) self.projects_mock.get.return_value = self.project def test_network_rbac_create_no_type(self): @@ -224,57 +229,28 @@ def test_network_rbac_create_all_options(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) - def test_network_rbac_create_qos_object(self): - self.rbac_policy.object_type = 'qos_policy' - self.rbac_policy.object_id = self.qos_object.id - arglist = [ - '--type', 'qos_policy', - '--action', self.rbac_policy.action, - '--target-project', self.rbac_policy.target_tenant, - self.qos_object.name, - ] - verifylist = [ - ('type', 'qos_policy'), - ('action', self.rbac_policy.action), - ('target_project', self.rbac_policy.target_tenant), - ('rbac_object', self.qos_object.name), - ] - 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.create_rbac_policy.assert_called_with(**{ - 'object_id': self.qos_object.id, - 'object_type': 'qos_policy', - 'action': self.rbac_policy.action, - 'target_tenant': self.rbac_policy.target_tenant, - }) - self.data = [ - self.rbac_policy.action, - self.rbac_policy.id, - self.qos_object.id, - 'qos_policy', - self.rbac_policy.tenant_id, - self.rbac_policy.target_tenant, - ] - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) + @ddt.data( + ('qos_policy', "qos_object"), + ('security_group', "sg_object"), + ('address_scope', "as_object") + ) + @ddt.unpack + def test_network_rbac_create_object(self, obj_type, obj_fake_attr): + obj_fake = getattr(self, obj_fake_attr) - def test_network_rbac_create_security_group_object(self): - self.rbac_policy.object_type = 'security_group' - self.rbac_policy.object_id = self.sg_object.id + self.rbac_policy.object_type = obj_type + self.rbac_policy.object_id = obj_fake.id arglist = [ - '--type', 'security_group', + '--type', obj_type, '--action', self.rbac_policy.action, '--target-project', self.rbac_policy.target_tenant, - self.sg_object.name, + obj_fake.name, ] verifylist = [ - ('type', 'security_group'), + ('type', obj_type), ('action', self.rbac_policy.action), ('target_project', self.rbac_policy.target_tenant), - ('rbac_object', self.sg_object.name), + ('rbac_object', obj_fake.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -282,16 +258,16 @@ def test_network_rbac_create_security_group_object(self): columns, data = self.cmd.take_action(parsed_args) self.network.create_rbac_policy.assert_called_with(**{ - 'object_id': self.sg_object.id, - 'object_type': 'security_group', + 'object_id': obj_fake.id, + 'object_type': obj_type, 'action': self.rbac_policy.action, 'target_tenant': self.rbac_policy.target_tenant, }) self.data = [ self.rbac_policy.action, self.rbac_policy.id, - self.sg_object.id, - 'security_group', + obj_fake.id, + obj_type, self.rbac_policy.tenant_id, self.rbac_policy.target_tenant, ] diff --git a/releasenotes/notes/rbac-add-address-scope-7f6409ab70d36306.yaml b/releasenotes/notes/rbac-add-address-scope-7f6409ab70d36306.yaml new file mode 100644 index 0000000000..22b3838e3a --- /dev/null +++ b/releasenotes/notes/rbac-add-address-scope-7f6409ab70d36306.yaml @@ -0,0 +1,4 @@ +features: + - | + Add ``address_scope`` as a valid ``--type`` value for the + ``network rbac create`` and ``network rbac list`` commands. diff --git a/test-requirements.txt b/test-requirements.txt index 55ae1ea459..bdbf716e3a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -17,3 +17,4 @@ tempest>=17.1.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 bandit!=1.6.0,>=1.1.0 # Apache-2.0 wrapt>=1.7.0 # BSD License +ddt>=1.0.1 # MIT From 97d027caecdb977779ff130bb9ee2b3204c5646c Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Mar 2020 08:46:13 -0500 Subject: [PATCH 0207/1120] Add libc6-dev to bindep The python-builder base image was updated to no longer install recommends. This is inline with the other Infra images and keeps image sizes smaller. gcc recommended libc6-dev - but it turns out we need that for limits.h for one of our depends. Add it to fix our image builds. Change-Id: I97950d71bc455c269490812c6597fbe432641733 --- bindep.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/bindep.txt b/bindep.txt index c5ae6c6097..cf94d2a8cb 100644 --- a/bindep.txt +++ b/bindep.txt @@ -2,6 +2,7 @@ # see https://docs.openstack.org/infra/bindep/ for additional information. gcc [compile test] +libc6-dev [compile test platform:dpkg] libffi-devel [platform:rpm] libffi-dev [compile test platform:dpkg] libffi6 [platform:dpkg] From 8efb319819534545a63c57e98ef8446becdb94c2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 26 Mar 2020 08:48:20 -0500 Subject: [PATCH 0208/1120] Be explicit about python version in image python-base has versions available now, defaulting to 3.7. Update our config to 3.7 to be explicit about what we're using. This will let us update the version as we feel like. Change-Id: I40ffde91808a8bb95479697b9127dba16de8a8cd --- .zuul.yaml | 8 ++++---- Dockerfile | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7a7e264ca7..4901a1ed8d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -174,8 +174,8 @@ description: Build Docker images. allowed-projects: openstack/python-openstackclient requires: - - python-builder-container-image - - python-base-container-image + - python-builder-3.7-container-image + - python-base-3.7-container-image provides: osc-container-image vars: &osc_image_vars docker_images: @@ -188,8 +188,8 @@ description: Build Docker images and upload to Docker Hub. allowed-projects: openstack/python-openstackclient requires: - - python-builder-container-image - - python-base-container-image + - python-builder-3.7-container-image + - python-base-3.7-container-image provides: osc-container-image secrets: - name: docker_credentials diff --git a/Dockerfile b/Dockerfile index 9ff8084c30..ca60bb0ecb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,12 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM docker.io/opendevorg/python-builder as builder +FROM docker.io/opendevorg/python-builder:3.7 as builder COPY . /tmp/src RUN assemble -FROM docker.io/opendevorg/python-base +FROM docker.io/opendevorg/python-base:3.7 COPY --from=builder /output/ /output RUN /output/install-from-bindep From 05da145eaee329e299b449ba2d7ea88d1325e432 Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Thu, 5 Dec 2019 16:48:16 +0530 Subject: [PATCH 0209/1120] Adding options to user cli User options [1] can be set by making POST and PATCH request for /v3/users API calls but cannot by openstack CLI because of no user options defined in create and update user CLI [2]. This patch adds the user options [1] in create user and update user CLI. [1] https://docs.openstack.org/keystone/latest/admin/resource-options.html#multi-factor-auth-rules [2] https://docs.openstack.org/api-ref/identity/v3/#create-user Change-Id: I4e41bae2e8cfbe92d52b14d856991bedcd44164f --- doc/source/cli/command-objects/user.rst | 126 ++ openstackclient/identity/v3/user.py | 120 +- .../tests/unit/identity/v3/fakes.py | 3 + .../tests/unit/identity/v3/test_user.py | 1326 ++++++++++++++--- ..._user_create_and_set-302401520f36d153.yaml | 19 + 5 files changed, 1356 insertions(+), 238 deletions(-) create mode 100644 releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml diff --git a/doc/source/cli/command-objects/user.rst b/doc/source/cli/command-objects/user.rst index 632d0e2566..d0fc3f8734 100644 --- a/doc/source/cli/command-objects/user.rst +++ b/doc/source/cli/command-objects/user.rst @@ -19,6 +19,12 @@ Create new user [--password-prompt] [--email ] [--description ] + [--multi-factor-auth-rule ] + [--ignore-lockout-failure-attempts| --no-ignore-lockout-failure-attempts] + [--ignore-password-expiry| --no-ignore-password-expiry] + [--ignore-change-password-upon-first-use| --no-ignore-change-password-upon-first-use] + [--enable-lock-password| --disable-lock-password] + [--enable-multi-factor-auth| --disable-multi-factor-auth] [--enable | --disable] [--or-show] @@ -56,6 +62,63 @@ Create new user .. versionadded:: 3 +.. option:: --ignore-lockout-failure-attempts + + Opt into ignoring the number of times a user has authenticated and + locking out the user as a result + +.. option:: --no-ignore-lockout-failure-attempts + + Opt out of ignoring the number of times a user has authenticated + and locking out the user as a result + +.. option:: --ignore-change-password-upon-first-use + + Control if a user should be forced to change their password immediately + after they log into keystone for the first time. Opt into ignoring + the user to change their password during first time login in keystone. + +.. option:: --no-ignore-change-password-upon-first-use + + Control if a user should be forced to change their password immediately + after they log into keystone for the first time. Opt out of ignoring + the user to change their password during first time login in keystone. + +.. option:: --ignore-password-expiry + + Opt into allowing user to continue using passwords that may be + expired + +.. option:: --no-ignore-password-expiry + + Opt out of allowing user to continue using passwords that may be + expired + +.. option:: --enable-lock-password + + Disables the ability for a user to change its password through + self-service APIs + +.. option:: --disable-lock-password + + Enables the ability for a user to change its password through + self-service APIs + +.. option:: --enable-multi-factor-auth + + Enables the MFA (Multi Factor Auth) + +.. option:: --disable-multi-factor-auth + + Disables the MFA (Multi Factor Auth) + +.. option:: --multi-factor-auth-rule + + Set multi-factor auth rules. For example, to set a rule requiring the + "password" and "totp" auth methods to be provided, + use: "--multi-factor-auth-rule password,totp". + May be provided multiple times to set different rule combinations. + .. option:: --enable Enable user (default) @@ -146,6 +209,12 @@ Set user properties [--password-prompt] [--email ] [--description ] + [--multi-factor-auth-rule ] + [--ignore-lockout-failure-attempts| --no-ignore-lockout-failure-attempts] + [--ignore-password-expiry| --no-ignore-password-expiry] + [--ignore-change-password-upon-first-use| --no-ignore-change-password-upon-first-use] + [--enable-lock-password| --disable-lock-password] + [--enable-multi-factor-auth| --disable-multi-factor-auth] [--enable|--disable] @@ -187,6 +256,63 @@ Set user properties .. versionadded:: 3 +.. option:: --ignore-lockout-failure-attempts + + Opt into ignoring the number of times a user has authenticated and + locking out the user as a result + +.. option:: --no-ignore-lockout-failure-attempts + + Opt out of ignoring the number of times a user has authenticated + and locking out the user as a result + +.. option:: --ignore-change-password-upon-first-use + + Control if a user should be forced to change their password immediately + after they log into keystone for the first time. Opt into ignoring + the user to change their password during first time login in keystone. + +.. option:: --no-ignore-change-password-upon-first-use + + Control if a user should be forced to change their password immediately + after they log into keystone for the first time. Opt out of ignoring + the user to change their password during first time login in keystone. + +.. option:: --ignore-password-expiry + + Opt into allowing user to continue using passwords that may be + expired + +.. option:: --no-ignore-password-expiry + + Opt out of allowing user to continue using passwords that may be + expired + +.. option:: --enable-lock-password + + Disables the ability for a user to change its password through + self-service APIs + +.. option:: --disable-lock-password + + Enables the ability for a user to change its password through + self-service APIs + +.. option:: --enable-multi-factor-auth + + Enables the MFA (Multi Factor Auth) + +.. option:: --disable-multi-factor-auth + + Disables the MFA (Multi Factor Auth) + +.. option:: --multi-factor-auth-rule + + Set multi-factor auth rules. For example, to set a rule requiring the + "password" and "totp" auth methods to be provided, + use: "--multi-factor-auth-rule password,totp". + May be provided multiple times to set different rule combinations. + .. option:: --enable Enable user (default) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index ca85c5d8a8..cbc112a058 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -30,6 +30,114 @@ LOG = logging.getLogger(__name__) +def _get_options_for_user(identity_client, parsed_args): + options = {} + if parsed_args.ignore_lockout_failure_attempts: + options['ignore_lockout_failure_attempts'] = True + if parsed_args.no_ignore_lockout_failure_attempts: + options['ignore_lockout_failure_attempts'] = False + if parsed_args.ignore_password_expiry: + options['ignore_password_expiry'] = True + if parsed_args.no_ignore_password_expiry: + options['ignore_password_expiry'] = False + if parsed_args.ignore_change_password_upon_first_use: + options['ignore_change_password_upon_first_use'] = True + if parsed_args.no_ignore_change_password_upon_first_use: + options['ignore_change_password_upon_first_use'] = False + if parsed_args.enable_lock_password: + options['lock_password'] = True + if parsed_args.disable_lock_password: + options['lock_password'] = False + if parsed_args.enable_multi_factor_auth: + options['multi_factor_auth_enabled'] = True + if parsed_args.disable_multi_factor_auth: + options['multi_factor_auth_enabled'] = False + if parsed_args.multi_factor_auth_rule: + auth_rules = [rule.split(",") for rule in + parsed_args.multi_factor_auth_rule] + if auth_rules: + options['multi_factor_auth_rules'] = auth_rules + return options + + +def _add_user_options(parser): + # Add additional user options + + parser.add_argument( + '--ignore-lockout-failure-attempts', + action="store_true", + help=_('Opt into ignoring the number of times a user has ' + 'authenticated and locking out the user as a result'), + ) + parser.add_argument( + '--no-ignore-lockout-failure-attempts', + action="store_true", + help=_('Opt out of ignoring the number of times a user has ' + 'authenticated and locking out the user as a result'), + ) + parser.add_argument( + '--ignore-password-expiry', + action="store_true", + help=_('Opt into allowing user to continue using passwords that ' + 'may be expired'), + ) + parser.add_argument( + '--no-ignore-password-expiry', + action="store_true", + help=_('Opt out of allowing user to continue using passwords ' + 'that may be expired'), + ) + parser.add_argument( + '--ignore-change-password-upon-first-use', + action="store_true", + help=_('Control if a user should be forced to change their password ' + 'immediately after they log into keystone for the first time. ' + 'Opt into ignoring the user to change their password during ' + 'first time login in keystone'), + ) + parser.add_argument( + '--no-ignore-change-password-upon-first-use', + action="store_true", + help=_('Control if a user should be forced to change their password ' + 'immediately after they log into keystone for the first time. ' + 'Opt out of ignoring the user to change their password during ' + 'first time login in keystone'), + ) + parser.add_argument( + '--enable-lock-password', + action="store_true", + help=_('Disables the ability for a user to change its password ' + 'through self-service APIs'), + ) + parser.add_argument( + '--disable-lock-password', + action="store_true", + help=_('Enables the ability for a user to change its password ' + 'through self-service APIs'), + ) + parser.add_argument( + '--enable-multi-factor-auth', + action="store_true", + help=_('Enables the MFA (Multi Factor Auth)'), + ) + parser.add_argument( + '--disable-multi-factor-auth', + action="store_true", + help=_('Disables the MFA (Multi Factor Auth)'), + ) + parser.add_argument( + '--multi-factor-auth-rule', + metavar='', + action="append", + default=[], + help=_('Set multi-factor auth rules. For example, to set a rule ' + 'requiring the "password" and "totp" auth methods to be ' + 'provided, use: "--multi-factor-auth-rule password,totp". ' + 'May be provided multiple times to set different rule ' + 'combinations.') + ) + + class CreateUser(command.ShowOne): _description = _("Create new user") @@ -72,6 +180,8 @@ def get_parser(self, prog_name): metavar='', help=_('User description'), ) + _add_user_options(parser) + enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -113,6 +223,7 @@ def take_action(self, parsed_args): if not parsed_args.password: LOG.warning(_("No password was supplied, authentication will fail " "when a user does not have a password.")) + options = _get_options_for_user(identity_client, parsed_args) try: user = identity_client.users.create( @@ -122,7 +233,8 @@ def take_action(self, parsed_args): password=parsed_args.password, email=parsed_args.email, description=parsed_args.description, - enabled=enabled + enabled=enabled, + options=options, ) except ks_exc.Conflict: if parsed_args.or_show: @@ -333,6 +445,8 @@ def get_parser(self, prog_name): metavar='', help=_('Set user description'), ) + _add_user_options(parser) + enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -390,6 +504,10 @@ def take_action(self, parsed_args): if parsed_args.disable: kwargs['enabled'] = False + options = _get_options_for_user(identity_client, parsed_args) + if options: + kwargs['options'] = options + identity_client.users.update(user.id, **kwargs) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index eb3ce2a356..58d5d14d04 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -108,6 +108,9 @@ "rules": MAPPING_RULES_2 } +mfa_opt1 = 'password,totp' +mfa_opt2 = 'password' + project_id = '8-9-64' project_name = 'beatles' project_description = 'Fab Four' diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 4b14bca05a..c71435bacf 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -111,6 +111,7 @@ def test_user_create_no_options(self): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -150,6 +151,7 @@ def test_user_create_password(self): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': 'secret', } @@ -190,6 +192,7 @@ def test_user_create_password_prompt(self): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': 'abc123', } @@ -228,6 +231,7 @@ def test_user_create_email(self): 'domain': None, 'email': 'barney@example.com', 'enabled': True, + 'options': {}, 'password': None, } # UserManager.create(name=, domain=, project=, password=, email=, @@ -265,6 +269,7 @@ def test_user_create_project(self): 'domain': None, 'email': None, 'enabled': True, + 'options': {}, 'password': None, } # UserManager.create(name=, domain=, project=, password=, email=, @@ -311,6 +316,7 @@ def test_user_create_project_domain(self): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -356,6 +362,7 @@ def test_user_create_domain(self): 'description': None, 'domain': self.domain.id, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -392,6 +399,7 @@ def test_user_create_enable(self): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': True, 'password': None, } @@ -428,6 +436,7 @@ def test_user_create_disable(self): 'description': None, 'domain': None, 'email': None, + 'options': {}, 'enabled': False, 'password': None, } @@ -438,280 +447,1112 @@ def test_user_create_disable(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_user_create_ignore_lockout_failure_attempts(self): + arglist = [ + '--ignore-lockout-failure-attempts', + self.user.name, + ] + verifylist = [ + ('ignore_lockout_failure_attempts', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) -class TestUserDelete(TestUser): - - user = identity_fakes.FakeUser.create_one_user() - - def setUp(self): - super(TestUserDelete, self).setUp() + # 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) - # This is the return value for utils.find_resource() - self.users_mock.get.return_value = self.user - self.users_mock.delete.return_value = None + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) - # Get the command object to test - self.cmd = user.DeleteUser(self.app, None) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) - def test_user_delete_no_options(self): + def test_user_create_no_ignore_lockout_failure_attempts(self): arglist = [ - self.user.id, + '--no-ignore-lockout-failure-attempts', + self.user.name, ] verifylist = [ - ('users', [self.user.id]), + ('no_ignore_lockout_failure_attempts', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + # 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.users_mock.delete.assert_called_with( - self.user.id, + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs ) - self.assertIsNone(result) - @mock.patch.object(utils, 'find_resource') - def test_delete_multi_users_with_exception(self, find_mock): - find_mock.side_effect = [self.user, - exceptions.CommandError] + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_ignore_password_expiry(self): arglist = [ - self.user.id, - 'unexist_user', + '--ignore-password-expiry', + self.user.name, ] verifylist = [ - ('users', arglist), + ('ignore_password_expiry', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 users failed to delete.', - str(e)) + # 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) - find_mock.assert_any_call(self.users_mock, self.user.id) - find_mock.assert_any_call(self.users_mock, 'unexist_user') + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_password_expiry': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) - self.assertEqual(2, find_mock.call_count) - self.users_mock.delete.assert_called_once_with(self.user.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + def test_user_create_no_ignore_password_expiry(self): + arglist = [ + '--no-ignore-password-expiry', + self.user.name, + ] + verifylist = [ + ('no_ignore_password_expiry', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) -class TestUserList(TestUser): + # 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) - domain = identity_fakes.FakeDomain.create_one_domain() - project = identity_fakes.FakeProject.create_one_project() - user = identity_fakes.FakeUser.create_one_user( - attrs={'domain_id': domain.id, - 'default_project_id': project.id} - ) - group = identity_fakes.FakeGroup.create_one_group() - role_assignment = ( - identity_fakes.FakeRoleAssignment.create_one_role_assignment( - attrs={'user': {'id': user.id}})) + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_password_expiry': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) - columns = [ - 'ID', - 'Name' - ] - datalist = ( - ( - user.id, - user.name, - ), - ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) - def setUp(self): - super(TestUserList, self).setUp() + def test_user_create_ignore_change_password_upon_first_use(self): + arglist = [ + '--ignore-change-password-upon-first-use', + self.user.name, + ] + verifylist = [ + ('ignore_change_password_upon_first_use', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.users_mock.get.return_value = self.user - self.users_mock.list.return_value = [self.user] - self.domains_mock.get.return_value = self.domain - self.groups_mock.get.return_value = self.group - self.projects_mock.get.return_value = self.project - self.role_assignments_mock.list.return_value = [self.role_assignment] + # 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) - # Get the command object to test - self.cmd = user.ListUser(self.app, None) + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_change_password_upon_first_use': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) - def test_user_list_no_options(self): - arglist = [] - verifylist = [] + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_no_ignore_change_password_upon_first_use(self): + arglist = [ + '--no-ignore-change-password-upon-first-use', + self.user.name, + ] + verifylist = [ + ('no_ignore_change_password_upon_first_use', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] 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. + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, 'domain': None, - 'group': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_change_password_upon_first_use': False}, + 'password': None, } - - self.users_mock.list.assert_called_with( + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( **kwargs ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertEqual(self.datalist, data) + + def test_user_create_enables_lock_password(self): + arglist = [ + '--enable-lock-password', + self.user.name, + ] + verifylist = [ + ('enable_lock_password', True), + ('enable', False), + ('disable', False), + ('name', self.user.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) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'lock_password': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_disables_lock_password(self): + arglist = [ + '--disable-lock-password', + self.user.name, + ] + verifylist = [ + ('disable_lock_password', True), + ('enable', False), + ('disable', False), + ('name', self.user.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) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'lock_password': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_enable_multi_factor_auth(self): + arglist = [ + '--enable-multi-factor-auth', + self.user.name, + ] + verifylist = [ + ('enable_multi_factor_auth', True), + ('enable', False), + ('disable', False), + ('name', self.user.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) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'multi_factor_auth_enabled': True}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_disable_multi_factor_auth(self): + arglist = [ + '--disable-multi-factor-auth', + self.user.name, + ] + verifylist = [ + ('disable_multi_factor_auth', True), + ('enable', False), + ('disable', False), + ('name', self.user.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) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'multi_factor_auth_enabled': False}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_option_with_multi_factor_auth_rule(self): + arglist = [ + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, + '--multi-factor-auth-rule', identity_fakes.mfa_opt2, + self.user.name, + ] + verifylist = [ + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1, + identity_fakes.mfa_opt2]), + ('enable', False), + ('disable', False), + ('name', self.user.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) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'multi_factor_auth_rules': [["password", "totp"], + ["password"]]}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_user_create_with_multiple_options(self): + arglist = [ + '--ignore-password-expiry', + '--disable-multi-factor-auth', + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, + self.user.name, + ] + verifylist = [ + ('ignore_password_expiry', True), + ('disable_multi_factor_auth', True), + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('enable', False), + ('disable', False), + ('name', self.user.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) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'options': {'ignore_password_expiry': True, + 'multi_factor_auth_enabled': False, + 'multi_factor_auth_rules': [["password", "totp"]]}, + 'password': None, + } + # UserManager.create(name=, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + +class TestUserDelete(TestUser): + + user = identity_fakes.FakeUser.create_one_user() + + def setUp(self): + super(TestUserDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.users_mock.get.return_value = self.user + self.users_mock.delete.return_value = None + + # Get the command object to test + self.cmd = user.DeleteUser(self.app, None) + + def test_user_delete_no_options(self): + arglist = [ + self.user.id, + ] + verifylist = [ + ('users', [self.user.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.users_mock.delete.assert_called_with( + self.user.id, + ) + self.assertIsNone(result) + + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.user, + exceptions.CommandError] + arglist = [ + self.user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.user.id) + + +class TestUserList(TestUser): + + domain = identity_fakes.FakeDomain.create_one_domain() + project = identity_fakes.FakeProject.create_one_project() + user = identity_fakes.FakeUser.create_one_user( + attrs={'domain_id': domain.id, + 'default_project_id': project.id} + ) + group = identity_fakes.FakeGroup.create_one_group() + role_assignment = ( + identity_fakes.FakeRoleAssignment.create_one_role_assignment( + attrs={'user': {'id': user.id}})) + + columns = [ + 'ID', + 'Name' + ] + datalist = ( + ( + user.id, + user.name, + ), + ) + + def setUp(self): + super(TestUserList, self).setUp() + + self.users_mock.get.return_value = self.user + self.users_mock.list.return_value = [self.user] + self.domains_mock.get.return_value = self.domain + self.groups_mock.get.return_value = self.group + self.projects_mock.get.return_value = self.project + self.role_assignments_mock.list.return_value = [self.role_assignment] + + # Get the command object to test + self.cmd = user.ListUser(self.app, None) + + def test_user_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': None, + 'group': None, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_user_list_domain(self): + arglist = [ + '--domain', self.domain.id, + ] + verifylist = [ + ('domain', self.domain.id), + ] + 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 = { + 'domain': self.domain.id, + 'group': None, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_user_list_group(self): + arglist = [ + '--group', self.group.name, + ] + verifylist = [ + ('group', self.group.name), + ] + 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 = { + 'domain': None, + 'group': self.group.id, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_user_list_long(self): + 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 = { + 'domain': None, + 'group': None, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) + + collist = [ + 'ID', + 'Name', + 'Project', + 'Domain', + 'Description', + 'Email', + 'Enabled', + ] + self.assertEqual(collist, columns) + datalist = ( + ( + self.user.id, + self.user.name, + self.project.id, + self.domain.id, + '', + self.user.email, + True, + ), + ) + self.assertEqual(datalist, tuple(data)) + + def test_user_list_project(self): + arglist = [ + '--project', self.project.name, + ] + verifylist = [ + ('project', self.project.name), + ] + 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) + + kwargs = { + 'project': self.project.id, + } + + self.role_assignments_mock.list.assert_called_with(**kwargs) + self.users_mock.get.assert_called_with(self.user.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestUserSet(TestUser): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + user = identity_fakes.FakeUser.create_one_user( + attrs={'default_project_id': project.id} + ) + user2 = identity_fakes.FakeUser.create_one_user( + attrs={'default_project_id': project.id, + 'domain_id': domain.id} + ) + + def setUp(self): + super(TestUserSet, self).setUp() + + self.projects_mock.get.return_value = self.project + self.users_mock.get.return_value = self.user + self.users_mock.update.return_value = self.user + + # Get the command object to test + self.cmd = user.SetUser(self.app, None) + + def test_user_set_no_options(self): + arglist = [ + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + + def test_user_set_name(self): + arglist = [ + '--name', 'qwerty', + self.user.name, + ] + verifylist = [ + ('name', 'qwerty'), + ('password', None), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'name': 'qwerty', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_specify_domain(self): + arglist = [ + '--name', 'qwerty', + '--domain', self.domain.id, + self.user2.name + ] + verifylist = [ + ('name', 'qwerty'), + ('password', None), + ('domain', self.domain.id), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user2.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + 'enabled': True, + 'name': 'qwerty' + } + + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_password(self): + arglist = [ + '--password', 'secret', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', 'secret'), + ('password_prompt', False), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'password': 'secret', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_password_prompt(self): + arglist = [ + '--password-prompt', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('password_prompt', True), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = 'abc123' + with mock.patch("osc_lib.utils.get_password", mocker): + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'password': 'abc123', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_email(self): + arglist = [ + '--email', 'barney@example.com', + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', 'barney@example.com'), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'email': 'barney@example.com', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_project(self): + arglist = [ + '--project', self.project.id, + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('project', self.project.id), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'default_project': self.project.id, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + + def test_user_set_project_domain(self): + arglist = [ + '--project', self.project.id, + '--project-domain', self.project.domain_id, + self.user.name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('project', self.project.id), + ('project_domain', self.project.domain_id), + ('enable', False), + ('disable', False), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'default_project': self.project.id, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) - def test_user_list_domain(self): + def test_user_set_enable(self): arglist = [ - '--domain', self.domain.id, + '--enable', + self.user.name, ] verifylist = [ - ('domain', self.domain.id), + ('name', None), + ('password', None), + ('email', None), + ('project', None), + ('enable', True), + ('disable', False), + ('user', self.user.name), ] 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) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'domain': self.domain.id, - 'group': None, + 'enabled': True, } - - self.users_mock.list.assert_called_with( + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, **kwargs ) + self.assertIsNone(result) - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) - - def test_user_list_group(self): + def test_user_set_disable(self): arglist = [ - '--group', self.group.name, + '--disable', + self.user.name, ] verifylist = [ - ('group', self.group.name), + ('name', None), + ('password', None), + ('email', None), + ('project', None), + ('enable', False), + ('disable', True), + ('user', self.user.name), ] 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) + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'domain': None, - 'group': self.group.id, + 'enabled': False, } - - self.users_mock.list.assert_called_with( + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, **kwargs ) + self.assertIsNone(result) - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) - - def test_user_list_long(self): + def test_user_set_ignore_lockout_failure_attempts(self): arglist = [ - '--long', + '--ignore-lockout-failure-attempts', + self.user.name, ] verifylist = [ - ('long', True), + ('name', None), + ('password', None), + ('email', None), + ('ignore_lockout_failure_attempts', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), ] 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) - + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'domain': None, - 'group': None, + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': True}, } - - self.users_mock.list.assert_called_with( + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, **kwargs ) + self.assertIsNone(result) - collist = [ - 'ID', - 'Name', - 'Project', - 'Domain', - 'Description', - 'Email', - 'Enabled', - ] - self.assertEqual(collist, columns) - datalist = ( - ( - self.user.id, - self.user.name, - self.project.id, - self.domain.id, - '', - self.user.email, - True, - ), - ) - self.assertEqual(datalist, tuple(data)) - - def test_user_list_project(self): + def test_user_set_no_ignore_lockout_failure_attempts(self): arglist = [ - '--project', self.project.name, + '--no-ignore-lockout-failure-attempts', + self.user.name, ] verifylist = [ - ('project', self.project.name), + ('name', None), + ('password', None), + ('email', None), + ('no_ignore_lockout_failure_attempts', True), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user.name), ] 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) - + result = self.cmd.take_action(parsed_args) + # Set expected values kwargs = { - 'project': self.project.id, + 'enabled': True, + 'options': {'ignore_lockout_failure_attempts': False}, } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) - self.role_assignments_mock.list.assert_called_with(**kwargs) - self.users_mock.get.assert_called_with(self.user.id) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) - - -class TestUserSet(TestUser): - - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() - user = identity_fakes.FakeUser.create_one_user( - attrs={'default_project_id': project.id} - ) - user2 = identity_fakes.FakeUser.create_one_user( - attrs={'default_project_id': project.id, - 'domain_id': domain.id} - ) - - def setUp(self): - super(TestUserSet, self).setUp() - - self.projects_mock.get.return_value = self.project - self.users_mock.get.return_value = self.user - self.users_mock.update.return_value = self.user - - # Get the command object to test - self.cmd = user.SetUser(self.app, None) - - def test_user_set_no_options(self): + def test_user_set_ignore_password_expiry(self): arglist = [ + '--ignore-password-expiry', self.user.name, ] verifylist = [ ('name', None), ('password', None), ('email', None), + ('ignore_password_expiry', True), ('project', None), ('enable', False), ('disable', False), @@ -720,18 +1561,29 @@ def test_user_set_no_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - + # Set expected values + kwargs = { + 'enabled': True, + 'options': {'ignore_password_expiry': True}, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) self.assertIsNone(result) - def test_user_set_name(self): + def test_user_set_no_ignore_password_expiry(self): arglist = [ - '--name', 'qwerty', + '--no-ignore-password-expiry', self.user.name, ] verifylist = [ - ('name', 'qwerty'), + ('name', None), ('password', None), ('email', None), + ('no_ignore_password_expiry', True), ('project', None), ('enable', False), ('disable', False), @@ -740,11 +1592,10 @@ def test_user_set_name(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { 'enabled': True, - 'name': 'qwerty', + 'options': {'ignore_password_expiry': False}, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -754,47 +1605,47 @@ def test_user_set_name(self): ) self.assertIsNone(result) - def test_user_set_specify_domain(self): + def test_user_set_ignore_change_password_upon_first_use(self): arglist = [ - '--name', 'qwerty', - '--domain', self.domain.id, - self.user2.name + '--ignore-change-password-upon-first-use', + self.user.name, ] verifylist = [ - ('name', 'qwerty'), + ('name', None), ('password', None), - ('domain', self.domain.id), ('email', None), + ('ignore_change_password_upon_first_use', True), ('project', None), ('enable', False), ('disable', False), - ('user', self.user2.name), + ('user', self.user.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - + # Set expected values kwargs = { 'enabled': True, - 'name': 'qwerty' + 'options': {'ignore_change_password_upon_first_use': True}, } - + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) self.users_mock.update.assert_called_with( self.user.id, **kwargs ) self.assertIsNone(result) - def test_user_set_password(self): + def test_user_set_no_ignore_change_password_upon_first_use(self): arglist = [ - '--password', 'secret', + '--no-ignore-change-password-upon-first-use', self.user.name, ] verifylist = [ ('name', None), - ('password', 'secret'), - ('password_prompt', False), + ('password', None), ('email', None), + ('no_ignore_change_password_upon_first_use', True), ('project', None), ('enable', False), ('disable', False), @@ -803,11 +1654,10 @@ def test_user_set_password(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { 'enabled': True, - 'password': 'secret', + 'options': {'ignore_change_password_upon_first_use': False}, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -817,16 +1667,16 @@ def test_user_set_password(self): ) self.assertIsNone(result) - def test_user_set_password_prompt(self): + def test_user_set_enable_lock_password(self): arglist = [ - '--password-prompt', + '--enable-lock-password', self.user.name, ] verifylist = [ ('name', None), ('password', None), - ('password_prompt', True), ('email', None), + ('enable_lock_password', True), ('project', None), ('enable', False), ('disable', False), @@ -834,15 +1684,11 @@ def test_user_set_password_prompt(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - mocker = mock.Mock() - mocker.return_value = 'abc123' - with mock.patch("osc_lib.utils.get_password", mocker): - result = self.cmd.take_action(parsed_args) - + result = self.cmd.take_action(parsed_args) # Set expected values kwargs = { 'enabled': True, - 'password': 'abc123', + 'options': {'lock_password': True}, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -852,15 +1698,16 @@ def test_user_set_password_prompt(self): ) self.assertIsNone(result) - def test_user_set_email(self): + def test_user_set_disable_lock_password(self): arglist = [ - '--email', 'barney@example.com', + '--disable-lock-password', self.user.name, ] verifylist = [ ('name', None), ('password', None), - ('email', 'barney@example.com'), + ('email', None), + ('disable_lock_password', True), ('project', None), ('enable', False), ('disable', False), @@ -869,11 +1716,10 @@ def test_user_set_email(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { 'enabled': True, - 'email': 'barney@example.com', + 'options': {'lock_password': False}, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -883,16 +1729,17 @@ def test_user_set_email(self): ) self.assertIsNone(result) - def test_user_set_project(self): + def test_user_set_enable_multi_factor_auth(self): arglist = [ - '--project', self.project.id, + '--enable-multi-factor-auth', self.user.name, ] verifylist = [ ('name', None), ('password', None), ('email', None), - ('project', self.project.id), + ('enable_multi_factor_auth', True), + ('project', None), ('enable', False), ('disable', False), ('user', self.user.name), @@ -900,11 +1747,10 @@ def test_user_set_project(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { 'enabled': True, - 'default_project': self.project.id, + 'options': {'multi_factor_auth_enabled': True}, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -914,18 +1760,17 @@ def test_user_set_project(self): ) self.assertIsNone(result) - def test_user_set_project_domain(self): + def test_user_set_disable_multi_factor_auth(self): arglist = [ - '--project', self.project.id, - '--project-domain', self.project.domain_id, + '--disable-multi-factor-auth', self.user.name, ] verifylist = [ ('name', None), ('password', None), ('email', None), - ('project', self.project.id), - ('project_domain', self.project.domain_id), + ('disable_multi_factor_auth', True), + ('project', None), ('enable', False), ('disable', False), ('user', self.user.name), @@ -933,11 +1778,10 @@ def test_user_set_project_domain(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { 'enabled': True, - 'default_project': self.project.id, + 'options': {'multi_factor_auth_enabled': False}, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -947,28 +1791,29 @@ def test_user_set_project_domain(self): ) self.assertIsNone(result) - def test_user_set_enable(self): + def test_user_set_option_multi_factor_auth_rule(self): arglist = [ - '--enable', + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, self.user.name, ] verifylist = [ ('name', None), ('password', None), ('email', None), + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), ('project', None), - ('enable', True), + ('enable', False), ('disable', False), ('user', self.user.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { 'enabled': True, - } + 'options': {'multi_factor_auth_rules': [["password", "totp"]]}} + # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) self.users_mock.update.assert_called_with( @@ -977,28 +1822,35 @@ def test_user_set_enable(self): ) self.assertIsNone(result) - def test_user_set_disable(self): + def test_user_set_with_multiple_options(self): arglist = [ - '--disable', + '--ignore-password-expiry', + '--enable-multi-factor-auth', + '--multi-factor-auth-rule', identity_fakes.mfa_opt1, self.user.name, ] verifylist = [ ('name', None), ('password', None), ('email', None), + ('ignore_password_expiry', True), + ('enable_multi_factor_auth', True), + ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), ('project', None), ('enable', False), - ('disable', True), + ('disable', False), ('user', self.user.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { - 'enabled': False, - } + 'enabled': True, + 'options': {'ignore_password_expiry': True, + 'multi_factor_auth_enabled': True, + 'multi_factor_auth_rules': [["password", "totp"]]}} + # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) self.users_mock.update.assert_called_with( diff --git a/releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml b/releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml new file mode 100644 index 0000000000..698a6f189d --- /dev/null +++ b/releasenotes/notes/add_options_to_user_create_and_set-302401520f36d153.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Added the below mentioned parameters to the user create and set commands. + + * --ignore-lockout-failure-attempts + * --no-ignore-lockout-failure-attempts + * --ignore-password-expiry + * --no-ignore-password-expiry + * --ignore-change-password-upon-first-use + * --no-ignore-change-password-upon-first-use + * --enable-lock-password + * --disable-lock-password + * --enable-multi-factor-auth + * --disable-multi-factor-auth + * --multi-factor-auth-rule + + This will now allow users to set user options via CLI. + From dba57c85d5536369d600ae98599c08b921713bd5 Mon Sep 17 00:00:00 2001 From: Bence Romsics Date: Wed, 31 Jul 2019 15:44:27 +0200 Subject: [PATCH 0210/1120] Add command: router add/remove route --route Add commands to osc to call the two new API methods introduced by new Neutron extension: extraroute-atomic. Bump our openstacksdk requirement to >=0.38.0 which contains the corresponding sdk change. The lower-constraints of dogpile.cache and keystoneauth1 are bumped because of requirements bumps in openstacksdk. The lower-constraint of decorator is bumped because of problem already fixed by amotoki here: https://review.opendev.org/701706 Change-Id: Ia9b9c216f1d1161ebedac31594a2c464d77f4ae2 Depends-On: https://review.opendev.org/674324 Partial-Bug: #1826396 (rfe) Related-Change: https://review.opendev.org/655680 (spec) --- lower-constraints.txt | 8 +- openstackclient/network/v2/router.py | 97 +++++++++++- .../functional/network/v2/test_router.py | 43 ++++++ .../tests/unit/network/v2/test_router.py | 140 ++++++++++++++++++ ...er-extraroute-atomic-d6d406ffb15695f2.yaml | 12 ++ requirements.txt | 2 +- setup.cfg | 2 + 7 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 1fa30674e1..83e19fe60f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -12,11 +12,11 @@ contextlib2==0.4.0 coverage==4.0 cryptography==2.1 debtcollector==1.2.0 -decorator==3.4.0 +decorator==4.4.1 deprecation==1.0 docker-pycreds==0.2.1 docker==2.4.2 -dogpile.cache==0.6.2 +dogpile.cache==0.6.5 eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 @@ -38,7 +38,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.16.0 +keystoneauth1==3.18.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.0 @@ -50,7 +50,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.36.0 +openstacksdk==0.38.0 os-client-config==1.28.0 os-service-types==1.7.0 os-testr==1.0.0 diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 464dbbec78..81b81f98b5 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -168,6 +168,93 @@ def take_action(self, parsed_args): subnet_id=subnet.id) +class AddExtraRoutesToRouter(command.ShowOne): + _description = _("Add extra static routes to a router's routing table.") + + def get_parser(self, prog_name): + parser = super(AddExtraRoutesToRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_("Router to which extra static routes " + "will be added (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=,gateway=', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=[], + required_keys=['destination', 'gateway'], + help=_("Add extra static route to the router. " + "destination: destination subnet (in CIDR notation), " + "gateway: nexthop IP address. " + "Repeat option to add multiple routes. " + "Trying to add a route that's already present " + "(exactly, including destination and nexthop) " + "in the routing table is allowed and is considered " + "a successful operation.") + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.routes is not None: + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + client = self.app.client_manager.network + router_obj = client.add_extra_routes_to_router( + client.find_router(parsed_args.router, ignore_missing=False), + body={'router': {'routes': parsed_args.routes}}) + display_columns, columns = _get_columns(router_obj) + data = utils.get_item_properties( + router_obj, columns, formatters=_formatters) + return (display_columns, data) + + +class RemoveExtraRoutesFromRouter(command.ShowOne): + _description = _( + "Remove extra static routes from a router's routing table.") + + def get_parser(self, prog_name): + parser = super(RemoveExtraRoutesFromRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_("Router from which extra static routes " + "will be removed (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=,gateway=', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=[], + required_keys=['destination', 'gateway'], + help=_("Remove extra static route from the router. " + "destination: destination subnet (in CIDR notation), " + "gateway: nexthop IP address. " + "Repeat option to remove multiple routes. " + "Trying to remove a route that's already missing " + "(fully, including destination and nexthop) " + "from the routing table is allowed and is considered " + "a successful operation.") + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.routes is not None: + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + client = self.app.client_manager.network + router_obj = client.remove_extra_routes_from_router( + client.find_router(parsed_args.router, ignore_missing=False), + body={'router': {'routes': parsed_args.routes}}) + display_columns, columns = _get_columns(router_obj) + data = utils.get_item_properties( + router_obj, columns, formatters=_formatters) + return (display_columns, data) + + # TODO(yanxing'an): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. class CreateRouter(command.ShowOne): @@ -540,17 +627,21 @@ def get_parser(self, prog_name): dest='routes', default=None, required_keys=['destination', 'gateway'], - help=_("Routes associated with the router " + help=_("Add routes to the router " "destination: destination subnet (in CIDR notation) " "gateway: nexthop IP address " - "(repeat option to set multiple routes)") + "(repeat option to add multiple routes). " + "This is deprecated in favor of 'router add/remove route' " + "since it is prone to race conditions between concurrent " + "clients when not used together with --no-route to " + "overwrite the current value of 'routes'.") ) parser.add_argument( '--no-route', action='store_true', help=_("Clear routes associated with the router. " "Specify both --route and --no-route to overwrite " - "current value of route.") + "current value of routes.") ) routes_ha = parser.add_mutually_exclusive_group() routes_ha.add_argument( diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 05aad7a013..0769dca6ff 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -261,3 +261,46 @@ def test_router_set_show_unset(self): new_name )) self.assertIsNone(cmd_output["external_gateway_info"]) + + def test_router_add_remove_route(self): + network_name = uuid.uuid4().hex + subnet_name = uuid.uuid4().hex + router_name = uuid.uuid4().hex + + self.openstack('network create %s' % network_name) + self.addCleanup(self.openstack, 'network delete %s' % network_name) + + self.openstack( + 'subnet create %s ' + '--network %s --subnet-range 10.0.0.0/24' % ( + subnet_name, network_name)) + + self.openstack('router create %s' % router_name) + self.addCleanup(self.openstack, 'router delete %s' % router_name) + + self.openstack('router add subnet %s %s' % (router_name, subnet_name)) + self.addCleanup(self.openstack, 'router remove subnet %s %s' % ( + router_name, subnet_name)) + + out1 = json.loads(self.openstack( + 'router add route -f json %s ' + '--route destination=10.0.10.0/24,gateway=10.0.0.10' % + router_name)), + self.assertEqual(1, len(out1[0]['routes'])) + + self.addCleanup( + self.openstack, 'router set %s --no-route' % router_name) + + out2 = json.loads(self.openstack( + 'router add route -f json %s ' + '--route destination=10.0.10.0/24,gateway=10.0.0.10 ' + '--route destination=10.0.11.0/24,gateway=10.0.0.11' % + router_name)), + self.assertEqual(2, len(out2[0]['routes'])) + + out3 = json.loads(self.openstack( + 'router remove route -f json %s ' + '--route destination=10.0.11.0/24,gateway=10.0.0.11 ' + '--route destination=10.0.12.0/24,gateway=10.0.0.12' % + router_name)), + self.assertEqual(1, len(out3[0]['routes'])) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 38861b0ad5..09b4957cce 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -776,6 +776,146 @@ def test_remove_subnet_required_options(self): self.assertIsNone(result) +class TestAddExtraRoutesToRouter(TestRouter): + + _router = network_fakes.FakeRouter.create_one_router() + + def setUp(self): + super(TestAddExtraRoutesToRouter, self).setUp() + self.network.add_extra_routes_to_router = mock.Mock( + return_value=self._router) + self.cmd = router.AddExtraRoutesToRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + + def test_add_no_extra_route(self): + arglist = [ + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': []}}) + self.assertEqual(2, len(result)) + + def test_add_one_extra_route(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + ]}}) + self.assertEqual(2, len(result)) + + def test_add_multiple_extra_routes(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + '--route', 'destination=dst2,gateway=gw2', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [ + {'destination': 'dst1', 'gateway': 'gw1'}, + {'destination': 'dst2', 'gateway': 'gw2'}, + ]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + {'destination': 'dst2', 'nexthop': 'gw2'}, + ]}}) + self.assertEqual(2, len(result)) + + +class TestRemoveExtraRoutesFromRouter(TestRouter): + + _router = network_fakes.FakeRouter.create_one_router() + + def setUp(self): + super(TestRemoveExtraRoutesFromRouter, self).setUp() + self.network.remove_extra_routes_from_router = mock.Mock( + return_value=self._router) + self.cmd = router.RemoveExtraRoutesFromRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + + def test_remove_no_extra_route(self): + arglist = [ + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': []}}) + self.assertEqual(2, len(result)) + + def test_remove_one_extra_route(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + ]}}) + self.assertEqual(2, len(result)) + + def test_remove_multiple_extra_routes(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + '--route', 'destination=dst2,gateway=gw2', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [ + {'destination': 'dst1', 'gateway': 'gw1'}, + {'destination': 'dst2', 'gateway': 'gw2'}, + ]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + {'destination': 'dst2', 'nexthop': 'gw2'}, + ]}}) + self.assertEqual(2, len(result)) + + class TestSetRouter(TestRouter): # The router to set. diff --git a/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml new file mode 100644 index 0000000000..33b5ba7a89 --- /dev/null +++ b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Add new commands ``router add route`` and ``router remove route`` to + support new Neutron extension: ``extraroute-atomic`` (see `Neutron RFE + `_). +deprecations: + - | + The use of ``router set --route`` to add extra routes next to already + existing extra routes is deprecated in favor of ``router add route + --route``, because ``router set --route`` if used from multiple clients + concurrently may lead to lost updates. diff --git a/requirements.txt b/requirements.txt index b17b6a5593..f7a12dae85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 -openstacksdk>=0.36.0 # Apache-2.0 +openstacksdk>=0.38.0 # Apache-2.0 osc-lib>=2.0.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 60caf5db1e..1e5e36b8c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -480,11 +480,13 @@ openstack.network.v2 = port_unset = openstackclient.network.v2.port:UnsetPort router_add_port = openstackclient.network.v2.router:AddPortToRouter + router_add_route = openstackclient.network.v2.router:AddExtraRoutesToRouter router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter router_create = openstackclient.network.v2.router:CreateRouter router_delete = openstackclient.network.v2.router:DeleteRouter router_list = openstackclient.network.v2.router:ListRouter router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter + router_remove_route = openstackclient.network.v2.router:RemoveExtraRoutesFromRouter router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter router_set = openstackclient.network.v2.router:SetRouter router_show = openstackclient.network.v2.router:ShowRouter From f01a0f336c497584cc38e4396ee87282bdef1f5c Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Fri, 27 Mar 2020 16:26:50 +0100 Subject: [PATCH 0211/1120] Cleanup Python 2.7 support OpenStack is dropping the py2.7 support in ussuri cycle. Make a few cleanups: - Remove python 2.7 stanza from setup.py - Add python-requires to setup.cfg so that pypi and pip know about support Python version - Remove ancient sections from setup.cfg - Remove version_info setting from conf.py, openstackdocstheme does this automatically nowadays. Change-Id: I5b9c159752c932f874015f20822862c70562c2bd --- doc/source/conf.py | 13 ------------- setup.cfg | 4 +--- setup.py | 9 --------- 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index ff37783fb5..4de95fbb82 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,8 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import pbr.version - # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -55,17 +53,6 @@ project = u'OpenStack Command Line Client' copyright = u'2012-2013 OpenStack Foundation' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -version_info = pbr.version.VersionInfo('python-openstackclient') -# -# The short X.Y version. -version = version_info.version_string() -# The full version, including alpha/beta/rc tags. -release = version_info.release_string() - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None diff --git a/setup.cfg b/setup.cfg index 60caf5db1e..434da5b5be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,7 @@ description-file = author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-openstackclient/latest/ +python-requires = >=3.6 classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -715,9 +716,6 @@ openstack.volume.v3 = volume_transfer_request_list = openstackclient.volume.v2.volume_transfer_request:ListTransferRequest volume_transfer_request_show = openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest -[upload_sphinx] -upload-dir = doc/build/html - [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg diff --git a/setup.py b/setup.py index 566d84432e..cd35c3c35b 100644 --- a/setup.py +++ b/setup.py @@ -13,17 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass - setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) From 5e62411e5f6c5b68512d1f72470b4222199eb456 Mon Sep 17 00:00:00 2001 From: Tom Stappaerts Date: Thu, 5 Mar 2020 17:54:55 +0100 Subject: [PATCH 0212/1120] Support for stateless security groups Add support for stateful attribute of security groups, using --stateful and --no-stateful flag on security group. This allows a user to create security groups with stateful false. Change-Id: Ifd20b5fc47fd0ea0bb5aeda84820dcc0fb1e8847 Blueprint: stateless-security-groups Depends-On: https://review.opendev.org/711513/ --- openstackclient/network/v2/security_group.py | 34 +++++++++++++++++++ .../network/v2/test_security_group.py | 4 ++- .../tests/unit/network/v2/fakes.py | 1 + .../network/v2/test_security_group_network.py | 10 ++++++ ...teful-security-group-a21fa8498e866b90.yaml | 6 ++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index f8153fa8b2..dbfd908bfe 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -120,6 +120,19 @@ def update_parser_network(self, parser): metavar='', help=self.enhance_help_neutron(_("Owner's project (name or ID)")) ) + stateful_group = parser.add_mutually_exclusive_group() + stateful_group.add_argument( + "--stateful", + action='store_true', + default=None, + help=_("Security group is stateful (Default)") + ) + stateful_group.add_argument( + "--stateless", + action='store_true', + default=None, + help=_("Security group is stateless") + ) identity_common.add_project_domain_option_to_parser( parser, enhance_help=self.enhance_help_neutron) _tag.add_tag_option_to_parser_for_create( @@ -138,6 +151,10 @@ def take_action_network(self, client, parsed_args): attrs = {} attrs['name'] = parsed_args.name attrs['description'] = self._get_description(parsed_args) + if parsed_args.stateful: + attrs['stateful'] = True + if parsed_args.stateless: + attrs['stateful'] = False if parsed_args.project is not None: identity_client = self.app.client_manager.identity project_id = identity_common.find_project( @@ -313,6 +330,19 @@ def update_parser_common(self, parser): metavar="", help=_("New security group description") ) + stateful_group = parser.add_mutually_exclusive_group() + stateful_group.add_argument( + "--stateful", + action='store_true', + default=None, + help=_("Security group is stateful (Default)") + ) + stateful_group.add_argument( + "--stateless", + action='store_true', + default=None, + help=_("Security group is stateless") + ) return parser def update_parser_network(self, parser): @@ -329,6 +359,10 @@ def take_action_network(self, client, parsed_args): attrs['name'] = parsed_args.name if parsed_args.description is not None: attrs['description'] = parsed_args.description + if parsed_args.stateful: + attrs['stateful'] = True + if parsed_args.stateless: + attrs['stateful'] = False # NOTE(rtheis): Previous behavior did not raise a CommandError # if there were no updates. Maintain this behavior and issue # the update. diff --git a/openstackclient/tests/functional/network/v2/test_security_group.py b/openstackclient/tests/functional/network/v2/test_security_group.py index 8ae24b7247..d46f8db787 100644 --- a/openstackclient/tests/functional/network/v2/test_security_group.py +++ b/openstackclient/tests/functional/network/v2/test_security_group.py @@ -42,7 +42,7 @@ def test_security_group_list(self): def test_security_group_set(self): other_name = uuid.uuid4().hex raw_output = self.openstack( - 'security group set --description NSA --name ' + + 'security group set --description NSA --stateless --name ' + other_name + ' ' + self.NAME ) self.assertEqual('', raw_output) @@ -50,8 +50,10 @@ def test_security_group_set(self): cmd_output = json.loads(self.openstack( 'security group show -f json ' + other_name)) self.assertEqual('NSA', cmd_output['description']) + self.assertFalse(cmd_output['stateful']) def test_security_group_show(self): cmd_output = json.loads(self.openstack( 'security group show -f json ' + self.NAME)) self.assertEqual(self.NAME, cmd_output['name']) + self.assertTrue(cmd_output['stateful']) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 5809b225e1..f670bd0a0f 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1226,6 +1226,7 @@ def create_one_security_group(attrs=None): 'id': 'security-group-id-' + uuid.uuid4().hex, 'name': 'security-group-name-' + uuid.uuid4().hex, 'description': 'security-group-description-' + uuid.uuid4().hex, + 'stateful': True, 'project_id': 'project-id-' + uuid.uuid4().hex, 'security_group_rules': [], 'tags': [] diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index 14d5751436..20983bfea1 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -49,6 +49,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): 'name', 'project_id', 'rules', + 'stateful', 'tags', ) @@ -58,6 +59,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.name, _security_group.project_id, security_group.NetworkSecurityGroupRulesColumn([]), + _security_group.stateful, _security_group.tags, ) @@ -101,6 +103,7 @@ def test_create_all_options(self): '--description', self._security_group.description, '--project', self.project.name, '--project-domain', self.domain.name, + '--stateful', self._security_group.name, ] verifylist = [ @@ -108,6 +111,7 @@ def test_create_all_options(self): ('name', self._security_group.name), ('project', self.project.name), ('project_domain', self.domain.name), + ('stateful', self._security_group.stateful), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -115,6 +119,7 @@ def test_create_all_options(self): self.network.create_security_group.assert_called_once_with(**{ 'description': self._security_group.description, + 'stateful': self._security_group.stateful, 'name': self._security_group.name, 'tenant_id': self.project.id, }) @@ -414,11 +419,13 @@ def test_set_all_options(self): arglist = [ '--name', new_name, '--description', new_description, + '--stateful', self._security_group.name, ] verifylist = [ ('description', new_description), ('group', self._security_group.name), + ('stateful', self._security_group.stateful), ('name', new_name), ] @@ -428,6 +435,7 @@ def test_set_all_options(self): attrs = { 'description': new_description, 'name': new_name, + 'stateful': True, } self.network.update_security_group.assert_called_once_with( self._security_group, @@ -482,6 +490,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): 'name', 'project_id', 'rules', + 'stateful', 'tags', ) @@ -492,6 +501,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.project_id, security_group.NetworkSecurityGroupRulesColumn( [_security_group_rule._info]), + _security_group.stateful, _security_group.tags, ) diff --git a/releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml b/releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml new file mode 100644 index 0000000000..9b70bce86c --- /dev/null +++ b/releasenotes/notes/stateful-security-group-a21fa8498e866b90.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--stateful`` and ``--stateless`` option to the + ``security group create`` and ``security group set`` commands + to support stateful and stateless security groups. From 74a7c1d9d6efc545676cec1d9efeb2a86c5bc548 Mon Sep 17 00:00:00 2001 From: pedro Date: Mon, 23 Mar 2020 15:15:59 -0300 Subject: [PATCH 0213/1120] Add description field to portforwarding NAT rules Add the `description` field to Floating IP Port Forwardings Depends-On: https://review.opendev.org/#/c/705038/ Change-Id: I6477368e32570c96cacddba4f86455262e533277 Implements: blueprint portforwarding-description Closes-Bug: #1850818 --- lower-constraints.txt | 4 ++-- .../network/v2/floating_ip_port_forwarding.py | 20 +++++++++++++++++++ .../tests/unit/network/v2/fakes.py | 1 + .../v2/test_floating_ip_port_forwarding.py | 15 +++++++++++++- ...d-in-port-forwarding-c536e077b243d517.yaml | 6 ++++++ requirements.txt | 2 +- 6 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 83e19fe60f..acf8d78143 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -24,7 +24,7 @@ fixtures==3.0.0 flake8-import-order==0.13 flake8==2.6.2 future==0.16.0 -futurist==1.2.0 +futurist==2.1.0 gitdb==0.6.4 GitPython==1.0.1 gnocchiclient==3.3.1 @@ -50,7 +50,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.38.0 +openstacksdk==0.44.0 os-client-config==1.28.0 os-service-types==1.7.0 os-testr==1.0.0 diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py index f94bcc065a..06b3df8bcd 100644 --- a/openstackclient/network/v2/floating_ip_port_forwarding.py +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -75,6 +75,12 @@ def get_parser(self, prog_name): required=True, help=_("The protocol used in the floating IP " "port forwarding, for instance: TCP, UDP") + ), + parser.add_argument( + '--description', + metavar='', + help=_("A text to describe/contextualize the use of the " + "port forwarding configuration") ) parser.add_argument( 'floating_ip', @@ -113,6 +119,9 @@ def take_action(self, parsed_args): attrs['internal_ip_address'] = parsed_args.internal_ip_address attrs['protocol'] = parsed_args.protocol + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + obj = client.create_floating_ip_port_forwarding( floating_ip.id, **attrs @@ -212,6 +221,7 @@ def take_action(self, parsed_args): 'internal_port', 'external_port', 'protocol', + 'description', ) headers = ( 'ID', @@ -220,6 +230,7 @@ def take_action(self, parsed_args): 'Internal Port', 'External Port', 'Protocol', + 'Description', ) query = {} @@ -296,6 +307,12 @@ def get_parser(self, prog_name): metavar='', choices=['tcp', 'udp'], help=_("The IP protocol used in the floating IP port forwarding") + ), + parser.add_argument( + '--description', + metavar='', + help=_("A text to describe/contextualize the use of " + "the port forwarding configuration") ) return parser @@ -332,6 +349,9 @@ def take_action(self, parsed_args): if parsed_args.protocol: attrs['protocol'] = parsed_args.protocol + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + client.update_floating_ip_port_forwarding( floating_ip.id, parsed_args.port_forwarding_id, **attrs) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index a553f501e2..cc598834dd 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1843,6 +1843,7 @@ def create_one_port_forwarding(attrs=None): 'internal_port': randint(1, 65535), 'external_port': randint(1, 65535), 'protocol': 'tcp', + 'description': 'some description', } # Overwrite default attributes. 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 ea6cdd266f..1028c18ae9 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 @@ -62,6 +62,7 @@ def setUp(self): self.app, self.namespace) self.columns = ( + 'description', 'external_port', 'floatingip_id', 'id', @@ -73,6 +74,7 @@ def setUp(self): ) self.data = ( + self.new_port_forwarding.description, self.new_port_forwarding.external_port, self.new_port_forwarding.floatingip_id, self.new_port_forwarding.id, @@ -102,6 +104,8 @@ def test_create_all_options(self): self.new_port_forwarding.floatingip_id, '--internal-ip-address', self.new_port_forwarding.internal_ip_address, + '--description', + self.new_port_forwarding.description, ] verifylist = [ ('port', self.new_port_forwarding.internal_port_id), @@ -111,6 +115,7 @@ def test_create_all_options(self): ('floating_ip', self.new_port_forwarding.floatingip_id), ('internal_ip_address', self.new_port_forwarding. internal_ip_address), + ('description', self.new_port_forwarding.description), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -126,6 +131,7 @@ def test_create_all_options(self): 'internal_port_id': self.new_port_forwarding. internal_port_id, 'protocol': self.new_port_forwarding.protocol, + 'description': self.new_port_forwarding.description, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -251,7 +257,8 @@ class TestListFloatingIPPortForwarding(TestFloatingIPPortForwarding): 'Internal IP Address', 'Internal Port', 'External Port', - 'Protocol' + 'Protocol', + 'Description', ) def setUp(self): @@ -273,6 +280,7 @@ def setUp(self): port_forwarding.internal_port, port_forwarding.external_port, port_forwarding.protocol, + port_forwarding.description, )) self.network.floating_ip_port_forwardings = mock.Mock( return_value=self.port_forwardings @@ -393,6 +401,7 @@ def test_set_all_thing(self): '--internal-protocol-port', '100', '--external-protocol-port', '200', '--protocol', 'tcp', + '--description', 'some description', self._port_forwarding.floatingip_id, self._port_forwarding.id, ] @@ -402,6 +411,7 @@ def test_set_all_thing(self): ('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), ] @@ -415,6 +425,7 @@ def test_set_all_thing(self): 'internal_port': 100, 'external_port': 200, 'protocol': 'tcp', + 'description': 'some description', } self.network.update_floating_ip_port_forwarding.assert_called_with( self._port_forwarding.floatingip_id, @@ -428,6 +439,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): # The port forwarding to show. columns = ( + 'description', 'external_port', 'floatingip_id', 'id', @@ -450,6 +462,7 @@ def setUp(self): ) ) self.data = ( + self._port_forwarding.description, self._port_forwarding.external_port, self._port_forwarding.floatingip_id, self._port_forwarding.id, diff --git a/releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml b/releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml new file mode 100644 index 0000000000..6df5bb3a2b --- /dev/null +++ b/releasenotes/notes/add-description-field-in-port-forwarding-c536e077b243d517.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add a new option `--description` to + ``floating ip port forwarding create`` and + ``floating ip port forwarding set`` commands. diff --git a/requirements.txt b/requirements.txt index f7a12dae85..b6f97b4d35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 -openstacksdk>=0.38.0 # Apache-2.0 +openstacksdk>=0.44.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 From 725e004d32d538f9d163c727308ee20385c0310d Mon Sep 17 00:00:00 2001 From: Sean McGinnis Date: Fri, 3 Apr 2020 17:11:40 -0500 Subject: [PATCH 0214/1120] Use unittest.mock instead of third party mock Now that we no longer support py27, we can use the standard library unittest.mock module instead of the third party mock lib. Change-Id: Ibd39328c27b68190e2edbf1f52fcea52db3ae791 Signed-off-by: Sean McGinnis --- lower-constraints.txt | 1 - openstackclient/tests/unit/identity/v3/test_access_rule.py | 2 +- test-requirements.txt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 83e19fe60f..ac41658cda 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -43,7 +43,6 @@ kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.0 mccabe==0.2.1 -mock==2.0.0 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 diff --git a/openstackclient/tests/unit/identity/v3/test_access_rule.py b/openstackclient/tests/unit/identity/v3/test_access_rule.py index f8b6093a6c..904fe323d9 100644 --- a/openstackclient/tests/unit/identity/v3/test_access_rule.py +++ b/openstackclient/tests/unit/identity/v3/test_access_rule.py @@ -14,8 +14,8 @@ # import copy +from unittest import mock -import mock from osc_lib import exceptions from osc_lib import utils diff --git a/test-requirements.txt b/test-requirements.txt index 55ae1ea459..41600a6a22 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,6 @@ hacking>=2.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD flake8-import-order>=0.13 # LGPLv3 -mock>=2.0.0 # BSD oslotest>=3.2.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 From bdaebeb508a65f778b1230cc6f112f34745d6351 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Mon, 6 Apr 2020 11:16:22 +0000 Subject: [PATCH 0215/1120] Revert "Disallow setting default on internal network" The original patch assumes that both --external and --is-default are set in the same request and broke case when --is-default is set as an network update. The validation logic have to be moved on API side to avoid extra API calls from openstackclient. This reverts commit 962efd949feb461283a9bb4a668fbd310f80ba40. Related-Bug: #1745658 Change-Id: Idf08abb0e08a6880f89c3e9df9dd2ac82f36c432 --- openstackclient/network/v2/network.py | 7 +--- .../tests/unit/network/v2/test_network.py | 33 ------------------- ...-on-internal-network-824fdea1a900891c.yaml | 9 ----- 3 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 3f579b6d52..00cb782b73 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -16,7 +16,6 @@ from cliff import columns as cliff_columns from osc_lib.cli import format_columns from osc_lib.command import command -from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag @@ -126,9 +125,6 @@ def _get_attrs_network(client_manager, parsed_args): attrs['is_default'] = False if parsed_args.default: attrs['is_default'] = True - if attrs.get('is_default') and not attrs.get('router:external'): - msg = _("Cannot set default for internal network") - raise exceptions.CommandError(msg) # Update Provider network options if parsed_args.provider_network_type: attrs['provider:network_type'] = parsed_args.provider_network_type @@ -706,8 +702,7 @@ def get_parser(self, prog_name): default_router_grp.add_argument( '--default', action='store_true', - help=_("Set the network as the default external network " - "(cannot be used with internal network).") + help=_("Set the network as the default external network") ) default_router_grp.add_argument( '--no-default', diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 45d6008b5b..5f8eed6702 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -278,24 +278,6 @@ def test_create_with_tags(self): def test_create_with_no_tag(self): self._test_create_with_tag(add_tags=False) - def test_create_default_internal(self): - arglist = [ - self._network.name, - "--default", - ] - verifylist = [ - ('name', self._network.name), - ('enable', True), - ('share', None), - ('project', None), - ('external', False), - ('default', True), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - class TestCreateNetworkIdentityV2(TestNetwork): @@ -1043,21 +1025,6 @@ def test_set_with_tags(self): def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) - def test_set_default_internal(self): - arglist = [ - self._network.name, - '--internal', - '--default', - ] - verifylist = [ - ('internal', True), - ('default', True), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - class TestShowNetwork(TestNetwork): diff --git a/releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml b/releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml deleted file mode 100644 index baf4efe91d..0000000000 --- a/releasenotes/notes/disallow-setting-default-on-internal-network-824fdea1a900891c.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -fixes: - - | - For ``network create`` the - `--default`` option should be only used for external networks. - After this release, we enforce this scenario. If a users attempts - to create an internal default network or update a network to be - internal default, the command will be denied. - [Bug `1745658 `_] From 7f66273d3f2fb6449d7b50d88460ace0cb81bf30 Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Thu, 26 Mar 2020 22:23:57 +0530 Subject: [PATCH 0216/1120] Add resource option immutable This patch adds the --immutable and --no-immutable option to the role, project and domain CLI. Related-Patch: https://review.opendev.org/#/c/712182/ Change-Id: I9c3bdd741f28bf558267fb217818d947597ce13e --- doc/source/cli/command-objects/project.rst | 20 +++ doc/source/cli/command-objects/role.rst | 20 +++ openstackclient/identity/common.py | 24 +++ openstackclient/identity/v3/domain.py | 9 + openstackclient/identity/v3/project.py | 9 + openstackclient/identity/v3/role.py | 10 +- .../tests/unit/identity/v3/test_domain.py | 110 ++++++++++++ .../tests/unit/identity/v3/test_project.py | 161 ++++++++++++++++- .../tests/unit/identity/v3/test_role.py | 162 ++++++++++++++++++ ...rce_option_immutable-efed6e1ebdc69591.yaml | 9 + 10 files changed, 523 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml diff --git a/doc/source/cli/command-objects/project.rst b/doc/source/cli/command-objects/project.rst index ac7e8cd1ce..afa785cbbe 100644 --- a/doc/source/cli/command-objects/project.rst +++ b/doc/source/cli/command-objects/project.rst @@ -16,6 +16,7 @@ Create new project [--domain ] [--parent ] [--description ] + [--immutable | --no-immutable] [--enable | --disable] [--property ] [--or-show] @@ -46,6 +47,15 @@ Create new project Disable project +.. option:: --immutable + + Make project immutable. An immutable project may not be deleted or + modified except to remove the immutable flag + +.. option:: --no-immutable + + Make project mutable (default) + .. option:: --property Add a property to :ref:`\ ` @@ -180,6 +190,7 @@ Set project properties [--name ] [--domain ] [--description ] + [--immutable | --no-immutable] [--enable | --disable] [--property ] [--tag | --clear-tags | --remove-tags ] @@ -199,6 +210,15 @@ Set project properties Set project description +.. option:: --immutable + + Make project immutable. An immutable project may not be deleted or + modified except to remove the immutable flag + +.. option:: --no-immutable + + Make project mutable (default) + .. option:: --enable Enable project (default) diff --git a/doc/source/cli/command-objects/role.rst b/doc/source/cli/command-objects/role.rst index 752dc4e03c..f9fd28eb75 100644 --- a/doc/source/cli/command-objects/role.rst +++ b/doc/source/cli/command-objects/role.rst @@ -97,6 +97,7 @@ Create new role openstack role create [--or-show] [--domain ] + [--immutable | --no-immutable] .. option:: --domain @@ -119,6 +120,15 @@ Create new role Add description about the role +.. option:: --immutable + + Make role immutable. An immutable role may not be deleted or modified + except to remove the immutable flag + +.. option:: --no-immutable + + Make role mutable (default) + role delete ----------- @@ -253,6 +263,7 @@ Set role properties openstack role set [--name ] [--domain ] + [--immutable | --no-immutable] .. option:: --name @@ -269,6 +280,15 @@ Set role properties Role to modify (name or ID) +.. option:: --immutable + + Make role immutable. An immutable role may not be deleted or modified + except to remove the immutable flag + +.. option:: --no-immutable + + Make role mutable (default) + role show --------- diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 7be2a17b72..e70d87d21f 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -213,6 +213,15 @@ def _find_identity_resource(identity_client_manager, name_or_id, return resource_type(None, {'id': name_or_id, 'name': name_or_id}) +def get_immutable_options(parsed_args): + options = {} + if parsed_args.immutable: + options['immutable'] = True + if parsed_args.no_immutable: + options['immutable'] = False + return options + + def add_user_domain_option_to_parser(parser): parser.add_argument( '--user-domain', @@ -261,3 +270,18 @@ def add_inherited_option_to_parser(parser): help=_('Specifies if the role grant is inheritable to the sub ' 'projects'), ) + + +def add_resource_option_to_parser(parser): + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--immutable', + action='store_true', + help=_('Make resource immutable. An immutable project may not ' + 'be deleted or modified except to remove the immutable flag'), + ) + enable_group.add_argument( + '--no-immutable', + action='store_true', + help=_('Make resource mutable (default)'), + ) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index dbcc97f6d2..e33fce05c5 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -60,6 +60,7 @@ def get_parser(self, prog_name): action='store_true', help=_('Return existing domain'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -69,10 +70,13 @@ def take_action(self, parsed_args): if parsed_args.disable: enabled = False + options = common.get_immutable_options(parsed_args) + try: domain = identity_client.domains.create( name=parsed_args.name, description=parsed_args.description, + options=options, enabled=enabled, ) except ks_exc.Conflict: @@ -163,6 +167,7 @@ def get_parser(self, prog_name): action='store_true', help=_('Disable domain'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -180,6 +185,10 @@ def take_action(self, parsed_args): if parsed_args.disable: kwargs['enabled'] = False + options = common.get_immutable_options(parsed_args) + if options: + kwargs['options'] = options + identity_client.domains.update(domain.id, **kwargs) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 9ecc70ef4e..e32da165b4 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -78,6 +78,7 @@ def get_parser(self, prog_name): action='store_true', help=_('Return existing project'), ) + common.add_resource_option_to_parser(parser) tag.add_tag_option_to_parser_for_create(parser, _('project')) return parser @@ -99,6 +100,9 @@ def take_action(self, parsed_args): enabled = True if parsed_args.disable: enabled = False + + options = common.get_immutable_options(parsed_args) + kwargs = {} if parsed_args.property: kwargs = parsed_args.property.copy() @@ -111,6 +115,7 @@ def take_action(self, parsed_args): parent=parent, description=parsed_args.description, enabled=enabled, + options=options, **kwargs ) except ks_exc.Conflict: @@ -317,6 +322,7 @@ def get_parser(self, prog_name): help=_('Set a property on ' '(repeat option to set multiple properties)'), ) + common.add_resource_option_to_parser(parser) tag.add_tag_option_to_parser_for_set(parser, _('project')) return parser @@ -336,6 +342,9 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False + options = common.get_immutable_options(parsed_args) + if options: + kwargs['options'] = options if parsed_args.property: kwargs.update(parsed_args.property) tag.update_tags_in_args(parsed_args, project, kwargs) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 36f3f9384b..980ebf1165 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -191,6 +191,7 @@ def get_parser(self, prog_name): action='store_true', help=_('Return existing role'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -201,10 +202,12 @@ def take_action(self, parsed_args): domain_id = common.find_domain(identity_client, parsed_args.domain).id + options = common.get_immutable_options(parsed_args) + try: role = identity_client.roles.create( name=parsed_args.name, domain=domain_id, - description=parsed_args.description) + description=parsed_args.description, options=options) except ks_exc.Conflict: if parsed_args.or_show: @@ -366,6 +369,7 @@ def get_parser(self, prog_name): metavar='', help=_('Set role name'), ) + common.add_resource_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -376,12 +380,14 @@ def take_action(self, parsed_args): domain_id = common.find_domain(identity_client, parsed_args.domain).id + options = common.get_immutable_options(parsed_args) role = utils.find_resource(identity_client.roles, parsed_args.role, domain_id=domain_id) identity_client.roles.update(role.id, name=parsed_args.name, - description=parsed_args.description) + description=parsed_args.description, + options=options) class ShowRole(command.ShowOne): diff --git a/openstackclient/tests/unit/identity/v3/test_domain.py b/openstackclient/tests/unit/identity/v3/test_domain.py index 014986e573..46f389e890 100644 --- a/openstackclient/tests/unit/identity/v3/test_domain.py +++ b/openstackclient/tests/unit/identity/v3/test_domain.py @@ -68,6 +68,7 @@ def test_domain_create_no_options(self): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -97,6 +98,7 @@ def test_domain_create_description(self): kwargs = { 'name': self.domain.name, 'description': 'new desc', + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -126,6 +128,7 @@ def test_domain_create_enable(self): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': True, } self.domains_mock.create.assert_called_with( @@ -155,6 +158,7 @@ def test_domain_create_disable(self): kwargs = { 'name': self.domain.name, 'description': None, + 'options': {}, 'enabled': False, } self.domains_mock.create.assert_called_with( @@ -164,6 +168,66 @@ def test_domain_create_disable(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_domain_create_with_immutable(self): + arglist = [ + '--immutable', + self.domain.name, + ] + verifylist = [ + ('immutable', True), + ('name', self.domain.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) + + # Set expected values + kwargs = { + 'name': self.domain.name, + 'description': None, + 'options': {'immutable': True}, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_domain_create_with_no_immutable(self): + arglist = [ + '--no-immutable', + self.domain.name, + ] + verifylist = [ + ('no_immutable', True), + ('name', self.domain.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) + + # Set expected values + kwargs = { + 'name': self.domain.name, + 'description': None, + 'options': {'immutable': False}, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + class TestDomainDelete(TestDomain): @@ -354,6 +418,52 @@ def test_domain_set_disable(self): ) self.assertIsNone(result) + def test_domain_set_immutable_option(self): + arglist = [ + '--immutable', + self.domain.id, + ] + verifylist = [ + ('immutable', True), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': True}, + } + self.domains_mock.update.assert_called_with( + self.domain.id, + **kwargs + ) + self.assertIsNone(result) + + def test_domain_set_no_immutable_option(self): + arglist = [ + '--no-immutable', + self.domain.id, + ] + verifylist = [ + ('no_immutable', True), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': False}, + } + self.domains_mock.update.assert_called_with( + self.domain.id, + **kwargs + ) + self.assertIsNone(result) + class TestDomainShow(TestDomain): diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 466bea1867..8852aa8ecc 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -98,7 +98,8 @@ def test_project_create_no_options(self): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -156,7 +157,8 @@ def test_project_create_description(self): 'description': 'new desc', 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -194,7 +196,8 @@ def test_project_create_domain(self): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -232,7 +235,8 @@ def test_project_create_domain_no_perms(self): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } self.projects_mock.create.assert_called_with( **kwargs @@ -266,7 +270,8 @@ def test_project_create_enable(self): 'description': None, 'enabled': True, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -302,7 +307,8 @@ def test_project_create_disable(self): 'description': None, 'enabled': False, 'parent': None, - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, # description=, enabled=, **kwargs) @@ -339,7 +345,8 @@ def test_project_create_property(self): 'parent': None, 'fee': 'fi', 'fo': 'fum', - 'tags': [] + 'tags': [], + 'options': {}, } # ProjectManager.create(name=, domain=, description=, # enabled=, **kwargs) @@ -380,7 +387,8 @@ def test_project_create_parent(self): 'parent': self.parent.id, 'description': None, 'enabled': True, - 'tags': [] + 'tags': [], + 'options': {}, } self.projects_mock.create.assert_called_with( @@ -465,8 +473,89 @@ def test_project_create_with_tags(self): 'description': None, 'enabled': True, 'parent': None, - 'tags': ['foo'] + 'tags': ['foo'], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_with_immutable_option(self): + arglist = [ + '--immutable', + self.project.name, + ] + verifylist = [ + ('immutable', True), + ('description', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('parent', None), + ('tags', []) + ] + 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) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'tags': [], + 'options': {'immutable': True}, } + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_with_no_immutable_option(self): + arglist = [ + '--no-immutable', + self.project.name, + ] + verifylist = [ + ('no_immutable', True), + ('description', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('parent', None), + ('tags', []) + ] + 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) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'tags': [], + 'options': {'immutable': False}, + } + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( **kwargs ) @@ -927,6 +1016,60 @@ def test_project_set_tags(self): ) self.assertIsNone(result) + def test_project_set_with_immutable_option(self): + arglist = [ + '--domain', self.project.domain_id, + '--immutable', + self.project.name, + ] + verifylist = [ + ('domain', self.project.domain_id), + ('immutable', True), + ('enable', False), + ('disable', False), + ('project', self.project.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': True}, + } + self.projects_mock.update.assert_called_with( + self.project.id, + **kwargs + ) + self.assertIsNone(result) + + def test_project_set_with_no_immutable_option(self): + arglist = [ + '--domain', self.project.domain_id, + '--no-immutable', + self.project.name, + ] + verifylist = [ + ('domain', self.project.domain_id), + ('no_immutable', True), + ('enable', False), + ('disable', False), + ('project', self.project.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'options': {'immutable': False}, + } + self.projects_mock.update.assert_called_with( + self.project.id, + **kwargs + ) + self.assertIsNone(result) + class TestProjectShow(TestProject): diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 4278ab1cc1..544da7c15d 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -333,6 +333,7 @@ def test_role_create_no_options(self): 'domain': None, 'name': identity_fakes.role_name, 'description': None, + 'options': {}, } # RoleManager.create(name=, domain=) @@ -377,6 +378,7 @@ def test_role_create_with_domain(self): 'domain': identity_fakes.domain_id, 'name': identity_fakes.ROLE_2['name'], 'description': None, + 'options': {}, } # RoleManager.create(name=, domain=) @@ -420,6 +422,97 @@ def test_role_create_with_description(self): 'description': identity_fakes.role_description, 'name': identity_fakes.ROLE_2['name'], 'domain': None, + 'options': {}, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + + def test_role_create_with_immutable_option(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('immutable', True), + ('name', identity_fakes.ROLE_2['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) + + # Set expected values + kwargs = { + + 'options': {'immutable': True}, + 'description': None, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, + } + + # RoleManager.create(name=, domain=) + self.roles_mock.create.assert_called_with( + **kwargs + ) + + collist = ('domain', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + 'd1', + identity_fakes.ROLE_2['id'], + identity_fakes.ROLE_2['name'], + ) + self.assertEqual(datalist, data) + + def test_role_create_with_no_immutable_option(self): + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--no-immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('no_immutable', True), + ('name', identity_fakes.ROLE_2['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) + + # Set expected values + kwargs = { + + 'options': {'immutable': False}, + 'description': None, + 'name': identity_fakes.ROLE_2['name'], + 'domain': None, } # RoleManager.create(name=, domain=) @@ -871,6 +964,7 @@ def test_role_set_no_options(self): kwargs = { 'name': 'over', 'description': None, + 'options': {}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( @@ -903,6 +997,7 @@ def test_role_set_domain_role(self): kwargs = { 'name': 'over', 'description': None, + 'options': {}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( @@ -935,6 +1030,73 @@ def test_role_set_description(self): kwargs = { 'name': 'over', 'description': identity_fakes.role_description, + 'options': {}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_with_immutable(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('immutable', True), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': None, + 'options': {'immutable': True}, + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.ROLE_2['id'], + **kwargs + ) + self.assertIsNone(result) + + def test_role_set_with_no_immutable(self): + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE_2), + loaded=True, + ) + arglist = [ + '--name', 'over', + '--no-immutable', + identity_fakes.ROLE_2['name'], + ] + verifylist = [ + ('name', 'over'), + ('no_immutable', True), + ('role', identity_fakes.ROLE_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + 'description': None, + 'options': {'immutable': False}, } # RoleManager.update(role, name=) self.roles_mock.update.assert_called_with( diff --git a/releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml b/releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml new file mode 100644 index 0000000000..814823fe19 --- /dev/null +++ b/releasenotes/notes/add_resource_option_immutable-efed6e1ebdc69591.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added the below mentioned parameters to the role, project and domain commands. + + * --immutable + * --no-immutable + + This will allow user to set "immutable" resource option. \ No newline at end of file From 557e65d8ebaf7eaecb2f938d65feffa0e97a86d4 Mon Sep 17 00:00:00 2001 From: Igor Malinovskiy Date: Thu, 27 Feb 2020 17:50:48 +0200 Subject: [PATCH 0217/1120] Add 'subnetpool' type support to rbac commands Change-Id: Id6e528ebd1bf21ca142e60052d28371f97f629ac Partial-Bug: #1862032 Depends-On: https://review.opendev.org/710755 --- openstackclient/network/v2/network_rbac.py | 15 ++++++++++----- .../tests/unit/network/v2/test_network_rbac.py | 4 ++++ .../rbac-add-subnetpool-f1fc0e728ff61654.yaml | 4 ++++ 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/rbac-add-subnetpool-f1fc0e728ff61654.yaml diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 0d6e07927b..2c34d7ef9a 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -62,6 +62,11 @@ def _get_attrs(client_manager, parsed_args): object_id = network_client.find_address_scope( parsed_args.rbac_object, ignore_missing=False).id + if parsed_args.type == 'subnetpool': + object_id = network_client.find_subnet_pool( + parsed_args.rbac_object, + ignore_missing=False).id + attrs['object_id'] = object_id identity_client = client_manager.identity @@ -101,11 +106,11 @@ def get_parser(self, prog_name): '--type', metavar="", required=True, - choices=['address_scope', 'security_group', + choices=['address_scope', 'security_group', 'subnetpool', 'qos_policy', 'network'], help=_('Type of the object that RBAC policy ' - 'affects ("address_scope", "security_group", ' - '"qos_policy" or "network")') + 'affects ("address_scope", "security_group", "subnetpool",' + ' "qos_policy" or "network")') ) parser.add_argument( '--action', @@ -194,11 +199,11 @@ def get_parser(self, prog_name): parser.add_argument( '--type', metavar='', - choices=['address_scope', 'security_group', + choices=['address_scope', 'security_group', 'subnetpool', 'qos_policy', 'network'], help=_('List network RBAC policies according to ' 'given object type ("address_scope", "security_group", ' - '"qos_policy" or "network")') + '"subnetpool", "qos_policy" or "network")') ) parser.add_argument( '--action', diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index b3f8de4050..d7c71ea7d0 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -41,6 +41,7 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): qos_object = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() sg_object = network_fakes.FakeNetworkSecGroup.create_one_security_group() as_object = network_fakes.FakeAddressScope.create_one_address_scope() + snp_object = network_fakes.FakeSubnetPool.create_one_subnet_pool() project = identity_fakes_v3.FakeProject.create_one_project() rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac( attrs={'tenant_id': project.id, @@ -82,6 +83,8 @@ def setUp(self): return_value=self.sg_object) self.network.find_address_scope = mock.Mock( return_value=self.as_object) + self.network.find_subnet_pool = mock.Mock( + return_value=self.snp_object) self.projects_mock.get.return_value = self.project def test_network_rbac_create_no_type(self): @@ -232,6 +235,7 @@ def test_network_rbac_create_all_options(self): @ddt.data( ('qos_policy', "qos_object"), ('security_group', "sg_object"), + ('subnetpool', "snp_object"), ('address_scope', "as_object") ) @ddt.unpack diff --git a/releasenotes/notes/rbac-add-subnetpool-f1fc0e728ff61654.yaml b/releasenotes/notes/rbac-add-subnetpool-f1fc0e728ff61654.yaml new file mode 100644 index 0000000000..5bb3b59738 --- /dev/null +++ b/releasenotes/notes/rbac-add-subnetpool-f1fc0e728ff61654.yaml @@ -0,0 +1,4 @@ +features: + - | + Add ``subnetpool`` as a valid ``--type`` value for the + ``network rbac create`` and ``network rbac list`` commands. \ No newline at end of file From b328cf74df7f94a20de85b3c0992dcb65e818458 Mon Sep 17 00:00:00 2001 From: hackertron Date: Thu, 19 Mar 2020 14:35:54 +0100 Subject: [PATCH 0218/1120] Add '--force; parameter to 'openstack quota set' The compute service allows us to to force set a quota, setting a quota value that is less than the amount of the resource currently consumed. Expose this feature by way of a '--force' boolean parameter. Change-Id: I1d1ac1ac46f49f64794ffc8631e166935537966c --- openstackclient/common/quota.py | 8 +++ .../tests/functional/common/test_quota.py | 44 ++++++++++++++++ .../tests/unit/common/test_quota.py | 50 +++++++++++++++++++ ...flag-openstackclient-c172de2717e5cfac.yaml | 6 +++ 4 files changed, 108 insertions(+) create mode 100644 releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 37437344dc..7a0dda1492 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -516,6 +516,11 @@ def get_parser(self, prog_name): metavar='', help=_('Set quotas for a specific '), ) + parser.add_argument( + '--force', + action='store_true', + help=_('Force quota update (only supported by compute)') + ) return parser def take_action(self, parsed_args): @@ -529,6 +534,9 @@ def take_action(self, parsed_args): if value is not None: compute_kwargs[k] = value + if parsed_args.force: + compute_kwargs['force'] = True + volume_kwargs = {} for k, v in VOLUME_QUOTAS.items(): value = getattr(parsed_args, k, None) diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 9c05746077..4c2fc0e34d 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -165,3 +165,47 @@ def test_quota_set_class(self): # returned attributes self.assertTrue(cmd_output["key-pairs"] >= 0) self.assertTrue(cmd_output["snapshots"] >= 0) + + def test_quota_set_force(self): + """Test to set instance value by force """ + json_output = json.loads(self.openstack( + 'quota list -f json --detail --compute' + )) + in_use = limit = None + for j in json_output: + if j["Resource"] == "instances": + in_use = j["In Use"] + limit = j["Limit"] + + # Reduce count of in_use + in_use = in_use - 1 + # cannot have negative instances limit + if in_use < 0: + in_use = 0 + + # set the limit by force now + self.openstack( + 'quota set ' + self.PROJECT_NAME + + '--instances ' + str(in_use) + ' --force' + ) + cmd_output = json.loads(self.openstack( + 'quota show -f json ' + self.PROJECT_NAME + )) + self.assertIsNotNone(cmd_output) + self.assertEqual( + in_use, + cmd_output["instances"] + ) + + # Set instances limit to original limit now + self.openstack( + 'quota set ' + self.PROJECT_NAME + '--instances ' + str(limit) + ) + cmd_output = json.loads(self.openstack( + 'quota show -f json ' + self.PROJECT_NAME + )) + self.assertIsNotNone(cmd_output) + self.assertEqual( + limit, + cmd_output["instances"] + ) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index bd59ca77fe..0018e0670d 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -831,6 +831,56 @@ def test_quota_set_with_class(self): self.assertNotCalled(self.network_mock.update_quota) self.assertIsNone(result) + def test_quota_set_with_force(self): + arglist = [ + '--cores', str(compute_fakes.core_num), + '--ram', str(compute_fakes.ram_num), + '--instances', str(compute_fakes.instance_num), + '--volumes', str(volume_fakes.QUOTA['volumes']), + '--subnets', str(network_fakes.QUOTA['subnet']), + '--force', + self.projects[0].name, + ] + verifylist = [ + ('cores', compute_fakes.core_num), + ('ram', compute_fakes.ram_num), + ('instances', compute_fakes.instance_num), + ('volumes', volume_fakes.QUOTA['volumes']), + ('subnet', network_fakes.QUOTA['subnet']), + ('force', True), + ('project', self.projects[0].name), + ] + self.app.client_manager.network_endpoint_enabled = True + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs_compute = { + 'cores': compute_fakes.core_num, + 'ram': compute_fakes.ram_num, + 'instances': compute_fakes.instance_num, + 'force': True, + } + kwargs_volume = { + 'volumes': volume_fakes.QUOTA['volumes'], + } + kwargs_network = { + 'subnet': network_fakes.QUOTA['subnet'], + } + self.compute_quotas_mock.update.assert_called_once_with( + self.projects[0].id, + **kwargs_compute + ) + self.volume_quotas_mock.update.assert_called_once_with( + self.projects[0].id, + **kwargs_volume + ) + self.network_mock.update_quota.assert_called_once_with( + self.projects[0].id, + **kwargs_network + ) + self.assertIsNone(result) + class TestQuotaShow(TestQuota): diff --git a/releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml b/releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml new file mode 100644 index 0000000000..e1980d1186 --- /dev/null +++ b/releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add ``--force`` options to the ``openstack quota set`` + command. The compute service allows us to to force set a quota, setting a + quota value that is less than the amount of the resource currently + consumed. Expose this feature by way of a ``--force`` boolean parameter. From da4e1ca95f276f761844b5cf119eb70055e2c126 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 17 Apr 2020 13:14:47 +0200 Subject: [PATCH 0219/1120] Remove Babel from requirements It's not a runtime dependency (and even oslo.i18n has dropped it). The translation infrastructure installs Babel explicitly. See this mailing list thread for a full reasoning: http://lists.openstack.org/pipermail/openstack-discuss/2020-April/014227.html Keeping Babel in lower-constraints since other projects still pull it. Change-Id: Ib24d2941ac0f780f9092e48b17c343f8eb1d7151 --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b6f97b4d35..3aac36af2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 six>=1.10.0 # MIT -Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 openstacksdk>=0.44.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 From 662024646c585de6fae97390af754916151ad391 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 28 Apr 2020 15:18:35 +0000 Subject: [PATCH 0220/1120] Update master for stable/ussuri Add file to the reno documentation build to show release notes for stable/ussuri. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/ussuri. Change-Id: Ia0c7a6f2d84a7ed08514a64a73a5de577fcfb1d0 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/ussuri.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/ussuri.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index fed41f9ec4..889eeb0c5c 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + ussuri train stein rocky diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 0000000000..e21e50e0c6 --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri From cc135e3b04a2804a3ea3af487ab5dda797303d34 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 28 Apr 2020 15:18:38 +0000 Subject: [PATCH 0221/1120] Add Python3 victoria unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for victoria. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I5aa55a9582127ce07efd841b15857c272bebdb9f --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 4901a1ed8d..1ed2ab9eef 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -225,7 +225,7 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-ussuri-jobs + - openstack-python3-victoria-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 From ae6731710965bd5be6827223abf4b679266d079c Mon Sep 17 00:00:00 2001 From: Adam Harwell Date: Mon, 4 May 2020 16:06:02 -0700 Subject: [PATCH 0222/1120] Correct image lookup during server rebuild The switch to using glance from the SDK accidentally used get_image directly during a server rebuild, when it should have used find_image to match existing functionality. Bug introduced in: I36f292fb70c98f6e558f58be55d533d979c47ca7 Change-Id: I2005bd40a1bd6719670c7f7854316b4f9801b140 Story: 2007620 Task: 39643 --- openstackclient/compute/v2/server.py | 9 +++-- .../tests/unit/compute/v2/test_server.py | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 8be78049c8..93e9f966ae 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1921,9 +1921,12 @@ def _show_progress(progress): compute_client.servers, parsed_args.server) # If parsed_args.image is not set, default to the currently used one. - image_id = parsed_args.image or server.to_dict().get( - 'image', {}).get('id') - image = image_client.get_image(image_id) + 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) kwargs = {} if parsed_args.property: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 8ec8217dfe..7e4c71c50c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3577,6 +3577,41 @@ def setUp(self): self.cmd = server.RebuildServer(self.app, None) + def test_rebuild_with_image_name(self): + image_name = 'my-custom-image' + user_image = image_fakes.FakeImage.create_one_image( + attrs={'name': image_name}) + self.find_image_mock.return_value = user_image + + attrs = { + 'image': { + 'id': user_image.id + }, + 'networks': {}, + 'adminPass': 'passw0rd', + } + new_server = compute_fakes.FakeServer.create_one_server(attrs=attrs) + self.server.rebuild.return_value = new_server + + arglist = [ + self.server.id, + '--image', image_name + ] + verifylist = [ + ('server', self.server.id), + ('image', image_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Get the command object to test. + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.find_image_mock.assert_called_with( + image_name, ignore_missing=False) + self.get_image_mock.assert_called_with(user_image.id) + self.server.rebuild.assert_called_with(user_image, None) + def test_rebuild_with_current_image(self): arglist = [ self.server.id, @@ -3590,6 +3625,7 @@ def test_rebuild_with_current_image(self): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) + self.find_image_mock.assert_not_called() self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) From 52ff421e3d339f81c2625bff429e6829a2d9af67 Mon Sep 17 00:00:00 2001 From: Pete Zaitcev Date: Thu, 14 May 2020 13:53:38 -0500 Subject: [PATCH 0223/1120] Resolve PEP8 No idea how this happened, but reviews started failing the pep8 gate job. The failures are legitimate, see the commit. I guess the pep8 tests became smarter and found these issues. Change-Id: Id9a0dad644134dafd68eed37fe8f41c583d7a619 --- openstackclient/tests/functional/identity/v3/test_project.py | 1 - openstackclient/tests/unit/network/v2/fakes.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openstackclient/tests/functional/identity/v3/test_project.py b/openstackclient/tests/functional/identity/v3/test_project.py index 96d41c3a14..27cf448130 100644 --- a/openstackclient/tests/functional/identity/v3/test_project.py +++ b/openstackclient/tests/functional/identity/v3/test_project.py @@ -79,7 +79,6 @@ def test_project_set(self): '--disable ' '--property k0=v0 ' '%(name)s' % {'new_name': new_project_name, - 'domain': self.domain_name, 'name': project_name}) self.assertEqual(0, len(raw_output)) # check project details diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 2b88986a27..cef0a11c91 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1833,7 +1833,7 @@ def create_one_port_forwarding(attrs=None): """ attrs = attrs or {} floatingip_id = ( - attrs.get('floatingip_id') or'floating-ip-id-' + uuid.uuid4().hex + attrs.get('floatingip_id') or 'floating-ip-id-' + uuid.uuid4().hex ) # Set default attributes. port_forwarding_attrs = { From f6ee42cd32833bf7484cbf18eab9ed110a5e9782 Mon Sep 17 00:00:00 2001 From: Pete Zaitcev Date: Tue, 12 May 2020 23:19:38 -0500 Subject: [PATCH 0224/1120] Cleanup: remove a useless reference to "object" The method "object_list" does not have an argument "object", so we were using a built-in class "object" by mistake. Change-Id: I74687659223d31d3c3c119eee5874edff30634fd --- openstackclient/api/object_store_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index c8514a5723..dcb32878c6 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -328,7 +328,7 @@ def object_list( headers will be a dict and all header names will be lowercase. """ - if container is None or object is None: + if container is None: return None params['format'] = 'json' From 41a2e82939e3fa60922923263e8d96d9c1da2635 Mon Sep 17 00:00:00 2001 From: Pete Zaitcev Date: Fri, 15 May 2020 01:00:32 -0500 Subject: [PATCH 0225/1120] Make container list --all work The caller in openstackclient/object/v1/object.py passed a keyword argument full_listing, but the eventual callee container_list() expected all_data. So, --all did not work at all. The issue passed undetected because --all did not have a test, so we added a unit test. In addition, exisiting tests were using a test set that did not look like the real container listing, so we changed LIST_CONTAINER_RESP to be realistic. Change-Id: Id0604bcab25892e43c26cd6656b2b2eef5daa69b --- openstackclient/api/object_store_v1.py | 12 ++-- .../tests/unit/api/test_object_store_v1.py | 60 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index c8514a5723..220edb984e 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -87,7 +87,7 @@ def container_delete( def container_list( self, - all_data=False, + full_listing=False, limit=None, marker=None, end_marker=None, @@ -96,7 +96,7 @@ def container_list( ): """Get containers in an account - :param boolean all_data: + :param boolean full_listing: if True, return a full listing, else returns a max of 10000 listings :param integer limit: @@ -113,7 +113,7 @@ def container_list( params['format'] = 'json' - if all_data: + if full_listing: data = listing = self.container_list( limit=limit, marker=marker, @@ -299,7 +299,7 @@ def object_delete( def object_list( self, container=None, - all_data=False, + full_listing=False, limit=None, marker=None, end_marker=None, @@ -311,7 +311,7 @@ def object_list( :param string container: container name to get a listing for - :param boolean all_data: + :param boolean full_listing: if True, return a full listing, else returns a max of 10000 listings :param integer limit: @@ -332,7 +332,7 @@ def object_list( return None params['format'] = 'json' - if all_data: + if full_listing: data = listing = self.object_list( container=container, limit=limit, diff --git a/openstackclient/tests/unit/api/test_object_store_v1.py b/openstackclient/tests/unit/api/test_object_store_v1.py index 96c68d5a1e..b9e0740c88 100644 --- a/openstackclient/tests/unit/api/test_object_store_v1.py +++ b/openstackclient/tests/unit/api/test_object_store_v1.py @@ -30,8 +30,10 @@ FAKE_OBJECT = 'spigot' LIST_CONTAINER_RESP = [ - 'qaz', - 'fred', + {"name": "qaz", "count": 0, "bytes": 0, + "last_modified": "2020-05-16T05:52:07.377550"}, + {"name": "fred", "count": 0, "bytes": 0, + "last_modified": "2020-05-16T05:55:07.377550"}, ] LIST_OBJECT_RESP = [ @@ -117,34 +119,32 @@ def test_container_list_marker_limit_end(self): ) self.assertEqual(LIST_CONTAINER_RESP, ret) -# def test_container_list_full_listing(self): -# sess = self.app.client_manager.session -# -# def side_effect(*args, **kwargs): -# rv = sess.get().json.return_value -# sess.get().json.return_value = [] -# sess.get().json.side_effect = None -# return rv -# -# resp = [{'name': 'is-name'}] -# sess.get().json.return_value = resp -# sess.get().json.side_effect = side_effect -# -# data = lib_container.list_containers( -# self.app.client_manager.session, -# fake_url, -# full_listing=True, -# ) -# -# # Check expected values -# sess.get.assert_called_with( -# fake_url, -# params={ -# 'format': 'json', -# 'marker': 'is-name', -# } -# ) -# self.assertEqual(resp, data) + def test_container_list_full_listing(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '?limit=1&format=json', + json=[LIST_CONTAINER_RESP[0]], + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + FAKE_URL + + '?marker=%s&limit=1&format=json' % LIST_CONTAINER_RESP[0]['name'], + json=[LIST_CONTAINER_RESP[1]], + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + FAKE_URL + + '?marker=%s&limit=1&format=json' % LIST_CONTAINER_RESP[1]['name'], + json=[], + status_code=200, + ) + ret = self.api.container_list( + limit=1, + full_listing=True, + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) def test_container_show(self): headers = { From 709dfd9c214218d69a9278d0a12f09af5b4fdff2 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 18 May 2020 22:28:48 +0200 Subject: [PATCH 0226/1120] Switch to newer openstackdocstheme and reno versions Switch to openstackdocstheme 2.2.1 and reno 3.1.0 versions. Using these versions will allow especially: * Linking from HTML to PDF document * Allow parallel building of documents * Fix some rendering problems Update Sphinx version as well. openstackdocstheme renames some variables, so follow the renames before the next release removes them. A couple of variables are also not needed anymore, remove them. Set openstackdocs_auto_name to use 'project' as name. Change pygments_style to 'native' since old theme version always used 'native' and the theme now respects the setting and using 'sphinx' can lead to some strange rendering. Depends-On: https://review.opendev.org/729744 Change-Id: Id3989cabdbf2204821531b00149aa0f1cb8a4955 --- doc/requirements.txt | 6 +++--- doc/source/conf.py | 14 +++++--------- releasenotes/source/conf.py | 12 ++++-------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 5cfc337b13..ef24b68773 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,9 +1,9 @@ # 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>=1.23.2 # Apache-2.0 -reno>=2.5.0 # Apache-2.0 -sphinx!=1.6.6,!=1.6.7,>=1.6.5 # BSD +openstackdocstheme>=2.2.1 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 +sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD # redirect tests in docs diff --git a/doc/source/conf.py b/doc/source/conf.py index 4de95fbb82..2216ddd7c2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -29,11 +29,12 @@ ] # openstackdocstheme options -repository_name = 'openstack/python-openstackclient' -use_storyboard = True +openstackdocs_repo_name = 'openstack/python-openstackclient' +openstackdocs_use_storyboard = True +openstackdocs_auto_name = False # Add project 'foo' to this list to enable the :foo-doc: role -openstack_projects = [ +openstackdocs_projects = [ 'neutron', ] @@ -83,7 +84,7 @@ #show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['openstackclient.'] @@ -126,11 +127,6 @@ # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' -html_last_updated_fmt = '%Y-%m-%d %H:%M' - # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 4060f49c79..206c0ce2ac 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -44,8 +44,9 @@ ] # openstackdocstheme options -repository_name = 'openstack/python-openstackclient' -use_storyboard = True +openstackdocs_repo_name = 'openstack/python-openstackclient' +openstackdocs_use_storyboard = True +openstackdocs_auto_name = False # Set aliases for extlinks # * lpbug - generic Launchpad bug :lpbug:`123456` @@ -118,7 +119,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -169,11 +170,6 @@ # directly to the root of the documentation. # html_extra_path = [] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' -html_last_updated_fmt = '%Y-%m-%d %H:%M' - # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True From 7049fd85fc3a78b2653a4e1c153c904212b8fed8 Mon Sep 17 00:00:00 2001 From: zhangboye Date: Fri, 22 May 2020 15:05:12 +0800 Subject: [PATCH 0227/1120] Add py38 package metadata Change-Id: I75ecaed730a2ef870f41812cfbf55c74fdc36b7b --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 4129be24b1..1ba0691cc0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [files] packages = From 533af9f1b2de40d98f69e83cdf89ecf254cf3879 Mon Sep 17 00:00:00 2001 From: yanpuqing Date: Mon, 20 May 2019 06:47:44 +0000 Subject: [PATCH 0228/1120] Client should parse string to boolean for value 'is_domain' When we use "--property" parameter, client get lists these the value is string type, but the type of the value 'is_domain' should be boolean, so we should judge it and parse it. The patch parse string to boolean for value 'is_domain'. Co-Authored-By: Lance Bragstad Change-Id: I37c9eb854524bde3a1530bfe2e3a03810fb1a676 Task: 30039 Story: 2005246 --- openstackclient/identity/v3/project.py | 8 ++ .../tests/unit/identity/v3/test_project.py | 120 ++++++++++++++++++ .../notes/bug-2005246-3fb70206bafc5444.yaml | 5 + 3 files changed, 133 insertions(+) create mode 100644 releasenotes/notes/bug-2005246-3fb70206bafc5444.yaml diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index e32da165b4..5e8ce82981 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -106,6 +106,14 @@ def take_action(self, parsed_args): kwargs = {} if parsed_args.property: kwargs = parsed_args.property.copy() + if 'is_domain' in kwargs.keys(): + if kwargs['is_domain'].lower() == "true": + kwargs['is_domain'] = True + elif kwargs['is_domain'].lower() == "false": + kwargs['is_domain'] = False + elif kwargs['is_domain'].lower() == "none": + kwargs['is_domain'] = None + kwargs['tags'] = list(set(parsed_args.tags)) try: diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 8852aa8ecc..dfd0805b29 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -357,6 +357,126 @@ def test_project_create_property(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_project_create_is_domain_false_property(self): + arglist = [ + '--property', 'is_domain=false', + self.project.name, + ] + verifylist = [ + ('parent', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('tags', []), + ('property', {'is_domain': 'false'}), + ('name', self.project.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) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'is_domain': False, + 'tags': [], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_is_domain_true_property(self): + arglist = [ + '--property', 'is_domain=true', + self.project.name, + ] + verifylist = [ + ('parent', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('tags', []), + ('property', {'is_domain': 'true'}), + ('name', self.project.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) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'is_domain': True, + 'tags': [], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + + def test_project_create_is_domain_none_property(self): + arglist = [ + '--property', 'is_domain=none', + self.project.name, + ] + verifylist = [ + ('parent', None), + ('enable', False), + ('disable', False), + ('name', self.project.name), + ('tags', []), + ('property', {'is_domain': 'none'}), + ('name', self.project.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) + + # Set expected values + kwargs = { + 'name': self.project.name, + 'domain': None, + 'description': None, + 'enabled': True, + 'parent': None, + 'is_domain': None, + 'tags': [], + 'options': {}, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + def test_project_create_parent(self): self.parent = identity_fakes.FakeProject.create_one_project() self.project = identity_fakes.FakeProject.create_one_project( diff --git a/releasenotes/notes/bug-2005246-3fb70206bafc5444.yaml b/releasenotes/notes/bug-2005246-3fb70206bafc5444.yaml new file mode 100644 index 0000000000..4d7bdd5d42 --- /dev/null +++ b/releasenotes/notes/bug-2005246-3fb70206bafc5444.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + [Story `2005246 `_] + The `is_domain` property safely handles type checking. From a15b1addb41147415716417f385f8ee32aace64b Mon Sep 17 00:00:00 2001 From: Alfredo Moralejo Date: Mon, 1 Jun 2020 16:39:15 +0200 Subject: [PATCH 0229/1120] Replace assertItemsEqual with assertCountEqual assertItemsEqual was removed from Python's unittest.TestCase in Python 3.3 [1][2]. We have been able to use them since then, because testtools required unittest2, which still included it. With testtools removing Python 2.7 support [3][4], we will lose support for assertItemsEqual, so we should switch to use assertCountEqual. [1] - https://bugs.python.org/issue17866 [2] - https://hg.python.org/cpython/rev/d9921cb6e3cd [3] - testing-cabal/testtools#286 [4] - testing-cabal/testtools#277 Change-Id: I1ad0da8deda3a8cbec384b5a9c88860a526eb48c --- openstackclient/tests/unit/common/test_parseractions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/unit/common/test_parseractions.py b/openstackclient/tests/unit/common/test_parseractions.py index d015da430e..736cd0b6d2 100644 --- a/openstackclient/tests/unit/common/test_parseractions.py +++ b/openstackclient/tests/unit/common/test_parseractions.py @@ -92,7 +92,7 @@ def test_good_values(self): {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] - self.assertItemsEqual(expect, actual) + self.assertCountEqual(expect, actual) def test_empty_required_optional(self): self.parser.add_argument( @@ -116,7 +116,7 @@ def test_empty_required_optional(self): {'req1': 'aaa', 'req2': 'bbb'}, {'req1': '', 'req2': ''}, ] - self.assertItemsEqual(expect, actual) + self.assertCountEqual(expect, actual) def test_error_values_with_comma(self): self.assertRaises( From b5389dab026658ba5acc40fa8ee0513dcf6c3f5f Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Thu, 4 Jun 2020 18:29:49 +0200 Subject: [PATCH 0230/1120] Remove congress Congress and python-congressclient have been retired and also removed from global requirements, thus requirements-check job fails. Remove congress from this repo. Change-Id: Ibf68fee49e69264a1c46b2f456901d2620befe3c --- doc/requirements.txt | 1 - doc/source/cli/commands.rst | 4 ---- doc/source/cli/plugin-commands/congress.rst | 4 ---- doc/source/cli/plugin-commands/index.rst | 1 - doc/source/contributor/plugins.rst | 1 - lower-constraints.txt | 1 - 6 files changed, 12 deletions(-) delete mode 100644 doc/source/cli/plugin-commands/congress.rst diff --git a/doc/requirements.txt b/doc/requirements.txt index 5cfc337b13..8ceb80a5cf 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -14,7 +14,6 @@ aodhclient>=0.9.0 # Apache-2.0 gnocchiclient>=3.3.1 # Apache-2.0 osc-placement>=1.7.0 # Apache-2.0 python-barbicanclient>=4.5.2 # Apache-2.0 -python-congressclient<2000,>=1.9.0 # Apache-2.0 python-designateclient>=2.7.0 # Apache-2.0 python-heatclient>=1.10.0 # Apache-2.0 python-ironicclient>=2.3.0 # Apache-2.0 diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 7c38aa5b5c..97a829b558 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -195,10 +195,6 @@ conflicts when creating new plugins. For a complete list check out * ``cluster profile``: (**Clustering (Senlin)**) * ``cluster profile type``: (**Clustering (Senlin)**) * ``cluster receiver``: (**Clustering (Senlin)**) -* ``congress datasource``: (**Policy (Congress)**) -* ``congress driver``: (**Policy (Congress)**) -* ``congress policy``: (**Policy (Congress)**) -* ``congress policy rule``: (**Policy (Congress)**) * ``cron trigger``: (**Workflow Engine (Mistral)**) * ``database flavor``: (**Database (Trove)**) * ``dataprocessing data source``: (**Data Processing (Sahara)**) diff --git a/doc/source/cli/plugin-commands/congress.rst b/doc/source/cli/plugin-commands/congress.rst deleted file mode 100644 index 7a1e63b76a..0000000000 --- a/doc/source/cli/plugin-commands/congress.rst +++ /dev/null @@ -1,4 +0,0 @@ -congress --------- - -.. autoprogram-cliff:: openstack.congressclient.v1 diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst index 2c3bda3e0f..8217115faa 100644 --- a/doc/source/cli/plugin-commands/index.rst +++ b/doc/source/cli/plugin-commands/index.rst @@ -9,7 +9,6 @@ Plugin Commands aodh barbican - congress designate gnocchi heat diff --git a/doc/source/contributor/plugins.rst b/doc/source/contributor/plugins.rst index d6bcbee197..374b274e6a 100644 --- a/doc/source/contributor/plugins.rst +++ b/doc/source/contributor/plugins.rst @@ -27,7 +27,6 @@ The following is a list of projects that are an OpenStackClient plugin. - gnocchiclient - osc-placement - python-barbicanclient -- python-congressclient - python-designateclient - python-heatclient - python-ironicclient diff --git a/lower-constraints.txt b/lower-constraints.txt index df79ef5dd3..0c754f5c27 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -87,7 +87,6 @@ pyparsing==2.1.0 pyperclip==1.5.27 python-barbicanclient==4.5.2 python-cinderclient==3.3.0 -python-congressclient==1.9.0 python-dateutil==2.5.3 python-designateclient==2.7.0 python-glanceclient==2.8.0 From aa7b84fd08ef3d4d096d76837c636b708d743f46 Mon Sep 17 00:00:00 2001 From: Maari Tamm Date: Thu, 6 Feb 2020 10:36:41 +0000 Subject: [PATCH 0231/1120] Add OpenStack Client for Manila docs This commit includes documentation about OSC implementation for Manila. bp openstack-client-support Change-Id: Ic91a81e16e506103c08ef42ed0f8634a2b70e1dd --- doc/requirements.txt | 1 + doc/source/cli/plugin-commands/index.rst | 1 + doc/source/cli/plugin-commands/manila.rst | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 doc/source/cli/plugin-commands/manila.rst diff --git a/doc/requirements.txt b/doc/requirements.txt index 5cfc337b13..24d43970cc 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -20,6 +20,7 @@ python-heatclient>=1.10.0 # Apache-2.0 python-ironicclient>=2.3.0 # Apache-2.0 python-ironic-inspector-client>=1.5.0 # Apache-2.0 python-karborclient>=0.6.0 # Apache-2.0 +python-manilaclient>=2.0.0 # Apache-2.0 python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 python-muranoclient>=0.8.2 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst index 2c3bda3e0f..2eb89dd748 100644 --- a/doc/source/cli/plugin-commands/index.rst +++ b/doc/source/cli/plugin-commands/index.rst @@ -16,6 +16,7 @@ Plugin Commands ironic ironic-inspector karbor + manila mistral neutron octavia diff --git a/doc/source/cli/plugin-commands/manila.rst b/doc/source/cli/plugin-commands/manila.rst new file mode 100644 index 0000000000..c42d6065f4 --- /dev/null +++ b/doc/source/cli/plugin-commands/manila.rst @@ -0,0 +1,4 @@ +manila +------ + +.. autoprogram-cliff:: openstack.share.v2 From 2cb41935790e0d99139340ede22792ec8a842b00 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 Jun 2020 09:19:24 -0500 Subject: [PATCH 0232/1120] Add cliff to libs-from-git for devstack functional tips We should also use cliff from git in the tips jobs so that we can see whether changes there break things. Change-Id: Ie3375eb7bafafef7fa8209aa6500d1254e29954e --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 1ed2ab9eef..5e0e73f2ea 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -132,7 +132,7 @@ - openstack/python-openstackclient vars: devstack_localrc: - LIBS_FROM_GIT: python-openstackclient,openstacksdk,osc-lib,os-client-config + LIBS_FROM_GIT: python-openstackclient,openstacksdk,osc-lib,os-client-config,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 From 7696593dc18aff34ab0abbc9020616b608219e58 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 8 Jun 2020 09:26:00 -0500 Subject: [PATCH 0233/1120] Remove os-client-config references We've depended on openstacksdk for config for ages now, clean up after ourselves and stop installing it in tests. Change-Id: I66b3ec2a36bc462d2f1ac151e95ccbdc946076b8 --- .zuul.yaml | 5 +---- lower-constraints.txt | 1 - .../tests/unit/integ/cli/test_shell.py | 20 ++++--------------- test-requirements.txt | 1 - tox.ini | 2 -- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 5e0e73f2ea..734b1ededc 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -9,7 +9,6 @@ - openstack/cliff - openstack/keystoneauth - openstack/openstacksdk - - openstack/os-client-config - openstack/osc-lib - openstack/python-openstackclient vars: @@ -30,7 +29,6 @@ - openstack/cliff - openstack/keystoneauth - openstack/openstacksdk - - openstack/os-client-config - openstack/osc-lib - openstack/python-openstackclient vars: @@ -127,12 +125,11 @@ - openstack/cliff - openstack/keystoneauth - openstack/openstacksdk - - openstack/os-client-config - openstack/osc-lib - openstack/python-openstackclient vars: devstack_localrc: - LIBS_FROM_GIT: python-openstackclient,openstacksdk,osc-lib,os-client-config,cliff + 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 diff --git a/lower-constraints.txt b/lower-constraints.txt index 0c754f5c27..f16eca4184 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -51,7 +51,6 @@ munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 openstacksdk==0.44.0 -os-client-config==1.28.0 os-service-types==1.7.0 os-testr==1.0.0 osc-lib==2.0.0 diff --git a/openstackclient/tests/unit/integ/cli/test_shell.py b/openstackclient/tests/unit/integ/cli/test_shell.py index 0c98a12942..5788b47375 100644 --- a/openstackclient/tests/unit/integ/cli/test_shell.py +++ b/openstackclient/tests/unit/integ/cli/test_shell.py @@ -20,16 +20,6 @@ from openstackclient.tests.unit.integ import base as test_base from openstackclient.tests.unit import test_shell -# NOTE(dtroyer): Attempt the import to detect if the SDK installed is new -# enough to contain the os_client_config code. If so, use -# that path for mocks. -CONFIG_MOCK_BASE = "openstack.config.loader" -try: - from openstack.config import defaults # noqa -except ImportError: - # Fall back to os-client-config - CONFIG_MOCK_BASE = "os_client_config.config" - class TestIntegShellCliNoAuth(test_base.TestInteg): @@ -455,8 +445,8 @@ def get_temp_file_path(self, filename): temp_dir = self.useFixture(fixtures.TempDir()) return temp_dir.join(filename) - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_vendor_file") - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_config_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_1(self, config_mock, vendor_mock): """Precedence run 1 @@ -473,7 +463,6 @@ def vendor_mock_return(): return ('file.yaml', copy.deepcopy(test_shell.PUBLIC_1)) vendor_mock.side_effect = vendor_mock_return - print("CONFIG_MOCK_BASE=%s" % CONFIG_MOCK_BASE) _shell = shell.OpenStackShell() _shell.run( "--os-password qaz extension list".split(), @@ -527,8 +516,8 @@ def vendor_mock_return(): # +env, +cli, +occ # see test_shell_args_precedence_2() - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_vendor_file") - @mock.patch(CONFIG_MOCK_BASE + ".OpenStackConfig._load_config_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_vendor_file") + @mock.patch("openstack.config.loader.OpenStackConfig._load_config_file") def test_shell_args_precedence_2(self, config_mock, vendor_mock): """Precedence run 2 @@ -545,7 +534,6 @@ def vendor_mock_return(): return ('file.yaml', copy.deepcopy(test_shell.PUBLIC_1)) vendor_mock.side_effect = vendor_mock_return - print("CONFIG_MOCK_BASE=%s" % CONFIG_MOCK_BASE) _shell = shell.OpenStackShell() _shell.run( "--os-username zarquon --os-password qaz " diff --git a/test-requirements.txt b/test-requirements.txt index ed3cf0c004..f2b6a13402 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,6 @@ oslotest>=3.2.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0 -os-client-config>=1.28.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 3b4b66a0fe..3a2a307de8 100644 --- a/tox.ini +++ b/tox.ini @@ -64,7 +64,6 @@ commands = python -m pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" python -m pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" python -m pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" - python -m pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" pythom -m pip install -q -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" python -m pip freeze stestr run {posargs} @@ -83,7 +82,6 @@ commands = python -m pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" python -m pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" python -m pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" - python -m pip install -q -U -e "git+file://{toxinidir}/../os-client-config#egg=os_client_config" python -m pip install -q -U -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" python -m pip freeze stestr run {posargs} From 26878e7d53a531adb67c9ddde8fd6d3c3092ce34 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Mon, 4 Nov 2019 17:16:34 -0600 Subject: [PATCH 0234/1120] identity: autogenerate docs $namespace = openstack.identity.v{2|3} The subcommand documents for $namespace were hardcoded and thus prone to drift over time. This commit removes the hardcoded content and uses the autoprogram-cliff directive to generate them automatically from the subcommand configuration classes. Special things: - Some reorganization happened here. Certain subcommand names, such as `endpoint` and `project`, are shared by identify v2 and v3. Previously the hardcoded documents had them combined and interleaved. Attempting to preserve this with autoprogram-cliff would have required significant additional infrastructure. However, since most readers care completely about one and not at all about the other, we instead split the v2 and v3 versions of these commands into separate pages. In case links to the old pages exist in the wild, they are preserved, but moved (with redirects) to a hidden directory, and populated simply with links to the new version-specific generated documents. - The `federation domain` and `federation project` subcommands were previously absent from the docs. They are added. These are such small commands and they seem related, so they're put into a single document. - Some pages were already being generated but were listing operations individually instead of using wildcards (possibly because they were created before wildcarding was supported by cliff). These are changed to use wildcarding. (We want to do this wherever possible as it is more future-proof in the event that more operations are added to a subcommand later.) - The `service provider` document was incorrectly titled `identity provider`. Fixed. Change-Id: I2030f9fe370038c5908b6eb6bed9692a73fe5067 --- doc/source/_extra/.htaccess | 3 + doc/source/cli/_hidden/ec2-credentials.rst | 13 + doc/source/cli/_hidden/endpoint.rst | 13 + doc/source/cli/_hidden/project.rst | 13 + doc/source/cli/_hidden/role.rst | 13 + doc/source/cli/_hidden/service.rst | 13 + doc/source/cli/_hidden/token.rst | 13 + doc/source/cli/_hidden/user.rst | 13 + .../application-credentials.rst | 107 +----- doc/source/cli/command-objects/catalog.rst | 5 +- doc/source/cli/command-objects/consumer.rst | 14 +- doc/source/cli/command-objects/credential.rst | 124 +------ .../command-objects/ec2-credentials-v2.rst | 6 + .../command-objects/ec2-credentials-v3.rst | 6 + .../cli/command-objects/ec2-credentials.rst | 138 ------- .../cli/command-objects/endpoint-v2.rst | 6 + .../cli/command-objects/endpoint-v3.rst | 24 ++ doc/source/cli/command-objects/endpoint.rst | 275 -------------- .../cli/command-objects/endpoint_group.rst | 20 +- .../federation-domain-project.rst | 11 + .../command-objects/federation-protocol.rst | 14 +- doc/source/cli/command-objects/group.rst | 245 +----------- .../cli/command-objects/identity-provider.rst | 14 +- .../cli/command-objects/implied_role.rst | 53 +-- doc/source/cli/command-objects/limit.rst | 127 +------ doc/source/cli/command-objects/mapping.rst | 14 +- doc/source/cli/command-objects/policy.rst | 14 +- doc/source/cli/command-objects/project-v2.rst | 6 + doc/source/cli/command-objects/project-v3.rst | 6 + doc/source/cli/command-objects/project.rst | 297 --------------- doc/source/cli/command-objects/region.rst | 14 +- .../cli/command-objects/registered-limit.rst | 133 +------ .../cli/command-objects/request-token.rst | 5 +- doc/source/cli/command-objects/role-v2.rst | 6 + doc/source/cli/command-objects/role-v3.rst | 6 + doc/source/cli/command-objects/role.rst | 312 ---------------- .../cli/command-objects/service-provider.rst | 14 +- doc/source/cli/command-objects/service-v2.rst | 6 + doc/source/cli/command-objects/service-v3.rst | 18 + doc/source/cli/command-objects/service.rst | 143 ------- doc/source/cli/command-objects/token-v2.rst | 7 + doc/source/cli/command-objects/token-v3.rst | 7 + doc/source/cli/command-objects/token.rst | 30 -- doc/source/cli/command-objects/trust.rst | 11 +- doc/source/cli/command-objects/user-v2.rst | 7 + doc/source/cli/command-objects/user-v3.rst | 7 + doc/source/cli/command-objects/user.rst | 349 ------------------ doc/source/cli/index.rst | 9 + doc/test/redirect-tests.txt | 7 + setup.cfg | 18 +- 50 files changed, 271 insertions(+), 2458 deletions(-) create mode 100644 doc/source/cli/_hidden/ec2-credentials.rst create mode 100644 doc/source/cli/_hidden/endpoint.rst create mode 100644 doc/source/cli/_hidden/project.rst create mode 100644 doc/source/cli/_hidden/role.rst create mode 100644 doc/source/cli/_hidden/service.rst create mode 100644 doc/source/cli/_hidden/token.rst create mode 100644 doc/source/cli/_hidden/user.rst create mode 100644 doc/source/cli/command-objects/ec2-credentials-v2.rst create mode 100644 doc/source/cli/command-objects/ec2-credentials-v3.rst delete mode 100644 doc/source/cli/command-objects/ec2-credentials.rst create mode 100644 doc/source/cli/command-objects/endpoint-v2.rst create mode 100644 doc/source/cli/command-objects/endpoint-v3.rst delete mode 100644 doc/source/cli/command-objects/endpoint.rst create mode 100644 doc/source/cli/command-objects/federation-domain-project.rst create mode 100644 doc/source/cli/command-objects/project-v2.rst create mode 100644 doc/source/cli/command-objects/project-v3.rst delete mode 100644 doc/source/cli/command-objects/project.rst create mode 100644 doc/source/cli/command-objects/role-v2.rst create mode 100644 doc/source/cli/command-objects/role-v3.rst delete mode 100644 doc/source/cli/command-objects/role.rst create mode 100644 doc/source/cli/command-objects/service-v2.rst create mode 100644 doc/source/cli/command-objects/service-v3.rst delete mode 100644 doc/source/cli/command-objects/service.rst create mode 100644 doc/source/cli/command-objects/token-v2.rst create mode 100644 doc/source/cli/command-objects/token-v3.rst delete mode 100644 doc/source/cli/command-objects/token.rst create mode 100644 doc/source/cli/command-objects/user-v2.rst create mode 100644 doc/source/cli/command-objects/user-v3.rst delete mode 100644 doc/source/cli/command-objects/user.rst diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess index 513deddbdc..1ac063cc35 100644 --- a/doc/source/_extra/.htaccess +++ b/doc/source/_extra/.htaccess @@ -9,3 +9,6 @@ redirectmatch 301 ^/python-openstackclient/([^/]+)/specs/([^/.]+).html$ /python- redirectmatch 301 ^/python-openstackclient/([^/]+)/(command-(beta|errors|logs|options|wrappers)|developing|humaninterfaceguide|plugins).html$ /python-openstackclient/$1/contributor/$2.html redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/plugin-commands.html$ /python-openstackclient/$1/cli/plugin-commands/index.html +# Identity pages were split into -v2 and -v3 for common subcommand names. +# The unversioned page is hidden but contains links to the versioned pages so links in the wild redirect somewhere sane. +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(ec2-credentials|endpoint|project|role|service|token|user).html$ /python-openstackclient/$1/cli/_hidden/$2.html diff --git a/doc/source/cli/_hidden/ec2-credentials.rst b/doc/source/cli/_hidden/ec2-credentials.rst new file mode 100644 index 0000000000..c54459d877 --- /dev/null +++ b/doc/source/cli/_hidden/ec2-credentials.rst @@ -0,0 +1,13 @@ +=============== +ec2 credentials +=============== + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/ec2-credentials-v2 + ../command-objects/ec2-credentials-v3 diff --git a/doc/source/cli/_hidden/endpoint.rst b/doc/source/cli/_hidden/endpoint.rst new file mode 100644 index 0000000000..744e3badcb --- /dev/null +++ b/doc/source/cli/_hidden/endpoint.rst @@ -0,0 +1,13 @@ +======== +endpoint +======== + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/endpoint-v2 + ../command-objects/endpoint-v3 diff --git a/doc/source/cli/_hidden/project.rst b/doc/source/cli/_hidden/project.rst new file mode 100644 index 0000000000..209a3129df --- /dev/null +++ b/doc/source/cli/_hidden/project.rst @@ -0,0 +1,13 @@ +======= +project +======= + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/project-v2 + ../command-objects/project-v3 diff --git a/doc/source/cli/_hidden/role.rst b/doc/source/cli/_hidden/role.rst new file mode 100644 index 0000000000..c85f48146c --- /dev/null +++ b/doc/source/cli/_hidden/role.rst @@ -0,0 +1,13 @@ +==== +role +==== + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/role-v2 + ../command-objects/role-v3 diff --git a/doc/source/cli/_hidden/service.rst b/doc/source/cli/_hidden/service.rst new file mode 100644 index 0000000000..8b33638610 --- /dev/null +++ b/doc/source/cli/_hidden/service.rst @@ -0,0 +1,13 @@ +======= +service +======= + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/service-v2 + ../command-objects/service-v3 diff --git a/doc/source/cli/_hidden/token.rst b/doc/source/cli/_hidden/token.rst new file mode 100644 index 0000000000..6ebf801b76 --- /dev/null +++ b/doc/source/cli/_hidden/token.rst @@ -0,0 +1,13 @@ +===== +token +===== + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/token-v2 + ../command-objects/token-v3 diff --git a/doc/source/cli/_hidden/user.rst b/doc/source/cli/_hidden/user.rst new file mode 100644 index 0000000000..34eb59954f --- /dev/null +++ b/doc/source/cli/_hidden/user.rst @@ -0,0 +1,13 @@ +==== +user +==== + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/user-v2 + ../command-objects/user-v3 diff --git a/doc/source/cli/command-objects/application-credentials.rst b/doc/source/cli/command-objects/application-credentials.rst index 047f5ab661..cef3ef0dfc 100644 --- a/doc/source/cli/command-objects/application-credentials.rst +++ b/doc/source/cli/command-objects/application-credentials.rst @@ -8,109 +8,6 @@ With application credentials, a user can grant their applications limited access to their cloud resources. Once created, users can authenticate with an application credential by using the ``v3applicationcredential`` auth type. -application credential create ------------------------------ -Create new application credential - -.. program:: application credential create -.. code:: bash - - openstack application credential create - [--secret ] - [--role ] - [--expiration ] - [--description ] - [--restricted|--unrestricted] - [--access-rules ] - - -.. option:: --secret - - Secret to use for authentication (if not provided, one will be generated) - -.. option:: --role - - Roles to authorize (name or ID) (repeat option to set multiple values) - -.. option:: --expiration - - Sets an expiration date for the application credential (format of - YYYY-mm-ddTHH:MM:SS) - -.. option:: --description - - Application credential description - -.. option:: --unrestricted - - Enable application credential to create and delete other application - credentials and trusts (this is potentially dangerous behavior and is - disabled by default) - -.. option:: --restricted - - Prohibit application credential from creating and deleting other - application credentials and trusts (this is the default behavior) - -.. option:: --access-rules - - Either a string or file path containing a JSON-formatted list of access - rules, each containing a request method, path, and service, for example - '[{"method": "GET", "path": "/v2.1/servers", "service": "compute"}]' - -.. describe:: - - Name of the application credential - - -application credential delete ------------------------------ - -Delete application credential(s) - -.. program:: application credential delete -.. code:: bash - - openstack application credential delete - [ ...] - -.. describe:: - - Application credential(s) to delete (name or ID) - -application credential list ---------------------------- - -List application credentials - -.. program:: application credential list -.. code:: bash - - openstack application credential list - [--user ] - [--user-domain ] - -.. option:: --user - - User whose application credentials to list (name or ID) - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - -application credential show ---------------------------- - -Display application credential details - -.. program:: application credential show -.. code:: bash - - openstack application credential show - - -.. describe:: - - Application credential to display (name or ID) +.. autoprogram-cliff:: openstack.identity.v3 + :command: application credential * diff --git a/doc/source/cli/command-objects/catalog.rst b/doc/source/cli/command-objects/catalog.rst index 6db8227ec4..84cd160a0b 100644 --- a/doc/source/cli/command-objects/catalog.rst +++ b/doc/source/cli/command-objects/catalog.rst @@ -6,7 +6,4 @@ A **catalog** lists OpenStack services that are available on the cloud. Applicable to Identity v2 and v3 .. autoprogram-cliff:: openstack.identity.v3 - :command: catalog list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: catalog show + :command: catalog * diff --git a/doc/source/cli/command-objects/consumer.rst b/doc/source/cli/command-objects/consumer.rst index 8f6dda0ad4..17cfc0c996 100644 --- a/doc/source/cli/command-objects/consumer.rst +++ b/doc/source/cli/command-objects/consumer.rst @@ -7,16 +7,4 @@ is used to create a **request token** and **access token**. Applicable to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: consumer create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: consumer delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: consumer list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: consumer set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: consumer show + :command: consumer * diff --git a/doc/source/cli/command-objects/credential.rst b/doc/source/cli/command-objects/credential.rst index 7fe57310f3..f490f1cfee 100644 --- a/doc/source/cli/command-objects/credential.rst +++ b/doc/source/cli/command-objects/credential.rst @@ -4,125 +4,5 @@ credential Identity v3 -credential create ------------------ - -Create new credential - -.. program:: credential create -.. code:: bash - - openstack credential create - [--type ] - [--project ] - - -.. option:: --type - - New credential type: cert, ec2, totp and so on - -.. option:: --project - - Project which limits the scope of the credential (name or ID) - -.. _credential_create: -.. describe:: - - User that owns the credential (name or ID) - -.. describe:: - - New credential data - -credential delete ------------------ - -Delete credential(s) - -.. program:: credential delete -.. code:: bash - - openstack credential delete - [ ...] - -.. _credential_delete: -.. describe:: - - ID(s) of credential to delete - -credential list ---------------- - -List credentials - -.. program:: credential list -.. code:: bash - - openstack credential list - [--user [--user-domain ]] - [--type ] - -.. option:: --user - - Filter credentials by (name or ID) - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - -.. option:: --type - - Filter credentials by type: cert, ec2, totp and so on - -credential set --------------- - -Set credential properties - -.. program:: credential set -.. code:: bash - - openstack credential set - [--user ] - [--type ] - [--data ] - [--project ] - - -.. option:: --user - - User that owns the credential (name or ID) - -.. option:: --type - - New credential type: cert, ec2, totp and so on. - -.. option:: --data - - New credential data - -.. option:: --project - - Project which limits the scope of the credential (name or ID) - -.. _credential_set: -.. describe:: - - ID of credential to change - -credential show ---------------- - -Display credential details - -.. program:: credential show -.. code:: bash - - openstack credential show - - -.. _credential_show: -.. describe:: - - ID of credential to display +.. autoprogram-cliff:: openstack.identity.v3 + :command: credential * diff --git a/doc/source/cli/command-objects/ec2-credentials-v2.rst b/doc/source/cli/command-objects/ec2-credentials-v2.rst new file mode 100644 index 0000000000..ace3d84ec5 --- /dev/null +++ b/doc/source/cli/command-objects/ec2-credentials-v2.rst @@ -0,0 +1,6 @@ +============================= +ec2 credentials (Identity v2) +============================= + +.. autoprogram-cliff:: openstack.identity.v2 + :command: ec2 credentials * diff --git a/doc/source/cli/command-objects/ec2-credentials-v3.rst b/doc/source/cli/command-objects/ec2-credentials-v3.rst new file mode 100644 index 0000000000..761d428050 --- /dev/null +++ b/doc/source/cli/command-objects/ec2-credentials-v3.rst @@ -0,0 +1,6 @@ +============================= +ec2 credentials (Identity v3) +============================= + +.. autoprogram-cliff:: openstack.identity.v3 + :command: ec2 credentials * diff --git a/doc/source/cli/command-objects/ec2-credentials.rst b/doc/source/cli/command-objects/ec2-credentials.rst deleted file mode 100644 index 9174b0413e..0000000000 --- a/doc/source/cli/command-objects/ec2-credentials.rst +++ /dev/null @@ -1,138 +0,0 @@ -=============== -ec2 credentials -=============== - -Identity v2 - -ec2 credentials create ----------------------- - -Create EC2 credentials - -.. program:: ec2 credentials create -.. code-block:: bash - - openstack ec2 credentials create - [--project ] - [--user ] - [--user-domain ] - [--project-domain ] - -.. option:: --project - - Create credentials in project (name or ID; default: current authenticated project) - -.. option:: --user - - Create credentials for user (name or ID; default: current authenticated user) - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - - .. versionadded:: 3 - -.. option:: --project-domain - - Domain the project belongs to (name or ID). This can be - used in case collisions between user names exist. - - .. versionadded:: 3 - -The :option:`--project` and :option:`--user` options are typically only -useful for admin users, but may be allowed for other users depending on -the policy of the cloud and the roles granted to the user. - -ec2 credentials delete ----------------------- - -Delete EC2 credentials - -.. program:: ec2 credentials delete -.. code-block:: bash - - openstack ec2 credentials delete - [--user ] - [--user-domain ] - [ ...] - -.. option:: --user - - Delete credentials for user (name or ID) - -.. option:: --user-domain - - Select user from a specific domain (name or ID) - This can be used in case collisions between user names exist. - - .. versionadded:: 3 - -.. _ec2_credentials_delete-access-key: -.. describe:: access-key - - Credentials access key(s) - -The :option:`--user` option is typically only useful for admin users, but -may be allowed for other users depending on the policy of the cloud and -the roles granted to the user. - -ec2 credentials list --------------------- - -List EC2 credentials - -.. program:: ec2 credentials list -.. code-block:: bash - - openstack ec2 credentials list - [--user ] - [--user-domain ] - -.. option:: --user - - Filter list by (name or ID) - -.. option:: --user-domain - - Select user from a specific domain (name or ID) - This can be used in case collisions between user names exist. - - .. versionadded:: 3 - -The :option:`--user` option is typically only useful for admin users, but -may be allowed for other users depending on the policy of the cloud and -the roles granted to the user. - -ec2 credentials show --------------------- - -Display EC2 credentials details - -.. program:: ec2 credentials show -.. code-block:: bash - - openstack ec2 credentials show - [--user ] - [--user-domain ] - - -.. option:: --user - - Show credentials for user (name or ID) - -.. option:: --user-domain - - Select user from a specific domain (name or ID) - This can be used in case collisions between user names exist. - - .. versionadded:: 3 - -.. _ec2_credentials_show-access-key: -.. describe:: access-key - - Credentials access key - -The :option:`--user` option is typically only useful for admin users, but -may be allowed for other users depending on the policy of the cloud and -the roles granted to the user. diff --git a/doc/source/cli/command-objects/endpoint-v2.rst b/doc/source/cli/command-objects/endpoint-v2.rst new file mode 100644 index 0000000000..7badfd4080 --- /dev/null +++ b/doc/source/cli/command-objects/endpoint-v2.rst @@ -0,0 +1,6 @@ +====================== +endpoint (Identity v2) +====================== + +.. autoprogram-cliff:: openstack.identity.v2 + :command: endpoint * diff --git a/doc/source/cli/command-objects/endpoint-v3.rst b/doc/source/cli/command-objects/endpoint-v3.rst new file mode 100644 index 0000000000..f12063fb0c --- /dev/null +++ b/doc/source/cli/command-objects/endpoint-v3.rst @@ -0,0 +1,24 @@ +====================== +endpoint (Identity v3) +====================== + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint add project + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint create + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint remove project + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint set + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint show diff --git a/doc/source/cli/command-objects/endpoint.rst b/doc/source/cli/command-objects/endpoint.rst deleted file mode 100644 index 6d0253278b..0000000000 --- a/doc/source/cli/command-objects/endpoint.rst +++ /dev/null @@ -1,275 +0,0 @@ -======== -endpoint -======== - -Identity v2, v3 - -endpoint add project --------------------- - -Associate a project to an endpoint for endpoint filtering - -.. program:: endpoint add project -.. code:: bash - - openstack endpoint add project - [--project-domain ] - - - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _endpoint_add_project-endpoint: -.. describe:: - - Endpoint to associate with specified project (name or ID) - -.. _endpoint_add_project-project: -.. describe:: - - Project to associate with specified endpoint (name or ID) - -endpoint create ---------------- - -Create new endpoint - -*Identity version 2 only* - -.. program:: endpoint create (v2) -.. code:: bash - - openstack endpoint create - --publicurl - [--adminurl ] - [--internalurl ] - [--region ] - - -.. option:: --publicurl - - New endpoint public URL (required) - -.. option:: --adminurl - - New endpoint admin URL - -.. option:: --internalurl - - New endpoint internal URL - -.. option:: --region - - New endpoint region ID - -.. _endpoint_create-endpoint: -.. describe:: - - Service to be associated with new endpoint (name or ID) - -*Identity version 3 only* - -.. program:: endpoint create (v3) -.. code:: bash - - openstack endpoint create - [--region ] - [--enable | --disable] - - - - -.. option:: --region - - New endpoint region ID - -.. option:: --enable - - Enable endpoint (default) - -.. option:: --disable - - Disable endpoint - -.. describe:: - - Service to be associated with new endpoint(name or ID) - -.. describe:: - - New endpoint interface type (admin, public or internal) - -.. describe:: - - New endpoint URL - -endpoint delete ---------------- - -Delete endpoint(s) - -.. program:: endpoint delete -.. code:: bash - - openstack endpoint delete - [ ...] - -.. _endpoint_delete-endpoint: -.. describe:: - - Endpoint(s) to delete (ID only) - -endpoint list -------------- - -List endpoints - -.. program:: endpoint list -.. code:: bash - - openstack endpoint list - [--service ] - [--interface ] - [--region ] - [--long] - [--endpoint | - --project [--project-domain ]] - -.. option:: --service - - Filter by service (type, name or ID) - - *Identity version 3 only* - -.. option:: --interface - - Filter by interface type (admin, public or internal) - - *Identity version 3 only* - -.. option:: --region - - Filter by region ID - - *Identity version 3 only* - -.. option:: --long - - List additional fields in output - - *Identity version 2 only* - -.. option:: --endpoint - - List projects that have access to that endpoint using - endpoint filtering - - *Identity version 3 only* - -.. option:: --project - - List endpoints available for the project using - endpoint filtering - - *Identity version 3 only* - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - *Identity version 3 only* - -endpoint remove project ------------------------ - -Dissociate a project from an endpoint. - -.. program:: endpoint remove project -.. code:: bash - - openstack endpoint remove project - [--project-domain ] - - - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _endpoint_remove_project-endpoint: -.. describe:: - - Endpoint to dissociate with specified project (name or ID) - -.. _endpoint_remove_project-project: -.. describe:: - - Project to dissociate with specified endpoint (name or ID) - -endpoint set ------------- - -Set endpoint properties - -*Identity version 3 only* - -.. program:: endpoint set -.. code:: bash - - openstack endpoint set - [--region ] - [--interface ] - [--url ] - [--service ] - [--enable | --disable] - - -.. option:: --region - - New endpoint region ID - -.. option:: --interface - - New endpoint interface type (admin, public or internal) - -.. option:: --url - - New endpoint URL - -.. option:: --service - - New endpoint service (name or ID) - -.. option:: --enable - - Enable endpoint - -.. option:: --disable - - Disable endpoint - -.. _endpoint_set-endpoint: -.. describe:: - - Endpoint to modify (ID only) - -endpoint show -------------- - -Display endpoint details - -.. program:: endpoint show -.. code:: bash - - openstack endpoint show - - -.. _endpoint_show-endpoint: -.. describe:: - - Endpoint to display (endpoint ID, service ID, service name, service type) diff --git a/doc/source/cli/command-objects/endpoint_group.rst b/doc/source/cli/command-objects/endpoint_group.rst index ccfe5f6615..b0d988e17c 100644 --- a/doc/source/cli/command-objects/endpoint_group.rst +++ b/doc/source/cli/command-objects/endpoint_group.rst @@ -7,22 +7,4 @@ can be used to filter the endpoints that are available to a project. Applicable to Identity v3 .. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group add project - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group remove project - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group show + :command: endpoint group * diff --git a/doc/source/cli/command-objects/federation-domain-project.rst b/doc/source/cli/command-objects/federation-domain-project.rst new file mode 100644 index 0000000000..68db5705b0 --- /dev/null +++ b/doc/source/cli/command-objects/federation-domain-project.rst @@ -0,0 +1,11 @@ +========================= +federation domain/project +========================= + +Identity v3 + +.. autoprogram-cliff:: openstack.identity.v3 + :command: federation domain * + +.. autoprogram-cliff:: openstack.identity.v3 + :command: federation project * diff --git a/doc/source/cli/command-objects/federation-protocol.rst b/doc/source/cli/command-objects/federation-protocol.rst index 3a99fd72b9..81b3f9fcd3 100644 --- a/doc/source/cli/command-objects/federation-protocol.rst +++ b/doc/source/cli/command-objects/federation-protocol.rst @@ -7,16 +7,4 @@ extension. It is used by **identity providers** and **mappings**. Applicable to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: federation protocol create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation protocol delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation protocol list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation protocol set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation protocol show + :command: federation protocol * diff --git a/doc/source/cli/command-objects/group.rst b/doc/source/cli/command-objects/group.rst index ac938efdc2..a1071069db 100644 --- a/doc/source/cli/command-objects/group.rst +++ b/doc/source/cli/command-objects/group.rst @@ -4,246 +4,5 @@ group Identity v3 -group add user --------------- - -Add user to group - -.. program:: group add user -.. code:: bash - - openstack group add user - [--group-domain ] - [--user-domain ] - - [ ...] - -.. option:: --group-domain - - Domain the group belongs to (name or ID). This can be - used in case collisions between group names exist. - - .. versionadded:: 3 - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - - .. versionadded:: 3 - -.. describe:: - - Group to contain (name or ID) - -.. describe:: - - User(s) to add to (name or ID) - (repeat option to add multiple users) - -group contains user -------------------- - -Check user membership in group - -.. program:: group contains user -.. code:: bash - - openstack group contains user - [--group-domain ] - [--user-domain ] - - - -.. option:: --group-domain - - Domain the group belongs to (name or ID). This can be - used in case collisions between group names exist. - - .. versionadded:: 3 - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - - .. versionadded:: 3 - -.. describe:: - - Group to check (name or ID) - -.. describe:: - - User to check (name or ID) - -group create ------------- - -Create new group - -.. program:: group create -.. code:: bash - - openstack group create - [--domain ] - [--description ] - [--or-show] - - -.. option:: --domain - - Domain to contain new group (name or ID) - -.. option:: --description - - New group description - -.. option:: --or-show - - Return existing group - - If the group already exists, return the existing group data and do not fail. - -.. describe:: - - New group name - -group delete ------------- - -Delete group - -.. program:: group delete -.. code:: bash - - openstack group delete - [--domain ] - [ ...] - -.. option:: --domain - - Domain containing group(s) (name or ID) - -.. describe:: - - Group(s) to delete (name or ID) - -group list ----------- - -List groups - -.. program:: group list -.. code:: bash - - openstack group list - [--domain ] - [--user [--user-domain ]] - [--long] - -.. option:: --domain - - Filter group list by (name or ID) - -.. option:: --user - - Filter group list by (name or ID) - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - - .. versionadded:: 3 - -.. option:: --long - - List additional fields in output - -group remove user ------------------ - -Remove user from group - -.. program:: group remove user -.. code:: bash - - openstack group remove user - [--group-domain ] - [--user-domain ] - - [ ...] - -.. option:: --group-domain - - Domain the group belongs to (name or ID). This can be - used in case collisions between group names exist. - - .. versionadded:: 3 - -.. option:: --user-domain - - Domain the user belongs to (name or ID). This can be - used in case collisions between user names exist. - - .. versionadded:: 3 - -.. describe:: - - Group containing (name or ID) - -.. describe:: - - User(s) to remove from (name or ID) - (repeat option to remove multiple users) - -group set ---------- - -Set group properties - -.. program:: group set -.. code:: bash - - openstack group set - [--domain ] - [--name ] - [--description ] - - -.. option:: --domain - - Domain containing (name or ID) - -.. option:: --name - - New group name - -.. option:: --description - - New group description - -.. describe:: - - Group to modify (name or ID) - -group show ----------- - -Display group details - -.. program:: group show -.. code:: bash - - openstack group show - [--domain ] - - -.. option:: --domain - - Domain containing (name or ID) - -.. describe:: - - Group to display (name or ID) +.. autoprogram-cliff:: openstack.identity.v3 + :command: group * diff --git a/doc/source/cli/command-objects/identity-provider.rst b/doc/source/cli/command-objects/identity-provider.rst index 36c9264a64..ed85cb7ac8 100644 --- a/doc/source/cli/command-objects/identity-provider.rst +++ b/doc/source/cli/command-objects/identity-provider.rst @@ -7,16 +7,4 @@ extension. It is used by **federation protocols** and **mappings**. Applicable to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: identity provider create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: identity provider delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: identity provider list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: identity provider set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: identity provider show + :command: identity provider * diff --git a/doc/source/cli/command-objects/implied_role.rst b/doc/source/cli/command-objects/implied_role.rst index e43c9ea39e..09532c0b02 100644 --- a/doc/source/cli/command-objects/implied_role.rst +++ b/doc/source/cli/command-objects/implied_role.rst @@ -4,54 +4,5 @@ implied role Identity v3 - -implied role create -------------------- - -Creates an association between prior and implied roles - -.. program:: implied role create -.. code:: bash - - openstack implied role create - - --implied-role - -.. option:: - - Prior role (name or ID) implies another role - -.. option:: --implied-role - - (name or ID) implied by another role - - -implied role delete -------------------- - -Deletes an association between prior and implied roles - -.. program:: implied role delete -.. code:: bash - - openstack implied role delete - - --implied-role - -.. option:: - - Prior role (name or ID) implies another role - -.. option:: --implied-role - - (name or ID) implied by another role - -implied role list ------------------ - -List implied roles - -.. program:: implied role list -.. code:: bash - - openstack implied role list +.. autoprogram-cliff:: openstack.identity.v3 + :command: implied role * diff --git a/doc/source/cli/command-objects/limit.rst b/doc/source/cli/command-objects/limit.rst index 46da6c1b70..784d0cb423 100644 --- a/doc/source/cli/command-objects/limit.rst +++ b/doc/source/cli/command-objects/limit.rst @@ -6,128 +6,5 @@ Identity v3 Limits are used to specify project-specific limits thresholds of resources. -limit create ------------- - -Create a new limit - -.. program:: limit create -.. code:: bash - - openstack limit create - [--description ] - [--region ] - --project - --service - --resource-limit - - -.. option:: --description - - Useful description of the limit or its purpose - -.. option:: --region - - Region that the limit should be applied to - -.. describe:: --project - - The project that the limit applies to (required) - -.. describe:: --service - - The service that is responsible for the resource being limited (required) - -.. describe:: --resource-limit - - The limit to apply to the project (required) - -.. describe:: - - The name of the resource to limit (e.g. cores or volumes) - -limit delete ------------- - -Delete project-specific limit(s) - -.. program:: limit delete -.. code:: bash - - openstack limit delete - [ ...] - -.. describe:: - - Limit(s) to delete (ID) - -limit list ----------- - -List project-specific limits - -.. program:: limit list -.. code:: bash - - openstack limit list - [--service ] - [--resource-name ] - [--region ] - [--project ] - -.. option:: --service - - The service to filter the response by (name or ID) - -.. option:: --resource-name - - The name of the resource to filter the response by - -.. option:: --region - - The region name to filter the response by - -.. option:: --project - - List resource limits associated with project - -limit show ----------- - -Display details about a limit - -.. program:: limit show -.. code:: bash - - openstack limit show - - -.. describe:: - - Limit to display (ID) - -limit set ---------- - -Update a limit - -.. program:: limit show -.. code:: bash - - openstack limit set - [--description ] - [--resource-limit ] - - - -.. option:: --description - - Useful description of the limit or its purpose - -.. option:: --resource-limit - - The limit to apply to the project - -.. describe:: - - Limit to update (ID) +.. autoprogram-cliff:: openstack.identity.v3 + :command: limit * diff --git a/doc/source/cli/command-objects/mapping.rst b/doc/source/cli/command-objects/mapping.rst index 73dbbc8d8b..5653b52bb9 100644 --- a/doc/source/cli/command-objects/mapping.rst +++ b/doc/source/cli/command-objects/mapping.rst @@ -7,16 +7,4 @@ extension. It is used by **federation protocols** and **identity providers**. Applicable to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: mapping create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: mapping delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: mapping list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: mapping set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: mapping show + :command: mapping * \ No newline at end of file diff --git a/doc/source/cli/command-objects/policy.rst b/doc/source/cli/command-objects/policy.rst index 0fe104c56d..66bc2545a6 100644 --- a/doc/source/cli/command-objects/policy.rst +++ b/doc/source/cli/command-objects/policy.rst @@ -6,16 +6,4 @@ A **policy** is an arbitrarily serialized policy engine rule set to be consumed by a remote service. Applies to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: policy create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: policy delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: policy list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: policy set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: policy show + :command: policy * diff --git a/doc/source/cli/command-objects/project-v2.rst b/doc/source/cli/command-objects/project-v2.rst new file mode 100644 index 0000000000..502154d37e --- /dev/null +++ b/doc/source/cli/command-objects/project-v2.rst @@ -0,0 +1,6 @@ +===================== +project (Identity v2) +===================== + +.. autoprogram-cliff:: openstack.identity.v2 + :command: project * diff --git a/doc/source/cli/command-objects/project-v3.rst b/doc/source/cli/command-objects/project-v3.rst new file mode 100644 index 0000000000..9bee6ae8cf --- /dev/null +++ b/doc/source/cli/command-objects/project-v3.rst @@ -0,0 +1,6 @@ +===================== +project (Identity v3) +===================== + +.. autoprogram-cliff:: openstack.identity.v3 + :command: project * diff --git a/doc/source/cli/command-objects/project.rst b/doc/source/cli/command-objects/project.rst deleted file mode 100644 index afa785cbbe..0000000000 --- a/doc/source/cli/command-objects/project.rst +++ /dev/null @@ -1,297 +0,0 @@ -======= -project -======= - -Identity v2, v3 - -project create --------------- - -Create new project - -.. program:: project create -.. code:: bash - - openstack project create - [--domain ] - [--parent ] - [--description ] - [--immutable | --no-immutable] - [--enable | --disable] - [--property ] - [--or-show] - [--tag ] - - -.. option:: --domain - - Domain owning the project (name or ID) - - .. versionadded:: 3 - -.. option:: --parent - - Parent of the project (name or ID) - - .. versionadded:: 3 - -.. option:: --description - - Project description - -.. option:: --enable - - Enable project (default) - -.. option:: --disable - - Disable project - -.. option:: --immutable - - Make project immutable. An immutable project may not be deleted or - modified except to remove the immutable flag - -.. option:: --no-immutable - - Make project mutable (default) - -.. option:: --property - - Add a property to :ref:`\ ` - (repeat option to set multiple properties) - -.. option:: --or-show - - Return existing project - - If the project already exists return the existing project data and do not fail. - -.. option:: --tag - - Add a tag to the project - (repeat option to set multiple tags) - - .. versionadded:: 3 - -.. _project_create-name: -.. describe:: - - New project name - -project delete --------------- - -Delete project(s) - -.. program:: project delete -.. code:: bash - - openstack project delete - [--domain ] - [ ...] - -.. option:: --domain - - Domain owning :ref:`\ ` (name or ID) - - .. versionadded:: 3 - -.. _project_delete-project: -.. describe:: - - Project to delete (name or ID) - -project list ------------- - -List projects - -.. program:: project list -.. code:: bash - - openstack project list - [--domain ] - [--parent ] - [--user ] - [--my-projects] - [--long] - [--sort [:,:,..]] - [--tags [,,...]] [--tags-any [,,...]] - [--not-tags [,,...]] [--not-tags-any [,,...]] - -.. option:: --domain - - Filter projects by :option:`\ <--domain>` (name or ID) - - .. versionadded:: 3 - -.. option:: --parent - - Filter projects whose parent is :option:`\ <--parent>` (name or ID) - - .. versionadded:: 3 - -.. option:: --user - - Filter projects by :option:`\ <--user>` (name or ID) - - .. versionadded:: 3 - -.. option:: --my-projects - - List projects for the authenticated user. Supersedes other filters. - - .. versionadded:: 3 - -.. option:: --long - - List additional fields in output - -.. option:: --sort [:,:,..] - - Sort output by selected keys and directions (asc or desc) (default: asc), - multiple keys and directions can be specified --sort - [:,:,..] - -.. option:: --tags [,,...] - - List projects which have all given tag(s) - - .. versionadded:: 3 - -.. option:: --tags-any [,,...] - - List projects which have any given tag(s) - - .. versionadded:: 3 - -.. option:: --not-tags [,,...] - - Exclude projects which have all given tag(s) - - .. versionadded:: 3 - -.. option:: --not-tags-any [,,...] - - Exclude projects which have any given tag(s) - - .. versionadded:: 3 - -project set ------------ - -Set project properties - -.. program:: project set -.. code:: bash - - openstack project set - [--name ] - [--domain ] - [--description ] - [--immutable | --no-immutable] - [--enable | --disable] - [--property ] - [--tag | --clear-tags | --remove-tags ] - - -.. option:: --name - - Set project name - -.. option:: --domain - - Domain owning :ref:`\ ` (name or ID) - - .. versionadded:: 3 - -.. option:: --description - - Set project description - -.. option:: --immutable - - Make project immutable. An immutable project may not be deleted or - modified except to remove the immutable flag - -.. option:: --no-immutable - - Make project mutable (default) - -.. option:: --enable - - Enable project (default) - -.. option:: --disable - - Disable project - -.. option:: --property - - Set a property on :ref:`\ ` - (repeat option to set multiple properties) - - *Identity version 2 only* - -.. _project_set-project: -.. describe:: - - Project to modify (name or ID) - -project show ------------- - -Display project details - -.. program:: project show -.. code:: bash - - openstack project show - [--domain ] - - -.. option:: --domain - - Domain owning :ref:`\ ` (name or ID) - - .. versionadded:: 3 - -.. option:: --parents - - Show the project\'s parents as a list - - .. versionadded:: 3 - -.. option:: --children - - Show project\'s subtree (children) as a list - - .. versionadded:: 3 - -.. _project_show-project: -.. describe:: - - Project to display (name or ID) - -project unset -------------- - -Unset project properties - -*Identity version 2 only* - -.. program:: project unset -.. code:: bash - - openstack project unset - --property [--property ...] - - -.. option:: --property - - Property key to remove from project (repeat option to remove multiple properties) - -.. describe:: - - Project to modify (name or ID) diff --git a/doc/source/cli/command-objects/region.rst b/doc/source/cli/command-objects/region.rst index 58cc341f9a..86e7ca8ae0 100644 --- a/doc/source/cli/command-objects/region.rst +++ b/doc/source/cli/command-objects/region.rst @@ -7,16 +7,4 @@ zero or more sub-regions with a region to create a tree-like structured hierarchy. Applies to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: region create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: region delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: region list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: region set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: region show + :command: region * diff --git a/doc/source/cli/command-objects/registered-limit.rst b/doc/source/cli/command-objects/registered-limit.rst index 586fd1ffcc..98a5efba9e 100644 --- a/doc/source/cli/command-objects/registered-limit.rst +++ b/doc/source/cli/command-objects/registered-limit.rst @@ -7,134 +7,5 @@ Identity v3 Registered limits are used to define default limits for resources within a deployment. -registered limit create ------------------------ - -Create a new registered limit - -.. program:: registered limit create -.. code:: bash - - openstack registered limit create - [--description ] - [--region ] - --service - --default-limit - - -.. option:: --description - - Useful description of the registered limit or its purpose - -.. option:: --region - - Region that the limit should be applied to - -.. describe:: --service - - The service that is responsible for the resource being limited (required) - -.. describe:: --default-limit - - The default limit for projects to assume unless explicitly overridden - (required) - -.. describe:: - - The name of the resource to limit (e.g. cores or volumes) - -registered limit delete ------------------------ - -Delete registered limit(s) - -.. program:: registered limit delete -.. code:: bash - - openstack registered limit delete - [ ...] - - -.. describe:: - - Registered limit(s) to delete (ID) - -registered limit list ---------------------- - -List registered limits - -.. program:: registered limit list -.. code:: bash - - openstack registered limit list - [--service ] - [--resource-name ] - [--region ] - -.. option:: --service - - The service to filter the response by (name or ID) - -.. option:: --resource-name - - The name of the resource to filter the response by - -.. option:: --region - - The region name to filter the response by - -registered limit show ---------------------- - -Display details about a registered limit - -.. program:: registered limit show -.. code:: bash - - openstack registered limit show - - -.. describe:: - - Registered limit to display (ID) - -registered limit set --------------------- - -Update a registered limit - -.. program:: registered limit set -.. code:: bash - - openstack registered limit set - [--service ] - [--resource-name ] - [--default-limit ] - [--description ] - [--region ] - - -.. option:: --service - - The service of the registered limit to update (ID or name) - -.. option:: --resource-name - - The name of the resource for the limit - -.. option:: --default-limit - - The default limit for projects to assume for a given resource - -.. option:: --description - - A useful description of the limit or its purpose - -.. option:: --region - - The region the limit should apply to - -.. describe:: - - Registered limit to display (ID) +.. autoprogram-cliff:: openstack.identity.v3 + :command: registered limit * diff --git a/doc/source/cli/command-objects/request-token.rst b/doc/source/cli/command-objects/request-token.rst index e10f6640ef..ea333c4aba 100644 --- a/doc/source/cli/command-objects/request-token.rst +++ b/doc/source/cli/command-objects/request-token.rst @@ -7,7 +7,4 @@ is used by the **consumer** to request **access tokens**. Applicable to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: request token authorize - -.. autoprogram-cliff:: openstack.identity.v3 - :command: request token create + :command: request token * diff --git a/doc/source/cli/command-objects/role-v2.rst b/doc/source/cli/command-objects/role-v2.rst new file mode 100644 index 0000000000..dc5bac6c65 --- /dev/null +++ b/doc/source/cli/command-objects/role-v2.rst @@ -0,0 +1,6 @@ +================== +role (Identity v2) +================== + +.. autoprogram-cliff:: openstack.identity.v2 + :command: role * diff --git a/doc/source/cli/command-objects/role-v3.rst b/doc/source/cli/command-objects/role-v3.rst new file mode 100644 index 0000000000..e36bef773f --- /dev/null +++ b/doc/source/cli/command-objects/role-v3.rst @@ -0,0 +1,6 @@ +================== +role (Identity v3) +================== + +.. autoprogram-cliff:: openstack.identity.v3 + :command: role * diff --git a/doc/source/cli/command-objects/role.rst b/doc/source/cli/command-objects/role.rst deleted file mode 100644 index f9fd28eb75..0000000000 --- a/doc/source/cli/command-objects/role.rst +++ /dev/null @@ -1,312 +0,0 @@ -==== -role -==== - -Identity v2, v3 - -role add --------- - -Add role assignment to a user or group in a project or domain - -.. program:: role add -.. code:: bash - - openstack role add - --system | --domain | --project [--project-domain ] - --user [--user-domain ] | --group [--group-domain ] - --role-domain - --inherited - - -.. option:: --system - - Include - - System or service to grant authorization to. Currently only ``all`` is - supported which encompasses the entire deployment system. - - .. versionadded:: 3 - -.. option:: --domain - - Include (name or ID) - - .. versionadded:: 3 - -.. option:: --project - - Include (name or ID) - -.. option:: --user - - Include (name or ID) - -.. option:: --group - - Include (name or ID) - - .. versionadded:: 3 - -.. option:: --user-domain - - Domain the user belongs to (name or ID). - This can be used in case collisions between user names exist. - - .. versionadded:: 3 - -.. option:: --group-domain - - Domain the group belongs to (name or ID). - This can be used in case collisions between group names exist. - - .. versionadded:: 3 - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - .. versionadded:: 3 - -.. option:: --inherited - - Specifies if the role grant is inheritable to the sub projects. - - .. versionadded:: 3 - -.. option:: --role-domain - - Domain the role belongs to (name or ID). - This must be specified when the name of a domain specific role is used. - - .. versionadded:: 3 - -.. describe:: - - Role to add to : (name or ID) - -role create ------------ - -Create new role - -.. program:: role create -.. code:: bash - - openstack role create - [--or-show] - [--domain ] - [--immutable | --no-immutable] - - -.. option:: --domain - - Domain the role belongs to (name or ID). - - .. versionadded:: 3 - -.. option:: --or-show - - Return existing role - - If the role already exists return the existing role data and do not fail. - -.. describe:: - - New role name - -.. option:: --description - - Add description about the role - -.. option:: --immutable - - Make role immutable. An immutable role may not be deleted or modified - except to remove the immutable flag - -.. option:: --no-immutable - - Make role mutable (default) - -role delete ------------ - -Delete role(s) - -.. program:: role delete -.. code:: bash - - openstack role delete - [ ...] - [--domain ] - -.. describe:: - - Role to delete (name or ID) - -.. option:: --domain - - Domain the role belongs to (name or ID). - - .. versionadded:: 3 - -role list ---------- - -List roles - -.. program:: role list -.. code:: bash - - openstack role list - [--domain ] - -.. option:: --domain - - Filter roles by (name or ID) - - .. versionadded:: 3 - -role remove ------------ - -Remove role assignment from domain/project : user/group - -.. program:: role remove -.. code:: bash - - openstack role remove - --system | --domain | --project [--project-domain ] - --user [--user-domain ] | --group [--group-domain ] - --role-domain - --inherited - - -.. option:: --system - - Include - - System or service to remove authorization from. Currently only ``all`` is - supported which encompasses the entire deployment system. - - .. versionadded:: 3 - -.. option:: --domain - - Include (name or ID) - - .. versionadded:: 3 - -.. option:: --project - - Include (name or ID) - -.. option:: --user - - Include (name or ID) - -.. option:: --group - - Include (name or ID) - - .. versionadded:: 3 - -.. option:: --user-domain - - Domain the user belongs to (name or ID). - This can be used in case collisions between user names exist. - - .. versionadded:: 3 - -.. option:: --group-domain - - Domain the group belongs to (name or ID). - This can be used in case collisions between group names exist. - - .. versionadded:: 3 - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - .. versionadded:: 3 - -.. option:: --inherited - - Specifies if the role grant is inheritable to the sub projects. - - .. versionadded:: 3 - -.. option:: --role-domain - - Domain the role belongs to (name or ID). - This must be specified when the name of a domain specific role is used. - - .. versionadded:: 3 - -.. describe:: - - Role to remove (name or ID) - -role set --------- - -Set role properties - -.. versionadded:: 3 - -.. program:: role set -.. code:: bash - - openstack role set - [--name ] - [--domain ] - [--immutable | --no-immutable] - - -.. option:: --name - - Set role name - -.. option:: --domain - - Domain the role belongs to (name or ID). - - .. versionadded:: 3 - -.. describe:: - - Role to modify (name or ID) - -.. option:: --immutable - - Make role immutable. An immutable role may not be deleted or modified - except to remove the immutable flag - -.. option:: --no-immutable - - Make role mutable (default) - -role show ---------- - -Display role details - -.. program:: role show -.. code:: bash - - openstack role show - [--domain ] - - -.. option:: --domain - - Domain the role belongs to (name or ID). - - .. versionadded:: 3 - -.. describe:: - - Role to display (name or ID) diff --git a/doc/source/cli/command-objects/service-provider.rst b/doc/source/cli/command-objects/service-provider.rst index b7a282cba5..47a503278c 100644 --- a/doc/source/cli/command-objects/service-provider.rst +++ b/doc/source/cli/command-objects/service-provider.rst @@ -7,16 +7,4 @@ extension. It is used by to register another OpenStack Identity service. Applicable to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: service provider create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service provider delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service provider list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service provider set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service provider show + :command: service provider * diff --git a/doc/source/cli/command-objects/service-v2.rst b/doc/source/cli/command-objects/service-v2.rst new file mode 100644 index 0000000000..6b22b7eeb6 --- /dev/null +++ b/doc/source/cli/command-objects/service-v2.rst @@ -0,0 +1,6 @@ +===================== +service (Identity v2) +===================== + +.. autoprogram-cliff:: openstack.identity.v2 + :command: service * diff --git a/doc/source/cli/command-objects/service-v3.rst b/doc/source/cli/command-objects/service-v3.rst new file mode 100644 index 0000000000..d4b702067b --- /dev/null +++ b/doc/source/cli/command-objects/service-v3.rst @@ -0,0 +1,18 @@ +===================== +service (Identity v3) +===================== + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service create + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service show + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service set diff --git a/doc/source/cli/command-objects/service.rst b/doc/source/cli/command-objects/service.rst deleted file mode 100644 index a69c69504b..0000000000 --- a/doc/source/cli/command-objects/service.rst +++ /dev/null @@ -1,143 +0,0 @@ -======= -service -======= - -Identity v2, v3 - -service create --------------- - -Create new service - -.. program:: service create -.. code-block:: bash - - openstack service create - [--name ] - [--description ] - [--enable | --disable] - - -.. option:: --name - - New service name - -.. option:: --description - - New service description - -.. option:: --enable - - Enable service (default) - - *Identity version 3 only* - -.. option:: --disable - - Disable service - - *Identity version 3 only* - -.. _service_create-type: -.. describe:: - - New service type (compute, image, identity, volume, etc) - -service delete --------------- - -Delete service(s) - -.. program:: service delete -.. code-block:: bash - - openstack service delete - [ ...] - -.. _service_delete-service: -.. describe:: - - Service(s) to delete (type, name or ID) - -service list ------------- - -List services - -.. program:: service list -.. code-block:: bash - - openstack service list - [--long] - -.. option:: --long - - List additional fields in output - -Returns service fields ID, Name and Type. :option:`--long` adds Description -and Enabled (*Identity version 3 only*) to the output. - -service set ------------ - -Set service properties - -* Identity version 3 only* - -.. program:: service set -.. code-block:: bash - - openstack service set - [--type ] - [--name ] - [--description ] - [--enable | --disable] - - -.. option:: --type - - New service type (compute, image, identity, volume, etc) - -.. option:: --name - - New service name - -.. option:: --description - - New service description - -.. option:: --enable - - Enable service - -.. option:: --disable - - Disable service - -.. _service_set-service: -.. describe:: - - Service to modify (type, name or ID) - -service show ------------- - -Display service details - -.. program:: service show -.. code-block:: bash - - openstack service show - [--catalog] - - -.. option:: --catalog - - Show service catalog information - - *Identity version 2 only* - -.. _service_show-service: -.. describe:: - - Service to display (type, name or ID) diff --git a/doc/source/cli/command-objects/token-v2.rst b/doc/source/cli/command-objects/token-v2.rst new file mode 100644 index 0000000000..c66302bfb2 --- /dev/null +++ b/doc/source/cli/command-objects/token-v2.rst @@ -0,0 +1,7 @@ +=================== +token (Identity v2) +=================== + + +.. autoprogram-cliff:: openstack.identity.v2 + :command: token * diff --git a/doc/source/cli/command-objects/token-v3.rst b/doc/source/cli/command-objects/token-v3.rst new file mode 100644 index 0000000000..6b2d87a61d --- /dev/null +++ b/doc/source/cli/command-objects/token-v3.rst @@ -0,0 +1,7 @@ +=================== +token (Identity v3) +=================== + + +.. autoprogram-cliff:: openstack.identity.v3 + :command: token * diff --git a/doc/source/cli/command-objects/token.rst b/doc/source/cli/command-objects/token.rst deleted file mode 100644 index b4b14cd9fd..0000000000 --- a/doc/source/cli/command-objects/token.rst +++ /dev/null @@ -1,30 +0,0 @@ -===== -token -===== - -Identity v2, v3 - -token issue ------------ - -Issue new token - -.. program:: token issue -.. code:: bash - - openstack token issue - -token revoke ------------- - -Revoke existing token - -.. program:: token revoke -.. code:: bash - - openstack token revoke - - -.. describe:: - - Token to be deleted diff --git a/doc/source/cli/command-objects/trust.rst b/doc/source/cli/command-objects/trust.rst index febef1c5b6..738c640c48 100644 --- a/doc/source/cli/command-objects/trust.rst +++ b/doc/source/cli/command-objects/trust.rst @@ -6,13 +6,4 @@ A **trust** provide project-specific role delegation between users, with optional impersonation. Requires the OS-TRUST extension. Applies to Identity v3. .. autoprogram-cliff:: openstack.identity.v3 - :command: trust create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: trust delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: trust list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: trust show + :command: trust * diff --git a/doc/source/cli/command-objects/user-v2.rst b/doc/source/cli/command-objects/user-v2.rst new file mode 100644 index 0000000000..966bd37620 --- /dev/null +++ b/doc/source/cli/command-objects/user-v2.rst @@ -0,0 +1,7 @@ +================== +user (Identity v2) +================== + + +.. autoprogram-cliff:: openstack.identity.v2 + :command: user * diff --git a/doc/source/cli/command-objects/user-v3.rst b/doc/source/cli/command-objects/user-v3.rst new file mode 100644 index 0000000000..c11ff9a645 --- /dev/null +++ b/doc/source/cli/command-objects/user-v3.rst @@ -0,0 +1,7 @@ +================== +user (Identity v3) +================== + + +.. autoprogram-cliff:: openstack.identity.v3 + :command: user * diff --git a/doc/source/cli/command-objects/user.rst b/doc/source/cli/command-objects/user.rst deleted file mode 100644 index d0fc3f8734..0000000000 --- a/doc/source/cli/command-objects/user.rst +++ /dev/null @@ -1,349 +0,0 @@ -==== -user -==== - -Identity v2, v3 - -user create ------------ - -Create new user - -.. program:: user create -.. code:: bash - - openstack user create - [--domain ] - [--project [--project-domain ]] - [--password ] - [--password-prompt] - [--email ] - [--description ] - [--multi-factor-auth-rule ] - [--ignore-lockout-failure-attempts| --no-ignore-lockout-failure-attempts] - [--ignore-password-expiry| --no-ignore-password-expiry] - [--ignore-change-password-upon-first-use| --no-ignore-change-password-upon-first-use] - [--enable-lock-password| --disable-lock-password] - [--enable-multi-factor-auth| --disable-multi-factor-auth] - [--enable | --disable] - [--or-show] - - -.. option:: --domain - - Default domain (name or ID) - - .. versionadded:: 3 - -.. option:: --project - - Default project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --password - - Set user password - -.. option:: --password-prompt - - Prompt interactively for password - -.. option:: --email - - Set user email address - -.. option:: --description - - User description - - .. versionadded:: 3 - -.. option:: --ignore-lockout-failure-attempts - - Opt into ignoring the number of times a user has authenticated and - locking out the user as a result - -.. option:: --no-ignore-lockout-failure-attempts - - Opt out of ignoring the number of times a user has authenticated - and locking out the user as a result - -.. option:: --ignore-change-password-upon-first-use - - Control if a user should be forced to change their password immediately - after they log into keystone for the first time. Opt into ignoring - the user to change their password during first time login in keystone. - -.. option:: --no-ignore-change-password-upon-first-use - - Control if a user should be forced to change their password immediately - after they log into keystone for the first time. Opt out of ignoring - the user to change their password during first time login in keystone. - -.. option:: --ignore-password-expiry - - Opt into allowing user to continue using passwords that may be - expired - -.. option:: --no-ignore-password-expiry - - Opt out of allowing user to continue using passwords that may be - expired - -.. option:: --enable-lock-password - - Disables the ability for a user to change its password through - self-service APIs - -.. option:: --disable-lock-password - - Enables the ability for a user to change its password through - self-service APIs - -.. option:: --enable-multi-factor-auth - - Enables the MFA (Multi Factor Auth) - -.. option:: --disable-multi-factor-auth - - Disables the MFA (Multi Factor Auth) - -.. option:: --multi-factor-auth-rule - - Set multi-factor auth rules. For example, to set a rule requiring the - "password" and "totp" auth methods to be provided, - use: "--multi-factor-auth-rule password,totp". - May be provided multiple times to set different rule combinations. - -.. option:: --enable - - Enable user (default) - -.. option:: --disable - - Disable user - -.. option:: --or-show - - Return existing user - - If the username already exist return the existing user data and do not fail. - -.. describe:: - - New user name - -user delete ------------ - -Delete user(s) - -.. program:: user delete -.. code:: bash - - openstack user delete - [--domain ] - [ ...] - -.. option:: --domain - - Domain owning :ref:`\ ` (name or ID) - - .. versionadded:: 3 - -.. _user_delete-user: -.. describe:: - - User(s) to delete (name or ID) - -user list ---------- - -List users - -.. program:: user list -.. code:: bash - - openstack user list - [--project ] - [--domain ] - [--group | --project ] - [--long] - -.. option:: --project - - Filter users by `` (name or ID) - -.. option:: --domain - - Filter users by `` (name or ID) - - *Identity version 3 only* - -.. option:: --group - - Filter users by `` membership (name or ID) - - *Identity version 3 only* - -.. option:: --long - - List additional fields in output - -user set --------- - -Set user properties - -.. program:: user set -.. code:: bash - - openstack user set - [--name ] - [--project [--project-domain ]] - [--password ] - [--password-prompt] - [--email ] - [--description ] - [--multi-factor-auth-rule ] - [--ignore-lockout-failure-attempts| --no-ignore-lockout-failure-attempts] - [--ignore-password-expiry| --no-ignore-password-expiry] - [--ignore-change-password-upon-first-use| --no-ignore-change-password-upon-first-use] - [--enable-lock-password| --disable-lock-password] - [--enable-multi-factor-auth| --disable-multi-factor-auth] - [--enable|--disable] - - -.. option:: --name - - Set user name - -.. option:: --domain - - Domain the user belongs to (name or ID). - This can be used in case collisions between user names exist. - - .. versionadded:: 3 - -.. option:: --project - - Set default project (name or ID) - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. option:: --password - - Set user password - -.. option:: --password-prompt - - Prompt interactively for password - -.. option:: --email - - Set user email address - -.. option:: --description - - Set user description - - .. versionadded:: 3 - -.. option:: --ignore-lockout-failure-attempts - - Opt into ignoring the number of times a user has authenticated and - locking out the user as a result - -.. option:: --no-ignore-lockout-failure-attempts - - Opt out of ignoring the number of times a user has authenticated - and locking out the user as a result - -.. option:: --ignore-change-password-upon-first-use - - Control if a user should be forced to change their password immediately - after they log into keystone for the first time. Opt into ignoring - the user to change their password during first time login in keystone. - -.. option:: --no-ignore-change-password-upon-first-use - - Control if a user should be forced to change their password immediately - after they log into keystone for the first time. Opt out of ignoring - the user to change their password during first time login in keystone. - -.. option:: --ignore-password-expiry - - Opt into allowing user to continue using passwords that may be - expired - -.. option:: --no-ignore-password-expiry - - Opt out of allowing user to continue using passwords that may be - expired - -.. option:: --enable-lock-password - - Disables the ability for a user to change its password through - self-service APIs - -.. option:: --disable-lock-password - - Enables the ability for a user to change its password through - self-service APIs - -.. option:: --enable-multi-factor-auth - - Enables the MFA (Multi Factor Auth) - -.. option:: --disable-multi-factor-auth - - Disables the MFA (Multi Factor Auth) - -.. option:: --multi-factor-auth-rule - - Set multi-factor auth rules. For example, to set a rule requiring the - "password" and "totp" auth methods to be provided, - use: "--multi-factor-auth-rule password,totp". - May be provided multiple times to set different rule combinations. - -.. option:: --enable - - Enable user (default) - -.. option:: --disable - - Disable user - -.. describe:: - - User to modify (name or ID) - -user show ---------- - -Display user details - -.. program:: user show -.. code:: bash - - openstack user show - [--domain ] - - -.. option:: --domain - - Domain owning :ref:`\ ` (name or ID) - - .. versionadded:: 3 - -.. _user_show-user: -.. describe:: - - User to display (name or ID) diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 17e50a07d2..9cb3b33a73 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -13,3 +13,12 @@ interactive decoder backwards-incompatible + +.. NOTE(efried): Everything must be in a toctree but we don't want these to + show up to the reader. + +.. toctree:: + :glob: + :hidden: + + _hidden/* diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt index ef3a48decb..d18660a0b2 100644 --- a/doc/test/redirect-tests.txt +++ b/doc/test/redirect-tests.txt @@ -16,3 +16,10 @@ /python-openstackclient/latest/humaninterfaceguide.html 301 /python-openstackclient/latest/contributor/humaninterfaceguide.html /python-openstackclient/latest/plugins.html 301 /python-openstackclient/latest/contributor/plugins.html /python-openstackclient/latest/cli/plugin-commands.html 301 /python-openstackclient/latest/cli/plugin-commands/index.html +/python-openstackclient/latest/cli/command-objects/ec2-credentials.html 301 /python-openstackclient/latest/cli/_hidden/ec2-credentials.html +/python-openstackclient/latest/cli/command-objects/endpoint.html 301 /python-openstackclient/latest/cli/_hidden/endpoint.html +/python-openstackclient/latest/cli/command-objects/project.html 301 /python-openstackclient/latest/cli/_hidden/project.html +/python-openstackclient/latest/cli/command-objects/role.html 301 /python-openstackclient/latest/cli/_hidden/role.html +/python-openstackclient/latest/cli/command-objects/service.html 301 /python-openstackclient/latest/cli/_hidden/service.html +/python-openstackclient/latest/cli/command-objects/token.html 301 /python-openstackclient/latest/cli/_hidden/token.html +/python-openstackclient/latest/cli/command-objects/user.html 301 /python-openstackclient/latest/cli/_hidden/user.html diff --git a/setup.cfg b/setup.cfg index 1ba0691cc0..273529b1ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -248,6 +248,15 @@ openstack.identity.v3 = endpoint_group_set = openstackclient.identity.v3.endpoint_group:SetEndpointGroup endpoint_group_show = openstackclient.identity.v3.endpoint_group:ShowEndpointGroup + federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains + federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects + + federation_protocol_create = openstackclient.identity.v3.federation_protocol:CreateProtocol + federation_protocol_delete = openstackclient.identity.v3.federation_protocol:DeleteProtocol + federation_protocol_list = openstackclient.identity.v3.federation_protocol:ListProtocols + federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol + federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol + group_add_user = openstackclient.identity.v3.group:AddUserToGroup group_contains_user = openstackclient.identity.v3.group:CheckUserInGroup group_create = openstackclient.identity.v3.group:CreateGroup @@ -291,15 +300,6 @@ openstack.identity.v3 = project_set = openstackclient.identity.v3.project:SetProject project_show = openstackclient.identity.v3.project:ShowProject - federation_protocol_create = openstackclient.identity.v3.federation_protocol:CreateProtocol - federation_protocol_delete = openstackclient.identity.v3.federation_protocol:DeleteProtocol - federation_protocol_list = openstackclient.identity.v3.federation_protocol:ListProtocols - federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol - federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol - - federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains - federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects - region_create = openstackclient.identity.v3.region:CreateRegion region_delete = openstackclient.identity.v3.region:DeleteRegion region_list = openstackclient.identity.v3.region:ListRegion From da3c3bde24a0263a9ff3769a5fa67e2aff8f3784 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Tue, 5 Nov 2019 08:58:58 -0600 Subject: [PATCH 0235/1120] image: autogenerate docs $namespace = openstack.image.v{1|2} The subcommand documents for $namespace were hardcoded and thus prone to drift over time. This commit removes the hardcoded content and uses the autoprogram-cliff directive to generate them automatically from the subcommand configuration classes. Some reorganization happened here. The `image` subcommand name is shared by image v1 and v2. Previously the hardcoded document had them combined and interleaved. Attempting to preserve this with autoprogram-cliff would have required significant additional infrastructure. However, since most readers care completely about one and not at all about the other, we instead split the v1 and v2 versions of these commands into separate pages. In case links to the old pages exist in the wild, they are preserved, but moved (with redirects) to a hidden directory, and populated simply with links to the new version-specific generated documents. Change-Id: I24dc6dc10671c7f1267c27002542f61f8a3c18ae --- doc/source/_extra/.htaccess | 6 +- doc/source/cli/_hidden/image.rst | 13 + doc/source/cli/command-objects/image-v1.rst | 6 + doc/source/cli/command-objects/image-v2.rst | 6 + doc/source/cli/command-objects/image.rst | 650 -------------------- doc/test/redirect-tests.txt | 1 + 6 files changed, 30 insertions(+), 652 deletions(-) create mode 100644 doc/source/cli/_hidden/image.rst create mode 100644 doc/source/cli/command-objects/image-v1.rst create mode 100644 doc/source/cli/command-objects/image-v2.rst delete mode 100644 doc/source/cli/command-objects/image.rst diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess index 1ac063cc35..8c0a005425 100644 --- a/doc/source/_extra/.htaccess +++ b/doc/source/_extra/.htaccess @@ -9,6 +9,8 @@ redirectmatch 301 ^/python-openstackclient/([^/]+)/specs/([^/.]+).html$ /python- redirectmatch 301 ^/python-openstackclient/([^/]+)/(command-(beta|errors|logs|options|wrappers)|developing|humaninterfaceguide|plugins).html$ /python-openstackclient/$1/contributor/$2.html redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/plugin-commands.html$ /python-openstackclient/$1/cli/plugin-commands/index.html -# Identity pages were split into -v2 and -v3 for common subcommand names. +# For common subcommand names: +# - identity pages were split into -v2 and -v3 +# - image pages were split into -v1 and -v2 # The unversioned page is hidden but contains links to the versioned pages so links in the wild redirect somewhere sane. -redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(ec2-credentials|endpoint|project|role|service|token|user).html$ /python-openstackclient/$1/cli/_hidden/$2.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(ec2-credentials|endpoint|image|project|role|service|token|user).html$ /python-openstackclient/$1/cli/_hidden/$2.html diff --git a/doc/source/cli/_hidden/image.rst b/doc/source/cli/_hidden/image.rst new file mode 100644 index 0000000000..85ffde6f39 --- /dev/null +++ b/doc/source/cli/_hidden/image.rst @@ -0,0 +1,13 @@ +===== +image +===== + +.. NOTE(efried): This page is hidden from the main TOC; it's here so links in + the wild redirect somewhere sane, because previously identity v2 and v3 were + combined in a single page. + +.. toctree:: + :maxdepth: 2 + + ../command-objects/image-v1 + ../command-objects/image-v2 diff --git a/doc/source/cli/command-objects/image-v1.rst b/doc/source/cli/command-objects/image-v1.rst new file mode 100644 index 0000000000..4f7edc4322 --- /dev/null +++ b/doc/source/cli/command-objects/image-v1.rst @@ -0,0 +1,6 @@ +======== +image v1 +======== + +.. autoprogram-cliff:: openstack.image.v1 + :command: image * diff --git a/doc/source/cli/command-objects/image-v2.rst b/doc/source/cli/command-objects/image-v2.rst new file mode 100644 index 0000000000..473b26d07b --- /dev/null +++ b/doc/source/cli/command-objects/image-v2.rst @@ -0,0 +1,6 @@ +======== +image v2 +======== + +.. autoprogram-cliff:: openstack.image.v2 + :command: image * diff --git a/doc/source/cli/command-objects/image.rst b/doc/source/cli/command-objects/image.rst deleted file mode 100644 index 459a077029..0000000000 --- a/doc/source/cli/command-objects/image.rst +++ /dev/null @@ -1,650 +0,0 @@ -===== -image -===== - -Image v1, v2 - -image add project ------------------ - -*Only supported for Image v2* - -Associate project with image - -.. program:: image add project -.. code:: bash - - openstack image add project - [--project-domain ] - - - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _image_add_project-image: -.. describe:: - - Image to share (name or ID). - -.. _image_add_project-project: -.. describe:: - - Project to associate with image (ID) - -image create ------------- - -*Image v1, v2* - -Create/upload an image - -.. program:: image create -.. code:: bash - - openstack image create - [--id ] - [--store ] - [--container-format ] - [--disk-format ] - [--size ] - [--min-disk ] - [--min-ram ] - [--location ] - [--copy-from ] - [--file | --volume ] - [--force] - [--checksum ] - [--protected | --unprotected] - [--public | --private | --community | --shared] - [--property [...] ] - [--tag [...] ] - [--project ] - [--project-domain ] - - -.. option:: --id - - Image ID to reserve - -.. option:: --store - - Upload image to this store - - *Image version 1 only.* - -.. option:: --container-format - - Image container format. The supported options are: ami, ari, aki, - bare, docker, ova, ovf. The default format is: bare - -.. option:: --disk-format - - Image disk format. The supported options are: ami, ari, aki, vhd, vmdk, - raw, qcow2, vhdx, vdi, iso, and ploop. The default format is: raw - -.. option:: --size - - Image size, in bytes (only used with :option:`--location` and :option:`--copy-from`) - - *Image version 1 only.* - -.. option:: --min-disk - - Minimum disk size needed to boot image, in gigabytes - -.. option:: --min-ram - - Minimum RAM size needed to boot image, in megabytes - -.. option:: --location - - Download image from an existing URL - - *Image version 1 only.* - -.. option:: --copy-from - - Copy image from the data store (similar to :option:`--location`) - - *Image version 1 only.* - -.. option:: --file - - Upload image from local file - -.. option:: --volume - - Create image from a volume - -.. option:: --force - - Force image creation if volume is in use (only meaningful with :option:`--volume`) - -.. option:: --checksum - - Image hash used for verification - - *Image version 1 only.* - -.. option:: --protected - - Prevent image from being deleted - -.. option:: --unprotected - - Allow image to be deleted (default) - -.. option:: --public - - Image is accessible to the public - -.. option:: --private - - Image is inaccessible to the public (default) - -.. option:: --community - - Image is accessible to the community - -.. option:: --shared - - Image can be shared - -.. option:: --property - - Set a property on this image (repeat option to set multiple properties) - -.. option:: --tag - - Set a tag on this image (repeat option to set multiple tags) - - .. versionadded:: 2 - -.. option:: --project - - Set an alternate project on this image (name or ID). - Previously known as `--owner`. - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - .. versionadded:: 2 - -.. _image_create-image-name: -.. describe:: - - New image name - -image delete ------------- - -Delete image(s) - -.. program:: image delete -.. code:: bash - - openstack image delete - - -.. _image_delete-image: -.. describe:: - - Image(s) to delete (name or ID) - -image list ----------- - -List available images - -.. program:: image list -.. code:: bash - - openstack image list - [--public | --private | --community | --shared] - [--property ] - [--name ] - [--status ] - [--member-status ] - [--tag ] - [--long] - [--sort [:]] - [--limit ] - [--marker ] - -.. option:: --public - - List only public images - -.. option:: --private - - List only private images - -.. option:: --community - - List only community images - - *Image version 2 only.* - -.. option:: --shared - - List only shared images - - *Image version 2 only.* - -.. option:: --property - - Filter output based on property - -.. option:: --name - - Filter images based on name - - *Image version 2 only.* - -.. option:: --status - - Filter images based on status - - *Image version 2 only* - -.. option:: --member-status - - Filter images based on member status - - *Image version 2 only* - -.. option:: --tag - - Filter images based on tag - - *Image version 2 only* - -.. option:: --long - - List additional fields in output - -.. option:: --sort [:] - - Sort output by selected keys and directions(asc or desc) (default: name:asc), - multiple keys and directions can be specified separated by comma - -.. option:: --limit - - Maximum number of images to display. - - *Image version 2 only* - -.. option:: --marker - - The last image of the previous page. Display list of images - after marker. Display all images if not specified. (name or ID) - - *Image version 2 only* - -image member list ------------------ - -List projects associated with image - -.. program:: image member list -.. code:: bash - - openstack image member list - - -.. _image_member_list-image: -.. describe:: - - Image(s) to view members for (name or ID) - -image remove project --------------------- - -*Only supported for Image v2* - -Disassociate project with image - -.. program:: image remove project -.. code:: bash - - openstack image remove project - [--project-domain ] - - - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - -.. _image_remove_project: -.. describe:: - - Image to unshare (name or ID). - -.. describe:: - - Project to disassociate with image (name or ID) - -image save ----------- - -Save an image locally - -.. program:: image save -.. code:: bash - - openstack image save - --file - - -.. option:: --file - - Downloaded image save filename (default: stdout) - -.. _image_save-image: -.. describe:: - - Image to save (name or ID) - -image set ---------- - -*Image v1, v2* - -Set image properties - -.. program:: image set -.. code:: bash - - openstack image set - [--name ] - [--min-disk ] - [--min-ram ] - [--container-format ] - [--disk-format ] - [--size ] - [--protected | --unprotected] - [--public | --private | --community | --shared] - [--store ] - [--location ] - [--copy-from ] - [--file ] - [--volume ] - [--force] - [--checksum ] - [--stdin] - [--property [...] ] - [--tag [...] ] - [--architecture ] - [--instance-id ] - [--kernel-id ] - [--os-distro ] - [--os-version ] - [--ramdisk-id ] - [--deactivate | --activate] - [--project ] - [--project-domain ] - [--accept | --reject | --pending] - - -.. option:: --name - - New image name - -.. option:: --min-disk - - Minimum disk size needed to boot image, in gigabytes - -.. option:: --min-ram - - Minimum RAM size needed to boot image, in megabytes - -.. option:: --container-format - - Image container format. The supported options are: ami, ari, aki, - bare, docker, ova, ovf. - -.. option:: --disk-format - - Image disk format. The supported options are: ami, ari, aki, vhd, vmdk, - raw, qcow2, vhdx, vdi, iso, and ploop. - -.. option:: --size - - Size of image data (in bytes) - - *Image version 1 only.* - -.. option:: --protected - - Prevent image from being deleted - -.. option:: --unprotected - - Allow image to be deleted (default) - -.. option:: --public - - Image is accessible to the public - -.. option:: --private - - Image is inaccessible to the public (default) - -.. option:: --community - - Image is accessible to the community - -.. option:: --shared - - Image can be shared - -.. option:: --store - - Upload image to this store - - *Image version 1 only.* - -.. option:: --location - - Download image from an existing URL - - *Image version 1 only.* - -.. option:: --copy-from - - Copy image from the data store (similar to :option:`--location`) - - *Image version 1 only.* - -.. option:: --file - - Upload image from local file - - *Image version 1 only.* - -.. option:: --volume - - Update image with a volume - - *Image version 1 only.* - -.. option:: --force - - Force image update if volume is in use (only meaningful with :option:`--volume`) - - *Image version 1 only.* - -.. option:: --checksum - - Image hash used for verification - - *Image version 1 only.* - -.. option:: --stdin - - Allow to read image data from standard input - - *Image version 1 only.* - -.. option:: --property - - Set a property on this image (repeat option to set multiple properties) - - .. versionadded:: 2 - -.. option:: --tag - - Set a tag on this image (repeat option to set multiple tags) - - .. versionadded:: 2 - -.. option:: --architecture - - Operating system architecture - - .. versionadded:: 2 - -.. option:: --instance-id - - ID of server instance used to create this image - - .. versionadded:: 2 - -.. option:: --kernel-id - - ID of kernel image used to boot this disk image - - .. versionadded:: 2 - -.. option:: --os-distro - - Operating system distribution name - - .. versionadded:: 2 - -.. option:: --os-version - - Operating system distribution version - - .. versionadded:: 2 - -.. option:: --ramdisk-id - - ID of ramdisk image used to boot this disk image - - .. versionadded:: 2 - -.. option:: --deactivate - - Deactivate the image. - - .. versionadded:: 2 - -.. option:: --activate - - Activate the image. - - .. versionadded:: 2 - -.. option:: --project - - Set an alternate project on this image (name or ID). - Previously known as `--owner`. - -.. option:: --project-domain - - Domain the project belongs to (name or ID). - This can be used in case collisions between project names exist. - - .. versionadded:: 2 - -.. option:: --accept - - Accept the image membership. - - If `--project` is passed, this will update the membership status for the - given project, otherwise `--project` will default to the project the user - is authenticated to. - - .. versionadded:: 2 - -.. option:: --reject - - Reject the image membership. - - If `--project` is passed, this will update the membership status for the - given project, otherwise `--project` will default to the project the user - is authenticated to. - - .. versionadded:: 2 - -.. option:: --pending - - Reset the image membership to 'pending'. - - If `--project` is passed, this will update the membership status for the - given project, otherwise `--project` will default to the project the user - is authenticated to. - - .. versionadded:: 2 - -.. _image_set-image: -.. describe:: - - Image to modify (name or ID) - -image show ----------- - -Display image details - -.. program:: image show -.. code:: bash - - openstack image show - [--human-readable] - - -.. option:: --human-readable - - Print image size in a human-friendly format. - -.. _image_show-image: -.. describe:: - - Image to display (name or ID) - -image unset ------------ - -*Only supported for Image v2* - -Unset image tags or properties - -.. program:: image unset -.. code:: bash - - openstack image unset - [--tag ] - [--property ] - - -.. option:: --tag - - Unset a tag on this image (repeat option to unset multiple tags) - -.. option:: --property - - Unset a property on this image (repeat option to unset multiple properties) - -.. _image_unset-image: -.. describe:: - - Image to modify (name or ID) diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt index d18660a0b2..5ed7c19cb5 100644 --- a/doc/test/redirect-tests.txt +++ b/doc/test/redirect-tests.txt @@ -18,6 +18,7 @@ /python-openstackclient/latest/cli/plugin-commands.html 301 /python-openstackclient/latest/cli/plugin-commands/index.html /python-openstackclient/latest/cli/command-objects/ec2-credentials.html 301 /python-openstackclient/latest/cli/_hidden/ec2-credentials.html /python-openstackclient/latest/cli/command-objects/endpoint.html 301 /python-openstackclient/latest/cli/_hidden/endpoint.html +/python-openstackclient/latest/cli/command-objects/image.html 301 /python-openstackclient/latest/cli/_hidden/image.html /python-openstackclient/latest/cli/command-objects/project.html 301 /python-openstackclient/latest/cli/_hidden/project.html /python-openstackclient/latest/cli/command-objects/role.html 301 /python-openstackclient/latest/cli/_hidden/role.html /python-openstackclient/latest/cli/command-objects/service.html 301 /python-openstackclient/latest/cli/_hidden/service.html From 176907f70eb5b84d15efc03571ca772706a164a6 Mon Sep 17 00:00:00 2001 From: Gabriel Ramirez Date: Thu, 4 Jun 2020 12:46:25 -0700 Subject: [PATCH 0236/1120] Allow openstack flavor set to update flavor description using name Modified take_action() method for SetFlavor to use flavor id instead of flavor name when setting description Closes-Bug: #1844708 Story: #2007781 Task: #40019 Change-Id: If6798c89fef4c9feb4ebb460722b891f5655037d --- openstackclient/compute/v2/flavor.py | 2 +- .../tests/unit/compute/v2/test_flavor.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 42649db504..805e919ea3 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -402,7 +402,7 @@ def take_action(self, parsed_args): if compute_client.api_version < api_versions.APIVersion("2.55"): msg = _("--os-compute-api-version 2.55 or later is required") raise exceptions.CommandError(msg) - compute_client.flavors.update(flavor=parsed_args.flavor, + compute_client.flavors.update(flavor=flavor.id, description=parsed_args.description) diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index fe7ce17481..4732cc822d 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -749,6 +749,42 @@ def test_flavor_set_description_api_older(self): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_flavor_set_description_using_name_api_newer(self): + arglist = [ + '--description', 'description', + self.flavor.name, + ] + verifylist = [ + ('description', 'description'), + ('flavor', self.flavor.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.compute.api_version = 2.55 + with mock.patch.object(novaclient.api_versions, + 'APIVersion', + return_value=2.55): + result = self.cmd.take_action(parsed_args) + self.flavors_mock.update.assert_called_with( + flavor=self.flavor.id, description='description') + self.assertIsNone(result) + + def test_flavor_set_description_using_name_api_older(self): + arglist = [ + '--description', 'description', + self.flavor.name, + ] + verifylist = [ + ('description', 'description'), + ('flavor', self.flavor.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.compute.api_version = 2.54 + with mock.patch.object(novaclient.api_versions, + 'APIVersion', + return_value=2.55): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestFlavorShow(TestFlavor): From a04172969a5e440af50127dfd27a80a6beb7ddb8 Mon Sep 17 00:00:00 2001 From: "Jens Harbott (frickler)" Date: Wed, 17 Jun 2020 10:07:12 +0000 Subject: [PATCH 0237/1120] Revert "Format location columns in network commands" This reverts commit 6ee7b8d138e07bfc37c5cd887f7afa49cdabb02f. Change-Id: I5f59959ba8a01aba49e29f4cb007397467344e58 --- openstackclient/network/v2/address_scope.py | 10 ++-------- openstackclient/network/v2/floating_ip.py | 2 -- openstackclient/network/v2/ip_availability.py | 2 -- openstackclient/network/v2/network.py | 1 - openstackclient/network/v2/network_agent.py | 1 - .../v2/network_auto_allocated_topology.py | 17 +++++------------ openstackclient/network/v2/network_flavor.py | 10 ++-------- .../network/v2/network_flavor_profile.py | 10 ++-------- openstackclient/network/v2/network_meter.py | 10 ++-------- .../network/v2/network_meter_rule.py | 10 ++-------- .../network/v2/network_qos_policy.py | 10 ++-------- openstackclient/network/v2/network_qos_rule.py | 10 ++-------- .../network/v2/network_qos_rule_type.py | 8 +------- openstackclient/network/v2/network_rbac.py | 10 ++-------- openstackclient/network/v2/network_segment.py | 10 ++-------- .../network/v2/network_segment_range.py | 9 ++------- openstackclient/network/v2/port.py | 1 - openstackclient/network/v2/router.py | 1 - openstackclient/network/v2/security_group.py | 3 --- .../network/v2/security_group_rule.py | 10 ++-------- openstackclient/network/v2/subnet.py | 1 - openstackclient/network/v2/subnet_pool.py | 1 - 22 files changed, 28 insertions(+), 119 deletions(-) diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index 7efbb6311e..71c1a9afb7 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -106,7 +100,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_address_scope(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -295,6 +289,6 @@ def take_action(self, parsed_args): parsed_args.address_scope, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index f3e3e5c44c..a2765cd1ef 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -13,7 +13,6 @@ """IP Floating action implementations""" -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils from osc_lib.utils import tags as _tag @@ -25,7 +24,6 @@ _formatters = { - 'location': format_columns.DictColumn, 'port_details': utils.format_dict, } diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index c026baa0cc..ddc88e557e 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -21,9 +21,7 @@ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils - _formatters = { - 'location': format_columns.DictColumn, 'subnet_ip_availability': format_columns.ListDictColumn, } diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 00cb782b73..7a12d523e2 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -40,7 +40,6 @@ def human_readable(self): 'subnet_ids': format_columns.ListColumn, 'admin_state_up': AdminStateColumn, 'is_admin_state_up': AdminStateColumn, - 'location': format_columns.DictColumn, 'router:external': RouterExternalColumn, 'is_router_external': RouterExternalColumn, 'availability_zones': format_columns.ListColumn, diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index a6ed36299c..1678485434 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -43,7 +43,6 @@ def human_readable(self): 'alive': AliveColumn, 'admin_state_up': AdminStateColumn, 'is_admin_state_up': AdminStateColumn, - 'location': format_columns.DictColumn, 'configurations': format_columns.DictColumn, } diff --git a/openstackclient/network/v2/network_auto_allocated_topology.py b/openstackclient/network/v2/network_auto_allocated_topology.py index f6070a0277..36f392006e 100644 --- a/openstackclient/network/v2/network_auto_allocated_topology.py +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -26,11 +25,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -99,17 +93,16 @@ def check_resource_topology(self, client, parsed_args): obj = client.validate_auto_allocated_topology(parsed_args.project) columns = _format_check_resource_columns() - data = utils.get_item_properties( - _format_check_resource(obj), - columns, - formatters=_formatters, - ) + data = utils.get_item_properties(_format_check_resource(obj), + columns, + formatters={}) + return (columns, data) def get_topology(self, client, parsed_args): obj = client.get_auto_allocated_topology(parsed_args.project) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) def take_action(self, parsed_args): diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py index 355d04c1af..c9d368bfc1 100644 --- a/openstackclient/network/v2/network_flavor.py +++ b/openstackclient/network/v2/network_flavor.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_enabled': 'enabled', @@ -142,7 +136,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_flavor(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -306,5 +300,5 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_flavor(parsed_args.flavor, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py index 492fd432a9..6cf0c4124d 100644 --- a/openstackclient/network/v2/network_flavor_profile.py +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -13,7 +13,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -26,11 +25,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_enabled': 'enabled', @@ -116,7 +110,7 @@ def take_action(self, parsed_args): obj = client.create_service_profile(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -252,5 +246,5 @@ def take_action(self, parsed_args): obj = client.find_service_profile(parsed_args.flavor_profile, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py index cde7a30403..df0e1da119 100644 --- a/openstackclient/network/v2/network_meter.py +++ b/openstackclient/network/v2/network_meter.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,11 +26,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -108,7 +102,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_metering_label(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -192,5 +186,5 @@ def take_action(self, parsed_args): obj = client.find_metering_label(parsed_args.meter, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index 5f31255a69..49ff9e1b0e 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,11 +26,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -122,7 +116,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_metering_label_rule(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -205,5 +199,5 @@ def take_action(self, parsed_args): obj = client.find_metering_label_rule(parsed_args.meter_rule_id, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index 1622de4a7a..fd5ff93771 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'is_shared': 'shared', @@ -125,7 +119,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_qos_policy(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) @@ -285,5 +279,5 @@ def take_action(self, parsed_args): obj = client.find_qos_policy(parsed_args.policy, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index d74beda70f..28c5600aa6 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -15,7 +15,6 @@ import itertools -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -47,11 +46,6 @@ ACTION_SHOW = 'get' -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'tenant_id': 'project_id', @@ -214,7 +208,7 @@ def take_action(self, parsed_args): msg = (_('Failed to create Network QoS rule: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data @@ -364,5 +358,5 @@ def take_action(self, parsed_args): {'rule': rule_id, 'e': e}) raise exceptions.CommandError(msg) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_qos_rule_type.py b/openstackclient/network/v2/network_qos_rule_type.py index e842944cce..7b92c8ad4b 100644 --- a/openstackclient/network/v2/network_qos_rule_type.py +++ b/openstackclient/network/v2/network_qos_rule_type.py @@ -13,7 +13,6 @@ # 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 osc_lib import utils @@ -21,11 +20,6 @@ from openstackclient.network import sdk_utils -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { "type": "rule_type_name", @@ -71,5 +65,5 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.get_qos_rule_type(parsed_args.rule_type) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 2c34d7ef9a..b88ef019e0 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -28,11 +27,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): column_map = { 'target_tenant': 'target_project_id', @@ -153,7 +147,7 @@ def take_action(self, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_rbac_policy(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data @@ -311,5 +305,5 @@ def take_action(self, parsed_args): obj = client.find_rbac_policy(parsed_args.rbac_policy, ignore_missing=False) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return display_columns, data diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index 5899dc697b..c1a672e2d5 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -15,7 +15,6 @@ import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -27,11 +26,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) @@ -96,7 +90,7 @@ def take_action(self, parsed_args): attrs['segmentation_id'] = parsed_args.segment obj = client.create_segment(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) @@ -248,5 +242,5 @@ def take_action(self, parsed_args): ignore_missing=False ) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index b38c72c248..6229995aab 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -19,7 +19,6 @@ import itertools import logging -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -31,10 +30,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) @@ -216,7 +211,7 @@ def take_action(self, parsed_args): attrs['physical_network'] = parsed_args.physical_network obj = network_client.create_network_segment_range(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) @@ -455,6 +450,6 @@ def take_action(self, parsed_args): ignore_missing=False ) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index a22bcafb56..68154c3d77 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -51,7 +51,6 @@ def human_readable(self): 'dns_assignment': format_columns.ListDictColumn, 'extra_dhcp_opts': format_columns.ListDictColumn, 'fixed_ips': format_columns.ListDictColumn, - 'location': format_columns.DictColumn, 'security_group_ids': format_columns.ListColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 81b81f98b5..e3e8accd21 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -61,7 +61,6 @@ def human_readable(self): 'external_gateway_info': RouterInfoColumn, 'availability_zones': format_columns.ListColumn, 'availability_zone_hints': format_columns.ListColumn, - 'location': format_columns.DictColumn, 'routes': RoutesColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index aec9c56efe..0732c23edc 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -16,7 +16,6 @@ import argparse from cliff import columns as cliff_columns -from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils from osc_lib.utils import tags as _tag @@ -77,13 +76,11 @@ def human_readable(self): _formatters_network = { - 'location': format_columns.DictColumn, 'security_group_rules': NetworkSecurityGroupRulesColumn, } _formatters_compute = { - 'location': format_columns.DictColumn, 'rules': ComputeSecurityGroupRulesColumn, } diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index f48478ea74..1fbd97abc3 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -16,7 +16,6 @@ import argparse import logging -from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils @@ -31,11 +30,6 @@ LOG = logging.getLogger(__name__) -_formatters = { - 'location': format_columns.DictColumn, -} - - def _format_security_group_rule_show(obj): data = network_utils.transform_compute_security_group_rule(obj) return zip(*sorted(data.items())) @@ -353,7 +347,7 @@ def take_action_network(self, client, parsed_args): # Create and show the security group rule. obj = client.create_security_group_rule(**attrs) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) def take_action_compute(self, client, parsed_args): @@ -620,7 +614,7 @@ def take_action_network(self, client, parsed_args): if not obj['remote_ip_prefix']: obj['remote_ip_prefix'] = _format_remote_ip_prefix(obj) display_columns, columns = _get_columns(obj) - data = utils.get_item_properties(obj, columns, formatters=_formatters) + data = utils.get_item_properties(obj, columns) return (display_columns, data) def take_action_compute(self, client, parsed_args): diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index f684406558..f87f7abe1b 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -61,7 +61,6 @@ def human_readable(self): 'allocation_pools': AllocationPoolsColumn, 'dns_nameservers': format_columns.ListColumn, 'host_routes': HostRoutesColumn, - 'location': format_columns.DictColumn, 'service_types': format_columns.ListColumn, 'tags': format_columns.ListColumn, } diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 2750574a97..56cf61526c 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -42,7 +42,6 @@ def _get_columns(item): _formatters = { - 'location': format_columns.DictColumn, 'prefixes': format_columns.ListColumn, 'tags': format_columns.ListColumn, } From 8b7a2c8d5931f15b69d1ef6baad994f4b9904579 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Fri, 30 Aug 2019 13:15:22 +0000 Subject: [PATCH 0238/1120] Don't display Munch objects in the output When the sdk gives us a resource that contains Munch columns, drop them from the output as they are for programmatic usage only and have no use in a CLI context. Change-Id: Idd7306cd763b5a017a66e410e70e1adb02663c2a --- openstackclient/network/sdk_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openstackclient/network/sdk_utils.py b/openstackclient/network/sdk_utils.py index af9c74f944..cff3071354 100644 --- a/openstackclient/network/sdk_utils.py +++ b/openstackclient/network/sdk_utils.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import munch + def get_osc_show_columns_for_sdk_resource( sdk_resource, @@ -38,6 +40,9 @@ def get_osc_show_columns_for_sdk_resource( # Build the OSC column names to display for the SDK resource. attr_map = {} display_columns = list(resource_dict.keys()) + for col_name in display_columns: + if isinstance(resource_dict[col_name], munch.Munch): + display_columns.remove(col_name) invisible_columns = [] if invisible_columns is None else invisible_columns for col_name in invisible_columns: if col_name in display_columns: From 307d23bb58f0f44c9b9facdeaaa10227748ef50d Mon Sep 17 00:00:00 2001 From: Mohammed Naser Date: Tue, 16 Jun 2020 15:31:26 -0400 Subject: [PATCH 0239/1120] port: add --host to list command This adds an option to allow filtering ports bound to a specific host when listing them. Change-Id: I747ed0f8b070074c51789576a158345f670fe733 --- openstackclient/network/v2/port.py | 6 ++++++ .../tests/unit/network/v2/test_port.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index a22bcafb56..4011f5f0a2 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -515,6 +515,10 @@ def get_parser(self, prog_name): "This is the entity that uses the port (for example, " "network:dhcp).") ) + parser.add_argument( + '--host', + metavar='', + help=_("List only ports bound to this host ID")) parser.add_argument( '--network', metavar='', @@ -603,6 +607,8 @@ def take_action(self, parsed_args): server = utils.find_resource(compute_client.servers, parsed_args.server) filters['device_id'] = server.id + if parsed_args.host: + filters['binding:host_id'] = parsed_args.host if parsed_args.network: network = network_client.find_network(parsed_args.network, ignore_missing=False) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index b1a18da6c1..87aea61fac 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -1054,6 +1054,22 @@ def test_list_port_with_long(self): self.assertEqual(self.columns_long, columns) self.assertListItemEqual(self.data_long, list(data)) + def test_port_list_host(self): + arglist = [ + '--host', 'foobar', + ] + verifylist = [ + ('host', 'foobar'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'binding:host_id': 'foobar'} + + self.network.ports.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertListItemEqual(self.data, list(data)) + def test_port_list_project(self): project = identity_fakes.FakeProject.create_one_project() self.projects_mock.get.return_value = project From ff2a70c418c9686b13f64fed760432f30391441a Mon Sep 17 00:00:00 2001 From: melissaml Date: Tue, 23 Jun 2020 12:22:58 +0800 Subject: [PATCH 0240/1120] Remove translation sections from setup.cfg These translation sections are not needed anymore, Babel can generate translation files without them. Change-Id: Ic5d57186766257e9d37b3588e71f973cddad9be4 --- babel.cfg | 1 - lower-constraints.txt | 1 - setup.cfg | 14 -------------- 3 files changed, 16 deletions(-) delete mode 100644 babel.cfg diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab818b..0000000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] diff --git a/lower-constraints.txt b/lower-constraints.txt index f16eca4184..840d32a8f6 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -2,7 +2,6 @@ amqp==2.1.1 aodhclient==0.9.0 appdirs==1.3.0 asn1crypto==0.23.0 -Babel==2.3.4 bandit==1.1.0 cachetools==2.0.0 cffi==1.7.0 diff --git a/setup.cfg b/setup.cfg index 273529b1ec..3502396f72 100644 --- a/setup.cfg +++ b/setup.cfg @@ -718,17 +718,3 @@ openstack.volume.v3 = volume_transfer_request_delete = openstackclient.volume.v2.volume_transfer_request:DeleteTransferRequest volume_transfer_request_list = openstackclient.volume.v2.volume_transfer_request:ListTransferRequest volume_transfer_request_show = openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = openstackclient/locale/openstackclient.pot - -[update_catalog] -domain = openstackclient -output_dir = openstackclient/locale -input_file = openstackclient/locale/openstackclient.pot - -[compile_catalog] -directory = openstackclient/locale -domain = openstackclient From c04ec16cf7ad2dbe7bf8edf5f6e7840c54b0efa3 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 23 Jun 2020 15:45:47 -0500 Subject: [PATCH 0241/1120] Expose flag for forcing use of import for images openstacksdk added support for using image import as a fallback which is transparently supported here, but also provides an override flag to allow a user to force use of import. Expose that here. Depends-On: https://review.opendev.org/737608 Change-Id: Ied938a8f63f305305a20ace42e9f4c84b0a5c00e --- lower-constraints.txt | 2 +- openstackclient/image/v2/image.py | 11 ++++++++++ .../tests/unit/image/v2/test_image.py | 22 +++++++++++++++++++ ...dd-image-import-flag-899869dc5a92aea7.yaml | 5 +++++ requirements.txt | 2 +- 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index f16eca4184..c3ac29106e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -50,7 +50,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.44.0 +openstacksdk==0.48.0 os-service-types==1.7.0 os-testr==1.0.0 osc-lib==2.0.0 diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 53ce560dcd..b068ddafd1 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -324,6 +324,14 @@ def get_parser(self, prog_name): metavar="", help=_("Set an alternate project on this image (name or ID)"), ) + parser.add_argument( + "--import", + dest="use_import", + action="store_true", + help=_( + "Force the use of glance image import instead of" + " direct upload") + ) common.add_project_domain_option_to_parser(parser) for deadopt in self.deadopts: parser.add_argument( @@ -388,6 +396,9 @@ def take_action(self, parsed_args): parsed_args.project_domain, ).id + if parsed_args.use_import: + kwargs['use_import'] = True + # 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. diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index a021cfc7fc..310f6b7636 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -271,6 +271,28 @@ def test_image_create_dead_options(self): exceptions.CommandError, self.cmd.take_action, parsed_args) + @mock.patch('sys.stdin', side_effect=[None]) + def test_image_create_import(self, raw_input): + + arglist = [ + '--import', + self.new_image.name, + ] + verifylist = [ + ('name', self.new_image.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.client.create_image.assert_called_with( + name=self.new_image.name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + use_import=True + ) + class TestAddProjectToImage(TestImage): diff --git a/releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml b/releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml new file mode 100644 index 0000000000..fbd29bcd8f --- /dev/null +++ b/releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added ``--import`` flag to ``openstack image create`` to allow + the user to force use of the image import codepath. diff --git a/requirements.txt b/requirements.txt index 3aac36af2f..89e9080602 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 six>=1.10.0 # MIT cliff!=2.9.0,>=2.8.0 # Apache-2.0 -openstacksdk>=0.44.0 # Apache-2.0 +openstacksdk>=0.48.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 From 4638dbc7f373177277dbf8aabe6d84f15334492e Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Fri, 3 Jul 2020 10:02:34 -0500 Subject: [PATCH 0242/1120] Remove enabling of glance v1 API devstack removing the glance v1 api enable option[1] because there is no v1 entry point in glance now[2]. Let's remove ths GLANCE_V1_ENABLED variable setting from zuul job too to avoid any confusion of glance v1 is still available. [1] https://review.opendev.org/#/c/698808/ [2] https://review.opendev.org/#/c/532503/ Change-Id: I6d3a38eee0c75bbc795bad732fe547181d15c677 --- .zuul.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 734b1ededc..e566ceb845 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -51,8 +51,6 @@ vars: devstack_localrc: LIBS_FROM_GIT: python-openstackclient - # NOTE(dtroyer): OSC needs to support Image v1 for a while yet so re-enable - GLANCE_V1_ENABLED: true # NOTE(dtroyer): Functional tests need a bit more volume headroom VOLUME_BACKING_FILE_SIZE: 20G devstack_local_conf: From b1fc587a6d4eac795a5bc27c2dffd2d876161caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Piliszek?= Date: Sun, 5 Jul 2020 09:53:14 +0200 Subject: [PATCH 0243/1120] Make volume backup record commands available in v3 They work just fine in Volume API v3 but they were limited in OSC to v2. Change-Id: I510383f8e0cbf05ec24caa1cad330f12f82a913d Story: 2007896 Task: 40279 --- releasenotes/notes/task-40279-eb0d718ac1959c50.yaml | 5 +++++ setup.cfg | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 releasenotes/notes/task-40279-eb0d718ac1959c50.yaml diff --git a/releasenotes/notes/task-40279-eb0d718ac1959c50.yaml b/releasenotes/notes/task-40279-eb0d718ac1959c50.yaml new file mode 100644 index 0000000000..aedb3ea929 --- /dev/null +++ b/releasenotes/notes/task-40279-eb0d718ac1959c50.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Makes ``volume backup record`` commands available in Volume API v3. + `Task 40279 `__ diff --git a/setup.cfg b/setup.cfg index 273529b1ec..473950916f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -685,6 +685,9 @@ openstack.volume.v3 = volume_backup_set = openstackclient.volume.v2.volume_backup:SetVolumeBackup volume_backup_show = openstackclient.volume.v2.volume_backup:ShowVolumeBackup + volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord + volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord + volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost volume_snapshot_create = openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot From 870cf0114848f145f15a78415e3f4203c6338cd1 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sat, 4 Jul 2020 11:34:05 -0400 Subject: [PATCH 0244/1120] switch to stevedore for entry points Importing pkg_resources scans every installed distribution to find all of the entry points. Stevedore is adding a new caching layer using importlib.metadata, which will not. Switching to the stevedore should eventually speed up load times, especially for command line apps. This change makes the switch now to ensure API compatibility. We were already using stevedore for tests, so this moves the dependency from test-requirements.txt to requirements.txt and raises the minimum version to something more recent. Change-Id: I3e3632783bc745979b6db73e610df8a77ffaceb0 Signed-off-by: Doug Hellmann --- lower-constraints.txt | 2 +- openstackclient/common/clientmanager.py | 23 ++++++++++++++++------- requirements.txt | 1 + test-requirements.txt | 1 - 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index f16eca4184..c320edbe6e 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -124,7 +124,7 @@ six==1.10.0 smmap==0.9.0 statsd==3.2.1 stestr==1.0.0 -stevedore==1.20.0 +stevedore==2.0.1 sushy==0.1.0 tempest==17.1.0 tenacity==3.2.1 diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index c1118ad3ef..66dc880e07 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -15,12 +15,13 @@ """Manage access to the clients, including authenticating when needed.""" +import importlib import logging import sys from osc_lib import clientmanager from osc_lib import shell -import pkg_resources +import stevedore LOG = logging.getLogger(__name__) @@ -143,17 +144,25 @@ def is_volume_endpoint_enabled(self, volume_client): def get_plugin_modules(group): """Find plugin entry points""" mod_list = [] - for ep in pkg_resources.iter_entry_points(group): + mgr = stevedore.ExtensionManager(group) + for ep in mgr: LOG.debug('Found plugin %s', ep.name) + # Different versions of stevedore use different + # implementations of EntryPoint from other libraries, which + # are not API-compatible. try: - __import__(ep.module_name) - except Exception: + module_name = ep.entry_point.module_name + except AttributeError: + module_name = ep.entry_point.module + + try: + module = importlib.import_module(module_name) + except Exception as err: sys.stderr.write( - "WARNING: Failed to import plugin %s.\n" % ep.name) + "WARNING: Failed to import plugin %s: %s.\n" % (ep.name, err)) continue - module = sys.modules[ep.module_name] mod_list.append(module) init_func = getattr(module, 'Initialize', None) if init_func: @@ -164,7 +173,7 @@ def get_plugin_modules(group): clientmanager.ClientManager, module.API_NAME, clientmanager.ClientCache( - getattr(sys.modules[ep.module_name], 'make_client', None) + getattr(sys.modules[module_name], 'make_client', None) ), ) return mod_list diff --git a/requirements.txt b/requirements.txt index 3aac36af2f..b1421a831f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ oslo.utils>=3.33.0 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=15.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 +stevedore>=2.0.1 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index f2b6a13402..3dce687bc7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,7 +8,6 @@ flake8-import-order>=0.13 # LGPLv3 oslotest>=3.2.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 -stevedore>=1.20.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 From 5c4eb0bf9d7bc154e583f1d864984d56192ccb4b Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Jul 2020 14:50:59 -0500 Subject: [PATCH 0245/1120] Add a command to trigger entrypoint cache creation stevedore will cache the entrypoint scan when needed. Since we just installed the things here, do an openstack --help to cause the entrypoints to get scanned at build time and for the cache file to be written into the container image. Change-Id: I73502be6d68c4a38561c9524b4def3c6a6f61ac6 --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index ca60bb0ecb..bf5de3c755 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,4 +23,7 @@ FROM docker.io/opendevorg/python-base:3.7 COPY --from=builder /output/ /output RUN /output/install-from-bindep +# Trigger entrypoint loading to trigger stevedore entrypoint caching +RUN openstack --help >/dev/null 2>&1 + CMD ["/usr/local/bin/openstack"] From c06d82563526c715d3ed508fa3cc5f9dc0963294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Gr=C3=A4b?= Date: Tue, 7 Jul 2020 11:04:33 +0200 Subject: [PATCH 0246/1120] Fix uploading an signed image does not work if private signing key is encrypted Change-Id: Ia7c84aa7b840bf92aeb7db7246d14119eb727b03 Story: 2007890 Task: 40269 --- openstackclient/image/v2/image.py | 6 ++++++ releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 53ce560dcd..50a64d4cd4 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -429,8 +429,14 @@ def take_action(self, parsed_args): prompt=("Please enter private key password, leave " "empty if none: "), confirm=False) + if not pw or len(pw) < 1: pw = None + else: + # load_private_key() requires the password to be + # passed as bytes + pw = pw.encode() + signer.load_private_key( sign_key_path, password=pw) diff --git a/releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml b/releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml new file mode 100644 index 0000000000..87d6f18bae --- /dev/null +++ b/releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + While uploading a signed image, a private key to sign that image must be + specified. The CLI client asks for the password of that private key. Due + to wrong encoding handling while using Python 3, the password is not + accepted, whether it is correct or not. From a8aad9fec80bcb6c9917d2dd076373f06467849f Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Thu, 9 Jul 2020 16:50:01 -0500 Subject: [PATCH 0247/1120] Add system role assignment tests for users and groups I was writing some additional functionality and noticed these tests were missing. This commit adds tests for adding and removing system role assignments for users and groups. Change-Id: I30fdc6ec55e1eb1cfa55f4cbf92c3f001d89865f --- .../tests/unit/identity/v3/test_role.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 544da7c15d..c1e91f9a0f 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -102,6 +102,40 @@ def setUp(self): # Get the command object to test self.cmd = role.AddRole(self.app, None) + def test_role_add_user_system(self): + arglist = [ + '--user', identity_fakes.user_name, + '--system', 'all', + identity_fakes.role_name, + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('system', 'all'), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'system': 'all', + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.grant(role, user=, group=, domain=, project=) + self.roles_mock.grant.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_add_user_domain(self): arglist = [ '--user', identity_fakes.user_name, @@ -168,6 +202,40 @@ def test_role_add_user_project(self): ) self.assertIsNone(result) + def test_role_add_group_system(self): + arglist = [ + '--group', identity_fakes.group_name, + '--system', 'all', + identity_fakes.role_name, + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('system', 'all'), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'system': 'all', + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.grant(role, user=, group=, domain=, project=) + self.roles_mock.grant.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_add_group_domain(self): arglist = [ '--group', identity_fakes.group_name, @@ -744,6 +812,40 @@ def setUp(self): # Get the command object to test self.cmd = role.RemoveRole(self.app, None) + def test_role_remove_user_system(self): + arglist = [ + '--user', identity_fakes.user_name, + '--system', 'all', + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('system', 'all'), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'system': 'all', + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_user_domain(self): arglist = [ '--user', identity_fakes.user_name, @@ -810,6 +912,41 @@ def test_role_remove_user_project(self): ) self.assertIsNone(result) + def test_role_remove_group_system(self): + arglist = [ + '--group', identity_fakes.group_name, + '--system', 'all', + identity_fakes.role_name, + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('system', 'all'), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'system': 'all', + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_group_domain(self): arglist = [ '--group', identity_fakes.group_name, From 82ebddca006d1dc61855fdd34b0616222039ea58 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Tue, 14 Jul 2020 01:02:00 +0800 Subject: [PATCH 0248/1120] Fix compatibility issue in 5.3 The offending entry point object looks like: EntryPoint(name='compute', value='openstackclient.compute.client', group='openstack.cli.base') Story: 2007917 Task: 40323 Change-Id: I0f3cc62e23efdc14203ce6645581d5ba5dbf7fa0 --- openstackclient/common/clientmanager.py | 5 ++++- releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 66dc880e07..36c3ce2688 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -154,7 +154,10 @@ def get_plugin_modules(group): try: module_name = ep.entry_point.module_name except AttributeError: - module_name = ep.entry_point.module + try: + module_name = ep.entry_point.module + except AttributeError: + module_name = ep.entry_point.value try: module = importlib.import_module(module_name) diff --git a/releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml b/releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml new file mode 100644 index 0000000000..cb25cff14f --- /dev/null +++ b/releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes an issue with python 3.8 and entrypoint loading where the + new builtin importlib entrypoint support had a different + attribute api than expected. From 8628e52de7412e57e13238ad1ba7113deb6a2e1b Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Tue, 21 Jul 2020 18:24:58 +0530 Subject: [PATCH 0249/1120] Add name and enabled param in ListDomain parser when doing openstack domain list --name xyz_id, and openstack domain list --enabled CLI raising error unrecognized arguments, whereas in api-ref document [1], user can pass name and enabled as optional query param. This addresses the above issue, by adding param --name and --enabled in parser of ListDomain. [1]https://docs.openstack.org/api-ref/identity/v3/?expanded=list-domains-detail#list-domains Change-Id: I3cdb511d3c7059ddfb802ca025188d8976c9302c --- openstackclient/identity/v3/domain.py | 23 +++++++- .../tests/unit/identity/v3/test_domain.py | 55 +++++++++++++++++++ ...abled_to_list_domain-6d23f02994b51c67.yaml | 3 + 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add_name_and_enabled_to_list_domain-6d23f02994b51c67.yaml diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index e33fce05c5..e0bd10202a 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -126,9 +126,30 @@ def take_action(self, parsed_args): class ListDomain(command.Lister): _description = _("List domains") + def get_parser(self, prog_name): + parser = super(ListDomain, self).get_parser(prog_name) + parser.add_argument( + '--name', + metavar='', + help=_('The domain name'), + ) + parser.add_argument( + '--enabled', + dest='enabled', + action='store_true', + help=_('The domains that are enabled will be returned'), + ) + return parser + def take_action(self, parsed_args): + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.enabled: + kwargs['enabled'] = True + columns = ('ID', 'Name', 'Enabled', 'Description') - data = self.app.client_manager.identity.domains.list() + data = self.app.client_manager.identity.domains.list(**kwargs) return (columns, (utils.get_item_properties( s, columns, diff --git a/openstackclient/tests/unit/identity/v3/test_domain.py b/openstackclient/tests/unit/identity/v3/test_domain.py index 46f389e890..c39f1bd3d7 100644 --- a/openstackclient/tests/unit/identity/v3/test_domain.py +++ b/openstackclient/tests/unit/identity/v3/test_domain.py @@ -293,6 +293,61 @@ def test_domain_list_no_options(self): ), ) self.assertEqual(datalist, tuple(data)) + def test_domain_list_with_option_name(self): + arglist = ['--name', + self.domain.name] + verifylist = [ + ('name', self.domain.name) + ] + 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) + + kwargs = { + 'name': self.domain.name + } + self.domains_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Name', 'Enabled', 'Description') + self.assertEqual(collist, columns) + datalist = (( + self.domain.id, + self.domain.name, + True, + self.domain.description, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_domain_list_with_option_enabled(self): + arglist = ['--enabled'] + verifylist = [ + ('enabled', 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) + + kwargs = { + 'enabled': True + } + self.domains_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Name', 'Enabled', 'Description') + self.assertEqual(collist, columns) + datalist = (( + self.domain.id, + self.domain.name, + True, + self.domain.description, + ), ) + self.assertEqual(datalist, tuple(data)) + class TestDomainSet(TestDomain): diff --git a/releasenotes/notes/add_name_and_enabled_to_list_domain-6d23f02994b51c67.yaml b/releasenotes/notes/add_name_and_enabled_to_list_domain-6d23f02994b51c67.yaml new file mode 100644 index 0000000000..4eb58b7833 --- /dev/null +++ b/releasenotes/notes/add_name_and_enabled_to_list_domain-6d23f02994b51c67.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add ``--name`` and ``--domain`` option to ``domain list`` command. \ No newline at end of file From 12f1e56ebf2ea3999c57246410501c09fced2c4c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 22 Jul 2020 10:39:47 +0100 Subject: [PATCH 0250/1120] Add 'openstack server create --use-config-drive' Despite what the help text for this options says, the nova API only accepts boolean values for this value and has done so since at least the introduction of the 2.1 microversioned API. While it would be nice to convert '--config-drive' to a boolean flag, we'd need to be able to retain temporary support for people passing arguments. 'nargs=?' [1] looks promising but it has an annoying tendency to swallow a positional argument following it [2]. Since that is not an option, we have to live with a new config option, '--use-config-drive' and a '--no-config-drive' counterpart. [1] https://docs.python.org/3/library/argparse.html#nargs [2] https://bugs.python.org/issue9338 Change-Id: If9cce0ad4094cc9cef1c9136b80c3b0f35a82c7a Signed-off-by: Stephen Finucane Story: #2005468 Task: #30547 --- openstackclient/compute/v2/server.py | 45 ++++++++++++++----- .../tests/unit/compute/v2/test_server.py | 5 ++- ...ver-use-config-drive-9fc68552365cfefa.yaml | 8 ++++ 3 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966ae..54cfe770f2 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -693,12 +693,30 @@ def get_parser(self, prog_name): default={}, help=_('Hints for the scheduler (optional extension)'), ) - parser.add_argument( + config_drive_group = parser.add_mutually_exclusive_group() + config_drive_group.add_argument( + '--use-config-drive', + action='store_true', + dest='config_drive', + help=_("Enable config drive."), + ) + config_drive_group.add_argument( + '--no-config-drive', + action='store_false', + dest='config_drive', + help=_("Disable config drive."), + ) + # TODO(stephenfin): Drop support in the next major version bump after + # Victoria + config_drive_group.add_argument( '--config-drive', metavar='|True', default=False, - help=_('Use specified volume as the config drive, ' - 'or \'True\' to use an ephemeral drive'), + help=_( + "**Deprecated** Use specified volume as the config drive, " + "or 'True' to use an ephemeral drive. Replaced by " + "'--use-config-drive'." + ), ) parser.add_argument( '--min', @@ -991,16 +1009,19 @@ def _match_image(image_api, wanted_properties): else: hints[key] = values - # What does a non-boolean value for config-drive do? - # --config-drive argument is either a volume id or - # 'True' (or '1') to use an ephemeral volume - if str(parsed_args.config_drive).lower() in ("true", "1"): - config_drive = True - elif str(parsed_args.config_drive).lower() in ("false", "0", - "", "none"): - config_drive = None + if isinstance(parsed_args.config_drive, bool): + # NOTE(stephenfin): The API doesn't accept False as a value :'( + config_drive = parsed_args.config_drive or None else: - config_drive = parsed_args.config_drive + # TODO(stephenfin): Remove when we drop support for + # '--config-drive' + if str(parsed_args.config_drive).lower() in ("true", "1"): + config_drive = True + elif str(parsed_args.config_drive).lower() in ("false", "0", + "", "none"): + config_drive = None + else: + config_drive = parsed_args.config_drive boot_kwargs = dict( meta=parsed_args.property, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c50c..dc699c0f43 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -857,6 +857,7 @@ def test_server_create_with_options(self): '--key-name', 'keyname', '--property', 'Beta=b', '--security-group', 'securitygroup', + '--use-config-drive', '--hint', 'a=b', '--hint', 'a=c', self.new_server.name, @@ -868,7 +869,7 @@ def test_server_create_with_options(self): ('property', {'Beta': 'b'}), ('security_group', ['securitygroup']), ('hint', {'a': ['b', 'c']}), - ('config_drive', False), + ('config_drive', True), ('server_name', self.new_server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -900,7 +901,7 @@ def test_server_create_with_options(self): block_device_mapping_v2=[], nics=[], scheduler_hints={'a': ['b', 'c']}, - config_drive=None, + config_drive=True, ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( diff --git a/releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml b/releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml new file mode 100644 index 0000000000..786ede4f78 --- /dev/null +++ b/releasenotes/notes/story-2005468-server-use-config-drive-9fc68552365cfefa.yaml @@ -0,0 +1,8 @@ +--- +deprecations: + - | + The ``--config-drive`` option on the ``openstack server create`` command + has been deprecated in favour of the ``--use-config-drive`` and + ``--no-config-drive`` arguments. The ``--config-drive`` option expected + either a string or bool-like argument, but the nova API has only supported + boolean values since API v2.1 was introduced. From 0a8753dc3eaeda25554ccd769350de1e9792a62b Mon Sep 17 00:00:00 2001 From: Roger Luethi Date: Thu, 23 Jul 2020 13:20:09 +0200 Subject: [PATCH 0251/1120] Fix reverted osc-lib interface change The patch https://review.opendev.org/#/c/673389/ introduced a regression by changing the osc-lib interface. Two conflicting attempts to fix the regression were launched: 1) Reverting the patch. 2) The patch https://review.opendev.org/683119 changes the exception from the generic CommandError back to a specific Forbidden exception. The patch https://review.opendev.org/683118 catches this exception and passes on, i.e. re-implements the same behavior as before. The first idea was implemented, the initial patch reverted. The second idea was partially implemented. The change in python-openstackclient (683118) was merged. The change in osc-lib was approved but failed to merge because the initial change had been reverted. Now we have again a situation where the exception produced in osc-lib does not match the exception expected by the caller. It is unclear if the osc-lib interface will ever get a rebased version of https://review.opendev.org/683119 merged, so the safest way to address the issue is to also catch the exception that used to be thrown before the inital change and is again thrown after the inital change has been reverted. Change-Id: I2ea2def607ec5be112e42d53a1e660fef0cdd69c --- openstackclient/identity/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index e70d87d21f..a75db4f8dd 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -207,7 +207,7 @@ def _find_identity_resource(identity_client_manager, name_or_id, name_or_id, **kwargs) if identity_resource is not None: return identity_resource - except exceptions.Forbidden: + except (exceptions.Forbidden, identity_exc.Forbidden): pass return resource_type(None, {'id': name_or_id, 'name': name_or_id}) From 4e2aefb5fa2ce236473187650149cf421c5f8c53 Mon Sep 17 00:00:00 2001 From: mb711d Date: Wed, 15 Jul 2020 17:18:53 -0400 Subject: [PATCH 0252/1120] Delete the testcases that arent needed anymore The file test_examples.py has never worked since its written and and cli in the example directory are covered by other functional tests for container, flavor and object lists and they have better asserts. So, deleting the file Change-Id: Ib9af40da96e66354fe878e79a80048a58f8dd6fe --- openstackclient/tests/functional/base.py | 4 --- .../tests/functional/examples/__init__.py | 0 .../functional/examples/test_examples.py | 28 ------------------- 3 files changed, 32 deletions(-) delete mode 100644 openstackclient/tests/functional/examples/__init__.py delete mode 100644 openstackclient/tests/functional/examples/test_examples.py diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 08e9390e3e..3542a82756 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -19,10 +19,6 @@ import testtools -COMMON_DIR = os.path.dirname(os.path.abspath(__file__)) -FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..')) -ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..')) -EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples') ADMIN_CLOUD = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin') diff --git a/openstackclient/tests/functional/examples/__init__.py b/openstackclient/tests/functional/examples/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstackclient/tests/functional/examples/test_examples.py b/openstackclient/tests/functional/examples/test_examples.py deleted file mode 100644 index 031f036a98..0000000000 --- a/openstackclient/tests/functional/examples/test_examples.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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.tests.functional import base - - -class ExampleTests(base.TestCase): - """Functional tests for running examples.""" - - def test_common(self): - # NOTE(stevemar): If an examples has a non-zero return - # code, then execute will raise an error by default. - base.execute('python', base.EXAMPLE_DIR + '/common.py --debug') - - def test_object_api(self): - base.execute('python', base.EXAMPLE_DIR + '/object_api.py --debug') - - def test_osc_lib(self): - base.execute('python', base.EXAMPLE_DIR + '/osc-lib.py --debug') From 1e053babf4d674ac31d51dfba048704f32b558b3 Mon Sep 17 00:00:00 2001 From: Vishakha Agarwal Date: Fri, 24 Jul 2020 19:22:39 +0530 Subject: [PATCH 0253/1120] Add id and enabled param in ListIdentityProvider parser when doing openstack identity provider list --name xyz_id, and openstack identity provider list --enabled CLI raising error unrecognized arguments, whereas in api-ref document [1], user can pass name and enabled as optional query param. This addresses the above issue, by adding param --id and --enabled in parser of ListIdentityProvider. [1] https://docs.openstack.org/api-ref/identity/v3-ext/?expanded=list-identity-providers-detail#list-identity-providers Change-Id: I59ce3a5f54700ba5a735f0b3b4b3b73b3a8658fa --- .../identity/v3/identity_provider.py | 24 +++++++- .../identity/v3/test_identity_provider.py | 55 +++++++++++++++++++ ...st_identity_provider-e0981063a2dc5961.yaml | 3 + 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add_id_and_enabled_to_list_identity_provider-e0981063a2dc5961.yaml diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 2b2d9d11bd..7307cea00c 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -143,10 +143,32 @@ def take_action(self, parsed_args): class ListIdentityProvider(command.Lister): _description = _("List identity providers") + def get_parser(self, prog_name): + parser = super(ListIdentityProvider, self).get_parser(prog_name) + parser.add_argument( + '--id', + metavar='', + help=_('The Identity Providers’ ID attribute'), + ) + parser.add_argument( + '--enabled', + dest='enabled', + action='store_true', + help=_('The Identity Providers that are enabled will be returned'), + ) + return parser + def take_action(self, parsed_args): columns = ('ID', 'Enabled', 'Domain ID', 'Description') identity_client = self.app.client_manager.identity - data = identity_client.federation.identity_providers.list() + + kwargs = {} + if parsed_args.id: + kwargs['id'] = parsed_args.id + if parsed_args.enabled: + kwargs['enabled'] = True + + data = identity_client.federation.identity_providers.list(**kwargs) return (columns, (utils.get_item_properties( s, columns, diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index a419a9bcb1..39a37db24a 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -384,6 +384,61 @@ def test_identity_provider_list_no_options(self): ), ) self.assertListItemEqual(datalist, tuple(data)) + def test_identity_provider_list_ID_option(self): + arglist = ['--id', + identity_fakes.idp_id] + verifylist = [ + ('id', identity_fakes.idp_id) + ] + 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) + + kwargs = { + 'id': identity_fakes.idp_id + } + self.identity_providers_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Enabled', 'Domain ID', 'Description') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.idp_id, + True, + identity_fakes.domain_id, + identity_fakes.idp_description, + ), ) + self.assertListItemEqual(datalist, tuple(data)) + + def test_identity_provider_list_enabled_option(self): + arglist = ['--enabled'] + verifylist = [ + ('enabled', 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) + + kwargs = { + 'enabled': True + } + self.identity_providers_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Enabled', 'Domain ID', 'Description') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.idp_id, + True, + identity_fakes.domain_id, + identity_fakes.idp_description, + ), ) + self.assertListItemEqual(datalist, tuple(data)) + class TestIdentityProviderSet(TestIdentityProvider): diff --git a/releasenotes/notes/add_id_and_enabled_to_list_identity_provider-e0981063a2dc5961.yaml b/releasenotes/notes/add_id_and_enabled_to_list_identity_provider-e0981063a2dc5961.yaml new file mode 100644 index 0000000000..fccd0a6353 --- /dev/null +++ b/releasenotes/notes/add_id_and_enabled_to_list_identity_provider-e0981063a2dc5961.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add ``--id`` and ``--enabled`` option to ``identity provider list`` command. \ No newline at end of file From 454b2195649aec1a9eb1fdaa60b64663cb1f7298 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 10 Jul 2020 13:14:26 +0000 Subject: [PATCH 0254/1120] Add NUMA affinity policy parameter to "port" Added port NUMA affinity policy parameter to "port create", "port set" and "port unset" commands. Change-Id: I48cacab275856af2911829f9b7176bb87fd039b3 Related-Bug: #1886798 --- openstackclient/network/v2/port.py | 33 +++++++ .../tests/unit/network/v2/fakes.py | 1 + .../tests/unit/network/v2/test_port.py | 95 +++++++++++++++++++ ...numa-affinity-policy-4706b0f9485a5d4d.yaml | 5 + 4 files changed, 134 insertions(+) create mode 100644 releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index a21324ae27..8ea1077ab3 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -158,6 +158,16 @@ def _get_attrs(client_manager, parsed_args): parsed_args.disable_uplink_status_propagation): attrs['propagate_uplink_status'] = False + if ('numa_policy_required' in parsed_args and + parsed_args.numa_policy_required): + attrs['numa_affinity_policy'] = 'required' + elif ('numa_policy_preferred' in parsed_args and + parsed_args.numa_policy_preferred): + attrs['numa_affinity_policy'] = 'preferred' + elif ('numa_policy_legacy' in parsed_args and + parsed_args.numa_policy_legacy): + attrs['numa_affinity_policy'] = 'legacy' + return attrs @@ -265,6 +275,22 @@ def _add_updatable_args(parser): help=_("Set DNS name for this port " "(requires DNS integration extension)") ) + numa_affinity_policy_group = parser.add_mutually_exclusive_group() + numa_affinity_policy_group.add_argument( + '--numa-policy-required', + action='store_true', + help=_("NUMA affinity policy required to schedule this port") + ) + numa_affinity_policy_group.add_argument( + '--numa-policy-preferred', + action='store_true', + help=_("NUMA affinity policy preferred to schedule this port") + ) + numa_affinity_policy_group.add_argument( + '--numa-policy-legacy', + action='store_true', + help=_("NUMA affinity policy using legacy mode to schedule this port") + ) # TODO(abhiraut): Use the SDK resource mapped attribute names once the @@ -904,6 +930,11 @@ def get_parser(self, prog_name): action='store_true', help=_("Clear existing information of data plane status") ) + parser.add_argument( + '--numa-policy', + action='store_true', + help=_("Clear existing NUMA affinity policy") + ) _tag.add_tag_option_to_parser_for_unset(parser, _('port')) @@ -959,6 +990,8 @@ def take_action(self, parsed_args): attrs['qos_policy_id'] = None if parsed_args.data_plane_status: attrs['data_plane_status'] = None + if parsed_args.numa_policy: + attrs['numa_affinity_policy'] = None if attrs: client.update_port(obj, **attrs) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index cef0a11c91..3df4042cba 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -634,6 +634,7 @@ def create_one_port(attrs=None): 'mac_address': 'fa:16:3e:a9:4e:72', 'name': 'port-name-' + uuid.uuid4().hex, 'network_id': 'network-id-' + uuid.uuid4().hex, + 'numa_affinity_policy': 'required', 'port_security_enabled': True, 'security_group_ids': [], 'status': 'ACTIVE', diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 87aea61fac..70fa063dce 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -59,6 +59,7 @@ def _get_common_cols_data(fake_port): 'mac_address', 'name', 'network_id', + 'numa_affinity_policy', 'port_security_enabled', 'project_id', 'qos_network_policy_id', @@ -90,6 +91,7 @@ def _get_common_cols_data(fake_port): fake_port.mac_address, fake_port.name, fake_port.network_id, + fake_port.numa_affinity_policy, fake_port.port_security_enabled, fake_port.project_id, fake_port.qos_network_policy_id, @@ -655,6 +657,50 @@ def test_create_port_with_extra_dhcp_option(self): 'name': 'test-port', }) + def _test_create_with_numa_affinity_policy(self, policy=None): + arglist = [ + '--network', self._port.network_id, + 'test-port', + ] + if policy: + arglist += ['--numa-policy-%s' % policy] + + numa_affinity_policy = None if not policy else policy + verifylist = [ + ('network', self._port.network_id,), + ('name', 'test-port'), + ] + if policy: + verifylist.append(('numa_policy_%s' % policy, True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + create_args = { + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'name': 'test-port', + } + if numa_affinity_policy: + create_args['numa_affinity_policy'] = numa_affinity_policy + self.network.create_port.assert_called_once_with(**create_args) + + self.assertEqual(self.columns, columns) + self.assertItemEqual(self.data, data) + + def test_create_with_numa_affinity_policy_required(self): + self._test_create_with_numa_affinity_policy(policy='required') + + def test_create_with_numa_affinity_policy_preferred(self): + self._test_create_with_numa_affinity_policy(policy='preferred') + + def test_create_with_numa_affinity_policy_legacy(self): + self._test_create_with_numa_affinity_policy(policy='legacy') + + def test_create_with_numa_affinity_policy_null(self): + self._test_create_with_numa_affinity_policy() + class TestDeletePort(TestPort): @@ -1668,6 +1714,32 @@ def test_set_with_tags(self): def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) + def _test_create_with_numa_affinity_policy(self, policy): + arglist = [ + '--numa-policy-%s' % policy, + self._port.id, + ] + verifylist = [ + ('numa_policy_%s' % policy, True), + ('port', self._port.id,) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.network.update_port.assert_called_once_with( + self._port, **{'numa_affinity_policy': policy}) + + def test_create_with_numa_affinity_policy_required(self): + self._test_create_with_numa_affinity_policy('required') + + def test_create_with_numa_affinity_policy_preferred(self): + self._test_create_with_numa_affinity_policy('preferred') + + def test_create_with_numa_affinity_policy_legacy(self): + self._test_create_with_numa_affinity_policy('legacy') + class TestShowPort(TestPort): @@ -1923,3 +1995,26 @@ def test_unset_with_tags(self): def test_unset_with_all_tag(self): self._test_unset_tags(with_tags=False) + + def test_unset_numa_affinity_policy(self): + _fake_port = network_fakes.FakePort.create_one_port( + {'numa_affinity_policy': 'required'}) + self.network.find_port = mock.Mock(return_value=_fake_port) + arglist = [ + '--numa-policy', + _fake_port.name, + ] + verifylist = [ + ('numa_policy', True), + ('port', _fake_port.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'numa_affinity_policy': None, + } + + self.network.update_port.assert_called_once_with(_fake_port, **attrs) + self.assertIsNone(result) diff --git a/releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml b/releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml new file mode 100644 index 0000000000..0fbc54b187 --- /dev/null +++ b/releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add NUMA affinity policy to ``port create``, ``port set`` and + ``port unset`` commands. From e24673267093de85beee753860cda1fb224ce4bc Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Thu, 9 Jul 2020 17:07:52 -0500 Subject: [PATCH 0255/1120] Bypass user and group verification in RemoveRole Keystone let's users remove role assignments that reference non-existent users and groups. This is nice when keystone backs to an identity store like LDAP and users or groups are removed. Previously, openstackclient would validate the user and group existed in keystone before sending the request to delete the role assignment. This commit updates the code to bypass that validation so that users can use IDs to forcibly cleanup role assignments. Change-Id: I102b41677736bbe37a82abaa3c5b3e1faf2475d5 Story: 2006635 Task: 36848 --- openstackclient/identity/v3/role.py | 68 ++--- .../tests/unit/identity/v3/test_role.py | 242 ++++++++++++++++++ .../notes/bug-2006635-3110f7a87a186e62.yaml | 7 + 3 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 980ebf1165..a674564fe0 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -64,59 +64,61 @@ def _add_identity_and_resource_options_to_parser(parser): def _process_identity_and_resource_options(parsed_args, - identity_client_manager): + identity_client_manager, + validate_actor_existence=True): + + def _find_user(): + try: + return common.find_user( + identity_client_manager, + parsed_args.user, + parsed_args.user_domain + ).id + except exceptions.CommandError: + if not validate_actor_existence: + return parsed_args.user + raise + + def _find_group(): + try: + return common.find_group( + identity_client_manager, + parsed_args.group, + parsed_args.group_domain + ).id + except exceptions.CommandError: + if not validate_actor_existence: + return parsed_args.group + raise + kwargs = {} if parsed_args.user and parsed_args.system: - kwargs['user'] = common.find_user( - identity_client_manager, - parsed_args.user, - parsed_args.user_domain, - ).id + kwargs['user'] = _find_user() kwargs['system'] = parsed_args.system elif parsed_args.user and parsed_args.domain: - kwargs['user'] = common.find_user( - identity_client_manager, - parsed_args.user, - parsed_args.user_domain, - ).id + kwargs['user'] = _find_user() kwargs['domain'] = common.find_domain( identity_client_manager, parsed_args.domain, ).id elif parsed_args.user and parsed_args.project: - kwargs['user'] = common.find_user( - identity_client_manager, - parsed_args.user, - parsed_args.user_domain, - ).id + kwargs['user'] = _find_user() kwargs['project'] = common.find_project( identity_client_manager, parsed_args.project, parsed_args.project_domain, ).id elif parsed_args.group and parsed_args.system: - kwargs['group'] = common.find_group( - identity_client_manager, - parsed_args.group, - parsed_args.group_domain, - ).id + kwargs['group'] = _find_group() kwargs['system'] = parsed_args.system elif parsed_args.group and parsed_args.domain: - kwargs['group'] = common.find_group( - identity_client_manager, - parsed_args.group, - parsed_args.group_domain, - ).id + kwargs['group'] = _find_group() kwargs['domain'] = common.find_domain( identity_client_manager, parsed_args.domain, ).id elif parsed_args.group and parsed_args.project: - kwargs['group'] = common.find_group( - identity_client_manager, - parsed_args.group, - parsed_args.group_domain, - ).id + kwargs['group'] = _find_group() kwargs['project'] = common.find_project( identity_client_manager, parsed_args.project, @@ -340,7 +342,9 @@ def take_action(self, parsed_args): ) kwargs = _process_identity_and_resource_options( - parsed_args, self.app.client_manager.identity) + parsed_args, self.app.client_manager.identity, + validate_actor_existence=False + ) identity_client.roles.revoke(role.id, **kwargs) diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index c1e91f9a0f..774b2c2b5f 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -19,6 +19,7 @@ from osc_lib import exceptions from osc_lib import utils +from openstackclient.identity import common from openstackclient.identity.v3 import role from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -846,6 +847,47 @@ def test_role_remove_user_system(self): ) self.assertIsNone(result) + @mock.patch.object(common, 'find_user') + def test_role_remove_non_existent_user_system(self, find_mock): + # Simulate the user not being in keystone, the client should gracefully + # handle this exception and send the request to remove the role since + # keystone supports removing role assignments with non-existent actors + # (e.g., users or groups). + find_mock.side_effect = exceptions.CommandError + + arglist = [ + '--user', identity_fakes.user_id, + '--system', 'all', + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', identity_fakes.user_id), + ('group', None), + ('system', 'all'), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'system': 'all', + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_user_domain(self): arglist = [ '--user', identity_fakes.user_name, @@ -879,6 +921,46 @@ def test_role_remove_user_domain(self): ) self.assertIsNone(result) + @mock.patch.object(common, 'find_user') + def test_role_remove_non_existent_user_domain(self, find_mock): + # Simulate the user not being in keystone, the client the gracefully + # handle this exception and send the request to remove the role since + # keystone will validate. + find_mock.side_effect = exceptions.CommandError + + arglist = [ + '--user', identity_fakes.user_id, + '--domain', identity_fakes.domain_name, + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', identity_fakes.user_id), + ('group', None), + ('system', None), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'domain': identity_fakes.domain_id, + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_user_project(self): arglist = [ '--user', identity_fakes.user_name, @@ -912,6 +994,46 @@ def test_role_remove_user_project(self): ) self.assertIsNone(result) + @mock.patch.object(common, 'find_user') + def test_role_remove_non_existent_user_project(self, find_mock): + # Simulate the user not being in keystone, the client the gracefully + # handle this exception and send the request to remove the role since + # keystone will validate. + find_mock.side_effect = exceptions.CommandError + + arglist = [ + '--user', identity_fakes.user_id, + '--project', identity_fakes.project_name, + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', identity_fakes.user_id), + ('group', None), + ('system', None), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'project': identity_fakes.project_id, + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_group_system(self): arglist = [ '--group', identity_fakes.group_name, @@ -947,6 +1069,46 @@ def test_role_remove_group_system(self): ) self.assertIsNone(result) + @mock.patch.object(common, 'find_group') + def test_role_remove_non_existent_group_system(self, find_mock): + # Simulate the user not being in keystone, the client the gracefully + # handle this exception and send the request to remove the role since + # keystone will validate. + find_mock.side_effect = exceptions.CommandError + + arglist = [ + '--group', identity_fakes.group_id, + '--system', 'all', + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', None), + ('group', identity_fakes.group_id), + ('system', 'all'), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'system': 'all', + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_group_domain(self): arglist = [ '--group', identity_fakes.group_name, @@ -981,6 +1143,46 @@ def test_role_remove_group_domain(self): ) self.assertIsNone(result) + @mock.patch.object(common, 'find_group') + def test_role_remove_non_existent_group_domain(self, find_mock): + # Simulate the user not being in keystone, the client the gracefully + # handle this exception and send the request to remove the role since + # keystone will validate. + find_mock.side_effect = exceptions.CommandError + + arglist = [ + '--group', identity_fakes.group_id, + '--domain', identity_fakes.domain_name, + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', None), + ('group', identity_fakes.group_id), + ('system', None), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'domain': identity_fakes.domain_id, + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_group_project(self): arglist = [ '--group', identity_fakes.group_name, @@ -1014,6 +1216,46 @@ def test_role_remove_group_project(self): ) self.assertIsNone(result) + @mock.patch.object(common, 'find_group') + def test_role_remove_non_existent_group_project(self, find_mock): + # Simulate the user not being in keystone, the client the gracefully + # handle this exception and send the request to remove the role since + # keystone will validate. + find_mock.side_effect = exceptions.CommandError + + arglist = [ + '--group', identity_fakes.group_id, + '--project', identity_fakes.project_name, + identity_fakes.role_name + ] + if self._is_inheritance_testcase(): + arglist.append('--inherited') + verifylist = [ + ('user', None), + ('group', identity_fakes.group_id), + ('system', None), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', identity_fakes.role_name), + ('inherited', self._is_inheritance_testcase()), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'project': identity_fakes.project_id, + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + self.assertIsNone(result) + def test_role_remove_domain_role_on_group_domain(self): self.roles_mock.get.return_value = fakes.FakeResource( None, diff --git a/releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml b/releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml new file mode 100644 index 0000000000..ecc58c8a3b --- /dev/null +++ b/releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + You can now remove role assignments from keystone that reference non-existent + users or groups. + + [Bug `2006635 `_] From ed6d8d941104c60f447de852582eb9388b9d2e73 Mon Sep 17 00:00:00 2001 From: Lewis Denny Date: Thu, 20 Aug 2020 20:18:42 +1000 Subject: [PATCH 0256/1120] Add API check for server_groups.list The policies parameter has been replaced with the policy parameter since Nova API version 2.64[1] This commit adds a check to make sure the correct parameter is used. [1]https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59 Change-Id: Ia37beb7790884d6d15bec45074f446e64af1a2aa Story: #2008041 Task: #40703 --- openstackclient/compute/v2/server_group.py | 9 +- .../tests/unit/compute/v2/fakes.py | 35 ++++++- .../unit/compute/v2/test_server_group.py | 98 +++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index c49a552f2c..cb6a503f6c 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -17,6 +17,7 @@ import logging +from novaclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -136,11 +137,15 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute data = compute_client.server_groups.list(parsed_args.all_projects) + policy_key = 'Policies' + if compute_client.api_version >= api_versions.APIVersion("2.64"): + policy_key = 'Policy' + if parsed_args.long: column_headers = columns = ( 'ID', 'Name', - 'Policies', + policy_key, 'Members', 'Project Id', 'User Id', @@ -149,7 +154,7 @@ def take_action(self, parsed_args): column_headers = columns = ( 'ID', 'Name', - 'Policies', + policy_key, ) return (column_headers, diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 6e12f735d4..3143098474 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1244,7 +1244,7 @@ class FakeServerGroup(object): """Fake one server group""" @staticmethod - def create_one_server_group(attrs=None): + def _create_one_server_group(attrs=None): """Create a fake server group :param Dictionary attrs: @@ -1261,7 +1261,6 @@ def create_one_server_group(attrs=None): 'members': [], 'metadata': {}, 'name': 'server-group-name-' + uuid.uuid4().hex, - 'policies': [], 'project_id': 'server-group-project-id-' + uuid.uuid4().hex, 'user_id': 'server-group-user-id-' + uuid.uuid4().hex, } @@ -1274,6 +1273,38 @@ def create_one_server_group(attrs=None): loaded=True) return server_group + @staticmethod + def create_one_server_group(attrs=None): + """Create a fake server group + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + if attrs is None: + attrs = {} + attrs.setdefault('policies', ['policy1', 'policy2']) + return FakeServerGroup._create_one_server_group(attrs) + + +class FakeServerGroupV264(object): + """Fake one server group fo API >= 2.64""" + + @staticmethod + def create_one_server_group(attrs=None): + """Create a fake server group + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + if attrs is None: + attrs = {} + attrs.setdefault('policy', 'policy1') + return FakeServerGroup._create_one_server_group(attrs) + class FakeUsage(object): """Fake one or more usage.""" diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 9cd876ead1..4e20ed4231 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -15,6 +15,7 @@ from unittest import mock +from novaclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -53,6 +54,33 @@ def setUp(self): self.server_groups_mock.reset_mock() +class TestServerGroupV264(TestServerGroup): + + fake_server_group = \ + compute_fakes.FakeServerGroupV264.create_one_server_group() + + columns = ( + 'id', + 'members', + 'name', + 'policy', + 'project_id', + 'user_id', + ) + + data = ( + fake_server_group.id, + utils.format_list(fake_server_group.members), + fake_server_group.name, + fake_server_group.policy, + fake_server_group.project_id, + fake_server_group.user_id, + ) + + def setUp(self): + super(TestServerGroupV264, self).setUp() + + class TestServerGroupCreate(TestServerGroup): def setUp(self): @@ -230,6 +258,76 @@ def test_server_group_list_with_all_projects_and_long(self): self.assertEqual(self.list_data_long, tuple(data)) +class TestServerGroupListV264(TestServerGroupV264): + + list_columns = ( + 'ID', + 'Name', + 'Policy', + ) + + list_columns_long = ( + 'ID', + 'Name', + 'Policy', + 'Members', + 'Project Id', + 'User Id', + ) + + list_data = (( + TestServerGroupV264.fake_server_group.id, + TestServerGroupV264.fake_server_group.name, + TestServerGroupV264.fake_server_group.policy, + ),) + + list_data_long = (( + TestServerGroupV264.fake_server_group.id, + TestServerGroupV264.fake_server_group.name, + TestServerGroupV264.fake_server_group.policy, + utils.format_list(TestServerGroupV264.fake_server_group.members), + TestServerGroupV264.fake_server_group.project_id, + TestServerGroupV264.fake_server_group.user_id, + ),) + + def setUp(self): + super(TestServerGroupListV264, self).setUp() + + self.server_groups_mock.list.return_value = [self.fake_server_group] + self.cmd = server_group.ListServerGroup(self.app, None) + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.64') + + def test_server_group_list(self): + arglist = [] + verifylist = [ + ('all_projects', False), + ('long', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.list.assert_called_once_with(False) + + self.assertEqual(self.list_columns, columns) + self.assertEqual(self.list_data, tuple(data)) + + def test_server_group_list_with_all_projects_and_long(self): + arglist = [ + '--all-projects', + '--long', + ] + verifylist = [ + ('all_projects', True), + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.list.assert_called_once_with(True) + + self.assertEqual(self.list_columns_long, columns) + self.assertEqual(self.list_data_long, tuple(data)) + + class TestServerGroupShow(TestServerGroup): def setUp(self): From 51a1ea65f4d095b073381200e5268f909bf360de Mon Sep 17 00:00:00 2001 From: Lewis Denny Date: Wed, 17 Jun 2020 15:34:09 +1000 Subject: [PATCH 0257/1120] Add API check for server_groups.create The policies field has been replaced with the policy field since Nova API version 2.64[1] This commit adds a check to make sure the correct field is used. [1]https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59 Change-Id: I06d3211937d822c26070b7f8ad757c365dcbb1bb Story: #2007822 Task: #40101 --- openstackclient/compute/v2/server_group.py | 9 ++++++-- .../unit/compute/v2/test_server_group.py | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index c49a552f2c..9bc2bb432d 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -17,6 +17,7 @@ import logging +from novaclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -67,9 +68,13 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute info = {} + + policy_arg = {'policies': [parsed_args.policy]} + if compute_client.api_version >= api_versions.APIVersion("2.64"): + policy_arg = {'policy': parsed_args.policy} server_group = compute_client.server_groups.create( - name=parsed_args.name, - policies=[parsed_args.policy]) + name=parsed_args.name, **policy_arg) + info.update(server_group._info) columns = _get_columns(info) diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 9cd876ead1..b9bb79f620 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -15,6 +15,7 @@ from unittest import mock +from novaclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -80,6 +81,28 @@ def test_server_group_create(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_server_group_create_v264(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.64') + + arglist = [ + '--policy', 'soft-anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'soft-anti-affinity'), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.create.assert_called_once_with( + name=parsed_args.name, + policy=parsed_args.policy, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestServerGroupDelete(TestServerGroup): From 4a3c5207c1b2d628d1ec8b30e28f56e5956bd2e0 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 27 Jul 2020 22:04:47 +0000 Subject: [PATCH 0258/1120] Show words indicating booted from volume for server image For a server booted from a volume, nova API does not store an image_id and instead returns an empty string. Currently, openstackclient similarly shows an empty string for Image Name and Image ID for servers booted from volumes. To aid CLI users in understanding the meaning of no image_id, we can display the string "N/A (booted from volume)" in the image field if the server was booted from a volume. Change-Id: I9c62cf6fe23b2e934dcbf5ebbf706b2705d2e424 --- openstackclient/compute/v2/server.py | 16 ++++++++++++++-- .../functional/compute/v2/test_server.py | 19 +++++++++++++++++-- .../tests/unit/compute/v2/test_server.py | 8 ++++---- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966ae..ff40565a6b 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -38,6 +38,8 @@ LOG = logging.getLogger(__name__) +IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)' + def _format_servers_list_networks(networks): """Return a formatted string of a server's networks @@ -147,6 +149,12 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): info['image'] = "%s (%s)" % (image.name, image_id) except Exception: info['image'] = image_id + else: + # NOTE(melwitt): An server booted from a volume will have no image + # associated with it. We fill in the image with "N/A (booted from + # volume)" to help users who want to be able to grep for + # boot-from-volume servers when using the CLI. + info['image'] = IMAGE_STRING_FOR_BFV # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) @@ -1520,8 +1528,12 @@ def take_action(self, parsed_args): s.image_name = image.name s.image_id = s.image['id'] else: - s.image_name = '' - s.image_id = '' + # NOTE(melwitt): An server booted from a volume will have no + # image associated with it. We fill in the Image Name and ID + # with "N/A (booted from volume)" to help users who want to be + # able to grep for boot-from-volume servers when using the CLI. + s.image_name = IMAGE_STRING_FOR_BFV + s.image_id = IMAGE_STRING_FOR_BFV if 'id' in s.flavor: flavor = flavors.get(s.flavor['id']) if flavor: diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 6e080e9ba2..3123057c8e 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -16,6 +16,7 @@ from tempest.lib import exceptions +from openstackclient.compute.v2 import server as v2_server from openstackclient.tests.functional.compute.v2 import common from openstackclient.tests.functional.volume.v2 import common as volume_common @@ -509,6 +510,20 @@ def test_server_boot_from_volume(self): server['name'], ) + # check that image indicates server "booted from volume" + self.assertEqual( + v2_server.IMAGE_STRING_FOR_BFV, + server['image'], + ) + # check server list too + servers = json.loads(self.openstack( + 'server list -f json' + )) + self.assertEqual( + v2_server.IMAGE_STRING_FOR_BFV, + servers[0]['Image'] + ) + # check volumes cmd_output = json.loads(self.openstack( 'volume show -f json ' + @@ -779,8 +794,8 @@ def test_boot_from_volume(self): self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id) # Since the server is volume-backed the GET /servers/{server_id} - # response will have image=''. - self.assertEqual('', cmd_output['image']) + # response will have image='N/A (booted from volume)'. + self.assertEqual(v2_server.IMAGE_STRING_FOR_BFV, cmd_output['image']) # check the volume that attached on server cmd_output = json.loads(self.openstack( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c50c..405e05d160 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2609,7 +2609,7 @@ def setUp(self): s.status, server._format_servers_list_networks(s.networks), # Image will be an empty string if boot-from-volume - self.image.name if s.image else s.image, + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, )) self.data_long.append(( @@ -2622,8 +2622,8 @@ def setUp(self): ), server._format_servers_list_networks(s.networks), # Image will be an empty string if boot-from-volume - self.image.name if s.image else s.image, - s.image['id'] if s.image else s.image, + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, s.flavor['id'], getattr(s, 'OS-EXT-AZ:availability_zone'), @@ -2636,7 +2636,7 @@ def setUp(self): s.status, server._format_servers_list_networks(s.networks), # Image will be an empty string if boot-from-volume - s.image['id'] if s.image else s.image, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, s.flavor['id'] )) From 58f1c90971969ee12089c2e5aeea4993d8c095d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Weing=C3=A4rtner?= Date: Wed, 2 Sep 2020 17:26:00 -0300 Subject: [PATCH 0259/1120] Add source_ip_prefix and destination_ip_prefix to metering label rules As proposed in the RFE and then approved in the spec, we are adding to the neutron metering rules two new parameters. The source IP prefix, and destination IP prefix. Partially-Implements: https://bugs.launchpad.net/neutron/+bug/1889431 RFE: https://bugs.launchpad.net/neutron/+bug/1889431 Depends-On: https://review.opendev.org/#/c/746586/ Change-Id: Ic44d88fabea0fffef2279f2f2c3d2b1da6426d4d --- .../network/v2/network_meter_rule.py | 22 ++++++++++++++++++- .../tests/unit/network/v2/fakes.py | 2 ++ .../network/v2/test_network_meter_rule.py | 12 ++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index 49ff9e1b0e..1cf0395f44 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -46,6 +46,10 @@ def _get_attrs(client_manager, parsed_args): attrs['direction'] = 'egress' if parsed_args.remote_ip_prefix is not None: attrs['remote_ip_prefix'] = parsed_args.remote_ip_prefix + if parsed_args.source_ip_prefix is not None: + attrs['source_ip_prefix'] = parsed_args.source_ip_prefix + if parsed_args.destination_ip_prefix is not None: + attrs['destination_ip_prefix'] = parsed_args.destination_ip_prefix if parsed_args.meter is not None: attrs['metering_label_id'] = parsed_args.meter if parsed_args.project is not None: @@ -97,9 +101,21 @@ def get_parser(self, prog_name): parser.add_argument( '--remote-ip-prefix', metavar='', - required=True, + required=False, help=_('The remote IP prefix to associate with this rule'), ) + parser.add_argument( + '--source-ip-prefix', + metavar='', + required=False, + help=_('The source IP prefix to associate with this rule'), + ) + parser.add_argument( + '--destination-ip-prefix', + metavar='', + required=False, + help=_('The destination IP prefix to associate with this rule'), + ) parser.add_argument( 'meter', metavar='', @@ -168,12 +184,16 @@ def take_action(self, parsed_args): 'excluded', 'direction', 'remote_ip_prefix', + 'source_ip_prefix', + 'destination_ip_prefix', ) column_headers = ( 'ID', 'Excluded', 'Direction', 'Remote IP Prefix', + 'Source IP Prefix', + 'Destination IP Prefix', ) data = client.metering_label_rules() return (column_headers, diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index cef0a11c91..5095cb9125 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1590,6 +1590,8 @@ def create_one_rule(attrs=None): 'excluded': False, 'metering_label_id': 'meter-label-id-' + uuid.uuid4().hex, 'remote_ip_prefix': '10.0.0.0/24', + 'source_ip_prefix': '8.8.8.8/32', + 'destination_ip_prefix': '10.0.0.0/24', 'tenant_id': 'project-id-' + uuid.uuid4().hex, } diff --git a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py index 8f8922c020..e9224fa650 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py @@ -42,20 +42,24 @@ class TestCreateMeterRule(TestMeterRule): ) columns = ( + 'destination_ip_prefix', 'direction', 'excluded', 'id', 'metering_label_id', 'project_id', 'remote_ip_prefix', + 'source_ip_prefix', ) data = ( + new_rule.destination_ip_prefix, new_rule.direction, new_rule.excluded, new_rule.id, new_rule.metering_label_id, new_rule.project_id, new_rule.remote_ip_prefix, + new_rule.source_ip_prefix, ) def setUp(self): @@ -228,6 +232,8 @@ class TestListMeterRule(TestMeterRule): 'Excluded', 'Direction', 'Remote IP Prefix', + 'Source IP Prefix', + 'Destination IP Prefix' ) data = [] @@ -238,6 +244,8 @@ class TestListMeterRule(TestMeterRule): rule.excluded, rule.direction, rule.remote_ip_prefix, + rule.source_ip_prefix, + rule.destination_ip_prefix )) def setUp(self): @@ -270,21 +278,25 @@ class TestShowMeterRule(TestMeterRule): ) columns = ( + 'destination_ip_prefix', 'direction', 'excluded', 'id', 'metering_label_id', 'project_id', 'remote_ip_prefix', + 'source_ip_prefix', ) data = ( + new_rule.destination_ip_prefix, new_rule.direction, new_rule.excluded, new_rule.id, new_rule.metering_label_id, new_rule.project_id, new_rule.remote_ip_prefix, + new_rule.source_ip_prefix, ) def setUp(self): From 67700e6dd95b0f58b2f01cc816620819a4a6deae Mon Sep 17 00:00:00 2001 From: Miguel Lavalle Date: Sun, 26 Apr 2020 17:40:17 -0500 Subject: [PATCH 0260/1120] Support tagging Neutron ports on creation This change adds support for tagging ports on creation Co-Authored-By: Slawek Kaplonski Change-Id: I3148a568664588eb2f529138f984859570c0fca1 Related-Bug: #1815933 --- .zuul.yaml | 1 + openstackclient/network/v2/port.py | 15 ++++- .../tests/unit/network/v2/test_port.py | 58 ++++++++++++++----- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index e566ceb845..c04f3446d5 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -91,6 +91,7 @@ neutron-segments: true q-metering: true q-qos: true + neutron-tag-ports-during-bulk-creation: true tox_envlist: functional - job: diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index a21324ae27..be3e1709ff 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -454,12 +454,23 @@ def take_action(self, parsed_args): if parsed_args.qos_policy: attrs['qos_policy_id'] = client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False).id + + set_tags_in_post = bool( + client.find_extension('tag-ports-during-bulk-creation')) + if set_tags_in_post: + if parsed_args.no_tag: + attrs['tags'] = [] + if parsed_args.tags: + attrs['tags'] = list(set(parsed_args.tags)) + with common.check_missing_extension_if_error( self.app.client_manager.network, attrs): obj = client.create_port(**attrs) - # tags cannot be set when created, so tags need to be set later. - _tag.update_tags_for_set(client, obj, parsed_args) + if not set_tags_in_post: + # tags cannot be set when created, so tags need to be set later. + _tag.update_tags_for_set(client, obj, parsed_args) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 87aea61fac..a3d0c517d7 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -119,6 +119,7 @@ def setUp(self): self.network.find_network = mock.Mock(return_value=fake_net) self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + self.network.find_extension = mock.Mock(return_value=[]) # Get the command object to test self.cmd = port.CreatePort(self.app, self.namespace) @@ -534,7 +535,7 @@ def test_create_port_security_disabled(self): 'name': 'test-port', }) - def _test_create_with_tag(self, add_tags=True): + def _test_create_with_tag(self, add_tags=True, add_tags_in_post=True): arglist = [ '--network', self._port.network_id, 'test-port', @@ -553,28 +554,59 @@ def _test_create_with_tag(self, add_tags=True): else: verifylist.append(('no_tag', True)) + self.network.find_extension = mock.Mock(return_value=add_tags_in_post) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = (self.cmd.take_action(parsed_args)) - self.network.create_port.assert_called_once_with( - admin_state_up=True, - network_id=self._port.network_id, - name='test-port' - ) - if add_tags: - self.network.set_tags.assert_called_once_with( - self._port, - tests_utils.CompareBySet(['red', 'blue'])) + args = { + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'name': 'test-port', + } + if add_tags_in_post: + if add_tags: + args['tags'] = sorted(['red', 'blue']) + else: + args['tags'] = [] + self.network.create_port.assert_called_once() + # Now we need to verify if arguments to call create_port are as + # expected, + # But we can't simply use assert_called_once_with() method because + # duplicates from 'tags' are removed with + # list(set(parsed_args.tags)) and that don't quarantee order of + # tags list which is used to call create_port(). + create_port_call_kwargs = self.network.create_port.call_args[1] + create_port_call_kwargs['tags'] = sorted( + create_port_call_kwargs['tags']) + self.assertDictEqual(args, create_port_call_kwargs) else: - self.assertFalse(self.network.set_tags.called) + self.network.create_port.assert_called_once_with( + admin_state_up=True, + network_id=self._port.network_id, + name='test-port' + ) + if add_tags: + self.network.set_tags.assert_called_once_with( + self._port, + tests_utils.CompareBySet(['red', 'blue'])) + else: + self.assertFalse(self.network.set_tags.called) + self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) def test_create_with_tags(self): - self._test_create_with_tag(add_tags=True) + self._test_create_with_tag(add_tags=True, add_tags_in_post=True) def test_create_with_no_tag(self): - self._test_create_with_tag(add_tags=False) + self._test_create_with_tag(add_tags=False, add_tags_in_post=True) + + def test_create_with_tags_using_put(self): + self._test_create_with_tag(add_tags=True, add_tags_in_post=False) + + def test_create_with_no_tag_using_put(self): + self._test_create_with_tag(add_tags=False, add_tags_in_post=False) def _test_create_with_uplink_status_propagation(self, enable=True): arglist = [ From f0642bc05f202395b6e2f7a4f7b0e10155b7c1f8 Mon Sep 17 00:00:00 2001 From: likui Date: Tue, 8 Sep 2020 15:44:51 +0800 Subject: [PATCH 0261/1120] Update developing.rst Use unittest.mock instead of mock Change-Id: Ib573e9d217b4f18ef4e7ba3ab581164be423cb26 --- doc/source/contributor/developing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index 5b859199bf..35c7c7b91f 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -199,7 +199,6 @@ Example import copy import fixtures - import mock import os from osc_lib.api import auth @@ -208,4 +207,5 @@ Example from openstackclient import shell from openstackclient.tests import utils + from unittest import mock From fbd2c00b8999202917671bcdb39298eb39c3ad47 Mon Sep 17 00:00:00 2001 From: Myeongchul Chae Date: Sun, 16 Aug 2020 13:44:45 +0000 Subject: [PATCH 0262/1120] Fix --image-property option in 'create server' There was a problem that the '-image-property' option, which can be used to create an instance, did not work as intended. I found that there were two problems with this option. First, I cannot select an image as its metadata. The second is that when there are multiple images available, the desired image may not be selected depending on the situation. This patch solves these two problems. I wrote the test case with these two problems considered together. Change-Id: Ib2745d7e067056ff4ca8bfaf6cff492d0dacb73a story: #2007860 --- openstackclient/compute/v2/server.py | 14 ++++- .../tests/unit/compute/v2/test_server.py | 59 +++++++++++++++++++ ...-property-field.yaml-c51bf37c3106d6ff.yaml | 6 ++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966ae..d7b01a0fb3 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -751,19 +751,27 @@ def _match_image(image_api, wanted_properties): images_matched = [] for img in image_list: img_dict = {} + # exclude any unhashable entries - for key, value in img.items(): + img_dict_items = list(img.items()) + if img.properties: + img_dict_items.extend(list(img.properties.items())) + for key, value in img_dict_items: try: set([key, value]) except TypeError: + if key != 'properties': + LOG.debug('Skipped the \'%s\' attribute. ' + 'That cannot be compared. ' + '(image: %s, value: %s)', + key, img.id, value) pass else: img_dict[key] = value + if all(k in img_dict and img_dict[k] == v for k, v in wanted_properties.items()): images_matched.append(img) - else: - return [] return images_matched images = _match_image(image_client, parsed_args.image_property) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c50c..6c9497b55c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2048,6 +2048,65 @@ def test_server_create_image_property_missed(self): self.cmd.take_action, parsed_args) + def test_server_create_image_property_with_image_list(self): + arglist = [ + '--image-property', + 'owner_specified.openstack.object=image/cirros', + '--flavor', 'flavor1', + '--nic', 'none', + self.new_server.name, + ] + + verifylist = [ + ('image_property', + {'owner_specified.openstack.object': 'image/cirros'}), + ('flavor', 'flavor1'), + ('nic', ['none']), + ('server_name', self.new_server.name), + ] + # create a image_info as the side_effect of the fake image_list() + image_info = { + 'properties': { + 'owner_specified.openstack.object': 'image/cirros' + } + } + + target_image = image_fakes.FakeImage.create_one_image(image_info) + another_image = image_fakes.FakeImage.create_one_image({}) + self.images_mock.return_value = [target_image, another_image] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='none', + meta=None, + scheduler_hints={}, + config_drive=None, + ) + + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + target_image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + def test_server_create_invalid_hint(self): # Not a key-value pair arglist = [ diff --git a/releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml b/releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml new file mode 100644 index 0000000000..cf082f45c8 --- /dev/null +++ b/releasenotes/notes/properties-with-image-property-field.yaml-c51bf37c3106d6ff.yaml @@ -0,0 +1,6 @@ +--- +features: + - Support for image search via properties of image. Currently + "openstack server create --image-property" only takes image property. + Now it can also search image via properties (user defined) too. + Story https://storyboard.openstack.org/#!/story/2007860. From 99b5adf9c65d3cd2e2aa8c40c08a478fae0a49f3 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 10 Sep 2020 18:39:54 +0200 Subject: [PATCH 0263/1120] Fix gate due to switch to focal In focal we do not have libffi6. cffi and greenlet versions in lower-contraints are too old. Change-Id: Iab3634039845adb649c7fd69d1812b405a61433c --- bindep.txt | 1 - lower-constraints.txt | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bindep.txt b/bindep.txt index cf94d2a8cb..4c90a026fe 100644 --- a/bindep.txt +++ b/bindep.txt @@ -5,7 +5,6 @@ gcc [compile test] libc6-dev [compile test platform:dpkg] libffi-devel [platform:rpm] libffi-dev [compile test platform:dpkg] -libffi6 [platform:dpkg] libssl-dev [compile test platform:dpkg] python3-dev [compile test platform:dpkg] python3-devel [compile test platform:rpm] diff --git a/lower-constraints.txt b/lower-constraints.txt index 0a453dae25..403ba4e05c 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -4,7 +4,7 @@ appdirs==1.3.0 asn1crypto==0.23.0 bandit==1.1.0 cachetools==2.0.0 -cffi==1.7.0 +cffi==1.14.0 cliff==2.8.0 cmd2==0.8.0 contextlib2==0.4.0 @@ -28,7 +28,7 @@ futurist==2.1.0 gitdb==0.6.4 GitPython==1.0.1 gnocchiclient==3.3.1 -greenlet==0.4.10 +greenlet==0.4.15 hacking==2.0.0 httplib2==0.9.1 idna==2.6 From 5aeec30701bbb0df59359b33fe107904a975e6c0 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Mon, 14 Sep 2020 10:34:02 +0800 Subject: [PATCH 0264/1120] Remove install unnecessary packages The docs and releasenotes requirements migrated to doc/requirements.txt we need not install things from requirements.txt. Change-Id: I4403cee833448beb69afaec503519d5a951f7e34 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3a2a307de8..fdff6539e8 100644 --- a/tox.ini +++ b/tox.ini @@ -111,7 +111,6 @@ commands = [testenv:docs] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html From bae89b30144fdf40d0fea31e1a595b507e32c4f3 Mon Sep 17 00:00:00 2001 From: jay Date: Thu, 4 Jun 2020 14:09:03 +0200 Subject: [PATCH 0265/1120] Output correct json for security groups in 'openstack server show' Fixes incorrect json output for 'openstack server show -f json'. The security group json output groups all the json as one for e.g. "security_groups": "name='group1'\nname='group2'" The correct output should be "security_groups" : [{"name" : "group1"}, {"name" : "group2"}] properties and volumes_attached fields also has similar issue. Story: 2007755 Change-Id: I1b1cac716329e0530400aff782c08000b21d8e1d --- openstackclient/compute/v2/server.py | 17 +++++--- .../functional/compute/v2/test_server.py | 39 ++++++++++++------- .../tests/unit/compute/v2/test_server.py | 3 ++ ...ty-grp-json-fix.yaml-2af1f48a48034d64.yaml | 4 ++ 4 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966ae..b3b0b7be15 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -24,6 +24,7 @@ from novaclient import api_versions from novaclient.v2 import servers from openstack import exceptions as sdk_exceptions +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -166,14 +167,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): if 'os-extended-volumes:volumes_attached' in info: info.update( { - 'volumes_attached': utils.format_list_of_dicts( + 'volumes_attached': format_columns.ListDictColumn( info.pop('os-extended-volumes:volumes_attached')) } ) if 'security_groups' in info: info.update( { - 'security_groups': utils.format_list_of_dicts( + 'security_groups': format_columns.ListDictColumn( info.pop('security_groups')) } ) @@ -182,9 +183,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): info['addresses'] = _format_servers_list_networks(server.networks) # Map 'metadata' field to 'properties' - info.update( - {'properties': utils.format_dict(info.pop('metadata'))} - ) + if not info['metadata']: + info.update( + {'properties': utils.format_dict(info.pop('metadata'))} + ) + else: + info.update( + {'properties': format_columns.DictColumn(info.pop('metadata'))} + ) # Migrate tenant_id to project_id naming if 'tenant_id' in info: @@ -2530,7 +2536,6 @@ def take_action(self, parsed_args): data = _prep_server_detail(compute_client, self.app.client_manager.image, server, refresh=False) - return zip(*sorted(data.items())) diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 6e080e9ba2..50c59c17a3 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -230,7 +230,7 @@ def test_server_set(self): )) # Really, shouldn't this be a list? self.assertEqual( - "a='b', c='d'", + {'a': 'b', 'c': 'd'}, cmd_output['properties'], ) @@ -244,7 +244,7 @@ def test_server_set(self): name )) self.assertEqual( - "c='d'", + {'c': 'd'}, cmd_output['properties'], ) @@ -619,8 +619,8 @@ def test_server_boot_with_bdm_snapshot(self): server_name )) volumes_attached = cmd_output['volumes_attached'] - self.assertTrue(volumes_attached.startswith('id=')) - attached_volume_id = volumes_attached.replace('id=', '') + self.assertIsNotNone(volumes_attached) + attached_volume_id = volumes_attached[0]["id"] # check the volume that attached on server cmd_output = json.loads(self.openstack( @@ -699,8 +699,8 @@ def test_server_boot_with_bdm_image(self): server_name )) volumes_attached = cmd_output['volumes_attached'] - self.assertTrue(volumes_attached.startswith('id=')) - attached_volume_id = volumes_attached.replace('id=', '') + self.assertIsNotNone(volumes_attached) + attached_volume_id = volumes_attached[0]["id"] # check the volume that attached on server cmd_output = json.loads(self.openstack( @@ -773,10 +773,12 @@ def test_boot_from_volume(self): server_name )) volumes_attached = cmd_output['volumes_attached'] - self.assertTrue(volumes_attached.startswith('id=')) - attached_volume_id = volumes_attached.replace('id=', '') - # Don't leak the volume when the test exits. - self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id) + self.assertIsNotNone(volumes_attached) + attached_volume_id = volumes_attached[0]["id"] + for vol in volumes_attached: + self.assertIsNotNone(vol['id']) + # Don't leak the volume when the test exits. + self.addCleanup(self.openstack, 'volume delete ' + vol['id']) # Since the server is volume-backed the GET /servers/{server_id} # response will have image=''. @@ -785,7 +787,7 @@ def test_boot_from_volume(self): # check the volume that attached on server cmd_output = json.loads(self.openstack( 'volume show -f json ' + - attached_volume_id + volumes_attached[0]["id"] )) # The volume size should be what we specified on the command line. self.assertEqual(1, int(cmd_output['size'])) @@ -879,14 +881,21 @@ def test_server_create_with_security_group(self): self.assertIsNotNone(server['id']) self.assertEqual(server_name, server['name']) - self.assertIn(str(security_group1['id']), server['security_groups']) - self.assertIn(str(security_group2['id']), server['security_groups']) + sec_grp = "" + for sec in server['security_groups']: + sec_grp += sec['name'] + self.assertIn(str(security_group1['id']), sec_grp) + self.assertIn(str(security_group2['id']), sec_grp) self.wait_for_status(server_name, 'ACTIVE') server = json.loads(self.openstack( 'server show -f json ' + server_name )) - self.assertIn(sg_name1, server['security_groups']) - self.assertIn(sg_name2, server['security_groups']) + # check if security group exists in list + sec_grp = "" + for sec in server['security_groups']: + sec_grp += sec['name'] + self.assertIn(sg_name1, sec_grp) + self.assertIn(sg_name2, sec_grp) def test_server_create_with_empty_network_option_latest(self): """Test server create with empty network option in nova 2.latest.""" diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c50c..933c4e7ddd 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -5166,6 +5166,8 @@ def test_prep_server_detail(self, find_resource): 'tenant_id': u'tenant-id-xxx', 'networks': {u'public': [u'10.20.30.40', u'2001:db8::f']}, 'links': u'http://xxx.yyy.com', + 'properties': '', + 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}], } _server = compute_fakes.FakeServer.create_one_server(attrs=server_info) find_resource.side_effect = [_server, _flavor] @@ -5182,6 +5184,7 @@ def test_prep_server_detail(self, find_resource): 'properties': '', 'OS-EXT-STS:power_state': server._format_servers_list_power_state( getattr(_server, 'OS-EXT-STS:power_state')), + 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}], } # Call _prep_server_detail(). diff --git a/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml b/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml new file mode 100644 index 0000000000..3a0155a1da --- /dev/null +++ b/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - The ``openstack server show -f json`` command was not outputting + json for security groups, volumes and properties properly. From cbc1fb08ec7fd335eb434606a64e9ae051ea28bf Mon Sep 17 00:00:00 2001 From: wangzihao Date: Fri, 18 Sep 2020 11:15:47 +0800 Subject: [PATCH 0266/1120] bump py37 to py38 in tox.ini in 'victoria' cycle, we should test py38 by default. and remove redundant python env. Change-Id: I6426cc55ee9b6bee96620a8185fbdb39c24a68a8 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3a2a307de8..6dd04d5b27 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.2.0 -envlist = py37,pep8 +envlist = py38,pep8 skipdist = True # Automatic envs (pyXX) will only use the python version appropriate to that # env and ignore basepython inherited from [testenv] if we set From e9bd4ef007153e4f2e2d69f3bcb94eef8e8983c2 Mon Sep 17 00:00:00 2001 From: asarfaty Date: Sun, 16 Aug 2020 09:23:56 +0200 Subject: [PATCH 0267/1120] Remove None valued network quota entries Since the openstack SDK still has the neutron-lbaas entries in the network quota, but those are already deprecated [1], the 'opentack quota show' command shows those as None value. This fix removes those empty deprecated values from the output. [1] https://review.opendev.org/#/c/658494/ Change-Id: I8dbdba2a029ea8e6a268ddf29627e1466a7e3a8a --- openstackclient/common/quota.py | 13 +++++++---- .../tests/unit/common/test_quota.py | 23 +++++++++++++++++++ .../remove-nlbaas-quota-8b38e0c91ab113cb.yaml | 4 ++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/remove-nlbaas-quota-8b38e0c91ab113cb.yaml diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 11de986b8e..643cb4e474 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -161,6 +161,13 @@ def get_volume_quota(self, client, parsed_args): raise return quota._info + def _network_quota_to_dict(self, network_quota): + 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) @@ -174,13 +181,11 @@ def get_network_quota(self, parsed_args): client = self.app.client_manager.network if default: network_quota = client.get_quota_default(project) - if type(network_quota) is not dict: - network_quota = network_quota.to_dict() + network_quota = self._network_quota_to_dict(network_quota) else: network_quota = client.get_quota(project, details=detail) - if type(network_quota) is not dict: - network_quota = network_quota.to_dict() + 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" diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 6504c5b085..8771359cb5 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -1087,3 +1087,26 @@ def test_quota_show_no_project(self): identity_fakes.project_id, details=False ) 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 diff --git a/releasenotes/notes/remove-nlbaas-quota-8b38e0c91ab113cb.yaml b/releasenotes/notes/remove-nlbaas-quota-8b38e0c91ab113cb.yaml new file mode 100644 index 0000000000..572e215b25 --- /dev/null +++ b/releasenotes/notes/remove-nlbaas-quota-8b38e0c91ab113cb.yaml @@ -0,0 +1,4 @@ +--- +other: + - | + Remove deprecated neutron-lbaas results from ``quota show`` command. From 4b709a2c044ccc7f96836baa6c33a0f800e57be1 Mon Sep 17 00:00:00 2001 From: maaoyu Date: Thu, 24 Sep 2020 17:08:56 +0800 Subject: [PATCH 0268/1120] Remove install unnecessary packages The docs requirements migrated to doc/requirements.txt we need not install things from requirements.txt. Change-Id: I35a367505b2b423c345b05519e4134113cb66648 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3a2a307de8..fdff6539e8 100644 --- a/tox.ini +++ b/tox.ini @@ -111,7 +111,6 @@ commands = [testenv:docs] deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html From b77c28d2954a2c5861a3a6a1df29d8de448f7c08 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Thu, 8 Aug 2019 19:46:30 +0800 Subject: [PATCH 0269/1120] Add server migration list CLI Add ``openstack server migration list`` to fetch server migrations. Part of blueprint add-user-id-field-to-the-migrations-table Change-Id: I15b4a5aca8d0dee59dd293e7b1c7272cdfbeea20 --- .../cli/command-objects/server-migration.rst | 12 + openstackclient/compute/v2/server.py | 161 ++++++ .../tests/unit/compute/v2/fakes.py | 90 ++++ .../tests/unit/compute/v2/test_server.py | 484 ++++++++++++++++++ ...the-migrations-table-299b99ccb1f12a1f.yaml | 5 + setup.cfg | 1 + 6 files changed, 753 insertions(+) create mode 100644 doc/source/cli/command-objects/server-migration.rst create mode 100644 releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml diff --git a/doc/source/cli/command-objects/server-migration.rst b/doc/source/cli/command-objects/server-migration.rst new file mode 100644 index 0000000000..6e2982cf26 --- /dev/null +++ b/doc/source/cli/command-objects/server-migration.rst @@ -0,0 +1,12 @@ +================ +server migration +================ + +A server migration provides a way to move an instance from one +host to another. There are four types of migration operation +supported: live migration, cold migration, resize and evacuation. + +Compute v2 + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server migration list diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966ae..05050d4a7b 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1768,6 +1768,167 @@ def _show_progress(progress): raise SystemExit +class ListMigration(command.Command): + _description = _("""List server migrations.""") + + def get_parser(self, prog_name): + parser = super(ListMigration, self).get_parser(prog_name) + parser.add_argument( + "--server", + metavar="", + dest='server', + default=None, + help=_('Server to show migration details (name or ID).') + ) + parser.add_argument( + "--host", + metavar="", + default=None, + help=_('Fetch migrations for the given host.') + ) + parser.add_argument( + "--status", + metavar="", + default=None, + help=_('Fetch migrations for the given status.') + ) + parser.add_argument( + "--marker", + metavar="", + dest='marker', + default=None, + help=_("The last migration of the previous page; displays list " + "of migrations after 'marker'. Note that the marker is " + "the migration UUID. (Supported with " + "``--os-compute-api-version`` 2.59 or greater.)") + ) + parser.add_argument( + "--limit", + metavar="", + dest='limit', + type=int, + default=None, + help=_("Maximum number of migrations to display. Note that there " + "is a configurable max limit on the server, and the limit " + "that is used will be the minimum of what is requested " + "here and what is configured in the server. " + "(Supported with ``--os-compute-api-version`` 2.59 " + "or greater.)") + ) + parser.add_argument( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_("List only migrations changed later or equal to a certain " + "point of time. The provided time should be an ISO 8061 " + "formatted time, e.g. ``2016-03-04T06:27:59Z``. " + "(Supported with ``--os-compute-api-version`` 2.59 " + "or greater.)") + ) + parser.add_argument( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_("List only migrations changed earlier or equal to a " + "certain point of time. The provided time should be an ISO " + "8061 formatted time, e.g. ``2016-03-04T06:27:59Z``. " + "(Supported with ``--os-compute-api-version`` 2.66 or " + "greater.)") + ) + parser.add_argument( + '--project', + metavar='', + dest='project_id', + default=None, + help=_("Filter the migrations by the given project ID. " + "(Supported with ``--os-compute-api-version`` 2.80 " + "or greater.)"), + ) + parser.add_argument( + '--user', + metavar='', + dest='user_id', + default=None, + help=_("Filter the migrations by the given user ID. " + "(Supported with ``--os-compute-api-version`` 2.80 " + "or greater.)"), + ) + return parser + + def print_migrations(self, parsed_args, compute_client, migrations): + columns = ['Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', + 'Server UUID', 'Old Flavor', 'New Flavor', + 'Created At', 'Updated At'] + + # Insert migrations UUID after ID + if compute_client.api_version >= api_versions.APIVersion("2.59"): + columns.insert(0, "UUID") + + # TODO(brinzhang): It also suppports filter migrations by type + # since 2.1. https://review.opendev.org/#/c/675117 supported + # filtering the migrations by 'migration_type' and 'source_compute' + # in novaclient, that will be added in OSC by follow-up. + if compute_client.api_version >= api_versions.APIVersion("2.23"): + columns.insert(0, "Id") + columns.insert(len(columns) - 2, "Type") + + if compute_client.api_version >= api_versions.APIVersion("2.80"): + if parsed_args.project_id: + columns.insert(len(columns) - 2, "Project") + if parsed_args.user_id: + columns.insert(len(columns) - 2, "User") + + columns_header = columns + return (columns_header, (utils.get_item_properties( + mig, columns) for mig in migrations)) + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + + search_opts = { + "host": parsed_args.host, + "server": parsed_args.server, + "status": parsed_args.status, + } + + if (parsed_args.marker or parsed_args.limit or + parsed_args.changes_since): + if compute_client.api_version < api_versions.APIVersion("2.59"): + msg = _("marker, limit and/or changes_since is not supported " + "for --os-compute-api-version less than 2.59") + raise exceptions.CommandError(msg) + if parsed_args.marker: + search_opts['marker'] = parsed_args.marker + if parsed_args.limit: + search_opts['limit'] = parsed_args.limit + if parsed_args.changes_since: + search_opts['changes_since'] = parsed_args.changes_since + + if parsed_args.changes_before: + if compute_client.api_version < api_versions.APIVersion("2.66"): + msg = _("changes_before is not supported for " + "--os-compute-api-version less than 2.66") + raise exceptions.CommandError(msg) + search_opts['changes_before'] = parsed_args.changes_before + + if parsed_args.project_id or parsed_args.user_id: + if compute_client.api_version < api_versions.APIVersion("2.80"): + msg = _("Project and/or user is not supported for " + "--os-compute-api-version less than 2.80") + raise exceptions.CommandError(msg) + if parsed_args.project_id: + search_opts['project_id'] = parsed_args.project_id + if parsed_args.user_id: + search_opts['user_id'] = parsed_args.user_id + + migrations = compute_client.migrations.list(**search_opts) + + return self.print_migrations(parsed_args, compute_client, migrations) + + class PauseServer(command.Command): _description = _("Pause server(s)") diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 6e12f735d4..2ad489e7cb 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -14,6 +14,7 @@ # import copy +import random from unittest import mock import uuid @@ -198,6 +199,9 @@ def __init__(self, **kwargs): self.instance_action = mock.Mock() self.instance_action.resource_class = fakes.FakeResource(None, {}) + self.migrations = mock.Mock() + self.migrations.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -1539,3 +1543,89 @@ def __init__(self, verb, uri, value, remain, self.remain = remain self.unit = unit self.next_available = next_available + + +class FakeServerMigration(object): + """Fake one or more server migrations.""" + + @staticmethod + def create_one_server_migration(attrs=None, methods=None): + """Create a fake server migration. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object, with id, type, and so on + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + migration_info = { + "dest_host": "10.0.2.15", + "status": "migrating", + "type": "migration", + "updated_at": "2017-01-31T08:03:25.000000", + "created_at": "2017-01-31T08:03:21.000000", + "dest_compute": "compute-" + uuid.uuid4().hex, + "id": random.randint(1, 999), + "source_node": "node-" + uuid.uuid4().hex, + "server": uuid.uuid4().hex, + "dest_node": "node-" + uuid.uuid4().hex, + "source_compute": "compute-" + uuid.uuid4().hex, + "uuid": uuid.uuid4().hex, + "old_instance_type_id": uuid.uuid4().hex, + "new_instance_type_id": uuid.uuid4().hex, + "project": uuid.uuid4().hex, + "user": uuid.uuid4().hex + } + + # Overwrite default attributes. + migration_info.update(attrs) + + migration = fakes.FakeResource(info=copy.deepcopy(migration_info), + methods=methods, + loaded=True) + return migration + + @staticmethod + def create_server_migrations(attrs=None, methods=None, count=2): + """Create multiple fake server migrations. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :param int count: + The number of server migrations to fake + :return: + A list of FakeResource objects faking the server migrations + """ + migrations = [] + for i in range(0, count): + migrations.append( + FakeServerMigration.create_one_server_migration( + attrs, methods)) + + return migrations + + @staticmethod + def get_server_migrations(migrations=None, count=2): + """Get an iterable MagicMock object with a list of faked migrations. + + If server migrations list is provided, then initialize the Mock object + with the list. Otherwise create one. + + :param List migrations: + A list of FakeResource objects faking server migrations + :param int count: + The number of server migrations to fake + :return: + An iterable Mock object with side_effect set to a list of faked + server migrations + """ + if migrations is None: + migrations = FakeServerMigration.create_server_migrations(count) + return mock.Mock(side_effect=migrations) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c50c..deb4345a91 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3523,6 +3523,490 @@ def test_server_migrate_with_wait_fails(self, mock_wait_for_status): self.assertNotCalled(self.servers_mock.live_migrate) +class TestServerMigration(TestServer): + + def setUp(self): + super(TestServerMigration, self).setUp() + + # Get a shortcut to the compute client ServerManager Mock + self.servers_mock = self.app.client_manager.compute.servers + self.servers_mock.reset_mock() + + self.migrations_mock = ( + self.app.client_manager.compute.migrations) + self.migrations_mock.reset_mock() + + self.server = self.setup_servers_mock(1)[0] + + def setup_servers_mock(self, count): + servers = compute_fakes.FakeServer.create_servers(count=count) + + # This is the return value for utils.find_resource() + self.servers_mock.get = compute_fakes.FakeServer.get_servers(servers) + return servers + + def setup_server_migrations_mock(self, count): + return compute_fakes.FakeServerMigration.create_server_migrations( + count=count) + + +class TestListMigration(TestServerMigration): + """Test fetch all migrations.""" + + MIGRATION_COLUMNS = [ + 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Old Flavor', 'New Flavor', 'Created At', 'Updated At' + ] + + def setUp(self): + super(TestListMigration, self).setUp() + + self.cmd = server.ListMigration(self.app, None) + self.migrations = self.setup_server_migrations_mock(3) + self.migrations_mock.list.return_value = self.migrations + self.setup_server_migrations_data(self.migrations) + + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.20') + + def setup_server_migrations_data(self, migrations): + self.data = (common_utils.get_item_properties( + s, self.MIGRATION_COLUMNS) for s in migrations) + + def test_server_migraton_list(self): + arglist = [ + '--status', 'migrating' + ] + verifylist = [ + ('status', 'migrating') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'host': None, + 'server': None, + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + +class TestListMigrationV223(TestListMigration): + """Test fetch all migrations. """ + + MIGRATION_COLUMNS = [ + 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Old Flavor', 'New Flavor', 'Created At', 'Updated At' + ] + + def setUp(self): + super(TestListMigrationV223, self).setUp() + self.cmd = server.ListMigration(self.app, None) + self.migrations = self.setup_server_migrations_mock(3) + self.migrations_mock.list.return_value = self.migrations + self.setup_server_migrations_data(self.migrations) + + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.23') + + def test_server_migraton_list(self): + arglist = [ + '--status', 'migrating' + ] + verifylist = [ + ('status', 'migrating') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'host': None, + 'server': None, + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.MIGRATION_COLUMNS.insert(0, "Id") + self.MIGRATION_COLUMNS.insert( + len(self.MIGRATION_COLUMNS) - 2, 'Type') + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + +class TestListMigrationV259(TestListMigration): + """Test fetch all migrations. """ + + MIGRATION_COLUMNS = [ + 'Id', 'UUID', 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' + ] + + def setUp(self): + super(TestListMigrationV259, self).setUp() + self.cmd = server.ListMigration(self.app, None) + self.migrations = self.setup_server_migrations_mock(3) + self.migrations_mock.list.return_value = self.migrations + self.setup_server_migrations_data(self.migrations) + + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.59') + + def test_server_migraton_list(self): + arglist = [ + '--status', 'migrating', + '--limit', '1', + '--marker', 'test_kp', + '--changes-since', '2019-08-09T08:03:25Z' + ] + verifylist = [ + ('status', 'migrating'), + ('limit', 1), + ('marker', 'test_kp'), + ('changes_since', '2019-08-09T08:03:25Z') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'limit': 1, + 'marker': 'test_kp', + 'host': None, + 'server': None, + 'changes_since': '2019-08-09T08:03:25Z', + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_migraton_list_with_limit_pre_v259(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.58') + arglist = [ + '--status', 'migrating', + '--limit', '1' + ] + verifylist = [ + ('status', 'migrating'), + ('limit', 1) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_server_migraton_list_with_marker_pre_v259(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.58') + arglist = [ + '--status', 'migrating', + '--marker', 'test_kp' + ] + verifylist = [ + ('status', 'migrating'), + ('marker', 'test_kp') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_server_migraton_list_with_changes_since_pre_v259(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.58') + arglist = [ + '--status', 'migrating', + '--changes-since', '2019-08-09T08:03:25Z' + ] + verifylist = [ + ('status', 'migrating'), + ('changes_since', '2019-08-09T08:03:25Z') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + +class TestListMigrationV266(TestListMigration): + """Test fetch all migrations by changes-before. """ + + MIGRATION_COLUMNS = [ + 'Id', 'UUID', 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' + ] + + def setUp(self): + super(TestListMigrationV266, self).setUp() + self.cmd = server.ListMigration(self.app, None) + self.migrations = self.setup_server_migrations_mock(3) + self.migrations_mock.list.return_value = self.migrations + self.setup_server_migrations_data(self.migrations) + + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.66') + + def test_server_migraton_list_with_changes_before(self): + arglist = [ + '--status', 'migrating', + '--limit', '1', + '--marker', 'test_kp', + '--changes-since', '2019-08-07T08:03:25Z', + '--changes-before', '2019-08-09T08:03:25Z' + ] + verifylist = [ + ('status', 'migrating'), + ('limit', 1), + ('marker', 'test_kp'), + ('changes_since', '2019-08-07T08:03:25Z'), + ('changes_before', '2019-08-09T08:03:25Z') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'limit': 1, + 'marker': 'test_kp', + 'host': None, + 'server': None, + 'changes_since': '2019-08-07T08:03:25Z', + 'changes_before': '2019-08-09T08:03:25Z', + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_migraton_list_with_changes_before_pre_v266(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.65') + arglist = [ + '--status', 'migrating', + '--changes-before', '2019-08-09T08:03:25Z' + ] + verifylist = [ + ('status', 'migrating'), + ('changes_before', '2019-08-09T08:03:25Z') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + +class TestListMigrationV280(TestListMigration): + """Test fetch all migrations by user-id and/or project-id. """ + + MIGRATION_COLUMNS = [ + 'Id', 'UUID', 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' + ] + + def setUp(self): + super(TestListMigrationV280, self).setUp() + self.cmd = server.ListMigration(self.app, None) + self.migrations = self.setup_server_migrations_mock(3) + self.migrations_mock.list.return_value = self.migrations + self.setup_server_migrations_data(self.migrations) + + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.80') + + def test_server_migraton_list_with_project(self): + arglist = [ + '--status', 'migrating', + '--limit', '1', + '--marker', 'test_kp', + '--changes-since', '2019-08-07T08:03:25Z', + '--changes-before', '2019-08-09T08:03:25Z', + '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3' + ] + verifylist = [ + ('status', 'migrating'), + ('limit', 1), + ('marker', 'test_kp'), + ('changes_since', '2019-08-07T08:03:25Z'), + ('changes_before', '2019-08-09T08:03:25Z'), + ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'limit': 1, + 'marker': 'test_kp', + 'host': None, + 'server': None, + 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3', + 'changes_since': '2019-08-07T08:03:25Z', + 'changes_before': "2019-08-09T08:03:25Z", + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.MIGRATION_COLUMNS.insert( + len(self.MIGRATION_COLUMNS) - 2, "Project") + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + # Clean up global variables MIGRATION_COLUMNS + self.MIGRATION_COLUMNS.remove('Project') + + def test_get_migrations_with_project_pre_v280(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.79') + arglist = [ + '--status', 'migrating', + '--changes-before', '2019-08-09T08:03:25Z', + '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3' + ] + verifylist = [ + ('status', 'migrating'), + ('changes_before', '2019-08-09T08:03:25Z'), + ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_server_migraton_list_with_user(self): + arglist = [ + '--status', 'migrating', + '--limit', '1', + '--marker', 'test_kp', + '--changes-since', '2019-08-07T08:03:25Z', + '--changes-before', '2019-08-09T08:03:25Z', + '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + ] + verifylist = [ + ('status', 'migrating'), + ('limit', 1), + ('marker', 'test_kp'), + ('changes_since', '2019-08-07T08:03:25Z'), + ('changes_before', '2019-08-09T08:03:25Z'), + ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'limit': 1, + 'marker': 'test_kp', + 'host': None, + 'server': None, + 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6', + 'changes_since': '2019-08-07T08:03:25Z', + 'changes_before': "2019-08-09T08:03:25Z", + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.MIGRATION_COLUMNS.insert( + len(self.MIGRATION_COLUMNS) - 2, "User") + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + # Clean up global variables MIGRATION_COLUMNS + self.MIGRATION_COLUMNS.remove('User') + + def test_get_migrations_with_user_pre_v280(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.79') + arglist = [ + '--status', 'migrating', + '--changes-before', '2019-08-09T08:03:25Z', + '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + ] + verifylist = [ + ('status', 'migrating'), + ('changes_before', '2019-08-09T08:03:25Z'), + ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_server_migraton_list_with_project_and_user(self): + arglist = [ + '--status', 'migrating', + '--limit', '1', + '--changes-since', '2019-08-07T08:03:25Z', + '--changes-before', '2019-08-09T08:03:25Z', + '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3', + '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + ] + verifylist = [ + ('status', 'migrating'), + ('limit', 1), + ('changes_since', '2019-08-07T08:03:25Z'), + ('changes_before', '2019-08-09T08:03:25Z'), + ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3'), + ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': 'migrating', + 'limit': 1, + 'host': None, + 'server': None, + 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3', + 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6', + 'changes_since': '2019-08-07T08:03:25Z', + 'changes_before': "2019-08-09T08:03:25Z", + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.MIGRATION_COLUMNS.insert( + len(self.MIGRATION_COLUMNS) - 2, "Project") + self.MIGRATION_COLUMNS.insert( + len(self.MIGRATION_COLUMNS) - 2, "User") + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) + # Clean up global variables MIGRATION_COLUMNS + self.MIGRATION_COLUMNS.remove('Project') + self.MIGRATION_COLUMNS.remove('User') + + def test_get_migrations_with_project_and_user_pre_v280(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.79') + arglist = [ + '--status', 'migrating', + '--changes-before', '2019-08-09T08:03:25Z', + '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3', + '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + ] + verifylist = [ + ('status', 'migrating'), + ('changes_before', '2019-08-09T08:03:25Z'), + ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3'), + ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + class TestServerPause(TestServer): def setUp(self): diff --git a/releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml b/releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml new file mode 100644 index 0000000000..766cd0bff2 --- /dev/null +++ b/releasenotes/notes/bp-add-user-id-field-to-the-migrations-table-299b99ccb1f12a1f.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``server migration list`` command. This command allows + users to list the status of ongoing server migrations. diff --git a/setup.cfg b/setup.cfg index 1d9d6df66d..6cf18cc29b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -108,6 +108,7 @@ openstack.compute.v2 = server_migrate = openstackclient.compute.v2.server:MigrateServer server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert + server_migration_list = openstackclient.compute.v2.server:ListMigration server_pause = openstackclient.compute.v2.server:PauseServer server_reboot = openstackclient.compute.v2.server:RebootServer server_rebuild = openstackclient.compute.v2.server:RebuildServer From 63b46ac5ec06378a106ed1368f588eef9d8aed7a Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 2 Oct 2020 20:49:41 +0000 Subject: [PATCH 0270/1120] Update master for stable/victoria Add file to the reno documentation build to show release notes for stable/victoria. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/victoria. Change-Id: I6c1a9cdff90f7073082fc057f0f11b184de5dc32 Sem-Ver: feature --- releasenotes/source/index.rst | 1 + releasenotes/source/victoria.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/victoria.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 889eeb0c5c..8c276de2d8 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + victoria ussuri train stein diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst new file mode 100644 index 0000000000..4efc7b6f3b --- /dev/null +++ b/releasenotes/source/victoria.rst @@ -0,0 +1,6 @@ +============================= +Victoria Series Release Notes +============================= + +.. release-notes:: + :branch: stable/victoria From 098a3fe2dea70eb16f13b717d62f51a4c890891d Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 2 Oct 2020 20:49:43 +0000 Subject: [PATCH 0271/1120] Add Python3 wallaby unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for wallaby. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I27a03e0b51aa7ffe3dcf82ad9bedd9109b016aa1 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index c04f3446d5..fdc212ae8f 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -221,7 +221,7 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-victoria-jobs + - openstack-python3-wallaby-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 From 17678c9bd6f3f4c5b9e7b5ceb27c2bd12493fba4 Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Tue, 6 Oct 2020 14:03:19 +1100 Subject: [PATCH 0272/1120] Restore behavior of image create with same name. With 60e7c51df4cf061ebbb435a959ad63c7d3a296bf the behaviour of `openstack image create` changed so that you can't create an image with the same name. This patch restores the previous functionality. Story: 2008229 Task: 41069 Change-Id: Ia0f4920371a918e94d1ccf9fcfcbf90ff885a455 --- openstackclient/image/v2/image.py | 2 +- openstackclient/tests/unit/image/v2/test_image.py | 6 +++++- .../restore-create-image-duplicates-92e06f64038b120c.yaml | 6 ++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 029f57a3b1..4f3e9d0bcf 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -354,7 +354,7 @@ def take_action(self, parsed_args): # Build an attribute dict from the parsed args, only include # attributes that were actually set on the command line - kwargs = {} + kwargs = {'allow_duplicates': True} copy_attrs = ('name', 'id', 'container_format', 'disk_format', 'min_disk', 'min_ram', 'tags', 'visibility') diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 310f6b7636..b094817efe 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -100,6 +100,7 @@ def test_image_reserve_no_options(self, raw_input): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, ) @@ -152,6 +153,7 @@ def test_image_reserve_options(self, raw_input): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format='ovf', disk_format='ami', min_disk=10, @@ -239,6 +241,7 @@ def test_image_create_file(self): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, is_protected=self.new_image.is_protected, @@ -246,7 +249,7 @@ def test_image_create_file(self): Alpha='1', Beta='2', tags=self.new_image.tags, - filename=imagefile.name + filename=imagefile.name, ) self.assertEqual( @@ -288,6 +291,7 @@ def test_image_create_import(self, raw_input): # ImageManager.create(name=, **) self.client.create_image.assert_called_with( name=self.new_image.name, + allow_duplicates=True, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, use_import=True diff --git a/releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml b/releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml new file mode 100644 index 0000000000..7f36e29c41 --- /dev/null +++ b/releasenotes/notes/restore-create-image-duplicates-92e06f64038b120c.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes default behaviour of `openstack image create` in allowing images + with the same name. In version 5.2.0 this changed to not allow + duplicates by default. From f50bd408666df161603f9ce20a5d00428a13b2ef Mon Sep 17 00:00:00 2001 From: pedh Date: Tue, 18 Aug 2020 20:28:27 +0800 Subject: [PATCH 0273/1120] Fix: port attribute name propagate_uplink_status Change the incorrect port attribute name "uplink_status_propagation" to "propagate_uplink_status". Change-Id: Icd7c49af8d988a6e3a52a58c784bd701b2d36faf Closes-Bug: #1891873 --- openstackclient/tests/unit/network/v2/fakes.py | 6 +++--- openstackclient/tests/unit/network/v2/test_port.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 3df4042cba..74001375a4 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -642,7 +642,7 @@ def create_one_port(attrs=None): 'qos_network_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'tags': [], - 'uplink_status_propagation': False, + 'propagate_uplink_status': False, } # Overwrite default attributes. @@ -662,8 +662,8 @@ def create_one_port(attrs=None): port.project_id = port_attrs['tenant_id'] port.security_group_ids = port_attrs['security_group_ids'] port.qos_policy_id = port_attrs['qos_policy_id'] - port.uplink_status_propagation = port_attrs[ - 'uplink_status_propagation'] + port.propagate_uplink_status = port_attrs[ + 'propagate_uplink_status'] return port diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index f729b552ff..d8889ae558 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -62,12 +62,12 @@ def _get_common_cols_data(fake_port): 'numa_affinity_policy', 'port_security_enabled', 'project_id', + 'propagate_uplink_status', 'qos_network_policy_id', 'qos_policy_id', 'security_group_ids', 'status', 'tags', - 'uplink_status_propagation', ) data = ( @@ -94,12 +94,12 @@ def _get_common_cols_data(fake_port): fake_port.numa_affinity_policy, fake_port.port_security_enabled, fake_port.project_id, + fake_port.propagate_uplink_status, fake_port.qos_network_policy_id, fake_port.qos_policy_id, format_columns.ListColumn(fake_port.security_group_ids), fake_port.status, format_columns.ListColumn(fake_port.tags), - fake_port.uplink_status_propagation, ) return columns, data From 74db8dd65d35b326d3fa1c680b04a668a3f66bdc Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 9 Sep 2020 19:01:27 +0200 Subject: [PATCH 0274/1120] Switch openstack console log show operation to use OpenStackSDK A short switch onto SDK for fetching console logs of the server Change-Id: I3f750ea4f13a4e72272aa67ea4506bd7182b13f9 --- openstackclient/compute/v2/console.py | 19 +++-- .../tests/unit/compute/v2/test_console.py | 79 +++++++++++++++++++ ...h-console-log-to-sdk-6ee92b7833364d3d.yaml | 4 + 3 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/switch-console-log-to-sdk-6ee92b7833364d3d.yaml diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 110b21b81e..f0abaf4cf4 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -44,19 +44,18 @@ 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 - server = utils.find_resource( - compute_client.servers, - parsed_args.server, + server = compute_client.find_server( + name_or_id=parsed_args.server, + ignore_missing=False ) - length = parsed_args.lines - if length: - # NOTE(dtroyer): get_console_output() appears to shortchange the - # output by one line - length += 1 - data = server.get_console_output(length=length) + output = compute_client.get_server_console_output( + server.id, length=parsed_args.lines) + data = None + if output: + data = output.get('output', None) if data and data[-1] != '\n': data += '\n' diff --git a/openstackclient/tests/unit/compute/v2/test_console.py b/openstackclient/tests/unit/compute/v2/test_console.py index 99a14f048d..1c6d658bc1 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -17,16 +17,95 @@ from openstackclient.compute.v2 import console from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit import utils class TestConsole(compute_fakes.TestComputev2): def setUp(self): super(TestConsole, self).setUp() + + # SDK mock + self.app.client_manager.sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection.compute = mock.Mock() + self.sdk_client = self.app.client_manager.sdk_connection.compute + self.sdk_client.find_server = mock.Mock() + self.sdk_client.get_server_console_output = mock.Mock() + self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() +class TestConsoleLog(TestConsole): + _server = compute_fakes.FakeServer.create_one_server() + + def setUp(self): + super(TestConsoleLog, self).setUp() + + self.sdk_client.find_server.return_value = self._server + + self.cmd = console.ShowConsoleLog(self.app, None) + + def test_show_no_args(self): + arglist = [ + ] + verifylist = [ + ] + self.assertRaises(utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist) + + def test_show(self): + arglist = [ + 'fake_server' + ] + verifylist = [ + ('server', 'fake_server') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + output = { + 'output': '1st line\n2nd line\n' + } + self.sdk_client.get_server_console_output.return_value = output + self.cmd.take_action(parsed_args) + + self.sdk_client.find_server.assert_called_with( + name_or_id='fake_server', ignore_missing=False) + self.sdk_client.get_server_console_output.assert_called_with( + self._server.id, + length=None + ) + stdout = self.app.stdout.content + self.assertEqual(stdout[0], output['output']) + + def test_show_lines(self): + arglist = [ + 'fake_server', + '--lines', '15' + ] + verifylist = [ + ('server', 'fake_server'), + ('lines', 15) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + output = { + 'output': '1st line\n2nd line' + } + self.sdk_client.get_server_console_output.return_value = output + self.cmd.take_action(parsed_args) + + self.sdk_client.find_server.assert_called_with( + name_or_id='fake_server', ignore_missing=False) + self.sdk_client.get_server_console_output.assert_called_with( + self._server.id, + length=15 + ) + + class TestConsoleUrlShow(TestConsole): def setUp(self): diff --git a/releasenotes/notes/switch-console-log-to-sdk-6ee92b7833364d3d.yaml b/releasenotes/notes/switch-console-log-to-sdk-6ee92b7833364d3d.yaml new file mode 100644 index 0000000000..2bd0bab49b --- /dev/null +++ b/releasenotes/notes/switch-console-log-to-sdk-6ee92b7833364d3d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Switch console logs operation to use SDK From c2df9215e19752714e83fcad82c8ae3708f85d7a Mon Sep 17 00:00:00 2001 From: songwenping Date: Tue, 6 Oct 2020 14:26:27 +0800 Subject: [PATCH 0275/1120] Remove usage of six With python3.x, classes can use 'metaclass=' instead of 'six.add_metaclass', 'six.iteritems' and 'six.iterkeys' can be replaced by 'items' and 'keys', 'six.moves.urllib.parse' can be replaced by 'urllib.parse', 'six.StringIO' and 'six.moves.cStringIO' can be replaced by 'io.StringIO', 'six.text_type' and 'six.string_type' are just 'str'. Change-Id: I84848c0bf8ab3c36dd821141191e2725e4e3b58b --- doc/source/contributor/developing.rst | 1 - lower-constraints.txt | 1 - openstackclient/api/object_store_v1.py | 2 +- openstackclient/common/sdk_utils.py | 4 +--- openstackclient/compute/v2/server.py | 5 ++--- openstackclient/identity/v3/access_rule.py | 3 +-- openstackclient/network/common.py | 22 +++++++++---------- openstackclient/shell.py | 8 ------- .../tests/unit/compute/v2/test_server.py | 15 ++++++------- .../tests/unit/compute/v2/test_service.py | 5 ++--- openstackclient/tests/unit/fakes.py | 3 +-- openstackclient/tests/unit/object/v1/fakes.py | 3 +-- .../tests/unit/object/v1/test_object_all.py | 6 ++--- openstackclient/tests/unit/utils.py | 2 +- requirements.txt | 1 - 15 files changed, 30 insertions(+), 51 deletions(-) diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index 35c7c7b91f..a319849396 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -203,7 +203,6 @@ Example from osc_lib.api import auth from osc_lib import utils - import six from openstackclient import shell from openstackclient.tests import utils diff --git a/lower-constraints.txt b/lower-constraints.txt index 403ba4e05c..2fa6586e08 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -119,7 +119,6 @@ rfc3986==0.3.1 Routes==2.3.1 rsd-lib==0.1.0 simplejson==3.5.1 -six==1.10.0 smmap==0.9.0 statsd==3.2.1 stestr==1.0.0 diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 8092abd06a..67c7923023 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -17,9 +17,9 @@ import logging import os import sys +import urllib from osc_lib import utils -from six.moves import urllib from openstackclient.api import api diff --git a/openstackclient/common/sdk_utils.py b/openstackclient/common/sdk_utils.py index 9f0856175d..af9c74f944 100644 --- a/openstackclient/common/sdk_utils.py +++ b/openstackclient/common/sdk_utils.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - def get_osc_show_columns_for_sdk_resource( sdk_resource, @@ -44,7 +42,7 @@ def get_osc_show_columns_for_sdk_resource( for col_name in invisible_columns: if col_name in display_columns: display_columns.remove(col_name) - for sdk_attr, osc_attr in six.iteritems(osc_column_map): + for sdk_attr, osc_attr in osc_column_map.items(): if sdk_attr in display_columns: attr_map[osc_attr] = sdk_attr display_columns.remove(sdk_attr) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1d1fc74165..756903f7a1 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -30,7 +30,6 @@ from osc_lib import exceptions from osc_lib import utils from oslo_utils import timeutils -import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -97,7 +96,7 @@ def _get_ip_address(addresses, address_type, ip_address_family): for network in addresses: for addy in addresses[network]: # Case where it is list of strings - if isinstance(addy, six.string_types): + if isinstance(addy, str): if new_address_type == 'fixed': return addresses[network][0] else: @@ -876,7 +875,7 @@ def _match_image(image_api, wanted_properties): boot_args = [parsed_args.server_name, image, flavor] # Handle block device by device name order, like: vdb -> vdc -> vdd - for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)): + for dev_name in sorted(parsed_args.block_device_mapping): dev_map = parsed_args.block_device_mapping[dev_name] dev_map = dev_map.split(':') if dev_map[0]: diff --git a/openstackclient/identity/v3/access_rule.py b/openstackclient/identity/v3/access_rule.py index 65e78be1d0..ffda04f9e5 100644 --- a/openstackclient/identity/v3/access_rule.py +++ b/openstackclient/identity/v3/access_rule.py @@ -20,7 +20,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -import six from openstackclient.i18n import _ from openstackclient.identity import common @@ -115,4 +114,4 @@ def take_action(self, parsed_args): access_rule._info.pop('links', None) - return zip(*sorted(six.iteritems(access_rule._info))) + return zip(*sorted(access_rule._info.items())) diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index e68628b3f1..47ffbe77a6 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -18,7 +18,6 @@ import openstack.exceptions from osc_lib.command import command from osc_lib import exceptions -import six from openstackclient.i18n import _ @@ -54,8 +53,7 @@ def check_missing_extension_if_error(client_manager, attrs): raise -@six.add_metaclass(abc.ABCMeta) -class NetDetectionMixin(object): +class NetDetectionMixin(metaclass=abc.ABCMeta): """Convenience methods for nova-network vs. neutron decisions. A live environment detects which network type it is running and creates its @@ -166,8 +164,8 @@ def take_action_compute(self, client, parsed_args): pass -@six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeCommand(NetDetectionMixin, command.Command): +class NetworkAndComputeCommand(NetDetectionMixin, command.Command, + metaclass=abc.ABCMeta): """Network and Compute Command Command class for commands that support implementation via @@ -178,8 +176,8 @@ class NetworkAndComputeCommand(NetDetectionMixin, command.Command): pass -@six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeDelete(NetworkAndComputeCommand): +class NetworkAndComputeDelete(NetworkAndComputeCommand, + metaclass=abc.ABCMeta): """Network and Compute Delete Delete class for commands that support implementation via @@ -222,8 +220,8 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -@six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeLister(NetDetectionMixin, command.Lister): +class NetworkAndComputeLister(NetDetectionMixin, command.Lister, + metaclass=abc.ABCMeta): """Network and Compute Lister Lister class for commands that support implementation via @@ -234,8 +232,8 @@ class NetworkAndComputeLister(NetDetectionMixin, command.Lister): pass -@six.add_metaclass(abc.ABCMeta) -class NetworkAndComputeShowOne(NetDetectionMixin, command.ShowOne): +class NetworkAndComputeShowOne(NetDetectionMixin, command.ShowOne, + metaclass=abc.ABCMeta): """Network and Compute ShowOne ShowOne class for commands that support implementation via @@ -255,5 +253,5 @@ def take_action(self, parsed_args): except openstack.exceptions.HttpException as exc: msg = _("Error while executing command: %s") % exc.message if exc.details: - msg += ", " + six.text_type(exc.details) + msg += ", " + str(exc.details) raise exceptions.CommandError(msg) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 755af24d2e..bc88e1f1e2 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -16,13 +16,11 @@ """Command-line interface to the OpenStack APIs""" -import locale import sys from osc_lib.api import auth from osc_lib.command import commandmanager from osc_lib import shell -import six import openstackclient from openstackclient.common import clientmanager @@ -143,12 +141,6 @@ def initialize_app(self, argv): def main(argv=None): if argv is None: argv = sys.argv[1:] - if six.PY2: - # Emulate Py3, decode argv into Unicode based on locale so that - # commands always see arguments as text instead of binary data - encoding = locale.getpreferredencoding() - if encoding: - argv = map(lambda arg: arg.decode(encoding), argv) return OpenStackShell().run(argv) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 02bb406c11..2445e1438b 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -24,7 +24,6 @@ from osc_lib import exceptions from osc_lib import utils as common_utils from oslo_utils import timeutils -import six from openstackclient.compute.v2 import server from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -1907,7 +1906,7 @@ def test_server_create_volume_boot_from_volume_conflict(self): self.cmd.take_action, parsed_args) # Assert it is the error we expect. self.assertIn('--volume is not allowed with --boot-from-volume', - six.text_type(ex)) + str(ex)) def test_server_create_image_property(self): arglist = [ @@ -3288,7 +3287,7 @@ def test_server_migrate_with_host_pre_2_56(self): # Make sure it's the error we expect. self.assertIn('--os-compute-api-version 2.56 or greater is required ' 'to use --host without --live-migration.', - six.text_type(ex)) + str(ex)) self.servers_mock.get.assert_called_with(self.server.id) self.assertNotCalled(self.servers_mock.live_migrate) @@ -3323,7 +3322,7 @@ def test_server_live_migrate(self): # A warning should have been logged for using --live. mock_warning.assert_called_once() self.assertIn('The --live option has been deprecated.', - six.text_type(mock_warning.call_args[0][0])) + str(mock_warning.call_args[0][0])) def test_server_live_migrate_host_pre_2_30(self): # Tests that the --host option is not supported for --live-migration @@ -3346,7 +3345,7 @@ def test_server_live_migrate_host_pre_2_30(self): # Make sure it's the error we expect. self.assertIn('--os-compute-api-version 2.30 or greater is required ' - 'when using --host', six.text_type(ex)) + 'when using --host', str(ex)) self.servers_mock.get.assert_called_with(self.server.id) self.assertNotCalled(self.servers_mock.live_migrate) @@ -3436,7 +3435,7 @@ def test_server_live_migrate_without_host_override_live(self): # A warning should have been logged for using --live. mock_warning.assert_called_once() self.assertIn('The --live option has been deprecated.', - six.text_type(mock_warning.call_args[0][0])) + str(mock_warning.call_args[0][0])) def test_server_live_migrate_live_and_host_mutex(self): # Tests specifying both the --live and --host options which are in a @@ -4352,7 +4351,7 @@ def test_server_resize_confirm(self): # A warning should have been logged for using --confirm. mock_warning.assert_called_once() self.assertIn('The --confirm option has been deprecated.', - six.text_type(mock_warning.call_args[0][0])) + str(mock_warning.call_args[0][0])) def test_server_resize_revert(self): arglist = [ @@ -4377,7 +4376,7 @@ def test_server_resize_revert(self): # A warning should have been logged for using --revert. mock_warning.assert_called_once() self.assertIn('The --revert option has been deprecated.', - six.text_type(mock_warning.call_args[0][0])) + str(mock_warning.call_args[0][0])) @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_server_resize_with_wait_ok(self, mock_wait_for_status): diff --git a/openstackclient/tests/unit/compute/v2/test_service.py b/openstackclient/tests/unit/compute/v2/test_service.py index 7a03683364..87e54747f1 100644 --- a/openstackclient/tests/unit/compute/v2/test_service.py +++ b/openstackclient/tests/unit/compute/v2/test_service.py @@ -18,7 +18,6 @@ from novaclient import api_versions from osc_lib import exceptions -import six from openstackclient.compute.v2 import service from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -502,7 +501,7 @@ def test_service_set_find_service_by_host_and_binary_no_results(self): self.cmd._find_service_by_host_and_binary, self.service_mock, 'fake-host', 'nova-compute') self.assertIn('Compute service for host "fake-host" and binary ' - '"nova-compute" not found.', six.text_type(ex)) + '"nova-compute" not found.', str(ex)) def test_service_set_find_service_by_host_and_binary_many_results(self): # Tests that more than one compute service is found by host and binary. @@ -512,4 +511,4 @@ def test_service_set_find_service_by_host_and_binary_many_results(self): self.service_mock, 'fake-host', 'nova-compute') self.assertIn('Multiple compute services found for host "fake-host" ' 'and binary "nova-compute". Unable to proceed.', - six.text_type(ex)) + str(ex)) diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index e5476f06bb..00e0c12925 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -19,7 +19,6 @@ from keystoneauth1 import fixture import requests -import six AUTH_TOKEN = "foobar" @@ -253,7 +252,7 @@ def __init__(self, headers=None, status_code=200, self.headers.update(headers) self._content = json.dumps(data) - if not isinstance(self._content, six.binary_type): + if not isinstance(self._content, bytes): self._content = self._content.encode() diff --git a/openstackclient/tests/unit/object/v1/fakes.py b/openstackclient/tests/unit/object/v1/fakes.py index 0ed791a5bc..1808d5b7d9 100644 --- a/openstackclient/tests/unit/object/v1/fakes.py +++ b/openstackclient/tests/unit/object/v1/fakes.py @@ -14,7 +14,6 @@ # from keystoneauth1 import session -import six from openstackclient.api import object_store_v1 as object_store from openstackclient.tests.unit import utils @@ -68,7 +67,7 @@ 'last_modified': object_modified_1, } -object_1_content = six.b('object 1 content') +object_1_content = b'object 1 content' OBJECT_2 = { 'name': object_name_2, diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py index dd587142fa..7e88409f71 100644 --- a/openstackclient/tests/unit/object/v1/test_object_all.py +++ b/openstackclient/tests/unit/object/v1/test_object_all.py @@ -12,11 +12,11 @@ # import copy +import io from unittest import mock from osc_lib import exceptions from requests_mock.contrib import fixture -import six from openstackclient.object.v1 import object as object_cmds from openstackclient.tests.unit.object.v1 import fakes as object_fakes @@ -241,9 +241,9 @@ def test_save_to_stdout(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - class FakeStdout(six.BytesIO): + class FakeStdout(io.BytesIO): def __init__(self): - six.BytesIO.__init__(self) + io.BytesIO.__init__(self) self.context_manager_calls = [] def __enter__(self): diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index 4f1bc46a98..4130f18e2b 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -14,11 +14,11 @@ # under the License. # +from io import StringIO import os from cliff import columns as cliff_columns import fixtures -from six.moves import StringIO import testtools from openstackclient.tests.unit import fakes diff --git a/requirements.txt b/requirements.txt index 2b7976e59a..64261f97d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ # 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 -six>=1.10.0 # MIT cliff!=2.9.0,>=2.8.0 # Apache-2.0 openstacksdk>=0.48.0 # Apache-2.0 From 1c7fe3b6bd60aa22ae525d0418729590f6429e96 Mon Sep 17 00:00:00 2001 From: tianhui Date: Fri, 6 Jul 2018 08:23:06 +0000 Subject: [PATCH 0276/1120] Compute: Add tag support for server add volume Change-Id: Id9f2e09426f6824e9ca672bf7808b5165c650a69 Story: 2002195 Task: 21675 --- openstackclient/compute/v2/server.py | 64 +++++++++++++------ .../tests/unit/compute/v2/test_server.py | 60 +++++++++++++++-- ...rt-server-add-volume-278e79a22dd482f4.yaml | 5 ++ 3 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 06bbb7ee70..0b1209f496 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -460,20 +460,32 @@ def get_parser(self, prog_name): metavar='', help=_('Server internal device name for volume'), ) + parser.add_argument( + '--tag', + metavar='', + help=_( + "Tag for the attached volume. " + "(Supported by API versions '2.49' - '2.latest')" + ), + ) termination_group = parser.add_mutually_exclusive_group() termination_group.add_argument( '--enable-delete-on-termination', action='store_true', - help=_("Specify if the attached volume should be deleted when " - "the server is destroyed. (Supported with " - "``--os-compute-api-version`` 2.79 or greater.)"), + help=_( + "Specify if the attached volume should be deleted when the " + "server is destroyed. " + "(Supported by API versions '2.79' - '2.latest')" + ), ) termination_group.add_argument( '--disable-delete-on-termination', action='store_true', - help=_("Specify if the attached volume should not be deleted " - "when the server is destroyed. (Supported with " - "``--os-compute-api-version`` 2.79 or greater.)"), + help=_( + "Specify if the attached volume should not be deleted when " + "the server is destroyed. " + "(Supported by API versions '2.79' - '2.latest')" + ), ) return parser @@ -490,28 +502,38 @@ def take_action(self, parsed_args): parsed_args.volume, ) - support_set_delete_on_termination = (compute_client.api_version >= - api_versions.APIVersion('2.79')) - - if not support_set_delete_on_termination: - if parsed_args.enable_delete_on_termination: - msg = _('--os-compute-api-version 2.79 or greater ' - 'is required to support the ' - '--enable-delete-on-termination option.') - raise exceptions.CommandError(msg) - if parsed_args.disable_delete_on_termination: - msg = _('--os-compute-api-version 2.79 or greater ' - 'is required to support the ' - '--disable-delete-on-termination option.') - raise exceptions.CommandError(msg) - kwargs = { "device": parsed_args.device } + if parsed_args.tag: + if compute_client.api_version < api_versions.APIVersion('2.49'): + msg = _( + '--os-compute-api-version 2.49 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + kwargs['tag'] = parsed_args.tag + if parsed_args.enable_delete_on_termination: + if compute_client.api_version < api_versions.APIVersion('2.79'): + msg = _( + '--os-compute-api-version 2.79 or greater is required to ' + 'support the --enable-delete-on-termination option.' + ) + raise exceptions.CommandError(msg) + kwargs['delete_on_termination'] = True + if parsed_args.disable_delete_on_termination: + if compute_client.api_version < api_versions.APIVersion('2.79'): + msg = _( + '--os-compute-api-version 2.79 or greater is required to ' + 'support the --disable-delete-on-termination option.' + ) + raise exceptions.CommandError(msg) + kwargs['delete_on_termination'] = False compute_client.volumes.create_server_volume( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index dffedc6dab..d3e159159f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -503,8 +503,57 @@ def test_server_add_volume(self): servers[0].id, self.volume.id, device='/dev/sdb') self.assertIsNone(result) + def test_server_add_volume_with_tag(self): + # requires API 2.49 or later + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.49') + + servers = self.setup_servers_mock(count=1) + arglist = [ + '--device', '/dev/sdb', + '--tag', 'foo', + servers[0].id, + self.volume.id, + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('device', '/dev/sdb'), + ('tag', 'foo'), + ] -class TestServerVolumeV279(TestServerVolume): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.create_server_volume.assert_called_once_with( + servers[0].id, self.volume.id, device='/dev/sdb', tag='foo') + self.assertIsNone(result) + + def test_server_add_volume_with_tag_pre_v249(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.48') + + servers = self.setup_servers_mock(count=1) + arglist = [ + servers[0].id, + self.volume.id, + '--tag', 'foo', + ] + verifylist = [ + ('server', servers[0].id), + ('volume', self.volume.id), + ('tag', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.49 or greater is required', + str(ex)) def test_server_add_volume_with_enable_delete_on_termination(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -561,7 +610,8 @@ def test_server_add_volume_with_disable_delete_on_termination(self): self.assertIsNone(result) def test_server_add_volume_with_enable_delete_on_termination_pre_v279( - self): + self, + ): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.78') @@ -585,7 +635,8 @@ def test_server_add_volume_with_enable_delete_on_termination_pre_v279( str(ex)) def test_server_add_volume_with_disable_delete_on_termination_pre_v279( - self): + self, + ): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.78') @@ -609,7 +660,8 @@ def test_server_add_volume_with_disable_delete_on_termination_pre_v279( str(ex)) def test_server_add_volume_with_disable_and_enable_delete_on_termination( - self): + self, + ): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.79') diff --git a/releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml b/releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml new file mode 100644 index 0000000000..510218b2f3 --- /dev/null +++ b/releasenotes/notes/add-tag-support-server-add-volume-278e79a22dd482f4.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--tag`` option to ``server add volume`` command when + add a volume to server. Only available starting with + ``--os-compute-api-version 2.49``. From f3fbb1b648a96e541b53f96904b85d3fdce2af10 Mon Sep 17 00:00:00 2001 From: tianhui Date: Mon, 9 Jul 2018 09:34:07 +0000 Subject: [PATCH 0277/1120] Compute: Add tag support for server add port Change-Id: Ice6bf5fb57afeb10862c870b42732dcf166772d1 Story: 2002195 Task: 21676 --- openstackclient/compute/v2/server.py | 25 ++++++++- .../tests/unit/compute/v2/test_server.py | 53 +++++++++++++++++++ ...port-server-add-port-7e30aa38202d0839.yaml | 5 ++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e5a7a32833..5370a63f81 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -354,6 +354,14 @@ def get_parser(self, prog_name): metavar="", help=_("Port to add to the server (name or ID)"), ) + parser.add_argument( + '--tag', + metavar='', + help=_( + "Tag for the attached interface. " + "(Supported by API versions '2.49' - '2.latest')" + ) + ) return parser def take_action(self, parsed_args): @@ -369,7 +377,22 @@ def take_action(self, parsed_args): else: port_id = parsed_args.port - server.interface_attach(port_id=port_id, net_id=None, fixed_ip=None) + kwargs = { + 'port_id': port_id, + 'net_id': None, + 'fixed_ip': None, + } + + if parsed_args.tag: + if compute_client.api_version < api_versions.APIVersion("2.49"): + msg = _( + '--os-compute-api-version 2.49 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + kwargs['tag'] = parsed_args.tag + + server.interface_attach(**kwargs) class AddNetwork(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 59282b4a12..c16bb333f7 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -465,6 +465,59 @@ def test_server_add_port_no_neutron(self): self._test_server_add_port('fake-port') self.find_port.assert_not_called() + def test_server_add_port_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.49') + + servers = self.setup_servers_mock(count=1) + self.find_port.return_value.id = 'fake-port' + arglist = [ + servers[0].id, + 'fake-port', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('port', 'fake-port'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + servers[0].interface_attach.assert_called_once_with( + port_id='fake-port', + net_id=None, + fixed_ip=None, + tag='tag1') + + def test_server_add_port_with_tag_pre_v249(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.48') + + servers = self.setup_servers_mock(count=1) + self.find_port.return_value.id = 'fake-port' + arglist = [ + servers[0].id, + 'fake-port', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('port', 'fake-port'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.49 or greater is required', + str(ex)) + class TestServerVolume(TestServer): diff --git a/releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml b/releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml new file mode 100644 index 0000000000..a5a63128c7 --- /dev/null +++ b/releasenotes/notes/add-tag-support-server-add-port-7e30aa38202d0839.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--tag`` option to ``server add port`` command when + add a port to server. Only available starting with + ``--os-compute-api-version 2.49``. From 4855fef8b8f29542ebdaf86aa5da4daa1f97b060 Mon Sep 17 00:00:00 2001 From: tianhui Date: Mon, 25 Jun 2018 09:46:06 +0000 Subject: [PATCH 0278/1120] Compute: Add 'keypair create --type' parameter Change-Id: I2d251e1b97fb9a8069431c867fb7fc5f42d1fd6e Story: 2002606 Task: 22225 --- openstackclient/compute/v2/keypair.py | 42 +++-- .../tests/unit/compute/v2/fakes.py | 1 + .../tests/unit/compute/v2/test_keypair.py | 144 +++++++++++++++--- ...keypair-support-type-6f7c32aab3b61f7b.yaml | 5 + 4 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 2b365cebff..6affaca302 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -20,6 +20,7 @@ import os import sys +from novaclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -53,6 +54,15 @@ def get_parser(self, prog_name): help=_("Filename for private key to save. If not used, " "print private key in console.") ) + parser.add_argument( + '--type', + metavar='', + choices=['ssh', 'x509'], + help=_( + "Keypair type. Can be ssh or x509. " + "(Supported by API versions '2.2' - '2.latest')" + ), + ) return parser def take_action(self, parsed_args): @@ -70,17 +80,28 @@ def take_action(self, parsed_args): "exception": e} ) - keypair = compute_client.keypairs.create( - parsed_args.name, - public_key=public_key, - ) + kwargs = { + 'name': parsed_args.name, + 'public_key': public_key, + } + if parsed_args.type: + if compute_client.api_version < api_versions.APIVersion('2.2'): + msg = _( + '--os-compute-api-version 2.2 or greater is required to ' + 'support the --type option.' + ) + raise exceptions.CommandError(msg) + + kwargs['key_type'] = parsed_args.type + + keypair = compute_client.keypairs.create(**kwargs) private_key = parsed_args.private_key # Save private key into specified file if private_key: try: with io.open( - os.path.expanduser(parsed_args.private_key), 'w+' + os.path.expanduser(parsed_args.private_key), 'w+' ) as p: p.write(keypair.private_key) except IOError as e: @@ -150,10 +171,13 @@ def take_action(self, parsed_args): ) data = compute_client.keypairs.list() - return (columns, - (utils.get_item_properties( - s, columns, - ) for s in data)) + if compute_client.api_version >= api_versions.APIVersion('2.2'): + columns += ("Type", ) + + return ( + columns, + (utils.get_item_properties(s, columns) for s in data), + ) class ShowKeypair(command.ShowOne): diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 3143098474..2d66f53c41 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -877,6 +877,7 @@ def create_one_keypair(attrs=None, no_pri=False): # Set default attributes. keypair_info = { 'name': 'keypair-name-' + uuid.uuid4().hex, + 'type': 'ssh', 'fingerprint': 'dummy', 'public_key': 'dummy', 'user_id': 'user' diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 1f3f56f989..ca3bfe7c8b 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -17,6 +17,7 @@ from unittest.mock import call import uuid +from novaclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -45,11 +46,13 @@ def setUp(self): self.columns = ( 'fingerprint', 'name', + 'type', 'user_id' ) self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) @@ -71,7 +74,7 @@ def test_key_pair_create_no_options(self): columns, data = self.cmd.take_action(parsed_args) self.keypairs_mock.create.assert_called_with( - self.keypair.name, + name=self.keypair.name, public_key=None ) @@ -87,6 +90,7 @@ def test_keypair_create_public_key(self): self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) @@ -96,7 +100,7 @@ def test_keypair_create_public_key(self): ] verifylist = [ ('public_key', self.keypair.public_key), - ('name', self.keypair.name) + ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -109,8 +113,8 @@ def test_keypair_create_public_key(self): columns, data = self.cmd.take_action(parsed_args) self.keypairs_mock.create.assert_called_with( - self.keypair.name, - public_key=self.keypair.public_key + name=self.keypair.name, + public_key=self.keypair.public_key, ) self.assertEqual(self.columns, columns) @@ -124,7 +128,7 @@ def test_keypair_create_private_key(self): ] verifylist = [ ('private_key', tmp_pk_file), - ('name', self.keypair.name) + ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -136,8 +140,8 @@ def test_keypair_create_private_key(self): columns, data = self.cmd.take_action(parsed_args) self.keypairs_mock.create.assert_called_with( - self.keypair.name, - public_key=None + name=self.keypair.name, + public_key=None, ) mock_open.assert_called_once_with(tmp_pk_file, 'w+') @@ -146,6 +150,79 @@ def test_keypair_create_private_key(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_keypair_create_with_key_type(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.2') + + for key_type in ['x509', 'ssh']: + self.keypair = compute_fakes.FakeKeypair.create_one_keypair( + no_pri=True) + self.keypairs_mock.create.return_value = self.keypair + + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.type, + self.keypair.user_id, + ) + arglist = [ + '--public-key', self.keypair.public_key, + self.keypair.name, + '--type', key_type, + ] + verifylist = [ + ('public_key', self.keypair.public_key), + ('name', self.keypair.name), + ('type', key_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('io.open') as mock_open: + mock_open.return_value = mock.MagicMock() + m_file = mock_open.return_value.__enter__.return_value + m_file.read.return_value = 'dummy' + columns, data = self.cmd.take_action(parsed_args) + + self.keypairs_mock.create.assert_called_with( + name=self.keypair.name, + public_key=self.keypair.public_key, + key_type=key_type, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_keypair_create_with_key_type_pre_v22(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.1') + + for key_type in ['x509', 'ssh']: + arglist = [ + '--public-key', self.keypair.public_key, + self.keypair.name, + '--type', 'ssh', + ] + verifylist = [ + ('public_key', self.keypair.public_key), + ('name', self.keypair.name), + ('type', 'ssh'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('io.open') as mock_open: + mock_open.return_value = mock.MagicMock() + m_file = mock_open.return_value.__enter__.return_value + m_file.read.return_value = 'dummy' + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.2 or greater is required', + str(ex)) + class TestKeypairDelete(TestKeypair): @@ -227,16 +304,6 @@ class TestKeypairList(TestKeypair): # Return value of self.keypairs_mock.list(). keypairs = compute_fakes.FakeKeypair.create_keypairs(count=1) - columns = ( - "Name", - "Fingerprint" - ) - - data = (( - keypairs[0].name, - keypairs[0].fingerprint - ), ) - def setUp(self): super(TestKeypairList, self).setUp() @@ -247,8 +314,7 @@ def setUp(self): def test_keypair_list_no_options(self): arglist = [] - verifylist = [ - ] + verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -261,8 +327,39 @@ def test_keypair_list_no_options(self): self.keypairs_mock.list.assert_called_with() - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(('Name', 'Fingerprint'), columns) + self.assertEqual( + ((self.keypairs[0].name, self.keypairs[0].fingerprint), ), + tuple(data) + ) + + def test_keypair_list_v22(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.2') + + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + + self.keypairs_mock.list.assert_called_with() + + self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) + self.assertEqual( + (( + self.keypairs[0].name, + self.keypairs[0].fingerprint, + self.keypairs[0].type, + ), ), + tuple(data) + ) class TestKeypairShow(TestKeypair): @@ -279,16 +376,18 @@ def setUp(self): self.columns = ( "fingerprint", "name", + "type", "user_id" ) self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) - def test_show_no_options(self): + def test_keypair_show_no_options(self): arglist = [] verifylist = [] @@ -306,6 +405,7 @@ def test_keypair_show(self): self.data = ( self.keypair.fingerprint, self.keypair.name, + self.keypair.type, self.keypair.user_id ) diff --git a/releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml b/releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml new file mode 100644 index 0000000000..549629d8a4 --- /dev/null +++ b/releasenotes/notes/keypair-support-type-6f7c32aab3b61f7b.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--key-type`` option to ``keypair create`` command to set keypair + type. Can be ssh or x509. Note that ``--os-compute-api-version 2.2`` or + later is required. From 6f1602312b00bcba6e04a34f7f04af9d69cf2d9c Mon Sep 17 00:00:00 2001 From: tianhui Date: Wed, 11 Jul 2018 05:52:32 +0000 Subject: [PATCH 0279/1120] Compute: Add tag support for server add network Change-Id: I31a66b2d4dac44052a71f43a5a67836247ccac64 Story: 2002195 Task: 21678 --- openstackclient/compute/v2/server.py | 26 ++++++++- .../tests/unit/compute/v2/test_server.py | 56 +++++++++++++++++++ ...t-server-add-network-a8590cab5d7babf0.yaml | 5 ++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e5a7a32833..adbbb930c7 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -387,6 +387,14 @@ def get_parser(self, prog_name): metavar="", help=_("Network to add to the server (name or ID)"), ) + parser.add_argument( + '--tag', + metavar='', + help=_( + 'Tag for the attached interface. ' + '(supported by --os-compute-api-version 2.49 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -402,7 +410,23 @@ def take_action(self, parsed_args): else: net_id = parsed_args.network - server.interface_attach(port_id=None, net_id=net_id, fixed_ip=None) + kwargs = { + 'port_id': None, + 'net_id': net_id, + 'fixed_ip': None, + } + + if parsed_args.tag: + if compute_client.api_version < api_versions.APIVersion('2.49'): + msg = _( + '--os-compute-api-version 2.49 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + kwargs['tag'] = parsed_args.tag + + server.interface_attach(**kwargs) class AddServerSecurityGroup(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 59282b4a12..fd5412e682 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -681,6 +681,62 @@ def test_server_add_network_no_neutron(self): self._test_server_add_network('fake-network') self.find_network.assert_not_called() + def test_server_add_network_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.49') + + servers = self.setup_servers_mock(count=1) + self.find_network.return_value.id = 'fake-network' + + arglist = [ + servers[0].id, + 'fake-network', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('network', 'fake-network'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + servers[0].interface_attach.assert_called_once_with( + port_id=None, + net_id='fake-network', + fixed_ip=None, + tag='tag1' + ) + + def test_server_add_network_with_tag_pre_v249(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.48') + + servers = self.setup_servers_mock(count=1) + self.find_network.return_value.id = 'fake-network' + + arglist = [ + servers[0].id, + 'fake-network', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('network', 'fake-network'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.49 or greater is required', + str(ex)) + @mock.patch( 'openstackclient.api.compute_v2.APIv2.security_group_find' diff --git a/releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml b/releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml new file mode 100644 index 0000000000..3442ad6a29 --- /dev/null +++ b/releasenotes/notes/add-tag-support-server-add-network-a8590cab5d7babf0.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--tag`` option to ``server add network`` command + when add network to server. Only available starting + with ``--os-compute-api-version 2.49``. From 742c80a82574e5a6928c305842047b8b6b2b3807 Mon Sep 17 00:00:00 2001 From: tianhui Date: Fri, 27 Jul 2018 06:20:56 +0000 Subject: [PATCH 0280/1120] Compute: Add tag support for server add fixed ip Change-Id: I62ed4729dead9f91630d1f568c834c9642965558 Story: 2002195 Task: 21679 --- openstackclient/compute/v2/server.py | 30 +++++++-- .../tests/unit/compute/v2/test_server.py | 66 +++++++++++++++++++ ...-server-add-fixed-ip-8de2db58f2a80e85.yaml | 5 ++ 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e5a7a32833..3e69d93fed 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -236,6 +236,14 @@ def get_parser(self, prog_name): metavar="", help=_("Requested fixed IP address"), ) + parser.add_argument( + '--tag', + metavar='', + help=_( + 'Tag for the attached interface. ' + '(supported by --os-compute-api-version 2.52 or above)' + ) + ) return parser def take_action(self, parsed_args): @@ -246,11 +254,23 @@ def take_action(self, parsed_args): network = compute_client.api.network_find(parsed_args.network) - server.interface_attach( - port_id=None, - net_id=network['id'], - fixed_ip=parsed_args.fixed_ip_address, - ) + kwargs = { + 'port_id': None, + 'net_id': network['id'], + 'fixed_ip': parsed_args.fixed_ip_address, + } + + if parsed_args.tag: + if compute_client.api_version < api_versions.APIVersion('2.49'): + msg = _( + '--os-compute-api-version 2.49 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + kwargs['tag'] = parsed_args.tag + + server.interface_attach(**kwargs) class AddFloatingIP(network_common.NetworkAndComputeCommand): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 59282b4a12..c91d6e4899 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -177,6 +177,72 @@ def test_server_add_specific_fixed_ip(self): extralist = ['--fixed-ip-address', '5.6.7.8'] self._test_server_add_fixed_ip(extralist, '5.6.7.8') + def test_server_add_fixed_ip_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.49') + + servers = self.setup_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + with mock.patch( + 'openstackclient.api.compute_v2.APIv2.network_find' + ) as net_mock: + net_mock.return_value = network + + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + servers[0].interface_attach.assert_called_once_with( + port_id=None, + net_id=network['id'], + fixed_ip='5.6.7.8', + tag='tag1' + ) + self.assertIsNone(result) + + def test_server_add_fixed_ip_with_tag_pre_v249(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.48') + + servers = self.setup_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + with mock.patch( + 'openstackclient.api.compute_v2.APIv2.network_find' + ) as net_mock: + net_mock.return_value = network + + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8', + '--tag', 'tag1', + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8'), + ('tag', 'tag1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.49 or greater is required', + str(ex)) + @mock.patch( 'openstackclient.api.compute_v2.APIv2.floating_ip_add' diff --git a/releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml b/releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml new file mode 100644 index 0000000000..731236eb2b --- /dev/null +++ b/releasenotes/notes/add-tag-support-server-add-fixed-ip-8de2db58f2a80e85.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--tag`` option to ``server add fixed ip`` command + when adding a fixed IP to server. Only available starting + with ``--os-compute-api-version 2.49``. From 415545ab9fd842bdc19b7fbfa63e3332dd63fe6c Mon Sep 17 00:00:00 2001 From: yanpuqing Date: Thu, 20 Jun 2019 11:14:22 +0800 Subject: [PATCH 0281/1120] Add an error message when server bind floating IP If we add a floating IP for the server with no fixed IP, CLI doesn't report an error and nothing happens. The patch adds an error message when the server which don't have fixed IP bind floating IP. Change-Id: I400f2bab08521bb7fa443d87c7f45cc79eb80694 Task: 27941 Story: 2004346 --- openstackclient/compute/v2/server.py | 4 ++ .../tests/unit/compute/v2/test_server.py | 48 +++++++++++++++---- ...ing-ip-with-no-ports-399c5559e1699816.yaml | 7 +++ 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e5a7a32833..30057111bb 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -292,6 +292,10 @@ def take_action_network(self, client, parsed_args): parsed_args.server, ) ports = list(client.ports(device_id=server.id)) + if not ports: + msg = _('No attached ports found to associate floating IP with') + raise exceptions.CommandError(msg) + # If the fixed IP address was specified, we need to find the # corresponding port. if parsed_args.fixed_ip_address: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 59282b4a12..52a6f51711 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -249,7 +249,7 @@ def setUp(self): # Get the command object to test self.cmd = server.AddFloatingIP(self.app, self.namespace) - def test_server_add_floating_ip_default(self): + def test_server_add_floating_ip(self): _server = compute_fakes.FakeServer.create_one_server() self.servers_mock.get.return_value = _server _port = network_fakes.FakePort.create_one_port() @@ -284,8 +284,41 @@ def test_server_add_floating_ip_default(self): **attrs ) - def test_server_add_floating_ip_default_no_external_gateway(self, - success=False): + def test_server_add_floating_ip_no_ports(self): + server = compute_fakes.FakeServer.create_one_server() + floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() + + self.servers_mock.get.return_value = server + self.network.find_ip = mock.Mock(return_value=floating_ip) + self.network.ports = mock.Mock(return_value=[]) + + arglist = [ + server.id, + floating_ip['floating_ip_address'], + ] + verifylist = [ + ('server', server.id), + ('ip_address', floating_ip['floating_ip_address']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + 'No attached ports found to associate floating IP with', + str(ex)) + + self.network.find_ip.assert_called_once_with( + floating_ip['floating_ip_address'], + ignore_missing=False, + ) + self.network.ports.assert_called_once_with( + device_id=server.id, + ) + + def test_server_add_floating_ip_no_external_gateway(self, success=False): _server = compute_fakes.FakeServer.create_one_server() self.servers_mock.get.return_value = _server _port = network_fakes.FakePort.create_one_port() @@ -338,11 +371,10 @@ def test_server_add_floating_ip_default_no_external_gateway(self, **attrs ) - def test_server_add_floating_ip_default_one_external_gateway(self): - self.test_server_add_floating_ip_default_no_external_gateway( - success=True) + def test_server_add_floating_ip_one_external_gateway(self): + self.test_server_add_floating_ip_no_external_gateway(success=True) - def test_server_add_floating_ip_fixed(self): + def test_server_add_floating_ip_with_fixed_ip(self): _server = compute_fakes.FakeServer.create_one_server() self.servers_mock.get.return_value = _server _port = network_fakes.FakePort.create_one_port() @@ -384,7 +416,7 @@ def test_server_add_floating_ip_fixed(self): **attrs ) - def test_server_add_floating_ip_fixed_no_port_found(self): + def test_server_add_floating_ip_with_fixed_ip_no_port_found(self): _server = compute_fakes.FakeServer.create_one_server() self.servers_mock.get.return_value = _server _port = network_fakes.FakePort.create_one_port() diff --git a/releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml b/releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml new file mode 100644 index 0000000000..ebcb7f6312 --- /dev/null +++ b/releasenotes/notes/story-2004346-add-floating-ip-with-no-ports-399c5559e1699816.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Associating a floating IP with a server using the ``server add floating + ip`` command requires the server have at least one port associated with it. + Previously, this was not validated, meaning the operation would silently + fail. This has been resolved. From 671f73694a207b4b2b5a7cf377bc46dad7e79324 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 12 Oct 2020 17:25:03 +0100 Subject: [PATCH 0282/1120] zuul: Stop testing against Tempest Neither Tempest itself nor any of the service projects use OSC. As such, there's no reason to run Tempest jobs here. It's simply a waste of resources. Change-Id: I74b0b196fe59e5e1462e3dadc659cf6680a53f80 Signed-off-by: Stephen Finucane --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index fdc212ae8f..42c29d162d 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,3 +1,4 @@ +--- - job: name: osc-tox-unit-tips parent: openstack-tox @@ -225,7 +226,6 @@ - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 - - lib-forward-testing-python3 check: jobs: - osc-build-image From 9385113d40f3d9dd77f2d7dfa5ebb71d92635548 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 6 Jul 2020 12:10:19 -0500 Subject: [PATCH 0283/1120] Remove oslo.utils Oslo things are really server-side oriented and are heavy-weight for client things. Remove oslo.utils and just use iso8601 and importlib directly. It's not actually a bad library, but pulling it and its other deps in just for a couple of wrapper methods is a bit much here. oslo.i18n, fwiw, is lightweight and helpful. Change-Id: I463993170c03a1d98c47ab6a3c19131b7fca1099 --- openstackclient/compute/v2/server.py | 10 +++++----- openstackclient/compute/v2/server_backup.py | 5 +++-- openstackclient/compute/v2/server_image.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_server.py | 6 +++--- openstackclient/tests/unit/test_shell.py | 9 +++++---- requirements.txt | 2 +- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e5a7a32833..2ede3af674 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -21,6 +21,7 @@ import logging import os +import iso8601 from novaclient import api_versions from novaclient.v2 import servers from openstack import exceptions as sdk_exceptions @@ -29,7 +30,6 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -from oslo_utils import timeutils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -1404,8 +1404,8 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) try: - timeutils.parse_isotime(search_opts['changes-before']) - except ValueError: + iso8601.parse_date(search_opts['changes-before']) + except (TypeError, iso8601.ParseError): raise exceptions.CommandError( _('Invalid changes-before value: %s') % search_opts['changes-before'] @@ -1413,8 +1413,8 @@ def take_action(self, parsed_args): if search_opts['changes-since']: try: - timeutils.parse_isotime(search_opts['changes-since']) - except ValueError: + iso8601.parse_date(search_opts['changes-since']) + except (TypeError, iso8601.ParseError): raise exceptions.CommandError( _('Invalid changes-since value: %s') % search_opts['changes-since'] diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index a5d43fc6f8..b1b821b24e 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -15,10 +15,11 @@ """Compute v2 Server action implementations""" +import importlib + from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -from oslo_utils import importutils from openstackclient.i18n import _ @@ -119,7 +120,7 @@ def _show_progress(progress): info['properties'] = utils.format_dict(info.get('properties', {})) else: # Get the right image module to format the output - image_module = importutils.import_module( + image_module = importlib.import_module( self.IMAGE_API_VERSIONS[ self.app.client_manager._api_version['image'] ] diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index fea87af81f..c12bc2b3ae 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -15,12 +15,12 @@ """Compute v2 Server action implementations""" +import importlib import logging from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils -from oslo_utils import importutils from openstackclient.i18n import _ @@ -99,7 +99,7 @@ def _show_progress(progress): info['properties'] = utils.format_dict(info.get('properties', {})) else: # Get the right image module to format the output - image_module = importutils.import_module( + image_module = importlib.import_module( self.IMAGE_API_VERSIONS[ self.app.client_manager._api_version['image'] ] diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 59282b4a12..4bdb322e51 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -19,11 +19,11 @@ from unittest import mock from unittest.mock import call +import iso8601 from novaclient import api_versions from openstack import exceptions as sdk_exceptions from osc_lib import exceptions from osc_lib import utils as common_utils -from oslo_utils import timeutils from openstackclient.compute.v2 import server from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -2945,7 +2945,7 @@ def test_server_list_with_changes_since(self): self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) - @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) + @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): arglist = [ @@ -2988,7 +2988,7 @@ def test_server_list_v266_with_changes_before(self): self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) - @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) + @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) def test_server_list_v266_with_invalid_changes_before( self, mock_parse_isotime): self.app.client_manager.compute.api_version = ( diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py index 94f4f44d24..366c364e06 100644 --- a/openstackclient/tests/unit/test_shell.py +++ b/openstackclient/tests/unit/test_shell.py @@ -13,12 +13,12 @@ # under the License. # +import importlib import os import sys from unittest import mock from osc_lib.tests import utils as osc_lib_test_utils -from oslo_utils import importutils import wrapt from openstackclient import shell @@ -151,12 +151,13 @@ def setUp(self): super(TestShell, self).setUp() # TODO(dtroyer): remove this once the shell_class_patch patch is # released in osc-lib - self.shell_class = importutils.import_class(self.shell_class_name) + mod_str, _sep, class_str = self.shell_class_name.rpartition('.') + self.shell_class = getattr(importlib.import_module(mod_str), class_str) def _assert_admin_token_auth(self, cmd_options, default_args): with mock.patch( - self.shell_class_name + ".initialize_app", - self.app, + self.shell_class_name + ".initialize_app", + self.app, ): _shell = osc_lib_test_utils.make_shell( shell_class=self.shell_class, diff --git a/requirements.txt b/requirements.txt index 64261f97d0..ee6b6241ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,10 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 cliff!=2.9.0,>=2.8.0 # Apache-2.0 +iso8601>=0.1.11 # MIT openstacksdk>=0.48.0 # Apache-2.0 osc-lib>=2.0.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>=15.1.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From fd9a235de3b3592366818ab7799e73f6be029b15 Mon Sep 17 00:00:00 2001 From: Zhaokun Fu Date: Thu, 3 Aug 2017 19:51:27 -0700 Subject: [PATCH 0284/1120] compute: Add --password option for openstack server create Change-Id: Iaf923200efe023655a58ac5acac0b087d2fd5366 Story: #1708570 Task: #13780 --- openstackclient/compute/v2/server.py | 6 +++++ .../tests/unit/compute/v2/test_server.py | 25 +++++++++++++++++++ .../notes/bug-1708570-bb19e1213e887723.yaml | 5 ++++ 3 files changed, 36 insertions(+) create mode 100644 releasenotes/notes/bug-1708570-bb19e1213e887723.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e5a7a32833..da0cf09732 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -555,6 +555,11 @@ def get_parser(self, prog_name): 'duplicate mapping using --block-device-mapping for this ' 'volume.'), ) + parser.add_argument( + '--password', + metavar='', + help=_("Set the password to this server"), + ) parser.add_argument( '--flavor', metavar='', @@ -1054,6 +1059,7 @@ def _match_image(image_api, wanted_properties): userdata=userdata, key_name=parsed_args.key_name, availability_zone=parsed_args.availability_zone, + admin_pass=parsed_args.password, block_device_mapping_v2=block_device_mapping_v2, nics=nics, scheduler_hints=hints, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 59282b4a12..9a8fb3ad07 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -831,6 +831,7 @@ def test_server_create_minimal(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -857,6 +858,7 @@ def test_server_create_with_options(self): '--property', 'Beta=b', '--security-group', 'securitygroup', '--use-config-drive', + '--password', 'passw0rd', '--hint', 'a=b', '--hint', 'a=c', self.new_server.name, @@ -869,6 +871,7 @@ def test_server_create_with_options(self): ('security_group', ['securitygroup']), ('hint', {'a': ['b', 'c']}), ('config_drive', True), + ('password', 'passw0rd'), ('server_name', self.new_server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -897,6 +900,7 @@ def test_server_create_with_options(self): userdata=None, key_name='keyname', availability_zone=None, + admin_pass='passw0rd', block_device_mapping_v2=[], nics=[], scheduler_hints={'a': ['b', 'c']}, @@ -983,6 +987,7 @@ def test_server_create_with_security_group_in_nova_network(self): userdata=None, key_name='keyname', availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1069,6 +1074,7 @@ def test_server_create_with_network(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[{'net-id': 'net1_uuid', 'v4-fixed-ip': '', @@ -1133,6 +1139,7 @@ def test_server_create_with_auto_network(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -1182,6 +1189,7 @@ def test_server_create_with_auto_network_default_v2_37(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -1227,6 +1235,7 @@ def test_server_create_with_none_network(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', scheduler_hints={}, @@ -1392,6 +1401,7 @@ def test_server_create_with_wait_ok(self, mock_wait_for_status): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1442,6 +1452,7 @@ def test_server_create_with_wait_fails(self, mock_wait_for_status): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1497,6 +1508,7 @@ def test_server_create_userdata(self, mock_open): userdata=mock_file, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics=[], scheduler_hints={}, @@ -1543,6 +1555,7 @@ def test_server_create_with_block_device_mapping(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vda', 'uuid': self.volume.id, @@ -1595,6 +1608,7 @@ def test_server_create_with_block_device_mapping_min_input(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vdf', 'uuid': self.volume.id, @@ -1646,6 +1660,7 @@ def test_server_create_with_block_device_mapping_default_input(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vdf', 'uuid': self.volume.id, @@ -1699,6 +1714,7 @@ def test_server_create_with_block_device_mapping_full_input(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vde', 'uuid': self.volume.id, @@ -1754,6 +1770,7 @@ def test_server_create_with_block_device_mapping_snapshot(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[{ 'device_name': 'vds', 'uuid': self.snapshot.id, @@ -1809,6 +1826,7 @@ def test_server_create_with_block_device_mapping_multiple(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[ { 'device_name': 'vdb', @@ -1945,6 +1963,7 @@ def test_server_create_image_property(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', meta=None, @@ -2000,6 +2019,7 @@ def test_server_create_image_property_multi(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', meta=None, @@ -2089,6 +2109,7 @@ def test_server_create_image_property_with_image_list(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='none', meta=None, @@ -2169,6 +2190,7 @@ def test_server_create_with_description_api_newer(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -2253,6 +2275,7 @@ def test_server_create_with_host_v274(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -2338,6 +2361,7 @@ def test_server_create_with_hypervisor_hostname_v274(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, @@ -2425,6 +2449,7 @@ def test_server_create_with_host_and_hypervisor_hostname_v274(self): userdata=None, key_name=None, availability_zone=None, + admin_pass=None, block_device_mapping_v2=[], nics='auto', scheduler_hints={}, diff --git a/releasenotes/notes/bug-1708570-bb19e1213e887723.yaml b/releasenotes/notes/bug-1708570-bb19e1213e887723.yaml new file mode 100644 index 0000000000..1a1cbdbef2 --- /dev/null +++ b/releasenotes/notes/bug-1708570-bb19e1213e887723.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--password`` option to ``server create`` command, allowing users to + set the admin password when creating a new instance. From 1c3cf11331a5734700e1c333c98928ab933c0e92 Mon Sep 17 00:00:00 2001 From: hackertron Date: Thu, 2 Apr 2020 13:47:02 +0200 Subject: [PATCH 0285/1120] Add 'server migration abort' command This is equivalent to nova client's 'live-migration-abort' command. Change-Id: I0ff520ccfdf2de52c427affad7bef4554c86a06f Story: 2007489 Task: 39210 --- openstackclient/compute/v2/server.py | 38 +++++++++++++ .../tests/unit/compute/v2/fakes.py | 3 + .../tests/unit/compute/v2/test_server.py | 56 +++++++++++++++++++ .../notes/bug-2007489-42e41b14e42128ce.yaml | 4 ++ setup.cfg | 1 + 5 files changed, 102 insertions(+) create mode 100644 releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index f8d6aad0e8..0a96eb86de 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2026,6 +2026,44 @@ def take_action(self, parsed_args): return self.print_migrations(parsed_args, compute_client, migrations) +class AbortMigration(command.Command): + """Cancel an ongoing live migration. + + This command requires ``--os-compute-api-version`` 2.24 or greater. + """ + + def get_parser(self, prog_name): + parser = super(AbortMigration, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + parser.add_argument( + 'migration', + metavar='', + help=_("Migration (ID)"), + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + + if compute_client.api_version < api_versions.APIVersion('2.24'): + msg = _( + '--os-compute-api-version 2.24 or greater is required to ' + 'support the server migration abort command' + ) + raise exceptions.CommandError(msg) + + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + compute_client.server_migrations.live_migration_abort( + server.id, parsed_args.migration) + + class PauseServer(command.Command): _description = _("Pause server(s)") diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 6aeb5da72f..ce556fa6cd 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -196,6 +196,9 @@ def __init__(self, **kwargs): self.server_groups = mock.Mock() self.server_groups.resource_class = fakes.FakeResource(None, {}) + self.server_migrations = mock.Mock() + self.server_migrations.resource_class = fakes.FakeResource(None, {}) + self.instance_action = mock.Mock() self.instance_action.resource_class = fakes.FakeResource(None, {}) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index a9f72cf6da..9ac2935236 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -42,6 +42,11 @@ def setUp(self): self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() + # Get a shortcut to the compute client ServerMigrationsManager Mock + self.server_migrations_mock = \ + self.app.client_manager.compute.server_migrations + self.server_migrations_mock.reset_mock() + # Get a shortcut to the compute client volumeManager Mock self.servers_volumes_mock = self.app.client_manager.compute.volumes self.servers_volumes_mock.reset_mock() @@ -4207,6 +4212,57 @@ def test_get_migrations_with_project_and_user_pre_v280(self): parsed_args) +class TestServerMigrationAbort(TestServer): + + def setUp(self): + super(TestServerMigrationAbort, self).setUp() + + self.server = compute_fakes.FakeServer.create_one_server() + + # Return value for utils.find_resource for server. + self.servers_mock.get.return_value = self.server + + # Get the command object to test + self.cmd = server.AbortMigration(self.app, None) + + def test_migration_abort(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.24') + + arglist = [ + self.server.id, + '2', # arbitrary migration ID + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server_migrations_mock.live_migration_abort.assert_called_with( + self.server.id, '2',) + self.assertIsNone(result) + + def test_migration_abort_pre_v224(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.23') + + arglist = [ + self.server.id, + '2', # arbitrary migration ID + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.24 or greater is required', + str(ex)) + + class TestServerPause(TestServer): def setUp(self): diff --git a/releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml b/releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml new file mode 100644 index 0000000000..99dd5062b0 --- /dev/null +++ b/releasenotes/notes/bug-2007489-42e41b14e42128ce.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add ``server migration abort`` command to abort ongoing live migrations. diff --git a/setup.cfg b/setup.cfg index 6cf18cc29b..56934a192b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,6 +109,7 @@ openstack.compute.v2 = server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert server_migration_list = openstackclient.compute.v2.server:ListMigration + server_migration_abort = openstackclient.compute.v2.server:AbortMigration server_pause = openstackclient.compute.v2.server:PauseServer server_reboot = openstackclient.compute.v2.server:RebootServer server_rebuild = openstackclient.compute.v2.server:RebuildServer From 08b0e5855be5dc12a2fd04c5c92027c8a85a0d00 Mon Sep 17 00:00:00 2001 From: jay Date: Tue, 21 Apr 2020 14:23:58 +0200 Subject: [PATCH 0286/1120] Add 'server migration force complete' command This is equivalent to novaclient's 'live-migration-force-complete' command. Change-Id: Ic4dc639afa16cdf8c5a46774895e850d92985292 Story: 2007513 Task: 39293 --- openstackclient/compute/v2/server.py | 38 ++++++++++++++ .../tests/unit/compute/v2/test_server.py | 51 +++++++++++++++++++ .../notes/bug-2007513-ae39456aeb93bb98.yaml | 4 ++ setup.cfg | 1 + 4 files changed, 94 insertions(+) create mode 100644 releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 0a96eb86de..c3aec0ed70 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2064,6 +2064,44 @@ def take_action(self, parsed_args): server.id, parsed_args.migration) +class ForceCompleteMigration(command.Command): + """Force an ongoing live migration to complete. + + This command requires ``--os-compute-api-version`` 2.22 or greater. + """ + + def get_parser(self, prog_name): + parser = super(ForceCompleteMigration, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + parser.add_argument( + 'migration', + metavar='', + help=_('Migration (ID)') + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + + if compute_client.api_version < api_versions.APIVersion('2.22'): + msg = _( + '--os-compute-api-version 2.22 or greater is required to ' + 'support the server migration force complete command' + ) + raise exceptions.CommandError(msg) + + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + compute_client.server_migrations.live_migrate_force_complete( + server.id, parsed_args.migration) + + class PauseServer(command.Command): _description = _("Pause server(s)") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9ac2935236..3b98c873ec 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4263,6 +4263,57 @@ def test_migration_abort_pre_v224(self): str(ex)) +class TestServerMigrationForceComplete(TestServer): + + def setUp(self): + super(TestServerMigrationForceComplete, self).setUp() + + self.server = compute_fakes.FakeServer.create_one_server() + + # Return value for utils.find_resource for server. + self.servers_mock.get.return_value = self.server + + # Get the command object to test + self.cmd = server.ForceCompleteMigration(self.app, None) + + def test_migration_force_complete(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.22') + + arglist = [ + self.server.id, + '2', # arbitrary migration ID + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server_migrations_mock.live_migrate_force_complete\ + .assert_called_with(self.server.id, '2',) + self.assertIsNone(result) + + def test_migration_force_complete_pre_v222(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.21') + + arglist = [ + self.server.id, + '2', # arbitrary migration ID + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.22 or greater is required', + str(ex)) + + class TestServerPause(TestServer): def setUp(self): diff --git a/releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml b/releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml new file mode 100644 index 0000000000..56de14b2c5 --- /dev/null +++ b/releasenotes/notes/bug-2007513-ae39456aeb93bb98.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add ``server migration force complete`` command to force complete + ongoing live migrations. diff --git a/setup.cfg b/setup.cfg index 56934a192b..8363ec6cfd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -110,6 +110,7 @@ openstack.compute.v2 = server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert server_migration_list = openstackclient.compute.v2.server:ListMigration server_migration_abort = openstackclient.compute.v2.server:AbortMigration + server_migration_force_complete = openstackclient.compute.v2.server:ForceCompleteMigration server_pause = openstackclient.compute.v2.server:PauseServer server_reboot = openstackclient.compute.v2.server:RebootServer server_rebuild = openstackclient.compute.v2.server:RebuildServer From 5fd399eabaff7c7994d4ba79c7e9d77131436c5d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 13 Oct 2020 14:31:11 +0100 Subject: [PATCH 0287/1120] Cleanup of 'server migration list' command Address some post merge nits. Change-Id: Ie59521d81fab191194f6c1a114b007fa17f5299f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 193 ++++++++++-------- .../tests/unit/compute/v2/test_server.py | 185 +++++++++-------- 2 files changed, 211 insertions(+), 167 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c3aec0ed70..2fa0c52403 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1866,99 +1866,99 @@ def _show_progress(progress): class ListMigration(command.Command): - _description = _("""List server migrations.""") + _description = _("""List server migrations""") def get_parser(self, prog_name): parser = super(ListMigration, self).get_parser(prog_name) parser.add_argument( - "--server", - metavar="", - dest='server', - default=None, - help=_('Server to show migration details (name or ID).') + '--server', + metavar='', + help=_( + 'Filter migrations by server (name or ID)' + ) ) parser.add_argument( - "--host", - metavar="", - default=None, - help=_('Fetch migrations for the given host.') + '--host', + metavar='', + help=_( + 'Filter migrations by source or destination host' + ), ) parser.add_argument( - "--status", - metavar="", - default=None, - help=_('Fetch migrations for the given status.') + '--status', + metavar='', + help=_('Filter migrations by status') ) parser.add_argument( - "--marker", - metavar="", - dest='marker', - default=None, - help=_("The last migration of the previous page; displays list " - "of migrations after 'marker'. Note that the marker is " - "the migration UUID. (Supported with " - "``--os-compute-api-version`` 2.59 or greater.)") + '--marker', + metavar='', + help=_( + "The last migration of the previous page; displays list " + "of migrations after 'marker'. Note that the marker is " + "the migration UUID. " + "(supported with --os-compute-api-version 2.59 or above)" + ), ) parser.add_argument( - "--limit", - metavar="", - dest='limit', + '--limit', + metavar='', type=int, - default=None, - help=_("Maximum number of migrations to display. Note that there " - "is a configurable max limit on the server, and the limit " - "that is used will be the minimum of what is requested " - "here and what is configured in the server. " - "(Supported with ``--os-compute-api-version`` 2.59 " - "or greater.)") + help=_( + "Maximum number of migrations to display. Note that there " + "is a configurable max limit on the server, and the limit " + "that is used will be the minimum of what is requested " + "here and what is configured in the server. " + "(supported with --os-compute-api-version 2.59 or above)" + ), ) parser.add_argument( '--changes-since', dest='changes_since', metavar='', - default=None, - help=_("List only migrations changed later or equal to a certain " - "point of time. The provided time should be an ISO 8061 " - "formatted time, e.g. ``2016-03-04T06:27:59Z``. " - "(Supported with ``--os-compute-api-version`` 2.59 " - "or greater.)") + help=_( + "List only migrations changed later or equal to a certain " + "point of time. The provided time should be an ISO 8061 " + "formatted time, e.g. ``2016-03-04T06:27:59Z``. " + "(supported with --os-compute-api-version 2.59 or above)" + ), ) parser.add_argument( '--changes-before', dest='changes_before', metavar='', - default=None, - help=_("List only migrations changed earlier or equal to a " - "certain point of time. The provided time should be an ISO " - "8061 formatted time, e.g. ``2016-03-04T06:27:59Z``. " - "(Supported with ``--os-compute-api-version`` 2.66 or " - "greater.)") + help=_( + "List only migrations changed earlier or equal to a " + "certain point of time. The provided time should be an ISO " + "8061 formatted time, e.g. ``2016-03-04T06:27:59Z``. " + "(supported with --os-compute-api-version 2.66 or above)" + ), ) parser.add_argument( '--project', metavar='', dest='project_id', - default=None, - help=_("Filter the migrations by the given project ID. " - "(Supported with ``--os-compute-api-version`` 2.80 " - "or greater.)"), + help=_( + "Filter migrations by project (ID) " + "(supported with --os-compute-api-version 2.80 or above)" + ), ) parser.add_argument( '--user', metavar='', dest='user_id', - default=None, - help=_("Filter the migrations by the given user ID. " - "(Supported with ``--os-compute-api-version`` 2.80 " - "or greater.)"), + help=_( + "Filter migrations by user (ID) " + "(supported with --os-compute-api-version 2.80 or above)" + ), ) return parser def print_migrations(self, parsed_args, compute_client, migrations): - columns = ['Source Node', 'Dest Node', 'Source Compute', - 'Dest Compute', 'Dest Host', 'Status', - 'Server UUID', 'Old Flavor', 'New Flavor', - 'Created At', 'Updated At'] + columns = [ + 'Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', + 'Dest Host', 'Status', 'Server UUID', 'Old Flavor', 'New Flavor', + 'Created At', 'Updated At', + ] # Insert migrations UUID after ID if compute_client.api_version >= api_versions.APIVersion("2.59"): @@ -1978,48 +1978,73 @@ def print_migrations(self, parsed_args, compute_client, migrations): if parsed_args.user_id: columns.insert(len(columns) - 2, "User") - columns_header = columns - return (columns_header, (utils.get_item_properties( - mig, columns) for mig in migrations)) + return ( + columns, + (utils.get_item_properties(mig, columns) for mig in migrations), + ) def take_action(self, parsed_args): compute_client = self.app.client_manager.compute search_opts = { - "host": parsed_args.host, - "server": parsed_args.server, - "status": parsed_args.status, + 'host': parsed_args.host, + 'server': parsed_args.server, + 'status': parsed_args.status, } - if (parsed_args.marker or parsed_args.limit or - parsed_args.changes_since): - if compute_client.api_version < api_versions.APIVersion("2.59"): - msg = _("marker, limit and/or changes_since is not supported " - "for --os-compute-api-version less than 2.59") + if parsed_args.marker: + if compute_client.api_version < api_versions.APIVersion('2.59'): + msg = _( + '--os-compute-api-version 2.59 or greater is required to ' + 'support the --marker option' + ) raise exceptions.CommandError(msg) - if parsed_args.marker: - search_opts['marker'] = parsed_args.marker - if parsed_args.limit: - search_opts['limit'] = parsed_args.limit - if parsed_args.changes_since: - search_opts['changes_since'] = parsed_args.changes_since + search_opts['marker'] = parsed_args.marker + + if parsed_args.limit: + if compute_client.api_version < api_versions.APIVersion('2.59'): + msg = _( + '--os-compute-api-version 2.59 or greater is required to ' + 'support the --limit option' + ) + raise exceptions.CommandError(msg) + search_opts['limit'] = parsed_args.limit + + if parsed_args.changes_since: + if compute_client.api_version < api_versions.APIVersion('2.59'): + msg = _( + '--os-compute-api-version 2.59 or greater is required to ' + 'support the --changes-since option' + ) + raise exceptions.CommandError(msg) + search_opts['changes_since'] = parsed_args.changes_since if parsed_args.changes_before: - if compute_client.api_version < api_versions.APIVersion("2.66"): - msg = _("changes_before is not supported for " - "--os-compute-api-version less than 2.66") + if compute_client.api_version < api_versions.APIVersion('2.66'): + msg = _( + '--os-compute-api-version 2.66 or greater is required to ' + 'support the --changes-before option' + ) raise exceptions.CommandError(msg) search_opts['changes_before'] = parsed_args.changes_before - if parsed_args.project_id or parsed_args.user_id: - if compute_client.api_version < api_versions.APIVersion("2.80"): - msg = _("Project and/or user is not supported for " - "--os-compute-api-version less than 2.80") + if parsed_args.project_id: + if compute_client.api_version < api_versions.APIVersion('2.80'): + msg = _( + '--os-compute-api-version 2.80 or greater is required to ' + 'support the --project option' + ) + raise exceptions.CommandError(msg) + search_opts['project_id'] = parsed_args.project_id + + if parsed_args.user_id: + if compute_client.api_version < api_versions.APIVersion('2.80'): + msg = _( + '--os-compute-api-version 2.80 or greater is required to ' + 'support the --user option' + ) raise exceptions.CommandError(msg) - if parsed_args.project_id: - search_opts['project_id'] = parsed_args.project_id - if parsed_args.user_id: - search_opts['user_id'] = parsed_args.user_id + search_opts['user_id'] = parsed_args.user_id migrations = compute_client.migrations.list(**search_opts) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 3b98c873ec..4b000180ae 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -47,10 +47,14 @@ def setUp(self): self.app.client_manager.compute.server_migrations self.server_migrations_mock.reset_mock() - # Get a shortcut to the compute client volumeManager Mock + # Get a shortcut to the compute client VolumeManager mock self.servers_volumes_mock = self.app.client_manager.compute.volumes self.servers_volumes_mock.reset_mock() + # Get a shortcut to the compute client MigrationManager mock + self.migrations_mock = self.app.client_manager.compute.migrations + self.migrations_mock.reset_mock() + # Get a shortcut to the compute client FlavorManager Mock self.flavors_mock = self.app.client_manager.compute.flavors self.flavors_mock.reset_mock() @@ -3728,34 +3732,7 @@ def test_server_migrate_with_wait_fails(self, mock_wait_for_status): self.assertNotCalled(self.servers_mock.live_migrate) -class TestServerMigration(TestServer): - - def setUp(self): - super(TestServerMigration, self).setUp() - - # Get a shortcut to the compute client ServerManager Mock - self.servers_mock = self.app.client_manager.compute.servers - self.servers_mock.reset_mock() - - self.migrations_mock = ( - self.app.client_manager.compute.migrations) - self.migrations_mock.reset_mock() - - self.server = self.setup_servers_mock(1)[0] - - def setup_servers_mock(self, count): - servers = compute_fakes.FakeServer.create_servers(count=count) - - # This is the return value for utils.find_resource() - self.servers_mock.get = compute_fakes.FakeServer.get_servers(servers) - return servers - - def setup_server_migrations_mock(self, count): - return compute_fakes.FakeServerMigration.create_server_migrations( - count=count) - - -class TestListMigration(TestServerMigration): +class TestListMigration(TestServer): """Test fetch all migrations.""" MIGRATION_COLUMNS = [ @@ -3767,24 +3744,48 @@ class TestListMigration(TestServerMigration): def setUp(self): super(TestListMigration, self).setUp() - self.cmd = server.ListMigration(self.app, None) - self.migrations = self.setup_server_migrations_mock(3) - self.migrations_mock.list.return_value = self.migrations - self.setup_server_migrations_data(self.migrations) + self.server = compute_fakes.FakeServer.create_one_server() + self.servers_mock.get.return_value = self.server - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.20') + self.migrations = compute_fakes.FakeServerMigration\ + .create_server_migrations(count=3) + self.migrations_mock.list.return_value = self.migrations - def setup_server_migrations_data(self, migrations): self.data = (common_utils.get_item_properties( - s, self.MIGRATION_COLUMNS) for s in migrations) + s, self.MIGRATION_COLUMNS) for s in self.migrations) + + # Get the command object to test + self.cmd = server.ListMigration(self.app, None) + + def test_server_migration_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'status': None, + 'host': None, + 'server': None, + } + + self.migrations_mock.list.assert_called_with(**kwargs) + + self.assertEqual(self.MIGRATION_COLUMNS, columns) + self.assertEqual(tuple(self.data), tuple(data)) - def test_server_migraton_list(self): + def test_server_migration_list(self): arglist = [ - '--status', 'migrating' + '--server', 'server1', + '--host', 'host1', + '--status', 'migrating', ] verifylist = [ - ('status', 'migrating') + ('server', 'server1'), + ('host', 'host1'), + ('status', 'migrating'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -3792,8 +3793,8 @@ def test_server_migraton_list(self): # Set expected values kwargs = { 'status': 'migrating', - 'host': None, - 'server': None, + 'host': 'host1', + 'server': 'server1', } self.migrations_mock.list.assert_called_with(**kwargs) @@ -3813,15 +3814,11 @@ class TestListMigrationV223(TestListMigration): def setUp(self): super(TestListMigrationV223, self).setUp() - self.cmd = server.ListMigration(self.app, None) - self.migrations = self.setup_server_migrations_mock(3) - self.migrations_mock.list.return_value = self.migrations - self.setup_server_migrations_data(self.migrations) self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.23') - def test_server_migraton_list(self): + def test_server_migration_list(self): arglist = [ '--status', 'migrating' ] @@ -3858,15 +3855,11 @@ class TestListMigrationV259(TestListMigration): def setUp(self): super(TestListMigrationV259, self).setUp() - self.cmd = server.ListMigration(self.app, None) - self.migrations = self.setup_server_migrations_mock(3) - self.migrations_mock.list.return_value = self.migrations - self.setup_server_migrations_data(self.migrations) self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.59') - def test_server_migraton_list(self): + def test_server_migration_list(self): arglist = [ '--status', 'migrating', '--limit', '1', @@ -3897,7 +3890,7 @@ def test_server_migraton_list(self): self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) - def test_server_migraton_list_with_limit_pre_v259(self): + def test_server_migration_list_with_limit_pre_v259(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.58') arglist = [ @@ -3909,10 +3902,15 @@ def test_server_migraton_list_with_limit_pre_v259(self): ('limit', 1) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.59 or greater is required', + str(ex)) - def test_server_migraton_list_with_marker_pre_v259(self): + def test_server_migration_list_with_marker_pre_v259(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.58') arglist = [ @@ -3924,10 +3922,15 @@ def test_server_migraton_list_with_marker_pre_v259(self): ('marker', 'test_kp') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.59 or greater is required', + str(ex)) - def test_server_migraton_list_with_changes_since_pre_v259(self): + def test_server_migration_list_with_changes_since_pre_v259(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.58') arglist = [ @@ -3939,8 +3942,13 @@ def test_server_migraton_list_with_changes_since_pre_v259(self): ('changes_since', '2019-08-09T08:03:25Z') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.59 or greater is required', + str(ex)) class TestListMigrationV266(TestListMigration): @@ -3954,15 +3962,11 @@ class TestListMigrationV266(TestListMigration): def setUp(self): super(TestListMigrationV266, self).setUp() - self.cmd = server.ListMigration(self.app, None) - self.migrations = self.setup_server_migrations_mock(3) - self.migrations_mock.list.return_value = self.migrations - self.setup_server_migrations_data(self.migrations) self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.66') - def test_server_migraton_list_with_changes_before(self): + def test_server_migration_list_with_changes_before(self): arglist = [ '--status', 'migrating', '--limit', '1', @@ -3996,7 +4000,7 @@ def test_server_migraton_list_with_changes_before(self): self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) - def test_server_migraton_list_with_changes_before_pre_v266(self): + def test_server_migration_list_with_changes_before_pre_v266(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.65') arglist = [ @@ -4008,8 +4012,13 @@ def test_server_migraton_list_with_changes_before_pre_v266(self): ('changes_before', '2019-08-09T08:03:25Z') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.66 or greater is required', + str(ex)) class TestListMigrationV280(TestListMigration): @@ -4023,15 +4032,11 @@ class TestListMigrationV280(TestListMigration): def setUp(self): super(TestListMigrationV280, self).setUp() - self.cmd = server.ListMigration(self.app, None) - self.migrations = self.setup_server_migrations_mock(3) - self.migrations_mock.list.return_value = self.migrations - self.setup_server_migrations_data(self.migrations) self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.80') - def test_server_migraton_list_with_project(self): + def test_server_migration_list_with_project(self): arglist = [ '--status', 'migrating', '--limit', '1', @@ -4086,10 +4091,15 @@ def test_get_migrations_with_project_pre_v280(self): ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.80 or greater is required', + str(ex)) - def test_server_migraton_list_with_user(self): + def test_server_migration_list_with_user(self): arglist = [ '--status', 'migrating', '--limit', '1', @@ -4144,11 +4154,15 @@ def test_get_migrations_with_user_pre_v280(self): ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.80 or greater is required', + str(ex)) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - - def test_server_migraton_list_with_project_and_user(self): + def test_server_migration_list_with_project_and_user(self): arglist = [ '--status', 'migrating', '--limit', '1', @@ -4208,8 +4222,13 @@ def test_get_migrations_with_project_and_user_pre_v280(self): ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.80 or greater is required', + str(ex)) class TestServerMigrationAbort(TestServer): From bf35f04682fa2bdd00a7cff7b9a963b4ffb80eff Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 13 Oct 2020 14:47:25 +0100 Subject: [PATCH 0288/1120] Add 'openstack server migration list --type' option Another gap with novaclient closed. Change-Id: Id3ca95ceda6f438fa72496ab9ab15ac09bb64fa5 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 19 +++++++++++++++---- .../tests/unit/compute/v2/test_server.py | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 2fa0c52403..dec6eb62e6 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1889,6 +1889,14 @@ def get_parser(self, prog_name): metavar='', help=_('Filter migrations by status') ) + parser.add_argument( + '--type', + metavar='', + choices=[ + 'evacuation', 'live-migration', 'cold-migration', 'resize', + ], + help=_('Filter migrations by type'), + ) parser.add_argument( '--marker', metavar='', @@ -1964,10 +1972,6 @@ def print_migrations(self, parsed_args, compute_client, migrations): if compute_client.api_version >= api_versions.APIVersion("2.59"): columns.insert(0, "UUID") - # TODO(brinzhang): It also suppports filter migrations by type - # since 2.1. https://review.opendev.org/#/c/675117 supported - # filtering the migrations by 'migration_type' and 'source_compute' - # in novaclient, that will be added in OSC by follow-up. if compute_client.api_version >= api_versions.APIVersion("2.23"): columns.insert(0, "Id") columns.insert(len(columns) - 2, "Type") @@ -1992,6 +1996,13 @@ def take_action(self, parsed_args): 'status': parsed_args.status, } + if parsed_args.type: + migration_type = parsed_args.type + # we're using an alias because the default value is confusing + if migration_type == 'cold-migration': + migration_type = 'migration' + search_opts['type'] = migration_type + if parsed_args.marker: if compute_client.api_version < api_versions.APIVersion('2.59'): msg = _( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 4b000180ae..51a3beb557 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3781,6 +3781,7 @@ def test_server_migration_list(self): '--server', 'server1', '--host', 'host1', '--status', 'migrating', + '--type', 'cold-migration', ] verifylist = [ ('server', 'server1'), @@ -3795,6 +3796,7 @@ def test_server_migration_list(self): 'status': 'migrating', 'host': 'host1', 'server': 'server1', + 'type': 'migration', } self.migrations_mock.list.assert_called_with(**kwargs) From ab0b1fe885ee0a210a58008b631521025be7f3eb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 13 Oct 2020 15:35:21 +0100 Subject: [PATCH 0289/1120] Validate 'server group create --policy' option We were documenting that some of these policies were only supported with specific microversions, however, we weren't actually enforcing that, leading to a poor user experience. Correct this. Change-Id: Ic3c555226a220efd9b0f27edffccf6c4c95c2747 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_group.py | 25 ++++++++--- .../unit/compute/v2/test_server_group.py | 43 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index 1af6e28dc7..a33632446c 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -56,12 +56,18 @@ def get_parser(self, prog_name): parser.add_argument( '--policy', metavar='', + choices=[ + 'affinity', + 'anti-affinity', + 'soft-affinity', + 'soft-anti-affinity', + ], default='affinity', - help=_("Add a policy to " - "('affinity' or 'anti-affinity', " - "defaults to 'affinity'). Specify --os-compute-api-version " - "2.15 or higher for the 'soft-affinity' or " - "'soft-anti-affinity' policy.") + help=_( + "Add a policy to " + "Specify --os-compute-api-version 2.15 or higher for the " + "'soft-affinity' or 'soft-anti-affinity' policy." + ) ) return parser @@ -69,9 +75,18 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute info = {} + if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'): + if compute_client.api_version < api_versions.APIVersion('2.15'): + msg = _( + '--os-compute-api-version 2.15 or greater is required to ' + 'support the %s policy' + ) + raise exceptions.CommandError(msg % parsed_args.policy) + policy_arg = {'policies': [parsed_args.policy]} if compute_client.api_version >= api_versions.APIVersion("2.64"): policy_arg = {'policy': parsed_args.policy} + server_group = compute_client.server_groups.create( name=parsed_args.name, **policy_arg) diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 359cd2bd02..bf0ea0ba45 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -90,6 +90,28 @@ def setUp(self): self.cmd = server_group.CreateServerGroup(self.app, None) def test_server_group_create(self): + arglist = [ + '--policy', 'anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'anti-affinity'), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.server_groups_mock.create.assert_called_once_with( + name=parsed_args.name, + policies=[parsed_args.policy], + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_server_group_create_with_soft_policies(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.15') + arglist = [ '--policy', 'soft-anti-affinity', 'affinity_group', @@ -108,6 +130,27 @@ def test_server_group_create(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_server_group_create_with_soft_policies_pre_v215(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.14') + + arglist = [ + '--policy', 'soft-anti-affinity', + 'affinity_group', + ] + verifylist = [ + ('policy', 'soft-anti-affinity'), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.15 or greater is required', + str(ex)) + def test_server_group_create_v264(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.64') From 2f76bfa3a69b6867491a6c5f0bf3c7e9f62743ca Mon Sep 17 00:00:00 2001 From: tianhui Date: Fri, 18 May 2018 18:59:37 +0800 Subject: [PATCH 0290/1120] Compute: Add tags support for server Change-Id: If065602792958ff0145ae9f2e05f5b7a3177905c Story: 2002006 Task: 19641 --- openstackclient/compute/v2/server.py | 115 +++++++- .../tests/unit/compute/v2/test_server.py | 260 ++++++++++++++++++ .../server-add-tag-63f9cd01dbd82d1b.yaml | 14 + 3 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 89ea006709..09842f8874 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -828,6 +828,18 @@ def get_parser(self, prog_name): action='store_true', help=_('Wait for build to complete'), ) + parser.add_argument( + '--tag', + metavar='', + action='append', + default=[], + dest='tags', + help=_( + 'Tags for the server. ' + 'Specify multiple times to add multiple tags. ' + '(supported by --os-compute-api-version 2.52 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -1141,6 +1153,16 @@ def _match_image(image_api, wanted_properties): if parsed_args.description: boot_kwargs['description'] = parsed_args.description + if parsed_args.tags: + if compute_client.api_version < api_versions.APIVersion('2.52'): + msg = _( + '--os-compute-api-version 2.52 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + boot_kwargs['tags'] = parsed_args.tags + if parsed_args.host: if compute_client.api_version < api_versions.APIVersion("2.74"): msg = _("Specifying --host is not supported for " @@ -1408,6 +1430,30 @@ def get_parser(self, prog_name): help=_('Only display unlocked servers. ' 'Requires ``--os-compute-api-version`` 2.73 or greater.'), ) + parser.add_argument( + '--tags', + metavar='', + action='append', + default=[], + dest='tags', + help=_( + 'Only list servers with the specified tag. ' + 'Specify multiple times to filter on multiple tags. ' + '(supported by --os-compute-api-version 2.26 or above)' + ), + ) + parser.add_argument( + '--not-tags', + metavar='', + action='append', + default=[], + dest='not_tags', + help=_( + 'Only list servers without the specified tag. ' + 'Specify multiple times to filter on multiple tags. ' + '(supported by --os-compute-api-version 2.26 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -1463,6 +1509,27 @@ def take_action(self, parsed_args): 'changes-before': parsed_args.changes_before, 'changes-since': parsed_args.changes_since, } + + if parsed_args.tags: + if compute_client.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + search_opts['tags'] = parsed_args.tags + + if parsed_args.not_tags: + if compute_client.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --not-tag option' + ) + raise exceptions.CommandError(msg) + + search_opts['not-tags'] = parsed_args.not_tags + support_locked = (compute_client.api_version >= api_versions.APIVersion('2.73')) if not support_locked and (parsed_args.locked or parsed_args.unlocked): @@ -2795,6 +2862,18 @@ def get_parser(self, prog_name): help=_('New server description (supported by ' '--os-compute-api-version 2.19 or above)'), ) + parser.add_argument( + '--tag', + metavar='', + action='append', + default=[], + dest='tags', + help=_( + 'Tag for the server. ' + 'Specify multiple times to add multiple tags. ' + '(supported by --os-compute-api-version 2.26 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -2833,6 +2912,17 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) server.update(description=parsed_args.description) + if parsed_args.tags: + if server.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + for tag in parsed_args.tags: + server.add_tag(tag=tag) + class ShelveServer(command.Command): _description = _("Shelve server(s)") @@ -3174,7 +3264,7 @@ def take_action(self, parsed_args): class UnsetServer(command.Command): - _description = _("Unset server properties") + _description = _("Unset server properties and tags") def get_parser(self, prog_name): parser = super(UnsetServer, self).get_parser(prog_name) @@ -3198,6 +3288,18 @@ def get_parser(self, prog_name): help=_('Unset server description (supported by ' '--os-compute-api-version 2.19 or above)'), ) + parser.add_argument( + '--tag', + metavar='', + action='append', + default=[], + dest='tags', + help=_( + 'Tag to remove from the server. ' + 'Specify multiple times to remove multiple tags. ' + '(supported by --os-compute-api-version 2.26 or later' + ), + ) return parser def take_action(self, parsed_args): @@ -3223,6 +3325,17 @@ def take_action(self, parsed_args): description="", ) + if parsed_args.tags: + if compute_client.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + + for tag in parsed_args.tags: + compute_client.servers.delete_tag(server, tag=tag) + class UnshelveServer(command.Command): _description = _("Unshelve server(s)") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5f1d5d0620..544e013740 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2434,6 +2434,87 @@ def test_server_create_with_description_api_older(self): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_create_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.52') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--tag', 'tag1', + '--tag', 'tag2', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('tags', ['tag1', 'tag2']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'block_device_mapping_v2': [], + 'admin_pass': None, + 'nics': 'auto', + 'scheduler_hints': {}, + 'config_drive': None, + 'tags': ['tag1', 'tag2'], + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_tag_pre_v252(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.51') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--tag', 'tag1', + '--tag', 'tag2', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('tags', ['tag1', 'tag2']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.52 or greater is required', + str(ex)) + def test_server_create_with_host_v274(self): # Explicit host is supported for nova api version 2.74 or above @@ -3206,6 +3287,7 @@ def test_server_list_v266_with_changes_before(self): self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' self.search_opts['deleted'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) @@ -3298,6 +3380,92 @@ def test_server_list_v269_with_partial_constructs(self): 'UNKNOWN', '', '', '') self.assertEqual(expected_row, partial_server) + def test_server_list_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.26') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['tags'] = ['tag1', 'tag2'] + + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_tag_pre_v225(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.25') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.26 or greater is required', + str(ex)) + + def test_server_list_with_not_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.26') + + arglist = [ + '--not-tag', 'tag1', + '--not-tag', 'tag2', + ] + verifylist = [ + ('not_tags', ['tag1', 'tag2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['not-tags'] = ['tag1', 'tag2'] + + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_not_tag_pre_v226(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.25') + + arglist = [ + '--not-tag', 'tag1', + '--not-tag', 'tag2', + ] + verifylist = [ + ('not_tags', ['tag1', 'tag2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.26 or greater is required', + str(ex)) + class TestServerLock(TestServer): @@ -5388,6 +5556,8 @@ def setUp(self): 'update': None, 'reset_state': None, 'change_password': None, + 'add_tag': None, + 'set_tags': None, } self.fake_servers = self.setup_servers_mock(2) @@ -5528,6 +5698,50 @@ def test_server_set_with_description_api_older(self): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_set_with_tag(self): + self.fake_servers[0].api_version = api_versions.APIVersion('2.26') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.fake_servers[0].add_tag.assert_has_calls([ + mock.call(tag='tag1'), + mock.call(tag='tag2'), + ]) + self.assertIsNone(result) + + def test_server_set_with_tag_pre_v226(self): + self.fake_servers[0].api_version = api_versions.APIVersion('2.25') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.26 or greater is required', + str(ex)) + class TestServerShelve(TestServer): @@ -5853,6 +6067,52 @@ def test_server_unset_with_description_api_older(self): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_unset_with_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.26') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.delete_tag.assert_has_calls([ + mock.call(self.fake_server, tag='tag1'), + mock.call(self.fake_server, tag='tag2'), + ]) + + def test_server_unset_with_tag_pre_v226(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.25') + + arglist = [ + '--tag', 'tag1', + '--tag', 'tag2', + 'foo_vm', + ] + verifylist = [ + ('tags', ['tag1', 'tag2']), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.26 or greater is required', + str(ex)) + class TestServerUnshelve(TestServer): diff --git a/releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml b/releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml new file mode 100644 index 0000000000..78e7482c48 --- /dev/null +++ b/releasenotes/notes/server-add-tag-63f9cd01dbd82d1b.yaml @@ -0,0 +1,14 @@ +--- +features: + - Add ``--tag`` option to ``server create`` command to add tags when creating + a server. + Only available starting with ``--os-compute-api-version 2.52``. + - Add ``--tag`` option to ``server set`` command to add a tag to an + existing server. + Only available starting with ``--os-compute-api-version 2.26``. + - Add ``--tag`` options to ``server unset`` command to remove a tag from an + existing server. + Only available starting with ``--os-compute-api-version 2.26``. + - Add ``--tags`` and ``--not-tags`` options to ``server list`` command to + list instances with and without the specified tag(s), respectively. + Only available starting with ``--os-compute-api-version 2.26``. From 98a0016cfa1d39bcc37144f0c7700e9a9d6b2c0b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 29 Sep 2020 16:30:34 +0100 Subject: [PATCH 0291/1120] Add support for 'keypairs list --user' parameter This has been supported by nova and novaclient since the veritable dark ages. Add it to OSC. Change-Id: Ifc95e7dd6c00807c80e87e10046ab154d0989014 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/keypair.py | 35 +++++++++- .../tests/unit/compute/v2/test_keypair.py | 66 ++++++++++++++++++- ...keypairs-user-filter-e1ce57a4c09c278b.yaml | 6 ++ 3 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-keypairs-user-filter-e1ce57a4c09c278b.yaml diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 6affaca302..ae653e76ee 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -26,6 +26,7 @@ from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) @@ -163,13 +164,45 @@ def take_action(self, parsed_args): class ListKeypair(command.Lister): _description = _("List key fingerprints") + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--user', + metavar='', + help=_( + 'Show keypairs for another user (admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_user_domain_option_to_parser(parser) + return parser + def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity + + kwargs = {} + + if parsed_args.user: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --user option' + ) + raise exceptions.CommandError(msg) + + kwargs['user_id'] = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ).id + + data = compute_client.keypairs.list(**kwargs) + columns = ( "Name", "Fingerprint" ) - data = compute_client.keypairs.list() if compute_client.api_version >= api_versions.APIVersion('2.2'): columns += ("Type", ) diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index ca3bfe7c8b..5e6a47414f 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -13,6 +13,7 @@ # under the License. # +import copy from unittest import mock from unittest.mock import call import uuid @@ -23,6 +24,8 @@ from openstackclient.compute.v2 import keypair from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.unit import utils as tests_utils @@ -307,6 +310,14 @@ class TestKeypairList(TestKeypair): def setUp(self): super(TestKeypairList, self).setUp() + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.keypairs_mock.list.return_value = self.keypairs # Get the command object to test @@ -334,8 +345,8 @@ def test_keypair_list_no_options(self): ) def test_keypair_list_v22(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.2') + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.2') arglist = [] verifylist = [] @@ -361,6 +372,57 @@ def test_keypair_list_v22(self): tuple(data) ) + def test_keypair_list_with_user(self): + + # Filtering by user is support for nova api 2.10 or above + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.get.assert_called_with(identity_fakes.user_name) + self.keypairs_mock.list.assert_called_with( + user_id=identity_fakes.user_id, + ) + + self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) + self.assertEqual( + (( + self.keypairs[0].name, + self.keypairs[0].fingerprint, + self.keypairs[0].type, + ), ), + tuple(data) + ) + + def test_keypair_list_with_user_pre_v210(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.9') + + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.10 or greater is required', str(ex)) + class TestKeypairShow(TestKeypair): diff --git a/releasenotes/notes/add-keypairs-user-filter-e1ce57a4c09c278b.yaml b/releasenotes/notes/add-keypairs-user-filter-e1ce57a4c09c278b.yaml new file mode 100644 index 0000000000..6e82091a0f --- /dev/null +++ b/releasenotes/notes/add-keypairs-user-filter-e1ce57a4c09c278b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + It is now possible to list the keypairs for a specific user using the + ``--user`` parameter. This is an admin-only action by default and requires + Compute API microversion 2.10 or later. From 5645fad7622a18e4f3c550ee7d409bf1b685a1a5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 29 Sep 2020 16:51:12 +0100 Subject: [PATCH 0292/1120] Add support for 'keypairs list --project' parameter It would be lovely to do this server side but doing so requires a new microversion, a blueprint and a spec. This is less performant but should do the trick for the odd time users want to do this. Change-Id: I26e7d38966304dd67be5da8ed0bb24f87191b82f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/keypair.py | 44 +++++++-- .../tests/unit/compute/v2/test_keypair.py | 95 +++++++++++++++++-- ...pairs-project-filter-99cb6938f247927f.yaml | 6 ++ 3 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/add-keypairs-project-filter-99cb6938f247927f.yaml diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index ae653e76ee..8c365cf0fe 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -166,7 +166,8 @@ class ListKeypair(command.Lister): def get_parser(self, prog_name): parser = super().get_parser(prog_name) - parser.add_argument( + user_group = parser.add_mutually_exclusive_group() + user_group.add_argument( '--user', metavar='', help=_( @@ -175,15 +176,44 @@ def get_parser(self, prog_name): ), ) identity_common.add_user_domain_option_to_parser(parser) + user_group.add_argument( + '--project', + metavar='', + help=_( + 'Show keypairs for all users associated with project ' + '(admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute identity_client = self.app.client_manager.identity - kwargs = {} + if parsed_args.project: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --project option' + ) + raise exceptions.CommandError(msg) - if parsed_args.user: + # NOTE(stephenfin): This is done client side because nova doesn't + # currently support doing so server-side. If this is slow, we can + # think about spinning up a threadpool or similar. + project = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + users = identity_client.users.list(tenant_id=project) + + data = [] + for user in users: + data.extend(compute_client.keypairs.list(user_id=user.id)) + elif parsed_args.user: if compute_client.api_version < api_versions.APIVersion('2.10'): msg = _( '--os-compute-api-version 2.10 or greater is required to ' @@ -191,13 +221,15 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - kwargs['user_id'] = identity_common.find_user( + user = identity_common.find_user( identity_client, parsed_args.user, parsed_args.user_domain, - ).id + ) - data = compute_client.keypairs.list(**kwargs) + data = compute_client.keypairs.list(user_id=user.id) + else: + data = compute_client.keypairs.list() columns = ( "Name", diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 5e6a47414f..286cbb0929 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -310,14 +310,6 @@ class TestKeypairList(TestKeypair): def setUp(self): super(TestKeypairList, self).setUp() - self.users_mock = self.app.client_manager.identity.users - self.users_mock.reset_mock() - self.users_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.USER), - loaded=True, - ) - self.keypairs_mock.list.return_value = self.keypairs # Get the command object to test @@ -378,6 +370,14 @@ def test_keypair_list_with_user(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.10') + users_mock = self.app.client_manager.identity.users + users_mock.reset_mock() + users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + arglist = [ '--user', identity_fakes.user_name, ] @@ -388,7 +388,7 @@ def test_keypair_list_with_user(self): columns, data = self.cmd.take_action(parsed_args) - self.users_mock.get.assert_called_with(identity_fakes.user_name) + users_mock.get.assert_called_with(identity_fakes.user_name) self.keypairs_mock.list.assert_called_with( user_id=identity_fakes.user_id, ) @@ -423,6 +423,83 @@ def test_keypair_list_with_user_pre_v210(self): self.assertIn( '--os-compute-api-version 2.10 or greater is required', str(ex)) + def test_keypair_list_with_project(self): + + # Filtering by user is support for nova api 2.10 or above + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + projects_mock = self.app.client_manager.identity.tenants + projects_mock.reset_mock() + projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + users_mock = self.app.client_manager.identity.users + users_mock.reset_mock() + users_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ), + ] + + arglist = ['--project', identity_fakes.project_name] + verifylist = [('project', identity_fakes.project_name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + projects_mock.get.assert_called_with(identity_fakes.project_name) + users_mock.list.assert_called_with(tenant_id=identity_fakes.project_id) + self.keypairs_mock.list.assert_called_with( + user_id=identity_fakes.user_id, + ) + + self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) + self.assertEqual( + (( + self.keypairs[0].name, + self.keypairs[0].fingerprint, + self.keypairs[0].type, + ), ), + tuple(data) + ) + + def test_keypair_list_with_project_pre_v210(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.9') + + arglist = ['--project', identity_fakes.project_name] + verifylist = [('project', identity_fakes.project_name)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.10 or greater is required', str(ex)) + + def test_keypair_list_conflicting_user_options(self): + + # Filtering by user is support for nova api 2.10 or above + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + arglist = [ + '--user', identity_fakes.user_name, + '--project', identity_fakes.project_name, + ] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, self.cmd, arglist, None) + class TestKeypairShow(TestKeypair): diff --git a/releasenotes/notes/add-keypairs-project-filter-99cb6938f247927f.yaml b/releasenotes/notes/add-keypairs-project-filter-99cb6938f247927f.yaml new file mode 100644 index 0000000000..77e7afe6ba --- /dev/null +++ b/releasenotes/notes/add-keypairs-project-filter-99cb6938f247927f.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + It is now possible to list the keypairs for all users in a project using + the ``--project`` parameter. This is an admin-only action by default and + requires Compute API microversion 2.10 or later. From f464bba792b45691e8667dcf966ac703119ff3c0 Mon Sep 17 00:00:00 2001 From: "wu.chunyang" Date: Wed, 21 Oct 2020 23:40:27 +0800 Subject: [PATCH 0293/1120] Remove the unused coding style modules Python modules related to coding style checks (listed in blacklist.txt in openstack/requirements repo) are dropped from lower-constraints.txt they are not needed during installation. Change-Id: Id735af397e8d41ad3ff583386844d4131fc9e12c --- lower-constraints.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 2fa6586e08..74eac6403b 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -21,15 +21,12 @@ eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 -flake8-import-order==0.13 -flake8==2.6.2 future==0.16.0 futurist==2.1.0 gitdb==0.6.4 GitPython==1.0.1 gnocchiclient==3.3.1 greenlet==0.4.15 -hacking==2.0.0 httplib2==0.9.1 idna==2.6 iso8601==0.1.11 @@ -42,7 +39,6 @@ keystoneauth1==3.18.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.0 -mccabe==0.2.1 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 @@ -78,7 +74,6 @@ prettytable==0.7.2 pyasn1==0.1.8 pycodestyle==2.0.0 pycparser==2.18 -pyflakes==0.8.1 pyinotify==0.9.6 pyOpenSSL==17.1.0 pyparsing==2.1.0 From 5f650853f7908bc04555ea3ee393798502b79fed Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 22 Oct 2020 09:52:57 +0100 Subject: [PATCH 0294/1120] Remove references to setuptools Newer versions of cliff and stevedore use importlib rather than setuptools to work with entry points. Replace any references to "setuptools' entry points mechanism" with "Python's entry points mechanism". Change-Id: Iae36155685ee37ab5e38a0c173110a5ece33d05d Signed-off-by: Stephen Finucane --- doc/source/cli/commands.rst | 2 +- doc/source/contributor/command-wrappers.rst | 2 +- doc/source/contributor/plugins.rst | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 97a829b558..497c79f0b9 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -306,7 +306,7 @@ Implementation -------------- The command structure is designed to support seamless addition of plugin -command modules via ``setuptools`` entry points. The plugin commands must +command modules via Python's *entry points* mechanism. The plugin commands must be subclasses of Cliff's ``command.Command`` object. See :ref:`plugins` for more information. diff --git a/doc/source/contributor/command-wrappers.rst b/doc/source/contributor/command-wrappers.rst index 2a5d92239d..cf4eece23a 100644 --- a/doc/source/contributor/command-wrappers.rst +++ b/doc/source/contributor/command-wrappers.rst @@ -7,7 +7,7 @@ We do this with a message logged at WARNING level before any command output is emitted. OpenStackClient command classes are derived from the ``cliff`` classes. -Cliff uses ``setuptools`` entry points for dispatching the parsed command +Cliff uses Python's *entry points* mechanism for dispatching the parsed command to the respective handler classes. This lends itself to modifying the command execution at run-time. diff --git a/doc/source/contributor/plugins.rst b/doc/source/contributor/plugins.rst index 374b274e6a..7ea48edd38 100644 --- a/doc/source/contributor/plugins.rst +++ b/doc/source/contributor/plugins.rst @@ -5,9 +5,8 @@ Plugins ======= The OpenStackClient plugin system is designed so that the plugin need only be -properly installed for OSC to find and use it. It utilizes the -``setuptools`` entry points mechanism to advertise to OSC the -plugin module and supported commands. +properly installed for OSC to find and use it. It utilizes Python's *entry +points* mechanism to advertise to OSC the plugin module and supported commands. Adoption ======== From e05d39abb56b2bf048533b84f44406d415210ffd Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 11 Sep 2020 14:31:18 +0200 Subject: [PATCH 0295/1120] Switch console url show operations to SDK Switch from using novaclient to SDK for openstack console url show operation. Depends-On: https://review.opendev.org/756286 Change-Id: Ibe247825148788c549c2c1e991aae92338cdf557 --- lower-constraints.txt | 4 +- openstackclient/compute/v2/console.py | 35 ++++---- .../tests/unit/compute/v2/test_console.py | 86 +++++++------------ requirements.txt | 4 +- 4 files changed, 54 insertions(+), 75 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 74eac6403b..52766b2290 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -5,7 +5,7 @@ asn1crypto==0.23.0 bandit==1.1.0 cachetools==2.0.0 cffi==1.14.0 -cliff==2.8.0 +cliff==3.4.0 cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 @@ -48,7 +48,7 @@ netifaces==0.10.4 openstacksdk==0.48.0 os-service-types==1.7.0 os-testr==1.0.0 -osc-lib==2.0.0 +osc-lib==2.2.0 osc-placement==1.7.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index f0abaf4cf4..0ab5c8a2a3 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -22,6 +22,15 @@ from openstackclient.i18n import _ +def _get_console_columns(item): + # To maintain backwards compatibility we need to rename sdk props to + # whatever OSC was using before + column_map = {} + hidden_columns = ['id', 'links', 'location', 'name'] + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + class ShowConsoleLog(command.Command): _description = _("Show server's console output") @@ -119,21 +128,15 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - server = utils.find_resource( - compute_client.servers, + compute_client = self.app.client_manager.sdk_connection.compute + server = compute_client.find_server( parsed_args.server, - ) + ignore_missing=False) + + data = compute_client.create_console(server.id, + console_type=parsed_args.url_type) + + display_columns, columns = _get_console_columns(data) + data = utils.get_dict_properties(data, columns) - data = server.get_console_url(parsed_args.url_type) - if not data: - return ({}, {}) - - info = {} - # NOTE(Rui Chen): Return 'remote_console' in compute microversion API - # 2.6 and later, return 'console' in compute - # microversion API from 2.0 to 2.5, do compatibility - # handle for different microversion API. - console_data = data.get('remote_console', data.get('console')) - info.update(console_data) - return zip(*sorted(info.items())) + return (display_columns, data) diff --git a/openstackclient/tests/unit/compute/v2/test_console.py b/openstackclient/tests/unit/compute/v2/test_console.py index 1c6d658bc1..db9603c9b9 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -32,9 +32,6 @@ def setUp(self): self.sdk_client.find_server = mock.Mock() self.sdk_client.get_server_console_output = mock.Mock() - self.servers_mock = self.app.client_manager.compute.servers - self.servers_mock.reset_mock() - class TestConsoleLog(TestConsole): _server = compute_fakes.FakeServer.create_one_server() @@ -107,18 +104,16 @@ def test_show_lines(self): class TestConsoleUrlShow(TestConsole): + _server = compute_fakes.FakeServer.create_one_server() def setUp(self): super(TestConsoleUrlShow, self).setUp() - fake_console_data = {'remote_console': {'url': 'http://localhost', - 'protocol': 'fake_protocol', - 'type': 'fake_type'}} - methods = { - 'get_console_url': fake_console_data - } - self.fake_server = compute_fakes.FakeServer.create_one_server( - methods=methods) - self.servers_mock.get.return_value = self.fake_server + self.sdk_client.find_server.return_value = self._server + fake_console_data = {'url': 'http://localhost', + 'protocol': 'fake_protocol', + 'type': 'fake_type'} + self.sdk_client.create_console = mock.Mock( + return_value=fake_console_data) self.columns = ( 'protocol', @@ -126,9 +121,9 @@ def setUp(self): 'url', ) self.data = ( - fake_console_data['remote_console']['protocol'], - fake_console_data['remote_console']['type'], - fake_console_data['remote_console']['url'] + fake_console_data['protocol'], + fake_console_data['type'], + fake_console_data['url'] ) self.cmd = console.ShowConsoleURL(self.app, None) @@ -143,7 +138,9 @@ def test_console_url_show_by_default(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('novnc') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='novnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -158,7 +155,9 @@ def test_console_url_show_with_novnc(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('novnc') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='novnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -173,7 +172,9 @@ def test_console_url_show_with_xvpvnc(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('xvpvnc') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='xvpvnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -188,41 +189,12 @@ def test_console_url_show_with_spice(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with( - 'spice-html5') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='spice-html5') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_console_url_show_compatible(self): - methods = { - 'get_console_url': {'console': {'url': 'http://localhost', - 'type': 'fake_type'}}, - } - old_fake_server = compute_fakes.FakeServer.create_one_server( - methods=methods) - old_columns = ( - 'type', - 'url', - ) - old_data = ( - methods['get_console_url']['console']['type'], - methods['get_console_url']['console']['url'] - ) - arglist = [ - 'foo_vm', - ] - verifylist = [ - ('url_type', 'novnc'), - ('server', 'foo_vm'), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(self.servers_mock, 'get', - return_value=old_fake_server): - columns, data = self.cmd.take_action(parsed_args) - old_fake_server.get_console_url.assert_called_once_with('novnc') - self.assertEqual(old_columns, columns) - self.assertEqual(old_data, data) - def test_console_url_show_with_rdp(self): arglist = [ '--rdp', @@ -234,8 +206,9 @@ def test_console_url_show_with_rdp(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with( - 'rdp-html5') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='rdp-html5') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -250,8 +223,9 @@ def test_console_url_show_with_serial(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with( - 'serial') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='serial') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -266,6 +240,8 @@ def test_console_url_show_with_mks(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_console_url.assert_called_once_with('webmks') + self.sdk_client.create_console.assert_called_once_with( + self._server.id, + console_type='webmks') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/requirements.txt b/requirements.txt index ee6b6241ae..99d409bf51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,10 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -cliff!=2.9.0,>=2.8.0 # Apache-2.0 +cliff>=3.4.0 # Apache-2.0 iso8601>=0.1.11 # MIT openstacksdk>=0.48.0 # Apache-2.0 -osc-lib>=2.0.0 # Apache-2.0 +osc-lib>=2.2.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=15.1.0 # Apache-2.0 From 5b04a86d1f59b13014839a826ff3c530fe330235 Mon Sep 17 00:00:00 2001 From: Elod Illes Date: Wed, 28 Oct 2020 12:37:58 +0100 Subject: [PATCH 0296/1120] Replace deprecated UPPER_CONSTRAINTS_FILE variable UPPER_CONSTRAINTS_FILE is deprecated and TOX_CONSTRAINTS_FILE is the new environment variable name that replaces it [1]. [1] https://zuul-ci.org/docs/zuul-jobs/python-roles.html#rolevar-tox.tox_constraints_file Change-Id: Ic0d2dcf20f7929733deb10a275e5d67f9627428f --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index c0f6dd5db1..663463e2f2 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ setenv = OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} @@ -88,7 +88,7 @@ commands = [testenv:venv] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = {posargs} @@ -110,7 +110,7 @@ commands = [testenv:docs] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html @@ -120,7 +120,7 @@ commands = [testenv:releasenotes] deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From 9b02f37e8f06bafb16a139aa1c978b4dd939f35e Mon Sep 17 00:00:00 2001 From: likui Date: Thu, 29 Oct 2020 09:53:16 +0800 Subject: [PATCH 0297/1120] update lower-constraints.txt We also need to change the lower-constraint requirements to make them py3.8 compatible. See https://bugs.launchpad.net/nova/+bug/1886298 MarkupSafe==1.1.1 paramiko==2.7.1 Change-Id: I04a0fcd98327b9f41e24e19bcab97c813760f414 --- lower-constraints.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 74eac6403b..32f6ee9e9b 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ jsonschema==2.6.0 keystoneauth1==3.18.0 kombu==4.0.0 linecache2==1.0.0 -MarkupSafe==1.1.0 +MarkupSafe==1.1.1 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 @@ -62,7 +62,7 @@ oslo.service==1.24.0 oslo.utils==3.33.0 oslotest==3.2.0 osprofiler==1.4.0 -paramiko==2.0.0 +paramiko==2.7.1 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 From 7c239403e98f7355c39aee696c2fd6f0c6328af5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 12 Oct 2020 15:12:08 +0100 Subject: [PATCH 0298/1120] trivial: Rework 'CreateServer' function This rather complex function has had stuff tacked on over the years. Help make working with it a bit easier through liberal application of whitespace and some nicer indentation. Some option help text is improved based on changes to modern nova. Change-Id: I8154dd395dd904c3bcd180a7d0f9037b7e0be64f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 295 ++++++++++++++++----------- 1 file changed, 178 insertions(+), 117 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 09842f8874..01132819d5 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -620,13 +620,15 @@ def get_parser(self, prog_name): disk_group.add_argument( '--volume', metavar='', - help=_('Create server using this volume as the boot disk (name ' - 'or ID).\n' - 'This option automatically creates a block device mapping ' - 'with a boot index of 0. On many hypervisors (libvirt/kvm ' - 'for example) this will be device vda. Do not create a ' - 'duplicate mapping using --block-device-mapping for this ' - 'volume.'), + help=_( + 'Create server using this volume as the boot disk (name or ID)' + '\n' + 'This option automatically creates a block device mapping ' + 'with a boot index of 0. On many hypervisors (libvirt/kvm ' + 'for example) this will be device vda. Do not create a ' + 'duplicate mapping using --block-device-mapping for this ' + 'volume.' + ), ) parser.add_argument( '--password', @@ -644,28 +646,34 @@ def get_parser(self, prog_name): metavar='', action='append', default=[], - help=_('Security group to assign to this server (name or ID) ' - '(repeat option to set multiple groups)'), + help=_( + 'Security group to assign to this server (name or ID) ' + '(repeat option to set multiple groups)' + ), ) parser.add_argument( '--key-name', metavar='', - help=_('Keypair to inject into this server (optional extension)'), + help=_('Keypair to inject into this server'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Set a property on this server ' - '(repeat option to set multiple values)'), + help=_( + 'Set a property on this server ' + '(repeat option to set multiple values)' + ), ) parser.add_argument( '--file', metavar='', action='append', default=[], - help=_('File to inject into image before boot ' - '(repeat option to set multiple files)'), + help=_( + 'File to inject into image before boot ' + '(repeat option to set multiple files)' + ), ) parser.add_argument( '--user-data', @@ -675,8 +683,10 @@ def get_parser(self, prog_name): parser.add_argument( '--description', metavar='', - help=_('Set description for the server (supported by ' - '--os-compute-api-version 2.19 or above)'), + help=_( + 'Set description for the server ' + '(supported by --os-compute-api-version 2.19 or above)' + ), ) parser.add_argument( '--availability-zone', @@ -686,29 +696,35 @@ def get_parser(self, prog_name): parser.add_argument( '--host', metavar='', - help=_('Requested host to create servers. Admin only ' - 'by default. (supported by --os-compute-api-version 2.74 ' - 'or above)'), + help=_( + 'Requested host to create servers. ' + '(admin only) ' + '(supported by --os-compute-api-version 2.74 or above)' + ), ) parser.add_argument( '--hypervisor-hostname', metavar='', - help=_('Requested hypervisor hostname to create servers. Admin ' - 'only by default. (supported by --os-compute-api-version ' - '2.74 or above)'), + help=_( + 'Requested hypervisor hostname to create servers. ' + '(admin only) ' + '(supported by --os-compute-api-version 2.74 or above)' + ), ) parser.add_argument( '--boot-from-volume', metavar='', type=int, - help=_('When used in conjunction with the ``--image`` or ' - '``--image-property`` option, this option automatically ' - 'creates a block device mapping with a boot index of 0 ' - 'and tells the compute service to create a volume of the ' - 'given size (in GB) from the specified image and use it ' - 'as the root disk of the server. The root volume will not ' - 'be deleted when the server is deleted. This option is ' - 'mutually exclusive with the ``--volume`` option.') + help=_( + 'When used in conjunction with the ``--image`` or ' + '``--image-property`` option, this option automatically ' + 'creates a block device mapping with a boot index of 0 ' + 'and tells the compute service to create a volume of the ' + 'given size (in GB) from the specified image and use it ' + 'as the root disk of the server. The root volume will not ' + 'be deleted when the server is deleted. This option is ' + 'mutually exclusive with the ``--volume`` option.' + ) ) parser.add_argument( '--block-device-mapping', @@ -718,37 +734,40 @@ def get_parser(self, prog_name): # NOTE(RuiChen): Add '\n' at the end of line to put each item in # the separated line, avoid the help message looks # messy, see _SmartHelpFormatter in cliff. - help=_('Create a block device on the server.\n' - 'Block device mapping in the format\n' - '=:::\n' - ': block device name, like: vdb, xvdc ' - '(required)\n' - ': Name or ID of the volume, volume snapshot or image ' - '(required)\n' - ': volume, snapshot or image; default: volume ' - '(optional)\n' - ': volume size if create from image or snapshot ' - '(optional)\n' - ': true or false; default: false ' - '(optional)\n' - '(optional extension)'), + help=_( + 'Create a block device on the server.\n' + 'Block device mapping in the format\n' + '=:::\n' + ': block device name, like: vdb, xvdc ' + '(required)\n' + ': Name or ID of the volume, volume snapshot or image ' + '(required)\n' + ': volume, snapshot or image; default: volume ' + '(optional)\n' + ': volume size if create from image or snapshot ' + '(optional)\n' + ': true or false; default: false ' + '(optional)\n' + ), ) parser.add_argument( '--nic', metavar="", action='append', - help=_("Create a NIC on the server. " - "Specify option multiple times to create multiple NICs. " - "Either net-id or port-id must be provided, but not both. " - "net-id: attach NIC to network with this UUID, " - "port-id: attach NIC to port with this UUID, " - "v4-fixed-ip: IPv4 fixed address for NIC (optional), " - "v6-fixed-ip: IPv6 fixed address for NIC (optional), " - "none: (v2.37+) no network is attached, " - "auto: (v2.37+) the compute service will automatically " - "allocate a network. Specifying a --nic of auto or none " - "cannot be used with any other --nic value."), + help=_( + "Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "Either net-id or port-id must be provided, but not both. " + "net-id: attach NIC to network with this UUID, " + "port-id: attach NIC to port with this UUID, " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "none: (v2.37+) no network is attached, " + "auto: (v2.37+) the compute service will automatically " + "allocate a network. Specifying a --nic of auto or none " + "cannot be used with any other --nic value." + ), ) parser.add_argument( '--network', @@ -756,13 +775,15 @@ def get_parser(self, prog_name): action='append', dest='nic', type=_prefix_checked_value('net-id='), - help=_("Create a NIC on the server and connect it to network. " - "Specify option multiple times to create multiple NICs. " - "This is a wrapper for the '--nic net-id=' " - "parameter that provides simple syntax for the standard " - "use case of connecting a new server to a given network. " - "For more advanced use cases, refer to the '--nic' " - "parameter."), + help=_( + "Create a NIC on the server and connect it to network. " + "Specify option multiple times to create multiple NICs. " + "This is a wrapper for the '--nic net-id=' " + "parameter that provides simple syntax for the standard " + "use case of connecting a new server to a given network. " + "For more advanced use cases, refer to the '--nic' " + "parameter." + ), ) parser.add_argument( '--port', @@ -770,12 +791,14 @@ def get_parser(self, prog_name): action='append', dest='nic', type=_prefix_checked_value('port-id='), - help=_("Create a NIC on the server and connect it to port. " - "Specify option multiple times to create multiple NICs. " - "This is a wrapper for the '--nic port-id=' " - "parameter that provides simple syntax for the standard " - "use case of connecting a new server to a given port. For " - "more advanced use cases, refer to the '--nic' parameter."), + help=_( + "Create a NIC on the server and connect it to port. " + "Specify option multiple times to create multiple NICs. " + "This is a wrapper for the '--nic port-id=' " + "parameter that provides simple syntax for the standard " + "use case of connecting a new server to a given port. For " + "more advanced use cases, refer to the '--nic' parameter." + ), ) parser.add_argument( '--hint', @@ -862,10 +885,13 @@ def _show_progress(progress): if not image and parsed_args.image_property: def emit_duplicated_warning(img, image_property): img_uuid_list = [str(image.id) for image in img] - LOG.warning(_('Multiple matching images: %(img_uuid_list)s\n' - 'Using image: %(chosen_one)s') % - {'img_uuid_list': img_uuid_list, - 'chosen_one': img_uuid_list[0]}) + LOG.warning( + 'Multiple matching images: %(img_uuid_list)s\n' + 'Using image: %(chosen_one)s', + { + 'img_uuid_list': img_uuid_list, + 'chosen_one': img_uuid_list[0], + }) def _match_image(image_api, wanted_properties): image_list = image_api.images() @@ -882,45 +908,52 @@ def _match_image(image_api, wanted_properties): set([key, value]) except TypeError: if key != 'properties': - LOG.debug('Skipped the \'%s\' attribute. ' - 'That cannot be compared. ' - '(image: %s, value: %s)', - key, img.id, value) + LOG.debug( + 'Skipped the \'%s\' attribute. ' + 'That cannot be compared. ' + '(image: %s, value: %s)', + key, img.id, value, + ) pass else: img_dict[key] = value - if all(k in img_dict and img_dict[k] == v - for k, v in wanted_properties.items()): + if all( + k in img_dict and img_dict[k] == v + for k, v in wanted_properties.items() + ): images_matched.append(img) + return images_matched images = _match_image(image_client, parsed_args.image_property) if len(images) > 1: - emit_duplicated_warning(images, - parsed_args.image_property) + emit_duplicated_warning(images, parsed_args.image_property) if images: image = images[0] else: - raise exceptions.CommandError(_("No images match the " - "property expected by " - "--image-property")) + msg = _( + 'No images match the property expected by ' + '--image-property' + ) + raise exceptions.CommandError(msg) # Lookup parsed_args.volume volume = None if parsed_args.volume: # --volume and --boot-from-volume are mutually exclusive. if parsed_args.boot_from_volume: - raise exceptions.CommandError( - _('--volume is not allowed with --boot-from-volume')) + msg = _('--volume is not allowed with --boot-from-volume') + raise exceptions.CommandError(msg) + volume = utils.find_resource( volume_client.volumes, parsed_args.volume, ).id # Lookup parsed_args.flavor - flavor = utils.find_resource(compute_client.flavors, - parsed_args.flavor) + flavor = utils.find_resource( + compute_client.flavors, parsed_args.flavor) files = {} for f in parsed_args.file: @@ -930,16 +963,17 @@ def _match_image(image_api, wanted_properties): except IOError as e: msg = _("Can't open '%(source)s': %(exception)s") raise exceptions.CommandError( - msg % {"source": src, - "exception": e} + msg % {'source': src, 'exception': e} ) if parsed_args.min > parsed_args.max: msg = _("min instances should be <= max instances") raise exceptions.CommandError(msg) + if parsed_args.min < 1: msg = _("min instances should be > 0") raise exceptions.CommandError(msg) + if parsed_args.max < 1: msg = _("max instances should be > 0") raise exceptions.CommandError(msg) @@ -951,8 +985,7 @@ def _match_image(image_api, wanted_properties): except IOError as e: msg = _("Can't open '%(data)s': %(exception)s") raise exceptions.CommandError( - msg % {"data": parsed_args.user_data, - "exception": e} + msg % {'data': parsed_args.user_data, 'exception': e} ) if parsed_args.description: @@ -963,11 +996,12 @@ def _match_image(image_api, wanted_properties): block_device_mapping_v2 = [] if volume: - block_device_mapping_v2 = [{'uuid': volume, - 'boot_index': '0', - 'source_type': 'volume', - 'destination_type': 'volume' - }] + block_device_mapping_v2 = [{ + 'uuid': volume, + 'boot_index': '0', + 'source_type': 'volume', + 'destination_type': 'volume' + }] elif parsed_args.boot_from_volume: # Tell nova to create a root volume from the image provided. block_device_mapping_v2 = [{ @@ -988,13 +1022,16 @@ def _match_image(image_api, wanted_properties): dev_map = dev_map.split(':') if dev_map[0]: mapping = {'device_name': dev_name} + # 1. decide source and destination type if (len(dev_map) > 1 and dev_map[1] in ('volume', 'snapshot', 'image')): mapping['source_type'] = dev_map[1] else: mapping['source_type'] = 'volume' + mapping['destination_type'] = 'volume' + # 2. check target exist, update target uuid according by # source type if mapping['source_type'] == 'volume': @@ -1020,14 +1057,18 @@ def _match_image(image_api, wanted_properties): image_id = image_client.find_image(dev_map[0], ignore_missing=False).id mapping['uuid'] = image_id + # 3. append size and delete_on_termination if exist if len(dev_map) > 2 and dev_map[2]: mapping['volume_size'] = dev_map[2] + if len(dev_map) > 3 and dev_map[3]: mapping['delete_on_termination'] = dev_map[3] else: - msg = _("Volume, volume snapshot or image (name or ID) must " - "be specified if --block-device-mapping is specified") + msg = _( + 'Volume, volume snapshot or image (name or ID) must ' + 'be specified if --block-device-mapping is specified' + ) raise exceptions.CommandError(msg) block_device_mapping_v2.append(mapping) @@ -1041,22 +1082,32 @@ def _match_image(image_api, wanted_properties): auto_or_none = True nics.append(nic_str) else: - nic_info = {"net-id": "", "v4-fixed-ip": "", - "v6-fixed-ip": "", "port-id": ""} + nic_info = { + 'net-id': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', + 'port-id': '', + } for kv_str in nic_str.split(","): k, sep, v = kv_str.partition("=") if k in nic_info and v: nic_info[k] = v else: - msg = (_("Invalid nic argument '%s'. Nic arguments " - "must be of the form --nic .")) + msg = _( + "Invalid nic argument '%s'. Nic arguments " + "must be of the form --nic ." + ) raise exceptions.CommandError(msg % k) + if bool(nic_info["net-id"]) == bool(nic_info["port-id"]): - msg = _("either network or port should be specified " - "but not both") + msg = _( + 'Either network or port should be specified ' + 'but not both' + ) raise exceptions.CommandError(msg) + if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network if nic_info["net-id"]: @@ -1073,17 +1124,22 @@ def _match_image(image_api, wanted_properties): nic_info["net-id"] )['id'] if nic_info["port-id"]: - msg = _("can't create server with port specified " - "since network endpoint not enabled") + msg = _( + "Can't create server with port specified " + "since network endpoint not enabled" + ) raise exceptions.CommandError(msg) + nics.append(nic_info) if nics: if auto_or_none: if len(nics) > 1: - msg = _('Specifying a --nic of auto or none cannot ' - 'be used with any other --nic, --network ' - 'or --port value.') + msg = _( + 'Specifying a --nic of auto or none cannot ' + 'be used with any other --nic, --network ' + 'or --port value.' + ) raise exceptions.CommandError(msg) nics = nics[0] else: @@ -1165,16 +1221,22 @@ def _match_image(image_api, wanted_properties): if parsed_args.host: if compute_client.api_version < api_versions.APIVersion("2.74"): - msg = _("Specifying --host is not supported for " - "--os-compute-api-version less than 2.74") + msg = _( + '--os-compute-api-version 2.74 or greater is required to ' + 'support the --host option' + ) raise exceptions.CommandError(msg) + boot_kwargs['host'] = parsed_args.host if parsed_args.hypervisor_hostname: if compute_client.api_version < api_versions.APIVersion("2.74"): - msg = _("Specifying --hypervisor-hostname is not supported " - "for --os-compute-api-version less than 2.74") + msg = _( + '--os-compute-api-version 2.74 or greater is required to ' + 'support the --hypervisor-hostname option' + ) raise exceptions.CommandError(msg) + boot_kwargs['hypervisor_hostname'] = ( parsed_args.hypervisor_hostname) @@ -1200,8 +1262,7 @@ def _match_image(image_api, wanted_properties): ): self.app.stdout.write('\n') else: - LOG.error(_('Error creating server: %s'), - parsed_args.server_name) + LOG.error('Error creating server: %s', parsed_args.server_name) self.app.stdout.write(_('Error creating server\n')) raise SystemExit From 17f641e1c3ac58acdc39d064369395198d2654d2 Mon Sep 17 00:00:00 2001 From: He Jie Xu Date: Tue, 19 Jun 2018 12:19:49 +0000 Subject: [PATCH 0299/1120] Compute: Add user id support for keypair This patch adds functionality of specific the user id when create, delete, show and list keypairs. Change-Id: Ib826f1f4f5a73d1875ba0f02e124b3222c4d05ed Co-Authored-By: tianhui --- openstackclient/compute/v2/keypair.py | 86 ++++++++- .../tests/unit/compute/v2/test_keypair.py | 171 +++++++++++++++++- .../keypair-user-id-db694210695a0ee0.yaml | 6 + 3 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/keypair-user-id-db694210695a0ee0.yaml diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index ae653e76ee..157dd8cd90 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -64,10 +64,20 @@ def get_parser(self, prog_name): "(Supported by API versions '2.2' - '2.latest')" ), ) + parser.add_argument( + '--user', + metavar='', + help=_( + 'The owner of the keypair. (admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_user_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity public_key = parsed_args.public_key if public_key: @@ -89,12 +99,26 @@ def take_action(self, parsed_args): if compute_client.api_version < api_versions.APIVersion('2.2'): msg = _( '--os-compute-api-version 2.2 or greater is required to ' - 'support the --type option.' + 'support the --type option' ) raise exceptions.CommandError(msg) kwargs['key_type'] = parsed_args.type + if parsed_args.user: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --user option' + ) + raise exceptions.CommandError(msg) + + kwargs['user_id'] = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ).id + keypair = compute_client.keypairs.create(**kwargs) private_key = parsed_args.private_key @@ -139,16 +163,43 @@ def get_parser(self, prog_name): nargs='+', help=_("Name of key(s) to delete (name only)") ) + parser.add_argument( + '--user', + metavar='', + help=_( + 'The owner of the keypair. (admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_user_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity + + kwargs = {} result = 0 + + if parsed_args.user: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --user option' + ) + raise exceptions.CommandError(msg) + + kwargs['user_id'] = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ).id + for n in parsed_args.name: try: data = utils.find_resource( compute_client.keypairs, n) - compute_client.keypairs.delete(data.name) + compute_client.keypairs.delete(data.name, **kwargs) except Exception as e: result += 1 LOG.error(_("Failed to delete key with name " @@ -229,12 +280,39 @@ def get_parser(self, prog_name): default=False, help=_("Show only bare public key paired with the generated key") ) + parser.add_argument( + '--user', + metavar='', + help=_( + 'The owner of the keypair. (admin only) (name or ID). ' + 'Requires ``--os-compute-api-version`` 2.10 or greater.' + ), + ) + identity_common.add_user_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - keypair = utils.find_resource(compute_client.keypairs, - parsed_args.name) + identity_client = self.app.client_manager.identity + + kwargs = {} + + if parsed_args.user: + if compute_client.api_version < api_versions.APIVersion('2.10'): + msg = _( + '--os-compute-api-version 2.10 or greater is required to ' + 'support the --user option' + ) + raise exceptions.CommandError(msg) + + kwargs['user_id'] = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ).id + + keypair = utils.find_resource( + compute_client.keypairs, parsed_args.name, **kwargs) info = {} info.update(keypair._info) diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 5e6a47414f..56cbe3f100 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -38,6 +38,15 @@ def setUp(self): self.keypairs_mock = self.app.client_manager.compute.keypairs self.keypairs_mock.reset_mock() + # Initialize the user mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + class TestKeypairCreate(TestKeypair): @@ -226,6 +235,54 @@ def test_keypair_create_with_key_type_pre_v22(self): '--os-compute-api-version 2.2 or greater is required', str(ex)) + def test_key_pair_create_with_user(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + arglist = [ + '--user', identity_fakes.user_name, + self.keypair.name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('name', self.keypair.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.keypairs_mock.create.assert_called_with( + name=self.keypair.name, + public_key=None, + user_id=identity_fakes.user_id, + ) + + self.assertEqual({}, columns) + self.assertEqual({}, data) + + def test_key_pair_create_with_user_pre_v210(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.9') + + arglist = [ + '--user', identity_fakes.user_name, + self.keypair.name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('name', self.keypair.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.10 or greater is required', str(ex)) + class TestKeypairDelete(TestKeypair): @@ -301,6 +358,51 @@ def test_delete_multiple_keypairs_with_exception(self): self.keypairs[0].name ) + def test_keypair_delete_with_user(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + arglist = [ + '--user', identity_fakes.user_name, + self.keypairs[0].name + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('name', [self.keypairs[0].name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ret = self.cmd.take_action(parsed_args) + + self.assertIsNone(ret) + self.keypairs_mock.delete.assert_called_with( + self.keypairs[0].name, + user_id=identity_fakes.user_id, + ) + + def test_keypair_delete_with_user_pre_v210(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.9') + + arglist = [ + '--user', identity_fakes.user_name, + self.keypairs[0].name + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('name', [self.keypairs[0].name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.10 or greater is required', str(ex)) + class TestKeypairList(TestKeypair): @@ -310,14 +412,6 @@ class TestKeypairList(TestKeypair): def setUp(self): super(TestKeypairList, self).setUp() - self.users_mock = self.app.client_manager.identity.users - self.users_mock.reset_mock() - self.users_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.USER), - loaded=True, - ) - self.keypairs_mock.list.return_value = self.keypairs # Get the command object to test @@ -477,11 +571,14 @@ def test_keypair_show(self): verifylist = [ ('name', self.keypair.name) ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + self.keypairs_mock.get.assert_called_with( + self.keypair.name, + ) + self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -502,3 +599,59 @@ def test_keypair_show_public(self): self.assertEqual({}, columns) self.assertEqual({}, data) + + def test_keypair_show_with_user(self): + + # overwrite the setup one because we want to omit private_key + self.keypair = compute_fakes.FakeKeypair.create_one_keypair( + no_pri=True) + self.keypairs_mock.get.return_value = self.keypair + + self.data = ( + self.keypair.fingerprint, + self.keypair.name, + self.keypair.type, + self.keypair.user_id + ) + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.10') + + arglist = [ + '--user', identity_fakes.user_name, + self.keypair.name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('name', self.keypair.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.get.assert_called_with(identity_fakes.user_name) + self.keypairs_mock.get.assert_called_with( + self.keypair.name, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_keypair_show_with_user_pre_v210(self): + + arglist = [ + '--user', identity_fakes.user_name, + self.keypair.name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('name', self.keypair.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.10 or greater is required', str(ex)) diff --git a/releasenotes/notes/keypair-user-id-db694210695a0ee0.yaml b/releasenotes/notes/keypair-user-id-db694210695a0ee0.yaml new file mode 100644 index 0000000000..073973e2cf --- /dev/null +++ b/releasenotes/notes/keypair-user-id-db694210695a0ee0.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--user`` option to the ``keypair create``, ``keypair delete``, and + ``keypair show`` commands. Only available starting with + ``--os-compute-api-version 2.10``. From ad3369ed1fdad73b5d457a40df7df8ad55eb69cd Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 2 Nov 2020 13:25:59 +0100 Subject: [PATCH 0300/1120] Fix formatting of the flavor properties Do not stringify flavor properties to allow proper output formatting to json/yaml/etc Change-Id: I9f4c42acb85b726af87123134dd19de98fe95074 --- lower-constraints.txt | 4 +- openstackclient/compute/v2/flavor.py | 59 +++++++++++++++---- .../functional/compute/v2/test_flavor.py | 43 ++++++++------ .../tests/unit/compute/v2/test_flavor.py | 26 ++++---- ...vor-props-formatting-d21e97745543caa7.yaml | 4 ++ requirements.txt | 4 +- 6 files changed, 94 insertions(+), 46 deletions(-) create mode 100644 releasenotes/notes/fix-flavor-props-formatting-d21e97745543caa7.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 74eac6403b..52766b2290 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -5,7 +5,7 @@ asn1crypto==0.23.0 bandit==1.1.0 cachetools==2.0.0 cffi==1.14.0 -cliff==2.8.0 +cliff==3.4.0 cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 @@ -48,7 +48,7 @@ netifaces==0.10.4 openstacksdk==0.48.0 os-service-types==1.7.0 os-testr==1.0.0 -osc-lib==2.0.0 +osc-lib==2.2.0 osc-placement==1.7.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 805e919ea3..00431b7b73 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -18,6 +18,7 @@ import logging from novaclient import api_versions +from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -30,6 +31,31 @@ LOG = logging.getLogger(__name__) +_formatters = { + 'extra_specs': format_columns.DictColumn, + # Unless we finish switch to use SDK resources this need to be doubled this + # way + 'properties': format_columns.DictColumn, + 'Properties': format_columns.DictColumn +} + + +def _get_flavor_columns(item): + # To maintain backwards compatibility we need to rename sdk props to + # whatever OSC was using before + column_map = { + 'extra_specs': 'properties', + 'ephemeral': 'OS-FLV-EXT-DATA:ephemeral', + 'is_disabled': 'OS-FLV-DISABLED:disabled', + 'is_public': 'os-flavor-access:is_public' + + } + hidden_columns = ['links', 'location'] + + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + def _find_flavor(compute_client, flavor): try: return compute_client.flavors.get(flavor) @@ -191,10 +217,16 @@ def take_action(self, parsed_args): LOG.error(_("Failed to set flavor property: %s"), e) flavor_info = flavor._info.copy() - flavor_info.pop("links") - flavor_info['properties'] = utils.format_dict(flavor.get_keys()) + flavor_info['properties'] = flavor.get_keys() - return zip(*sorted(flavor_info.items())) + display_columns, columns = _get_flavor_columns(flavor_info) + data = utils.get_dict_properties( + flavor_info, columns, + formatters=_formatters, + mixed_case_fields=['OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral']) + + return (display_columns, data) class DeleteFlavor(command.Command): @@ -309,7 +341,7 @@ def take_action(self, parsed_args): return (column_headers, (utils.get_item_properties( - s, columns, formatters={'Properties': utils.format_dict}, + s, columns, formatters=_formatters, ) for s in data)) @@ -428,11 +460,8 @@ def take_action(self, parsed_args): try: flavor_access = compute_client.flavor_access.list( flavor=resource_flavor.id) - projects = [utils.get_field(access, 'tenant_id') - for access in flavor_access] - # TODO(Huanxuan Ao): This format case can be removed after - # patch https://review.opendev.org/#/c/330223/ merged. - access_projects = utils.format_list(projects) + access_projects = [utils.get_field(access, 'tenant_id') + for access in flavor_access] except Exception as e: msg = _("Failed to get access projects list " "for flavor '%(flavor)s': %(e)s") @@ -442,11 +471,17 @@ def take_action(self, parsed_args): flavor.update({ 'access_project_ids': access_projects }) - flavor.pop("links", None) - flavor['properties'] = utils.format_dict(resource_flavor.get_keys()) + flavor['properties'] = resource_flavor.get_keys() + + display_columns, columns = _get_flavor_columns(flavor) + data = utils.get_dict_properties( + flavor, columns, + formatters=_formatters, + mixed_case_fields=['OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral']) - return zip(*sorted(flavor.items())) + return (display_columns, data) class UnsetFlavor(command.Command): diff --git a/openstackclient/tests/functional/compute/v2/test_flavor.py b/openstackclient/tests/functional/compute/v2/test_flavor.py index c274adf2e7..162d428789 100644 --- a/openstackclient/tests/functional/compute/v2/test_flavor.py +++ b/openstackclient/tests/functional/compute/v2/test_flavor.py @@ -115,8 +115,8 @@ def test_flavor_list(self): self.assertFalse( cmd_output["os-flavor-access:is_public"], ) - self.assertEqual( - "a='b2', b='d2'", + self.assertDictEqual( + {"a": "b2", "b": "d2"}, cmd_output["properties"], ) @@ -133,12 +133,18 @@ def test_flavor_list(self): "flavor list -f json " + "--long" )) - col_name = [x["Name"] for x in cmd_output] - col_properties = [x['Properties'] for x in cmd_output] - self.assertIn(name1, col_name) - self.assertIn("a='b', c='d'", col_properties) - self.assertNotIn(name2, col_name) - self.assertNotIn("b2', b='d2'", col_properties) + # We have list of complex json objects + # Iterate through the list setting flags + found_expected = False + for rec in cmd_output: + if rec['Name'] == name1: + found_expected = True + self.assertEqual('b', rec['Properties']['a']) + self.assertEqual('d', rec['Properties']['c']) + elif rec['Name'] == name2: + # We should have not seen private flavor + self.assertFalse(True) + self.assertTrue(found_expected) # Test list --public cmd_output = json.loads(self.openstack( @@ -201,8 +207,8 @@ def test_flavor_properties(self): self.assertFalse( cmd_output["os-flavor-access:is_public"], ) - self.assertEqual( - "a='first', b='second'", + self.assertDictEqual( + {"a": "first", "b": "second"}, cmd_output["properties"], ) @@ -223,9 +229,14 @@ def test_flavor_properties(self): cmd_output["id"], ) self.assertEqual( - "a='third and 10', b='second', g='fourth'", - cmd_output['properties'], - ) + 'third and 10', + cmd_output['properties']['a']) + self.assertEqual( + 'second', + cmd_output['properties']['b']) + self.assertEqual( + 'fourth', + cmd_output['properties']['g']) raw_output = self.openstack( "flavor unset " + @@ -238,7 +249,5 @@ def test_flavor_properties(self): "flavor show -f json " + name1 )) - self.assertEqual( - "a='third and 10', g='fourth'", - cmd_output["properties"], - ) + + self.assertNotIn('b', cmd_output['properties']) diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 4732cc822d..2828d74e36 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -17,8 +17,8 @@ from unittest.mock import call import novaclient +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.compute.v2 import flavor from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -70,7 +70,7 @@ class TestFlavorCreate(TestFlavor): flavor.id, flavor.name, flavor.is_public, - utils.format_dict(flavor.properties), + format_columns.DictColumn(flavor.properties), flavor.ram, flavor.rxtx_factor, flavor.swap, @@ -111,7 +111,7 @@ def test_flavor_create_default_options(self): self.flavors_mock.create.assert_called_once_with(*default_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_flavor_create_all_options(self): @@ -165,7 +165,7 @@ def test_flavor_create_all_options(self): self.flavor.get_keys.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_flavor_create_other_options(self): @@ -226,7 +226,7 @@ def test_flavor_create_other_options(self): {'key1': 'value1', 'key2': 'value2'}) self.flavor.get_keys.assert_called_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_public_flavor_create_with_project(self): arglist = [ @@ -300,7 +300,7 @@ def test_flavor_create_with_description_api_newer(self): self.flavors_mock.create.assert_called_once_with(*args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_flavor_create_with_description_api_older(self): arglist = [ @@ -429,7 +429,7 @@ class TestFlavorList(TestFlavor): data_long = (data[0] + ( flavors[0].swap, flavors[0].rxtx_factor, - u'property=\'value\'' + format_columns.DictColumn(flavors[0].properties) ), ) def setUp(self): @@ -583,7 +583,7 @@ def test_flavor_list_long(self): ) self.assertEqual(self.columns_long, columns) - self.assertEqual(tuple(self.data_long), tuple(data)) + self.assertListItemEqual(self.data_long, tuple(data)) class TestFlavorSet(TestFlavor): @@ -817,7 +817,7 @@ class TestFlavorShow(TestFlavor): flavor.id, flavor.name, flavor.is_public, - utils.format_dict(flavor.get_keys()), + format_columns.DictColumn(flavor.get_keys()), flavor.ram, flavor.rxtx_factor, flavor.swap, @@ -854,7 +854,7 @@ def test_public_flavor_show(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemEqual(self.data, data) def test_private_flavor_show(self): private_flavor = compute_fakes.FakeFlavor.create_one_flavor( @@ -874,13 +874,13 @@ def test_private_flavor_show(self): data_with_project = ( private_flavor.disabled, private_flavor.ephemeral, - self.flavor_access.tenant_id, + [self.flavor_access.tenant_id], private_flavor.description, private_flavor.disk, private_flavor.id, private_flavor.name, private_flavor.is_public, - utils.format_dict(private_flavor.get_keys()), + format_columns.DictColumn(private_flavor.get_keys()), private_flavor.ram, private_flavor.rxtx_factor, private_flavor.swap, @@ -894,7 +894,7 @@ def test_private_flavor_show(self): self.flavor_access_mock.list.assert_called_with( flavor=private_flavor.id) self.assertEqual(self.columns, columns) - self.assertEqual(data_with_project, data) + self.assertItemEqual(data_with_project, data) class TestFlavorUnset(TestFlavor): diff --git a/releasenotes/notes/fix-flavor-props-formatting-d21e97745543caa7.yaml b/releasenotes/notes/fix-flavor-props-formatting-d21e97745543caa7.yaml new file mode 100644 index 0000000000..2a026b3f76 --- /dev/null +++ b/releasenotes/notes/fix-flavor-props-formatting-d21e97745543caa7.yaml @@ -0,0 +1,4 @@ +--- +fixes: + Fix '-f json' output of the flavor properties to return valid json object + instead of stringying it. diff --git a/requirements.txt b/requirements.txt index ee6b6241ae..99d409bf51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,10 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -cliff!=2.9.0,>=2.8.0 # Apache-2.0 +cliff>=3.4.0 # Apache-2.0 iso8601>=0.1.11 # MIT openstacksdk>=0.48.0 # Apache-2.0 -osc-lib>=2.0.0 # Apache-2.0 +osc-lib>=2.2.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=15.1.0 # Apache-2.0 From 01eb4e839394fe433a92a06daf1499fb00f2fe69 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 15 Mar 2019 13:03:10 +0000 Subject: [PATCH 0301/1120] Add 'openstack server evacuate' command This change adds a new 'openstack server evacuate' command to provide parity with the 'nova evacuate' command. The term "evacuate" is notoriously poor, in that it implies the instance is moved rather than recreated, but it is retained since people are familiar with it now. Change-Id: I1e32ca51036c501862d8e89b3144a9695d98a06f --- doc/source/cli/command-objects/server.rst | 3 + openstackclient/compute/v2/server.py | 112 ++++++++++++ .../tests/unit/compute/v2/test_server.py | 161 ++++++++++++++++++ .../add-server-evacuate-8359246692cb642f.yaml | 6 + setup.cfg | 1 + 5 files changed, 283 insertions(+) create mode 100644 releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml diff --git a/doc/source/cli/command-objects/server.rst b/doc/source/cli/command-objects/server.rst index 89eb4e3710..cf7df1dae3 100644 --- a/doc/source/cli/command-objects/server.rst +++ b/doc/source/cli/command-objects/server.rst @@ -10,6 +10,9 @@ Compute v2 .. autoprogram-cliff:: openstack.compute.v2 :command: server create +.. autoprogram-cliff:: openstack.compute.v2 + :command: server evacuate + .. autoprogram-cliff:: openstack.compute.v2 :command: server delete diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 294b468331..7f1bc08852 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2502,6 +2502,118 @@ def _show_progress(progress): return zip(*sorted(details.items())) +class EvacuateServer(command.ShowOne): + _description = _("""Evacuate a server to a different host. + +This command is used to recreate a server after the host it was on has failed. +It can only be used if the compute service that manages the server is down. +This command should only be used by an admin after they have confirmed that the +instance is not running on the failed host. + +If the server instance was created with an ephemeral root disk on non-shared +storage the server will be rebuilt using the original glance image preserving +the ports and any attached data volumes. + +If the server uses boot for volume or has its root disk on shared storage the +root disk will be preserved and reused for the evacuated instance on the new +host.""") + + def get_parser(self, prog_name): + parser = super(EvacuateServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + + parser.add_argument( + '--wait', action='store_true', + help=_('Wait for evacuation to complete'), + ) + parser.add_argument( + '--host', metavar='', default=None, + help=_( + 'Set the preferred host on which to rebuild the evacuated ' + 'server. The host will be validated by the scheduler. ' + '(supported by --os-compute-api-version 2.29 or above)' + ), + ) + shared_storage_group = parser.add_mutually_exclusive_group() + shared_storage_group.add_argument( + '--password', metavar='', default=None, + help=_( + 'Set the password on the evacuated instance. This option is ' + 'mutually exclusive with the --shared-storage option' + ), + ) + shared_storage_group.add_argument( + '--shared-storage', action='store_true', dest='shared_storage', + help=_( + 'Indicate that the instance is on shared storage. ' + 'This will be auto-calculated with ' + '--os-compute-api-version 2.14 and greater and should not ' + 'be used with later microversions. This option is mutually ' + 'exclusive with the --password option' + ), + ) + return parser + + def take_action(self, parsed_args): + + def _show_progress(progress): + if progress: + self.app.stdout.write('\rProgress: %s' % progress) + self.app.stdout.flush() + + compute_client = self.app.client_manager.compute + image_client = self.app.client_manager.image + + if parsed_args.host: + if compute_client.api_version < api_versions.APIVersion('2.29'): + msg = _( + '--os-compute-api-version 2.29 or later is required ' + 'to specify a preferred host.' + ) + raise exceptions.CommandError(msg) + + if parsed_args.shared_storage: + if compute_client.api_version > api_versions.APIVersion('2.13'): + msg = _( + '--os-compute-api-version 2.13 or earlier is required ' + 'to specify shared-storage.' + ) + raise exceptions.CommandError(msg) + + kwargs = { + 'host': parsed_args.host, + 'password': parsed_args.password, + } + + if compute_client.api_version <= api_versions.APIVersion('2.13'): + kwargs['on_shared_storage'] = parsed_args.shared_storage + + server = utils.find_resource( + compute_client.servers, parsed_args.server) + + server = server.evacuate(**kwargs) + + if parsed_args.wait: + if utils.wait_for_status( + compute_client.servers.get, + server.id, + callback=_show_progress, + ): + self.app.stdout.write(_('Complete\n')) + else: + LOG.error(_('Error evacuating server: %s'), server.id) + self.app.stdout.write(_('Error evacuating server\n')) + raise SystemExit + + details = _prep_server_detail( + compute_client, image_client, server, refresh=False) + return zip(*sorted(details.items())) + + class RemoveFixedIP(command.Command): _description = _("Remove fixed IP address from server") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 380ef66b4e..20e3f70e95 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4982,6 +4982,167 @@ def test_rebuild_with_key_name_and_unset(self): self.cmd, arglist, verifylist) +class TestEvacuateServer(TestServer): + + def setUp(self): + super(TestEvacuateServer, self).setUp() + # Return value for utils.find_resource for image + self.image = image_fakes.FakeImage.create_one_image() + self.images_mock.get.return_value = self.image + + # Fake the rebuilt new server. + attrs = { + 'image': { + 'id': self.image.id + }, + '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 = { + 'evacuate': 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.EvacuateServer(self.app, None) + + def _test_evacuate(self, args, verify_args, evac_args): + parsed_args = self.check_parser(self.cmd, args, verify_args) + + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.evacuate.assert_called_with(**evac_args) + + def test_evacuate(self): + args = [ + self.server.id, + ] + verify_args = [ + ('server', self.server.id), + ] + evac_args = { + 'host': None, 'on_shared_storage': False, 'password': None, + } + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_with_password(self): + args = [ + self.server.id, + '--password', 'password', + ] + verify_args = [ + ('server', self.server.id), + ('password', 'password'), + ] + evac_args = { + 'host': None, 'on_shared_storage': False, 'password': 'password', + } + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_with_host(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.29') + + host = 'target-host' + args = [ + self.server.id, + '--host', 'target-host', + ] + verify_args = [ + ('server', self.server.id), + ('host', 'target-host'), + ] + evac_args = {'host': host, 'password': None} + + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_with_host_pre_v229(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.28') + + args = [ + self.server.id, + '--host', 'target-host', + ] + verify_args = [ + ('server', self.server.id), + ('host', 'target-host'), + ] + parsed_args = self.check_parser(self.cmd, args, verify_args) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_evacuate_without_share_storage(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.13') + + args = [ + self.server.id, + '--shared-storage' + ] + verify_args = [ + ('server', self.server.id), + ('shared_storage', True), + ] + evac_args = { + 'host': None, 'on_shared_storage': True, 'password': None, + } + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_without_share_storage_post_v213(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.14') + + args = [ + self.server.id, + '--shared-storage' + ] + verify_args = [ + ('server', self.server.id), + ('shared_storage', True), + ] + parsed_args = self.check_parser(self.cmd, args, verify_args) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_evacuate_with_wait_ok(self, mock_wait_for_status): + args = [ + self.server.id, + '--wait', + ] + verify_args = [ + ('server', self.server.id), + ('wait', True), + ] + evac_args = { + 'host': None, 'on_shared_storage': False, 'password': None, + } + self._test_evacuate(args, verify_args, evac_args) + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.server.id, + callback=mock.ANY, + ) + + class TestServerRemoveFixedIP(TestServer): def setUp(self): diff --git a/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml b/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml new file mode 100644 index 0000000000..41bbed736a --- /dev/null +++ b/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``server evacuate`` command. This command will recreate an instance + from scratch on a new host and is intended to be used when the original + host fails. diff --git a/setup.cfg b/setup.cfg index 8363ec6cfd..a29852e353 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,6 +103,7 @@ openstack.compute.v2 = server_create = openstackclient.compute.v2.server:CreateServer server_delete = openstackclient.compute.v2.server:DeleteServer server_dump_create = openstackclient.compute.v2.server:CreateServerDump + server_evacuate = openstackclient.compute.v2.server:EvacuateServer server_list = openstackclient.compute.v2.server:ListServer server_lock = openstackclient.compute.v2.server:LockServer server_migrate = openstackclient.compute.v2.server:MigrateServer From 311f4130d2c59638074531fa59e67783c2571e91 Mon Sep 17 00:00:00 2001 From: jay Date: Wed, 15 Jul 2020 15:10:19 +0200 Subject: [PATCH 0302/1120] Add a few selectable fields to the "openstack server list" output Added ``-c project_id | user_id | created_at`` to ``openstack server list`` command to get these columns as an output. Change-Id: I18991adf899c7b72c98bb89871bf0715d35943f0 Story: 2007925 --- openstackclient/compute/v2/server.py | 21 +++++++++++++++++++ .../tests/unit/compute/v2/test_server.py | 19 +++++++++++++++++ ...lectable-fields.yaml-1d5fb4784fa6f232.yaml | 4 ++++ 3 files changed, 44 insertions(+) create mode 100644 releasenotes/notes/server-list-selectable-fields.yaml-1d5fb4784fa6f232.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 93e9f966ae..db3914b358 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1446,6 +1446,27 @@ def take_action(self, parsed_args): marker_id = None + # support for additional columns + if parsed_args.columns: + # convert tuple to list to edit them + column_headers = list(column_headers) + columns = list(columns) + + for c in parsed_args.columns: + if c in ('Project ID', 'project_id'): + columns.append('tenant_id') + column_headers.append('Project ID') + if c in ('User ID', 'user_id'): + columns.append('user_id') + column_headers.append('User ID') + if c in ('Created At', 'created_at'): + columns.append('created_at') + column_headers.append('Created At') + + # convert back to tuple + column_headers = tuple(column_headers) + columns = tuple(columns) + if parsed_args.marker: # Check if both "--marker" and "--deleted" are used. # In that scenario a lookup is not needed as the marker diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 7e4c71c50c..f6506bf7ad 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2696,6 +2696,25 @@ def test_server_list_long_option(self): self.assertEqual(self.columns_long, columns) self.assertEqual(tuple(self.data_long), tuple(data)) + def test_server_list_column_option(self): + arglist = [ + '-c', 'Project ID', + '-c', 'User ID', + '-c', 'Created At', + '--long' + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertIn('Project ID', columns) + self.assertIn('User ID', columns) + self.assertIn('Created At', columns) + def test_server_list_no_name_lookup_option(self): arglist = [ '--no-name-lookup', diff --git a/releasenotes/notes/server-list-selectable-fields.yaml-1d5fb4784fa6f232.yaml b/releasenotes/notes/server-list-selectable-fields.yaml-1d5fb4784fa6f232.yaml new file mode 100644 index 0000000000..c40a050261 --- /dev/null +++ b/releasenotes/notes/server-list-selectable-fields.yaml-1d5fb4784fa6f232.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add ``--c project_id | user_id | created_at`` to ``openstack server list`` + command to get these columns as an output. From 0a7f2692c62846bb0d49d366df65f1cfc2565f03 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Oct 2020 21:50:40 +0100 Subject: [PATCH 0303/1120] Remove references to Python 2.7 We don't support it anymore. This is just noise now. Change-Id: I3640e7d8e520db69f83f95e9c7759279f7c15008 Signed-off-by: Stephen Finucane --- HACKING.rst | 21 +-------------------- doc/source/contributor/developing.rst | 15 +++++++-------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 52e96caa93..b5fbad3ca1 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -7,6 +7,7 @@ OpenStack Style Commandments General ------- + - thou shalt not violate causality in our time cone, or else Docstrings @@ -85,23 +86,3 @@ commandline arguments, etc.) should be presumed to be encoded as utf-8. returntext = do_some_magic_with(mytext) returnstring = returntext.encode('utf-8') outfile.write(returnstring) - -Python 3.x Compatibility ------------------------- - -OpenStackClient strives to be Python 3.3 compatible. Common guidelines: - -* Convert print statements to functions: print statements should be converted - to an appropriate log or other output mechanism. -* Prefer to x.items() over six.iteritems(x). - -Running Tests -------------- - -Note: Oh boy, are we behind on writing tests. But they are coming! - -The testing system is based on a combination of tox and testr. If you just -want to run the whole suite, run `tox` and all will be fine. However, if -you'd like to dig in a bit more, you might want to learn some things about -testr itself. A basic walkthrough for OpenStack can be found at -http://wiki.openstack.org/testr diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index a319849396..9142edb86c 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -59,13 +59,13 @@ To run the full suite of tests maintained within OpenStackClient. virtualenvs. You can later use the ``-r`` option with ``tox`` to rebuild your virtualenv in a similar manner. - -To run tests for one or more specific test environments(for example, the most common configuration of -Python 2.7 and PEP-8), list the environments with the ``-e`` option, separated by spaces: +To run tests for one or more specific test environments(for example, the most +common configuration of the latest Python version and PEP-8), list the +environments with the ``-e`` option, separated by spaces: .. code-block:: bash - $ tox -e py27,pep8 + $ tox -e py38,pep8 See ``tox.ini`` for the full list of available test environments. @@ -96,9 +96,9 @@ Using PDB breakpoints with ``tox`` and ``testr`` normally does not work since the tests fail with a `BdbQuit` exception rather than stopping at the breakpoint. -To run with PDB breakpoints during testing, use the `debug` ``tox`` environment -rather than ``py27``. For example, passing a test name since you will normally -only want to run the test that hits your breakpoint: +To run with PDB breakpoints during testing, use the ``debug`` ``tox`` +environment. For example, passing a test name since you will normally only want +to run the test that hits your breakpoint: .. code-block:: bash @@ -207,4 +207,3 @@ Example from openstackclient import shell from openstackclient.tests import utils from unittest import mock - From 4d3a3bb28f61a75c14a06e98dc0dd265308d658f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Oct 2020 21:52:45 +0100 Subject: [PATCH 0304/1120] Remove unnecessary test As noted, we're simply testing the default behavior of Python 3 in this test. Remove it, now that this is the only version(s) of Python 3 we have to worry about. Change-Id: I5f07343df8334457d907086033d5685f59c0bf0e Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/test_shell.py | 30 ------------------------ 1 file changed, 30 deletions(-) diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py index 366c364e06..bee2b40149 100644 --- a/openstackclient/tests/unit/test_shell.py +++ b/openstackclient/tests/unit/test_shell.py @@ -15,7 +15,6 @@ import importlib import os -import sys from unittest import mock from osc_lib.tests import utils as osc_lib_test_utils @@ -412,32 +411,3 @@ def test_empty_env(self): "network_api_version": LIB_NETWORK_API_VERSION } self._assert_cli(flag, kwargs) - - -class TestShellArgV(TestShell): - """Test the deferred help flag""" - - def test_shell_argv(self): - """Test argv decoding - - Python 2 does nothing with argv while Python 3 decodes it into - Unicode before we ever see it. We manually decode when running - under Python 2 so verify that we get the right argv types. - - Use the argv supplied by the test runner so we get actual Python - runtime behaviour; we only need to check the type of argv[0] - which will always be present. - """ - - with mock.patch( - self.shell_class_name + ".run", - self.app, - ): - # Ensure type gets through unmolested through shell.main() - argv = sys.argv - shell.main(sys.argv) - self.assertEqual(type(argv[0]), type(self.app.call_args[0][0][0])) - - # When shell.main() gets sys.argv itself it should be decoded - shell.main() - self.assertEqual(type(u'x'), type(self.app.call_args[0][0][0])) From ffd7e939615975b68bb7ec011cdaea077d9201c9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 6 Nov 2020 17:31:14 +0000 Subject: [PATCH 0305/1120] functional: Remove test for 'quota set --force' Change I1d1ac1ac46f49f64794ffc8631e166935537966c introduced the 'quota set --force' parameter to force set nova quotas. As part of that fix, we introduced a functional test, 'QuotaTests.test_quota_set_force' that works by attempting to set the 'limit' of the quota for instances to the current usage ('is_use') minus one. This test is flawed. It doesn't create any instances so when it fires by itself, it will always set the 'limit' to 0. When it fires at the same time as other tests (remember, we run tests in parallel), notably tests that rely on booting instances, it can cause other tests to fail with the following error: Quota exceeded for instances: Requested 1, but already used 0 of 0 instances (HTTP 403) We could attempt to work around this by creating a new project and using that project to fiddle with quotas. That's a lot of work though, and the returns are questionable: the 'quota set' command is an admin-only command by default and the '--force' parameter should almost never be used. Simply remove this test. Change-Id: Ic07ff6f4a7c1c27852c892eb906bb144aae91788 Signed-off-by: Stephen Finucane Story: #2008327 Task: #41225 --- .../tests/functional/common/test_quota.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 4c2fc0e34d..9c05746077 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -165,47 +165,3 @@ def test_quota_set_class(self): # returned attributes self.assertTrue(cmd_output["key-pairs"] >= 0) self.assertTrue(cmd_output["snapshots"] >= 0) - - def test_quota_set_force(self): - """Test to set instance value by force """ - json_output = json.loads(self.openstack( - 'quota list -f json --detail --compute' - )) - in_use = limit = None - for j in json_output: - if j["Resource"] == "instances": - in_use = j["In Use"] - limit = j["Limit"] - - # Reduce count of in_use - in_use = in_use - 1 - # cannot have negative instances limit - if in_use < 0: - in_use = 0 - - # set the limit by force now - self.openstack( - 'quota set ' + self.PROJECT_NAME + - '--instances ' + str(in_use) + ' --force' - ) - cmd_output = json.loads(self.openstack( - 'quota show -f json ' + self.PROJECT_NAME - )) - self.assertIsNotNone(cmd_output) - self.assertEqual( - in_use, - cmd_output["instances"] - ) - - # Set instances limit to original limit now - self.openstack( - 'quota set ' + self.PROJECT_NAME + '--instances ' + str(limit) - ) - cmd_output = json.loads(self.openstack( - 'quota show -f json ' + self.PROJECT_NAME - )) - self.assertIsNotNone(cmd_output) - self.assertEqual( - limit, - cmd_output["instances"] - ) From 512ba114a18d9ae49eb3e6b4123e92dd896d3f4f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 9 Nov 2020 13:14:53 +0100 Subject: [PATCH 0306/1120] Switch 'openstack keypair' ops to use SDK Let's continue our journey and start using SDK for the keypair operations Depends-On: https://review.opendev.org/#/c/761883/ Change-Id: Id411e70b8e1a79c0e88a0e22be7ff37e5c30fcda --- openstackclient/compute/v2/keypair.py | 86 ++++---- .../tests/unit/compute/v2/test_keypair.py | 185 ++++++++---------- ...witch-keypair-to-sdk-81e28380e66a7f9c.yaml | 3 + 3 files changed, 128 insertions(+), 146 deletions(-) create mode 100644 releasenotes/notes/switch-keypair-to-sdk-81e28380e66a7f9c.yaml diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 726dbfb9aa..19e30bff10 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -20,7 +20,7 @@ import os import sys -from novaclient import api_versions +from openstack import utils as sdk_utils from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -32,6 +32,19 @@ LOG = logging.getLogger(__name__) +def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False): + # To maintain backwards compatibility we need to rename sdk props to + # whatever OSC was using before + column_map = {} + hidden_columns = ['links', 'location'] + if hide_pub_key: + hidden_columns.append('public_key') + if hide_priv_key: + hidden_columns.append('private_key') + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + class CreateKeypair(command.ShowOne): _description = _("Create new public or private key for server ssh access") @@ -76,9 +89,13 @@ 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 identity_client = self.app.client_manager.identity + kwargs = { + 'name': parsed_args.name + } + public_key = parsed_args.public_key if public_key: try: @@ -91,12 +108,10 @@ def take_action(self, parsed_args): "exception": e} ) - kwargs = { - 'name': parsed_args.name, - 'public_key': public_key, - } + kwargs['public_key'] = public_key + if parsed_args.type: - if compute_client.api_version < api_versions.APIVersion('2.2'): + if not sdk_utils.supports_microversion(compute_client, '2.2'): msg = _( '--os-compute-api-version 2.2 or greater is required to ' 'support the --type option' @@ -106,7 +121,7 @@ def take_action(self, parsed_args): kwargs['key_type'] = parsed_args.type if parsed_args.user: - if compute_client.api_version < api_versions.APIVersion('2.10'): + if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( '--os-compute-api-version 2.10 or greater is required to ' 'support the --user option' @@ -119,7 +134,7 @@ def take_action(self, parsed_args): parsed_args.user_domain, ).id - keypair = compute_client.keypairs.create(**kwargs) + keypair = compute_client.create_keypair(**kwargs) private_key = parsed_args.private_key # Save private key into specified file @@ -139,14 +154,12 @@ def take_action(self, parsed_args): # NOTE(dtroyer): how do we want to handle the display of the private # key when it needs to be communicated back to the user # For now, duplicate nova keypair-add command output - info = {} if public_key or private_key: - info.update(keypair._info) - if 'public_key' in info: - del info['public_key'] - if 'private_key' in info: - del info['private_key'] - return zip(*sorted(info.items())) + display_columns, columns = _get_keypair_columns( + keypair, hide_pub_key=True, hide_priv_key=True) + data = utils.get_item_properties(keypair, columns) + + return (display_columns, data) else: sys.stdout.write(keypair.private_key) return ({}, {}) @@ -175,14 +188,14 @@ 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 identity_client = self.app.client_manager.identity kwargs = {} result = 0 if parsed_args.user: - if compute_client.api_version < api_versions.APIVersion('2.10'): + if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( '--os-compute-api-version 2.10 or greater is required to ' 'support the --user option' @@ -197,9 +210,8 @@ def take_action(self, parsed_args): for n in parsed_args.name: try: - data = utils.find_resource( - compute_client.keypairs, n) - compute_client.keypairs.delete(data.name, **kwargs) + compute_client.delete_keypair( + n, **kwargs, ignore_missing=False) except Exception as e: result += 1 LOG.error(_("Failed to delete key with name " @@ -240,11 +252,11 @@ 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 identity_client = self.app.client_manager.identity if parsed_args.project: - if compute_client.api_version < api_versions.APIVersion('2.10'): + if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( '--os-compute-api-version 2.10 or greater is required to ' 'support the --project option' @@ -263,9 +275,9 @@ def take_action(self, parsed_args): data = [] for user in users: - data.extend(compute_client.keypairs.list(user_id=user.id)) + data.extend(compute_client.keypairs(user_id=user.id)) elif parsed_args.user: - if compute_client.api_version < api_versions.APIVersion('2.10'): + if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( '--os-compute-api-version 2.10 or greater is required to ' 'support the --user option' @@ -278,16 +290,16 @@ def take_action(self, parsed_args): parsed_args.user_domain, ) - data = compute_client.keypairs.list(user_id=user.id) + data = compute_client.keypairs(user_id=user.id) else: - data = compute_client.keypairs.list() + data = compute_client.keypairs() columns = ( "Name", "Fingerprint" ) - if compute_client.api_version >= api_versions.APIVersion('2.2'): + if sdk_utils.supports_microversion(compute_client, '2.2'): columns += ("Type", ) return ( @@ -324,13 +336,13 @@ 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 identity_client = self.app.client_manager.identity kwargs = {} if parsed_args.user: - if compute_client.api_version < api_versions.APIVersion('2.10'): + if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( '--os-compute-api-version 2.10 or greater is required to ' 'support the --user option' @@ -343,16 +355,14 @@ def take_action(self, parsed_args): parsed_args.user_domain, ).id - keypair = utils.find_resource( - compute_client.keypairs, parsed_args.name, **kwargs) + keypair = compute_client.find_keypair( + parsed_args.name, **kwargs, ignore_missing=False) - info = {} - info.update(keypair._info) if not parsed_args.public_key: - del info['public_key'] - return zip(*sorted(info.items())) + display_columns, columns = _get_keypair_columns( + keypair, hide_pub_key=True) + data = utils.get_item_properties(keypair, columns) + return (display_columns, data) else: - # NOTE(dtroyer): a way to get the public key in a similar form - # as the private key in the create command sys.stdout.write(keypair.public_key) return ({}, {}) diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 9632a6671a..5a17808fa1 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -19,8 +19,8 @@ import uuid from novaclient import api_versions +from openstack import utils as sdk_utils from osc_lib import exceptions -from osc_lib import utils from openstackclient.compute.v2 import keypair from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -34,10 +34,6 @@ class TestKeypair(compute_fakes.TestComputev2): def setUp(self): super(TestKeypair, self).setUp() - # Get a shortcut to the KeypairManager Mock - self.keypairs_mock = self.app.client_manager.compute.keypairs - self.keypairs_mock.reset_mock() - # Initialize the user mock self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() @@ -47,6 +43,14 @@ def setUp(self): loaded=True, ) + self.app.client_manager.sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection.compute = mock.Mock() + self.sdk_client = self.app.client_manager.sdk_connection.compute + self.sdk_client.keypairs = mock.Mock() + self.sdk_client.create_keypair = mock.Mock() + self.sdk_client.delete_keypair = mock.Mock() + self.sdk_client.find_keypair = mock.Mock() + class TestKeypairCreate(TestKeypair): @@ -71,7 +75,7 @@ def setUp(self): # Get the command object to test self.cmd = keypair.CreateKeypair(self.app, None) - self.keypairs_mock.create.return_value = self.keypair + self.sdk_client.create_keypair.return_value = self.keypair def test_key_pair_create_no_options(self): @@ -85,9 +89,8 @@ def test_key_pair_create_no_options(self): columns, data = self.cmd.take_action(parsed_args) - self.keypairs_mock.create.assert_called_with( - name=self.keypair.name, - public_key=None + self.sdk_client.create_keypair.assert_called_with( + name=self.keypair.name ) self.assertEqual({}, columns) @@ -97,7 +100,7 @@ def test_keypair_create_public_key(self): # overwrite the setup one because we want to omit private_key self.keypair = compute_fakes.FakeKeypair.create_one_keypair( no_pri=True) - self.keypairs_mock.create.return_value = self.keypair + self.sdk_client.create_keypair.return_value = self.keypair self.data = ( self.keypair.fingerprint, @@ -124,7 +127,7 @@ def test_keypair_create_public_key(self): columns, data = self.cmd.take_action(parsed_args) - self.keypairs_mock.create.assert_called_with( + self.sdk_client.create_keypair.assert_called_with( name=self.keypair.name, public_key=self.keypair.public_key, ) @@ -151,9 +154,8 @@ def test_keypair_create_private_key(self): columns, data = self.cmd.take_action(parsed_args) - self.keypairs_mock.create.assert_called_with( + self.sdk_client.create_keypair.assert_called_with( name=self.keypair.name, - public_key=None, ) mock_open.assert_called_once_with(tmp_pk_file, 'w+') @@ -162,14 +164,12 @@ def test_keypair_create_private_key(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_keypair_create_with_key_type(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.2') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_keypair_create_with_key_type(self, sm_mock): for key_type in ['x509', 'ssh']: self.keypair = compute_fakes.FakeKeypair.create_one_keypair( no_pri=True) - self.keypairs_mock.create.return_value = self.keypair + self.sdk_client.create_keypair.return_value = self.keypair self.data = ( self.keypair.fingerprint, @@ -195,7 +195,7 @@ def test_keypair_create_with_key_type(self): m_file.read.return_value = 'dummy' columns, data = self.cmd.take_action(parsed_args) - self.keypairs_mock.create.assert_called_with( + self.sdk_client.create_keypair.assert_called_with( name=self.keypair.name, public_key=self.keypair.public_key, key_type=key_type, @@ -204,10 +204,8 @@ def test_keypair_create_with_key_type(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_keypair_create_with_key_type_pre_v22(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.1') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_keypair_create_with_key_type_pre_v22(self, sm_mock): for key_type in ['x509', 'ssh']: arglist = [ '--public-key', self.keypair.public_key, @@ -235,11 +233,8 @@ def test_keypair_create_with_key_type_pre_v22(self): '--os-compute-api-version 2.2 or greater is required', str(ex)) - def test_key_pair_create_with_user(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.10') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_key_pair_create_with_user(self, sm_mock): arglist = [ '--user', identity_fakes.user_name, self.keypair.name, @@ -252,20 +247,16 @@ def test_key_pair_create_with_user(self): columns, data = self.cmd.take_action(parsed_args) - self.keypairs_mock.create.assert_called_with( + self.sdk_client.create_keypair.assert_called_with( name=self.keypair.name, - public_key=None, user_id=identity_fakes.user_id, ) self.assertEqual({}, columns) self.assertEqual({}, data) - def test_key_pair_create_with_user_pre_v210(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.9') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_key_pair_create_with_user_pre_v210(self, sm_mock): arglist = [ '--user', identity_fakes.user_name, self.keypair.name, @@ -291,10 +282,6 @@ class TestKeypairDelete(TestKeypair): def setUp(self): super(TestKeypairDelete, self).setUp() - self.keypairs_mock.get = compute_fakes.FakeKeypair.get_keypairs( - self.keypairs) - self.keypairs_mock.delete.return_value = None - self.cmd = keypair.DeleteKeypair(self.app, None) def test_keypair_delete(self): @@ -310,7 +297,8 @@ def test_keypair_delete(self): ret = self.cmd.take_action(parsed_args) self.assertIsNone(ret) - self.keypairs_mock.delete.assert_called_with(self.keypairs[0].name) + self.sdk_client.delete_keypair.assert_called_with( + self.keypairs[0].name, ignore_missing=False) def test_delete_multiple_keypairs(self): arglist = [] @@ -325,8 +313,8 @@ def test_delete_multiple_keypairs(self): calls = [] for k in self.keypairs: - calls.append(call(k.name)) - self.keypairs_mock.delete.assert_has_calls(calls) + calls.append(call(k.name, ignore_missing=False)) + self.sdk_client.delete_keypair.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_keypairs_with_exception(self): @@ -340,29 +328,21 @@ def test_delete_multiple_keypairs_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.keypairs[0], exceptions.CommandError] - with mock.patch.object(utils, 'find_resource', - side_effect=find_mock_result) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 keys failed to delete.', str(e)) - - find_mock.assert_any_call( - self.keypairs_mock, self.keypairs[0].name) - find_mock.assert_any_call(self.keypairs_mock, 'unexist_keypair') - - self.assertEqual(2, find_mock.call_count) - self.keypairs_mock.delete.assert_called_once_with( - self.keypairs[0].name - ) - - def test_keypair_delete_with_user(self): + self.sdk_client.delete_keypair.side_effect = [ + None, exceptions.CommandError] + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 keys failed to delete.', str(e)) - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.10') + calls = [] + for k in arglist: + calls.append(call(k, ignore_missing=False)) + self.sdk_client.delete_keypair.assert_has_calls(calls) + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_keypair_delete_with_user(self, sm_mock): arglist = [ '--user', identity_fakes.user_name, self.keypairs[0].name @@ -376,12 +356,14 @@ def test_keypair_delete_with_user(self): ret = self.cmd.take_action(parsed_args) self.assertIsNone(ret) - self.keypairs_mock.delete.assert_called_with( + self.sdk_client.delete_keypair.assert_called_with( self.keypairs[0].name, user_id=identity_fakes.user_id, + ignore_missing=False ) - def test_keypair_delete_with_user_pre_v210(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_keypair_delete_with_user_pre_v210(self, sm_mock): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.9') @@ -406,18 +388,19 @@ def test_keypair_delete_with_user_pre_v210(self): class TestKeypairList(TestKeypair): - # Return value of self.keypairs_mock.list(). + # Return value of self.sdk_client.keypairs(). keypairs = compute_fakes.FakeKeypair.create_keypairs(count=1) def setUp(self): super(TestKeypairList, self).setUp() - self.keypairs_mock.list.return_value = self.keypairs + self.sdk_client.keypairs.return_value = self.keypairs # Get the command object to test self.cmd = keypair.ListKeypair(self.app, None) - def test_keypair_list_no_options(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_keypair_list_no_options(self, sm_mock): arglist = [] verifylist = [] @@ -430,7 +413,7 @@ def test_keypair_list_no_options(self): # Set expected values - self.keypairs_mock.list.assert_called_with() + self.sdk_client.keypairs.assert_called_with() self.assertEqual(('Name', 'Fingerprint'), columns) self.assertEqual( @@ -438,10 +421,8 @@ def test_keypair_list_no_options(self): tuple(data) ) - def test_keypair_list_v22(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.2') - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_keypair_list_v22(self, sm_mock): arglist = [] verifylist = [] @@ -454,7 +435,7 @@ def test_keypair_list_v22(self): # Set expected values - self.keypairs_mock.list.assert_called_with() + self.sdk_client.keypairs.assert_called_with() self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) self.assertEqual( @@ -466,11 +447,8 @@ def test_keypair_list_v22(self): tuple(data) ) - def test_keypair_list_with_user(self): - - # Filtering by user is support for nova api 2.10 or above - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.10') + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_keypair_list_with_user(self, sm_mock): users_mock = self.app.client_manager.identity.users users_mock.reset_mock() @@ -491,7 +469,7 @@ def test_keypair_list_with_user(self): columns, data = self.cmd.take_action(parsed_args) users_mock.get.assert_called_with(identity_fakes.user_name) - self.keypairs_mock.list.assert_called_with( + self.sdk_client.keypairs.assert_called_with( user_id=identity_fakes.user_id, ) @@ -505,10 +483,8 @@ def test_keypair_list_with_user(self): tuple(data) ) - def test_keypair_list_with_user_pre_v210(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.9') + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_keypair_list_with_user_pre_v210(self, sm_mock): arglist = [ '--user', identity_fakes.user_name, @@ -525,11 +501,8 @@ def test_keypair_list_with_user_pre_v210(self): self.assertIn( '--os-compute-api-version 2.10 or greater is required', str(ex)) - def test_keypair_list_with_project(self): - - # Filtering by user is support for nova api 2.10 or above - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.10') + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_keypair_list_with_project(self, sm_mock): projects_mock = self.app.client_manager.identity.tenants projects_mock.reset_mock() @@ -557,7 +530,7 @@ def test_keypair_list_with_project(self): projects_mock.get.assert_called_with(identity_fakes.project_name) users_mock.list.assert_called_with(tenant_id=identity_fakes.project_id) - self.keypairs_mock.list.assert_called_with( + self.sdk_client.keypairs.assert_called_with( user_id=identity_fakes.user_id, ) @@ -571,10 +544,8 @@ def test_keypair_list_with_project(self): tuple(data) ) - def test_keypair_list_with_project_pre_v210(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.9') + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_keypair_list_with_project_pre_v210(self, sm_mock): arglist = ['--project', identity_fakes.project_name] verifylist = [('project', identity_fakes.project_name)] @@ -589,10 +560,6 @@ def test_keypair_list_with_project_pre_v210(self): def test_keypair_list_conflicting_user_options(self): - # Filtering by user is support for nova api 2.10 or above - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.10') - arglist = [ '--user', identity_fakes.user_name, '--project', identity_fakes.project_name, @@ -610,7 +577,7 @@ class TestKeypairShow(TestKeypair): def setUp(self): super(TestKeypairShow, self).setUp() - self.keypairs_mock.get.return_value = self.keypair + self.sdk_client.find_keypair.return_value = self.keypair self.cmd = keypair.ShowKeypair(self.app, None) @@ -641,7 +608,7 @@ def test_keypair_show(self): # overwrite the setup one because we want to omit private_key self.keypair = compute_fakes.FakeKeypair.create_one_keypair( no_pri=True) - self.keypairs_mock.get.return_value = self.keypair + self.sdk_client.find_keypair.return_value = self.keypair self.data = ( self.keypair.fingerprint, @@ -660,8 +627,9 @@ def test_keypair_show(self): columns, data = self.cmd.take_action(parsed_args) - self.keypairs_mock.get.assert_called_with( + self.sdk_client.find_keypair.assert_called_with( self.keypair.name, + ignore_missing=False ) self.assertEqual(self.columns, columns) @@ -685,12 +653,13 @@ def test_keypair_show_public(self): self.assertEqual({}, columns) self.assertEqual({}, data) - def test_keypair_show_with_user(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_keypair_show_with_user(self, sm_mock): # overwrite the setup one because we want to omit private_key self.keypair = compute_fakes.FakeKeypair.create_one_keypair( no_pri=True) - self.keypairs_mock.get.return_value = self.keypair + self.sdk_client.find_keypair.return_value = self.keypair self.data = ( self.keypair.fingerprint, @@ -699,9 +668,6 @@ def test_keypair_show_with_user(self): self.keypair.user_id ) - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.10') - arglist = [ '--user', identity_fakes.user_name, self.keypair.name, @@ -715,14 +681,17 @@ def test_keypair_show_with_user(self): columns, data = self.cmd.take_action(parsed_args) self.users_mock.get.assert_called_with(identity_fakes.user_name) - self.keypairs_mock.get.assert_called_with( + self.sdk_client.find_keypair.assert_called_with( self.keypair.name, + ignore_missing=False, + user_id=identity_fakes.user_id ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_keypair_show_with_user_pre_v210(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_keypair_show_with_user_pre_v210(self, sm_mock): arglist = [ '--user', identity_fakes.user_name, diff --git a/releasenotes/notes/switch-keypair-to-sdk-81e28380e66a7f9c.yaml b/releasenotes/notes/switch-keypair-to-sdk-81e28380e66a7f9c.yaml new file mode 100644 index 0000000000..17e53c4cd6 --- /dev/null +++ b/releasenotes/notes/switch-keypair-to-sdk-81e28380e66a7f9c.yaml @@ -0,0 +1,3 @@ +--- +Features: + - Switch 'openstack keypair' commands to use OpenStackSDK From 1a5dd4af5bf7fdc1b903b300b917f302ba75a1bc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 9 Nov 2020 14:53:57 +0000 Subject: [PATCH 0307/1120] Resolve issues with 'server migration list' The 'os-migrations' API accepts 'instance_uuid' and 'migration_type' query string parameters, not 'server' and 'type'. For the former, as the name would suggest, the value should be a server UUID, not a name. In addition, this is a list command and therefore should subclass the 'Lister' base class. Change-Id: I736f5575156fc04d7ada7783a1865ab3b438396f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 11 ++++++++--- openstackclient/tests/unit/compute/v2/test_server.py | 12 +++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index fddafaee69..e897ab1ae7 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2041,7 +2041,7 @@ def _show_progress(progress): raise SystemExit -class ListMigration(command.Command): +class ListMigration(command.Lister): _description = _("""List server migrations""") def get_parser(self, prog_name): @@ -2168,16 +2168,21 @@ def take_action(self, parsed_args): search_opts = { 'host': parsed_args.host, - 'server': parsed_args.server, 'status': parsed_args.status, } + if parsed_args.server: + search_opts['instance_uuid'] = utils.find_resource( + compute_client.servers, + parsed_args.server, + ).id + if parsed_args.type: migration_type = parsed_args.type # we're using an alias because the default value is confusing if migration_type == 'cold-migration': migration_type = 'migration' - search_opts['type'] = migration_type + search_opts['migration_type'] = migration_type if parsed_args.marker: if compute_client.api_version < api_versions.APIVersion('2.59'): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 15cdbf1637..d095f0cb84 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4079,7 +4079,6 @@ def test_server_migration_list_no_options(self): kwargs = { 'status': None, 'host': None, - 'server': None, } self.migrations_mock.list.assert_called_with(**kwargs) @@ -4106,10 +4105,11 @@ def test_server_migration_list(self): kwargs = { 'status': 'migrating', 'host': 'host1', - 'server': 'server1', - 'type': 'migration', + 'instance_uuid': self.server.id, + 'migration_type': 'migration', } + self.servers_mock.get.assert_called_with('server1') self.migrations_mock.list.assert_called_with(**kwargs) self.assertEqual(self.MIGRATION_COLUMNS, columns) @@ -4145,7 +4145,6 @@ def test_server_migration_list(self): kwargs = { 'status': 'migrating', 'host': None, - 'server': None, } self.migrations_mock.list.assert_called_with(**kwargs) @@ -4194,7 +4193,6 @@ def test_server_migration_list(self): 'limit': 1, 'marker': 'test_kp', 'host': None, - 'server': None, 'changes_since': '2019-08-09T08:03:25Z', } @@ -4303,7 +4301,6 @@ def test_server_migration_list_with_changes_before(self): 'limit': 1, 'marker': 'test_kp', 'host': None, - 'server': None, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': '2019-08-09T08:03:25Z', } @@ -4375,7 +4372,6 @@ def test_server_migration_list_with_project(self): 'limit': 1, 'marker': 'test_kp', 'host': None, - 'server': None, 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3', 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", @@ -4438,7 +4434,6 @@ def test_server_migration_list_with_user(self): 'limit': 1, 'marker': 'test_kp', 'host': None, - 'server': None, 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6', 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", @@ -4500,7 +4495,6 @@ def test_server_migration_list_with_project_and_user(self): 'status': 'migrating', 'limit': 1, 'host': None, - 'server': None, 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3', 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6', 'changes_since': '2019-08-07T08:03:25Z', From 4c0bfb03fc688575fa93667bb59d399fd1c6b41d Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Thu, 5 Nov 2020 17:51:27 +0200 Subject: [PATCH 0308/1120] Allow to resize in-use volumes Since Pike (microversion 3.42) [1] Cinder API allows to resize in-use volumes. So no reason not to implement it in CLI. [1] https://opendev.org/openstack/cinder/src/branch/master/cinder/api/openstack/rest_api_version_history.rst#user-content-section-39 Change-Id: I22462a56d261e0a100aac3f27af7be47223edec0 --- openstackclient/volume/client.py | 11 +++++++++-- openstackclient/volume/v2/volume.py | 10 ++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 1fbfaaee78..64e8b9f34d 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -29,6 +29,7 @@ "1": "cinderclient.v1.client.Client", "2": "cinderclient.v2.client.Client", "3": "cinderclient.v3.client.Client", + "3.42": "cinderclient.v3.client.Client", } @@ -47,14 +48,19 @@ def make_client(instance): except Exception: del API_VERSIONS['1'] - if instance._api_version[API_NAME] == '1': + version = instance._api_version[API_NAME] + from cinderclient import api_versions + # convert to APIVersion object + version = api_versions.get_api_version(version) + + if version.ver_major == '1': # Monkey patch for v1 cinderclient volumes.Volume.NAME_ATTR = 'display_name' volume_snapshots.Snapshot.NAME_ATTR = 'display_name' volume_client = utils.get_client_class( API_NAME, - instance._api_version[API_NAME], + version.ver_major, API_VERSIONS ) LOG.debug('Instantiating volume client: %s', volume_client) @@ -76,6 +82,7 @@ def make_client(instance): http_log_debug=http_log_debug, region_name=instance.region_name, endpoint_override=endpoint_override, + api_version=version, **kwargs ) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 1e0cb183fc..cab0b2f4c7 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -605,14 +605,16 @@ def take_action(self, parsed_args): result = 0 if parsed_args.size: try: - if volume.status != 'available': - msg = (_("Volume is in %s state, it must be available " - "before size can be extended") % volume.status) - raise exceptions.CommandError(msg) if parsed_args.size <= volume.size: msg = (_("New size must be greater than %s GB") % volume.size) raise exceptions.CommandError(msg) + if volume.status != 'available' and \ + not volume_client.api_version.matches('3.42'): + + msg = (_("Volume is in %s state, it must be available " + "before size can be extended") % volume.status) + raise exceptions.CommandError(msg) volume_client.volumes.extend(volume.id, parsed_args.size) except Exception as e: LOG.error(_("Failed to set volume size: %s"), e) From ebaf0eae2c60bf000a0af53a8d5f5c54d32fc311 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Nov 2020 11:11:21 +0000 Subject: [PATCH 0309/1120] tests: Remove 'agent' functional tests The 'os-agents' API was recently removed from nova [1]. Remove the functional tests, since they will always fail going forward but will continue to run on older stable branches. Also Squeeze https://review.opendev.org/#/c/762559/ inside, since those 2 are simultaneously blocking gate [1] https://review.opendev.org/#/c/749309/ Change-Id: I0bf7d4c0ba2a9d4637db0d209d8d52163d772f12 Signed-off-by: Stephen Finucane --- .../tests/functional/compute/v2/test_agent.py | 196 ------------------ .../unit/volume/v2/test_volume_backend.py | 2 +- 2 files changed, 1 insertion(+), 197 deletions(-) delete mode 100644 openstackclient/tests/functional/compute/v2/test_agent.py diff --git a/openstackclient/tests/functional/compute/v2/test_agent.py b/openstackclient/tests/functional/compute/v2/test_agent.py deleted file mode 100644 index 25d8c868dc..0000000000 --- a/openstackclient/tests/functional/compute/v2/test_agent.py +++ /dev/null @@ -1,196 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import hashlib -import json - -from openstackclient.tests.functional import base - - -class ComputeAgentTests(base.TestCase): - """Functional tests for compute agent.""" - - # Generate two different md5hash - MD5HASH1 = hashlib.md5() - MD5HASH1.update('agent_1'.encode('utf-8')) - MD5HASH1 = MD5HASH1.hexdigest() - MD5HASH2 = hashlib.md5() - MD5HASH2.update('agent_2'.encode('utf-8')) - MD5HASH2 = MD5HASH2.hexdigest() - - def test_compute_agent_delete(self): - """Test compute agent create, delete multiple""" - os1 = "os_1" - arch1 = "x86_64" - ver1 = "v1" - url1 = "http://localhost" - md5hash1 = self.MD5HASH1 - hyper1 = "kvm" - cmd1 = ' '.join((os1, arch1, ver1, url1, md5hash1, hyper1)) - - cmd_output = json.loads(self.openstack( - 'compute agent create -f json ' + - cmd1 - )) - agent_id1 = str(cmd_output["agent_id"]) - - os2 = "os_2" - arch2 = "x86" - ver2 = "v2" - url2 = "http://openstack" - md5hash2 = self.MD5HASH2 - hyper2 = "xen" - cmd2 = ' '.join((os2, arch2, ver2, url2, md5hash2, hyper2)) - - cmd_output = json.loads(self.openstack( - 'compute agent create -f json ' + - cmd2 - )) - agent_id2 = str(cmd_output["agent_id"]) - - # Test compute agent delete - del_output = self.openstack( - 'compute agent delete ' + - agent_id1 + ' ' + agent_id2 - ) - self.assertOutput('', del_output) - - def test_compute_agent_list(self): - """Test compute agent create and list""" - os1 = "os_1" - arch1 = "x86_64" - ver1 = "v1" - url1 = "http://localhost" - md5hash1 = self.MD5HASH1 - hyper1 = "kvm" - cmd1 = ' '.join((os1, arch1, ver1, url1, md5hash1, hyper1)) - - cmd_output = json.loads(self.openstack( - 'compute agent create -f json ' + - cmd1 - )) - agent_id1 = str(cmd_output["agent_id"]) - self.addCleanup(self.openstack, 'compute agent delete ' + agent_id1) - - os2 = "os_2" - arch2 = "x86" - ver2 = "v2" - url2 = "http://openstack" - md5hash2 = self.MD5HASH2 - hyper2 = "xen" - cmd2 = ' '.join((os2, arch2, ver2, url2, md5hash2, hyper2)) - - cmd_output = json.loads(self.openstack( - 'compute agent create -f json ' + - cmd2 - )) - agent_id2 = str(cmd_output["agent_id"]) - self.addCleanup(self.openstack, 'compute agent delete ' + agent_id2) - - # Test compute agent list - cmd_output = json.loads(self.openstack( - 'compute agent list -f json' - )) - - hypervisors = [x["Hypervisor"] for x in cmd_output] - self.assertIn(hyper1, hypervisors) - self.assertIn(hyper2, hypervisors) - - os = [x['OS'] for x in cmd_output] - self.assertIn(os1, os) - self.assertIn(os2, os) - - archs = [x['Architecture'] for x in cmd_output] - self.assertIn(arch1, archs) - self.assertIn(arch2, archs) - - versions = [x['Version'] for x in cmd_output] - self.assertIn(ver1, versions) - self.assertIn(ver2, versions) - - md5hashes = [x['Md5Hash'] for x in cmd_output] - self.assertIn(md5hash1, md5hashes) - self.assertIn(md5hash2, md5hashes) - - urls = [x['URL'] for x in cmd_output] - self.assertIn(url1, urls) - self.assertIn(url2, urls) - - # Test compute agent list --hypervisor - cmd_output = json.loads(self.openstack( - 'compute agent list -f json ' + - '--hypervisor kvm' - )) - - hypervisors = [x["Hypervisor"] for x in cmd_output] - self.assertIn(hyper1, hypervisors) - self.assertNotIn(hyper2, hypervisors) - - os = [x['OS'] for x in cmd_output] - self.assertIn(os1, os) - self.assertNotIn(os2, os) - - archs = [x['Architecture'] for x in cmd_output] - self.assertIn(arch1, archs) - self.assertNotIn(arch2, archs) - - versions = [x['Version'] for x in cmd_output] - self.assertIn(ver1, versions) - self.assertNotIn(ver2, versions) - - md5hashes = [x['Md5Hash'] for x in cmd_output] - self.assertIn(md5hash1, md5hashes) - self.assertNotIn(md5hash2, md5hashes) - - urls = [x['URL'] for x in cmd_output] - self.assertIn(url1, urls) - self.assertNotIn(url2, urls) - - def test_compute_agent_set(self): - """Test compute agent set""" - os1 = "os_1" - arch1 = "x86_64" - ver1 = "v1" - ver2 = "v2" - url1 = "http://localhost" - url2 = "http://openstack" - md5hash1 = self.MD5HASH1 - md5hash2 = self.MD5HASH2 - hyper1 = "kvm" - cmd = ' '.join((os1, arch1, ver1, url1, md5hash1, hyper1)) - - cmd_output = json.loads(self.openstack( - 'compute agent create -f json ' + - cmd - )) - agent_id = str(cmd_output["agent_id"]) - self.assertEqual(ver1, cmd_output["version"]) - self.assertEqual(url1, cmd_output["url"]) - self.assertEqual(md5hash1, cmd_output["md5hash"]) - - self.addCleanup(self.openstack, 'compute agent delete ' + agent_id) - - raw_output = self.openstack( - 'compute agent set ' + - agent_id + ' ' + - '--agent-version ' + ver2 + ' ' + - '--url ' + url2 + ' ' + - '--md5hash ' + md5hash2 - ) - self.assertOutput('', raw_output) - - cmd_output = json.loads(self.openstack( - 'compute agent list -f json' - )) - self.assertEqual(ver2, cmd_output[0]["Version"]) - self.assertEqual(url2, cmd_output[0]["URL"]) - self.assertEqual(md5hash2, cmd_output[0]["Md5Hash"]) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backend.py b/openstackclient/tests/unit/volume/v2/test_volume_backend.py index db18866087..d9ac2c96c7 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backend.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backend.py @@ -65,7 +65,7 @@ def test_capability_show(self): # confirming if all expected values are present in the result. for cap in data: - self.assertTrue(cap[0] in capabilities) + self.assertIn(cap[0], capabilities) # checking if proper call was made to get capabilities self.capability_mock.get.assert_called_with( From a5101a413983c868c8dd865c6db42b5d87456d0e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 16 Nov 2020 11:15:32 +0000 Subject: [PATCH 0310/1120] trivial: Document removal of support for agents We can't remove these commands for a long time, given OSC's intention to support multiple releases of OpenStack, but we can at least indicate to users that this thing might not work anymore. Change-Id: I9093cc1197a0287984d83e2020fba100d0c958b3 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/agent.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index 3feb99ec83..15fb0f9c36 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -28,7 +28,12 @@ class CreateAgent(command.ShowOne): - _description = _("Create compute agent") + """Create compute agent. + + The compute agent functionality is hypervisor specific and is only + supported by the XenAPI hypervisor driver. It was removed from nova in the + 23.0.0 (Wallaby) release. + """ def get_parser(self, prog_name): parser = super(CreateAgent, self).get_parser(prog_name) @@ -80,7 +85,12 @@ def take_action(self, parsed_args): class DeleteAgent(command.Command): - _description = _("Delete compute agent(s)") + """Delete compute agent(s). + + The compute agent functionality is hypervisor specific and is only + supported by the XenAPI hypervisor driver. It was removed from nova in the + 23.0.0 (Wallaby) release. + """ def get_parser(self, prog_name): parser = super(DeleteAgent, self).get_parser(prog_name) @@ -111,7 +121,12 @@ def take_action(self, parsed_args): class ListAgent(command.Lister): - _description = _("List compute agents") + """List compute agents. + + The compute agent functionality is hypervisor specific and is only + supported by the XenAPI hypervisor driver. It was removed from nova in the + 23.0.0 (Wallaby) release. + """ def get_parser(self, prog_name): parser = super(ListAgent, self).get_parser(prog_name) @@ -141,7 +156,12 @@ def take_action(self, parsed_args): class SetAgent(command.Command): - _description = _("Set compute agent properties") + """Set compute agent properties. + + The compute agent functionality is hypervisor specific and is only + supported by the XenAPI hypervisor driver. It was removed from nova in the + 23.0.0 (Wallaby) release. + """ def get_parser(self, prog_name): parser = super(SetAgent, self).get_parser(prog_name) From 8387b114e38f21922967ba982bb1a25289fdb3ab Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 24 Sep 2020 14:49:55 +0000 Subject: [PATCH 0311/1120] Add "fields" parameter to ListPort query This new query parameter will allow to send a query to the Neutron server filtering only by those parameters needed by the list command: ID, name, MAC address, fixed IPs and status. When using input parameter "long", security groups IDs, device owner and tags will be added to the fields filter. With 4500 ports, those are the execution times for the command "openstack port list" (average values in a development environment): Neutron API (seconds) CLI (seconds) Without filter: 3.05 10.15 With filter: 2.76 8.19 Depends-On: https://review.opendev.org/#/c/754113/ Change-Id: I1cccf0bc3533f8085e8dd61bf2fbe78c49b74b31 Closes-Bug: #1897100 --- lower-constraints.txt | 2 +- openstackclient/network/v2/port.py | 2 +- .../tests/unit/network/v2/test_port.py | 74 ++++++++++++++----- requirements.txt | 2 +- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 2dae561cc9..b489501b87 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -45,7 +45,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.48.0 +openstacksdk==0.51.0 os-service-types==1.7.0 os-testr==1.0.0 osc-lib==2.2.0 diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 02ab06c1c8..cb77759efd 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -665,7 +665,7 @@ def take_action(self, parsed_args): _tag.get_tag_filtering_args(parsed_args, filters) - data = network_client.ports(**filters) + data = network_client.ports(fields=columns, **filters) headers, attrs = utils.calculate_header_and_attrs( column_headers, columns, parsed_args) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index d8889ae558..f7685f46e5 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -26,6 +26,10 @@ from openstackclient.tests.unit import utils as tests_utils +LIST_FIELDS_TO_RETRIEVE = ('id', 'name', 'mac_address', 'fixed_ips', 'status') +LIST_FIELDS_TO_RETRIEVE_LONG = ('security_group_ids', 'device_owner', 'tags') + + class TestPort(network_fakes.TestNetworkV2): def setUp(self): @@ -883,7 +887,8 @@ def test_port_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) - self.network.ports.assert_called_once_with() + self.network.ports.assert_called_once_with( + fields=LIST_FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -901,7 +906,8 @@ def test_port_list_router_opt(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'device_id': 'fake-router-id' + 'device_id': 'fake-router-id', + 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -921,7 +927,8 @@ def test_port_list_with_server_option(self, mock_find): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with( - device_id=fake_server.id) + device_id=fake_server.id, + fields=LIST_FIELDS_TO_RETRIEVE) mock_find.assert_called_once_with(mock.ANY, 'fake-server-name') self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -940,7 +947,8 @@ def test_port_list_device_id_opt(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'device_id': self._ports[0].device_id + 'device_id': self._ports[0].device_id, + 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -959,7 +967,8 @@ def test_port_list_device_owner_opt(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'device_owner': self._ports[0].device_owner + 'device_owner': self._ports[0].device_owner, + 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -987,7 +996,8 @@ def test_port_list_all_opt(self): 'device_owner': self._ports[0].device_owner, 'device_id': 'fake-router-id', 'network_id': 'fake-network-id', - 'mac_address': self._ports[0].mac_address + 'mac_address': self._ports[0].mac_address, + 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1006,7 +1016,8 @@ def test_port_list_mac_address_opt(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'mac_address': self._ports[0].mac_address + 'mac_address': self._ports[0].mac_address, + 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1025,7 +1036,9 @@ def test_port_list_fixed_ip_opt_ip_address(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'fixed_ips': ['ip_address=%s' % ip_address]}) + 'fixed_ips': ['ip_address=%s' % ip_address], + 'fields': LIST_FIELDS_TO_RETRIEVE, + }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1043,7 +1056,9 @@ def test_port_list_fixed_ip_opt_ip_address_substr(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'fixed_ips': ['ip_address_substr=%s' % ip_address_ss]}) + 'fixed_ips': ['ip_address_substr=%s' % ip_address_ss], + 'fields': LIST_FIELDS_TO_RETRIEVE, + }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1063,7 +1078,9 @@ def test_port_list_fixed_ip_opt_subnet_id(self): columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ - 'fixed_ips': ['subnet_id=%s' % subnet_id]}) + 'fixed_ips': ['subnet_id=%s' % subnet_id], + 'fields': LIST_FIELDS_TO_RETRIEVE, + }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1087,7 +1104,9 @@ def test_port_list_fixed_ip_opts(self): self.network.ports.assert_called_once_with(**{ 'fixed_ips': ['subnet_id=%s' % subnet_id, - 'ip_address=%s' % ip_address]}) + 'ip_address=%s' % ip_address], + 'fields': LIST_FIELDS_TO_RETRIEVE, + }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1103,15 +1122,19 @@ def test_port_list_fixed_ips(self): {'ip-address': ip_address}]) ] - self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( - {'id': subnet_id}) + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet({ + 'id': subnet_id, + 'fields': LIST_FIELDS_TO_RETRIEVE, + }) self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.network.ports.assert_called_once_with(**{ 'fixed_ips': ['subnet_id=%s' % subnet_id, - 'ip_address=%s' % ip_address]}) + 'ip_address=%s' % ip_address], + 'fields': LIST_FIELDS_TO_RETRIEVE, + }) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) @@ -1128,7 +1151,8 @@ def test_list_port_with_long(self): columns, data = self.cmd.take_action(parsed_args) - self.network.ports.assert_called_once_with() + self.network.ports.assert_called_once_with( + fields=LIST_FIELDS_TO_RETRIEVE + LIST_FIELDS_TO_RETRIEVE_LONG) self.assertEqual(self.columns_long, columns) self.assertListItemEqual(self.data_long, list(data)) @@ -1142,7 +1166,10 @@ def test_port_list_host(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'binding:host_id': 'foobar'} + filters = { + 'binding:host_id': 'foobar', + 'fields': LIST_FIELDS_TO_RETRIEVE, + } self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -1160,7 +1187,11 @@ def test_port_list_project(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = { + 'tenant_id': project.id, + 'project_id': project.id, + 'fields': LIST_FIELDS_TO_RETRIEVE, + } self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -1180,7 +1211,11 @@ def test_port_list_project_domain(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = { + 'tenant_id': project.id, + 'project_id': project.id, + 'fields': LIST_FIELDS_TO_RETRIEVE, + } self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -1206,7 +1241,8 @@ def test_list_with_tag_options(self): **{'tags': 'red,blue', 'any_tags': 'red,green', 'not_tags': 'orange,yellow', - 'not_any_tags': 'black,white'} + 'not_any_tags': 'black,white', + 'fields': LIST_FIELDS_TO_RETRIEVE} ) self.assertEqual(self.columns, columns) self.assertListItemEqual(self.data, list(data)) diff --git a/requirements.txt b/requirements.txt index 99d409bf51..9430a7fde0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 cliff>=3.4.0 # Apache-2.0 iso8601>=0.1.11 # MIT -openstacksdk>=0.48.0 # Apache-2.0 +openstacksdk>=0.51.0 # Apache-2.0 osc-lib>=2.2.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 From 5bdcd590ecacbc0aa8db2cbafa0ab1a9f3c28ce0 Mon Sep 17 00:00:00 2001 From: Simon Merrick Date: Thu, 19 Nov 2020 20:05:54 +1300 Subject: [PATCH 0312/1120] stop image downloads to memory + Fixes issue with large images hogging memory + stream image downloads + output to stdout if file not specified Change-Id: Ia01ff9b21a2dac5d0ccf2bd58a8640e88c5cbb36 Story: 2007672 Task: 39776 --- openstackclient/image/v1/image.py | 6 +++++- openstackclient/image/v2/image.py | 6 +++++- openstackclient/tests/unit/image/v2/test_image.py | 3 ++- .../fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml | 7 +++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index cf1d68171a..64aa3fcdab 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -478,7 +478,11 @@ def take_action(self, parsed_args): image_client = self.app.client_manager.image image = image_client.find_image(parsed_args.image) - image_client.download_image(image.id, output=parsed_args.file) + output_file = parsed_args.file + if output_file is None: + output_file = getattr(sys.stdout, "buffer", sys.stdout) + + image_client.download_image(image.id, stream=True, output=output_file) class SetImage(command.Command): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 4f3e9d0bcf..58d92f5165 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -803,7 +803,11 @@ def take_action(self, parsed_args): image_client = self.app.client_manager.image image = image_client.find_image(parsed_args.image) - image_client.download_image(image.id, output=parsed_args.file) + output_file = parsed_args.file + if output_file is None: + output_file = getattr(sys.stdout, "buffer", sys.stdout) + + image_client.download_image(image.id, stream=True, output=output_file) class SetImage(command.Command): diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index b094817efe..80e60ee818 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1618,7 +1618,7 @@ def test_save_data(self): verifylist = [ ('file', '/path/to/file'), - ('image', self.image.id) + ('image', self.image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1626,6 +1626,7 @@ def test_save_data(self): self.client.download_image.assert_called_once_with( self.image.id, + stream=True, output='/path/to/file') diff --git a/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml b/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml new file mode 100644 index 0000000000..857f3c507e --- /dev/null +++ b/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Stream image download to avoid buffering data in memory which rapidly + exhausts memory resulting in OOM kill or system crash for all but the + smallest of images. Fixes https://storyboard.openstack.org/#!/story/2007672 + - Restore default behavior of 'openstack image save' to send data to stdout + Relates to https://storyboard.openstack.org/#!/story/2007672. \ No newline at end of file From 0f02029d917366c6c757ebd2644d83e4fef1f33e Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Tue, 1 Dec 2020 12:44:30 +0200 Subject: [PATCH 0313/1120] Add option to filter instances by AZ Since nova API microversion 2.83 it is possible for users to filter instances by AZ. However even before that this functionality was available for admin role. Change-Id: Ife4c8e81aad2ff1dde50d9f23913d9dd9397b00c --- openstackclient/compute/v2/server.py | 8 ++++++++ openstackclient/tests/unit/compute/v2/test_server.py | 1 + 2 files changed, 9 insertions(+) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1e39010af4..c6da0fbaca 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1365,6 +1365,13 @@ class ListServer(command.Lister): def get_parser(self, prog_name): parser = super(ListServer, self).get_parser(prog_name) + parser.add_argument( + '--availability-zone', + metavar='', + help=_('Only return instances that match the availability zone. ' + 'Note that this option will be ignored for non-admin users ' + 'when using ``--os-compute-api-version`` prior to 2.83.'), + ) parser.add_argument( '--reservation-id', metavar='', @@ -1574,6 +1581,7 @@ def take_action(self, parsed_args): ignore_missing=False).id search_opts = { + 'availability_zone': parsed_args.availability_zone, 'reservation_id': parsed_args.reservation_id, 'ip': parsed_args.ip, 'ip6': parsed_args.ip6, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5fd15e6ab6..dfb8df30d0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2962,6 +2962,7 @@ def setUp(self): super(TestServerList, self).setUp() self.search_opts = { + 'availability_zone': None, 'reservation_id': None, 'ip': None, 'ip6': None, From 3e8968af3dc82715dc93e7e76209e1bdcbdb2c14 Mon Sep 17 00:00:00 2001 From: yanpuqing Date: Wed, 15 Aug 2018 07:36:10 +0000 Subject: [PATCH 0314/1120] Add NODE and HOST parameters in "server create" help text Add optional parameters "NODE" and "HOST" in the help text of the server create comand for --availability-zone. Co-Authored-By: tianhui Change-Id: I4faea8a3d3aecb21ec535e55c238c71745fc68cb Task: 24274 Story: 2003313 --- openstackclient/compute/v2/server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1e39010af4..996c924b2e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -711,7 +711,12 @@ def get_parser(self, prog_name): parser.add_argument( '--availability-zone', metavar='', - help=_('Select an availability zone for the server'), + help=_('Select an availability zone for the server. ' + 'Host and node are optional parameters. ' + 'Availability zone in the format ' + '::, ' + '::, : ' + 'or '), ) parser.add_argument( '--host', From 284c38bcf2624c8b82d77f2eb5a2015822f21aa0 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Thu, 31 Oct 2019 18:38:21 -0500 Subject: [PATCH 0315/1120] Let autoprogram-cliff know who's running The autoprogram-cliff directive has a habit of producing text like This command is provided by the $me plugin. which doesn't make any sense. Cliff recently added a config option whereby consumers can let it know who $me is so it can suppress that message where appropriate (while still producing it for $plugin, as intended). Depends-On: https://review.opendev.org/692464 Change-Id: I0d580fb1d34dd56740eb6d976caa795e0e951047 --- doc/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index 2216ddd7c2..4b60ce4a5d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -263,6 +263,9 @@ '--help', '--format', '--column', '--max-width', '--fit-width', '--print-empty', '--prefix', '--noindent', '--quote'] +# Prevent cliff from generating "This command is provided by the +# python-openstackclient plugin." +autoprogram_cliff_app_dist_name = 'python-openstackclient' # -- Options for sphinxcontrib.apidoc ---------------------------------------- From 0f4f42b65281b9b8b4f8fc3e58da8c9d8b68ee08 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 Sep 2020 22:20:07 +0200 Subject: [PATCH 0316/1120] Switch compute flavors from novaclient/direct to SDK Let's switch flavors from novaclient or direct API requests onto using SDK. Microversion agreement comes out of the box. SDK normalizes property names, while OSC uses server side names. In order not to break OSC users continue using server-side names. Depends-On: https://review.opendev.org/#/c/762989/ Change-Id: I62b2ed8488ee4ac9c42051311bcfb455506ddd90 --- lower-constraints.txt | 2 +- openstackclient/compute/v2/flavor.py | 262 ++++----- .../tests/unit/compute/v2/fakes.py | 20 +- .../tests/unit/compute/v2/test_flavor.py | 502 +++++++++++------- ...switch-flavor-to-sdk-b874a3c39559815e.yaml | 4 + requirements.txt | 2 +- 6 files changed, 453 insertions(+), 339 deletions(-) create mode 100644 releasenotes/notes/switch-flavor-to-sdk-b874a3c39559815e.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index b489501b87..e08b05d22c 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -45,7 +45,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.51.0 +openstacksdk==0.52.0 os-service-types==1.7.0 os-testr==1.0.0 osc-lib==2.2.0 diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 00431b7b73..8477e8efb1 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -17,7 +17,8 @@ import logging -from novaclient import api_versions +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command @@ -33,10 +34,7 @@ _formatters = { 'extra_specs': format_columns.DictColumn, - # Unless we finish switch to use SDK resources this need to be doubled this - # way - 'properties': format_columns.DictColumn, - 'Properties': format_columns.DictColumn + 'properties': format_columns.DictColumn } @@ -51,29 +49,10 @@ def _get_flavor_columns(item): } hidden_columns = ['links', 'location'] - return utils.get_osc_show_columns_for_sdk_resource( item, column_map, hidden_columns) -def _find_flavor(compute_client, flavor): - try: - return compute_client.flavors.get(flavor) - except Exception as ex: - if type(ex).__name__ == 'NotFound': - pass - else: - raise - try: - return compute_client.flavors.find(name=flavor, is_public=None) - except Exception as ex: - if type(ex).__name__ == 'NotFound': - msg = _("No flavor with a name or ID of '%s' exists.") % flavor - raise exceptions.CommandError(msg) - else: - raise - - class CreateFlavor(command.ShowOne): _description = _("Create new flavor") @@ -87,9 +66,7 @@ def get_parser(self, prog_name): parser.add_argument( "--id", metavar="", - default='auto', - help=_("Unique flavor ID; 'auto' creates a UUID " - "(default: auto)") + help=_("Unique flavor ID") ) parser.add_argument( "--ram", @@ -170,32 +147,36 @@ 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 identity_client = self.app.client_manager.identity if parsed_args.project and parsed_args.public: msg = _("--project is only allowed with --private") raise exceptions.CommandError(msg) + args = { + 'name': parsed_args.name, + 'ram': parsed_args.ram, + 'vcpus': parsed_args.vcpus, + 'disk': parsed_args.disk, + 'id': parsed_args.id, + 'ephemeral': parsed_args.ephemeral, + 'swap': parsed_args.swap, + 'rxtx_factor': parsed_args.rxtx_factor, + 'is_public': parsed_args.public, + } + if parsed_args.description: - if compute_client.api_version < api_versions.APIVersion("2.55"): - msg = _("--os-compute-api-version 2.55 or later is required") + if not sdk_utils.supports_microversion(compute_client, '2.55'): + msg = _( + 'The --description parameter requires server support for ' + 'API microversion 2.55' + ) raise exceptions.CommandError(msg) - args = ( - parsed_args.name, - parsed_args.ram, - parsed_args.vcpus, - parsed_args.disk, - parsed_args.id, - parsed_args.ephemeral, - parsed_args.swap, - parsed_args.rxtx_factor, - parsed_args.public, - parsed_args.description - ) + args['description'] = parsed_args.description - flavor = compute_client.flavors.create(*args) + flavor = compute_client.create_flavor(**args) if parsed_args.project: try: @@ -204,7 +185,7 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain, ).id - compute_client.flavor_access.add_tenant_access( + compute_client.flavor_add_tenant_access( flavor.id, project_id) except Exception as e: msg = _("Failed to add project %(project)s access to " @@ -212,19 +193,14 @@ def take_action(self, parsed_args): LOG.error(msg, {'project': parsed_args.project, 'e': e}) if parsed_args.property: try: - flavor.set_keys(parsed_args.property) + flavor = compute_client.create_flavor_extra_specs( + flavor, parsed_args.property) except Exception as e: LOG.error(_("Failed to set flavor property: %s"), e) - flavor_info = flavor._info.copy() - flavor_info['properties'] = flavor.get_keys() - - display_columns, columns = _get_flavor_columns(flavor_info) - data = utils.get_dict_properties( - flavor_info, columns, - formatters=_formatters, - mixed_case_fields=['OS-FLV-DISABLED:disabled', - 'OS-FLV-EXT-DATA:ephemeral']) + display_columns, columns = _get_flavor_columns(flavor) + data = utils.get_dict_properties(flavor, columns, + formatters=_formatters) return (display_columns, data) @@ -243,12 +219,12 @@ 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 result = 0 for f in parsed_args.flavor: try: - flavor = _find_flavor(compute_client, f) - compute_client.flavors.delete(flavor.id) + flavor = compute_client.find_flavor(f, ignore_missing=False) + compute_client.delete_flavor(flavor.id) except Exception as e: result += 1 LOG.error(_("Failed to delete flavor with name or " @@ -307,37 +283,63 @@ 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 + # is_public is ternary - None means give all flavors, + # True is public only and False is private only + # By default Nova assumes True and gives admins public flavors + # and flavors from their own projects only. + is_public = None if parsed_args.all else parsed_args.public + + query_attrs = { + 'is_public': is_public + } + if parsed_args.marker: + query_attrs['marker'] = parsed_args.marker + if parsed_args.limit: + query_attrs['limit'] = parsed_args.limit + if parsed_args.limit or parsed_args.marker: + # User passed explicit pagination request, switch off SDK + # pagination + query_attrs['paginated'] = False + + data = list(compute_client.flavors(**query_attrs)) + # 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: + compute_client.fetch_flavor_extra_specs(f) + columns = ( + "id", + "name", + "ram", + "disk", + "ephemeral", + "vcpus", + "is_public" + ) + if parsed_args.long: + columns += ( + "swap", + "rxtx_factor", + "extra_specs", + ) + + column_headers = ( "ID", "Name", "RAM", "Disk", "Ephemeral", "VCPUs", - "Is Public", + "Is Public" ) - - # is_public is ternary - None means give all flavors, - # True is public only and False is private only - # By default Nova assumes True and gives admins public flavors - # and flavors from their own projects only. - is_public = None if parsed_args.all else parsed_args.public - - data = compute_client.flavors.list(is_public=is_public, - marker=parsed_args.marker, - limit=parsed_args.limit) - if parsed_args.long: - columns = columns + ( + column_headers += ( "Swap", "RXTX Factor", "Properties", ) - for f in data: - f.properties = f.get_keys() - - column_headers = columns return (column_headers, (utils.get_item_properties( @@ -387,24 +389,42 @@ 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 identity_client = self.app.client_manager.identity - flavor = _find_flavor(compute_client, parsed_args.flavor) + try: + flavor = compute_client.find_flavor( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False) + except sdk_exceptions.ResourceNotFound as e: + raise exceptions.CommandError(e.message) + + if parsed_args.description: + if not sdk_utils.supports_microversion(compute_client, '2.55'): + msg = _( + 'The --description parameter requires server support for ' + 'API microversion 2.55' + ) + raise exceptions.CommandError(msg) + + compute_client.update_flavor( + flavor=flavor.id, description=parsed_args.description) result = 0 - key_list = [] if parsed_args.no_property: try: - for key in flavor.get_keys().keys(): - key_list.append(key) - flavor.unset_keys(key_list) + for key in flavor.extra_specs.keys(): + compute_client.delete_flavor_extra_specs_property( + flavor.id, key) except Exception as e: LOG.error(_("Failed to clear flavor property: %s"), e) result += 1 + if parsed_args.property: try: - flavor.set_keys(parsed_args.property) + compute_client.create_flavor_extra_specs( + flavor.id, parsed_args.property) except Exception as e: LOG.error(_("Failed to set flavor property: %s"), e) result += 1 @@ -420,7 +440,7 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain, ).id - compute_client.flavor_access.add_tenant_access( + compute_client.flavor_add_tenant_access( flavor.id, project_id) except Exception as e: LOG.error(_("Failed to set flavor access to project: %s"), e) @@ -430,13 +450,6 @@ def take_action(self, parsed_args): raise exceptions.CommandError(_("Command Failed: One or more of" " the operations failed")) - if parsed_args.description: - if compute_client.api_version < api_versions.APIVersion("2.55"): - msg = _("--os-compute-api-version 2.55 or later is required") - raise exceptions.CommandError(msg) - compute_client.flavors.update(flavor=flavor.id, - description=parsed_args.description) - class ShowFlavor(command.ShowOne): _description = _("Display flavor details") @@ -451,35 +464,32 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - resource_flavor = _find_flavor(compute_client, parsed_args.flavor) + compute_client = self.app.client_manager.sdk_connection.compute + flavor = compute_client.find_flavor( + parsed_args.flavor, get_extra_specs=True, ignore_missing=False) access_projects = None # get access projects list of this flavor - if not resource_flavor.is_public: + if not flavor.is_public: try: - flavor_access = compute_client.flavor_access.list( - flavor=resource_flavor.id) - access_projects = [utils.get_field(access, 'tenant_id') - for access in flavor_access] + flavor_access = compute_client.get_flavor_access( + flavor=flavor.id) + access_projects = [ + utils.get_field(access, 'tenant_id') + for access in flavor_access] except Exception as e: msg = _("Failed to get access projects list " "for flavor '%(flavor)s': %(e)s") LOG.error(msg, {'flavor': parsed_args.flavor, 'e': e}) - flavor = resource_flavor._info.copy() - flavor.update({ - 'access_project_ids': access_projects - }) - - flavor['properties'] = resource_flavor.get_keys() + # Since we need to inject "access_project_id" into resource - convert + # it to dict and treat it respectively + flavor = flavor.to_dict() + flavor['access_project_ids'] = access_projects display_columns, columns = _get_flavor_columns(flavor) data = utils.get_dict_properties( - flavor, columns, - formatters=_formatters, - mixed_case_fields=['OS-FLV-DISABLED:disabled', - 'OS-FLV-EXT-DATA:ephemeral']) + flavor, columns, formatters=_formatters) return (display_columns, data) @@ -512,32 +522,40 @@ 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 identity_client = self.app.client_manager.identity - flavor = _find_flavor(compute_client, parsed_args.flavor) + try: + flavor = compute_client.find_flavor( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False) + except sdk_exceptions.ResourceNotFound as e: + raise exceptions.CommandError(_(e.message)) result = 0 if parsed_args.property: - try: - flavor.unset_keys(parsed_args.property) - except Exception as e: - LOG.error(_("Failed to unset flavor property: %s"), e) - result += 1 + for key in parsed_args.property: + try: + compute_client.delete_flavor_extra_specs_property( + flavor.id, key) + except sdk_exceptions.SDKException as e: + LOG.error(_("Failed to unset flavor property: %s"), e) + result += 1 if parsed_args.project: try: if flavor.is_public: msg = _("Cannot remove access for a public flavor") raise exceptions.CommandError(msg) - else: - project_id = identity_common.find_project( - identity_client, - parsed_args.project, - parsed_args.project_domain, - ).id - compute_client.flavor_access.remove_tenant_access( - flavor.id, project_id) + + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + compute_client.flavor_remove_tenant_access( + flavor.id, project_id) except Exception as e: LOG.error(_("Failed to remove flavor access from project: %s"), e) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 3a06d27137..d3d037a9a8 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -19,6 +19,7 @@ import uuid from novaclient import api_versions +from openstack.compute.v2 import flavor as _flavor from openstackclient.api import compute_v2 from openstackclient.tests.unit import fakes @@ -164,7 +165,6 @@ def __init__(self, **kwargs): self.extensions.resource_class = fakes.FakeResource(None, {}) self.flavors = mock.Mock() - self.flavors.resource_class = fakes.FakeResource(None, {}) self.flavor_access = mock.Mock() self.flavor_access.resource_class = fakes.FakeResource(None, {}) @@ -777,27 +777,13 @@ def create_one_flavor(attrs=None): 'os-flavor-access:is_public': True, 'description': 'description', 'OS-FLV-EXT-DATA:ephemeral': 0, - 'properties': {'property': 'value'}, + 'extra_specs': {'property': 'value'}, } # Overwrite default attributes. flavor_info.update(attrs) - # Set default methods. - flavor_methods = { - 'set_keys': None, - 'unset_keys': None, - 'get_keys': {'property': 'value'}, - } - - flavor = fakes.FakeResource(info=copy.deepcopy(flavor_info), - methods=flavor_methods, - loaded=True) - - # Set attributes with special mappings in nova client. - flavor.disabled = flavor_info['OS-FLV-DISABLED:disabled'] - flavor.is_public = flavor_info['os-flavor-access:is_public'] - flavor.ephemeral = flavor_info['OS-FLV-EXT-DATA:ephemeral'] + flavor = _flavor.Flavor(**flavor_info) return flavor diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 2828d74e36..8625b71202 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -12,11 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. # - from unittest import mock -from unittest.mock import call -import novaclient +from openstack.compute.v2 import flavor as _flavor +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib import exceptions @@ -31,13 +31,19 @@ class TestFlavor(compute_fakes.TestComputev2): def setUp(self): super(TestFlavor, self).setUp() - # Get a shortcut to the FlavorManager Mock - self.flavors_mock = self.app.client_manager.compute.flavors - self.flavors_mock.reset_mock() - - # Get a shortcut to the FlavorAccessManager Mock - self.flavor_access_mock = self.app.client_manager.compute.flavor_access - self.flavor_access_mock.reset_mock() + # SDK mock + self.app.client_manager.sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection.compute = mock.Mock() + self.sdk_client = self.app.client_manager.sdk_connection.compute + self.sdk_client.flavors = mock.Mock() + self.sdk_client.find_flavor = mock.Mock() + self.sdk_client.delete_flavor = mock.Mock() + self.sdk_client.update_flavor = mock.Mock() + self.sdk_client.flavor_add_tenant_access = mock.Mock() + self.sdk_client.flavor_remove_tenant_access = mock.Mock() + self.sdk_client.create_flavor_extra_specs = mock.Mock() + self.sdk_client.update_flavor_extra_specs_property = mock.Mock() + self.sdk_client.delete_flavor_extra_specs_property = mock.Mock() self.projects_mock = self.app.client_manager.identity.projects self.projects_mock.reset_mock() @@ -48,6 +54,7 @@ class TestFlavorCreate(TestFlavor): flavor = compute_fakes.FakeFlavor.create_one_flavor( attrs={'links': 'flavor-links'}) project = identity_fakes.FakeProject.create_one_project() + columns = ( 'OS-FLV-DISABLED:disabled', 'OS-FLV-EXT-DATA:ephemeral', @@ -60,17 +67,32 @@ class TestFlavorCreate(TestFlavor): 'ram', 'rxtx_factor', 'swap', - 'vcpus', + 'vcpus' ) + data = ( - flavor.disabled, + flavor.is_disabled, flavor.ephemeral, flavor.description, flavor.disk, flavor.id, flavor.name, flavor.is_public, - format_columns.DictColumn(flavor.properties), + format_columns.DictColumn(flavor.extra_specs), + flavor.ram, + flavor.rxtx_factor, + flavor.swap, + flavor.vcpus, + ) + data_private = ( + flavor.is_disabled, + flavor.ephemeral, + flavor.description, + flavor.disk, + flavor.id, + flavor.name, + False, + format_columns.DictColumn(flavor.extra_specs), flavor.ram, flavor.rxtx_factor, flavor.swap, @@ -82,7 +104,7 @@ def setUp(self): # Return a project self.projects_mock.get.return_value = self.project - self.flavors_mock.create.return_value = self.flavor + self.sdk_client.create_flavor.return_value = self.flavor self.cmd = flavor.CreateFlavor(self.app, None) def test_flavor_create_default_options(self): @@ -95,20 +117,20 @@ def test_flavor_create_default_options(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - default_args = ( - self.flavor.name, - 256, - 1, - 0, - 'auto', - 0, - 0, - 1.0, - True, - None, - ) + default_args = { + 'name': self.flavor.name, + 'ram': 256, + 'vcpus': 1, + 'disk': 0, + 'id': None, + 'ephemeral': 0, + 'swap': 0, + 'rxtx_factor': 1.0, + 'is_public': True, + } + columns, data = self.cmd.take_action(parsed_args) - self.flavors_mock.create.assert_called_once_with(*default_args) + self.sdk_client.create_flavor.assert_called_once_with(**default_args) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -143,29 +165,44 @@ def test_flavor_create_all_options(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - args = ( - self.flavor.name, - self.flavor.ram, - self.flavor.vcpus, - self.flavor.disk, - self.flavor.id, - self.flavor.ephemeral, - self.flavor.swap, - self.flavor.rxtx_factor, - self.flavor.is_public, - self.flavor.description, - ) - self.app.client_manager.compute.api_version = 2.55 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + args = { + 'name': self.flavor.name, + 'ram': self.flavor.ram, + 'vcpus': self.flavor.vcpus, + 'disk': self.flavor.disk, + 'id': self.flavor.id, + 'ephemeral': self.flavor.ephemeral, + 'swap': self.flavor.swap, + 'rxtx_factor': self.flavor.rxtx_factor, + 'is_public': self.flavor.is_public, + 'description': self.flavor.description + } + + props = {'property': 'value'} + + # SDK updates the flavor object instance. In order to make the + # verification clear and preciese let's create new flavor and change + # expected props this way + create_flavor = _flavor.Flavor(**self.flavor) + expected_flavor = _flavor.Flavor(**self.flavor) + expected_flavor.extra_specs = props + # convert expected data tuple to list to be able to modify it + cmp_data = list(self.data) + cmp_data[7] = format_columns.DictColumn(props) + self.sdk_client.create_flavor.return_value = create_flavor + self.sdk_client.create_flavor_extra_specs.return_value = \ + expected_flavor + + with mock.patch.object(sdk_utils, 'supports_microversion', + return_value=True): columns, data = self.cmd.take_action(parsed_args) - self.flavors_mock.create.assert_called_once_with(*args) - self.flavor.set_keys.assert_called_once_with({'property': 'value'}) - self.flavor.get_keys.assert_called_once_with() + self.sdk_client.create_flavor.assert_called_once_with(**args) + self.sdk_client.create_flavor_extra_specs.assert_called_once_with( + create_flavor, props) + self.sdk_client.get_flavor_access.assert_not_called() - self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertEqual(self.columns, columns) + self.assertItemEqual(tuple(cmp_data), data) def test_flavor_create_other_options(self): @@ -200,33 +237,47 @@ def test_flavor_create_other_options(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - args = ( - self.flavor.name, - self.flavor.ram, - self.flavor.vcpus, - self.flavor.disk, - 'auto', - self.flavor.ephemeral, - self.flavor.swap, - self.flavor.rxtx_factor, - self.flavor.is_public, - self.flavor.description, - ) - self.app.client_manager.compute.api_version = 2.55 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + args = { + 'name': self.flavor.name, + 'ram': self.flavor.ram, + 'vcpus': self.flavor.vcpus, + 'disk': self.flavor.disk, + 'id': 'auto', + 'ephemeral': self.flavor.ephemeral, + 'swap': self.flavor.swap, + 'rxtx_factor': self.flavor.rxtx_factor, + 'is_public': False, + 'description': self.flavor.description + } + + props = {'key1': 'value1', 'key2': 'value2'} + + # SDK updates the flavor object instance. In order to make the + # verification clear and preciese let's create new flavor and change + # expected props this way + create_flavor = _flavor.Flavor(**self.flavor) + expected_flavor = _flavor.Flavor(**self.flavor) + expected_flavor.extra_specs = props + expected_flavor.is_public = False + # convert expected data tuple to list to be able to modify it + cmp_data = list(self.data_private) + cmp_data[7] = format_columns.DictColumn(props) + self.sdk_client.create_flavor.return_value = create_flavor + self.sdk_client.create_flavor_extra_specs.return_value = \ + expected_flavor + + with mock.patch.object(sdk_utils, 'supports_microversion', + return_value=True): columns, data = self.cmd.take_action(parsed_args) - self.flavors_mock.create.assert_called_once_with(*args) - self.flavor_access_mock.add_tenant_access.assert_called_with( + self.sdk_client.create_flavor.assert_called_once_with(**args) + self.sdk_client.flavor_add_tenant_access.assert_called_with( self.flavor.id, self.project.id, ) - self.flavor.set_keys.assert_called_with( - {'key1': 'value1', 'key2': 'value2'}) - self.flavor.get_keys.assert_called_with() + self.sdk_client.create_flavor_extra_specs.assert_called_with( + create_flavor, props) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemEqual(cmp_data, data) def test_public_flavor_create_with_project(self): arglist = [ @@ -278,29 +329,28 @@ def test_flavor_create_with_description_api_newer(self): ('name', self.flavor.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.compute.api_version = 2.55 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + with mock.patch.object(sdk_utils, 'supports_microversion', + return_value=True): + columns, data = self.cmd.take_action(parsed_args) - args = ( - self.flavor.name, - self.flavor.ram, - self.flavor.vcpus, - self.flavor.disk, - self.flavor.id, - self.flavor.ephemeral, - self.flavor.swap, - self.flavor.rxtx_factor, - False, - 'fake description', - ) + args = { + 'name': self.flavor.name, + 'ram': self.flavor.ram, + 'vcpus': self.flavor.vcpus, + 'disk': self.flavor.disk, + 'id': self.flavor.id, + 'ephemeral': self.flavor.ephemeral, + 'swap': self.flavor.swap, + 'rxtx_factor': self.flavor.rxtx_factor, + 'is_public': self.flavor.is_public, + 'description': 'fake description' + } - self.flavors_mock.create.assert_called_once_with(*args) + self.sdk_client.create_flavor.assert_called_once_with(**args) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemEqual(self.data_private, data) def test_flavor_create_with_description_api_older(self): arglist = [ @@ -318,10 +368,8 @@ def test_flavor_create_with_description_api_older(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.compute.api_version = 2.54 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + with mock.patch.object(sdk_utils, 'supports_microversion', + return_value=False): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) @@ -333,9 +381,7 @@ class TestFlavorDelete(TestFlavor): def setUp(self): super(TestFlavorDelete, self).setUp() - self.flavors_mock.get = ( - compute_fakes.FakeFlavor.get_flavors(self.flavors)) - self.flavors_mock.delete.return_value = None + self.sdk_client.delete_flavor.return_value = None self.cmd = flavor.DeleteFlavor(self.app, None) @@ -348,9 +394,13 @@ def test_flavor_delete(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.sdk_client.find_flavor.return_value = self.flavors[0] + result = self.cmd.take_action(parsed_args) - self.flavors_mock.delete.assert_called_with(self.flavors[0].id) + self.sdk_client.find_flavor.assert_called_with(self.flavors[0].id, + ignore_missing=False) + self.sdk_client.delete_flavor.assert_called_with(self.flavors[0].id) self.assertIsNone(result) def test_delete_multiple_flavors(self): @@ -362,12 +412,17 @@ def test_delete_multiple_flavors(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.sdk_client.find_flavor.side_effect = self.flavors + result = self.cmd.take_action(parsed_args) - calls = [] - for f in self.flavors: - calls.append(call(f.id)) - self.flavors_mock.delete.assert_has_calls(calls) + find_calls = [ + mock.call(i.id, ignore_missing=False) for i in self.flavors + ] + delete_calls = [mock.call(i.id) for i in self.flavors] + self.sdk_client.find_flavor.assert_has_calls(find_calls) + self.sdk_client.delete_flavor.assert_has_calls(delete_calls) self.assertIsNone(result) def test_multi_flavors_delete_with_exception(self): @@ -380,11 +435,10 @@ def test_multi_flavors_delete_with_exception(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.flavors[0], exceptions.CommandError] - self.flavors_mock.get = ( - mock.Mock(side_effect=find_mock_result) - ) - self.flavors_mock.find.side_effect = exceptions.NotFound(None) + self.sdk_client.find_flavor.side_effect = [ + self.flavors[0], + sdk_exceptions.ResourceNotFound + ] try: self.cmd.take_action(parsed_args) @@ -392,15 +446,18 @@ def test_multi_flavors_delete_with_exception(self): except exceptions.CommandError as e: self.assertEqual('1 of 2 flavors failed to delete.', str(e)) - self.flavors_mock.get.assert_any_call(self.flavors[0].id) - self.flavors_mock.get.assert_any_call('unexist_flavor') - self.flavors_mock.delete.assert_called_once_with(self.flavors[0].id) + find_calls = [ + mock.call(self.flavors[0].id, ignore_missing=False), + mock.call('unexist_flavor', ignore_missing=False), + ] + delete_calls = [mock.call(self.flavors[0].id)] + self.sdk_client.find_flavor.assert_has_calls(find_calls) + self.sdk_client.delete_flavor.assert_has_calls(delete_calls) class TestFlavorList(TestFlavor): - # Return value of self.flavors_mock.list(). - flavors = compute_fakes.FakeFlavor.create_flavors(count=1) + _flavor = compute_fakes.FakeFlavor.create_one_flavor() columns = ( 'ID', @@ -418,24 +475,27 @@ class TestFlavorList(TestFlavor): ) data = (( - flavors[0].id, - flavors[0].name, - flavors[0].ram, - flavors[0].disk, - flavors[0].ephemeral, - flavors[0].vcpus, - flavors[0].is_public, - ), ) + _flavor.id, + _flavor.name, + _flavor.ram, + _flavor.disk, + _flavor.ephemeral, + _flavor.vcpus, + _flavor.is_public, + ),) data_long = (data[0] + ( - flavors[0].swap, - flavors[0].rxtx_factor, - format_columns.DictColumn(flavors[0].properties) + _flavor.swap, + _flavor.rxtx_factor, + format_columns.DictColumn(_flavor.extra_specs) ), ) def setUp(self): super(TestFlavorList, self).setUp() - self.flavors_mock.list.return_value = self.flavors + self.api_mock = mock.Mock() + self.api_mock.side_effect = [[self._flavor], [], ] + + self.sdk_client.flavors = self.api_mock # Get the command object to test self.cmd = flavor.ListFlavor(self.app, None) @@ -458,16 +518,14 @@ def test_flavor_list_no_options(self): # Set expected values kwargs = { 'is_public': True, - 'limit': None, - 'marker': None } - self.flavors_mock.list.assert_called_with( + self.sdk_client.flavors.assert_called_with( **kwargs ) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_flavor_list_all_flavors(self): arglist = [ @@ -487,16 +545,14 @@ def test_flavor_list_all_flavors(self): # Set expected values kwargs = { 'is_public': None, - 'limit': None, - 'marker': None } - self.flavors_mock.list.assert_called_with( + self.sdk_client.flavors.assert_called_with( **kwargs ) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_flavor_list_private_flavors(self): arglist = [ @@ -516,16 +572,14 @@ def test_flavor_list_private_flavors(self): # Set expected values kwargs = { 'is_public': False, - 'limit': None, - 'marker': None } - self.flavors_mock.list.assert_called_with( + self.sdk_client.flavors.assert_called_with( **kwargs ) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_flavor_list_public_flavors(self): arglist = [ @@ -545,16 +599,14 @@ def test_flavor_list_public_flavors(self): # Set expected values kwargs = { 'is_public': True, - 'limit': None, - 'marker': None } - self.flavors_mock.list.assert_called_with( + self.sdk_client.flavors.assert_called_with( **kwargs ) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_flavor_list_long(self): arglist = [ @@ -574,11 +626,9 @@ def test_flavor_list_long(self): # Set expected values kwargs = { 'is_public': True, - 'limit': None, - 'marker': None } - self.flavors_mock.list.assert_called_with( + self.sdk_client.flavors.assert_called_with( **kwargs ) @@ -588,7 +638,7 @@ def test_flavor_list_long(self): class TestFlavorSet(TestFlavor): - # Return value of self.flavors_mock.find(). + # Return value of self.sdk_client.find_flavor(). flavor = compute_fakes.FakeFlavor.create_one_flavor( attrs={'os-flavor-access:is_public': False}) project = identity_fakes.FakeProject.create_one_project() @@ -596,8 +646,7 @@ class TestFlavorSet(TestFlavor): def setUp(self): super(TestFlavorSet, self).setUp() - self.flavors_mock.find.return_value = self.flavor - self.flavors_mock.get.side_effect = exceptions.NotFound(None) + self.sdk_client.find_flavor.return_value = self.flavor # Return a project self.projects_mock.get.return_value = self.project self.cmd = flavor.SetFlavor(self.app, None) @@ -614,9 +663,14 @@ def test_flavor_set_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, - is_public=None) - self.flavor.set_keys.assert_called_with({'FOO': '"B A R"'}) + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False + ) + self.sdk_client.create_flavor_extra_specs.assert_called_with( + self.flavor.id, + {'FOO': '"B A R"'}) self.assertIsNone(result) def test_flavor_set_no_property(self): @@ -631,9 +685,13 @@ def test_flavor_set_no_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, - is_public=None) - self.flavor.unset_keys.assert_called_with(['property']) + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False + ) + self.sdk_client.delete_flavor_extra_specs_property.assert_called_with( + self.flavor.id, 'property') self.assertIsNone(result) def test_flavor_set_project(self): @@ -649,13 +707,16 @@ def test_flavor_set_project(self): result = self.cmd.take_action(parsed_args) - self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, - is_public=None) - self.flavor_access_mock.add_tenant_access.assert_called_with( + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False + ) + self.sdk_client.flavor_add_tenant_access.assert_called_with( self.flavor.id, self.project.id, ) - self.flavor.set_keys.assert_not_called() + self.sdk_client.create_flavor_extra_specs.assert_not_called() self.assertIsNone(result) def test_flavor_set_no_project(self): @@ -681,8 +742,9 @@ def test_flavor_set_no_flavor(self): self.cmd, arglist, verifylist) def test_flavor_set_with_unexist_flavor(self): - self.flavors_mock.get.side_effect = exceptions.NotFound(None) - self.flavors_mock.find.side_effect = exceptions.NotFound(None) + self.sdk_client.find_flavor.side_effect = [ + sdk_exceptions.ResourceNotFound() + ] arglist = [ '--project', self.project.id, @@ -708,9 +770,12 @@ def test_flavor_set_nothing(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, - is_public=None) - self.flavor_access_mock.add_tenant_access.assert_not_called() + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False + ) + self.sdk_client.flavor_add_tenant_access.assert_not_called() self.assertIsNone(result) def test_flavor_set_description_api_newer(self): @@ -724,11 +789,11 @@ def test_flavor_set_description_api_newer(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.app.client_manager.compute.api_version = 2.55 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + with mock.patch.object(sdk_utils, + 'supports_microversion', + return_value=True): result = self.cmd.take_action(parsed_args) - self.flavors_mock.update.assert_called_with( + self.sdk_client.update_flavor.assert_called_with( flavor=self.flavor.id, description='description') self.assertIsNone(result) @@ -743,9 +808,9 @@ def test_flavor_set_description_api_older(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.app.client_manager.compute.api_version = 2.54 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + with mock.patch.object(sdk_utils, + 'supports_microversion', + return_value=False): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) @@ -760,11 +825,12 @@ def test_flavor_set_description_using_name_api_newer(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.app.client_manager.compute.api_version = 2.55 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + + with mock.patch.object(sdk_utils, + 'supports_microversion', + return_value=True): result = self.cmd.take_action(parsed_args) - self.flavors_mock.update.assert_called_with( + self.sdk_client.update_flavor.assert_called_with( flavor=self.flavor.id, description='description') self.assertIsNone(result) @@ -779,16 +845,17 @@ def test_flavor_set_description_using_name_api_older(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.app.client_manager.compute.api_version = 2.54 - with mock.patch.object(novaclient.api_versions, - 'APIVersion', - return_value=2.55): + + with mock.patch.object(sdk_utils, + 'supports_microversion', + return_value=False): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) class TestFlavorShow(TestFlavor): - # Return value of self.flavors_mock.find(). + # Return value of self.sdk_client.find_flavor(). flavor_access = compute_fakes.FakeFlavorAccess.create_one_flavor_access() flavor = compute_fakes.FakeFlavor.create_one_flavor() @@ -805,11 +872,11 @@ class TestFlavorShow(TestFlavor): 'ram', 'rxtx_factor', 'swap', - 'vcpus', + 'vcpus' ) data = ( - flavor.disabled, + flavor.is_disabled, flavor.ephemeral, None, flavor.description, @@ -817,7 +884,7 @@ class TestFlavorShow(TestFlavor): flavor.id, flavor.name, flavor.is_public, - format_columns.DictColumn(flavor.get_keys()), + format_columns.DictColumn(flavor.extra_specs), flavor.ram, flavor.rxtx_factor, flavor.swap, @@ -828,9 +895,8 @@ def setUp(self): super(TestFlavorShow, self).setUp() # Return value of _find_resource() - self.flavors_mock.find.return_value = self.flavor - self.flavors_mock.get.side_effect = exceptions.NotFound(None) - self.flavor_access_mock.list.return_value = [self.flavor_access] + self.sdk_client.find_flavor.return_value = self.flavor + self.sdk_client.get_flavor_access.return_value = [self.flavor_access] self.cmd = flavor.ShowFlavor(self.app, None) def test_show_no_options(self): @@ -862,7 +928,7 @@ def test_private_flavor_show(self): 'os-flavor-access:is_public': False, } ) - self.flavors_mock.find.return_value = private_flavor + self.sdk_client.find_flavor.return_value = private_flavor arglist = [ private_flavor.name, @@ -872,7 +938,7 @@ def test_private_flavor_show(self): ] data_with_project = ( - private_flavor.disabled, + private_flavor.is_disabled, private_flavor.ephemeral, [self.flavor_access.tenant_id], private_flavor.description, @@ -880,7 +946,7 @@ def test_private_flavor_show(self): private_flavor.id, private_flavor.name, private_flavor.is_public, - format_columns.DictColumn(private_flavor.get_keys()), + format_columns.DictColumn(private_flavor.extra_specs), private_flavor.ram, private_flavor.rxtx_factor, private_flavor.swap, @@ -891,7 +957,7 @@ def test_private_flavor_show(self): columns, data = self.cmd.take_action(parsed_args) - self.flavor_access_mock.list.assert_called_with( + self.sdk_client.get_flavor_access.assert_called_with( flavor=private_flavor.id) self.assertEqual(self.columns, columns) self.assertItemEqual(data_with_project, data) @@ -899,7 +965,7 @@ def test_private_flavor_show(self): class TestFlavorUnset(TestFlavor): - # Return value of self.flavors_mock.find(). + # Return value of self.sdk_client.find_flavor(). flavor = compute_fakes.FakeFlavor.create_one_flavor( attrs={'os-flavor-access:is_public': False}) project = identity_fakes.FakeProject.create_one_project() @@ -907,12 +973,13 @@ class TestFlavorUnset(TestFlavor): def setUp(self): super(TestFlavorUnset, self).setUp() - self.flavors_mock.find.return_value = self.flavor - self.flavors_mock.get.side_effect = exceptions.NotFound(None) + self.sdk_client.find_flavor.return_value = self.flavor # Return a project self.projects_mock.get.return_value = self.project self.cmd = flavor.UnsetFlavor(self.app, None) + self.mock_shortcut = self.sdk_client.delete_flavor_extra_specs_property + def test_flavor_unset_property(self): arglist = [ '--property', 'property', @@ -925,12 +992,49 @@ def test_flavor_unset_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, - is_public=None) - self.flavor.unset_keys.assert_called_with(['property']) - self.flavor_access_mock.remove_tenant_access.assert_not_called() + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False) + self.mock_shortcut.assert_called_with( + self.flavor.id, 'property') + self.sdk_client.flavor_remove_tenant_access.assert_not_called() self.assertIsNone(result) + def test_flavor_unset_properties(self): + arglist = [ + '--property', 'property1', + '--property', 'property2', + 'baremetal' + ] + verifylist = [ + ('property', ['property1', 'property2']), + ('flavor', 'baremetal'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, + get_extra_specs=True, + ignore_missing=False) + calls = [ + mock.call(self.flavor.id, 'property1'), + mock.call(self.flavor.id, 'property2') + ] + self.mock_shortcut.assert_has_calls( + calls) + + # A bit tricky way to ensure we do not unset other properties + calls.append(mock.call(self.flavor.id, 'property')) + self.assertRaises( + AssertionError, + self.mock_shortcut.assert_has_calls, + calls + ) + + self.sdk_client.flavor_remove_tenant_access.assert_not_called() + def test_flavor_unset_project(self): arglist = [ '--project', self.project.id, @@ -945,13 +1049,14 @@ def test_flavor_unset_project(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, - is_public=None) - self.flavor_access_mock.remove_tenant_access.assert_called_with( + self.sdk_client.find_flavor.assert_called_with( + parsed_args.flavor, get_extra_specs=True, + ignore_missing=False) + self.sdk_client.flavor_remove_tenant_access.assert_called_with( self.flavor.id, self.project.id, ) - self.flavor.unset_keys.assert_not_called() + self.sdk_client.delete_flavor_extra_specs_proerty.assert_not_called() self.assertIsNone(result) def test_flavor_unset_no_project(self): @@ -977,8 +1082,9 @@ def test_flavor_unset_no_flavor(self): self.cmd, arglist, verifylist) def test_flavor_unset_with_unexist_flavor(self): - self.flavors_mock.get.side_effect = exceptions.NotFound(None) - self.flavors_mock.find.side_effect = exceptions.NotFound(None) + self.sdk_client.find_flavor.side_effect = [ + sdk_exceptions.ResourceNotFound + ] arglist = [ '--project', self.project.id, @@ -1004,4 +1110,4 @@ def test_flavor_unset_nothing(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.flavor_access_mock.remove_tenant_access.assert_not_called() + self.sdk_client.flavor_remove_tenant_access.assert_not_called() diff --git a/releasenotes/notes/switch-flavor-to-sdk-b874a3c39559815e.yaml b/releasenotes/notes/switch-flavor-to-sdk-b874a3c39559815e.yaml new file mode 100644 index 0000000000..7863c32385 --- /dev/null +++ b/releasenotes/notes/switch-flavor-to-sdk-b874a3c39559815e.yaml @@ -0,0 +1,4 @@ +--- +features: + - Switch compute.flavor operations from direct API calls (novaclient) to + OpenStackSDK. diff --git a/requirements.txt b/requirements.txt index 9430a7fde0..b167628b51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 cliff>=3.4.0 # Apache-2.0 iso8601>=0.1.11 # MIT -openstacksdk>=0.51.0 # Apache-2.0 +openstacksdk>=0.52.0 # Apache-2.0 osc-lib>=2.2.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 From f36a34b675e69a811d5cd48f6bcfd6ce7bda6a5a Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 10 Nov 2020 13:56:11 +0100 Subject: [PATCH 0317/1120] Switch compute aggregate functions to SDK Continue journey towards having OSC consuming SDK for nova part. Depends-On: https://review.opendev.org/#/c/762131/ Change-Id: Id16e6c47aa93f02f15f49e1f59f73fecaa3e3b80 --- openstackclient/compute/v2/aggregate.py | 263 +++++++++--------- .../tests/unit/compute/v2/test_aggregate.py | 257 +++++++++++------ ...tch-aggregate-to-sdk-ced451a0f28bf6ea.yaml | 4 + setup.cfg | 1 + 4 files changed, 313 insertions(+), 212 deletions(-) create mode 100644 releasenotes/notes/switch-aggregate-to-sdk-ced451a0f28bf6ea.yaml diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 599659a3e2..8b70f42640 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -18,6 +18,7 @@ import logging +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command @@ -30,6 +31,25 @@ LOG = logging.getLogger(__name__) +_aggregate_formatters = { + 'Hosts': format_columns.ListColumn, + 'Metadata': format_columns.DictColumn, + 'hosts': format_columns.ListColumn, + 'metadata': format_columns.DictColumn, +} + + +def _get_aggregate_columns(item): + # To maintain backwards compatibility we need to rename sdk props to + # whatever OSC was using before + column_map = { + 'metadata': 'properties', + } + hidden_columns = ['links', 'location'] + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns) + + class AddAggregateHost(command.ShowOne): _description = _("Add host to aggregate") @@ -48,26 +68,18 @@ 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 - aggregate = utils.find_resource( - compute_client.aggregates, - parsed_args.aggregate, - ) - data = compute_client.aggregates.add_host(aggregate, parsed_args.host) - - info = {} - info.update(data._info) - - # Special mapping for columns to make the output easier to read: - # 'metadata' --> 'properties' - info.update( - { - 'hosts': format_columns.ListColumn(info.pop('hosts')), - 'properties': format_columns.DictColumn(info.pop('metadata')), - }, - ) - return zip(*sorted(info.items())) + aggregate = compute_client.find_aggregate( + parsed_args.aggregate, ignore_missing=False) + + aggregate = compute_client.add_host_to_aggregate( + aggregate.id, parsed_args.host) + + display_columns, columns = _get_aggregate_columns(aggregate) + data = utils.get_item_properties( + aggregate, columns, formatters=_aggregate_formatters) + return (display_columns, data) class CreateAggregate(command.ShowOne): @@ -95,36 +107,25 @@ 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 - info = {} - data = compute_client.aggregates.create( - parsed_args.name, - parsed_args.zone, - ) - info.update(data._info) + attrs = {'name': parsed_args.name} + + if parsed_args.zone: + attrs['availability_zone'] = parsed_args.zone + + aggregate = compute_client.create_aggregate(**attrs) if parsed_args.property: - info.update(compute_client.aggregates.set_metadata( - data, + aggregate = compute_client.set_aggregate_metadata( + aggregate.id, parsed_args.property, - )._info) - - # Special mapping for columns to make the output easier to read: - # 'metadata' --> 'properties' - hosts = None - properties = None - if 'hosts' in info.keys(): - hosts = format_columns.ListColumn(info.pop('hosts')) - if 'metadata' in info.keys(): - properties = format_columns.DictColumn(info.pop('metadata')) - info.update( - { - 'hosts': hosts, - 'properties': properties, - }, - ) - return zip(*sorted(info.items())) + ) + + display_columns, columns = _get_aggregate_columns(aggregate) + data = utils.get_item_properties( + aggregate, columns, formatters=_aggregate_formatters) + return (display_columns, data) class DeleteAggregate(command.Command): @@ -141,13 +142,14 @@ 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 result = 0 for a in parsed_args.aggregate: try: - data = utils.find_resource( - compute_client.aggregates, a) - compute_client.aggregates.delete(data.id) + aggregate = compute_client.find_aggregate( + a, ignore_missing=False) + compute_client.delete_aggregate( + aggregate.id, ignore_missing=False) except Exception as e: result += 1 LOG.error(_("Failed to delete aggregate with name or " @@ -175,15 +177,15 @@ 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 - data = compute_client.aggregates.list() + aggregates = list(compute_client.aggregates()) if parsed_args.long: # Remove availability_zone from metadata because Nova doesn't - for d in data: - if 'availability_zone' in d.metadata: - d.metadata.pop('availability_zone') + for aggregate in aggregates: + if 'availability_zone' in aggregate.metadata: + aggregate.metadata.pop('availability_zone') # This is the easiest way to change column headers column_headers = ( "ID", @@ -204,14 +206,11 @@ def take_action(self, parsed_args): "Availability Zone", ) - return (column_headers, - (utils.get_item_properties( - s, columns, - formatters={ - 'Hosts': format_columns.ListColumn, - 'Metadata': format_columns.DictColumn, - }, - ) for s in data)) + data = ( + utils.get_item_properties( + s, columns, formatters=_aggregate_formatters + ) for s in aggregates) + return (column_headers, data) class RemoveAggregateHost(command.ShowOne): @@ -232,29 +231,18 @@ 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 - aggregate = utils.find_resource( - compute_client.aggregates, - parsed_args.aggregate, - ) - data = compute_client.aggregates.remove_host( - aggregate, - parsed_args.host, - ) + aggregate = compute_client.find_aggregate( + parsed_args.aggregate, ignore_missing=False) - info = {} - info.update(data._info) + aggregate = compute_client.remove_host_from_aggregate( + aggregate.id, parsed_args.host) - # Special mapping for columns to make the output easier to read: - # 'metadata' --> 'properties' - info.update( - { - 'hosts': format_columns.ListColumn(info.pop('hosts')), - 'properties': format_columns.DictColumn(info.pop('metadata')), - }, - ) - return zip(*sorted(info.items())) + display_columns, columns = _get_aggregate_columns(aggregate) + data = utils.get_item_properties( + aggregate, columns, formatters=_aggregate_formatters) + return (display_columns, data) class SetAggregate(command.Command): @@ -296,11 +284,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - aggregate = utils.find_resource( - compute_client.aggregates, - parsed_args.aggregate, - ) + compute_client = self.app.client_manager.sdk_connection.compute + aggregate = compute_client.find_aggregate( + parsed_args.aggregate, ignore_missing=False) kwargs = {} if parsed_args.name: @@ -308,18 +294,12 @@ def take_action(self, parsed_args): if parsed_args.zone: kwargs['availability_zone'] = parsed_args.zone if kwargs: - compute_client.aggregates.update( - aggregate, - kwargs - ) + compute_client.update_aggregate(aggregate.id, **kwargs) set_property = {} if parsed_args.no_property: - # NOTE(RuiChen): "availability_zone" is removed from response of - # aggregate show and create commands, don't see it - # anywhere, so pop it, avoid the unexpected server - # exception(can't unset the availability zone from - # aggregate metadata in nova). + # NOTE(RuiChen): "availability_zone" can not be unset from + # properties. It is already excluded from show and create output. set_property.update({key: None for key in aggregate.metadata.keys() if key != 'availability_zone'}) @@ -327,8 +307,8 @@ def take_action(self, parsed_args): set_property.update(parsed_args.property) if set_property: - compute_client.aggregates.set_metadata( - aggregate, + compute_client.set_aggregate_metadata( + aggregate.id, set_property ) @@ -347,31 +327,18 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - data = utils.find_resource( - compute_client.aggregates, - parsed_args.aggregate, - ) + compute_client = self.app.client_manager.sdk_connection.compute + aggregate = compute_client.find_aggregate( + parsed_args.aggregate, ignore_missing=False) + # Remove availability_zone from metadata because Nova doesn't - if 'availability_zone' in data.metadata: - data.metadata.pop('availability_zone') - - # Special mapping for columns to make the output easier to read: - # 'metadata' --> 'properties' - data._info.update( - { - 'hosts': format_columns.ListColumn( - data._info.pop('hosts') - ), - 'properties': format_columns.DictColumn( - data._info.pop('metadata') - ), - }, - ) + if 'availability_zone' in aggregate.metadata: + aggregate.metadata.pop('availability_zone') - info = {} - info.update(data._info) - return zip(*sorted(info.items())) + display_columns, columns = _get_aggregate_columns(aggregate) + data = utils.get_item_properties( + aggregate, columns, formatters=_aggregate_formatters) + return (display_columns, data) class UnsetAggregate(command.Command): @@ -394,14 +361,56 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - aggregate = utils.find_resource( - compute_client.aggregates, - parsed_args.aggregate) + compute_client = self.app.client_manager.sdk_connection.compute + aggregate = compute_client.find_aggregate( + parsed_args.aggregate, ignore_missing=False) unset_property = {} if parsed_args.property: unset_property.update({key: None for key in parsed_args.property}) if unset_property: - compute_client.aggregates.set_metadata(aggregate, - unset_property) + compute_client.set_aggregate_metadata( + aggregate, unset_property) + + +class CacheImageForAggregate(command.Command): + _description = _("Request image caching for aggregate") + # NOTE(gtema): According to stephenfin and dansmith there is no and will + # not be anything to return. + + def get_parser(self, prog_name): + parser = super(CacheImageForAggregate, self).get_parser(prog_name) + parser.add_argument( + 'aggregate', + metavar='', + help=_("Aggregate (name or ID)") + ) + parser.add_argument( + 'image', + metavar='', + nargs='+', + help=_("Image ID to request caching for aggregate (name or ID). " + "May be specified multiple times.") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.sdk_connection.compute + + if not sdk_utils.supports_microversion(compute_client, '2.81'): + msg = _( + 'This operation requires server support for ' + 'API microversion 2.81' + ) + raise exceptions.CommandError(msg) + + aggregate = compute_client.find_aggregate( + parsed_args.aggregate, ignore_missing=False) + + images = [] + for img in parsed_args.image: + image = self.app.client_manager.sdk_connection.image.find_image( + img, ignore_missing=False) + images.append(image.id) + + compute_client.aggregate_precache_images(aggregate.id, images) diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index cd0c1525ee..e4c13ea55c 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -16,12 +16,14 @@ from unittest import mock from unittest.mock import call +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.compute.v2 import aggregate from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit.image.v2 import fakes as image_fakes class TestAggregate(compute_fakes.TestComputev2): @@ -48,8 +50,17 @@ def setUp(self): super(TestAggregate, self).setUp() # Get a shortcut to the AggregateManager Mock - self.aggregate_mock = self.app.client_manager.compute.aggregates - self.aggregate_mock.reset_mock() + self.app.client_manager.sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection.compute = mock.Mock() + self.sdk_client = self.app.client_manager.sdk_connection.compute + self.sdk_client.aggregates = mock.Mock() + self.sdk_client.find_aggregate = mock.Mock() + self.sdk_client.create_aggregate = mock.Mock() + self.sdk_client.update_aggregate = mock.Mock() + self.sdk_client.update_aggregate = mock.Mock() + self.sdk_client.set_aggregate_metadata = mock.Mock() + self.sdk_client.add_host_to_aggregate = mock.Mock() + self.sdk_client.remove_host_from_aggregate = mock.Mock() class TestAggregateAddHost(TestAggregate): @@ -57,8 +68,8 @@ class TestAggregateAddHost(TestAggregate): def setUp(self): super(TestAggregateAddHost, self).setUp() - self.aggregate_mock.get.return_value = self.fake_ag - self.aggregate_mock.add_host.return_value = self.fake_ag + self.sdk_client.find_aggregate.return_value = self.fake_ag + self.sdk_client.add_host_to_aggregate.return_value = self.fake_ag self.cmd = aggregate.AddAggregateHost(self.app, None) def test_aggregate_add_host(self): @@ -72,9 +83,10 @@ def test_aggregate_add_host(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.aggregate_mock.add_host.assert_called_once_with(self.fake_ag, - parsed_args.host) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.add_host_to_aggregate.assert_called_once_with( + self.fake_ag.id, parsed_args.host) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -84,8 +96,8 @@ class TestAggregateCreate(TestAggregate): def setUp(self): super(TestAggregateCreate, self).setUp() - self.aggregate_mock.create.return_value = self.fake_ag - self.aggregate_mock.set_metadata.return_value = self.fake_ag + self.sdk_client.create_aggregate.return_value = self.fake_ag + self.sdk_client.set_aggregate_metadata.return_value = self.fake_ag self.cmd = aggregate.CreateAggregate(self.app, None) def test_aggregate_create(self): @@ -97,8 +109,8 @@ def test_aggregate_create(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.aggregate_mock.create.assert_called_once_with(parsed_args.name, - None) + self.sdk_client.create_aggregate.assert_called_once_with( + name=parsed_args.name) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -114,8 +126,8 @@ def test_aggregate_create_with_zone(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.aggregate_mock.create.assert_called_once_with(parsed_args.name, - parsed_args.zone) + self.sdk_client.create_aggregate.assert_called_once_with( + name=parsed_args.name, availability_zone=parsed_args.zone) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -131,10 +143,10 @@ def test_aggregate_create_with_property(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.aggregate_mock.create.assert_called_once_with(parsed_args.name, - None) - self.aggregate_mock.set_metadata.assert_called_once_with( - self.fake_ag, parsed_args.property) + self.sdk_client.create_aggregate.assert_called_once_with( + name=parsed_args.name) + self.sdk_client.set_aggregate_metadata.assert_called_once_with( + self.fake_ag.id, parsed_args.property) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -146,7 +158,7 @@ class TestAggregateDelete(TestAggregate): def setUp(self): super(TestAggregateDelete, self).setUp() - self.aggregate_mock.get = ( + self.sdk_client.find_aggregate = ( compute_fakes.FakeAggregate.get_aggregates(self.fake_ags)) self.cmd = aggregate.DeleteAggregate(self.app, None) @@ -158,10 +170,11 @@ def test_aggregate_delete(self): ('aggregate', [self.fake_ags[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(self.fake_ags[0].id) - self.aggregate_mock.delete.assert_called_once_with(self.fake_ags[0].id) - self.assertIsNone(result) + self.cmd.take_action(parsed_args) + self.sdk_client.find_aggregate.assert_called_once_with( + self.fake_ags[0].id, ignore_missing=False) + self.sdk_client.delete_aggregate.assert_called_once_with( + self.fake_ags[0].id, ignore_missing=False) def test_delete_multiple_aggregates(self): arglist = [] @@ -172,13 +185,13 @@ def test_delete_multiple_aggregates(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) calls = [] for a in self.fake_ags: - calls.append(call(a.id)) - self.aggregate_mock.delete.assert_has_calls(calls) - self.assertIsNone(result) + calls.append(call(a.id, ignore_missing=False)) + self.sdk_client.find_aggregate.assert_has_calls(calls) + self.sdk_client.delete_aggregate.assert_has_calls(calls) def test_delete_multiple_agggregates_with_exception(self): arglist = [ @@ -191,23 +204,21 @@ def test_delete_multiple_agggregates_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.fake_ags[0], exceptions.CommandError] - with mock.patch.object(utils, 'find_resource', - side_effect=find_mock_result) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 aggregates failed to delete.', - str(e)) - - find_mock.assert_any_call(self.aggregate_mock, self.fake_ags[0].id) - find_mock.assert_any_call(self.aggregate_mock, 'unexist_aggregate') + self.sdk_client.find_aggregate.side_effect = [ + self.fake_ags[0], sdk_exceptions.NotFoundException] + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 aggregates failed to delete.', + str(e)) - self.assertEqual(2, find_mock.call_count) - self.aggregate_mock.delete.assert_called_once_with( - self.fake_ags[0].id - ) + calls = [] + for a in arglist: + calls.append(call(a, ignore_missing=False)) + self.sdk_client.find_aggregate.assert_has_calls(calls) + self.sdk_client.delete_aggregate.assert_called_with( + self.fake_ags[0].id, ignore_missing=False) class TestAggregateList(TestAggregate): @@ -245,7 +256,7 @@ class TestAggregateList(TestAggregate): def setUp(self): super(TestAggregateList, self).setUp() - self.aggregate_mock.list.return_value = [self.fake_ag] + self.sdk_client.aggregates.return_value = [self.fake_ag] self.cmd = aggregate.ListAggregate(self.app, None) def test_aggregate_list(self): @@ -275,11 +286,11 @@ class TestAggregateRemoveHost(TestAggregate): def setUp(self): super(TestAggregateRemoveHost, self).setUp() - self.aggregate_mock.get.return_value = self.fake_ag - self.aggregate_mock.remove_host.return_value = self.fake_ag + self.sdk_client.find_aggregate.return_value = self.fake_ag + self.sdk_client.remove_host_from_aggregate.return_value = self.fake_ag self.cmd = aggregate.RemoveAggregateHost(self.app, None) - def test_aggregate_add_host(self): + def test_aggregate_remove_host(self): arglist = [ 'ag1', 'host1', @@ -290,9 +301,10 @@ def test_aggregate_add_host(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.aggregate_mock.remove_host.assert_called_once_with( - self.fake_ag, parsed_args.host) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.remove_host_from_aggregate.assert_called_once_with( + self.fake_ag.id, parsed_args.host) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, data) @@ -302,7 +314,7 @@ class TestAggregateSet(TestAggregate): def setUp(self): super(TestAggregateSet, self).setUp() - self.aggregate_mock.get.return_value = self.fake_ag + self.sdk_client.find_aggregate.return_value = self.fake_ag self.cmd = aggregate.SetAggregate(self.app, None) def test_aggregate_set_no_option(self): @@ -315,9 +327,10 @@ def test_aggregate_set_no_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.assertNotCalled(self.aggregate_mock.update) - self.assertNotCalled(self.aggregate_mock.set_metadata) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.assertNotCalled(self.sdk_client.update_aggregate) + self.assertNotCalled(self.sdk_client.set_aggregate_metadata) self.assertIsNone(result) def test_aggregate_set_with_name(self): @@ -332,10 +345,11 @@ def test_aggregate_set_with_name(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.aggregate_mock.update.assert_called_once_with( - self.fake_ag, {'name': parsed_args.name}) - self.assertNotCalled(self.aggregate_mock.set_metadata) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.update_aggregate.assert_called_once_with( + self.fake_ag.id, name=parsed_args.name) + self.assertNotCalled(self.sdk_client.set_aggregate_metadata) self.assertIsNone(result) def test_aggregate_set_with_zone(self): @@ -350,10 +364,11 @@ def test_aggregate_set_with_zone(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.aggregate_mock.update.assert_called_once_with( - self.fake_ag, {'availability_zone': parsed_args.zone}) - self.assertNotCalled(self.aggregate_mock.set_metadata) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.update_aggregate.assert_called_once_with( + self.fake_ag.id, availability_zone=parsed_args.zone) + self.assertNotCalled(self.sdk_client.set_aggregate_metadata) self.assertIsNone(result) def test_aggregate_set_with_property(self): @@ -369,10 +384,11 @@ def test_aggregate_set_with_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.assertNotCalled(self.aggregate_mock.update) - self.aggregate_mock.set_metadata.assert_called_once_with( - self.fake_ag, parsed_args.property) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.assertNotCalled(self.sdk_client.update_aggregate) + self.sdk_client.set_aggregate_metadata.assert_called_once_with( + self.fake_ag.id, parsed_args.property) self.assertIsNone(result) def test_aggregate_set_with_no_property_and_property(self): @@ -388,10 +404,11 @@ def test_aggregate_set_with_no_property_and_property(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.assertNotCalled(self.aggregate_mock.update) - self.aggregate_mock.set_metadata.assert_called_once_with( - self.fake_ag, {'key1': None, 'key2': 'value2'}) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.assertNotCalled(self.sdk_client.update_aggregate) + self.sdk_client.set_aggregate_metadata.assert_called_once_with( + self.fake_ag.id, {'key1': None, 'key2': 'value2'}) self.assertIsNone(result) def test_aggregate_set_with_no_property(self): @@ -405,10 +422,11 @@ def test_aggregate_set_with_no_property(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.assertNotCalled(self.aggregate_mock.update) - self.aggregate_mock.set_metadata.assert_called_once_with( - self.fake_ag, {'key1': None}) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.assertNotCalled(self.sdk_client.update_aggregate) + self.sdk_client.set_aggregate_metadata.assert_called_once_with( + self.fake_ag.id, {'key1': None}) self.assertIsNone(result) def test_aggregate_set_with_zone_and_no_property(self): @@ -424,11 +442,12 @@ def test_aggregate_set_with_zone_and_no_property(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) - self.aggregate_mock.update.assert_called_once_with( - self.fake_ag, {'availability_zone': parsed_args.zone}) - self.aggregate_mock.set_metadata.assert_called_once_with( - self.fake_ag, {'key1': None}) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.update_aggregate.assert_called_once_with( + self.fake_ag.id, availability_zone=parsed_args.zone) + self.sdk_client.set_aggregate_metadata.assert_called_once_with( + self.fake_ag.id, {'key1': None}) self.assertIsNone(result) @@ -457,7 +476,7 @@ class TestAggregateShow(TestAggregate): def setUp(self): super(TestAggregateShow, self).setUp() - self.aggregate_mock.get.return_value = self.fake_ag + self.sdk_client.find_aggregate.return_value = self.fake_ag self.cmd = aggregate.ShowAggregate(self.app, None) def test_aggregate_show(self): @@ -469,7 +488,8 @@ def test_aggregate_show(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.aggregate_mock.get.assert_called_once_with(parsed_args.aggregate) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) self.assertEqual(self.columns, columns) self.assertItemEqual(self.data, tuple(data)) @@ -480,7 +500,7 @@ class TestAggregateUnset(TestAggregate): def setUp(self): super(TestAggregateUnset, self).setUp() - self.aggregate_mock.get.return_value = self.fake_ag + self.sdk_client.find_aggregate.return_value = self.fake_ag self.cmd = aggregate.UnsetAggregate(self.app, None) def test_aggregate_unset(self): @@ -495,7 +515,7 @@ def test_aggregate_unset(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.set_metadata.assert_called_once_with( + self.sdk_client.set_aggregate_metadata.assert_called_once_with( self.fake_ag, {'unset_key': None}) self.assertIsNone(result) @@ -512,7 +532,7 @@ def test_aggregate_unset_multiple_properties(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.aggregate_mock.set_metadata.assert_called_once_with( + self.sdk_client.set_aggregate_metadata.assert_called_once_with( self.fake_ag, {'unset_key1': None, 'unset_key2': None}) self.assertIsNone(result) @@ -526,5 +546,72 @@ def test_aggregate_unset_no_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.assertNotCalled(self.aggregate_mock.set_metadata) + self.assertNotCalled(self.sdk_client.set_aggregate_metadata) self.assertIsNone(result) + + +class TestAggregateCacheImage(TestAggregate): + + images = image_fakes.FakeImage.create_images(count=2) + + def setUp(self): + super(TestAggregateCacheImage, self).setUp() + + self.sdk_client.find_aggregate.return_value = self.fake_ag + self.find_image_mock = mock.Mock(side_effect=self.images) + self.app.client_manager.sdk_connection.image.find_image = \ + self.find_image_mock + + self.cmd = aggregate.CacheImageForAggregate(self.app, None) + + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_aggregate_not_supported(self, sm_mock): + arglist = [ + 'ag1', + 'im1' + ] + verifylist = [ + ('aggregate', 'ag1'), + ('image', ['im1']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args + ) + + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_aggregate_add_single_image(self, sm_mock): + arglist = [ + 'ag1', + 'im1' + ] + verifylist = [ + ('aggregate', 'ag1'), + ('image', ['im1']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.aggregate_precache_images.assert_called_once_with( + self.fake_ag.id, [self.images[0].id]) + + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_aggregate_add_multiple_images(self, sm_mock): + arglist = [ + 'ag1', + 'im1', + 'im2', + ] + verifylist = [ + ('aggregate', 'ag1'), + ('image', ['im1', 'im2']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.sdk_client.find_aggregate.assert_called_once_with( + parsed_args.aggregate, ignore_missing=False) + self.sdk_client.aggregate_precache_images.assert_called_once_with( + self.fake_ag.id, [self.images[0].id, self.images[1].id]) diff --git a/releasenotes/notes/switch-aggregate-to-sdk-ced451a0f28bf6ea.yaml b/releasenotes/notes/switch-aggregate-to-sdk-ced451a0f28bf6ea.yaml new file mode 100644 index 0000000000..0df2d63557 --- /dev/null +++ b/releasenotes/notes/switch-aggregate-to-sdk-ced451a0f28bf6ea.yaml @@ -0,0 +1,4 @@ +--- +features: + - Switch aggregate operations to use SDK + - Adds 'aggregate cache image' operation diff --git a/setup.cfg b/setup.cfg index a29852e353..9299fb918a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ openstack.compute.v2 = aggregate_set = openstackclient.compute.v2.aggregate:SetAggregate aggregate_show = openstackclient.compute.v2.aggregate:ShowAggregate aggregate_unset = openstackclient.compute.v2.aggregate:UnsetAggregate + aggregate_cache_image = openstackclient.compute.v2.aggregate:CacheImageForAggregate compute_service_delete = openstackclient.compute.v2.service:DeleteService compute_service_list = openstackclient.compute.v2.service:ListService From d688cb58a3a21ce5fbb5edf4e4feaae9998cb21c Mon Sep 17 00:00:00 2001 From: pedro Date: Mon, 4 Nov 2019 19:14:23 -0300 Subject: [PATCH 0318/1120] Add documentation about login with federation The documentation presents the parameters necessary to authenticate via federation (using password) and do a brief description of each parameter used in the process. Change-Id: Iae3b6d0b56ebd2bbbb94f9f3637b5086e75559a7 --- README.rst | 76 +++++++++++++++----- doc/source/cli/authentication.rst | 14 ++++ doc/source/cli/man/openstack.rst | 112 ++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 41d0112428..7dfabd8456 100644 --- a/README.rst +++ b/README.rst @@ -76,25 +76,63 @@ Configuration The CLI is configured via environment variables and command-line options as listed in https://docs.openstack.org/python-openstackclient/latest/cli/authentication.html. -Authentication using username/password is most commonly used:: - - export OS_AUTH_URL= - export OS_IDENTITY_API_VERSION=3 - export OS_PROJECT_NAME= - export OS_PROJECT_DOMAIN_NAME= - export OS_USERNAME= - export OS_USER_DOMAIN_NAME= - export OS_PASSWORD= # (optional) - -The corresponding command-line options look very similar:: - - --os-auth-url - --os-identity-api-version 3 - --os-project-name - --os-project-domain-name - --os-username - --os-user-domain-name - [--os-password ] +Authentication using username/password is most commonly used: + +- For a local user, your configuration will look like the one below:: + + export OS_AUTH_URL= + export OS_IDENTITY_API_VERSION=3 + export OS_PROJECT_NAME= + export OS_PROJECT_DOMAIN_NAME= + export OS_USERNAME= + export OS_USER_DOMAIN_NAME= + export OS_PASSWORD= # (optional) + + The corresponding command-line options look very similar:: + + --os-auth-url + --os-identity-api-version 3 + --os-project-name + --os-project-domain-name + --os-username + --os-user-domain-name + [--os-password ] + +- For a federated user, your configuration will look the so:: + + export OS_PROJECT_NAME= + export OS_PROJECT_DOMAIN_NAME= + export OS_AUTH_URL= + export OS_IDENTITY_API_VERSION=3 + export OS_AUTH_PLUGIN=openid + export OS_AUTH_TYPE=v3oidcpassword + export OS_USERNAME= + export OS_PASSWORD= + export OS_IDENTITY_PROVIDER= + export OS_CLIENT_ID= + export OS_CLIENT_SECRET= + export OS_OPENID_SCOPE= + export OS_PROTOCOL= + export OS_ACCESS_TOKEN_TYPE= + export OS_DISCOVERY_ENDPOINT= + + The corresponding command-line options look very similar:: + + --os-project-name + --os-project-domain-name + --os-auth-url + --os-identity-api-version 3 + --os-auth-plugin openid + --os-auth-type v3oidcpassword + --os-username + --os-password + --os-identity-provider + --os-client-id + --os-client-secret + --os-openid-scope + --os-protocol + --os-access-token-type + --os-discovery-endpoint If a password is not provided above (in plaintext), you will be interactively prompted to provide one securely. diff --git a/doc/source/cli/authentication.rst b/doc/source/cli/authentication.rst index 3b404bce5b..2e9148c356 100644 --- a/doc/source/cli/authentication.rst +++ b/doc/source/cli/authentication.rst @@ -133,3 +133,17 @@ Thus, a minimal set of environment variables would be: $ export OS_USERNAME=admin $ export OS_PASSWORD=secret $ export OS_PROJECT_NAME=admin + +Federated users support +----------------------- + +The OpenStackClient also allows the use of Federated users to log in. +It enables one to use the identity providers credentials such as Google or +Facebook to log in the OpenStackClient instead of using the Keystone +credentials. + +This is useful in a Federated environment where one credential give access +to many applications/services that the Federation supports. To check how to +configure the OpenStackClient to allow Federated users to log in, please check +the +:ref:`Authentication using federation. ` diff --git a/doc/source/cli/man/openstack.rst b/doc/source/cli/man/openstack.rst index 687e39eb9f..dc327a66e8 100644 --- a/doc/source/cli/man/openstack.rst +++ b/doc/source/cli/man/openstack.rst @@ -44,6 +44,7 @@ command line. The primary difference is the use of 'project' in the name of the * ``token``: Authentication with a token * ``password``: Authentication with a username and a password +* ``openid`` : Authentication using the protocol OpenID Connect 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. @@ -53,6 +54,31 @@ Additionally, it is possible to use Keystone's service token to authenticate, by .. NOTE:: To use the ``v3unscopedsaml`` method, the lxml package will need to be installed. +AUTHENTICATION USING FEDERATION +------------------------------- + +To use federated authentication, your configuration file needs the following: + +:: + + export OS_PROJECT_NAME= + export OS_PROJECT_DOMAIN_NAME= + export OS_AUTH_URL= + export OS_IDENTITY_API_VERSION=3 + export OS_AUTH_PLUGIN=openid + export OS_AUTH_TYPE=v3oidcpassword + export OS_USERNAME= + export OS_PASSWORD= + export OS_IDENTITY_PROVIDER= + export OS_CLIENT_ID= + export OS_CLIENT_SECRET= + export OS_OPENID_SCOPE= + export OS_PROTOCOL= + export OS_ACCESS_TOKEN_TYPE= + export OS_DISCOVERY_ENDPOINT= + export OS_ACCESS_TOKEN_ENDPOINT= + + OPTIONS ======= @@ -356,6 +382,24 @@ Show the detailed information for server ``appweb01``:: --os-auth-url http://localhost:5000:/v2.0 \ server show appweb01 +The same but using openid to authenticate in keystone:: + + openstack \ + --os-project-name ExampleCo \ + --os-auth-url http://localhost:5000:/v2.0 \ + --os-auth-plugin openid \ + --os-auth-type v3oidcpassword \ + --os-username demo-idp \ + --os-password secret-idp \ + --os-identity-provider google \ + --os-client-id the-id-assigned-to-keystone-in-google \ + --os-client-secret 3315162f-2b28-4809-9369-cb54730ac837 \ + --os-openid-scope 'openid email profile'\ + --os-protocol openid \ + --os-access-token-type access_token \ + --os-discovery-endpoint https://accounts.google.com/.well-known/openid-configuration \ + server show appweb01 + The same command if the auth environment variables (:envvar:`OS_AUTH_URL`, :envvar:`OS_PROJECT_NAME`, :envvar:`OS_USERNAME`, :envvar:`OS_PASSWORD`) are set:: @@ -404,6 +448,24 @@ The following environment variables can be set to alter the behaviour of :progra Authentication URL +.. envvar:: OS_AUTH_TYPE + + Define the authentication plugin that will be used to handle the + authentication process. One of the following: + + - ``v2password`` + - ``v2token`` + - ``v3password`` + - ``v3token`` + - ``v3oidcclientcredentials`` + - ``v3oidcpassword`` + - ``v3oidcauthorizationcode`` + - ``v3oidcaccesstoken`` + - ``v3totp`` + - ``v3tokenlessauth`` + - ``v3applicationcredential`` + - ``v3multifactor`` + .. envvar:: OS_URL Service URL (when using the service token) @@ -473,6 +535,56 @@ The following environment variables can be set to alter the behaviour of :progra Interface type. Valid options are `public`, `admin` and `internal`. +.. envvar:: OS_PROTOCOL + + Define the protocol that is used to execute the federated authentication + process. It is used in the Keystone authentication URL generation process. + +.. envvar:: OS_IDENTITY_PROVIDER + + Define the identity provider of your federation that will be used. It is + used by the Keystone authentication URL generation process. The available + Identity Providers can be listed using the + :program:`openstack identity provider list` command + +.. envvar:: OS_CLIENT_ID + + Configure the ``CLIENT_ID`` that the CLI will use to authenticate the + application (OpenStack) in the Identity Provider. This value is defined on + the identity provider side. Do not confuse with the user ID. + +.. envvar:: OS_CLIENT_SECRET + + Configure the OS_CLIENT_SECRET that the CLI will use to authenticate the + CLI (OpenStack secret in the identity provider). + +.. envvar:: OS_OPENID_SCOPE + + Configure the attribute scopes that will be claimed by the Service Provider + (SP), in this case OpenStack, from the identity provider. These scopes and + which attributes each scope contains are defined in the identity provider + side. This parameter can receive multiple values separated by space. + +.. envvar:: OS_ACCESS_TOKEN_TYPE + + Define the type of access token that is used in the token introspection + process. + This variable can assume only one of the states ("access_token" or + "id_token"). + +.. envvar:: OS_DISCOVERY_ENDPOINT + + Configure the identity provider's discovery URL. This URL will provide a + discover document that contains metadata describing the identity provider + endpoints. This variable is optional if the variable + ``OS_ACCESS_TOKEN_ENDPOINT`` is defined. + +.. envvar:: OS_ACCESS_TOKEN_ENDPOINT + + Overrides the value presented in the discovery document retrieved from + ``OS_DISCOVERY_ENDPOINT`` URL request. This variable is optional if the + ``OS_DISCOVERY_ENDPOINT`` is configured. + .. NOTE:: If you switch to openstackclient from project specified clients, like: novaclient, neutronclient and so on, please use `OS_INTERFACE` instead of From ceaba4c5729b3defe51466b6c5c4bb28696edecd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 13:57:44 +0000 Subject: [PATCH 0319/1120] trivial: Cleanup docs for 'server rebuild' Use consistent help strings and error messages. Change-Id: I42647a6b7e67ce4b8dd5f826e20802ade691c266 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 81 ++++++++++++++++++---------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 996c924b2e..4c177f7c3b 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2440,47 +2440,57 @@ def get_parser(self, prog_name): parser.add_argument( '--image', metavar='', - help=_('Recreate server from the specified image (name or ID).' - ' Defaults to the currently used one.'), + help=_( + 'Recreate server from the specified image (name or ID).' + 'Defaults to the currently used one.' + ), ) parser.add_argument( '--password', metavar='', - help=_("Set the password on the rebuilt instance"), + help=_('Set a password on the rebuilt server'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Set a property on the rebuilt instance ' - '(repeat option to set multiple values)'), + help=_( + 'Set a new property on the rebuilt server ' + '(repeat option to set multiple values)' + ), ) parser.add_argument( '--description', metavar='', - help=_('New description for the server (supported by ' - '--os-compute-api-version 2.19 or above'), - ) - parser.add_argument( - '--wait', - action='store_true', - help=_('Wait for rebuild to complete'), + help=_( + 'Set a new description on the rebuilt server ' + '(supported by --os-compute-api-version 2.19 or above)' + ), ) key_group = parser.add_mutually_exclusive_group() key_group.add_argument( '--key-name', metavar='', - help=_("Set the key name of key pair on the rebuilt instance." - " Cannot be specified with the '--key-unset' option." - " (Supported by API versions '2.54' - '2.latest')"), + help=_( + 'Set the key name of key pair on the rebuilt server. ' + 'Cannot be specified with the --key-unset option. ' + '(supported by --os-compute-api-version 2.54 or above)' + ), ) key_group.add_argument( '--key-unset', action='store_true', default=False, - help=_("Unset the key name of key pair on the rebuilt instance." - " Cannot be specified with the '--key-name' option." - " (Supported by API versions '2.54' - '2.latest')"), + help=_( + 'Unset the key name of key pair on the rebuilt server. ' + 'Cannot be specified with the --key-name option. ' + '(supported by --os-compute-api-version 2.54 or above)' + ), + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait for rebuild to complete'), ) return parser @@ -2506,24 +2516,38 @@ def _show_progress(progress): image = image_client.get_image(image_id) kwargs = {} + if parsed_args.property: kwargs['meta'] = parsed_args.property + if parsed_args.description: if server.api_version < api_versions.APIVersion("2.19"): - msg = _("Description is not supported for " - "--os-compute-api-version less than 2.19") + msg = _( + '--os-compute-api-version 2.19 or greater is required to ' + 'support the --description option' + ) raise exceptions.CommandError(msg) + kwargs['description'] = parsed_args.description - if parsed_args.key_name or parsed_args.key_unset: + if parsed_args.key_name: + if compute_client.api_version < api_versions.APIVersion('2.54'): + msg = _( + '--os-compute-api-version 2.54 or greater is required to ' + 'support the --key-name option' + ) + raise exceptions.CommandError(msg) + + kwargs['key_name'] = parsed_args.key_name + elif parsed_args.key_unset: if compute_client.api_version < api_versions.APIVersion('2.54'): - msg = _('--os-compute-api-version 2.54 or later is required') + msg = _( + '--os-compute-api-version 2.54 or greater is required to ' + 'support the --no-key-name option' + ) raise exceptions.CommandError(msg) - if parsed_args.key_unset: kwargs['key_name'] = None - if parsed_args.key_name: - kwargs['key_name'] = parsed_args.key_name server = server.rebuild(image, parsed_args.password, **kwargs) if parsed_args.wait: @@ -2534,13 +2558,12 @@ def _show_progress(progress): ): self.app.stdout.write(_('Complete\n')) else: - LOG.error(_('Error rebuilding server: %s'), - server.id) + LOG.error(_('Error rebuilding server: %s'), server.id) self.app.stdout.write(_('Error rebuilding server\n')) raise SystemExit - details = _prep_server_detail(compute_client, image_client, server, - refresh=False) + details = _prep_server_detail( + compute_client, image_client, server, refresh=False) return zip(*sorted(details.items())) From 30d5f14a700dd53d80e0fbedefd9b1ad337390f9 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 Dec 2020 15:40:24 +0100 Subject: [PATCH 0320/1120] Add support for token caching SDK starts caching token in keyring (when available and configured). A small change is required in OSC not to reject this state. Overall this helps avoiding reauthentication upon next openstack call. If token is not valid anymore automatically reauthentication is done. Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/735352 Depends-On: https://review.opendev.org/c/openstack/osc-lib/+/765650 Change-Id: I47261a32bd3b106a589974d3de5bf2a6ebd57263 --- openstackclient/common/clientmanager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 36c3ce2688..1ed6aa2400 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -83,10 +83,12 @@ def setup_auth(self): self._cli_options._openstack_config._pw_callback = \ shell.prompt_for_password try: - self._cli_options._auth = \ - self._cli_options._openstack_config.load_auth_plugin( - self._cli_options.config, - ) + # We might already get auth from SDK caching + if not self._cli_options._auth: + self._cli_options._auth = \ + self._cli_options._openstack_config.load_auth_plugin( + self._cli_options.config, + ) except TypeError as e: self._fallback_load_auth_plugin(e) From 20769cd7b27d51da84a324a17922427eba5c6eac Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 8 Dec 2020 10:20:19 +0000 Subject: [PATCH 0321/1120] Fix lower-constraints job pip 20.3 finally includes a proper dependency resolver. Its use is causing the following error messages on the lower-constraints job: ERROR: Cannot install ... because these package versions have conflicting dependencies. The conflict is caused by: bandit 1.1.0 depends on PyYAML>=3.1.0 cliff 3.4.0 depends on PyYAML>=3.12 openstacksdk 0.52.0 depends on PyYAML>=3.13 Bump our lower constraint for PyYAML to resolve this issue. With that resolved, we see a new issue: ERROR: Could not find a version that satisfies the requirement cryptography>=2.7 (from openstacksdk) ERROR: No matching distribution found for cryptography>=2.7 This is less self-explanatory but looking at the lower-constraints for openstacksdk 0.52.0 shows a dependency on cryptography 2.7 [1], meaning we need to bump this also. Next up, flake8-import-order seems to cause the dependency resolver to go nuts, eventually ending with the following error message in a Python 3.6 environment: Using cached enum34-1.1.2.zip (49 kB) ERROR: Command errored out with exit status 1: command: ... cwd: ... Complete output (9 lines): Traceback (most recent call last): File "", line 1, in File ".../lib/python3.6/site-packages/setuptools/__init__.py", line 7, in import setuptools.distutils_patch # noqa: F401 File ".../lib/python3.6/site-packages/setuptools/distutils_patch.py", line 9, in import re File "/usr/lib64/python3.6/re.py", line 142, in class RegexFlag(enum.IntFlag): AttributeError: module 'enum' has no attribute 'IntFlag' ---------------------------------------- A quick Google suggests this is because the enum34 package is not complete [2]. We shouldn't even be using it since our base virtualenv should at least use Python 3.6, but I guess some dependency doesn't properly restrict the dependency to <= Python 3.4. This is moved from 'test-requirements.txt' to 'tox.ini' since we don't need to use our constraints machinery for linters. Finally, the versions of bandit and hacking that pip is bringing in both requires in a newer version of babel, which in turn requires a new version of pytz. Collecting hacking>=2.0.0 ... ERROR: Cannot install oslo.i18n because these package versions have conflicting dependencies. The conflict is caused by: babel 2.9.0 depends on pytz>=2015.7 babel 2.8.1 depends on pytz>=2015.7 babel 2.8.0 depends on pytz>=2015.7 babel 2.7.0 depends on pytz>=2015.7 Seeing as we shouldn't be tracking bandit in lower-constraints, I'm not sure why we're want to bump these dependencies for just that. As above, we move these dependencies out of 'test-requirements' and into 'tox.ini' since we can do that for linters. [1] https://opendev.org/openstack/openstacksdk/src/tag/0.52.0/requirements.txt#L19 [2] https://github.com/iterative/dvc/issues/1995#issuecomment-491889669 Change-Id: I8ec738fbcabc8d8553db79a876e5592576cd18fa Signed-off-by: Stephen Finucane --- lower-constraints.txt | 4 ++-- test-requirements.txt | 3 --- tox.ini | 8 ++++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index e08b05d22c..cfb89d0765 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -9,7 +9,7 @@ cliff==3.4.0 cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 -cryptography==2.1 +cryptography==2.7 ddt==1.0.1 debtcollector==1.2.0 decorator==4.4.1 @@ -105,7 +105,7 @@ python-watcherclient==2.5.0 python-zaqarclient==1.0.0 python-zunclient==3.6.0 pytz==2013.6 -PyYAML==3.12 +PyYAML==3.13 repoze.lru==0.7 requests-mock==1.2.0 requests==2.14.2 diff --git a/test-requirements.txt b/test-requirements.txt index 3dce687bc7..8b61a5c077 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,10 +1,8 @@ # 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. -hacking>=2.0.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -flake8-import-order>=0.13 # LGPLv3 oslotest>=3.2.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 @@ -12,6 +10,5 @@ stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT tempest>=17.1.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 -bandit!=1.6.0,>=1.1.0 # Apache-2.0 wrapt>=1.7.0 # BSD License ddt>=1.0.1 # MIT diff --git a/tox.ini b/tox.ini index 663463e2f2..ebcdc2d5d9 100644 --- a/tox.ini +++ b/tox.ini @@ -28,9 +28,13 @@ commands = {toxinidir}/tools/fast8.sh [testenv:pep8] +deps = + hacking>=2.0.0 + bandit!=1.6.0,>=1.1.0 + flake8-import-order>=0.13 # LGPLv3 commands = - flake8 - bandit -r openstackclient -x tests -s B105,B106,B107,B401,B404,B603,B606,B607,B110,B605,B101 + flake8 + bandit -r openstackclient -x tests -s B105,B106,B107,B401,B404,B603,B606,B607,B110,B605,B101 [testenv:bandit] # This command runs the bandit security linter against the openstackclient From ecfda7654e47b8be419929cd742e2de6b29ed1cb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 8 Dec 2020 11:19:22 +0000 Subject: [PATCH 0322/1120] Update lower-constraints This had gotten pretty out-of-date and included a whole load of OSC plugins which I don't think we need to track from here. This updated version is simply generated via 'pip freeze' using pip >= 20.3, which includes the new dependency resolver. Change-Id: I4fb0b69dbd538f313c6fef97126c22078904c69f Signed-off-by: Stephen Finucane --- lower-constraints.txt | 57 ++++++++----------------------------------- 1 file changed, 10 insertions(+), 47 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index cfb89d0765..040b976705 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,21 +1,17 @@ amqp==2.1.1 -aodhclient==0.9.0 appdirs==1.3.0 asn1crypto==0.23.0 -bandit==1.1.0 +Babel==2.6.0 +bcrypt==3.2.0 cachetools==2.0.0 cffi==1.14.0 cliff==3.4.0 cmd2==0.8.0 -contextlib2==0.4.0 coverage==4.0 cryptography==2.7 ddt==1.0.1 debtcollector==1.2.0 decorator==4.4.1 -deprecation==1.0 -docker-pycreds==0.2.1 -docker==2.4.2 dogpile.cache==0.6.5 eventlet==0.18.2 extras==1.0.0 @@ -23,12 +19,8 @@ fasteners==0.7.0 fixtures==3.0.0 future==0.16.0 futurist==2.1.0 -gitdb==0.6.4 -GitPython==1.0.1 -gnocchiclient==3.3.1 greenlet==0.4.15 -httplib2==0.9.1 -idna==2.6 +importlib-metadata==3.1.1 iso8601==0.1.11 Jinja2==2.10 jmespath==0.9.0 @@ -39,6 +31,7 @@ keystoneauth1==3.18.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.1 +mock==4.0.2 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 @@ -46,10 +39,10 @@ munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 openstacksdk==0.52.0 +os-client-config==2.1.0 os-service-types==1.7.0 os-testr==1.0.0 osc-lib==2.2.0 -osc-placement==1.7.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.19.2 @@ -66,71 +59,41 @@ paramiko==2.7.1 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 -pika-pool==0.1.3 pika==0.10.0 -ply==3.10 -positional==1.2.1 +pika-pool==0.1.3 prettytable==0.7.2 -pyasn1==0.1.8 -pycodestyle==2.0.0 pycparser==2.18 pyinotify==0.9.6 -pyOpenSSL==17.1.0 +PyNaCl==1.4.0 pyparsing==2.1.0 pyperclip==1.5.27 -python-barbicanclient==4.5.2 python-cinderclient==3.3.0 python-dateutil==2.5.3 -python-designateclient==2.7.0 -python-glanceclient==2.8.0 -python-heatclient==1.10.0 -python-ironic-inspector-client==1.5.0 -python-ironicclient==2.3.0 -python-karborclient==0.6.0 python-keystoneclient==3.22.0 python-mimeparse==1.6.0 -python-mistralclient==3.1.0 -python-muranoclient==0.8.2 -python-neutronclient==6.7.0 python-novaclient==15.1.0 -python-octaviaclient==1.11.0 -python-rsdclient==1.0.1 -python-saharaclient==1.4.0 -python-searchlightclient==1.0.0 -python-senlinclient==1.1.0 python-subunit==1.0.0 -python-swiftclient==3.2.0 -python-troveclient==3.1.0 -python-watcherclient==2.5.0 -python-zaqarclient==1.0.0 -python-zunclient==3.6.0 pytz==2013.6 PyYAML==3.13 repoze.lru==0.7 -requests-mock==1.2.0 requests==2.14.2 +requests-mock==1.2.0 requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 -rsd-lib==0.1.0 simplejson==3.5.1 -smmap==0.9.0 +six==1.15.0 statsd==3.2.1 stestr==1.0.0 stevedore==2.0.1 -sushy==0.1.0 tempest==17.1.0 tenacity==3.2.1 testrepository==0.0.18 testtools==2.2.0 traceback2==1.4.0 -ujson==1.35 unittest2==1.1.0 urllib3==1.21.1 -validictory==1.1.1 vine==1.1.4 -warlock==1.2.0 WebOb==1.7.1 -websocket-client==0.44.0 wrapt==1.7.0 -yaql==1.1.3 +zipp==3.4.0 From a79e7db4aeb6990912497eb0fd313ae3e60adafb Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Sat, 28 Nov 2020 16:15:28 -0600 Subject: [PATCH 0323/1120] Remove retired Searchlight support Searchlight project is retiring in Wallaby cycle[1]. This commit removes the support/usage of Searchlight project before its code is removed. Needed-By: https://review.opendev.org/c/openstack/searchlight/+/764526 [1] http://lists.openstack.org/pipermail/openstack-discuss/2020-November/018637.html Change-Id: Idad97343b9ce66186d50ee0560a2fded66655f9b --- doc/requirements.txt | 1 - doc/source/cli/plugin-commands/index.rst | 1 - doc/source/cli/plugin-commands/searchlight.rst | 4 ---- doc/source/contributor/plugins.rst | 1 - 4 files changed, 7 deletions(-) delete mode 100644 doc/source/cli/plugin-commands/searchlight.rst diff --git a/doc/requirements.txt b/doc/requirements.txt index 962b0e2607..28030ca422 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -26,7 +26,6 @@ python-neutronclient>=6.7.0 # Apache-2.0 python-octaviaclient>=1.11.0 # Apache-2.0 python-rsdclient>=1.0.1 # Apache-2.0 python-saharaclient>=1.4.0 # Apache-2.0 -python-searchlightclient>=1.0.0 #Apache-2.0 python-senlinclient>=1.1.0 # Apache-2.0 python-troveclient>=3.1.0 # Apache-2.0 python-watcherclient>=2.5.0 # Apache-2.0 diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst index 33a8fe064f..8c9c5c13e3 100644 --- a/doc/source/cli/plugin-commands/index.rst +++ b/doc/source/cli/plugin-commands/index.rst @@ -22,7 +22,6 @@ Plugin Commands placement rsd sahara - searchlight senlin trove watcher diff --git a/doc/source/cli/plugin-commands/searchlight.rst b/doc/source/cli/plugin-commands/searchlight.rst deleted file mode 100644 index be934aeb36..0000000000 --- a/doc/source/cli/plugin-commands/searchlight.rst +++ /dev/null @@ -1,4 +0,0 @@ -searchlight ------------ - -.. autoprogram-cliff:: openstack.search.v1 diff --git a/doc/source/contributor/plugins.rst b/doc/source/contributor/plugins.rst index 7ea48edd38..fe2cc14ad0 100644 --- a/doc/source/contributor/plugins.rst +++ b/doc/source/contributor/plugins.rst @@ -37,7 +37,6 @@ The following is a list of projects that are an OpenStackClient plugin. - python-octaviaclient - python-rsdclient - python-saharaclient -- python-searchlightclient - python-senlinclient - python-tripleoclient\*\* - python-troveclient From f5b185c35728025ebfd4145c800648b34476b775 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Nov 2020 16:16:58 +0000 Subject: [PATCH 0324/1120] Make use of comparable 'FormattableColumn' subclasses This requires fixes found in cliff 3.5.0 [1] and osc-lib 2.3.0 [2]. With these fixes in place, we can remove the icky, still broken 'assertItemEqual' and 'assertListItemEqual' helpers. [1] https://review.opendev.org/761421 [2] https://review.opendev.org/761394 Change-Id: Id6c26b37c3c7d5ec6761361abca57f9219b76838 Signed-off-by: Stephen Finucane --- lower-constraints.txt | 4 +- openstackclient/identity/v2_0/catalog.py | 2 +- .../tests/unit/compute/v2/test_aggregate.py | 16 ++--- .../tests/unit/compute/v2/test_flavor.py | 16 ++--- .../unit/compute/v2/test_server_backup.py | 6 +- .../unit/compute/v2/test_server_image.py | 6 +- .../tests/unit/identity/v2_0/test_catalog.py | 17 +++-- .../tests/unit/identity/v2_0/test_project.py | 2 +- .../tests/unit/identity/v2_0/test_user.py | 8 +-- .../tests/unit/identity/v3/test_catalog.py | 10 +-- .../identity/v3/test_identity_provider.py | 24 +++---- .../tests/unit/image/v1/test_image.py | 10 +-- .../tests/unit/image/v2/test_image.py | 22 +++---- .../unit/network/v2/test_ip_availability.py | 8 +-- .../tests/unit/network/v2/test_network.py | 46 ++++++------- .../unit/network/v2/test_network_agent.py | 14 ++-- .../tests/unit/network/v2/test_port.py | 64 +++++++++---------- .../tests/unit/network/v2/test_router.py | 32 +++++----- .../network/v2/test_security_group_compute.py | 10 +-- .../network/v2/test_security_group_network.py | 16 ++--- .../tests/unit/network/v2/test_subnet.py | 42 ++++++------ .../tests/unit/network/v2/test_subnet_pool.py | 40 ++++++------ openstackclient/tests/unit/utils.py | 16 ----- .../tests/unit/volume/v1/test_qos_specs.py | 12 ++-- .../tests/unit/volume/v1/test_type.py | 14 ++-- .../tests/unit/volume/v1/test_volume.py | 36 +++++------ .../unit/volume/v1/test_volume_backup.py | 10 +-- .../unit/volume/v2/test_consistency_group.py | 14 ++-- .../tests/unit/volume/v2/test_qos_specs.py | 12 ++-- .../tests/unit/volume/v2/test_type.py | 24 +++---- .../tests/unit/volume/v2/test_volume.py | 40 ++++++------ .../unit/volume/v2/test_volume_backup.py | 4 +- requirements.txt | 4 +- 33 files changed, 295 insertions(+), 306 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 040b976705..287f7d4599 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -5,7 +5,7 @@ Babel==2.6.0 bcrypt==3.2.0 cachetools==2.0.0 cffi==1.14.0 -cliff==3.4.0 +cliff==3.5.0 cmd2==0.8.0 coverage==4.0 cryptography==2.7 @@ -42,7 +42,7 @@ openstacksdk==0.52.0 os-client-config==2.1.0 os-service-types==1.7.0 os-testr==1.0.0 -osc-lib==2.2.0 +osc-lib==2.3.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.19.2 diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index ccedbf3312..05d0e9aebc 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -91,7 +91,7 @@ def take_action(self, parsed_args): for service in auth_ref.service_catalog.catalog: if (service.get('name') == parsed_args.service or service.get('type') == parsed_args.service): - data = service + data = service.copy() data['endpoints'] = EndpointsColumn(data['endpoints']) if 'endpoints_links' in data: data.pop('endpoints_links') diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index e4c13ea55c..e12edd0fce 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -88,7 +88,7 @@ def test_aggregate_add_host(self): self.sdk_client.add_host_to_aggregate.assert_called_once_with( self.fake_ag.id, parsed_args.host) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestAggregateCreate(TestAggregate): @@ -112,7 +112,7 @@ def test_aggregate_create(self): self.sdk_client.create_aggregate.assert_called_once_with( name=parsed_args.name) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_aggregate_create_with_zone(self): arglist = [ @@ -129,7 +129,7 @@ def test_aggregate_create_with_zone(self): self.sdk_client.create_aggregate.assert_called_once_with( name=parsed_args.name, availability_zone=parsed_args.zone) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_aggregate_create_with_property(self): arglist = [ @@ -148,7 +148,7 @@ def test_aggregate_create_with_property(self): self.sdk_client.set_aggregate_metadata.assert_called_once_with( self.fake_ag.id, parsed_args.property) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestAggregateDelete(TestAggregate): @@ -265,7 +265,7 @@ def test_aggregate_list(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.list_columns, columns) - self.assertItemEqual(self.list_data, tuple(data)) + self.assertItemsEqual(self.list_data, tuple(data)) def test_aggregate_list_with_long(self): arglist = [ @@ -278,7 +278,7 @@ def test_aggregate_list_with_long(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.list_columns_long, columns) - self.assertListItemEqual(self.list_data_long, tuple(data)) + self.assertItemsEqual(self.list_data_long, tuple(data)) class TestAggregateRemoveHost(TestAggregate): @@ -306,7 +306,7 @@ def test_aggregate_remove_host(self): self.sdk_client.remove_host_from_aggregate.assert_called_once_with( self.fake_ag.id, parsed_args.host) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestAggregateSet(TestAggregate): @@ -492,7 +492,7 @@ def test_aggregate_show(self): parsed_args.aggregate, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, tuple(data)) + self.assertItemsEqual(self.data, tuple(data)) class TestAggregateUnset(TestAggregate): diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 8625b71202..4c4882ecbd 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -133,7 +133,7 @@ def test_flavor_create_default_options(self): self.sdk_client.create_flavor.assert_called_once_with(**default_args) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_flavor_create_all_options(self): @@ -201,8 +201,8 @@ def test_flavor_create_all_options(self): create_flavor, props) self.sdk_client.get_flavor_access.assert_not_called() - self.assertEqual(self.columns, columns) - self.assertItemEqual(tuple(cmp_data), data) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(tuple(cmp_data), data) def test_flavor_create_other_options(self): @@ -277,7 +277,7 @@ def test_flavor_create_other_options(self): self.sdk_client.create_flavor_extra_specs.assert_called_with( create_flavor, props) self.assertEqual(self.columns, columns) - self.assertItemEqual(cmp_data, data) + self.assertItemsEqual(cmp_data, data) def test_public_flavor_create_with_project(self): arglist = [ @@ -350,7 +350,7 @@ def test_flavor_create_with_description_api_newer(self): self.sdk_client.create_flavor.assert_called_once_with(**args) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data_private, data) + self.assertItemsEqual(self.data_private, data) def test_flavor_create_with_description_api_older(self): arglist = [ @@ -633,7 +633,7 @@ def test_flavor_list_long(self): ) self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, tuple(data)) + self.assertItemsEqual(self.data_long, tuple(data)) class TestFlavorSet(TestFlavor): @@ -920,7 +920,7 @@ def test_public_flavor_show(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_private_flavor_show(self): private_flavor = compute_fakes.FakeFlavor.create_one_flavor( @@ -960,7 +960,7 @@ def test_private_flavor_show(self): self.sdk_client.get_flavor_access.assert_called_with( flavor=private_flavor.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(data_with_project, data) + self.assertItemsEqual(data_with_project, data) class TestFlavorUnset(TestFlavor): diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 5cdc20800a..753db9cd70 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -139,7 +139,7 @@ def test_server_backup_defaults(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemEqual(self.image_data(images[0]), data) + self.assertItemsEqual(self.image_data(images[0]), data) def test_server_backup_create_options(self): servers = self.setup_servers_mock(count=1) @@ -173,7 +173,7 @@ def test_server_backup_create_options(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemEqual(self.image_data(images[0]), data) + self.assertItemsEqual(self.image_data(images[0]), data) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_backup_wait_fail(self, mock_wait_for_status): @@ -269,4 +269,4 @@ def test_server_backup_wait_ok(self, mock_wait_for_status): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemEqual(self.image_data(images[0]), data) + self.assertItemsEqual(self.image_data(images[0]), data) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 1cec5b685f..06f6017c11 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -133,7 +133,7 @@ def test_server_image_create_defaults(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemEqual(self.image_data(images[0]), data) + self.assertItemsEqual(self.image_data(images[0]), data) def test_server_image_create_options(self): servers = self.setup_servers_mock(count=1) @@ -161,7 +161,7 @@ def test_server_image_create_options(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemEqual(self.image_data(images[0]), data) + self.assertItemsEqual(self.image_data(images[0]), data) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_create_image_wait_fail(self, mock_wait_for_status): @@ -229,4 +229,4 @@ def test_server_create_image_wait_ok(self, mock_wait_for_status): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemEqual(self.image_data(images[0]), data) + self.assertItemsEqual(self.image_data(images[0]), data) diff --git a/openstackclient/tests/unit/identity/v2_0/test_catalog.py b/openstackclient/tests/unit/identity/v2_0/test_catalog.py index 1735507469..e2c56ba1ec 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_catalog.py +++ b/openstackclient/tests/unit/identity/v2_0/test_catalog.py @@ -71,9 +71,10 @@ def test_catalog_list(self): datalist = (( 'supernova', 'compute', - catalog.EndpointsColumn(self.service_catalog['endpoints']), + catalog.EndpointsColumn( + auth_ref.service_catalog.catalog[0]['endpoints']), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_catalog_list_with_endpoint_url(self): attr = { @@ -113,9 +114,10 @@ def test_catalog_list_with_endpoint_url(self): datalist = (( 'supernova', 'compute', - catalog.EndpointsColumn(service_catalog['endpoints']), + catalog.EndpointsColumn( + auth_ref.service_catalog.catalog[0]['endpoints']), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) class TestCatalogShow(TestCatalog): @@ -150,16 +152,17 @@ def test_catalog_show(self): collist = ('endpoints', 'id', 'name', 'type') self.assertEqual(collist, columns) datalist = ( - catalog.EndpointsColumn(self.service_catalog['endpoints']), + catalog.EndpointsColumn( + auth_ref.service_catalog.catalog[0]['endpoints']), self.service_catalog.id, 'supernova', 'compute', ) - self.assertItemEqual(datalist, data) + self.assertItemsEqual(datalist, data) class TestFormatColumns(TestCatalog): - def test_endpoints_column_human_readabale(self): + def test_endpoints_column_human_readable(self): col = catalog.EndpointsColumn(self.service_catalog['endpoints']) self.assertEqual( 'one\n publicURL: https://public.one.example.com\n ' diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index cd8c825d15..766d5dab55 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -643,7 +643,7 @@ def test_project_show(self): self.fake_proj_show.name, format_columns.DictColumn({}), ) - self.assertItemEqual(datalist, data) + self.assertItemsEqual(datalist, data) class TestProjectUnset(TestProject): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index 4308b05d05..dd30047814 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -482,7 +482,7 @@ def test_user_list_no_options(self): self.users_mock.list.assert_called_with(tenant_id=None) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_user_list_project(self): arglist = [ @@ -502,7 +502,7 @@ def test_user_list_project(self): self.users_mock.list.assert_called_with(tenant_id=project_id) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_user_list_long(self): arglist = [ @@ -531,7 +531,7 @@ def test_user_list_long(self): self.fake_user_l.email, True, ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) class TestUserSet(TestUser): @@ -819,4 +819,4 @@ def test_user_show(self): self.fake_user.name, self.fake_project.id, ) - self.assertItemEqual(datalist, data) + self.assertItemsEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_catalog.py b/openstackclient/tests/unit/identity/v3/test_catalog.py index 3630ccb6a3..97ce48f6a5 100644 --- a/openstackclient/tests/unit/identity/v3/test_catalog.py +++ b/openstackclient/tests/unit/identity/v3/test_catalog.py @@ -91,9 +91,10 @@ def test_catalog_list(self): datalist = (( 'supernova', 'compute', - catalog.EndpointsColumn(self.fake_service['endpoints']), + catalog.EndpointsColumn( + auth_ref.service_catalog.catalog[0]['endpoints']), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) class TestCatalogShow(TestCatalog): @@ -128,12 +129,13 @@ def test_catalog_show(self): collist = ('endpoints', 'id', 'name', 'type') self.assertEqual(collist, columns) datalist = ( - catalog.EndpointsColumn(self.fake_service['endpoints']), + catalog.EndpointsColumn( + auth_ref.service_catalog.catalog[0]['endpoints']), 'qwertyuiop', 'supernova', 'compute', ) - self.assertItemEqual(datalist, data) + self.assertItemsEqual(datalist, data) class TestFormatColumns(TestCatalog): diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index 39a37db24a..5aff2b1be2 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -89,7 +89,7 @@ def test_create_identity_provider_no_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_create_identity_provider_description(self): arglist = [ @@ -117,7 +117,7 @@ def test_create_identity_provider_description(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_create_identity_provider_remote_id(self): arglist = [ @@ -145,7 +145,7 @@ def test_create_identity_provider_remote_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_create_identity_provider_remote_ids_multiple(self): arglist = [ @@ -174,7 +174,7 @@ def test_create_identity_provider_remote_ids_multiple(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_create_identity_provider_remote_ids_file(self): arglist = [ @@ -207,7 +207,7 @@ def test_create_identity_provider_remote_ids_file(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_create_identity_provider_disabled(self): @@ -250,7 +250,7 @@ def test_create_identity_provider_disabled(self): identity_fakes.idp_id, identity_fakes.formatted_idp_remote_ids ) - self.assertItemEqual(datalist, data) + self.assertItemsEqual(datalist, data) def test_create_identity_provider_domain_name(self): arglist = [ @@ -278,7 +278,7 @@ def test_create_identity_provider_domain_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_create_identity_provider_domain_id(self): arglist = [ @@ -306,7 +306,7 @@ def test_create_identity_provider_domain_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) class TestIdentityProviderDelete(TestIdentityProvider): @@ -382,7 +382,7 @@ def test_identity_provider_list_no_options(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_identity_provider_list_ID_option(self): arglist = ['--id', @@ -410,7 +410,7 @@ def test_identity_provider_list_ID_option(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_identity_provider_list_enabled_option(self): arglist = ['--enabled'] @@ -437,7 +437,7 @@ def test_identity_provider_list_enabled_option(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) class TestIdentityProviderSet(TestIdentityProvider): @@ -722,4 +722,4 @@ def test_identity_provider_show(self): identity_fakes.idp_id, identity_fakes.formatted_idp_remote_ids ) - self.assertItemEqual(datalist, data) + self.assertItemsEqual(datalist, data) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 2f190a7a3c..db64983c97 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -100,7 +100,7 @@ def test_image_reserve_no_options(self, raw_input): self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) @mock.patch('sys.stdin', side_effect=[None]) def test_image_reserve_options(self, raw_input): @@ -149,7 +149,7 @@ def test_image_reserve_options(self, raw_input): self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) @mock.patch('openstackclient.image.v1.image.io.open', name='Open') def test_image_create_file(self, mock_open): @@ -205,7 +205,7 @@ def test_image_create_file(self, mock_open): self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestImageDelete(TestImage): @@ -386,7 +386,7 @@ def test_image_list_long_option(self): format_columns.DictColumn( {'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): @@ -737,7 +737,7 @@ def test_image_show(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_image_show_human_readable(self): arglist = [ diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 80e60ee818..b72e983599 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -111,7 +111,7 @@ def test_image_reserve_no_options(self, raw_input): self.assertEqual( self.expected_columns, columns) - self.assertItemEqual( + self.assertItemsEqual( self.expected_data, data) @@ -166,7 +166,7 @@ def test_image_reserve_options(self, raw_input): self.assertEqual( self.expected_columns, columns) - self.assertItemEqual( + self.assertItemsEqual( self.expected_data, data) @@ -255,7 +255,7 @@ def test_image_create_file(self): self.assertEqual( self.expected_columns, columns) - self.assertItemEqual( + self.assertItemsEqual( self.expected_data, data) @@ -513,7 +513,7 @@ def test_image_list_no_options(self): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_image_list_public_option(self): arglist = [ @@ -537,7 +537,7 @@ def test_image_list_public_option(self): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_image_list_private_option(self): arglist = [ @@ -561,7 +561,7 @@ def test_image_list_private_option(self): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_image_list_community_option(self): arglist = [ @@ -609,7 +609,7 @@ def test_image_list_shared_option(self): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_image_list_shared_member_status_option(self): arglist = [ @@ -697,7 +697,7 @@ def test_image_list_long_option(self): self._image.owner_id, format_columns.ListColumn(self._image.tags), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): @@ -725,7 +725,7 @@ def test_image_list_property_option(self, sf_mock): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) @mock.patch('osc_lib.utils.sort_items') def test_image_list_sort_option(self, si_mock): @@ -747,7 +747,7 @@ def test_image_list_sort_option(self, si_mock): str, ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_image_list_limit_option(self): ret_limit = 1 @@ -1472,7 +1472,7 @@ def test_image_show(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_image_show_human_readable(self): self.client.find_image.return_value = self.new_image diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index 9a712704c6..ade5783700 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -75,7 +75,7 @@ def test_list_no_options(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_ip_version(self): arglist = [ @@ -93,7 +93,7 @@ def test_list_ip_version(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_project(self): arglist = [ @@ -113,7 +113,7 @@ def test_list_project(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) class TestShowIPAvailability(TestIPAvailability): @@ -176,4 +176,4 @@ def test_show_all_options(self): self._ip_availability.network_name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 5f8eed6702..e29b72c769 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -146,7 +146,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -211,7 +211,7 @@ def test_create_all_options(self): 'dns_domain': 'example.org.', }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_other_options(self): arglist = [ @@ -238,7 +238,7 @@ def test_create_other_options(self): 'port_security_enabled': False, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self._network.name] @@ -270,7 +270,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -385,7 +385,7 @@ def test_create_with_project_identityv2(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_domain_identityv2(self): arglist = [ @@ -577,7 +577,7 @@ def test_network_list_no_options(self): self.network.networks.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_external(self): arglist = [ @@ -598,7 +598,7 @@ def test_list_external(self): **{'router:external': True, 'is_router_external': True} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_internal(self): arglist = [ @@ -615,7 +615,7 @@ def test_list_internal(self): **{'router:external': False, 'is_router_external': False} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_long(self): arglist = [ @@ -634,7 +634,7 @@ def test_network_list_long(self): self.network.networks.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_list_name(self): test_name = "fakename" @@ -653,7 +653,7 @@ def test_list_name(self): **{'name': test_name} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_enable(self): arglist = [ @@ -671,7 +671,7 @@ def test_network_list_enable(self): **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_disable(self): arglist = [ @@ -689,7 +689,7 @@ def test_network_list_disable(self): **{'admin_state_up': False, 'is_admin_state_up': False} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -708,7 +708,7 @@ def test_network_list_project(self): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -727,7 +727,7 @@ def test_network_list_project_domain(self): self.network.networks.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_share(self): arglist = [ @@ -744,7 +744,7 @@ def test_network_list_share(self): **{'shared': True, 'is_shared': True} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_no_share(self): arglist = [ @@ -761,7 +761,7 @@ def test_network_list_no_share(self): **{'shared': False, 'is_shared': False} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_status(self): choices = ['ACTIVE', 'BUILD', 'DOWN', 'ERROR'] @@ -780,7 +780,7 @@ def test_network_list_status(self): **{'status': test_status} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_provider_network_type(self): network_type = self._network[0].provider_network_type @@ -798,7 +798,7 @@ def test_network_list_provider_network_type(self): 'provider_network_type': network_type} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_provider_physical_network(self): physical_network = self._network[0].provider_physical_network @@ -816,7 +816,7 @@ def test_network_list_provider_physical_network(self): 'provider_physical_network': physical_network} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_provider_segment(self): segmentation_id = self._network[0].provider_segmentation_id @@ -834,7 +834,7 @@ def test_network_list_provider_segment(self): 'provider_segmentation_id': segmentation_id} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_list_dhcp_agent(self): arglist = [ @@ -853,7 +853,7 @@ def test_network_list_dhcp_agent(self): *attrs) self.assertEqual(self.columns, columns) - self.assertListItemEqual(list(data), list(self.data)) + self.assertItemsEqual(list(data), list(self.data)) def test_list_with_tag_options(self): arglist = [ @@ -878,7 +878,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) class TestSetNetwork(TestNetwork): @@ -1111,7 +1111,7 @@ def test_show_all_options(self): self._network.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestUnsetNetwork(TestNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 3181ee78a1..fceac68ea0 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -246,7 +246,7 @@ def test_network_agents_list(self): self.network.agents.assert_called_once_with(**{}) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_agents_list_agent_type(self): arglist = [ @@ -263,7 +263,7 @@ def test_network_agents_list_agent_type(self): 'agent_type': 'DHCP agent', }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_agents_list_host(self): arglist = [ @@ -280,7 +280,7 @@ def test_network_agents_list_host(self): 'host': self.network_agents[0].host, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_agents_list_networks(self): arglist = [ @@ -298,7 +298,7 @@ def test_network_agents_list_networks(self): self.network.network_hosting_dhcp_agents.assert_called_once_with( *attrs) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_agents_list_routers(self): arglist = [ @@ -318,7 +318,7 @@ def test_network_agents_list_routers(self): *attrs) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_network_agents_list_routers_with_long_option(self): arglist = [ @@ -343,7 +343,7 @@ def test_network_agents_list_routers_with_long_option(self): router_agent_data = [d + ('',) for d in self.data] self.assertEqual(router_agent_columns, columns) - self.assertListItemEqual(router_agent_data, list(data)) + self.assertItemsEqual(router_agent_data, list(data)) class TestRemoveNetworkFromAgent(TestNetworkAgent): @@ -571,4 +571,4 @@ def test_show_all_options(self): self.network.get_agent.assert_called_once_with( self._network_agent.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(list(self.data), list(data)) + self.assertItemsEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index f7685f46e5..e21f9d01f1 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -151,7 +151,7 @@ def test_create_default_options(self): self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_full_options(self): arglist = [ @@ -209,7 +209,7 @@ def test_create_full_options(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_invalid_json_binding_profile(self): arglist = [ @@ -260,7 +260,7 @@ def test_create_json_binding_profile(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_security_group(self): secgroup = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -289,7 +289,7 @@ def test_create_with_security_group(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_port_with_dns_name(self): arglist = [ @@ -315,7 +315,7 @@ def test_create_port_with_dns_name(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_security_groups(self): sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -345,7 +345,7 @@ def test_create_with_security_groups(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_no_security_groups(self): arglist = [ @@ -371,7 +371,7 @@ def test_create_with_no_security_groups(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_no_fixed_ips(self): arglist = [ @@ -397,7 +397,7 @@ def test_create_with_no_fixed_ips(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_port_with_allowed_address_pair_ipaddr(self): pairs = [{'ip_address': '192.168.1.123'}, @@ -427,7 +427,7 @@ def test_create_port_with_allowed_address_pair_ipaddr(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_port_with_allowed_address_pair(self): pairs = [{'ip_address': '192.168.1.123', @@ -463,7 +463,7 @@ def test_create_port_with_allowed_address_pair(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_port_with_qos(self): qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() @@ -491,7 +491,7 @@ def test_create_port_with_qos(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_port_security_enabled(self): arglist = [ @@ -600,7 +600,7 @@ def _test_create_with_tag(self, add_tags=True, add_tags_in_post=True): self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True, add_tags_in_post=True) @@ -643,7 +643,7 @@ def _test_create_with_uplink_status_propagation(self, enable=True): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_uplink_status_propagation_enabled(self): self._test_create_with_uplink_status_propagation(enable=True) @@ -723,7 +723,7 @@ def _test_create_with_numa_affinity_policy(self, policy=None): self.network.create_port.assert_called_once_with(**create_args) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_numa_affinity_policy_required(self): self._test_create_with_numa_affinity_policy(policy='required') @@ -890,7 +890,7 @@ def test_port_list_no_options(self): self.network.ports.assert_called_once_with( fields=LIST_FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_router_opt(self): arglist = [ @@ -910,7 +910,7 @@ def test_port_list_router_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) @mock.patch.object(utils, 'find_resource') def test_port_list_with_server_option(self, mock_find): @@ -931,7 +931,7 @@ def test_port_list_with_server_option(self, mock_find): fields=LIST_FIELDS_TO_RETRIEVE) mock_find.assert_called_once_with(mock.ANY, 'fake-server-name') self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_device_id_opt(self): arglist = [ @@ -951,7 +951,7 @@ def test_port_list_device_id_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_device_owner_opt(self): arglist = [ @@ -971,7 +971,7 @@ def test_port_list_device_owner_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_all_opt(self): arglist = [ @@ -1000,7 +1000,7 @@ def test_port_list_all_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_mac_address_opt(self): arglist = [ @@ -1020,7 +1020,7 @@ def test_port_list_mac_address_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_ip_address(self): ip_address = self._ports[0].fixed_ips[0]['ip_address'] @@ -1040,7 +1040,7 @@ def test_port_list_fixed_ip_opt_ip_address(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_ip_address_substr(self): ip_address_ss = self._ports[0].fixed_ips[0]['ip_address'][:-1] @@ -1060,7 +1060,7 @@ def test_port_list_fixed_ip_opt_ip_address_substr(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_subnet_id(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -1082,7 +1082,7 @@ def test_port_list_fixed_ip_opt_subnet_id(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_fixed_ip_opts(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -1108,7 +1108,7 @@ def test_port_list_fixed_ip_opts(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_fixed_ips(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -1136,7 +1136,7 @@ def test_port_list_fixed_ips(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_port_with_long(self): arglist = [ @@ -1154,7 +1154,7 @@ def test_list_port_with_long(self): self.network.ports.assert_called_once_with( fields=LIST_FIELDS_TO_RETRIEVE + LIST_FIELDS_TO_RETRIEVE_LONG) self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_port_list_host(self): arglist = [ @@ -1173,7 +1173,7 @@ def test_port_list_host(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_project(self): project = identity_fakes.FakeProject.create_one_project() @@ -1195,7 +1195,7 @@ def test_port_list_project(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_port_list_project_domain(self): project = identity_fakes.FakeProject.create_one_project() @@ -1219,7 +1219,7 @@ def test_port_list_project_domain(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -1245,7 +1245,7 @@ def test_list_with_tag_options(self): 'fields': LIST_FIELDS_TO_RETRIEVE} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) class TestSetPort(TestPort): @@ -1845,7 +1845,7 @@ def test_show_all_options(self): self._port.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestUnsetPort(TestPort): diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 09b4957cce..323c919828 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -184,7 +184,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def _test_create_with_ha_options(self, option, ha): arglist = [ @@ -208,7 +208,7 @@ def _test_create_with_ha_options(self, option, ha): 'ha': ha, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_ha_option(self): self._test_create_with_ha_options('--ha', True) @@ -237,7 +237,7 @@ def _test_create_with_distributed_options(self, option, distributed): 'distributed': distributed, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_distributed_option(self): self._test_create_with_distributed_options('--distributed', True) @@ -268,7 +268,7 @@ def test_create_with_AZ_hints(self): }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self.new_router.name] @@ -301,7 +301,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -494,7 +494,7 @@ def test_router_list_no_options(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_router_list_no_ha_no_distributed(self): _routers = network_fakes.FakeRouter.create_routers({ @@ -531,7 +531,7 @@ def test_router_list_long(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_router_list_long_no_az(self): arglist = [ @@ -552,7 +552,7 @@ def test_router_list_long_no_az(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long_no_az, columns) - self.assertListItemEqual(self.data_long_no_az, list(data)) + self.assertItemsEqual(self.data_long_no_az, list(data)) def test_list_name(self): test_name = "fakename" @@ -570,7 +570,7 @@ def test_list_name(self): **{'name': test_name} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_router_list_enable(self): arglist = [ @@ -587,7 +587,7 @@ def test_router_list_enable(self): **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_router_list_disable(self): arglist = [ @@ -605,7 +605,7 @@ def test_router_list_disable(self): ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_router_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -623,7 +623,7 @@ def test_router_list_project(self): self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_router_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -643,7 +643,7 @@ def test_router_list_project_domain(self): self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_router_list_agents_no_args(self): arglist = [ @@ -671,7 +671,7 @@ def test_router_list_agents(self): self.network.agent_hosted_routers( *attrs) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -696,7 +696,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) class TestRemovePortFromRouter(TestRouter): @@ -1403,7 +1403,7 @@ def test_show_all_options(self): 'device_id': self._router.id }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_show_no_ha_no_distributed(self): _router = network_fakes.FakeRouter.create_one_router({ diff --git a/openstackclient/tests/unit/network/v2/test_security_group_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_compute.py index b4ddcf803d..837c9b21af 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_compute.py @@ -88,7 +88,7 @@ def test_security_group_create_min_options(self, sg_mock): self._security_group['name'], ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_security_group_create_all_options(self, sg_mock): sg_mock.return_value = self._security_group @@ -109,7 +109,7 @@ def test_security_group_create_all_options(self, sg_mock): self._security_group['description'], ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) @mock.patch( @@ -255,7 +255,7 @@ def test_security_group_list_no_options(self, sg_mock): kwargs = {'search_opts': {'all_tenants': False}} sg_mock.assert_called_once_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_security_group_list_all_projects(self, sg_mock): sg_mock.return_value = self._security_groups @@ -272,7 +272,7 @@ def test_security_group_list_all_projects(self, sg_mock): kwargs = {'search_opts': {'all_tenants': True}} sg_mock.assert_called_once_with(**kwargs) self.assertEqual(self.columns_all_projects, columns) - self.assertListItemEqual(self.data_all_projects, list(data)) + self.assertItemsEqual(self.data_all_projects, list(data)) @mock.patch( @@ -401,4 +401,4 @@ def test_security_group_show_all_options(self, sg_mock): sg_mock.assert_called_once_with(self._security_group['id']) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index 7c1d7fb6b0..fe37778598 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -96,7 +96,7 @@ def test_create_min_options(self): 'name': self._security_group.name, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -124,7 +124,7 @@ def test_create_all_options(self): 'tenant_id': self.project.id, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self._security_group.name] @@ -155,7 +155,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -293,7 +293,7 @@ def test_security_group_list_no_options(self): self.network.security_groups.assert_called_once_with( fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_security_group_list_all_projects(self): arglist = [ @@ -309,7 +309,7 @@ def test_security_group_list_all_projects(self): self.network.security_groups.assert_called_once_with( fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_security_group_list_project(self): project = identity_fakes.FakeProject.create_one_project() @@ -329,7 +329,7 @@ def test_security_group_list_project(self): self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_security_group_list_project_domain(self): project = identity_fakes.FakeProject.create_one_project() @@ -351,7 +351,7 @@ def test_security_group_list_project_domain(self): self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -539,7 +539,7 @@ def test_show_all_options(self): self.network.find_security_group.assert_called_once_with( self._security_group.id, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestUnsetSecurityGroupNetwork(TestSecurityGroupNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 47d0c6b47f..1b4bfdad2f 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -255,7 +255,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_from_subnet_pool_options(self): # Mock SDK calls for this test. @@ -317,7 +317,7 @@ def test_create_from_subnet_pool_options(self): 'service_types': self._subnet_from_pool.service_types, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data_subnet_pool, data) + self.assertItemsEqual(self.data_subnet_pool, data) def test_create_options_subnet_range_ipv6(self): # Mock SDK calls for this test. @@ -390,7 +390,7 @@ def test_create_options_subnet_range_ipv6(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data_ipv6, data) + self.assertItemsEqual(self.data_ipv6, data) def test_create_with_network_segment(self): # Mock SDK calls for this test. @@ -424,7 +424,7 @@ def test_create_with_network_segment(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_description(self): # Mock SDK calls for this test. @@ -458,7 +458,7 @@ def test_create_with_description(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def _test_create_with_dns(self, publish_dns=True): arglist = [ @@ -490,7 +490,7 @@ def _test_create_with_dns(self, publish_dns=True): dns_publish_fixed_ip=publish_dns, ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_dns(self): self._test_create_with_dns(publish_dns=True) @@ -535,7 +535,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -691,7 +691,7 @@ def test_subnet_list_no_options(self): self.network.subnets.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_long(self): arglist = [ @@ -706,7 +706,7 @@ def test_subnet_list_long(self): self.network.subnets.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_subnet_list_ip_version(self): arglist = [ @@ -722,7 +722,7 @@ def test_subnet_list_ip_version(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_dhcp(self): arglist = [ @@ -738,7 +738,7 @@ def test_subnet_list_dhcp(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_no_dhcp(self): arglist = [ @@ -754,7 +754,7 @@ def test_subnet_list_no_dhcp(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_service_type(self): arglist = [ @@ -769,7 +769,7 @@ def test_subnet_list_service_type(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -787,7 +787,7 @@ def test_subnet_list_project(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_service_type_multiple(self): arglist = [ @@ -805,7 +805,7 @@ def test_subnet_list_service_type_multiple(self): 'network:floatingip_agent_gateway']} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -825,7 +825,7 @@ def test_subnet_list_project_domain(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_network(self): network = network_fakes.FakeNetwork.create_one_network() @@ -843,7 +843,7 @@ def test_subnet_list_network(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_gateway(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -861,7 +861,7 @@ def test_subnet_list_gateway(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_name(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -879,7 +879,7 @@ def test_subnet_list_name(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_list_subnet_range(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -897,7 +897,7 @@ def test_subnet_list_subnet_range(self): self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -1244,7 +1244,7 @@ def test_show_all_options(self): self._subnet.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestUnsetSubnet(TestSubnet): diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index eb454646de..243fc76df4 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -133,7 +133,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_prefixlen_options(self): arglist = [ @@ -163,7 +163,7 @@ def test_create_prefixlen_options(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_len_negative(self): arglist = [ @@ -201,7 +201,7 @@ def test_create_project_domain(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_address_scope_option(self): arglist = [ @@ -224,7 +224,7 @@ def test_create_address_scope_option(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_default_and_shared_options(self): arglist = [ @@ -250,7 +250,7 @@ def test_create_default_and_shared_options(self): 'shared': True, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_description(self): arglist = [ @@ -273,7 +273,7 @@ def test_create_with_description(self): 'description': self._subnet_pool.description, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_default_quota(self): arglist = [ @@ -294,7 +294,7 @@ def test_create_with_default_quota(self): 'default_quota': 10, }) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [ @@ -328,7 +328,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -476,7 +476,7 @@ def test_subnet_pool_list_no_option(self): self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_long(self): arglist = [ @@ -491,7 +491,7 @@ def test_subnet_pool_list_long(self): self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_subnet_pool_list_no_share(self): arglist = [ @@ -507,7 +507,7 @@ def test_subnet_pool_list_no_share(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_share(self): arglist = [ @@ -523,7 +523,7 @@ def test_subnet_pool_list_share(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_no_default(self): arglist = [ @@ -539,7 +539,7 @@ def test_subnet_pool_list_no_default(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_default(self): arglist = [ @@ -555,7 +555,7 @@ def test_subnet_pool_list_default(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -573,7 +573,7 @@ def test_subnet_pool_list_project(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -593,7 +593,7 @@ def test_subnet_pool_list_project_domain(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_name(self): subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() @@ -611,7 +611,7 @@ def test_subnet_pool_list_name(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_subnet_pool_list_address_scope(self): addr_scope = network_fakes.FakeAddressScope.create_one_address_scope() @@ -629,7 +629,7 @@ def test_subnet_pool_list_address_scope(self): self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_list_with_tag_options(self): arglist = [ @@ -654,7 +654,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) class TestSetSubnetPool(TestSubnetPool): @@ -1008,7 +1008,7 @@ def test_show_all_options(self): ignore_missing=False ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestUnsetSubnetPool(TestSubnetPool): diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index 4130f18e2b..39cb561474 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -17,7 +17,6 @@ from io import StringIO import os -from cliff import columns as cliff_columns import fixtures import testtools @@ -85,18 +84,3 @@ def check_parser(self, cmd, args, verify_args): self.assertIn(attr, parsed_args) self.assertEqual(value, getattr(parsed_args, attr)) return parsed_args - - def assertListItemEqual(self, expected, actual): - self.assertEqual(len(expected), len(actual)) - for item_expected, item_actual in zip(expected, actual): - self.assertItemEqual(item_expected, item_actual) - - def assertItemEqual(self, expected, actual): - self.assertEqual(len(expected), len(actual)) - for col_expected, col_actual in zip(expected, actual): - if isinstance(col_expected, cliff_columns.FormattableColumn): - self.assertIsInstance(col_actual, col_expected.__class__) - self.assertEqual(col_expected.human_readable(), - col_actual.human_readable()) - else: - self.assertEqual(col_expected, col_actual) diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index 83c533b6f0..5500438bd6 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -109,7 +109,7 @@ def test_qos_create_without_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_qos_create_with_consumer(self): arglist = [ @@ -129,7 +129,7 @@ def test_qos_create_with_consumer(self): {'consumer': self.new_qos_spec.consumer} ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_qos_create_with_properties(self): arglist = [ @@ -155,7 +155,7 @@ def test_qos_create_with_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) class TestQosDelete(TestQos): @@ -350,7 +350,7 @@ def test_qos_list(self): self.qos_mock.list.assert_called_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_qos_list_no_association(self): self.qos_mock.reset_mock() @@ -377,7 +377,7 @@ def test_qos_list_no_association(self): format_columns.ListColumn(None), format_columns.DictColumn(self.qos_specs[1].specs), ) - self.assertListItemEqual(ex_data, list(data)) + self.assertItemsEqual(ex_data, list(data)) class TestQosSet(TestQos): @@ -454,7 +454,7 @@ def test_qos_show(self): self.qos_spec.name, format_columns.DictColumn(self.qos_spec.specs), ) - self.assertItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) class TestQosUnset(TestQos): diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index 8bee574795..f1d469140b 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -78,7 +78,7 @@ def test_type_create(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_type_create_with_encryption(self): encryption_info = { @@ -139,7 +139,7 @@ def test_type_create_with_encryption(self): body, ) self.assertEqual(encryption_columns, columns) - self.assertItemEqual(encryption_data, data) + self.assertItemsEqual(encryption_data, data) class TestTypeDelete(TestType): @@ -270,7 +270,7 @@ def test_type_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = [ @@ -284,7 +284,7 @@ def test_type_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_type_list_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type( @@ -328,7 +328,7 @@ def test_type_list_with_encryption(self): self.encryption_types_mock.list.assert_called_once_with() self.types_mock.list.assert_called_once_with() self.assertEqual(encryption_columns, columns) - self.assertListItemEqual(encryption_data, list(data)) + self.assertItemsEqual(encryption_data, list(data)) class TestTypeSet(TestType): @@ -469,7 +469,7 @@ def test_type_show(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_type_show_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type() @@ -513,7 +513,7 @@ def test_type_show_with_encryption(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.encryption_types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(encryption_columns, columns) - self.assertItemEqual(encryption_data, data) + self.assertItemsEqual(encryption_data, data) class TestTypeUnset(TestType): diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 25cdf92a43..704a66da71 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -135,7 +135,7 @@ def test_volume_create_min_options(self): None, ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_options(self): arglist = [ @@ -179,7 +179,7 @@ def test_volume_create_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_user_project_id(self): # Return a project @@ -226,7 +226,7 @@ def test_volume_create_user_project_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_user_project_name(self): # Return a project @@ -273,7 +273,7 @@ def test_volume_create_user_project_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -314,7 +314,7 @@ def test_volume_create_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() @@ -357,7 +357,7 @@ def test_volume_create_image_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() @@ -400,7 +400,7 @@ def test_volume_create_image_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_with_source(self): self.volumes_mock.get.return_value = self.new_volume @@ -430,7 +430,7 @@ def test_volume_create_with_source(self): None, ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_with_bootable_and_readonly(self): arglist = [ @@ -468,7 +468,7 @@ def test_volume_create_with_bootable_and_readonly(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -510,7 +510,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, False) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -562,7 +562,7 @@ def test_volume_create_with_bootable_and_readonly_fail( self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -765,7 +765,7 @@ def test_volume_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_volume_list_name(self): arglist = [ @@ -782,7 +782,7 @@ def test_volume_list_name(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_volume_list_status(self): arglist = [ @@ -799,7 +799,7 @@ def test_volume_list_status(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_volume_list_all_projects(self): arglist = [ @@ -816,7 +816,7 @@ def test_volume_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_volume_list_long(self): arglist = [ @@ -856,7 +856,7 @@ def test_volume_list_long(self): volume.AttachmentsColumn(self._volume.attachments), format_columns.DictColumn(self._volume.metadata), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_with_limit(self): arglist = [ @@ -881,7 +881,7 @@ def test_volume_list_with_limit(self): 'all_tenants': False, } ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.datalist, tuple(data)) + self.assertItemsEqual(self.datalist, tuple(data)) def test_volume_list_negative_limit(self): arglist = [ @@ -1272,7 +1272,7 @@ def test_volume_show(self): self.volumes_mock.get.assert_called_with(self._volume.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_show_backward_compatibility(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v1/test_volume_backup.py b/openstackclient/tests/unit/volume/v1/test_volume_backup.py index 20aadcd344..a713155092 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_volume_backup.py @@ -100,7 +100,7 @@ def test_backup_create(self): self.new_backup.description, ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_backup_create_without_name(self): arglist = [ @@ -124,7 +124,7 @@ def test_backup_create_without_name(self): self.new_backup.description, ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestBackupDelete(TestBackup): @@ -277,7 +277,7 @@ def test_backup_list_without_options(self): search_opts=search_opts, ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = [ @@ -309,7 +309,7 @@ def test_backup_list_with_options(self): search_opts=search_opts, ) self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): @@ -391,4 +391,4 @@ def test_backup_show(self): self.backups_mock.get.assert_called_with(self.backup.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index c3bd71e325..6bb6c02978 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -251,7 +251,7 @@ def test_consistency_group_create_without_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_consistency_group_create_from_source(self): arglist = [ @@ -279,7 +279,7 @@ def test_consistency_group_create_from_source(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_consistency_group_create_from_snapshot(self): arglist = [ @@ -307,7 +307,7 @@ def test_consistency_group_create_from_snapshot(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestConsistencyGroupDelete(TestConsistencyGroup): @@ -463,7 +463,7 @@ def test_consistency_group_list_without_options(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_consistency_group_list_with_all_project(self): arglist = [ @@ -480,7 +480,7 @@ def test_consistency_group_list_with_all_project(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': True}) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_consistency_group_list_with_long(self): arglist = [ @@ -497,7 +497,7 @@ def test_consistency_group_list_with_long(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): @@ -705,4 +705,4 @@ def test_consistency_group_show(self): self.consistencygroups_mock.get.assert_called_once_with( self.consistency_group.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index 073ec5709b..bc4cee8b40 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -112,7 +112,7 @@ def test_qos_create_without_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_qos_create_with_consumer(self): arglist = [ @@ -133,7 +133,7 @@ def test_qos_create_with_consumer(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_qos_create_with_properties(self): arglist = [ @@ -159,7 +159,7 @@ def test_qos_create_with_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) class TestQosDelete(TestQos): @@ -342,7 +342,7 @@ def test_qos_list(self): self.qos_mock.list.assert_called_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_qos_list_no_association(self): self.qos_mock.reset_mock() @@ -369,7 +369,7 @@ def test_qos_list_no_association(self): format_columns.ListColumn(None), format_columns.DictColumn(self.qos_specs[1].specs), ) - self.assertListItemEqual(ex_data, list(data)) + self.assertItemsEqual(ex_data, list(data)) class TestQosSet(TestQos): @@ -449,7 +449,7 @@ def test_qos_show(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, tuple(data)) + self.assertItemsEqual(self.data, tuple(data)) class TestQosUnset(TestQos): diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index f13d08517a..000464c5d8 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -93,7 +93,7 @@ def test_type_create_public(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_type_create_private(self): arglist = [ @@ -119,7 +119,7 @@ def test_type_create_private(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_public_type_create_with_project(self): arglist = [ @@ -196,7 +196,7 @@ def test_type_create_with_encryption(self): body, ) self.assertEqual(encryption_columns, columns) - self.assertItemEqual(encryption_data, data) + self.assertItemsEqual(encryption_data, data) class TestTypeDelete(TestType): @@ -330,7 +330,7 @@ def test_type_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=None) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = [ @@ -348,7 +348,7 @@ def test_type_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=True) self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) def test_type_list_with_private_option(self): arglist = [ @@ -365,7 +365,7 @@ def test_type_list_with_private_option(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=False) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_type_list_with_default_option(self): arglist = [ @@ -383,7 +383,7 @@ def test_type_list_with_default_option(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.default.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data_with_default_type, list(data)) + self.assertItemsEqual(self.data_with_default_type, list(data)) def test_type_list_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type( @@ -427,7 +427,7 @@ def test_type_list_with_encryption(self): self.encryption_types_mock.list.assert_called_once_with() self.types_mock.list.assert_called_once_with(is_public=None) self.assertEqual(encryption_columns, columns) - self.assertListItemEqual(encryption_data, list(data)) + self.assertItemsEqual(encryption_data, list(data)) class TestTypeSet(TestType): @@ -713,7 +713,7 @@ def test_type_show(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.data, data) + self.assertItemsEqual(self.data, data) def test_type_show_with_access(self): arglist = [ @@ -746,7 +746,7 @@ def test_type_show_with_access(self): private_type.name, format_columns.DictColumn(private_type.extra_specs) ) - self.assertItemEqual(private_type_data, data) + self.assertItemsEqual(private_type_data, data) def test_type_show_with_list_access_exec(self): arglist = [ @@ -778,7 +778,7 @@ def test_type_show_with_list_access_exec(self): private_type.name, format_columns.DictColumn(private_type.extra_specs) ) - self.assertItemEqual(private_type_data, data) + self.assertItemsEqual(private_type_data, data) def test_type_show_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type() @@ -824,7 +824,7 @@ def test_type_show_with_encryption(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.encryption_types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(encryption_columns, columns) - self.assertItemEqual(encryption_data, data) + self.assertItemsEqual(encryption_data, data) class TestTypeUnset(TestType): diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 4e204ad1b2..b9fe4e834c 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -136,7 +136,7 @@ def test_volume_create_min_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_options(self): consistency_group = ( @@ -182,7 +182,7 @@ def test_volume_create_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -218,7 +218,7 @@ def test_volume_create_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() @@ -256,7 +256,7 @@ def test_volume_create_image_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() @@ -294,7 +294,7 @@ def test_volume_create_image_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_with_snapshot(self): snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() @@ -331,7 +331,7 @@ def test_volume_create_with_snapshot(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) def test_volume_create_with_bootable_and_readonly(self): arglist = [ @@ -369,7 +369,7 @@ def test_volume_create_with_bootable_and_readonly(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -411,7 +411,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): ) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, False) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -463,7 +463,7 @@ def test_volume_create_with_bootable_and_readonly_fail( self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertItemEqual(self.datalist, data) + self.assertItemsEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -680,7 +680,7 @@ def test_volume_list_no_options(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_project(self): arglist = [ @@ -720,7 +720,7 @@ def test_volume_list_project(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_project_domain(self): arglist = [ @@ -762,7 +762,7 @@ def test_volume_list_project_domain(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_user(self): arglist = [ @@ -801,7 +801,7 @@ def test_volume_list_user(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_user_domain(self): arglist = [ @@ -843,7 +843,7 @@ def test_volume_list_user_domain(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_name(self): arglist = [ @@ -883,7 +883,7 @@ def test_volume_list_name(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_status(self): arglist = [ @@ -923,7 +923,7 @@ def test_volume_list_status(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_all_projects(self): arglist = [ @@ -963,7 +963,7 @@ def test_volume_list_all_projects(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_long(self): arglist = [ @@ -1017,7 +1017,7 @@ def test_volume_list_long(self): volume.AttachmentsColumn(self.mock_volume.attachments), format_columns.DictColumn(self.mock_volume.metadata), ), ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_with_marker_and_limit(self): arglist = [ @@ -1056,7 +1056,7 @@ def test_volume_list_with_marker_and_limit(self): 'name': None, 'all_tenants': False, } ) - self.assertListItemEqual(datalist, tuple(data)) + self.assertItemsEqual(datalist, tuple(data)) def test_volume_list_negative_limit(self): arglist = [ @@ -1450,7 +1450,7 @@ def test_volume_show(self): volume_fakes.FakeVolume.get_volume_columns(self._volume), columns) - self.assertItemEqual( + self.assertItemsEqual( volume_fakes.FakeVolume.get_volume_data(self._volume), data) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index 4e1f7ee135..13513ed8ad 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -314,7 +314,7 @@ def test_backup_list_without_options(self): limit=None, ) self.assertEqual(self.columns, columns) - self.assertListItemEqual(self.data, list(data)) + self.assertItemsEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = [ @@ -353,7 +353,7 @@ def test_backup_list_with_options(self): limit=3, ) self.assertEqual(self.columns_long, columns) - self.assertListItemEqual(self.data_long, list(data)) + self.assertItemsEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): diff --git a/requirements.txt b/requirements.txt index b167628b51..ed55ce3794 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,10 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -cliff>=3.4.0 # Apache-2.0 +cliff>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT openstacksdk>=0.52.0 # Apache-2.0 -osc-lib>=2.2.0 # Apache-2.0 +osc-lib>=2.3.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=15.1.0 # Apache-2.0 From da03bd80e3b83faf465f1446c4553c5d97b5bad5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 14 Oct 2020 10:48:44 +0100 Subject: [PATCH 0325/1120] Add 'flavor list --min-disk', '--min-ram' options Allow us to filter on minimum disk and RAM, and close another gap with novaclient. Change-Id: Ib3f0bdf419675e1c35c3406fbac8a4c18ac56a33 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/flavor.py | 43 ++++++++++++++++--- .../tests/unit/compute/v2/test_flavor.py | 31 +++++++++++++ ...ist-min-disk-min-ram-65ba35e7acc24134.yaml | 6 +++ 3 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/flavor-list-min-disk-min-ram-65ba35e7acc24134.yaml diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 8477e8efb1..fa98e131b1 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -263,6 +263,18 @@ def get_parser(self, prog_name): default=False, help=_("List all flavors, whether public or private") ) + parser.add_argument( + '--min-disk', + type=int, + metavar='', + help=_('Filters the flavors by a minimum disk space, in GiB.'), + ) + parser.add_argument( + '--min-ram', + type=int, + metavar='', + help=_('Filters the flavors by a minimum RAM, in MiB.'), + ) parser.add_argument( '--long', action='store_true', @@ -277,8 +289,13 @@ def get_parser(self, prog_name): parser.add_argument( '--limit', type=int, - metavar="", - help=_("Maximum number of flavors to display") + metavar='', + help=_( + 'Maximum number of flavors to display. This is also ' + 'configurable on the server. The actual limit used will be ' + 'the lower of the user-supplied value and the server ' + 'configuration-derived value' + ), ) return parser @@ -293,15 +310,24 @@ def take_action(self, parsed_args): query_attrs = { 'is_public': is_public } + if parsed_args.marker: query_attrs['marker'] = parsed_args.marker + if parsed_args.limit: query_attrs['limit'] = parsed_args.limit + if parsed_args.limit or parsed_args.marker: # User passed explicit pagination request, switch off SDK # pagination query_attrs['paginated'] = False + if parsed_args.min_disk: + query_attrs['min_disk'] = parsed_args.min_disk + + if parsed_args.min_ram: + query_attrs['min_ram'] = parsed_args.min_ram + data = list(compute_client.flavors(**query_attrs)) # Even if server supports 2.61 some policy might stop it sending us # extra_specs. So try to fetch them if they are absent @@ -341,10 +367,13 @@ def take_action(self, parsed_args): "Properties", ) - return (column_headers, - (utils.get_item_properties( - s, columns, formatters=_formatters, - ) for s in data)) + return ( + column_headers, + ( + utils.get_item_properties(s, columns, formatters=_formatters) + for s in data + ), + ) class SetFlavor(command.Command): @@ -378,13 +407,13 @@ def get_parser(self, prog_name): help=_('Set flavor access to project (name or ID) ' '(admin only)'), ) + identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--description', metavar='', help=_("Set description for the flavor.(Supported by API " "versions '2.55' - '2.latest'") ) - identity_common.add_project_domain_option_to_parser(parser) return parser diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 8625b71202..af9e4a2c33 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -635,6 +635,37 @@ def test_flavor_list_long(self): self.assertEqual(self.columns_long, columns) self.assertListItemEqual(self.data_long, tuple(data)) + def test_flavor_list_min_disk_min_ram(self): + arglist = [ + '--min-disk', '10', + '--min-ram', '2048', + ] + verifylist = [ + ('min_disk', 10), + ('min_ram', 2048), + ] + + 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, + 'min_disk': 10, + 'min_ram': 2048, + } + + self.sdk_client.flavors.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + class TestFlavorSet(TestFlavor): diff --git a/releasenotes/notes/flavor-list-min-disk-min-ram-65ba35e7acc24134.yaml b/releasenotes/notes/flavor-list-min-disk-min-ram-65ba35e7acc24134.yaml new file mode 100644 index 0000000000..3418276c89 --- /dev/null +++ b/releasenotes/notes/flavor-list-min-disk-min-ram-65ba35e7acc24134.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``openstack flavor list`` command now accepts two additional + options, ``--min-disk`` and ``--min-ram``, to filter flavor by + minimum disk and minimum RAM, respectively. From f9fd3642f8e8c59cd5a819e260d0575f884ae174 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 14:29:24 +0000 Subject: [PATCH 0326/1120] compute: Add missing options for 'server rebuild' This accepts a large number of options that we weren't exposing. Add the following options: '--name', '--preserve-ephemeral', '--user-data', '--no-user-data', '--trusted-image-cert' and '--no-trusted-image-certs'. In addition, rename the '--key-unset' parameter to '--no-key-name', to mimic e.g. '--no-property' on other commands. Change-Id: I61c46e18bef1f086b62a015ebdc56be91071b826 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 149 +++++++- .../tests/unit/compute/v2/test_server.py | 358 ++++++++++++++---- ...-server-rebuild-opts-5c75e838d8f0487d.yaml | 13 + 3 files changed, 446 insertions(+), 74 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-rebuild-opts-5c75e838d8f0487d.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 4c177f7c3b..c9fbc42256 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2445,10 +2445,15 @@ def get_parser(self, prog_name): 'Defaults to the currently used one.' ), ) + parser.add_argument( + '--name', + metavar='', + help=_('Set the new name of the rebuilt server'), + ) parser.add_argument( '--password', metavar='', - help=_('Set a password on the rebuilt server'), + help=_('Set the password on the rebuilt server'), ) parser.add_argument( '--property', @@ -2467,6 +2472,24 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.19 or above)' ), ) + preserve_ephemeral_group = parser.add_mutually_exclusive_group() + preserve_ephemeral_group.add_argument( + '--preserve-ephemeral', + action='store_true', + default=None, + help=_( + 'Preserve the default ephemeral storage partition on rebuild.' + ), + ) + preserve_ephemeral_group.add_argument( + '--no-preserve-ephemeral', + action='store_false', + dest='preserve_ephemeral', + help=_( + 'Do not preserve the default ephemeral storage partition on ' + 'rebuild.' + ), + ) key_group = parser.add_mutually_exclusive_group() key_group.add_argument( '--key-name', @@ -2478,15 +2501,69 @@ def get_parser(self, prog_name): ), ) key_group.add_argument( - '--key-unset', + '--no-key-name', action='store_true', - default=False, + dest='no_key_name', help=_( 'Unset the key name of key pair on the rebuilt server. ' 'Cannot be specified with the --key-name option. ' '(supported by --os-compute-api-version 2.54 or above)' ), ) + # TODO(stephenfin): Remove this in a future major version bump + key_group.add_argument( + '--key-unset', + action='store_true', + dest='no_key_name', + help=argparse.SUPPRESS, + ) + user_data_group = parser.add_mutually_exclusive_group() + user_data_group.add_argument( + '--user-data', + metavar='', + help=_( + 'Add a new user data file to the rebuilt server. ' + 'Cannot be specified with the --no-user-data option. ' + '(supported by --os-compute-api-version 2.57 or above)' + ), + ) + user_data_group.add_argument( + '--no-user-data', + action='store_true', + default=False, + help=_( + 'Remove existing user data when rebuilding server. ' + 'Cannot be specified with the --user-data option. ' + '(supported by --os-compute-api-version 2.57 or above)' + ), + ) + trusted_certs_group = parser.add_mutually_exclusive_group() + trusted_certs_group.add_argument( + '--trusted-image-cert', + metavar='', + action='append', + dest='trusted_image_certs', + help=_( + 'Trusted image certificate IDs used to validate certificates ' + 'during the image signature verification process. ' + 'Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. ' + 'May be specified multiple times to pass multiple trusted ' + 'image certificate IDs. ' + 'Cannot be specified with the --no-trusted-certs option. ' + '(supported by --os-compute-api-version 2.63 or above)' + ), + ) + trusted_certs_group.add_argument( + '--no-trusted-image-certs', + action='store_true', + default=False, + help=_( + 'Remove any existing trusted image certificates from the ' + 'server. ' + 'Cannot be specified with the --trusted-certs option. ' + '(supported by --os-compute-api-version 2.63 or above)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -2517,11 +2594,17 @@ def _show_progress(progress): kwargs = {} + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + + if parsed_args.preserve_ephemeral is not None: + kwargs['preserve_ephemeral'] = parsed_args.preserve_ephemeral + if parsed_args.property: kwargs['meta'] = parsed_args.property if parsed_args.description: - if server.api_version < api_versions.APIVersion("2.19"): + if compute_client.api_version < api_versions.APIVersion('2.19'): msg = _( '--os-compute-api-version 2.19 or greater is required to ' 'support the --description option' @@ -2539,7 +2622,7 @@ def _show_progress(progress): raise exceptions.CommandError(msg) kwargs['key_name'] = parsed_args.key_name - elif parsed_args.key_unset: + elif parsed_args.no_key_name: if compute_client.api_version < api_versions.APIVersion('2.54'): msg = _( '--os-compute-api-version 2.54 or greater is required to ' @@ -2549,7 +2632,61 @@ def _show_progress(progress): kwargs['key_name'] = None - server = server.rebuild(image, parsed_args.password, **kwargs) + userdata = None + if parsed_args.user_data: + if compute_client.api_version < api_versions.APIVersion('2.54'): + msg = _( + '--os-compute-api-version 2.54 or greater is required to ' + 'support the --user-data option' + ) + raise exceptions.CommandError(msg) + + try: + userdata = io.open(parsed_args.user_data) + except IOError as e: + msg = _("Can't open '%(data)s': %(exception)s") + raise exceptions.CommandError( + msg % {'data': parsed_args.user_data, 'exception': e} + ) + + kwargs['userdata'] = userdata + elif parsed_args.no_user_data: + if compute_client.api_version < api_versions.APIVersion('2.54'): + msg = _( + '--os-compute-api-version 2.54 or greater is required to ' + 'support the --no-user-data option' + ) + raise exceptions.CommandError(msg) + + kwargs['userdata'] = None + + # TODO(stephenfin): Handle OS_TRUSTED_IMAGE_CERTIFICATE_IDS + if parsed_args.trusted_image_certs: + if compute_client.api_version < api_versions.APIVersion('2.63'): + msg = _( + '--os-compute-api-version 2.63 or greater is required to ' + 'support the --trusted-certs option' + ) + raise exceptions.CommandError(msg) + + certs = parsed_args.trusted_image_certs + kwargs['trusted_image_certificates'] = certs + elif parsed_args.no_trusted_image_certs: + if compute_client.api_version < api_versions.APIVersion('2.63'): + msg = _( + '--os-compute-api-version 2.63 or greater is required to ' + 'support the --no-trusted-certs option' + ) + raise exceptions.CommandError(msg) + + kwargs['trusted_image_certificates'] = None + + try: + server = server.rebuild(image, parsed_args.password, **kwargs) + finally: + if userdata and hasattr(userdata, 'close'): + userdata.close() + if parsed_args.wait: if utils.wait_for_status( compute_client.servers.get, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5fd15e6ab6..a4b1fad103 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4765,7 +4765,64 @@ 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_current_image_and_password(self): + def test_rebuild_with_name(self): + name = 'test-server-xxx' + arglist = [ + self.server.id, + '--name', name, + ] + verifylist = [ + ('server', self.server.id), + ('name', name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(self.image, None, name=name) + + def test_rebuild_with_preserve_ephemeral(self): + arglist = [ + self.server.id, + '--preserve-ephemeral', + ] + verifylist = [ + ('server', self.server.id), + ('preserve_ephemeral', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, preserve_ephemeral=True) + + def test_rebuild_with_no_preserve_ephemeral(self): + arglist = [ + self.server.id, + '--no-preserve-ephemeral', + ] + verifylist = [ + ('server', self.server.id), + ('preserve_ephemeral', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, preserve_ephemeral=False) + + def test_rebuild_with_password(self): password = 'password-xxx' arglist = [ self.server.id, @@ -4784,10 +4841,9 @@ def test_rebuild_with_current_image_and_password(self): self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) - def test_rebuild_with_description_api_older(self): - - # Description is not supported for nova api version below 2.19 - self.server.api_version = 2.18 + def test_rebuild_with_description(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.19') description = 'description1' arglist = [ @@ -4800,16 +4856,16 @@ def test_rebuild_with_description_api_older(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.19): - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + self.cmd.take_action(parsed_args) - def test_rebuild_with_description_api_newer(self): + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(self.image, None, + description=description) - # Description is supported for nova api version 2.19 or above - self.server.api_version = 2.19 + def test_rebuild_with_description_pre_v219(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.18') description = 'description1' arglist = [ @@ -4822,16 +4878,10 @@ def test_rebuild_with_description_api_newer(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.19): - # Get the command object to test - self.cmd.take_action(parsed_args) - - self.servers_mock.get.assert_called_with(self.server.id) - self.get_image_mock.assert_called_with(self.image.id) - self.server.rebuild.assert_called_with(self.image, None, - description=description) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_rebuild_with_wait_ok(self, mock_wait_for_status): @@ -4907,6 +4957,9 @@ def test_rebuild_with_property(self): self.image, None, meta=expected_property) def test_rebuild_with_keypair_name(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.54') + self.server.key_name = 'mykey' arglist = [ self.server.id, @@ -4918,23 +4971,17 @@ def test_rebuild_with_keypair_name(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.compute.api_version = 2.54 - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.54): - self.cmd.take_action(parsed_args) - args = ( - self.image, - None, - ) - kwargs = dict( - key_name=self.server.key_name, - ) - self.servers_mock.get.assert_called_with(self.server.id) - self.get_image_mock.assert_called_with(self.image.id) - self.server.rebuild.assert_called_with(*args, **kwargs) + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, key_name=self.server.key_name) + + def test_rebuild_with_keypair_name_pre_v254(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.53') - def test_rebuild_with_keypair_name_older_version(self): self.server.key_name = 'mykey' arglist = [ self.server.id, @@ -4946,55 +4993,230 @@ def test_rebuild_with_keypair_name_older_version(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.compute.api_version = 2.53 - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.54): - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_rebuild_with_no_keypair_name(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.54') - def test_rebuild_with_keypair_unset(self): self.server.key_name = 'mykey' arglist = [ self.server.id, - '--key-unset', + '--no-key-name', ] verifylist = [ ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.compute.api_version = 2.54 - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.54): - self.cmd.take_action(parsed_args) - args = ( - self.image, - None, - ) - kwargs = dict( - key_name=None, - ) - self.servers_mock.get.assert_called_with(self.server.id) - self.get_image_mock.assert_called_with(self.image.id) - self.server.rebuild.assert_called_with(*args, **kwargs) + self.cmd.take_action(parsed_args) + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, key_name=None) - def test_rebuild_with_key_name_and_unset(self): + def test_rebuild_with_keypair_name_and_unset(self): self.server.key_name = 'mykey' arglist = [ self.server.id, '--key-name', self.server.key_name, - '--key-unset', + '--no-key-name', ] verifylist = [ ('server', self.server.id), ('key_name', self.server.key_name) ] - self.assertRaises(utils.ParserException, - self.check_parser, - self.cmd, arglist, verifylist) + self.assertRaises( + utils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + @mock.patch('openstackclient.compute.v2.server.io.open') + def test_rebuild_with_user_data(self, mock_open): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.57') + + mock_file = mock.Mock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = '#!/bin/sh' + + arglist = [ + self.server.id, + '--user-data', 'userdata.sh', + ] + verifylist = [ + ('server', self.server.id), + ('user_data', 'userdata.sh'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Ensure the userdata file is opened + mock_open.assert_called_with('userdata.sh') + + # Ensure the userdata file is closed + mock_file.close.assert_called_with() + + self.servers_mock.get.assert_called_with(self.server.id) + self.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, + userdata=mock_file,) + + def test_rebuild_with_user_data_pre_257(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.56') + + arglist = [ + self.server.id, + '--user-data', 'userdata.sh', + ] + verifylist = [ + ('server', self.server.id), + ('user_data', 'userdata.sh'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_rebuild_with_no_user_data(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.54') + + self.server.key_name = 'mykey' + arglist = [ + self.server.id, + '--no-user-data', + ] + verifylist = [ + ('server', self.server.id), + ('no_user_data', True), + ] + 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.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, userdata=None) + + def test_rebuild_with_no_user_data_pre_254(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.53') + + arglist = [ + self.server.id, + '--no-user-data', + ] + verifylist = [ + ('server', self.server.id), + ('no_user_data', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_rebuild_with_user_data_and_unset(self): + arglist = [ + self.server.id, + '--user-data', 'userdata.sh', + '--no-user-data', + ] + self.assertRaises( + utils.ParserException, + self.check_parser, + self.cmd, arglist, None) + + def test_rebuild_with_trusted_image_cert(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.63') + + arglist = [ + self.server.id, + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + ] + verifylist = [ + ('server', self.server.id), + ('trusted_image_certs', ['foo', 'bar']), + ] + 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.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, trusted_image_certificates=['foo', 'bar']) + + def test_rebuild_with_trusted_image_cert_pre_v263(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.62') + + arglist = [ + self.server.id, + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + ] + verifylist = [ + ('server', self.server.id), + ('trusted_image_certs', ['foo', 'bar']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_rebuild_with_no_trusted_image_cert(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.63') + + arglist = [ + self.server.id, + '--no-trusted-image-certs', + ] + verifylist = [ + ('server', self.server.id), + ('no_trusted_image_certs', True), + ] + 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.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, trusted_image_certificates=None) + + def test_rebuild_with_no_trusted_image_cert_pre_257(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.62') + + arglist = [ + self.server.id, + '--no-trusted-image-certs', + ] + verifylist = [ + ('server', self.server.id), + ('no_trusted_image_certs', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) class TestEvacuateServer(TestServer): diff --git a/releasenotes/notes/add-missing-server-rebuild-opts-5c75e838d8f0487d.yaml b/releasenotes/notes/add-missing-server-rebuild-opts-5c75e838d8f0487d.yaml new file mode 100644 index 0000000000..81e0eabb7d --- /dev/null +++ b/releasenotes/notes/add-missing-server-rebuild-opts-5c75e838d8f0487d.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Add a number of additional options to the ``server rebuild`` command: + + - ``--name`` + - ``--preserve-ephemeral``, ``--no-preserve-ephemeral`` + - ``--user-data``, ``--no-user-data`` + - ``--trusted-image-cert``, ``--no-trusted-image-certs`` +upgrade: + - | + The ``--key-unset`` option of the ``server rebuild`` command has been + replaced by ``--no-key-name``. An alias is provided. From 054562238d31757b023da9a6a566f7970527ca4c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 14:58:38 +0000 Subject: [PATCH 0327/1120] trivial: Cleanup docs for 'server list' Change-Id: I2f2033a8d49ee42eb21696a9cd28e63ad9712fad Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 142 ++++++++++++++++----------- 1 file changed, 85 insertions(+), 57 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c9fbc42256..78e118ea43 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1383,9 +1383,11 @@ def get_parser(self, prog_name): parser.add_argument( '--ip6', metavar='', - help=_('Regular expression to match IPv6 addresses. Note ' - 'that this option only applies for non-admin users ' - 'when using ``--os-compute-api-version`` 2.5 or greater.'), + help=_( + 'Regular expression to match IPv6 addresses. Note ' + 'that this option only applies for non-admin users ' + 'when using ``--os-compute-api-version`` 2.5 or greater.' + ), ) parser.add_argument( '--name', @@ -1436,6 +1438,12 @@ def get_parser(self, prog_name): help=_('Search by user (admin only) (name or ID)'), ) identity_common.add_user_domain_option_to_parser(parser) + parser.add_argument( + '--deleted', + action='store_true', + default=False, + help=_('Only display deleted servers (admin only)'), + ) parser.add_argument( '--long', action='store_true', @@ -1447,74 +1455,83 @@ def get_parser(self, prog_name): '-n', '--no-name-lookup', action='store_true', default=False, - help=_('Skip flavor and image name lookup.' - 'Mutually exclusive with "--name-lookup-one-by-one"' - ' option.'), + help=_( + 'Skip flavor and image name lookup. ' + 'Mutually exclusive with "--name-lookup-one-by-one" option.' + ), ) name_lookup_group.add_argument( '--name-lookup-one-by-one', action='store_true', default=False, - help=_('When looking up flavor and image names, look them up' - 'one by one as needed instead of all together (default). ' - 'Mutually exclusive with "--no-name-lookup|-n" option.'), + help=_( + 'When looking up flavor and image names, look them up' + 'one by one as needed instead of all together (default). ' + 'Mutually exclusive with "--no-name-lookup|-n" option.' + ), ) parser.add_argument( '--marker', metavar='', default=None, - help=_('The last server of the previous page. Display ' - 'list of servers after marker. Display all servers if not ' - 'specified. When used with ``--deleted``, the marker must ' - 'be an ID, otherwise a name or ID can be used.'), + help=_( + 'The last server of the previous page. Display ' + 'list of servers after marker. Display all servers if not ' + 'specified. When used with ``--deleted``, the marker must ' + 'be an ID, otherwise a name or ID can be used.' + ), ) parser.add_argument( '--limit', metavar='', type=int, default=None, - help=_("Maximum number of servers to display. If limit equals -1, " - "all servers will be displayed. If limit is greater than " - "'osapi_max_limit' option of Nova API, " - "'osapi_max_limit' will be used instead."), - ) - parser.add_argument( - '--deleted', - action="store_true", - default=False, - help=_('Only display deleted servers (Admin only).') + help=_( + "Maximum number of servers to display. If limit equals -1, " + "all servers will be displayed. If limit is greater than " + "'osapi_max_limit' option of Nova API, " + "'osapi_max_limit' will be used instead." + ), ) parser.add_argument( '--changes-before', metavar='', default=None, - help=_("List only servers changed before a certain point of time. " - "The provided time should be an ISO 8061 formatted time " - "(e.g., 2016-03-05T06:27:59Z). " - "(Supported by API versions '2.66' - '2.latest')") + help=_( + "List only servers changed before a certain point of time. " + "The provided time should be an ISO 8061 formatted time " + "(e.g., 2016-03-05T06:27:59Z). " + '(supported by --os-compute-api-version 2.66 or above)' + ), ) parser.add_argument( '--changes-since', metavar='', default=None, - help=_("List only servers changed after a certain point of time." - " The provided time should be an ISO 8061 formatted time" - " (e.g., 2016-03-04T06:27:59Z).") + help=_( + "List only servers changed after a certain point of time. " + "The provided time should be an ISO 8061 formatted time " + "(e.g., 2016-03-04T06:27:59Z)." + ), ) lock_group = parser.add_mutually_exclusive_group() lock_group.add_argument( '--locked', action='store_true', default=False, - help=_('Only display locked servers. ' - 'Requires ``--os-compute-api-version`` 2.73 or greater.'), + help=_( + 'Only display locked servers ' + '(supported by --os-compute-api-version 2.73 or above)' + ), ) lock_group.add_argument( '--unlocked', action='store_true', default=False, - help=_('Only display unlocked servers. ' - 'Requires ``--os-compute-api-version`` 2.73 or greater.'), + help=_( + 'Only display unlocked servers ' + '(supported by --os-compute-api-version 2.73 or above)' + ), ) parser.add_argument( '--tags', @@ -1616,18 +1633,25 @@ def take_action(self, parsed_args): search_opts['not-tags'] = parsed_args.not_tags - support_locked = (compute_client.api_version >= - api_versions.APIVersion('2.73')) - if not support_locked and (parsed_args.locked or parsed_args.unlocked): - msg = _('--os-compute-api-version 2.73 or greater is required to ' - 'use the (un)locked filter option.') - raise exceptions.CommandError(msg) - elif support_locked: - # Only from 2.73. - if parsed_args.locked: - search_opts['locked'] = True - if parsed_args.unlocked: - search_opts['locked'] = False + if parsed_args.locked: + if compute_client.api_version < api_versions.APIVersion('2.73'): + msg = _( + '--os-compute-api-version 2.73 or greater is required to ' + 'support the --locked option' + ) + raise exceptions.CommandError(msg) + + search_opts['locked'] = True + elif parsed_args.unlocked: + if compute_client.api_version < api_versions.APIVersion('2.73'): + msg = _( + '--os-compute-api-version 2.73 or greater is required to ' + 'support the --unlocked option' + ) + raise exceptions.CommandError(msg) + + search_opts['locked'] = False + LOG.debug('search options: %s', search_opts) if search_opts['changes-before']: @@ -1834,17 +1858,21 @@ def take_action(self, parsed_args): s.flavor_name = '' s.flavor_id = '' - table = (column_headers, - (utils.get_item_properties( - s, columns, - mixed_case_fields=mixed_case_fields, - formatters={ - 'OS-EXT-STS:power_state': - _format_servers_list_power_state, - 'Networks': _format_servers_list_networks, - 'Metadata': utils.format_dict, - }, - ) for s in data)) + table = ( + column_headers, + ( + utils.get_item_properties( + s, columns, + mixed_case_fields=mixed_case_fields, + formatters={ + 'OS-EXT-STS:power_state': + _format_servers_list_power_state, + 'Networks': _format_servers_list_networks, + 'Metadata': utils.format_dict, + }, + ) for s in data + ), + ) return table From 3c80b1b3b29307381b869f6a767e14e9d47e212f Mon Sep 17 00:00:00 2001 From: okozachenko Date: Thu, 10 Dec 2020 20:22:55 +0200 Subject: [PATCH 0328/1120] Add project field in image list subcommand The motivation is to filter the image by owner Change-Id: I1f08da175a06e62a844f76b0ec18cb3332efef86 --- openstackclient/image/v2/image.py | 15 +++++++++++++++ openstackclient/tests/unit/image/v2/test_image.py | 15 +++++++++++++++ ...e-project-filter-support-ed6204391e503adf.yaml | 5 +++++ 3 files changed, 35 insertions(+) create mode 100644 releasenotes/notes/add-image-project-filter-support-ed6204391e503adf.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 58d92f5165..fa7f4be562 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -591,6 +591,12 @@ def get_parser(self, prog_name): "The supported options are: %s. ") % ', '.join(MEMBER_STATUS_CHOICES)) ) + parser.add_argument( + '--project', + metavar='', + help=_("Search by project (admin only) (name or ID)") + ) + common.add_project_domain_option_to_parser(parser) parser.add_argument( '--tag', metavar='', @@ -636,6 +642,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity image_client = self.app.client_manager.image kwargs = {} @@ -659,6 +666,14 @@ def take_action(self, parsed_args): kwargs['member_status'] = parsed_args.member_status if parsed_args.tag: kwargs['tag'] = parsed_args.tag + project_id = None + if parsed_args.project: + project_id = common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + kwargs['owner'] = project_id if parsed_args.long: columns = ( 'ID', diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index b72e983599..ebeb8353a5 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -769,6 +769,21 @@ def test_image_list_limit_option(self): self.assertEqual(self.columns, columns) self.assertEqual(ret_limit, len(tuple(data))) + def test_image_list_project_option(self): + self.client.find_image = mock.Mock(return_value=self._image) + arglist = [ + '--project', 'nova', + ] + verifylist = [ + ('project', 'nova'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.datalist, tuple(data)) + @mock.patch('osc_lib.utils.find_resource') def test_image_list_marker_option(self, fr_mock): # tangchen: Since image_fakes.IMAGE is a dict, it cannot offer a .id diff --git a/releasenotes/notes/add-image-project-filter-support-ed6204391e503adf.yaml b/releasenotes/notes/add-image-project-filter-support-ed6204391e503adf.yaml new file mode 100644 index 0000000000..e909803c93 --- /dev/null +++ b/releasenotes/notes/add-image-project-filter-support-ed6204391e503adf.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--project`` and ``--project-domain``options to ``image list`` + command to filter by owner. From 29a7c9afce32e7acbd40b9e4a73140ef9e05e7f3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 22 Dec 2020 16:46:33 +0000 Subject: [PATCH 0329/1120] image: Unset properties rather than setting to None Currently, we attempt to unset an image property by setting it to None. This doesn't work for known properties and is rightly rejected by the Glance API with the following error: BadRequestException: 400: Client Error for url: http://172.20.4.87/image/v2/images/368c5751-2b0b-4a38-a255-fd146fe52d31, Bad Request The solution is to actually unset the field by deleting it. Change-Id: Ie156bedbe0f9244f82c81401679706f484caf9aa Signed-off-by: Stephen Finucane Story: #2008463 Task: #41493 --- openstackclient/image/v2/image.py | 2 +- .../tests/functional/image/v2/test_image.py | 2 ++ .../tests/unit/image/v2/test_image.py | 29 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 58d92f5165..6081152217 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -1166,7 +1166,7 @@ def take_action(self, parsed_args): if parsed_args.properties: for k in parsed_args.properties: if k in image: - kwargs[k] = None + delattr(image, k) elif k in image.properties: # Since image is an "evil" object from SDK POV we need to # pass modified properties object, so that SDK can figure diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py index 264ba5199c..0a3a73602c 100644 --- a/openstackclient/tests/functional/image/v2/test_image.py +++ b/openstackclient/tests/functional/image/v2/test_image.py @@ -119,6 +119,7 @@ def test_image_attributes(self): 'image set ' + '--property a=b ' + '--property c=d ' + + '--property hw_rng_model=virtio ' + '--public ' + self.name ) @@ -133,6 +134,7 @@ def test_image_attributes(self): 'image unset ' + '--property a ' + '--property c ' + + '--property hw_rng_model ' + self.name ) json_output = json.loads(self.openstack( diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index b72e983599..8088e15e41 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1501,15 +1501,16 @@ def test_image_show_human_readable(self): class TestImageUnset(TestImage): - attrs = {} - attrs['tags'] = ['test'] - attrs['prop'] = 'test' - attrs['prop2'] = 'fake' - image = image_fakes.FakeImage.create_one_image(attrs) - def setUp(self): super(TestImageUnset, self).setUp() + attrs = {} + attrs['tags'] = ['test'] + attrs['hw_rng_model'] = 'virtio' + attrs['prop'] = 'test' + attrs['prop2'] = 'fake' + self.image = image_fakes.FakeImage.create_one_image(attrs) + self.client.find_image.return_value = self.image self.client.remove_tag.return_value = self.image self.client.update_image.return_value = self.image @@ -1552,22 +1553,20 @@ def test_image_unset_tag_option(self): def test_image_unset_property_option(self): arglist = [ + '--property', 'hw_rng_model', '--property', 'prop', self.image.id, ] verifylist = [ - ('properties', ['prop']), + ('properties', ['hw_rng_model', 'prop']), ('image', self.image.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - kwargs = {} self.client.update_image.assert_called_with( - self.image, - properties={'prop2': 'fake'}, - **kwargs) + self.image, properties={'prop2': 'fake'}) self.assertIsNone(result) @@ -1575,23 +1574,21 @@ def test_image_unset_mixed_option(self): arglist = [ '--tag', 'test', + '--property', 'hw_rng_model', '--property', 'prop', self.image.id, ] verifylist = [ ('tags', ['test']), - ('properties', ['prop']), + ('properties', ['hw_rng_model', 'prop']), ('image', self.image.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - kwargs = {} self.client.update_image.assert_called_with( - self.image, - properties={'prop2': 'fake'}, - **kwargs) + self.image, properties={'prop2': 'fake'}) self.client.remove_tag.assert_called_with( self.image.id, 'test' From f57e10b903fa71d02a6e104717824d004178bbf5 Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Tue, 18 Aug 2020 17:08:40 -0500 Subject: [PATCH 0330/1120] Support Neutron Address Group CRUD Add support for Neutron address group CRUD operations. Subsequent patches will be added to use address groups in security group rules. Change-Id: I3c313fc9329837dde67815901528a34dca98ebcc Implements: blueprint address-groups-in-sg-rules Depends-On: https://review.opendev.org/738274 Depends-On: https://review.opendev.org/745594 --- .../cli/command-objects/address-group.rst | 12 + openstackclient/network/v2/address_group.py | 296 +++++++++++ .../network/v2/test_address_group.py | 177 ++++++ .../tests/unit/network/v2/fakes.py | 73 +++ .../unit/network/v2/test_address_group.py | 503 ++++++++++++++++++ ...s-groups-in-sg-rules-e0dc7e889e107799.yaml | 7 + setup.cfg | 7 + 7 files changed, 1075 insertions(+) create mode 100644 doc/source/cli/command-objects/address-group.rst create mode 100644 openstackclient/network/v2/address_group.py create mode 100644 openstackclient/tests/functional/network/v2/test_address_group.py create mode 100644 openstackclient/tests/unit/network/v2/test_address_group.py create mode 100644 releasenotes/notes/bp-address-groups-in-sg-rules-e0dc7e889e107799.yaml diff --git a/doc/source/cli/command-objects/address-group.rst b/doc/source/cli/command-objects/address-group.rst new file mode 100644 index 0000000000..c1ff6f8858 --- /dev/null +++ b/doc/source/cli/command-objects/address-group.rst @@ -0,0 +1,12 @@ +============= +address group +============= + +An **address group** is a group of IPv4 or IPv6 address blocks which could be +referenced as a remote source or destination when creating a security group +rule. + +Network v2 + +.. autoprogram-cliff:: openstack.network.v2 + :command: address group * diff --git a/openstackclient/network/v2/address_group.py b/openstackclient/network/v2/address_group.py new file mode 100644 index 0000000000..fe1e14a316 --- /dev/null +++ b/openstackclient/network/v2/address_group.py @@ -0,0 +1,296 @@ +# 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. +# + +"""Address group action implementations""" + +import logging + +import netaddr +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _format_addresses(addresses): + ret = [] + for addr in addresses: + ret.append(str(netaddr.IPNetwork(addr))) + return ret + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + attrs['name'] = parsed_args.name + if parsed_args.description: + attrs['description'] = parsed_args.description + attrs['addresses'] = _format_addresses(parsed_args.address) + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +class CreateAddressGroup(command.ShowOne): + _description = _("Create a new Address Group") + + def get_parser(self, prog_name): + parser = super(CreateAddressGroup, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar="", + help=_("New address group name") + ) + parser.add_argument( + '--description', + metavar="", + help=_("New address group description") + ) + parser.add_argument( + "--address", + metavar="", + action='append', + default=[], + help=_("IP address or CIDR " + "(repeat option to set multiple addresses)"), + ) + parser.add_argument( + '--project', + metavar="", + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + + obj = client.create_address_group(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +class DeleteAddressGroup(command.Command): + _description = _("Delete address group(s)") + + def get_parser(self, prog_name): + parser = super(DeleteAddressGroup, self).get_parser(prog_name) + parser.add_argument( + 'address_group', + metavar="", + nargs='+', + help=_("Address group(s) to delete (name or ID)") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + for group in parsed_args.address_group: + try: + obj = client.find_address_group(group, ignore_missing=False) + client.delete_address_group(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete address group with " + "name or ID '%(group)s': %(e)s"), + {'group': group, 'e': e}) + + if result > 0: + total = len(parsed_args.address_group) + msg = (_("%(result)s of %(total)s address groups failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListAddressGroup(command.Lister): + _description = _("List address groups") + + def get_parser(self, prog_name): + parser = super(ListAddressGroup, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='', + help=_("List only address groups of given name in output") + ) + parser.add_argument( + '--project', + metavar="", + help=_("List address groups according to their project " + "(name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'id', + 'name', + 'description', + 'project_id', + 'addresses', + ) + column_headers = ( + 'ID', + 'Name', + 'Description', + 'Project', + 'Addresses', + ) + attrs = {} + if parsed_args.name: + attrs['name'] = parsed_args.name + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + attrs['project_id'] = project_id + data = client.address_groups(**attrs) + + return (column_headers, + (utils.get_item_properties( + s, columns, formatters={}, + ) for s in data)) + + +class SetAddressGroup(command.Command): + _description = _("Set address group properties") + + def get_parser(self, prog_name): + parser = super(SetAddressGroup, self).get_parser(prog_name) + parser.add_argument( + 'address_group', + metavar="", + help=_("Address group to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar="", + help=_('Set address group name') + ) + parser.add_argument( + '--description', + metavar="", + help=_('Set address group description') + ) + parser.add_argument( + "--address", + metavar="", + action='append', + default=[], + help=_("IP address or CIDR " + "(repeat option to set multiple addresses)"), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_address_group( + parsed_args.address_group, + ignore_missing=False) + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if attrs: + client.update_address_group(obj, **attrs) + if parsed_args.address: + client.add_addresses_to_address_group( + obj, _format_addresses(parsed_args.address)) + + +class ShowAddressGroup(command.ShowOne): + _description = _("Display address group details") + + def get_parser(self, prog_name): + parser = super(ShowAddressGroup, self).get_parser(prog_name) + parser.add_argument( + 'address_group', + metavar="", + help=_("Address group to display (name or ID)") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_address_group( + parsed_args.address_group, + ignore_missing=False) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +class UnsetAddressGroup(command.Command): + _description = _("Unset address group properties") + + def get_parser(self, prog_name): + parser = super(UnsetAddressGroup, self).get_parser(prog_name) + parser.add_argument( + 'address_group', + metavar="", + help=_("Address group to modify (name or ID)") + ) + parser.add_argument( + "--address", + metavar="", + action='append', + default=[], + help=_("IP address or CIDR " + "(repeat option to unset multiple addresses)"), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_address_group( + parsed_args.address_group, + ignore_missing=False) + if parsed_args.address: + client.remove_addresses_from_address_group( + obj, _format_addresses(parsed_args.address)) diff --git a/openstackclient/tests/functional/network/v2/test_address_group.py b/openstackclient/tests/functional/network/v2/test_address_group.py new file mode 100644 index 0000000000..52c628a36c --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_address_group.py @@ -0,0 +1,177 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import uuid + +from openstackclient.tests.functional.network.v2 import common + + +class AddressGroupTests(common.NetworkTests): + """Functional tests for address group""" + + def setUp(self): + super(AddressGroupTests, self).setUp() + # Nothing in this class works with Nova Network + if not self.haz_network: + self.skipTest("No Network service present") + if not self.is_extension_enabled('address-group'): + self.skipTest("No address-group extension present") + + def test_address_group_create_and_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address group create -f json ' + + name1 + )) + self.assertEqual( + name1, + cmd_output['name'], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address group create -f json ' + + name2 + )) + self.assertEqual( + name2, + cmd_output['name'], + ) + + raw_output = self.openstack( + 'address group delete ' + name1 + ' ' + name2, + ) + self.assertOutput('', raw_output) + + def test_address_group_list(self): + """Test create, list filters, delete""" + # Get project IDs + cmd_output = json.loads(self.openstack('token issue -f json ')) + auth_project_id = cmd_output['project_id'] + + cmd_output = json.loads(self.openstack('project list -f json ')) + admin_project_id = None + demo_project_id = None + for p in cmd_output: + if p['Name'] == 'admin': + admin_project_id = p['ID'] + if p['Name'] == 'demo': + demo_project_id = p['ID'] + + # Verify assumptions: + # * admin and demo projects are present + # * demo and admin are distinct projects + # * tests run as admin + self.assertIsNotNone(admin_project_id) + self.assertIsNotNone(demo_project_id) + self.assertNotEqual(admin_project_id, demo_project_id) + self.assertEqual(admin_project_id, auth_project_id) + + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address group create -f json ' + + name1 + )) + self.addCleanup(self.openstack, 'address group delete ' + name1) + self.assertEqual( + admin_project_id, + cmd_output["project_id"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address group create -f json ' + + '--project ' + demo_project_id + + ' ' + name2 + )) + self.addCleanup(self.openstack, 'address group delete ' + name2) + self.assertEqual( + demo_project_id, + cmd_output["project_id"], + ) + + # Test list + cmd_output = json.loads(self.openstack( + 'address group list -f json ', + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --project + cmd_output = json.loads(self.openstack( + 'address group list -f json ' + + '--project ' + demo_project_id + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'address group list -f json ' + + '--name ' + name1 + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + def test_address_group_set_unset_and_show(self): + """Tests create options, set, unset, and show""" + name = uuid.uuid4().hex + newname = name + "_" + cmd_output = json.loads(self.openstack( + 'address group create -f json ' + + '--description aaaa ' + + '--address 10.0.0.1 --address 2001::/16 ' + + name + )) + self.addCleanup(self.openstack, 'address group delete ' + newname) + self.assertEqual(name, cmd_output['name']) + self.assertEqual('aaaa', cmd_output['description']) + self.assertEqual(2, len(cmd_output['addresses'])) + + # Test set name, description and address + raw_output = self.openstack( + 'address group set ' + + '--name ' + newname + ' ' + + '--description bbbb ' + + '--address 10.0.0.2 --address 192.0.0.0/8 ' + + name, + ) + self.assertOutput('', raw_output) + + # Show the updated address group + cmd_output = json.loads(self.openstack( + 'address group show -f json ' + + newname, + )) + self.assertEqual(newname, cmd_output['name']) + self.assertEqual('bbbb', cmd_output['description']) + self.assertEqual(4, len(cmd_output['addresses'])) + + # Test unset address + raw_output = self.openstack( + 'address group unset ' + + '--address 10.0.0.1 --address 2001::/16 ' + + '--address 10.0.0.2 --address 192.0.0.0/8 ' + + newname, + ) + self.assertEqual('', raw_output) + + cmd_output = json.loads(self.openstack( + 'address group show -f json ' + + newname, + )) + self.assertEqual(0, len(cmd_output['addresses'])) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 2db83d3b98..798cfd967b 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -83,6 +83,79 @@ def setUp(self): ) +class FakeAddressGroup(object): + """Fake one or more address groups.""" + + @staticmethod + def create_one_address_group(attrs=None): + """Create a fake address group. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + address_group_attrs = { + 'name': 'address-group-name-' + uuid.uuid4().hex, + 'description': 'address-group-description-' + uuid.uuid4().hex, + 'id': 'address-group-id-' + uuid.uuid4().hex, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'addresses': ['10.0.0.1/32'], + } + + # Overwrite default attributes. + address_group_attrs.update(attrs) + + address_group = fakes.FakeResource( + info=copy.deepcopy(address_group_attrs), + loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + address_group.project_id = address_group_attrs['tenant_id'] + + return address_group + + @staticmethod + def create_address_groups(attrs=None, count=2): + """Create multiple fake address groups. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of address groups to fake + :return: + A list of FakeResource objects faking the address groups + """ + address_groups = [] + for i in range(0, count): + address_groups.append( + FakeAddressGroup.create_one_address_group(attrs)) + + return address_groups + + @staticmethod + def get_address_groups(address_groups=None, count=2): + """Get an iterable Mock object with a list of faked address groups. + + If address groups list is provided, then initialize the Mock object + with the list. Otherwise create one. + + :param List address_groups: + A list of FakeResource objects faking address groups + :param int count: + The number of address groups to fake + :return: + An iterable Mock object with side_effect set to a list of faked + address groups + """ + if address_groups is None: + address_groups = FakeAddressGroup.create_address_groups(count) + return mock.Mock(side_effect=address_groups) + + class FakeAddressScope(object): """Fake one or more address scopes.""" diff --git a/openstackclient/tests/unit/network/v2/test_address_group.py b/openstackclient/tests/unit/network/v2/test_address_group.py new file mode 100644 index 0000000000..3b2b1ab6b9 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_address_group.py @@ -0,0 +1,503 @@ +# 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 unittest import mock +from unittest.mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import address_group +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestAddressGroup(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestAddressGroup, self).setUp() + + # Get a shortcut to the network client + self.network = self.app.client_manager.network + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + + +class TestCreateAddressGroup(TestAddressGroup): + + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + # The new address group created. + new_address_group = ( + network_fakes.FakeAddressGroup.create_one_address_group( + attrs={ + 'tenant_id': project.id, + } + )) + columns = ( + 'addresses', + 'description', + 'id', + 'name', + 'project_id', + ) + data = ( + new_address_group.addresses, + new_address_group.description, + new_address_group.id, + new_address_group.name, + new_address_group.project_id, + ) + + def setUp(self): + super(TestCreateAddressGroup, self).setUp() + self.network.create_address_group = mock.Mock( + return_value=self.new_address_group) + + # Get the command object to test + self.cmd = address_group.CreateAddressGroup(self.app, self.namespace) + + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + self.new_address_group.name, + ] + verifylist = [ + ('project', None), + ('name', self.new_address_group.name), + ('description', None), + ('address', []), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_address_group.assert_called_once_with(**{ + 'name': self.new_address_group.name, + 'addresses': [], + }) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--project', self.project.name, + '--project-domain', self.domain.name, + '--address', '10.0.0.1', + '--description', self.new_address_group.description, + self.new_address_group.name, + ] + verifylist = [ + ('project', self.project.name), + ('project_domain', self.domain.name), + ('address', ['10.0.0.1']), + ('description', self.new_address_group.description), + ('name', self.new_address_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_address_group.assert_called_once_with(**{ + 'addresses': ['10.0.0.1/32'], + 'tenant_id': self.project.id, + 'name': self.new_address_group.name, + 'description': self.new_address_group.description, + }) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, data) + + +class TestDeleteAddressGroup(TestAddressGroup): + + # The address group to delete. + _address_groups = ( + network_fakes.FakeAddressGroup.create_address_groups(count=2)) + + def setUp(self): + super(TestDeleteAddressGroup, self).setUp() + self.network.delete_address_group = mock.Mock(return_value=None) + self.network.find_address_group = ( + network_fakes.FakeAddressGroup.get_address_groups( + address_groups=self._address_groups) + ) + + # Get the command object to test + self.cmd = address_group.DeleteAddressGroup(self.app, self.namespace) + + def test_address_group_delete(self): + arglist = [ + self._address_groups[0].name, + ] + verifylist = [ + ('address_group', [self._address_groups[0].name]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.find_address_group.assert_called_once_with( + self._address_groups[0].name, ignore_missing=False) + self.network.delete_address_group.assert_called_once_with( + self._address_groups[0]) + self.assertIsNone(result) + + def test_multi_address_groups_delete(self): + arglist = [] + + for a in self._address_groups: + arglist.append(a.name) + verifylist = [ + ('address_group', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for a in self._address_groups: + calls.append(call(a)) + self.network.delete_address_group.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multi_address_groups_delete_with_exception(self): + arglist = [ + self._address_groups[0].name, + 'unexist_address_group', + ] + verifylist = [ + ('address_group', + [self._address_groups[0].name, 'unexist_address_group']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._address_groups[0], exceptions.CommandError] + self.network.find_address_group = ( + mock.Mock(side_effect=find_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 address groups failed to delete.', str(e)) + + self.network.find_address_group.assert_any_call( + self._address_groups[0].name, ignore_missing=False) + self.network.find_address_group.assert_any_call( + 'unexist_address_group', ignore_missing=False) + self.network.delete_address_group.assert_called_once_with( + self._address_groups[0] + ) + + +class TestListAddressGroup(TestAddressGroup): + + # The address groups to list up. + address_groups = ( + network_fakes.FakeAddressGroup.create_address_groups(count=3)) + columns = ( + 'ID', + 'Name', + 'Description', + 'Project', + 'Addresses', + ) + data = [] + for group in address_groups: + data.append(( + group.id, + group.name, + group.description, + group.project_id, + group.addresses, + )) + + def setUp(self): + super(TestListAddressGroup, self).setUp() + self.network.address_groups = mock.Mock( + return_value=self.address_groups) + + # Get the command object to test + self.cmd = address_group.ListAddressGroup(self.app, self.namespace) + + def test_address_group_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_groups.assert_called_once_with(**{}) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_address_group_list_name(self): + arglist = [ + '--name', self.address_groups[0].name, + ] + verifylist = [ + ('name', self.address_groups[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_groups.assert_called_once_with( + **{'name': self.address_groups[0].name}) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_address_group_list_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_groups.assert_called_once_with( + **{'tenant_id': project.id, 'project_id': project.id}) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_address_group_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.address_groups.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + +class TestSetAddressGroup(TestAddressGroup): + + # The address group to set. + _address_group = network_fakes.FakeAddressGroup.create_one_address_group() + + def setUp(self): + super(TestSetAddressGroup, self).setUp() + self.network.update_address_group = mock.Mock(return_value=None) + self.network.find_address_group = mock.Mock( + return_value=self._address_group) + self.network.add_addresses_to_address_group = mock.Mock( + return_value=self._address_group) + # Get the command object to test + self.cmd = address_group.SetAddressGroup(self.app, self.namespace) + + def test_set_nothing(self): + arglist = [self._address_group.name, ] + verifylist = [ + ('address_group', self._address_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_address_group.assert_not_called() + self.network.add_addresses_to_address_group.assert_not_called() + self.assertIsNone(result) + + def test_set_name_and_description(self): + arglist = [ + '--name', 'new_address_group_name', + '--description', 'new_address_group_description', + self._address_group.name, + ] + verifylist = [ + ('name', 'new_address_group_name'), + ('description', 'new_address_group_description'), + ('address_group', self._address_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'name': "new_address_group_name", + 'description': 'new_address_group_description', + } + self.network.update_address_group.assert_called_with( + self._address_group, **attrs) + self.assertIsNone(result) + + def test_set_one_address(self): + arglist = [ + self._address_group.name, + '--address', '10.0.0.2', + ] + verifylist = [ + ('address_group', self._address_group.name), + ('address', ['10.0.0.2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.add_addresses_to_address_group.assert_called_once_with( + self._address_group, ['10.0.0.2/32']) + self.assertIsNone(result) + + def test_set_multiple_addresses(self): + arglist = [ + self._address_group.name, + '--address', '10.0.0.2', + '--address', '2001::/16', + ] + verifylist = [ + ('address_group', self._address_group.name), + ('address', ['10.0.0.2', '2001::/16']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.add_addresses_to_address_group.assert_called_once_with( + self._address_group, ['10.0.0.2/32', '2001::/16']) + self.assertIsNone(result) + + +class TestShowAddressGroup(TestAddressGroup): + + # The address group to show. + _address_group = network_fakes.FakeAddressGroup.create_one_address_group() + columns = ( + 'addresses', + 'description', + 'id', + 'name', + 'project_id', + ) + data = ( + _address_group.addresses, + _address_group.description, + _address_group.id, + _address_group.name, + _address_group.project_id, + ) + + def setUp(self): + super(TestShowAddressGroup, self).setUp() + self.network.find_address_group = mock.Mock( + return_value=self._address_group) + + # Get the command object to test + self.cmd = address_group.ShowAddressGroup(self.app, self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self._address_group.name, + ] + verifylist = [ + ('address_group', self._address_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_address_group.assert_called_once_with( + self._address_group.name, ignore_missing=False) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + +class TestUnsetAddressGroup(TestAddressGroup): + + # The address group to unset. + _address_group = network_fakes.FakeAddressGroup.create_one_address_group() + + def setUp(self): + super(TestUnsetAddressGroup, self).setUp() + self.network.find_address_group = mock.Mock( + return_value=self._address_group) + self.network.remove_addresses_from_address_group = mock.Mock( + return_value=self._address_group) + # Get the command object to test + self.cmd = address_group.UnsetAddressGroup(self.app, self.namespace) + + def test_unset_nothing(self): + arglist = [self._address_group.name, ] + verifylist = [ + ('address_group', self._address_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.remove_addresses_from_address_group.assert_not_called() + self.assertIsNone(result) + + def test_unset_one_address(self): + arglist = [ + self._address_group.name, + '--address', '10.0.0.2', + ] + verifylist = [ + ('address_group', self._address_group.name), + ('address', ['10.0.0.2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.remove_addresses_from_address_group.\ + assert_called_once_with(self._address_group, ['10.0.0.2/32']) + self.assertIsNone(result) + + def test_unset_multiple_addresses(self): + arglist = [ + self._address_group.name, + '--address', '10.0.0.2', + '--address', '2001::/16', + ] + verifylist = [ + ('address_group', self._address_group.name), + ('address', ['10.0.0.2', '2001::/16']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.remove_addresses_from_address_group.\ + assert_called_once_with(self._address_group, + ['10.0.0.2/32', '2001::/16']) + self.assertIsNone(result) diff --git a/releasenotes/notes/bp-address-groups-in-sg-rules-e0dc7e889e107799.yaml b/releasenotes/notes/bp-address-groups-in-sg-rules-e0dc7e889e107799.yaml new file mode 100644 index 0000000000..74725ad6e4 --- /dev/null +++ b/releasenotes/notes/bp-address-groups-in-sg-rules-e0dc7e889e107799.yaml @@ -0,0 +1,7 @@ +--- +features: + - Add ``address group create``, ``address group delete``, + ``address group list``, ``address group set``, ``address group show`` + and ``address group unset`` commands to support Neutron address group + CRUD operations. + [Blueprint `address-groups-in-sg-rules `_] diff --git a/setup.cfg b/setup.cfg index 9299fb918a..34ab4329ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -377,6 +377,13 @@ openstack.image.v2 = image_unset = openstackclient.image.v2.image:UnsetImage openstack.network.v2 = + address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup + address_group_delete = openstackclient.network.v2.address_group:DeleteAddressGroup + address_group_list = openstackclient.network.v2.address_group:ListAddressGroup + address_group_set = openstackclient.network.v2.address_group:SetAddressGroup + address_group_show = openstackclient.network.v2.address_group:ShowAddressGroup + address_group_unset = openstackclient.network.v2.address_group:UnsetAddressGroup + address_scope_create = openstackclient.network.v2.address_scope:CreateAddressScope address_scope_delete = openstackclient.network.v2.address_scope:DeleteAddressScope address_scope_list = openstackclient.network.v2.address_scope:ListAddressScope From 6f616a29b300238c004b676edd98a5337be38193 Mon Sep 17 00:00:00 2001 From: youngho choi <0505zxc@gmail.com> Date: Mon, 7 Sep 2020 06:35:04 +0900 Subject: [PATCH 0331/1120] Add support '--progress' option for 'image create' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openstack-client doesn’t support the upload progress bar. This patch shows progressbar when create image if you added '--progress' option like a python-glanceclient. like this. [=============================>] 100% +------------------+---------------------------+ | Field | Value | +------------------+---------------------------+ | container_format | bare | | created_at | 2020-09-06T20:44:40Z | ... How to use Add the'--progress' option on the 'openstack image create' command. Code was written by referring to 'python-glanceclient' project on stable/ussuri branch Change-Id: Ic3035b49da10b6555066eee607a14a5b73797c00 task: 40003 story: 2007777 --- openstackclient/common/progressbar.py | 67 ++++++++++++++++ openstackclient/image/v2/image.py | 12 +++ .../tests/unit/common/test_progressbar.py | 77 +++++++++++++++++++ ...ess-option-to-create-1ed1881d58ebad4b.yaml | 5 ++ 4 files changed, 161 insertions(+) create mode 100644 openstackclient/common/progressbar.py create mode 100644 openstackclient/tests/unit/common/test_progressbar.py create mode 100644 releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml diff --git a/openstackclient/common/progressbar.py b/openstackclient/common/progressbar.py new file mode 100644 index 0000000000..ef767a9cf8 --- /dev/null +++ b/openstackclient/common/progressbar.py @@ -0,0 +1,67 @@ +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + + +class _ProgressBarBase(object): + """A progress bar provider for a wrapped obect. + + Base abstract class used by specific class wrapper to show + a progress bar when the wrapped object are consumed. + + :param wrapped: Object to wrap that hold data to be consumed. + :param totalsize: The total size of the data in the wrapped object. + + :note: The progress will be displayed only if sys.stdout is a tty. + """ + + def __init__(self, wrapped, totalsize): + self._wrapped = wrapped + self._totalsize = float(totalsize) + self._show_progress = sys.stdout.isatty() and self._totalsize != 0 + self._percent = 0 + + def _display_progress_bar(self, size_read): + if self._show_progress: + self._percent += size_read / self._totalsize + # Output something like this: [==========> ] 49% + sys.stdout.write('\r[{0:<30}] {1:.0%}'.format( + '=' * int(round(self._percent * 29)) + '>', self._percent + )) + sys.stdout.flush() + + def __getattr__(self, attr): + # Forward other attribute access to the wrapped object. + return getattr(self._wrapped, attr) + + +class VerboseFileWrapper(_ProgressBarBase): + """A file wrapper with a progress bar. + + The file wrapper shows and advances a progress bar whenever the + wrapped file's read method is called. + """ + + def read(self, *args, **kwargs): + data = self._wrapped.read(*args, **kwargs) + if data: + self._display_progress_bar(len(data)) + else: + if self._show_progress: + # Break to a new line from the progress bar for incoming + # output. + sys.stdout.write('\n') + return data diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index fa7f4be562..7177929339 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -30,6 +30,7 @@ from osc_lib import exceptions from osc_lib import utils +from openstackclient.common import progressbar from openstackclient.common import sdk_utils from openstackclient.i18n import _ from openstackclient.identity import common @@ -255,6 +256,12 @@ def get_parser(self, prog_name): help=_("Force image creation if volume is in use " "(only meaningful with --volume)"), ) + parser.add_argument( + "--progress", + action="store_true", + default=False, + help=_("Show upload progress bar."), + ) parser.add_argument( '--sign-key-path', metavar="", @@ -412,6 +419,11 @@ def take_action(self, parsed_args): if fp is None and parsed_args.file: LOG.warning(_("Failed to get an image file.")) return {}, {} + if fp is not None and parsed_args.progress: + filesize = os.path.getsize(fname) + if filesize is not None: + kwargs['validate_checksum'] = False + kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize) elif fname: kwargs['filename'] = fname elif fp: diff --git a/openstackclient/tests/unit/common/test_progressbar.py b/openstackclient/tests/unit/common/test_progressbar.py new file mode 100644 index 0000000000..7bc0b6baf4 --- /dev/null +++ b/openstackclient/tests/unit/common/test_progressbar.py @@ -0,0 +1,77 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import sys + +import six + +from openstackclient.common import progressbar +from openstackclient.tests.unit import utils + + +class TestProgressBarWrapper(utils.TestCase): + + def test_iter_file_display_progress_bar(self): + size = 98304 + file_obj = six.StringIO('X' * size) + saved_stdout = sys.stdout + try: + sys.stdout = output = FakeTTYStdout() + file_obj = progressbar.VerboseFileWrapper(file_obj, size) + chunksize = 1024 + chunk = file_obj.read(chunksize) + while chunk: + chunk = file_obj.read(chunksize) + self.assertEqual( + '[%s>] 100%%\n' % ('=' * 29), + output.getvalue() + ) + finally: + sys.stdout = saved_stdout + + def test_iter_file_no_tty(self): + size = 98304 + file_obj = six.StringIO('X' * size) + saved_stdout = sys.stdout + try: + sys.stdout = output = FakeNoTTYStdout() + file_obj = progressbar.VerboseFileWrapper(file_obj, size) + chunksize = 1024 + chunk = file_obj.read(chunksize) + while chunk: + chunk = file_obj.read(chunksize) + # If stdout is not a tty progress bar should do nothing. + self.assertEqual('', output.getvalue()) + finally: + sys.stdout = saved_stdout + + +class FakeTTYStdout(six.StringIO): + """A Fake stdout that try to emulate a TTY device as much as possible.""" + + def isatty(self): + return True + + def write(self, data): + # When a CR (carriage return) is found reset file. + if data.startswith('\r'): + self.seek(0) + data = data[1:] + return six.StringIO.write(self, data) + + +class FakeNoTTYStdout(FakeTTYStdout): + """A Fake stdout that is not a TTY device.""" + + def isatty(self): + return False diff --git a/releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml b/releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml new file mode 100644 index 0000000000..593c5bd3b8 --- /dev/null +++ b/releasenotes/notes/add-image-progress-option-to-create-1ed1881d58ebad4b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--progress`` option to ``image create`` command to enable a progress + bar when creating and uploading an image. From 03776d82e58622b30b90260ed9c374b0cfc70f2b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Nov 2020 10:14:00 +0000 Subject: [PATCH 0332/1120] compute: Fix 'server * -f yaml' output Make use of 'FormattableColumn'-derived formatters, which provide better output than what we were using before, particularly for the YAML output format. For example, compare before for the 'server show' command: $ openstack --os-compute-api-version 2.79 server show test-server -f yaml ... addresses: private=fdff:77e3:9bb4:0:f816:3eff:fe6d:a944, 10.0.0.44 flavor: disk='1', ephemeral='0', extra_specs.hw_rng:allowed='True', original_name='m1.tiny', ram='512', swap='0', vcpus='1' ... To after: $ openstack --os-compute-api-version 2.79 server show test-server -f yaml ... addresses: private: - fdff:77e3:9bb4:0:f816:3eff:fe6d:a944 - 10.0.0.44 flavor: disk: 1 ephemeral: 0 extra_specs: hw_rng:allowed: 'True' original_name: m1.tiny ram: 512 swap: 0 vcpus: 1 ... Similarly, compare before for 'server list': $ openstack --os-compute-api-version 2.79 server list -f yaml - ... Networks: private=fdff:77e3:9bb4:0:f816:3eff:fe6d:a944, 10.0.0.44 Power State: Running Properties: '' ... To after: $ openstack --os-compute-api-version 2.79 server list -f yaml - ... Networks: private: - fdff:77e3:9bb4:0:f816:3eff:fe6d:a944 - 10.0.0.44 Power State: 1 Properties: {} ... We also fix the human-readable output for the 'tags' field. Before: $ openstack --os-compute-api-version 2.79 server list ... | tags | ['bar', 'foo'] | After: $ openstack --os-compute-api-version 2.79 server list ... | tags | bar, foo | Change-Id: I7a8349106e211c57c4577b75326b39b88bd9ac1e Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 58 +++----- .../functional/compute/v2/test_server.py | 20 ++- .../tests/unit/compute/v2/test_server.py | 126 ++++++++---------- ...proved-server-output-6965b664f6abda8d.yaml | 10 ++ 4 files changed, 99 insertions(+), 115 deletions(-) create mode 100644 releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 2512a8778c..ba0243ef3e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -21,6 +21,7 @@ import logging import os +from cliff import columns as cliff_columns import iso8601 from novaclient import api_versions from novaclient.v2 import servers @@ -41,28 +42,9 @@ IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)' -def _format_servers_list_networks(networks): - """Return a formatted string of a server's networks +class PowerStateColumn(cliff_columns.FormattableColumn): + """Generate a formatted string of a server's power state.""" - :param networks: a Server.networks field - :rtype: a string of formatted network addresses - """ - output = [] - for (network, addresses) in networks.items(): - if not addresses: - continue - addresses_csv = ', '.join(addresses) - group = "%s=%s" % (network, addresses_csv) - output.append(group) - return '; '.join(output) - - -def _format_servers_list_power_state(state): - """Return a formatted string of a server's power state - - :param state: the power state number of a server - :rtype: a string mapped to the power state number - """ power_states = [ 'NOSTATE', # 0x00 'Running', # 0x01 @@ -74,10 +56,11 @@ def _format_servers_list_power_state(state): 'Suspended' # 0x07 ] - try: - return power_states[state] - except Exception: - return 'N/A' + def human_readable(self): + try: + return self.power_states[self._value] + except Exception: + return 'N/A' def _get_ip_address(addresses, address_type, ip_address_family): @@ -169,7 +152,7 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): except Exception: info['flavor'] = flavor_id else: - info['flavor'] = utils.format_dict(flavor_info) + info['flavor'] = format_columns.DictColumn(flavor_info) if 'os-extended-volumes:volumes_attached' in info: info.update( @@ -185,19 +168,15 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): info.pop('security_groups')) } ) + if 'tags' in info: + info.update({'tags': format_columns.ListColumn(info.pop('tags'))}) + # NOTE(dtroyer): novaclient splits these into separate entries... # Format addresses in a useful way - info['addresses'] = _format_servers_list_networks(server.networks) + info['addresses'] = format_columns.DictListColumn(server.networks) # Map 'metadata' field to 'properties' - if not info['metadata']: - info.update( - {'properties': utils.format_dict(info.pop('metadata'))} - ) - else: - info.update( - {'properties': format_columns.DictColumn(info.pop('metadata'))} - ) + info['properties'] = format_columns.DictColumn(info.pop('metadata')) # Migrate tenant_id to project_id naming if 'tenant_id' in info: @@ -205,7 +184,7 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): # Map power state num to meaningful string if 'OS-EXT-STS:power_state' in info: - info['OS-EXT-STS:power_state'] = _format_servers_list_power_state( + info['OS-EXT-STS:power_state'] = PowerStateColumn( info['OS-EXT-STS:power_state']) # Remove values that are long and not too useful @@ -1873,10 +1852,9 @@ def take_action(self, parsed_args): s, columns, mixed_case_fields=mixed_case_fields, formatters={ - 'OS-EXT-STS:power_state': - _format_servers_list_power_state, - 'Networks': _format_servers_list_networks, - 'Metadata': utils.format_dict, + 'OS-EXT-STS:power_state': PowerStateColumn, + 'Networks': format_columns.DictListColumn, + 'Metadata': format_columns.DictColumn, }, ) for s in data ), diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 44d9c61ffb..bad3f93d47 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools import json import time import uuid @@ -346,6 +347,14 @@ def test_server_attach_detach_floating_ip(self): # DevStack without cells. self.skipTest("No Network service present") + def _chain_addresses(addresses): + # Flatten a dict of lists mapping network names to IP addresses, + # returning only the IP addresses + # + # >>> _chain_addresses({'private': ['10.1.0.32', '172.24.5.41']}) + # ['10.1.0.32', '172.24.5.41'] + return itertools.chain(*[*addresses.values()]) + cmd_output = self.server_create() name = cmd_output['name'] self.wait_for_status(name, "ACTIVE") @@ -387,7 +396,7 @@ def test_server_attach_detach_floating_ip(self): 'server show -f json ' + name )) - if floating_ip not in cmd_output['addresses']: + if floating_ip not in _chain_addresses(cmd_output['addresses']): # Hang out for a bit and try again print('retrying floating IP check') wait_time += 10 @@ -397,7 +406,7 @@ def test_server_attach_detach_floating_ip(self): self.assertIn( floating_ip, - cmd_output['addresses'], + _chain_addresses(cmd_output['addresses']), ) # detach ip @@ -417,7 +426,7 @@ def test_server_attach_detach_floating_ip(self): 'server show -f json ' + name )) - if floating_ip in cmd_output['addresses']: + if floating_ip in _chain_addresses(cmd_output['addresses']): # Hang out for a bit and try again print('retrying floating IP check') wait_time += 10 @@ -431,7 +440,7 @@ def test_server_attach_detach_floating_ip(self): )) self.assertNotIn( floating_ip, - cmd_output['addresses'], + _chain_addresses(cmd_output['addresses']), ) def test_server_reboot(self): @@ -856,8 +865,7 @@ def test_server_create_with_none_network(self): server = json.loads(self.openstack( 'server show -f json ' + server_name )) - self.assertIsNotNone(server['addresses']) - self.assertEqual('', server['addresses']) + self.assertEqual({}, server['addresses']) def test_server_create_with_security_group(self): """Test server create with security group ID and name""" diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 1c8541936b..efc105e565 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -22,6 +22,7 @@ import iso8601 from novaclient import api_versions from openstack import exceptions as sdk_exceptions +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils as common_utils @@ -33,6 +34,29 @@ from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes +class TestPowerStateColumn(utils.TestCase): + + def test_human_readable(self): + self.assertEqual( + 'NOSTATE', server.PowerStateColumn(0x00).human_readable()) + self.assertEqual( + 'Running', server.PowerStateColumn(0x01).human_readable()) + self.assertEqual( + '', server.PowerStateColumn(0x02).human_readable()) + self.assertEqual( + 'Paused', server.PowerStateColumn(0x03).human_readable()) + self.assertEqual( + 'Shutdown', server.PowerStateColumn(0x04).human_readable()) + self.assertEqual( + '', server.PowerStateColumn(0x05).human_readable()) + self.assertEqual( + 'Crashed', server.PowerStateColumn(0x06).human_readable()) + self.assertEqual( + 'Suspended', server.PowerStateColumn(0x07).human_readable()) + self.assertEqual( + 'N/A', server.PowerStateColumn(0x08).human_readable()) + + class TestServer(compute_fakes.TestComputev2): def setUp(self): @@ -1015,15 +1039,15 @@ class TestServerCreate(TestServer): def datalist(self): datalist = ( - server._format_servers_list_power_state( + server.PowerStateColumn( getattr(self.new_server, 'OS-EXT-STS:power_state')), - '', + format_columns.DictListColumn({}), self.flavor.name + ' (' + self.new_server.flavor.get('id') + ')', self.new_server.id, self.image.name + ' (' + self.new_server.image.get('id') + ')', self.new_server.name, self.new_server.networks, - '', + format_columns.DictColumn(self.new_server.metadata), ) return datalist @@ -3041,7 +3065,7 @@ def setUp(self): s.id, s.name, s.status, - server._format_servers_list_networks(s.networks), + format_columns.DictListColumn(s.networks), # Image will be an empty string if boot-from-volume self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, @@ -3051,10 +3075,10 @@ def setUp(self): s.name, s.status, getattr(s, 'OS-EXT-STS:task_state'), - server._format_servers_list_power_state( + server.PowerStateColumn( getattr(s, 'OS-EXT-STS:power_state') ), - server._format_servers_list_networks(s.networks), + format_columns.DictListColumn(s.networks), # Image will be an empty string if boot-from-volume self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, @@ -3062,13 +3086,13 @@ def setUp(self): s.flavor['id'], getattr(s, 'OS-EXT-AZ:availability_zone'), getattr(s, 'OS-EXT-SRV-ATTR:host'), - s.Metadata, + format_columns.DictColumn({}), )) self.data_no_name_lookup.append(( s.id, s.name, s.status, - server._format_servers_list_networks(s.networks), + format_columns.DictListColumn(s.networks), # Image will be an empty string if boot-from-volume s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, s.flavor['id'] @@ -3128,7 +3152,7 @@ def test_server_list_long_option(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns_long, columns) - self.assertEqual(tuple(self.data_long), tuple(data)) + self.assertCountEqual(tuple(self.data_long), tuple(data)) def test_server_list_column_option(self): arglist = [ @@ -3429,9 +3453,11 @@ def test_server_with_changes_before_older_version(self): def test_server_list_v269_with_partial_constructs(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.69') + arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # include "partial results" from non-responsive part of # infrastructure. server_dict = { @@ -3454,10 +3480,10 @@ def test_server_list_v269_with_partial_constructs(self): # it will fail at formatting the networks info later on. "networks": {} } - server = compute_fakes.fakes.FakeResource( + _server = compute_fakes.fakes.FakeResource( info=server_dict, ) - self.servers.append(server) + self.servers.append(_server) columns, data = self.cmd.take_action(parsed_args) # get the first three servers out since our interest is in the partial # server. @@ -3466,8 +3492,13 @@ def test_server_list_v269_with_partial_constructs(self): next(data) partial_server = next(data) expected_row = ( - 'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', '', - 'UNKNOWN', '', '', '') + 'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', + '', + 'UNKNOWN', + format_columns.DictListColumn({}), + '', + '', + ) self.assertEqual(expected_row, partial_server) def test_server_list_with_tag(self): @@ -6292,15 +6323,16 @@ def setUp(self): ) self.data = ( - 'Running', - 'public=10.20.30.40, 2001:db8::f', + server.PowerStateColumn( + getattr(self.server, 'OS-EXT-STS:power_state')), + format_columns.DictListColumn(self.server.networks), self.flavor.name + " (" + self.flavor.id + ")", self.server.id, self.image.name + " (" + self.image.id + ")", self.server.name, {'public': ['10.20.30.40', '2001:db8::f']}, 'tenant-id-xxx', - '', + format_columns.DictColumn({}), ) def test_show_no_options(self): @@ -6323,7 +6355,7 @@ def test_show(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_show_embedded_flavor(self): # Tests using --os-compute-api-version >= 2.47 where the flavor @@ -6350,7 +6382,7 @@ def test_show_embedded_flavor(self): self.assertEqual(self.columns, columns) # Since the flavor details are in a dict we can't be sure of the # ordering so just assert that one of the keys is in the output. - self.assertIn('original_name', data[2]) + self.assertIn('original_name', data[2]._value) def test_show_diagnostics(self): arglist = [ @@ -6719,50 +6751,6 @@ def test_get_ip_address(self): self.assertRaises(exceptions.CommandError, server._get_ip_address, self.OLD, 'private', [6]) - def test_format_servers_list_power_state(self): - self.assertEqual("NOSTATE", - server._format_servers_list_power_state(0x00)) - self.assertEqual("Running", - server._format_servers_list_power_state(0x01)) - self.assertEqual("", - server._format_servers_list_power_state(0x02)) - self.assertEqual("Paused", - server._format_servers_list_power_state(0x03)) - self.assertEqual("Shutdown", - server._format_servers_list_power_state(0x04)) - self.assertEqual("", - server._format_servers_list_power_state(0x05)) - self.assertEqual("Crashed", - server._format_servers_list_power_state(0x06)) - self.assertEqual("Suspended", - server._format_servers_list_power_state(0x07)) - self.assertEqual("N/A", - server._format_servers_list_power_state(0x08)) - - def test_format_servers_list_networks(self): - # Setup network info to test. - networks = { - u'public': [u'10.20.30.40', u'2001:db8::f'], - u'private': [u'2001:db8::f', u'10.20.30.40'], - } - - # Prepare expected data. - # Since networks is a dict, whose items are in random order, there - # could be two results after formatted. - data_1 = (u'private=2001:db8::f, 10.20.30.40; ' - u'public=10.20.30.40, 2001:db8::f') - data_2 = (u'public=10.20.30.40, 2001:db8::f; ' - u'private=2001:db8::f, 10.20.30.40') - - # Call _format_servers_list_networks(). - networks_format = server._format_servers_list_networks(networks) - - msg = ('Network string is not formatted correctly.\n' - 'reference = %s or %s\n' - 'actual = %s\n' % - (data_1, data_2, networks_format)) - self.assertIn(networks_format, (data_1, data_2), msg) - @mock.patch('osc_lib.utils.find_resource') def test_prep_server_detail(self, find_resource): # Setup mock method return value. utils.find_resource() will be called @@ -6789,14 +6777,14 @@ def test_prep_server_detail(self, find_resource): info = { 'id': _server.id, 'name': _server.name, - 'addresses': u'public=10.20.30.40, 2001:db8::f', - 'flavor': u'%s (%s)' % (_flavor.name, _flavor.id), - 'image': u'%s (%s)' % (_image.name, _image.id), - 'project_id': u'tenant-id-xxx', - 'properties': '', - 'OS-EXT-STS:power_state': server._format_servers_list_power_state( + 'image': '%s (%s)' % (_image.name, _image.id), + 'flavor': '%s (%s)' % (_flavor.name, _flavor.id), + 'OS-EXT-STS:power_state': server.PowerStateColumn( getattr(_server, 'OS-EXT-STS:power_state')), + 'properties': '', 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}], + 'addresses': format_columns.DictListColumn(_server.networks), + 'project_id': 'tenant-id-xxx', } # Call _prep_server_detail(). @@ -6809,4 +6797,4 @@ def test_prep_server_detail(self, find_resource): server_detail.pop('networks') # Check the results. - self.assertEqual(info, server_detail) + self.assertCountEqual(info, server_detail) diff --git a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml new file mode 100644 index 0000000000..cbe950eace --- /dev/null +++ b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + The ``addresses`` and ``flavor`` fields of the ``server show`` command will + now be correctly rendered as a list of objects and an object, respectively. + - | + The ``networks`` and ``properties`` fields of the ``server list`` command + will now be rendered as objects. In addition, the ``power_state`` field + will now be humanized and rendered as a string value when using the table + formatter. From af5e9d16e8a00c0d382b7090c66df211fefc0b3c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Nov 2020 11:22:05 +0000 Subject: [PATCH 0333/1120] compute: Fix 'usage * -f yaml' output Make use of 'FormattableColumn'-derived formatters, which provide better output than what we were using before, particularly for the YAML output format. Change-Id: Ic770f27cb1f74222636f05350f97400808adffbf Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/usage.py | 103 ++++++++++++++---- .../tests/unit/compute/v2/test_usage.py | 42 +++---- ...proved-server-output-6965b664f6abda8d.yaml | 6 + 3 files changed, 107 insertions(+), 44 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 307c238afe..69fa04e8ba 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -17,7 +17,9 @@ 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 @@ -25,6 +27,57 @@ from openstackclient.i18n import _ +# TODO(stephenfin): This exists in a couple of places and should be moved to a +# common module +class ProjectColumn(cliff_columns.FormattableColumn): + """Formattable column for project column. + + Unlike the parent FormattableColumn class, the initializer of the class + takes project_cache as the second argument. + ``osc_lib.utils.get_item_properties`` instantiates ``FormattableColumn`` + objects with a single parameter, the column value, so you need to pass a + partially initialized class like ``functools.partial(ProjectColumn, + project_cache)`` to use this. + """ + + def __init__(self, value, project_cache=None): + super().__init__(value) + self.project_cache = project_cache or {} + + def human_readable(self): + project = self._value + if not project: + return '' + + if project in self.project_cache.keys(): + return self.project_cache[project].name + + return project + + +class CountColumn(cliff_columns.FormattableColumn): + + def human_readable(self): + return len(self._value) + + +class FloatColumn(cliff_columns.FormattableColumn): + + def human_readable(self): + return float("%.2f" % self._value) + + +def _formatters(project_cache): + return { + 'tenant_id': functools.partial( + ProjectColumn, project_cache=project_cache), + 'server_usages': CountColumn, + 'total_memory_mb_usage': FloatColumn, + 'total_vcpus_usage': FloatColumn, + 'total_local_gb_usage': FloatColumn, + } + + def _get_usage_marker(usage): marker = None if hasattr(usage, 'server_usages') and usage.server_usages: @@ -147,17 +200,15 @@ def _format_project(project): "end": end.strftime(dateformat), }) - return (column_headers, - (utils.get_item_properties( + return ( + column_headers, + ( + utils.get_item_properties( s, columns, - formatters={ - 'tenant_id': _format_project, - 'server_usages': lambda x: len(x), - 'total_memory_mb_usage': lambda x: float("%.2f" % x), - 'total_vcpus_usage': lambda x: float("%.2f" % x), - 'total_local_gb_usage': lambda x: float("%.2f" % x), - }, - ) for s in usage_list)) + formatters=_formatters(project_cache), + ) for s in usage_list + ), + ) class ShowUsage(command.ShowOne): @@ -222,17 +273,21 @@ def take_action(self, parsed_args): "project": project, }) - info = {} - info['Servers'] = ( - len(usage.server_usages) - if hasattr(usage, "server_usages") else None) - info['RAM MB-Hours'] = ( - float("%.2f" % usage.total_memory_mb_usage) - if hasattr(usage, "total_memory_mb_usage") else None) - info['CPU Hours'] = ( - float("%.2f" % usage.total_vcpus_usage) - if hasattr(usage, "total_vcpus_usage") else None) - info['Disk GB-Hours'] = ( - float("%.2f" % usage.total_local_gb_usage) - if hasattr(usage, "total_local_gb_usage") else None) - return zip(*sorted(info.items())) + columns = ( + "tenant_id", + "server_usages", + "total_memory_mb_usage", + "total_vcpus_usage", + "total_local_gb_usage" + ) + column_headers = ( + "Project", + "Servers", + "RAM MB-Hours", + "CPU Hours", + "Disk GB-Hours" + ) + + data = utils.get_item_properties( + usage, columns, formatters=_formatters(None)) + return column_headers, data diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py index c08710256b..bbccb9bdc7 100644 --- a/openstackclient/tests/unit/compute/v2/test_usage.py +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -16,7 +16,7 @@ from novaclient import api_versions -from openstackclient.compute.v2 import usage +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 @@ -49,11 +49,11 @@ class TestUsageList(TestUsage): ) data = [( - usages[0].tenant_id, - len(usages[0].server_usages), - float("%.2f" % usages[0].total_memory_mb_usage), - float("%.2f" % usages[0].total_vcpus_usage), - float("%.2f" % usages[0].total_local_gb_usage), + usage_cmds.ProjectColumn(usages[0].tenant_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), + usage_cmds.FloatColumn(usages[0].total_local_gb_usage), )] def setUp(self): @@ -63,7 +63,7 @@ def setUp(self): self.projects_mock.list.return_value = [self.project] # Get the command object to test - self.cmd = usage.ListUsage(self.app, None) + self.cmd = usage_cmds.ListUsage(self.app, None) def test_usage_list_no_options(self): @@ -79,8 +79,8 @@ def test_usage_list_no_options(self): self.projects_mock.list.assert_called_with() - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), tuple(data)) def test_usage_list_with_options(self): arglist = [ @@ -102,8 +102,8 @@ def test_usage_list_with_options(self): datetime.datetime(2016, 12, 20, 0, 0), detailed=True) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), tuple(data)) def test_usage_list_with_pagination(self): arglist = [] @@ -127,8 +127,8 @@ def test_usage_list_with_pagination(self): mock.call(mock.ANY, mock.ANY, detailed=True, marker=self.usages[0]['server_usages'][0]['instance_id']) ]) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), tuple(data)) class TestUsageShow(TestUsage): @@ -139,17 +139,19 @@ class TestUsageShow(TestUsage): attrs={'tenant_id': project.name}) columns = ( + 'Project', + 'Servers', + 'RAM MB-Hours', 'CPU Hours', 'Disk GB-Hours', - 'RAM MB-Hours', - 'Servers', ) data = ( - float("%.2f" % usage.total_vcpus_usage), - float("%.2f" % usage.total_local_gb_usage), - float("%.2f" % usage.total_memory_mb_usage), - len(usage.server_usages), + usage_cmds.ProjectColumn(usage.tenant_id), + usage_cmds.CountColumn(usage.server_usages), + usage_cmds.FloatColumn(usage.total_memory_mb_usage), + usage_cmds.FloatColumn(usage.total_vcpus_usage), + usage_cmds.FloatColumn(usage.total_local_gb_usage), ) def setUp(self): @@ -159,7 +161,7 @@ def setUp(self): self.projects_mock.get.return_value = self.project # Get the command object to test - self.cmd = usage.ShowUsage(self.app, None) + self.cmd = usage_cmds.ShowUsage(self.app, None) def test_usage_show_no_options(self): diff --git a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml index cbe950eace..7a42fa899d 100644 --- a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml +++ b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml @@ -8,3 +8,9 @@ fixes: will now be rendered as objects. In addition, the ``power_state`` field will now be humanized and rendered as a string value when using the table formatter. + - | + The ``usage list`` and ``usage show`` commands will now display the name + of the project being queried rather than the ID when using the table + formatter. In addition, the ``server_usages``, ``total_memory_mb_usage``, + ``total_vcpus_usage`` and ``total_local_gb_usage`` values will only be + humanized when using the table formatter. From e2a9a9607cc84c0afc2fc5524681a3adebdc68ec Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Nov 2020 11:29:54 +0000 Subject: [PATCH 0334/1120] compute: Fix 'server group * -f yaml' output Make use of 'FormattableColumn'-derived formatters, which provide better output than what we were using before, particularly for the YAML output format. Change-Id: Id6d25a0a348596d5a0430ff7afbf87b049a76bc8 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_group.py | 27 ++++++----- .../compute/v2/test_server_group.py | 12 ++--- .../unit/compute/v2/test_server_group.py | 48 ++++++++++--------- ...proved-server-output-6965b664f6abda8d.yaml | 4 ++ 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index a33632446c..d245c0927f 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -18,6 +18,7 @@ import logging from novaclient import api_versions +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -29,8 +30,8 @@ _formatters = { - 'policies': utils.format_list, - 'members': utils.format_list, + 'policies': format_columns.ListColumn, + 'members': format_columns.ListColumn, } @@ -93,8 +94,8 @@ def take_action(self, parsed_args): info.update(server_group._info) columns = _get_columns(info) - data = utils.get_dict_properties(info, columns, - formatters=_formatters) + data = utils.get_dict_properties( + info, columns, formatters=_formatters) return columns, data @@ -176,14 +177,18 @@ def take_action(self, parsed_args): policy_key, ) - return (column_headers, - (utils.get_item_properties( + return ( + column_headers, + ( + utils.get_item_properties( s, columns, formatters={ - 'Policies': utils.format_list, - 'Members': utils.format_list, + 'Policies': format_columns.ListColumn, + 'Members': format_columns.ListColumn, } - ) for s in data)) + ) for s in data + ), + ) class ShowServerGroup(command.ShowOne): @@ -205,6 +210,6 @@ def take_action(self, parsed_args): info = {} info.update(group._info) columns = _get_columns(info) - data = utils.get_dict_properties(info, columns, - formatters=_formatters) + data = utils.get_dict_properties( + info, columns, formatters=_formatters) return columns, data diff --git a/openstackclient/tests/functional/compute/v2/test_server_group.py b/openstackclient/tests/functional/compute/v2/test_server_group.py index 44ecda1de7..3dff3dcdaf 100644 --- a/openstackclient/tests/functional/compute/v2/test_server_group.py +++ b/openstackclient/tests/functional/compute/v2/test_server_group.py @@ -33,7 +33,7 @@ def test_server_group_delete(self): cmd_output['name'] ) self.assertEqual( - 'affinity', + ['affinity'], cmd_output['policies'] ) @@ -47,7 +47,7 @@ def test_server_group_delete(self): cmd_output['name'] ) self.assertEqual( - 'anti-affinity', + ['anti-affinity'], cmd_output['policies'] ) @@ -74,7 +74,7 @@ def test_server_group_show_and_list(self): cmd_output['name'] ) self.assertEqual( - 'affinity', + ['affinity'], cmd_output['policies'] ) @@ -91,7 +91,7 @@ def test_server_group_show_and_list(self): cmd_output['name'] ) self.assertEqual( - 'anti-affinity', + ['anti-affinity'], cmd_output['policies'] ) @@ -102,5 +102,5 @@ def test_server_group_show_and_list(self): self.assertIn(name1, names) self.assertIn(name2, names) policies = [x["Policies"] for x in cmd_output] - self.assertIn('affinity', policies) - self.assertIn('anti-affinity', policies) + self.assertIn(['affinity'], policies) + self.assertIn(['anti-affinity'], policies) diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index bf0ea0ba45..8de7492c79 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -16,6 +16,7 @@ from unittest import mock from novaclient import api_versions +from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils @@ -39,9 +40,9 @@ class TestServerGroup(compute_fakes.TestComputev2): data = ( fake_server_group.id, - utils.format_list(fake_server_group.members), + format_columns.ListColumn(fake_server_group.members), fake_server_group.name, - utils.format_list(fake_server_group.policies), + format_columns.ListColumn(fake_server_group.policies), fake_server_group.project_id, fake_server_group.user_id, ) @@ -70,7 +71,7 @@ class TestServerGroupV264(TestServerGroup): data = ( fake_server_group.id, - utils.format_list(fake_server_group.members), + format_columns.ListColumn(fake_server_group.members), fake_server_group.name, fake_server_group.policy, fake_server_group.project_id, @@ -105,8 +106,8 @@ def test_server_group_create(self): policies=[parsed_args.policy], ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) def test_server_group_create_with_soft_policies(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -127,8 +128,8 @@ def test_server_group_create_with_soft_policies(self): policies=[parsed_args.policy], ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) def test_server_group_create_with_soft_policies_pre_v215(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -170,8 +171,8 @@ def test_server_group_create_v264(self): policy=parsed_args.policy, ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) class TestServerGroupDelete(TestServerGroup): @@ -275,14 +276,14 @@ class TestServerGroupList(TestServerGroup): list_data = (( TestServerGroup.fake_server_group.id, TestServerGroup.fake_server_group.name, - utils.format_list(TestServerGroup.fake_server_group.policies), + format_columns.ListColumn(TestServerGroup.fake_server_group.policies), ),) list_data_long = (( TestServerGroup.fake_server_group.id, TestServerGroup.fake_server_group.name, - utils.format_list(TestServerGroup.fake_server_group.policies), - utils.format_list(TestServerGroup.fake_server_group.members), + format_columns.ListColumn(TestServerGroup.fake_server_group.policies), + format_columns.ListColumn(TestServerGroup.fake_server_group.members), TestServerGroup.fake_server_group.project_id, TestServerGroup.fake_server_group.user_id, ),) @@ -303,8 +304,8 @@ def test_server_group_list(self): columns, data = self.cmd.take_action(parsed_args) self.server_groups_mock.list.assert_called_once_with(False) - self.assertEqual(self.list_columns, columns) - self.assertEqual(self.list_data, tuple(data)) + self.assertCountEqual(self.list_columns, columns) + self.assertCountEqual(self.list_data, tuple(data)) def test_server_group_list_with_all_projects_and_long(self): arglist = [ @@ -319,8 +320,8 @@ def test_server_group_list_with_all_projects_and_long(self): columns, data = self.cmd.take_action(parsed_args) self.server_groups_mock.list.assert_called_once_with(True) - self.assertEqual(self.list_columns_long, columns) - self.assertEqual(self.list_data_long, tuple(data)) + self.assertCountEqual(self.list_columns_long, columns) + self.assertCountEqual(self.list_data_long, tuple(data)) class TestServerGroupListV264(TestServerGroupV264): @@ -350,7 +351,8 @@ class TestServerGroupListV264(TestServerGroupV264): TestServerGroupV264.fake_server_group.id, TestServerGroupV264.fake_server_group.name, TestServerGroupV264.fake_server_group.policy, - utils.format_list(TestServerGroupV264.fake_server_group.members), + format_columns.ListColumn( + TestServerGroupV264.fake_server_group.members), TestServerGroupV264.fake_server_group.project_id, TestServerGroupV264.fake_server_group.user_id, ),) @@ -373,8 +375,8 @@ def test_server_group_list(self): columns, data = self.cmd.take_action(parsed_args) self.server_groups_mock.list.assert_called_once_with(False) - self.assertEqual(self.list_columns, columns) - self.assertEqual(self.list_data, tuple(data)) + self.assertCountEqual(self.list_columns, columns) + self.assertCountEqual(self.list_data, tuple(data)) def test_server_group_list_with_all_projects_and_long(self): arglist = [ @@ -389,8 +391,8 @@ def test_server_group_list_with_all_projects_and_long(self): columns, data = self.cmd.take_action(parsed_args) self.server_groups_mock.list.assert_called_once_with(True) - self.assertEqual(self.list_columns_long, columns) - self.assertEqual(self.list_data_long, tuple(data)) + self.assertCountEqual(self.list_columns_long, columns) + self.assertCountEqual(self.list_data_long, tuple(data)) class TestServerGroupShow(TestServerGroup): @@ -412,5 +414,5 @@ def test_server_group_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) diff --git a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml index 7a42fa899d..5dc2980c77 100644 --- a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml +++ b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml @@ -14,3 +14,7 @@ fixes: formatter. In addition, the ``server_usages``, ``total_memory_mb_usage``, ``total_vcpus_usage`` and ``total_local_gb_usage`` values will only be humanized when using the table formatter. + - | + The ``policies`` (or ``policy``, on newer microversions) and ``members`` + fields of the ``server group list`` and ``server group show`` commands + will now be rendered correctly as lists. From bf834f6d7582009672fc65aa983f314684dc4380 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 17 Nov 2020 12:33:40 +0000 Subject: [PATCH 0335/1120] compute: Fix 'hypervisor show -f yaml' output The 'cpu_info' field returned by the 'os-hypervisors' API is an object and should be formatted as such. However, this is complicated by the fact that the object in this field is stringified until microversion 2.28 and is only returned as an actual object on later microversions. Handle the conversion from the string for older microversions and display things correctly for all releases. Change-Id: Ide31466cbb9e89c96d6bd542fe039ab5ed1fac1f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/hypervisor.py | 24 ++++++++-- .../tests/unit/compute/v2/test_hypervisor.py | 45 ++++++++++++++++--- ...proved-server-output-6965b664f6abda8d.yaml | 3 ++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 7f110028b6..8fdb66983c 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -15,9 +15,12 @@ """Hypervisor action implementations""" +import json import re +from novaclient import api_versions from novaclient import exceptions as nova_exceptions +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils @@ -86,8 +89,8 @@ def take_action(self, parsed_args): if aggregates: # Hypervisors in nova cells are prefixed by "@" if "@" in hypervisor['service']['host']: - cell, service_host = hypervisor['service']['host'].split('@', - 1) + cell, service_host = hypervisor['service']['host'].split( + '@', 1) else: cell = None service_host = hypervisor['service']['host'] @@ -125,4 +128,19 @@ def take_action(self, parsed_args): hypervisor["service_host"] = hypervisor["service"]["host"] del hypervisor["service"] - return zip(*sorted(hypervisor.items())) + if compute_client.api_version < api_versions.APIVersion('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)) + data = utils.get_dict_properties( + hypervisor, columns, + formatters={ + 'cpu_info': format_columns.DictColumn, + }) + + return (columns, data) diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor.py b/openstackclient/tests/unit/compute/v2/test_hypervisor.py index 7200d04ed8..518109c6b0 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor.py @@ -14,8 +14,11 @@ # import copy +import json +from novaclient import api_versions from novaclient import exceptions as nova_exceptions +from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.compute.v2 import hypervisor @@ -247,7 +250,7 @@ def setUp(self): ) self.data = ( [], - {'aaa': 'aaa'}, + format_columns.DictColumn({'aaa': 'aaa'}), 0, 50, 50, @@ -278,6 +281,35 @@ def setUp(self): self.cmd = hypervisor.ShowHypervisor(self.app, None) def test_hypervisor_show(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.28') + + arglist = [ + self.hypervisor.hypervisor_hostname, + ] + verifylist = [ + ('hypervisor', self.hypervisor.hypervisor_hostname), + ] + 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, columns) + self.assertItemsEqual(self.data, data) + + def test_hypervisor_show_pre_v228(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.27') + + # 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 + arglist = [ self.hypervisor.hypervisor_hostname, ] @@ -292,9 +324,12 @@ def test_hypervisor_show(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.assertItemsEqual(self.data, data) + + def test_hypervisor_show_uptime_not_implemented(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.28') - def test_hyprvisor_show_uptime_not_implemented(self): arglist = [ self.hypervisor.hypervisor_hostname, ] @@ -337,7 +372,7 @@ def test_hyprvisor_show_uptime_not_implemented(self): ) expected_data = ( [], - {'aaa': 'aaa'}, + format_columns.DictColumn({'aaa': 'aaa'}), 0, 50, 50, @@ -361,4 +396,4 @@ def test_hyprvisor_show_uptime_not_implemented(self): ) self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) + self.assertItemsEqual(expected_data, data) diff --git a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml index 5dc2980c77..305e915e61 100644 --- a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml +++ b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml @@ -18,3 +18,6 @@ fixes: The ``policies`` (or ``policy``, on newer microversions) and ``members`` fields of the ``server group list`` and ``server group show`` commands will now be rendered correctly as lists. + - | + The ``cpu_info`` field of the ``hypervisor show`` output is now + correctly decoded and output as an object. From a5c6470f2d76afb9ac87d05c4326bebf925a55da Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Nov 2020 11:49:11 +0000 Subject: [PATCH 0336/1120] compute: Add 'server group create --rule' option This closes the remaining gap with the 2.64 compute API microversion. Change-Id: Ia42b23d813b7af6ddb1a41f4e9bdc8a6160b908c Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_group.py | 70 +++++++++++++------ .../unit/compute/v2/test_server_group.py | 31 +++++++- ...p-create_rule_option-9f84e52f35e7c3ba.yaml | 4 ++ 3 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/server-group-create_rule_option-9f84e52f35e7c3ba.yaml diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index d245c0927f..783fdbfea3 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -19,6 +19,7 @@ from novaclient import api_versions from osc_lib.cli import format_columns +from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -30,8 +31,9 @@ _formatters = { - 'policies': format_columns.ListColumn, 'members': format_columns.ListColumn, + 'policies': format_columns.ListColumn, + 'rules': format_columns.DictColumn, } @@ -68,7 +70,19 @@ def get_parser(self, prog_name): "Add a policy to " "Specify --os-compute-api-version 2.15 or higher for the " "'soft-affinity' or 'soft-anti-affinity' policy." - ) + ), + ) + parser.add_argument( + '--rule', + metavar='', + action=parseractions.KeyValueAction, + default={}, + dest='rules', + help=_( + "A rule for the policy. Currently, only the " + "'max_server_per_host' rule is supported for the " + "'anti-affinity' policy." + ), ) return parser @@ -84,12 +98,24 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg % parsed_args.policy) - policy_arg = {'policies': [parsed_args.policy]} - if compute_client.api_version >= api_versions.APIVersion("2.64"): - policy_arg = {'policy': parsed_args.policy} + if parsed_args.rules: + if compute_client.api_version < api_versions.APIVersion('2.64'): + msg = _( + '--os-compute-api-version 2.64 or greater is required to ' + 'support the --rule option' + ) + raise exceptions.CommandError(msg) + + if compute_client.api_version < api_versions.APIVersion('2.64'): + kwargs = {'policies': [parsed_args.policy]} + else: + kwargs = { + 'policy': parsed_args.policy, + 'rules': parsed_args.rules or None, + } server_group = compute_client.server_groups.create( - name=parsed_args.name, **policy_arg) + name=parsed_args.name, **kwargs) info.update(server_group._info) @@ -161,31 +187,33 @@ def take_action(self, parsed_args): if compute_client.api_version >= api_versions.APIVersion("2.64"): policy_key = 'Policy' + columns = ( + 'id', + 'name', + policy_key.lower(), + ) + column_headers = ( + 'ID', + 'Name', + policy_key, + ) if parsed_args.long: - column_headers = columns = ( - 'ID', - 'Name', - policy_key, + columns += ( + 'members', + 'project_id', + 'user_id', + ) + column_headers += ( 'Members', 'Project Id', 'User Id', ) - else: - column_headers = columns = ( - 'ID', - 'Name', - policy_key, - ) return ( column_headers, ( utils.get_item_properties( - s, columns, - formatters={ - 'Policies': format_columns.ListColumn, - 'Members': format_columns.ListColumn, - } + s, columns, formatters=_formatters, ) for s in data ), ) diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 8de7492c79..732c18810b 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -152,28 +152,55 @@ def test_server_group_create_with_soft_policies_pre_v215(self): '--os-compute-api-version 2.15 or greater is required', str(ex)) - def test_server_group_create_v264(self): + def test_server_group_create_with_rules(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.64') arglist = [ '--policy', 'soft-anti-affinity', + '--rule', 'max_server_per_host=2', 'affinity_group', ] verifylist = [ ('policy', 'soft-anti-affinity'), + ('rules', {'max_server_per_host': '2'}), ('name', 'affinity_group'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.server_groups_mock.create.assert_called_once_with( name=parsed_args.name, - policy=parsed_args.policy, + policy=parsed_args.policy, # should be 'policy', not 'policies' + rules=parsed_args.rules, ) self.assertCountEqual(self.columns, columns) self.assertCountEqual(self.data, data) + def test_server_group_create_with_rules_pre_v264(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.63') + + arglist = [ + '--policy', 'soft-anti-affinity', + '--rule', 'max_server_per_host=2', + 'affinity_group', + ] + verifylist = [ + ('policy', 'soft-anti-affinity'), + ('rules', {'max_server_per_host': '2'}), + ('name', 'affinity_group'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.64 or greater is required', + str(ex)) + class TestServerGroupDelete(TestServerGroup): diff --git a/releasenotes/notes/server-group-create_rule_option-9f84e52f35e7c3ba.yaml b/releasenotes/notes/server-group-create_rule_option-9f84e52f35e7c3ba.yaml new file mode 100644 index 0000000000..29ed3e941d --- /dev/null +++ b/releasenotes/notes/server-group-create_rule_option-9f84e52f35e7c3ba.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for ``--rule`` option for ``server group create``. From f200799848831a00f350c324bb77c00efa50da1c Mon Sep 17 00:00:00 2001 From: Yongli He Date: Mon, 9 Sep 2019 13:56:30 +0800 Subject: [PATCH 0337/1120] compute: Add 'server show --topology' option Add support for compute microversion 2.78 by adding a '--topology' option to 'openstack server show' command that retrieves server NUMA information. Change-Id: Ie22979df2ea9082ca64a4d43b571bd4025684825 --- doc/source/cli/data/nova.csv | 1 + openstackclient/compute/v2/server.py | 46 +++++++++++++---- .../tests/unit/compute/v2/test_server.py | 49 +++++++++++++++++++ ...y-microversion-v2_78-3891fc67f767177e.yaml | 5 ++ 4 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/show-server-topology-microversion-v2_78-3891fc67f767177e.yaml diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 872d6e09b2..2004007f8d 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -121,6 +121,7 @@ start,server start,Start the server(s). stop,server stop,Stop the server(s). suspend,server suspend,Suspend a server. trigger-crash-dump,server dump create,Trigger crash dump in an instance. +topology,openstack server show --topology,Retrieve server NUMA topology. unlock,server unlock,Unlock a server. unpause,server unpause,Unpause a server. unrescue,server unrescue,Restart the server from normal boot disk again. diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ba0243ef3e..c1716e5be1 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3383,7 +3383,8 @@ def take_action(self, parsed_args): class ShowServer(command.ShowOne): _description = _( "Show server details. Specify ``--os-compute-api-version 2.47`` " - "or higher to see the embedded flavor information for the server.") + "or higher to see the embedded flavor information for the server." + ) def get_parser(self, prog_name): parser = super(ShowServer, self).get_parser(prog_name) @@ -3392,18 +3393,29 @@ def get_parser(self, prog_name): metavar='', help=_('Server (name or ID)'), ) - parser.add_argument( + # TODO(stephenfin): This should be a separate command, not a flag + diagnostics_group = parser.add_mutually_exclusive_group() + diagnostics_group.add_argument( '--diagnostics', action='store_true', default=False, help=_('Display server diagnostics information'), ) + diagnostics_group.add_argument( + '--topology', + action='store_true', + default=False, + help=_( + 'Include topology information in the output ' + '(supported by --os-compute-api-version 2.78 or above)' + ), + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - server = utils.find_resource(compute_client.servers, - parsed_args.server) + server = utils.find_resource( + compute_client.servers, parsed_args.server) if parsed_args.diagnostics: (resp, data) = server.diagnostics() @@ -3412,10 +3424,26 @@ def take_action(self, parsed_args): "Error retrieving diagnostics data\n" )) return ({}, {}) - else: - data = _prep_server_detail(compute_client, - self.app.client_manager.image, server, - refresh=False) + return zip(*sorted(data.items())) + + topology = None + if parsed_args.topology: + if compute_client.api_version < api_versions.APIVersion('2.78'): + msg = _( + '--os-compute-api-version 2.78 or greater is required to ' + 'support the --topology option' + ) + raise exceptions.CommandError(msg) + + topology = server.topology() + + data = _prep_server_detail( + compute_client, self.app.client_manager.image, server, + refresh=False) + + if topology: + data['topology'] = format_columns.DictColumn(topology) + return zip(*sorted(data.items())) @@ -3731,7 +3759,7 @@ def get_parser(self, prog_name): help=_( 'Tag to remove from the server. ' 'Specify multiple times to remove multiple tags. ' - '(supported by --os-compute-api-version 2.26 or later' + '(supported by --os-compute-api-version 2.26 or above)' ), ) return parser diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index efc105e565..8c3bf317e0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6285,6 +6285,10 @@ def setUp(self): self.image = image_fakes.FakeImage.create_one_image() self.flavor = compute_fakes.FakeFlavor.create_one_flavor() + self.topology = { + 'nodes': [{'vcpu_set': [0, 1]}, {'vcpu_set': [2, 3]}], + 'pagesize_kb': None, + } server_info = { 'image': {'id': self.image.id}, 'flavor': {'id': self.flavor.id}, @@ -6298,6 +6302,7 @@ def setUp(self): resp.status_code = 200 server_method = { 'diagnostics': (resp, {'test': 'test'}), + 'topology': self.topology, } self.server = compute_fakes.FakeServer.create_one_server( attrs=server_info, methods=server_method) @@ -6348,6 +6353,7 @@ def test_show(self): ] verifylist = [ ('diagnostics', False), + ('topology', False), ('server', self.server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -6365,6 +6371,7 @@ def test_show_embedded_flavor(self): ] verifylist = [ ('diagnostics', False), + ('topology', False), ('server', self.server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -6391,6 +6398,7 @@ def test_show_diagnostics(self): ] verifylist = [ ('diagnostics', True), + ('topology', False), ('server', self.server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -6400,6 +6408,47 @@ def test_show_diagnostics(self): self.assertEqual(('test',), columns) self.assertEqual(('test',), data) + def test_show_topology(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.78') + + arglist = [ + '--topology', + self.server.name, + ] + verifylist = [ + ('diagnostics', False), + ('topology', True), + ('server', self.server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.columns += ('topology',) + self.data += (format_columns.DictColumn(self.topology),) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_show_topology_pre_v278(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.77') + + arglist = [ + '--topology', + self.server.name, + ] + verifylist = [ + ('diagnostics', False), + ('topology', True), + ('server', self.server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + class TestServerStart(TestServer): diff --git a/releasenotes/notes/show-server-topology-microversion-v2_78-3891fc67f767177e.yaml b/releasenotes/notes/show-server-topology-microversion-v2_78-3891fc67f767177e.yaml new file mode 100644 index 0000000000..0b48ed009c --- /dev/null +++ b/releasenotes/notes/show-server-topology-microversion-v2_78-3891fc67f767177e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for ``openstack server show --topology`` flag, which will + include NUMA topology information in the output. From bbf7de83ff83b7c124ff30e421bd3eea6f1a8765 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Nov 2020 12:27:17 +0000 Subject: [PATCH 0338/1120] trivial: Use plural for appended parameters Multiple compute commands take a '--property' parameter or variant thereof. These should be stored in a 'properties' (plural) dest for sanity's sake. Correct this. Change-Id: If393836925fa736404527d9abd212b8ac9931027 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/aggregate.py | 44 +++++++++---------- openstackclient/compute/v2/flavor.py | 21 +++++---- openstackclient/compute/v2/server.py | 37 ++++++++-------- .../tests/unit/compute/v2/test_aggregate.py | 20 ++++----- .../tests/unit/compute/v2/test_flavor.py | 10 ++--- .../tests/unit/compute/v2/test_server.py | 22 +++++----- 6 files changed, 78 insertions(+), 76 deletions(-) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 8b70f42640..e39eb2d272 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -101,6 +101,7 @@ def get_parser(self, prog_name): "--property", metavar="", action=parseractions.KeyValueAction, + dest="properties", help=_("Property to add to this aggregate " "(repeat option to set multiple properties)") ) @@ -116,10 +117,10 @@ def take_action(self, parsed_args): aggregate = compute_client.create_aggregate(**attrs) - if parsed_args.property: + if parsed_args.properties: aggregate = compute_client.set_aggregate_metadata( aggregate.id, - parsed_args.property, + parsed_args.properties, ) display_columns, columns = _get_aggregate_columns(aggregate) @@ -269,12 +270,12 @@ def get_parser(self, prog_name): "--property", metavar="", action=parseractions.KeyValueAction, + dest="properties", help=_("Property to set on " "(repeat option to set multiple properties)") ) parser.add_argument( "--no-property", - dest="no_property", action="store_true", help=_("Remove all properties from " "(specify both --property and --no-property to " @@ -296,21 +297,20 @@ def take_action(self, parsed_args): if kwargs: compute_client.update_aggregate(aggregate.id, **kwargs) - set_property = {} + properties = {} if parsed_args.no_property: # NOTE(RuiChen): "availability_zone" can not be unset from # properties. It is already excluded from show and create output. - set_property.update({key: None - for key in aggregate.metadata.keys() - if key != 'availability_zone'}) - if parsed_args.property: - set_property.update(parsed_args.property) - - if set_property: - compute_client.set_aggregate_metadata( - aggregate.id, - set_property - ) + properties.update({ + key: None for key in aggregate.metadata.keys() + if key != 'availability_zone' + }) + + if parsed_args.properties: + properties.update(parsed_args.properties) + + if properties: + compute_client.set_aggregate_metadata(aggregate.id, properties) class ShowAggregate(command.ShowOne): @@ -354,7 +354,9 @@ def get_parser(self, prog_name): parser.add_argument( "--property", metavar="", - action='append', + action="append", + default=[], + dest="properties", help=_("Property to remove from aggregate " "(repeat option to remove multiple properties)") ) @@ -365,12 +367,10 @@ def take_action(self, parsed_args): aggregate = compute_client.find_aggregate( parsed_args.aggregate, ignore_missing=False) - unset_property = {} - if parsed_args.property: - unset_property.update({key: None for key in parsed_args.property}) - if unset_property: - compute_client.set_aggregate_metadata( - aggregate, unset_property) + properties = {key: None for key in parsed_args.properties} + + if properties: + compute_client.set_aggregate_metadata(aggregate.id, properties) class CacheImageForAggregate(command.Command): diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index fa98e131b1..a55aba2a41 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -128,6 +128,7 @@ def get_parser(self, prog_name): "--property", metavar="", action=parseractions.KeyValueAction, + dest="properties", help=_("Property to add for this flavor " "(repeat option to set multiple properties)") ) @@ -191,12 +192,12 @@ def take_action(self, parsed_args): msg = _("Failed to add project %(project)s access to " "flavor: %(e)s") LOG.error(msg, {'project': parsed_args.project, 'e': e}) - if parsed_args.property: + if parsed_args.properties: try: flavor = compute_client.create_flavor_extra_specs( - flavor, parsed_args.property) + flavor, parsed_args.properties) except Exception as e: - LOG.error(_("Failed to set flavor property: %s"), e) + LOG.error(_("Failed to set flavor properties: %s"), e) display_columns, columns = _get_flavor_columns(flavor) data = utils.get_dict_properties(flavor, columns, @@ -398,6 +399,7 @@ def get_parser(self, prog_name): "--property", metavar="", action=parseractions.KeyValueAction, + dest="properties", help=_("Property to add or modify for this flavor " "(repeat option to set multiple properties)") ) @@ -447,15 +449,15 @@ def take_action(self, parsed_args): compute_client.delete_flavor_extra_specs_property( flavor.id, key) except Exception as e: - LOG.error(_("Failed to clear flavor property: %s"), e) + LOG.error(_("Failed to clear flavor properties: %s"), e) result += 1 - if parsed_args.property: + if parsed_args.properties: try: compute_client.create_flavor_extra_specs( - flavor.id, parsed_args.property) + flavor.id, parsed_args.properties) except Exception as e: - LOG.error(_("Failed to set flavor property: %s"), e) + LOG.error(_("Failed to set flavor properties: %s"), e) result += 1 if parsed_args.project: @@ -537,6 +539,7 @@ def get_parser(self, prog_name): "--property", metavar="", action='append', + dest="properties", help=_("Property to remove from flavor " "(repeat option to unset multiple properties)") ) @@ -563,8 +566,8 @@ def take_action(self, parsed_args): raise exceptions.CommandError(_(e.message)) result = 0 - if parsed_args.property: - for key in parsed_args.property: + if parsed_args.properties: + for key in parsed_args.properties: try: compute_client.delete_flavor_extra_specs_property( flavor.id, key) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c1716e5be1..6cb364fefe 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -614,6 +614,7 @@ def get_parser(self, prog_name): '--image-property', metavar='', action=parseractions.KeyValueAction, + dest='image_properties', help=_("Image property to be matched"), ) disk_group.add_argument( @@ -659,6 +660,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, + dest='properties', help=_( 'Set a property on this server ' '(repeat option to set multiple values)' @@ -886,8 +888,8 @@ def _show_progress(progress): image = image_client.find_image( parsed_args.image, ignore_missing=False) - if not image and parsed_args.image_property: - def emit_duplicated_warning(img, image_property): + if not image and parsed_args.image_properties: + def emit_duplicated_warning(img): img_uuid_list = [str(image.id) for image in img] LOG.warning( 'Multiple matching images: %(img_uuid_list)s\n' @@ -930,9 +932,9 @@ def _match_image(image_api, wanted_properties): return images_matched - images = _match_image(image_client, parsed_args.image_property) + images = _match_image(image_client, parsed_args.image_properties) if len(images) > 1: - emit_duplicated_warning(images, parsed_args.image_property) + emit_duplicated_warning(images, parsed_args.image_properties) if images: image = images[0] else: @@ -1195,7 +1197,7 @@ def _match_image(image_api, wanted_properties): config_drive = parsed_args.config_drive boot_kwargs = dict( - meta=parsed_args.property, + meta=parsed_args.properties, files=files, reservation_id=None, min_count=parsed_args.min, @@ -2473,6 +2475,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, + dest='properties', help=_( 'Set a new property on the rebuilt server ' '(repeat option to set multiple values)' @@ -2614,8 +2617,8 @@ def _show_progress(progress): if parsed_args.preserve_ephemeral is not None: kwargs['preserve_ephemeral'] = parsed_args.preserve_ephemeral - if parsed_args.property: - kwargs['meta'] = parsed_args.property + if parsed_args.properties: + kwargs['meta'] = parsed_args.properties if parsed_args.description: if compute_client.api_version < api_versions.APIVersion('2.19'): @@ -3278,9 +3281,10 @@ def get_parser(self, prog_name): help=_('Set new root password (interactive only)'), ) parser.add_argument( - "--property", - metavar="", + '--property', + metavar='', action=parseractions.KeyValueAction, + dest='properties', help=_('Property to add/change for this server ' '(repeat option to set multiple properties)'), ) @@ -3321,11 +3325,8 @@ def take_action(self, parsed_args): if parsed_args.name: server.update(name=parsed_args.name) - if parsed_args.property: - compute_client.servers.set_meta( - server, - parsed_args.property, - ) + if parsed_args.properties: + compute_client.servers.set_meta(server, parsed_args.properties) if parsed_args.state: server.reset_state(state=parsed_args.state) @@ -3740,6 +3741,7 @@ def get_parser(self, prog_name): metavar='', action='append', default=[], + dest='properties', help=_('Property key to remove from server ' '(repeat option to remove multiple values)'), ) @@ -3771,11 +3773,8 @@ def take_action(self, parsed_args): parsed_args.server, ) - if parsed_args.property: - compute_client.servers.delete_meta( - server, - parsed_args.property, - ) + if parsed_args.properties: + compute_client.servers.delete_meta(server, parsed_args.properties) if parsed_args.description: if compute_client.api_version < api_versions.APIVersion("2.19"): diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index e12edd0fce..8563f98811 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -138,7 +138,7 @@ def test_aggregate_create_with_property(self): 'ag1', ] verifylist = [ - ('property', {'key1': 'value1', 'key2': 'value2'}), + ('properties', {'key1': 'value1', 'key2': 'value2'}), ('name', 'ag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -146,7 +146,7 @@ def test_aggregate_create_with_property(self): self.sdk_client.create_aggregate.assert_called_once_with( name=parsed_args.name) self.sdk_client.set_aggregate_metadata.assert_called_once_with( - self.fake_ag.id, parsed_args.property) + self.fake_ag.id, parsed_args.properties) self.assertEqual(self.columns, columns) self.assertItemsEqual(self.data, data) @@ -378,7 +378,7 @@ def test_aggregate_set_with_property(self): 'ag1', ] verifylist = [ - ('property', {'key1': 'value1', 'key2': 'value2'}), + ('properties', {'key1': 'value1', 'key2': 'value2'}), ('aggregate', 'ag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -388,7 +388,7 @@ def test_aggregate_set_with_property(self): parsed_args.aggregate, ignore_missing=False) self.assertNotCalled(self.sdk_client.update_aggregate) self.sdk_client.set_aggregate_metadata.assert_called_once_with( - self.fake_ag.id, parsed_args.property) + self.fake_ag.id, parsed_args.properties) self.assertIsNone(result) def test_aggregate_set_with_no_property_and_property(self): @@ -399,7 +399,7 @@ def test_aggregate_set_with_no_property_and_property(self): ] verifylist = [ ('no_property', True), - ('property', {'key2': 'value2'}), + ('properties', {'key2': 'value2'}), ('aggregate', 'ag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -509,14 +509,14 @@ def test_aggregate_unset(self): 'ag1', ] verifylist = [ - ('property', ['unset_key']), + ('properties', ['unset_key']), ('aggregate', 'ag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.sdk_client.set_aggregate_metadata.assert_called_once_with( - self.fake_ag, {'unset_key': None}) + self.fake_ag.id, {'unset_key': None}) self.assertIsNone(result) def test_aggregate_unset_multiple_properties(self): @@ -526,14 +526,14 @@ def test_aggregate_unset_multiple_properties(self): 'ag1', ] verifylist = [ - ('property', ['unset_key1', 'unset_key2']), + ('properties', ['unset_key1', 'unset_key2']), ('aggregate', 'ag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.sdk_client.set_aggregate_metadata.assert_called_once_with( - self.fake_ag, {'unset_key1': None, 'unset_key2': None}) + self.fake_ag.id, {'unset_key1': None, 'unset_key2': None}) self.assertIsNone(result) def test_aggregate_unset_no_option(self): @@ -541,7 +541,7 @@ def test_aggregate_unset_no_option(self): 'ag1', ] verifylist = [ - ('property', None), + ('properties', []), ('aggregate', 'ag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 8c1147fd2a..ee4479b009 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -160,7 +160,7 @@ def test_flavor_create_all_options(self): ('rxtx_factor', self.flavor.rxtx_factor), ('public', True), ('description', self.flavor.description), - ('property', {'property': 'value'}), + ('properties', {'property': 'value'}), ('name', self.flavor.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -232,7 +232,7 @@ def test_flavor_create_other_options(self): ('public', False), ('description', 'description'), ('project', self.project.id), - ('property', {'key1': 'value1', 'key2': 'value2'}), + ('properties', {'key1': 'value1', 'key2': 'value2'}), ('name', self.flavor.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -688,7 +688,7 @@ def test_flavor_set_property(self): 'baremetal' ] verifylist = [ - ('property', {'FOO': '"B A R"'}), + ('properties', {'FOO': '"B A R"'}), ('flavor', 'baremetal') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1017,7 +1017,7 @@ def test_flavor_unset_property(self): 'baremetal' ] verifylist = [ - ('property', ['property']), + ('properties', ['property']), ('flavor', 'baremetal'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1039,7 +1039,7 @@ def test_flavor_unset_properties(self): 'baremetal' ] verifylist = [ - ('property', ['property1', 'property2']), + ('properties', ['property1', 'property2']), ('flavor', 'baremetal'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 8c3bf317e0..99f7eede4d 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1159,7 +1159,7 @@ def test_server_create_with_options(self): ('image', 'image1'), ('flavor', 'flavor1'), ('key_name', 'keyname'), - ('property', {'Beta': 'b'}), + ('properties', {'Beta': 'b'}), ('security_group', ['securitygroup']), ('hint', {'a': ['b', 'c']}), ('config_drive', True), @@ -2227,7 +2227,7 @@ def test_server_create_image_property(self): self.new_server.name, ] verifylist = [ - ('image_property', {'hypervisor_type': 'qemu'}), + ('image_properties', {'hypervisor_type': 'qemu'}), ('flavor', 'flavor1'), ('nic', ['none']), ('config_drive', False), @@ -2282,7 +2282,7 @@ def test_server_create_image_property_multi(self): self.new_server.name, ] verifylist = [ - ('image_property', {'hypervisor_type': 'qemu', + ('image_properties', {'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide'}), ('flavor', 'flavor1'), ('nic', ['none']), @@ -2338,7 +2338,7 @@ def test_server_create_image_property_missed(self): self.new_server.name, ] verifylist = [ - ('image_property', {'hypervisor_type': 'qemu', + ('image_properties', {'hypervisor_type': 'qemu', 'hw_disk_bus': 'virtio'}), ('flavor', 'flavor1'), ('nic', ['none']), @@ -2370,7 +2370,7 @@ def test_server_create_image_property_with_image_list(self): ] verifylist = [ - ('image_property', + ('image_properties', {'owner_specified.openstack.object': 'image/cirros'}), ('flavor', 'flavor1'), ('nic', ['none']), @@ -4973,10 +4973,10 @@ def test_rebuild_with_property(self): '--property', 'key1=value1', '--property', 'key2=value2' ] - expected_property = {'key1': 'value1', 'key2': 'value2'} + expected_properties = {'key1': 'value1', 'key2': 'value2'} verifylist = [ ('server', self.server.id), - ('property', expected_property) + ('properties', expected_properties) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4986,7 +4986,7 @@ def test_rebuild_with_property(self): self.servers_mock.get.assert_called_with(self.server.id) self.get_image_mock.assert_called_with(self.image.id) self.server.rebuild.assert_called_with( - self.image, None, meta=expected_property) + self.image, None, meta=expected_properties) def test_rebuild_with_keypair_name(self): self.app.client_manager.compute.api_version = \ @@ -6145,13 +6145,13 @@ def test_server_set_with_property(self): 'foo_vm', ] verifylist = [ - ('property', {'key1': 'value1', 'key2': 'value2'}), + ('properties', {'key1': 'value1', 'key2': 'value2'}), ('server', 'foo_vm'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.servers_mock.set_meta.assert_called_once_with( - self.fake_servers[0], parsed_args.property) + self.fake_servers[0], parsed_args.properties) self.assertIsNone(result) @mock.patch.object(getpass, 'getpass', @@ -6579,7 +6579,7 @@ def test_server_unset_with_property(self): 'foo_vm', ] verifylist = [ - ('property', ['key1', 'key2']), + ('properties', ['key1', 'key2']), ('server', 'foo_vm'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) From d0112a801a20c810d92dfcdf1f8fb71df3bd1d26 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 15:11:59 +0000 Subject: [PATCH 0339/1120] compute: Add missing options for 'server list' This accepts a large number of options that we weren't exposing. Add the following options: '--availability-zone', '--key-name', '--config-drive' and '--no-config-drive', '--progress', '--vm-state', '--task-state' and '--power-state'. In addition, refine the 'openstack server list --status' parameter to restrict users to the actual choices supported by the server. Change-Id: Ieeb1f22df7092e66a411b6a36eafb3e16efc2fc2 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 217 +++++++++++++++++- .../tests/unit/compute/v2/test_server.py | 140 ++++++++++- ...ing-server-list-opts-c41e97e86ff1e1ca.yaml | 16 ++ 3 files changed, 361 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-list-opts-c41e97e86ff1e1ca.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ba0243ef3e..2d6a4b1849 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1344,18 +1344,18 @@ def _show_progress(progress): raise SystemExit +def percent_type(x): + x = int(x) + if not 0 < x <= 100: + raise argparse.ArgumentTypeError("Must be between 0 and 100") + return x + + class ListServer(command.Lister): _description = _("List servers") def get_parser(self, prog_name): parser = super(ListServer, self).get_parser(prog_name) - parser.add_argument( - '--availability-zone', - metavar='', - help=_('Only return instances that match the availability zone. ' - 'Note that this option will be ignored for non-admin users ' - 'when using ``--os-compute-api-version`` prior to 2.83.'), - ) parser.add_argument( '--reservation-id', metavar='', @@ -1385,10 +1385,35 @@ def get_parser(self, prog_name): metavar='', help=_('Regular expression to match instance name (admin only)'), ) + # taken from 'task_and_vm_state_from_status' function in nova + # the API sadly reports these in upper case and while it would be + # wonderful to plaster over this ugliness client-side, there are + # already users in the wild doing this in upper case that we need to + # support parser.add_argument( '--status', metavar='', - # FIXME(dhellmann): Add choices? + choices=( + 'ACTIVE', + 'BUILD', + 'DELETED', + 'ERROR', + 'HARD_REBOOT', + 'MIGRATING', + 'PASSWORD', + 'PAUSED', + 'REBOOT', + 'REBUILD', + 'RESCUE', + 'RESIZE', + 'REVERT_RESIZE', + 'SHELVED', + 'SHELVED_OFFLOADED', + 'SHUTOFF', + 'SOFT_DELETED', + 'SUSPENDED', + 'VERIFY_RESIZE' + ), help=_('Search by server status'), ) parser.add_argument( @@ -1421,7 +1446,10 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Search by user (admin only) (name or ID)'), + help=_( + 'Search by user (name or ID) ' + '(admin only before microversion 2.83)' + ), ) identity_common.add_user_domain_option_to_parser(parser) parser.add_argument( @@ -1430,6 +1458,146 @@ def get_parser(self, prog_name): default=False, help=_('Only display deleted servers (admin only)'), ) + parser.add_argument( + '--availability-zone', + default=None, + help=_( + 'Search by availability zone ' + '(admin only before microversion 2.83)' + ), + ) + parser.add_argument( + '--key-name', + help=_( + 'Search by keypair name ' + '(admin only before microversion 2.83)' + ), + ) + config_drive_group = parser.add_mutually_exclusive_group() + config_drive_group.add_argument( + '--config-drive', + action='store_true', + dest='has_config_drive', + default=None, + help=_( + 'Only display servers with a config drive attached ' + '(admin only before microversion 2.83)' + ), + ) + # NOTE(gibi): this won't actually do anything until bug 1871409 is + # fixed and the REST API is cleaned up regarding the values of + # config_drive + config_drive_group.add_argument( + '--no-config-drive', + action='store_false', + dest='has_config_drive', + help=_( + 'Only display servers without a config drive attached ' + '(admin only before microversion 2.83)' + ), + ) + parser.add_argument( + '--progress', + type=percent_type, + default=None, + help=_( + 'Search by progress value (%%) ' + '(admin only before microversion 2.83)' + ), + ) + parser.add_argument( + '--vm-state', + metavar='', + # taken from 'InstanceState' object field in nova + choices=( + 'active', + 'building', + 'deleted', + 'error', + 'paused', + 'stopped', + 'suspended', + 'rescued', + 'resized', + 'shelved', + 'shelved_offloaded', + 'soft-delete', + ), + help=_( + 'Search by vm_state value ' + '(admin only before microversion 2.83)' + ), + ) + parser.add_argument( + '--task-state', + metavar='', + # taken from 'InstanceTaskState' object field in nova + choices=( + 'block_device_mapping', + 'deleting', + 'image_backup', + 'image_pending_upload', + 'image_snapshot', + 'image_snapshot_pending', + 'image_uploading', + 'migrating', + 'networking', + 'pausing', + 'powering-off', + 'powering-on', + 'rebooting', + 'reboot_pending', + 'reboot_started', + 'reboot_pending_hard', + 'reboot_started_hard', + 'rebooting_hard', + 'rebuilding', + 'rebuild_block_device_mapping', + 'rebuild_spawning', + 'rescuing', + 'resize_confirming', + 'resize_finish', + 'resize_migrated', + 'resize_migrating', + 'resize_prep', + 'resize_reverting', + 'restoring', + 'resuming', + 'scheduling', + 'shelving', + 'shelving_image_pending_upload', + 'shelving_image_uploading', + 'shelving_offloading', + 'soft-deleting', + 'spawning', + 'suspending', + 'updating_password', + 'unpausing', + 'unrescuing', + 'unshelving', + ), + help=_( + 'Search by task_state value ' + '(admin only before microversion 2.83)' + ), + ) + parser.add_argument( + '--power-state', + metavar='', + # taken from 'InstancePowerState' object field in nova + choices=( + 'pending', + 'running', + 'paused', + 'shutdown', + 'crashed', + 'suspended', + ), + help=_( + 'Search by power_state value ' + '(admin only before microversion 2.83)' + ), + ) parser.add_argument( '--long', action='store_true', @@ -1582,7 +1750,6 @@ def take_action(self, parsed_args): ignore_missing=False).id search_opts = { - 'availability_zone': parsed_args.availability_zone, 'reservation_id': parsed_args.reservation_id, 'ip': parsed_args.ip, 'ip6': parsed_args.ip6, @@ -1600,6 +1767,36 @@ def take_action(self, parsed_args): 'changes-since': parsed_args.changes_since, } + if parsed_args.availability_zone: + search_opts['availability_zone'] = parsed_args.availability_zone + + if parsed_args.key_name: + search_opts['key_name'] = parsed_args.key_name + + if parsed_args.has_config_drive is not None: + search_opts['config_drive'] = parsed_args.has_config_drive + + if parsed_args.progress is not None: + search_opts['progress'] = str(parsed_args.progress) + + if parsed_args.vm_state: + search_opts['vm_state'] = parsed_args.vm_state + + if parsed_args.task_state: + search_opts['task_state'] = parsed_args.task_state + + if parsed_args.power_state: + # taken from 'InstancePowerState' object field in nova + power_state = { + 'pending': 0, + 'running': 1, + 'paused': 3, + 'shutdown': 4, + 'crashed': 6, + 'suspended': 7, + }[parsed_args.power_state] + search_opts['power_state'] = power_state + if parsed_args.tags: if compute_client.api_version < api_versions.APIVersion('2.26'): msg = _( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index efc105e565..0eeb2cec23 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2986,7 +2986,6 @@ def setUp(self): super(TestServerList, self).setUp() self.search_opts = { - 'availability_zone': None, 'reservation_id': None, 'ip': None, 'ip6': None, @@ -3431,7 +3430,7 @@ def test_server_list_v266_with_invalid_changes_before( 'Invalid time value' ) - def test_server_with_changes_before_older_version(self): + def test_server_with_changes_before_pre_v266(self): self.app.client_manager.compute.api_version = ( api_versions.APIVersion('2.65')) @@ -3587,6 +3586,143 @@ def test_server_list_with_not_tag_pre_v226(self): '--os-compute-api-version 2.26 or greater is required', str(ex)) + def test_server_list_with_availability_zone(self): + arglist = [ + '--availability-zone', 'test-az', + ] + verifylist = [ + ('availability_zone', 'test-az'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['availability_zone'] = 'test-az' + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_key_name(self): + arglist = [ + '--key-name', 'test-key', + ] + verifylist = [ + ('key_name', 'test-key'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['key_name'] = 'test-key' + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_config_drive(self): + arglist = [ + '--config-drive', + ] + verifylist = [ + ('has_config_drive', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['config_drive'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_no_config_drive(self): + arglist = [ + '--no-config-drive', + ] + verifylist = [ + ('has_config_drive', False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['config_drive'] = False + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_progress(self): + arglist = [ + '--progress', '100', + ] + verifylist = [ + ('progress', 100), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['progress'] = '100' + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_progress_invalid(self): + arglist = [ + '--progress', '101', + ] + + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verify_args=[]) + + def test_server_list_with_vm_state(self): + arglist = [ + '--vm-state', 'active', + ] + verifylist = [ + ('vm_state', 'active'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['vm_state'] = 'active' + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_task_state(self): + arglist = [ + '--task-state', 'deleting', + ] + verifylist = [ + ('task_state', 'deleting'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['task_state'] = 'deleting' + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_server_list_with_power_state(self): + arglist = [ + '--power-state', 'running', + ] + verifylist = [ + ('power_state', 'running'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['power_state'] = 1 + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + class TestServerLock(TestServer): diff --git a/releasenotes/notes/add-missing-server-list-opts-c41e97e86ff1e1ca.yaml b/releasenotes/notes/add-missing-server-list-opts-c41e97e86ff1e1ca.yaml new file mode 100644 index 0000000000..f536eb9ea1 --- /dev/null +++ b/releasenotes/notes/add-missing-server-list-opts-c41e97e86ff1e1ca.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Add a number of additional options to the ``server list`` command: + + - ``--availability-zone`` + - ``--key-name`` + - ``--config-drive``, ``--no-config-drive`` + - ``--progress`` + - ``--vm-state`` + - ``--task-state`` + - ``--power-state`` +upgrade: + - | + The ``openstack server list --status`` parameter will now validate the + requested status. From 8a0f3fc6a8b30328c575bb5c3fc21ddc4f500d9d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 15:38:18 +0000 Subject: [PATCH 0340/1120] compute: Add missing options for 'server set' Add a new '--no-password' option to unset the password on an existing server. In addition, add a new '--password' option that replaces the interactive '--root-password' option. This makes sense given no other commands uses interactive password options. Checks that rely on specific API microversions now run before we execute any action, to avoid situations where an update is only partially applied. Change-Id: Ibf8717efdd418a2d95215b4d9ab2acf0d57c4a70 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 62 ++++++++++++++----- .../tests/unit/compute/v2/test_server.py | 32 ++++++++++ ...sing-server-set-opts-e1b4300f5f42e863.yaml | 10 +++ 3 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-set-opts-e1b4300f5f42e863.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c49c181559..50299d65d0 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3472,18 +3472,35 @@ def get_parser(self, prog_name): metavar='', help=_('New server name'), ) - parser.add_argument( + password_group = parser.add_mutually_exclusive_group() + password_group.add_argument( + '--password', + help=_('Set the server password'), + ) + password_group.add_argument( + '--no-password', + action='store_true', + help=_( + 'Clear the admin password for the server from the metadata ' + 'service; note that this action does not actually change the ' + 'server password' + ), + ) + # TODO(stephenfin): Remove this in a future major version + password_group.add_argument( '--root-password', action="store_true", - help=_('Set new root password (interactive only)'), + help=argparse.SUPPRESS, ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, dest='properties', - help=_('Property to add/change for this server ' - '(repeat option to set multiple properties)'), + help=_( + 'Property to add/change for this server ' + '(repeat option to set multiple properties)' + ), ) parser.add_argument( '--state', @@ -3494,8 +3511,10 @@ def get_parser(self, prog_name): parser.add_argument( '--description', metavar='', - help=_('New server description (supported by ' - '--os-compute-api-version 2.19 or above)'), + help=_( + 'New server description ' + '(supported by --os-compute-api-version 2.19 or above)' + ), ) parser.add_argument( '--tag', @@ -3519,6 +3538,22 @@ def take_action(self, parsed_args): parsed_args.server, ) + if parsed_args.description: + if server.api_version < api_versions.APIVersion("2.19"): + msg = _( + '--os-compute-api-version 2.19 or greater is required to ' + 'support the --description option' + ) + raise exceptions.CommandError(msg) + + if parsed_args.tags: + if server.api_version < api_versions.APIVersion('2.26'): + msg = _( + '--os-compute-api-version 2.26 or greater is required to ' + 'support the --tag option' + ) + raise exceptions.CommandError(msg) + if parsed_args.name: server.update(name=parsed_args.name) @@ -3536,22 +3571,15 @@ def take_action(self, parsed_args): else: msg = _("Passwords do not match, password unchanged") raise exceptions.CommandError(msg) + elif parsed_args.password: + server.change_password(parsed_args.password) + elif parsed_args.no_password: + server.clear_password() if parsed_args.description: - if server.api_version < api_versions.APIVersion("2.19"): - msg = _("Description is not supported for " - "--os-compute-api-version less than 2.19") - raise exceptions.CommandError(msg) server.update(description=parsed_args.description) if parsed_args.tags: - if server.api_version < api_versions.APIVersion('2.26'): - msg = _( - '--os-compute-api-version 2.26 or greater is required to ' - 'support the --tag option' - ) - raise exceptions.CommandError(msg) - for tag in parsed_args.tags: server.add_tag(tag=tag) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index f046925af1..0f33dd7047 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6207,6 +6207,7 @@ def setUp(self): 'update': None, 'reset_state': None, 'change_password': None, + 'clear_password': None, 'add_tag': None, 'set_tags': None, } @@ -6290,6 +6291,37 @@ def test_server_set_with_property(self): self.fake_servers[0], parsed_args.properties) self.assertIsNone(result) + def test_server_set_with_password(self): + arglist = [ + '--password', 'foo', + 'foo_vm', + ] + verifylist = [ + ('password', 'foo'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.fake_servers[0].change_password.assert_called_once_with('foo') + + def test_server_set_with_no_password(self): + arglist = [ + '--no-password', + 'foo_vm', + ] + verifylist = [ + ('no_password', True), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.fake_servers[0].clear_password.assert_called_once_with() + + # TODO(stephenfin): Remove this in a future major version @mock.patch.object(getpass, 'getpass', return_value=mock.sentinel.fake_pass) def test_server_set_with_root_password(self, mock_getpass): diff --git a/releasenotes/notes/add-missing-server-set-opts-e1b4300f5f42e863.yaml b/releasenotes/notes/add-missing-server-set-opts-e1b4300f5f42e863.yaml new file mode 100644 index 0000000000..ccffaa1fe7 --- /dev/null +++ b/releasenotes/notes/add-missing-server-set-opts-e1b4300f5f42e863.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add ``--no-password`` option to ``server set`` command, allowing users + to clear the admin password from the metadata service. Note that this does + not actually change the server password. +upgrade: + - | + The ``server set --root-password`` option has been deprecated in favour of + a non-interactive ``--password`` option. From fc24142ed41e15622687558c68d670bdc37223f0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 15:55:44 +0000 Subject: [PATCH 0341/1120] compute: Add missing options for 'keypair list' Add pagination parameters, '--limit' and '--marker'. This isn't compatible with our client-side '--project' parameter so we error out for that. Change-Id: I403cf0fb7aabad4a3dfda5adae62d47ecf7faf5c Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/keypair.py | 47 ++++++++++++- .../tests/unit/compute/v2/test_keypair.py | 68 +++++++++++++++++++ ...ng-keypair-list-opts-243a33d8276f91b8.yaml | 5 ++ 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-missing-keypair-list-opts-243a33d8276f91b8.yaml diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 19e30bff10..7dabf78d9c 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -249,12 +249,43 @@ def get_parser(self, prog_name): ), ) identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--marker', + help=_('The last keypair ID of the previous page'), + ) + parser.add_argument( + '--limit', + type=int, + help=_('Maximum number of keypairs to display'), + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute identity_client = self.app.client_manager.identity + kwargs = {} + + if parsed_args.marker: + if not sdk_utils.supports_microversion(compute_client, '2.35'): + msg = _( + '--os-compute-api-version 2.35 or greater is required ' + 'to support the --marker option' + ) + raise exceptions.CommandError(msg) + + kwargs['marker'] = parsed_args.marker + + if parsed_args.limit: + if not sdk_utils.supports_microversion(compute_client, '2.35'): + msg = _( + '--os-compute-api-version 2.35 or greater is required ' + 'to support the --limit option' + ) + raise exceptions.CommandError(msg) + + kwargs['limit'] = parsed_args.limit + if parsed_args.project: if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( @@ -263,6 +294,14 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) + if parsed_args.marker: + # NOTE(stephenfin): Because we're doing this client-side, we + # can't really rely on the marker, because we don't know what + # user the marker is associated with + msg = _( + '--project is not compatible with --marker' + ) + # NOTE(stephenfin): This is done client side because nova doesn't # currently support doing so server-side. If this is slow, we can # think about spinning up a threadpool or similar. @@ -275,7 +314,8 @@ def take_action(self, parsed_args): data = [] for user in users: - data.extend(compute_client.keypairs(user_id=user.id)) + kwargs['user_id'] = user.id + data.extend(compute_client.keypairs(**kwargs)) elif parsed_args.user: if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( @@ -289,10 +329,11 @@ def take_action(self, parsed_args): parsed_args.user, parsed_args.user_domain, ) + kwargs['user_id'] = user.id - data = compute_client.keypairs(user_id=user.id) + data = compute_client.keypairs(**kwargs) else: - data = compute_client.keypairs() + data = compute_client.keypairs(**kwargs) columns = ( "Name", diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 5a17808fa1..65d9396aee 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -569,6 +569,74 @@ def test_keypair_list_conflicting_user_options(self): tests_utils.ParserException, self.check_parser, self.cmd, arglist, None) + @mock.patch.object( + sdk_utils, 'supports_microversion', new=mock.Mock(return_value=True)) + def test_keypair_list_with_limit(self): + arglist = [ + '--limit', '1', + ] + verifylist = [ + ('limit', 1), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.sdk_client.keypairs.assert_called_with(limit=1) + + @mock.patch.object( + sdk_utils, 'supports_microversion', new=mock.Mock(return_value=False)) + def test_keypair_list_with_limit_pre_v235(self): + arglist = [ + '--limit', '1', + ] + verifylist = [ + ('limit', 1), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.35 or greater is required', str(ex)) + + @mock.patch.object( + sdk_utils, 'supports_microversion', new=mock.Mock(return_value=True)) + def test_keypair_list_with_marker(self): + arglist = [ + '--marker', 'test_kp', + ] + verifylist = [ + ('marker', 'test_kp'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.sdk_client.keypairs.assert_called_with(marker='test_kp') + + @mock.patch.object( + sdk_utils, 'supports_microversion', new=mock.Mock(return_value=False)) + def test_keypair_list_with_marker_pre_v235(self): + arglist = [ + '--marker', 'test_kp', + ] + verifylist = [ + ('marker', 'test_kp'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.35 or greater is required', str(ex)) + class TestKeypairShow(TestKeypair): diff --git a/releasenotes/notes/add-missing-keypair-list-opts-243a33d8276f91b8.yaml b/releasenotes/notes/add-missing-keypair-list-opts-243a33d8276f91b8.yaml new file mode 100644 index 0000000000..789eaa029e --- /dev/null +++ b/releasenotes/notes/add-missing-keypair-list-opts-243a33d8276f91b8.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--limit`` and ``--marker`` options to ``keypair list`` command, to + configure pagination of results. From b34905722015646538c8557f9fa91fc2b5edffdb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 8 Dec 2020 15:37:14 +0000 Subject: [PATCH 0342/1120] tests: Remove unused fake method The FakeServerMigration.get_server_migrations method was added in change I15b4a5aca8d0dee59dd293e7b1c7272cdfbeea20 but has never been used. Remove it. Change-Id: I6089c5200737b9319a8e96f2a2fc18b7cdd6b2c6 Signed-off-by: Stephen Finucane --- .../tests/unit/compute/v2/fakes.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index d3d037a9a8..b667c691d3 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1631,22 +1631,3 @@ def create_server_migrations(attrs=None, methods=None, count=2): attrs, methods)) return migrations - - @staticmethod - def get_server_migrations(migrations=None, count=2): - """Get an iterable MagicMock object with a list of faked migrations. - - If server migrations list is provided, then initialize the Mock object - with the list. Otherwise create one. - - :param List migrations: - A list of FakeResource objects faking server migrations - :param int count: - The number of server migrations to fake - :return: - An iterable Mock object with side_effect set to a list of faked - server migrations - """ - if migrations is None: - migrations = FakeServerMigration.create_server_migrations(count) - return mock.Mock(side_effect=migrations) From 958344733aa6d8aea6cb8d06cc4d879fe1ee44a6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Dec 2020 12:20:40 +0000 Subject: [PATCH 0343/1120] compute: Add missing options for 'server image create' Add a '--property' option to record arbitrary key/value metadata to 'meta_data.json' on the metadata server. Change-Id: I267f3290fce3692cbd1ff6a9af146c2736ee31fe Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_image.py | 17 +++++++++++++++-- .../tests/unit/compute/v2/test_server_image.py | 6 ++++++ ...rver-image-create-opts-3c19a7492dc50fd7.yaml | 6 ++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-image-create-opts-3c19a7492dc50fd7.yaml diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index c12bc2b3ae..6c0e3b22cf 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -18,6 +18,7 @@ import importlib import logging +from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -48,6 +49,16 @@ def get_parser(self, prog_name): metavar='', help=_('Name of new disk image (default: server name)'), ) + parser.add_argument( + '--property', + metavar='', + dest='properties', + action=parseractions.KeyValueAction, + help=_( + 'Set a new property to meta_data.json on the metadata server ' + '(repeat option to set multiple values)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -76,6 +87,7 @@ def _show_progress(progress): image_id = compute_client.servers.create_image( server.id, image_name, + parsed_args.properties, ) image_client = self.app.client_manager.image @@ -89,8 +101,8 @@ def _show_progress(progress): ): self.app.stdout.write('\n') else: - LOG.error(_('Error creating server image: %s'), - parsed_args.server) + LOG.error( + _('Error creating server image: %s'), parsed_args.server) raise exceptions.CommandError if self.app.client_manager._api_version['image'] == '1': @@ -105,4 +117,5 @@ def _show_progress(progress): ] ) info = image_module._format_image(image) + return zip(*sorted(info.items())) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 06f6017c11..66452a8bb8 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -130,6 +130,7 @@ def test_server_image_create_defaults(self): self.servers_mock.create_image.assert_called_with( servers[0].id, servers[0].name, + None, ) self.assertEqual(self.image_columns(images[0]), columns) @@ -141,11 +142,13 @@ def test_server_image_create_options(self): arglist = [ '--name', 'img-nam', + '--property', 'key=value', servers[0].id, ] verifylist = [ ('name', 'img-nam'), ('server', servers[0].id), + ('properties', {'key': 'value'}), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -158,6 +161,7 @@ def test_server_image_create_options(self): self.servers_mock.create_image.assert_called_with( servers[0].id, 'img-nam', + {'key': 'value'}, ) self.assertEqual(self.image_columns(images[0]), columns) @@ -188,6 +192,7 @@ def test_server_create_image_wait_fail(self, mock_wait_for_status): self.servers_mock.create_image.assert_called_with( servers[0].id, servers[0].name, + None, ) mock_wait_for_status.assert_called_once_with( @@ -220,6 +225,7 @@ def test_server_create_image_wait_ok(self, mock_wait_for_status): self.servers_mock.create_image.assert_called_with( servers[0].id, servers[0].name, + None, ) mock_wait_for_status.assert_called_once_with( diff --git a/releasenotes/notes/add-missing-server-image-create-opts-3c19a7492dc50fd7.yaml b/releasenotes/notes/add-missing-server-image-create-opts-3c19a7492dc50fd7.yaml new file mode 100644 index 0000000000..b5e3c0627a --- /dev/null +++ b/releasenotes/notes/add-missing-server-image-create-opts-3c19a7492dc50fd7.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--property`` option to ``server image create`` command, allowing + users to record arbitrary key/value metadata to ``meta_data.json`` on + the metadata server. From d5026278ede4dbe8126839ec59ec4ca371e806a8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Dec 2020 12:47:10 +0000 Subject: [PATCH 0344/1120] compute: Add 'server volume list' command This replaces the old 'nova volume-attachments' command. Change-Id: Icb98766f98bd1f2469bdb6df62b4624711f98422 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_volume.py | 73 ++++++++ .../tests/unit/compute/v2/fakes.py | 60 +++++++ .../unit/compute/v2/test_server_volume.py | 167 ++++++++++++++++++ ...server-volume-update-89740dca61596dd1.yaml | 5 + setup.cfg | 2 + 5 files changed, 307 insertions(+) create mode 100644 openstackclient/compute/v2/server_volume.py create mode 100644 openstackclient/tests/unit/compute/v2/test_server_volume.py create mode 100644 releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml diff --git a/openstackclient/compute/v2/server_volume.py b/openstackclient/compute/v2/server_volume.py new file mode 100644 index 0000000000..8a931ae5ab --- /dev/null +++ b/openstackclient/compute/v2/server_volume.py @@ -0,0 +1,73 @@ +# Copyright 2020, Red Hat 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. + +"""Compute v2 Server action implementations""" + +from novaclient import api_versions +from osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ + + +class ListServerVolume(command.Lister): + """List all the volumes attached to a server.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'server', + help=_('Server to list volume attachments for (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + + compute_client = self.app.client_manager.compute + + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + + volumes = compute_client.volumes.get_server_volumes(server.id) + + columns = ( + 'id', + 'device', + 'serverId', + 'volumeId', + ) + column_headers = ( + 'ID', + 'Device', + 'Server ID', + 'Volume ID', + ) + if compute_client.api_version >= api_versions.APIVersion('2.70'): + columns += ('tag',) + column_headers += ('Tag',) + + if compute_client.api_version >= api_versions.APIVersion('2.79'): + columns += ('delete_on_termination',) + column_headers += ('Delete On Termination?',) + + return ( + column_headers, + ( + utils.get_item_properties( + s, columns, mixed_case_fields=('serverId', 'volumeId') + ) for s in volumes + ), + ) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index b667c691d3..e4cf10454e 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1631,3 +1631,63 @@ def create_server_migrations(attrs=None, methods=None, count=2): attrs, methods)) return migrations + + +class FakeVolumeAttachment(object): + """Fake one or more volume attachments (BDMs).""" + + @staticmethod + def create_one_volume_attachment(attrs=None, methods=None): + """Create a fake volume attachment. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object, with id, device, and so on + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + volume_attachment_info = { + "id": uuid.uuid4().hex, + "device": "/dev/sdb", + "serverId": uuid.uuid4().hex, + "volumeId": uuid.uuid4().hex, + # introduced in API microversion 2.70 + "tag": "foo", + # introduced in API microversion 2.79 + "delete_on_termination": True, + } + + # Overwrite default attributes. + volume_attachment_info.update(attrs) + + volume_attachment = fakes.FakeResource( + info=copy.deepcopy(volume_attachment_info), + methods=methods, + loaded=True) + return volume_attachment + + @staticmethod + def create_volume_attachments(attrs=None, methods=None, count=2): + """Create multiple fake volume attachments (BDMs). + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :param int count: + The number of server migrations to fake + :return: + A list of FakeResource objects faking the volume attachments. + """ + volume_attachments = [] + for i in range(0, count): + volume_attachments.append( + FakeVolumeAttachment.create_one_volume_attachment( + attrs, methods)) + + return volume_attachments diff --git a/openstackclient/tests/unit/compute/v2/test_server_volume.py b/openstackclient/tests/unit/compute/v2/test_server_volume.py new file mode 100644 index 0000000000..d09c287450 --- /dev/null +++ b/openstackclient/tests/unit/compute/v2/test_server_volume.py @@ -0,0 +1,167 @@ +# 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 novaclient import api_versions + +from openstackclient.compute.v2 import server_volume +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes + + +class TestServerVolume(compute_fakes.TestComputev2): + + def setUp(self): + super().setUp() + + # Get a shortcut to the compute client ServerManager Mock + self.servers_mock = self.app.client_manager.compute.servers + self.servers_mock.reset_mock() + + # Get a shortcut to the compute client VolumeManager mock + self.servers_volumes_mock = self.app.client_manager.compute.volumes + self.servers_volumes_mock.reset_mock() + + +class TestServerVolumeList(TestServerVolume): + + def setUp(self): + super().setUp() + + self.server = compute_fakes.FakeServer.create_one_server() + self.volume_attachments = ( + compute_fakes.FakeVolumeAttachment.create_volume_attachments()) + + self.servers_mock.get.return_value = self.server + self.servers_volumes_mock.get_server_volumes.return_value = ( + self.volume_attachments) + + # Get the command object to test + self.cmd = server_volume.ListServerVolume(self.app, None) + + def test_server_volume_list(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.1') + + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(('ID', 'Device', 'Server ID', 'Volume ID'), columns) + self.assertEqual( + ( + ( + self.volume_attachments[0].id, + self.volume_attachments[0].device, + self.volume_attachments[0].serverId, + self.volume_attachments[0].volumeId, + ), + ( + self.volume_attachments[1].id, + self.volume_attachments[1].device, + self.volume_attachments[1].serverId, + self.volume_attachments[1].volumeId, + ), + ), + tuple(data), + ) + self.servers_volumes_mock.get_server_volumes.assert_called_once_with( + self.server.id) + + def test_server_volume_list_with_tags(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.70') + + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual( + ('ID', 'Device', 'Server ID', 'Volume ID', 'Tag',), columns, + ) + self.assertEqual( + ( + ( + self.volume_attachments[0].id, + self.volume_attachments[0].device, + self.volume_attachments[0].serverId, + self.volume_attachments[0].volumeId, + self.volume_attachments[0].tag, + ), + ( + self.volume_attachments[1].id, + self.volume_attachments[1].device, + self.volume_attachments[1].serverId, + self.volume_attachments[1].volumeId, + self.volume_attachments[1].tag, + ), + ), + tuple(data), + ) + self.servers_volumes_mock.get_server_volumes.assert_called_once_with( + self.server.id) + + def test_server_volume_list_with_delete_on_attachment(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.79') + + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual( + ( + 'ID', 'Device', 'Server ID', 'Volume ID', 'Tag', + 'Delete On Termination?', + ), + columns, + ) + self.assertEqual( + ( + ( + self.volume_attachments[0].id, + self.volume_attachments[0].device, + self.volume_attachments[0].serverId, + self.volume_attachments[0].volumeId, + self.volume_attachments[0].tag, + self.volume_attachments[0].delete_on_termination, + ), + ( + self.volume_attachments[1].id, + self.volume_attachments[1].device, + self.volume_attachments[1].serverId, + self.volume_attachments[1].volumeId, + self.volume_attachments[1].tag, + self.volume_attachments[1].delete_on_termination, + ), + ), + tuple(data), + ) + self.servers_volumes_mock.get_server_volumes.assert_called_once_with( + self.server.id) diff --git a/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml b/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml new file mode 100644 index 0000000000..d1c0501ff8 --- /dev/null +++ b/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``server volume list`` command, to list the volumes attached to an + instance. diff --git a/setup.cfg b/setup.cfg index 9299fb918a..3fe7fa38f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -153,6 +153,8 @@ openstack.compute.v2 = server_image_create = openstackclient.compute.v2.server_image:CreateServerImage + server_volume_list = openstackclient.compute.v2.server_volume:ListServerVolume + usage_list = openstackclient.compute.v2.usage:ListUsage usage_show = openstackclient.compute.v2.usage:ShowUsage From 64c2a1a453fce8f4e2e7e8441692af007c176459 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 11 Nov 2020 18:39:22 +0000 Subject: [PATCH 0345/1120] Add 'server shelve --offload', 'server shelve --wait' options The '--offload' option allows us to explicitly request that the server be offloaded once shelved or if already shelved. The '--wait' option allows us to wait for the shelve and/or offload operations to complete before returning. It is implied when attempting to offload a server than is not yet shelved. Change-Id: Id226831e3c09bc95c34b222151b27391a844b073 Signed-off-by: Stephen Finucane --- doc/source/cli/data/nova.csv | 2 +- openstackclient/compute/v2/server.py | 100 +++++++++++++- .../tests/functional/common/test_help.py | 2 +- .../tests/unit/compute/v2/test_server.py | 122 +++++++++++++++++- ...-shelve-offload-wait-d0a5c8ba92586f72.yaml | 8 ++ 5 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 2004007f8d..c319a4a69d 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -114,7 +114,7 @@ service-force-down,compute service set --force,Force service to down. service-list,compute service list,Show a list of all running services. set-password,server set --root-password,Change the admin password for a server. shelve,server shelve,Shelve a server. -shelve-offload,,Remove a shelved server from the compute node. +shelve-offload,shelve --offload,Remove a shelved server from the compute node. show,server show,Show details about the given server. ssh,server ssh,SSH into a server. start,server start,Start the server(s). diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 50299d65d0..33545a74e8 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3585,25 +3585,115 @@ def take_action(self, parsed_args): class ShelveServer(command.Command): - _description = _("Shelve server(s)") + """Shelve and optionally offload server(s). + + Shelving a server creates a snapshot of the server and stores this + snapshot before shutting down the server. This shelved server can then be + offloaded or deleted from the host, freeing up remaining resources on the + host, such as network interfaces. Shelved servers can be unshelved, + restoring the server from the snapshot. Shelving is therefore useful where + users wish to retain the UUID and IP of a server, without utilizing other + resources or disks. + + Most clouds are configured to automatically offload shelved servers + immediately or after a small delay. For clouds where this is not + configured, or where the delay is larger, offloading can be manually + specified. This is an admin-only operation by default. + """ def get_parser(self, prog_name): parser = super(ShelveServer, self).get_parser(prog_name) parser.add_argument( - 'server', + 'servers', metavar='', nargs='+', help=_('Server(s) to shelve (name or ID)'), ) + parser.add_argument( + '--offload', + action='store_true', + default=False, + help=_( + 'Remove the shelved server(s) from the host (admin only). ' + 'Invoking this option on an unshelved server(s) will result ' + 'in the server being shelved first' + ), + ) + parser.add_argument( + '--wait', + action='store_true', + default=False, + help=_('Wait for shelve and/or offload operation to complete'), + ) return parser def take_action(self, parsed_args): + + def _show_progress(progress): + if progress: + self.app.stdout.write('\rProgress: %s' % progress) + self.app.stdout.flush() + compute_client = self.app.client_manager.compute - for server in parsed_args.server: - utils.find_resource( + + for server in parsed_args.servers: + server_obj = utils.find_resource( + compute_client.servers, + server, + ) + if server_obj.status.lower() in ('shelved', 'shelved_offloaded'): + continue + + server_obj.shelve() + + # if we don't hav to wait, either because it was requested explicitly + # or is required implicitly, then our job is done + if not parsed_args.wait and not parsed_args.offload: + return + + for server in parsed_args.servers: + # TODO(stephenfin): We should wait for these in parallel using e.g. + # https://review.opendev.org/c/openstack/osc-lib/+/762503/ + if not utils.wait_for_status( + compute_client.servers.get, server_obj.id, + success_status=('shelved', 'shelved_offloaded'), + callback=_show_progress, + ): + LOG.error(_('Error shelving server: %s'), server_obj.id) + self.app.stdout.write( + _('Error shelving server: %s\n') % server_obj.id) + raise SystemExit + + if not parsed_args.offload: + return + + for server in parsed_args.servers: + server_obj = utils.find_resource( compute_client.servers, server, - ).shelve() + ) + if server_obj.status.lower() == 'shelved_offloaded': + continue + + server_obj.shelve_offload() + + if not parsed_args.wait: + return + + for server in parsed_args.servers: + # TODO(stephenfin): We should wait for these in parallel using e.g. + # https://review.opendev.org/c/openstack/osc-lib/+/762503/ + if not utils.wait_for_status( + compute_client.servers.get, server_obj.id, + success_status=('shelved_offloaded',), + callback=_show_progress, + ): + LOG.error( + _('Error offloading shelved server %s'), server_obj.id) + self.app.stdout.write( + _('Error offloading shelved server: %s\n') % ( + server_obj.id)) + raise SystemExit class ShowServer(command.ShowOne): diff --git a/openstackclient/tests/functional/common/test_help.py b/openstackclient/tests/functional/common/test_help.py index 3a9aef9ef3..c55741f19c 100644 --- a/openstackclient/tests/functional/common/test_help.py +++ b/openstackclient/tests/functional/common/test_help.py @@ -43,7 +43,7 @@ class HelpTests(base.TestCase): ('server resize', 'Scale server to a new flavor'), ('server resume', 'Resume server(s)'), ('server set', 'Set server properties'), - ('server shelve', 'Shelve server(s)'), + ('server shelve', 'Shelve and optionally offload server(s)'), ('server show', 'Show server details'), ('server ssh', 'SSH to server'), ('server start', 'Start server(s).'), diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 0f33dd7047..9ad6d15508 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6434,16 +6434,126 @@ def setUp(self): # Get the command object to test self.cmd = server.ShelveServer(self.app, None) - # Set shelve method to be tested. - self.methods = { + def test_shelve(self): + server_info = {'status': 'ACTIVE'} + server_methods = { 'shelve': None, + 'shelve_offload': None, } - def test_shelve_one_server(self): - self.run_method_with_servers('shelve', 1) + server = compute_fakes.FakeServer.create_one_server( + attrs=server_info, methods=server_methods) + self.servers_mock.get.return_value = server - def test_shelve_multi_servers(self): - self.run_method_with_servers('shelve', 3) + arglist = [server.name] + verifylist = [ + ('servers', [server.name]), + ('wait', False), + ('offload', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.get.assert_called_once_with(server.name) + server.shelve.assert_called_once_with() + server.shelve_offload.assert_not_called() + + def test_shelve_already_shelved(self): + server_info = {'status': 'SHELVED'} + server_methods = { + 'shelve': None, + 'shelve_offload': None, + } + + server = compute_fakes.FakeServer.create_one_server( + attrs=server_info, methods=server_methods) + self.servers_mock.get.return_value = server + + arglist = [server.name] + verifylist = [ + ('servers', [server.name]), + ('wait', False), + ('offload', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.get.assert_called_once_with(server.name) + server.shelve.assert_not_called() + server.shelve_offload.assert_not_called() + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_shelve_with_wait(self, mock_wait_for_status): + server_info = {'status': 'ACTIVE'} + server_methods = { + 'shelve': None, + 'shelve_offload': None, + } + + server = compute_fakes.FakeServer.create_one_server( + attrs=server_info, methods=server_methods) + self.servers_mock.get.return_value = server + + arglist = ['--wait', server.name] + verifylist = [ + ('servers', [server.name]), + ('wait', True), + ('offload', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.get.assert_called_once_with(server.name) + server.shelve.assert_called_once_with() + server.shelve_offload.assert_not_called() + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + server.id, + callback=mock.ANY, + success_status=('shelved', 'shelved_offloaded'), + ) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_shelve_offload(self, mock_wait_for_status): + server_info = {'status': 'ACTIVE'} + server_methods = { + 'shelve': None, + 'shelve_offload': None, + } + + server = compute_fakes.FakeServer.create_one_server( + attrs=server_info, methods=server_methods) + self.servers_mock.get.return_value = server + + arglist = ['--offload', server.name] + verifylist = [ + ('servers', [server.name]), + ('wait', False), + ('offload', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.get.assert_has_calls([ + mock.call(server.name), + mock.call(server.name), + ]) + server.shelve.assert_called_once_with() + server.shelve_offload.assert_called_once_with() + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + server.id, + callback=mock.ANY, + success_status=('shelved', 'shelved_offloaded'), + ) class TestServerShow(TestServer): diff --git a/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml b/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml new file mode 100644 index 0000000000..ddd3293191 --- /dev/null +++ b/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add support for ``--offload`` and ``--wait`` options for ``server shelve``. + ``--offload`` allows users to explicitly request offloading of a shelved + server in environments where automatic offloading is not configured, while + ``--wait`` allows users to wait for the shelve and/or shelve offload + operations to complete. From 2b073c2034acdabb8d4097b7f2c0408e53fe2d63 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 18 Nov 2020 11:27:30 +0000 Subject: [PATCH 0346/1120] Add 'server unshelve --wait' option This was recently added to the 'server shelve' command. Add it now for the 'unshelve' command. Change-Id: I633dd85b60cf70b4f8610f414d82669dd6a53111 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 61 ++++++++++---- .../tests/unit/compute/v2/test_server.py | 80 ++++++++++++------- ...-shelve-offload-wait-d0a5c8ba92586f72.yaml | 2 + 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 33545a74e8..59fc4b7d44 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -4131,25 +4131,54 @@ def get_parser(self, prog_name): 'SHELVED_OFFLOADED server (supported by ' '--os-compute-api-version 2.77 or above)'), ) + parser.add_argument( + '--wait', + action='store_true', + default=False, + help=_('Wait for unshelve operation to complete'), + ) return parser def take_action(self, parsed_args): + + def _show_progress(progress): + if progress: + self.app.stdout.write('\rProgress: %s' % progress) + self.app.stdout.flush() + compute_client = self.app.client_manager.compute - support_az = compute_client.api_version >= api_versions.APIVersion( - '2.77') - if not support_az and parsed_args.availability_zone: - msg = _("--os-compute-api-version 2.77 or greater is required " - "to support the '--availability-zone' option.") - raise exceptions.CommandError(msg) + kwargs = {} + + if parsed_args.availability_zone: + if compute_client.api_version < api_versions.APIVersion('2.77'): + msg = _( + '--os-compute-api-version 2.77 or greater is required ' + 'to support the --availability-zone option' + ) + raise exceptions.CommandError(msg) + + kwargs['availability_zone'] = parsed_args.availability_zone for server in parsed_args.server: - if support_az: - utils.find_resource( - compute_client.servers, - server - ).unshelve(availability_zone=parsed_args.availability_zone) - else: - utils.find_resource( - compute_client.servers, - server, - ).unshelve() + server_obj = utils.find_resource( + compute_client.servers, + server, + ) + + if server_obj.status.lower() not in ( + 'shelved', 'shelved_offloaded', + ): + continue + + server_obj.unshelve(**kwargs) + + if parsed_args.wait: + if not utils.wait_for_status( + compute_client.servers.get, server_obj.id, + success_status=('active', 'shutoff'), + callback=_show_progress, + ): + LOG.error(_('Error unshelving server %s'), server_obj.id) + self.app.stdout.write( + _('Error unshelving server: %s\n') % server_obj.id) + raise SystemExit diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9ad6d15508..2c0cadfc88 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6969,6 +6969,9 @@ def setUp(self): self.methods = { 'unshelve': None, } + self.attrs = { + 'status': 'SHELVED', + } def test_unshelve_one_server(self): self.run_method_with_servers('unshelve', 1) @@ -6976,55 +6979,74 @@ def test_unshelve_one_server(self): def test_unshelve_multi_servers(self): self.run_method_with_servers('unshelve', 3) - def test_unshelve_server_with_specified_az(self): - server = compute_fakes.FakeServer.create_one_server() + def test_unshelve_with_specified_az(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, '--availability-zone', "foo-az", + server.id, ] verifylist = [ ('availability_zone', "foo-az"), ('server', [server.id]) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - ex = self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) - self.assertIn( - '--os-compute-api-version 2.77 or greater is required', str(ex)) - -class TestServerUnshelveV277(TestServerUnshelve): - - def setUp(self): - super(TestServerUnshelveV277, self).setUp() - - self.server = compute_fakes.FakeServer.create_one_server( - methods=self.methods) - - # This is the return value for utils.find_resource() - self.servers_mock.get.return_value = self.server + self.cmd.take_action(parsed_args) - # Get the command object to test - self.cmd = server.UnshelveServer(self.app, None) + self.servers_mock.get.assert_called_with(server.id) + server.unshelve.assert_called_with(availability_zone="foo-az") - def test_specified_az_to_unshelve_with_v277(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.77') + def test_unshelve_with_specified_az_pre_v277(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.76') + server = compute_fakes.FakeServer.create_one_server( + attrs=self.attrs, methods=self.methods) arglist = [ + server.id, '--availability-zone', "foo-az", - self.server.id, ] verifylist = [ ('availability_zone', "foo-az"), - ('server', [self.server.id]) + ('server', [server.id]) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.77 or greater is required', str(ex)) - self.cmd.take_action(parsed_args) - self.servers_mock.get.assert_called_with(self.server.id) - self.server.unshelve.assert_called_with(availability_zone="foo-az") + @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( + attrs=self.attrs, methods=self.methods) + self.servers_mock.get.return_value = server + + arglist = ['--wait', server.name] + verifylist = [ + ('server', [server.name]), + ('wait', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.servers_mock.get.assert_called_once_with(server.name) + server.unshelve.assert_called_once_with() + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + server.id, + callback=mock.ANY, + success_status=('active', 'shutoff'), + ) class TestServerGeneral(TestServer): diff --git a/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml b/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml index ddd3293191..750abd6af1 100644 --- a/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml +++ b/releasenotes/notes/add-shelve-offload-wait-d0a5c8ba92586f72.yaml @@ -6,3 +6,5 @@ features: server in environments where automatic offloading is not configured, while ``--wait`` allows users to wait for the shelve and/or shelve offload operations to complete. + - | + Add support ``--wait`` option for ``server shelve``. From d33eb3e1da5730f13a539b70bd6eea04725b12e1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 8 Jan 2021 18:42:25 +0000 Subject: [PATCH 0347/1120] Remove retired Karbor support The Karbor project is being retired in Wallaby [1]. Remove the docs for its client. [1] http://lists.openstack.org/pipermail/openstack-discuss/2020-November/018643.html Change-Id: I52d0f6a76cc9bcfc8b33f0e2cd3751859770ac8a Signed-off-by: Stephen Finucane --- doc/requirements.txt | 1 - doc/source/cli/commands.rst | 9 --------- doc/source/cli/plugin-commands/index.rst | 1 - doc/source/cli/plugin-commands/karbor.rst | 4 ---- doc/source/contributor/plugins.rst | 1 - 5 files changed, 16 deletions(-) delete mode 100644 doc/source/cli/plugin-commands/karbor.rst diff --git a/doc/requirements.txt b/doc/requirements.txt index 28030ca422..60a877970c 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -18,7 +18,6 @@ python-designateclient>=2.7.0 # Apache-2.0 python-heatclient>=1.10.0 # Apache-2.0 python-ironicclient>=2.3.0 # Apache-2.0 python-ironic-inspector-client>=1.5.0 # Apache-2.0 -python-karborclient>=0.6.0 # Apache-2.0 python-manilaclient>=2.0.0 # Apache-2.0 python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 python-muranoclient>=0.8.2 # Apache-2.0 diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 497c79f0b9..0dfac00bdc 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -201,15 +201,6 @@ conflicts when creating new plugins. For a complete list check out * ``dataprocessing image``: (**Data Processing (Sahara)**) * ``dataprocessing image tags``: (**Data Processing (Sahara)**) * ``dataprocessing plugin``: (**Data Processing (Sahara)**) -* ``data protection plan``: (**Data Protection (Karbor)**) -* ``data protection restore``: (**Data Protection (Karbor)**) -* ``data protection provider``: (**Data Protection (Karbor)**) -* ``data protection protectable``: (**Data Protection (Karbor)**) -* ``data protection protectable instance``: (**Data Protection (Karbor)**) -* ``data protection trigger``: (**Data Protection (Karbor)**) -* ``data protection checkpoint``: (**Data Protection (Karbor)**) -* ``data protection scheduledoperation``: (**Data Protection (Karbor)**) -* ``data protection operationlog``: (**Data Protection (Karbor)**) * ``loadbalancer``: (**Load Balancer (Octavia)**) * ``loadbalancer healthmonitor``: (**Load Balancer (Octavia)**) * ``loadbalancer l7policy``: (**Load Balancer (Octavia)**) diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst index 8c9c5c13e3..4e1ce54b13 100644 --- a/doc/source/cli/plugin-commands/index.rst +++ b/doc/source/cli/plugin-commands/index.rst @@ -14,7 +14,6 @@ Plugin Commands heat ironic ironic-inspector - karbor manila mistral neutron diff --git a/doc/source/cli/plugin-commands/karbor.rst b/doc/source/cli/plugin-commands/karbor.rst deleted file mode 100644 index 0e28ba5733..0000000000 --- a/doc/source/cli/plugin-commands/karbor.rst +++ /dev/null @@ -1,4 +0,0 @@ -karbor ------- - -.. autoprogram-cliff:: openstack.data_protection.v1 diff --git a/doc/source/contributor/plugins.rst b/doc/source/contributor/plugins.rst index fe2cc14ad0..067c1b9912 100644 --- a/doc/source/contributor/plugins.rst +++ b/doc/source/contributor/plugins.rst @@ -30,7 +30,6 @@ The following is a list of projects that are an OpenStackClient plugin. - python-heatclient - python-ironicclient - python-ironic-inspector-client -- python-karborclient - python-mistralclient - python-muranoclient - python-neutronclient\*\*\* From bb15b29190dfe77ea0218a01edc1f4886993b177 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 11 Jan 2021 14:24:48 +0000 Subject: [PATCH 0348/1120] Add reno for change Ic3c555226a220efd9b0f27edffccf6c4c95c2747 Change Ic3c555226a220efd9b0f27edffccf6c4c95c2747 introduced some validation for the 'openstack server group create --policy' command. Call this out in the release notes. Change-Id: I7e00851a03470364db00f0f114fc724b0f686b72 Signed-off-by: Stephen Finucane --- releasenotes/notes/story-2007727-69b705c561309742.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 releasenotes/notes/story-2007727-69b705c561309742.yaml diff --git a/releasenotes/notes/story-2007727-69b705c561309742.yaml b/releasenotes/notes/story-2007727-69b705c561309742.yaml new file mode 100644 index 0000000000..4a9eeae39a --- /dev/null +++ b/releasenotes/notes/story-2007727-69b705c561309742.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The ``openstack server group create`` command will now validate the policy + value requested with ``--policy``, restricting it to the valid values + allowed by given microversions. From dd89efd5acbabcd4de0f071a2f7fbb93d067a950 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 11 Jan 2021 14:28:54 +0000 Subject: [PATCH 0349/1120] network: Address nits for I3c313fc9329837dde67815901528a34dca98ebcc Address comments left in the review for $subject. Change-Id: I69449112027736152c9fb62f5fe427efd6a25107 Signed-off-by: Stephen Finucane --- openstackclient/network/v2/address_group.py | 6 +----- .../tests/unit/network/v2/test_address_group.py | 7 +++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/openstackclient/network/v2/address_group.py b/openstackclient/network/v2/address_group.py index fe1e14a316..c5b2f12606 100644 --- a/openstackclient/network/v2/address_group.py +++ b/openstackclient/network/v2/address_group.py @@ -36,10 +36,7 @@ def _get_columns(item): def _format_addresses(addresses): - ret = [] - for addr in addresses: - ret.append(str(netaddr.IPNetwork(addr))) - return ret + return [str(netaddr.IPNetwork(addr)) for addr in addresses] def _get_attrs(client_manager, parsed_args): @@ -185,7 +182,6 @@ def take_action(self, parsed_args): parsed_args.project, parsed_args.project_domain, ).id - attrs['tenant_id'] = project_id attrs['project_id'] = project_id data = client.address_groups(**attrs) diff --git a/openstackclient/tests/unit/network/v2/test_address_group.py b/openstackclient/tests/unit/network/v2/test_address_group.py index 3b2b1ab6b9..e4fa8ab3dc 100644 --- a/openstackclient/tests/unit/network/v2/test_address_group.py +++ b/openstackclient/tests/unit/network/v2/test_address_group.py @@ -282,7 +282,7 @@ def test_address_group_list_project(self): columns, data = self.cmd.take_action(parsed_args) self.network.address_groups.assert_called_once_with( - **{'tenant_id': project.id, 'project_id': project.id}) + project_id=project.id) self.assertEqual(self.columns, columns) self.assertItemsEqual(self.data, list(data)) @@ -297,11 +297,10 @@ def test_address_group_project_domain(self): ('project', project.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} - self.network.address_groups.assert_called_once_with(**filters) + self.network.address_groups.assert_called_once_with( + project_id=project.id) self.assertEqual(self.columns, columns) self.assertItemsEqual(self.data, list(data)) From ca7f23d0d1876dc53ef4d5ecbf2c5f367aafe13e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Dec 2020 12:41:25 +0000 Subject: [PATCH 0350/1120] compute: Add 'server volume update' command We're not going to expose the ability to swap volumes since that's a things humans should not generally use. From the API docs [1]: When updating volumeId, this API is typically meant to only be used as part of a larger orchestrated volume migration operation initiated in the block storage service via the os-retype or os-migrate_volume volume actions. Direct usage of this API to update volumeId is not recommended and may result in needing to hard reboot the server to update details within the guest such as block storage serial IDs. Furthermore, updating volumeId via this API is only implemented by certain compute drivers. We *do* want users to have the ability to change the delete on termination behavior though, so that's what we expose. [1] https://docs.openstack.org/api-ref/compute/?expanded=update-a-volume-attachment-detail#update-a-volume-attachment Change-Id: I50938e1237b4d298521b26a5f9cb90c018dfebaf Signed-off-by: Stephen Finucane --- lower-constraints.txt | 2 +- openstackclient/compute/v2/server.py | 16 +-- openstackclient/compute/v2/server_volume.py | 67 ++++++++++ .../unit/compute/v2/test_server_volume.py | 120 ++++++++++++++++++ ...server-volume-update-89740dca61596dd1.yaml | 3 + requirements.txt | 2 +- setup.cfg | 1 + 7 files changed, 201 insertions(+), 10 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 287f7d4599..25f71e45a2 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -71,7 +71,7 @@ python-cinderclient==3.3.0 python-dateutil==2.5.3 python-keystoneclient==3.22.0 python-mimeparse==1.6.0 -python-novaclient==15.1.0 +python-novaclient==17.0.0 python-subunit==1.0.0 pytz==2013.6 PyYAML==3.13 diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 50299d65d0..fa27f68c88 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -513,27 +513,27 @@ def get_parser(self, prog_name): '--tag', metavar='', help=_( - "Tag for the attached volume. " - "(Supported by API versions '2.49' - '2.latest')" + 'Tag for the attached volume ' + '(supported by --os-compute-api-version 2.49 or above)' ), ) + # TODO(stephenfin): These should be called 'delete-on-termination' and + # 'preserve-on-termination' termination_group = parser.add_mutually_exclusive_group() termination_group.add_argument( '--enable-delete-on-termination', action='store_true', help=_( - "Specify if the attached volume should be deleted when the " - "server is destroyed. " - "(Supported by API versions '2.79' - '2.latest')" + 'Delete the volume when the server is destroyed ' + '(supported by --os-compute-api-version 2.79 or above)' ), ) termination_group.add_argument( '--disable-delete-on-termination', action='store_true', help=_( - "Specify if the attached volume should not be deleted when " - "the server is destroyed. " - "(Supported by API versions '2.79' - '2.latest')" + 'Do not delete the volume when the server is destroyed ' + '(supported by --os-compute-api-version 2.79 or above)' ), ) return parser diff --git a/openstackclient/compute/v2/server_volume.py b/openstackclient/compute/v2/server_volume.py index 8a931ae5ab..b53c92fe5e 100644 --- a/openstackclient/compute/v2/server_volume.py +++ b/openstackclient/compute/v2/server_volume.py @@ -16,6 +16,7 @@ from novaclient import api_versions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ @@ -71,3 +72,69 @@ def take_action(self, parsed_args): ) for s in volumes ), ) + + +class UpdateServerVolume(command.Command): + """Update a volume attachment on the server.""" + + def get_parser(self, prog_name): + parser = super(UpdateServerVolume, self).get_parser(prog_name) + parser.add_argument( + 'server', + help=_('Server to update volume for (name or ID)'), + ) + parser.add_argument( + 'volume', + help=_('Volume (ID)'), + ) + termination_group = parser.add_mutually_exclusive_group() + termination_group.add_argument( + '--delete-on-termination', + action='store_true', + dest='delete_on_termination', + default=None, + help=_( + 'Delete the volume when the server is destroyed ' + '(supported by --os-compute-api-version 2.85 or above)' + ), + ) + termination_group.add_argument( + '--preserve-on-termination', + action='store_false', + dest='delete_on_termination', + help=_( + 'Preserve the volume when the server is destroyed ' + '(supported by --os-compute-api-version 2.85 or above)' + ), + ) + return parser + + def take_action(self, parsed_args): + + compute_client = self.app.client_manager.compute + + if parsed_args.delete_on_termination is not None: + if compute_client.api_version < api_versions.APIVersion('2.85'): + msg = _( + '--os-compute-api-version 2.85 or greater is required to ' + 'support the --(no-)delete-on-termination option' + ) + raise exceptions.CommandError(msg) + + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + + # NOTE(stephenfin): This may look silly, and that's because it is. + # This API was originally used only for the swapping volumes, which + # is an internal operation that should only be done by + # orchestration software rather than a human. We're not going to + # expose that, but we are going to expose the ability to change the + # delete on termination behavior. + compute_client.volumes.update_server_volume( + server.id, + parsed_args.volume, + parsed_args.volume, + delete_on_termination=parsed_args.delete_on_termination, + ) diff --git a/openstackclient/tests/unit/compute/v2/test_server_volume.py b/openstackclient/tests/unit/compute/v2/test_server_volume.py index d09c287450..4d4916b7ec 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_volume.py +++ b/openstackclient/tests/unit/compute/v2/test_server_volume.py @@ -12,6 +12,7 @@ # from novaclient import api_versions +from osc_lib import exceptions from openstackclient.compute.v2 import server_volume from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -165,3 +166,122 @@ def test_server_volume_list_with_delete_on_attachment(self): ) self.servers_volumes_mock.get_server_volumes.assert_called_once_with( self.server.id) + + +class TestServerVolumeUpdate(TestServerVolume): + + def setUp(self): + super().setUp() + + self.server = compute_fakes.FakeServer.create_one_server() + self.servers_mock.get.return_value = self.server + + # Get the command object to test + self.cmd = server_volume.UpdateServerVolume(self.app, None) + + def test_server_volume_update(self): + + arglist = [ + self.server.id, + 'foo', + ] + verifylist = [ + ('server', self.server.id), + ('volume', 'foo'), + ('delete_on_termination', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # This is a no-op + self.servers_volumes_mock.update_server_volume.assert_not_called() + self.assertIsNone(result) + + def test_server_volume_update_with_delete_on_termination(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.85') + + arglist = [ + self.server.id, + 'foo', + '--delete-on-termination', + ] + verifylist = [ + ('server', self.server.id), + ('volume', 'foo'), + ('delete_on_termination', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.update_server_volume.assert_called_once_with( + self.server.id, 'foo', 'foo', + delete_on_termination=True) + self.assertIsNone(result) + + def test_server_volume_update_with_preserve_on_termination(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.85') + + arglist = [ + self.server.id, + 'foo', + '--preserve-on-termination', + ] + verifylist = [ + ('server', self.server.id), + ('volume', 'foo'), + ('delete_on_termination', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_volumes_mock.update_server_volume.assert_called_once_with( + self.server.id, 'foo', 'foo', + delete_on_termination=False) + self.assertIsNone(result) + + def test_server_volume_update_with_delete_on_termination_pre_v285(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.84') + + arglist = [ + self.server.id, + 'foo', + '--delete-on-termination', + ] + verifylist = [ + ('server', self.server.id), + ('volume', 'foo'), + ('delete_on_termination', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_server_volume_update_with_preserve_on_termination_pre_v285(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.84') + + arglist = [ + self.server.id, + 'foo', + '--preserve-on-termination', + ] + verifylist = [ + ('server', self.server.id), + ('volume', 'foo'), + ('delete_on_termination', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml b/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml index d1c0501ff8..b8a3de1a4b 100644 --- a/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml +++ b/releasenotes/notes/add-server-volume-update-89740dca61596dd1.yaml @@ -3,3 +3,6 @@ features: - | Add ``server volume list`` command, to list the volumes attached to an instance. + - | + Add ``server volume update`` command, to update the volumes attached to + an instance. diff --git a/requirements.txt b/requirements.txt index ed55ce3794..21e291013b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,6 @@ openstacksdk>=0.52.0 # Apache-2.0 osc-lib>=2.3.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 -python-novaclient>=15.1.0 # Apache-2.0 +python-novaclient>=17.0.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 stevedore>=2.0.1 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 3fe7fa38f9..ef6db57879 100644 --- a/setup.cfg +++ b/setup.cfg @@ -154,6 +154,7 @@ openstack.compute.v2 = server_image_create = openstackclient.compute.v2.server_image:CreateServerImage server_volume_list = openstackclient.compute.v2.server_volume:ListServerVolume + server_volume_update = openstackclient.compute.v2.server_volume:UpdateServerVolume usage_list = openstackclient.compute.v2.usage:ListUsage usage_show = openstackclient.compute.v2.usage:ShowUsage From e01e59caeb56cb7bf4f38403b82d0a265b163098 Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Wed, 14 Oct 2020 11:35:22 -0500 Subject: [PATCH 0351/1120] Support remote-address-group in SG rules Add support for using remote-address-group in security group rules. Change-Id: Ib1972244d484839943bc3cda07519a6c6d4b945a Implements: blueprint address-groups-in-sg-rules Depends-On: https://review.opendev.org/755644 --- .../network/v2/security_group_rule.py | 14 ++++++ .../tests/unit/network/v2/fakes.py | 1 + .../v2/test_security_group_rule_network.py | 43 +++++++++++++++++++ ...s-groups-in-sg-rules-b3b7742e4e6f5745.yaml | 9 ++++ 4 files changed, 67 insertions(+) create mode 100644 releasenotes/notes/bp-address-groups-in-sg-rules-b3b7742e4e6f5745.yaml diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index a770393313..17241ed256 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -126,6 +126,12 @@ def update_parser_common(self, parser): metavar="", help=_("Remote security group (name or ID)"), ) + if self.is_neutron: + remote_group.add_argument( + "--remote-address-group", + metavar="", + help=_("Remote address group (name or ID)"), + ) # NOTE(efried): The --dst-port, --protocol, and --proto options exist # for both nova-network and neutron, but differ slightly. For the sake @@ -328,6 +334,11 @@ def take_action_network(self, client, parsed_args): parsed_args.remote_group, ignore_missing=False ).id + elif parsed_args.remote_address_group is not None: + attrs['remote_address_group_id'] = client.find_address_group( + parsed_args.remote_address_group, + ignore_missing=False + ).id elif parsed_args.remote_ip is not None: attrs['remote_ip_prefix'] = parsed_args.remote_ip elif attrs['ethertype'] == 'IPv4': @@ -507,6 +518,8 @@ def _get_column_headers(self, parsed_args): 'Direction', 'Remote Security Group', ) + if self.is_neutron: + column_headers = column_headers + ('Remote Address Group',) if parsed_args.group is None: column_headers = column_headers + ('Security Group',) return column_headers @@ -526,6 +539,7 @@ def take_action_network(self, client, parsed_args): 'port_range', 'direction', 'remote_group_id', + 'remote_address_group_id', ) # Get the security group rules using the requested query. diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 798cfd967b..d690669038 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1382,6 +1382,7 @@ def create_one_security_group_rule(attrs=None): 'port_range_min': None, 'protocol': None, 'remote_group_id': None, + 'remote_address_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'security_group_id': 'security-group-id-' + uuid.uuid4().hex, 'tenant_id': 'project-id-' + uuid.uuid4().hex, diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py index 0141161134..bcdb0c2618 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py @@ -46,6 +46,9 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group = \ network_fakes.FakeSecurityGroup.create_one_security_group() + # The address group to be used in security group rules + _address_group = network_fakes.FakeAddressGroup.create_one_address_group() + expected_columns = ( 'description', 'direction', @@ -55,6 +58,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'port_range_min', 'project_id', 'protocol', + 'remote_address_group_id', 'remote_group_id', 'remote_ip_prefix', 'security_group_id', @@ -77,6 +81,7 @@ def _setup_security_group_rule(self, attrs=None): self._security_group_rule.port_range_min, self._security_group_rule.project_id, self._security_group_rule.protocol, + self._security_group_rule.remote_address_group_id, self._security_group_rule.remote_group_id, self._security_group_rule.remote_ip_prefix, self._security_group_rule.security_group_id, @@ -88,6 +93,9 @@ def setUp(self): self.network.find_security_group = mock.Mock( return_value=self._security_group) + self.network.find_address_group = mock.Mock( + return_value=self._address_group) + self.projects_mock.get.return_value = self.project self.domains_mock.get.return_value = self.domain @@ -103,6 +111,7 @@ def test_create_all_remote_options(self): arglist = [ '--remote-ip', '10.10.0.0/24', '--remote-group', self._security_group.id, + '--remote-address-group', self._address_group.id, self._security_group.id, ] self.assertRaises(tests_utils.ParserException, @@ -258,6 +267,34 @@ def test_create_protocol_any(self): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) + def test_create_remote_address_group(self): + self._setup_security_group_rule({ + 'protocol': 'icmp', + 'remote_address_group_id': self._address_group.id, + }) + arglist = [ + '--protocol', 'icmp', + '--remote-address-group', self._address_group.name, + self._security_group.id, + ] + verifylist = [ + ('remote_address_group', self._address_group.name), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ether_type, + 'protocol': self._security_group_rule.protocol, + 'remote_address_group_id': self._address_group.id, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + def test_create_remote_group(self): self._setup_security_group_rule({ 'protocol': 'tcp', @@ -878,6 +915,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'Port Range', 'Direction', 'Remote Security Group', + 'Remote Address Group', ) expected_columns_no_group = ( 'ID', @@ -887,6 +925,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'Port Range', 'Direction', 'Remote Security Group', + 'Remote Address Group', 'Security Group', ) @@ -902,6 +941,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rule), _security_group_rule.direction, _security_group_rule.remote_group_id, + _security_group_rule.remote_address_group_id, )) expected_data_no_group.append(( _security_group_rule.id, @@ -912,6 +952,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rule), _security_group_rule.direction, _security_group_rule.remote_group_id, + _security_group_rule.remote_address_group_id, _security_group_rule.security_group_id, )) @@ -1041,6 +1082,7 @@ class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'port_range_min', 'project_id', 'protocol', + 'remote_address_group_id', 'remote_group_id', 'remote_ip_prefix', 'security_group_id', @@ -1055,6 +1097,7 @@ class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rule.port_range_min, _security_group_rule.project_id, _security_group_rule.protocol, + _security_group_rule.remote_address_group_id, _security_group_rule.remote_group_id, _security_group_rule.remote_ip_prefix, _security_group_rule.security_group_id, diff --git a/releasenotes/notes/bp-address-groups-in-sg-rules-b3b7742e4e6f5745.yaml b/releasenotes/notes/bp-address-groups-in-sg-rules-b3b7742e4e6f5745.yaml new file mode 100644 index 0000000000..a962eadf6f --- /dev/null +++ b/releasenotes/notes/bp-address-groups-in-sg-rules-b3b7742e4e6f5745.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add ``--remote-address-group`` option to ``security group rule create`` + command for using an address group as the source/destination in security + group rules. Also add field ``remote_address_group_id`` to the output of + ``security group rule show`` and add column ``Remote Address Group`` to + the output of ``security group rule list``. + [Blueprint `address-groups-in-sg-rules `_] From 262e525aada8bfaedb4545be5d2bbd27edcc55fd Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Dec 2020 13:07:03 +0000 Subject: [PATCH 0352/1120] compute: Add missing options for 'hypervisor list' Yet more pagination parameters. Change-Id: I9f0145c89ddc49c1d907e6e6e294319cf80fc6ff Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/hypervisor.py | 63 ++++++++++-- .../tests/unit/compute/v2/test_hypervisor.py | 96 +++++++++++++++++++ ...hypervisor-list-opts-71da2cc36eac4edd.yaml | 5 + 3 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/add-missing-hypervisor-list-opts-71da2cc36eac4edd.yaml diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 8fdb66983c..5f7497b5b1 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -22,6 +22,7 @@ from novaclient import exceptions as nova_exceptions from osc_lib.cli import format_columns from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ @@ -33,10 +34,31 @@ class ListHypervisor(command.Lister): def get_parser(self, prog_name): parser = super(ListHypervisor, self).get_parser(prog_name) parser.add_argument( - "--matching", - metavar="", + '--matching', + metavar='', help=_("Filter hypervisors using substring") ) + parser.add_argument( + '--marker', + metavar='', + help=_( + "The UUID of the last hypervisor of the previous page; " + "displays list of hypervisors after 'marker'. " + "(supported with --os-compute-api-version 2.33 or above)" + ), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_( + "Maximum number of hypervisors to display. Note that there " + "is a configurable max limit on the server, and the limit " + "that is used will be the minimum of what is requested " + "here and what is configured in the server. " + "(supported with --os-compute-api-version 2.33 or above)" + ), + ) parser.add_argument( '--long', action='store_true', @@ -46,6 +68,33 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + + list_opts = {} + + if parsed_args.matching and (parsed_args.marker or parsed_args.limit): + msg = _( + '--matching is not compatible with --marker or --limit' + ) + raise exceptions.CommandError(msg) + + if parsed_args.marker: + if compute_client.api_version < api_versions.APIVersion('2.33'): + msg = _( + '--os-compute-api-version 2.33 or greater is required to ' + 'support the --marker option' + ) + raise exceptions.CommandError(msg) + list_opts['marker'] = parsed_args.marker + + if parsed_args.limit: + if compute_client.api_version < api_versions.APIVersion('2.33'): + msg = _( + '--os-compute-api-version 2.33 or greater is required to ' + 'support the --limit option' + ) + raise exceptions.CommandError(msg) + list_opts['limit'] = parsed_args.limit + columns = ( "ID", "Hypervisor Hostname", @@ -59,12 +108,12 @@ def take_action(self, parsed_args): if parsed_args.matching: data = compute_client.hypervisors.search(parsed_args.matching) else: - data = compute_client.hypervisors.list() + data = compute_client.hypervisors.list(**list_opts) - return (columns, - (utils.get_item_properties( - s, columns, - ) for s in data)) + return ( + columns, + (utils.get_item_properties(s, columns) for s in data), + ) class ShowHypervisor(command.ShowOne): diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor.py b/openstackclient/tests/unit/compute/v2/test_hypervisor.py index 518109c6b0..3220a76450 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor.py @@ -173,6 +173,30 @@ def test_hypervisor_list_matching_option_not_found(self): 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') + + arglist = [ + '--matching', self.hypervisors[0].hypervisor_hostname, + '--limit', '1', + '--marker', self.hypervisors[0].hypervisor_hostname, + ] + verifylist = [ + ('matching', self.hypervisors[0].hypervisor_hostname), + ('limit', 1), + ('marker', self.hypervisors[0].hypervisor_hostname), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--matching is not compatible with --marker or --limit', str(ex)) + def test_hypervisor_list_long_option(self): arglist = [ '--long', @@ -191,6 +215,78 @@ def test_hypervisor_list_long_option(self): 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') + + arglist = [ + '--limit', '1', + ] + verifylist = [ + ('limit', 1), + ] + + 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') + + arglist = [ + '--limit', '1', + ] + verifylist = [ + ('limit', 1), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.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') + + arglist = [ + '--marker', 'test_hyp', + ] + verifylist = [ + ('marker', 'test_hyp'), + ] + + 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') + + arglist = [ + '--marker', 'test_hyp', + ] + verifylist = [ + ('marker', 'test_hyp'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.33 or greater is required', str(ex)) + class TestHypervisorShow(TestHypervisor): diff --git a/releasenotes/notes/add-missing-hypervisor-list-opts-71da2cc36eac4edd.yaml b/releasenotes/notes/add-missing-hypervisor-list-opts-71da2cc36eac4edd.yaml new file mode 100644 index 0000000000..c5401210a4 --- /dev/null +++ b/releasenotes/notes/add-missing-hypervisor-list-opts-71da2cc36eac4edd.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--limit`` and ``--marker`` options to ``hypervisor list`` command, to + configure pagination of results. From 8a164bb09c0801c3ffd2431d41c3e232388ab407 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Dec 2020 13:13:16 +0000 Subject: [PATCH 0353/1120] compute: Add '--force' option to 'server delete' This is an admin-only operation by default but can be useful. Change-Id: I25a4da697e27c0fba4d28b504377667eb18f15fe Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 12 ++++++++++- .../tests/unit/compute/v2/test_server.py | 21 +++++++++++++++++++ ...g-server-delete-opts-071c3e054e3ce674.yaml | 5 +++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-missing-server-delete-opts-071c3e054e3ce674.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index fa27f68c88..aa4f237208 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1317,6 +1317,11 @@ def get_parser(self, prog_name): nargs="+", help=_('Server(s) to delete (name or ID)'), ) + parser.add_argument( + '--force', + action='store_true', + help=_('Force delete server(s)'), + ) parser.add_argument( '--wait', action='store_true', @@ -1335,7 +1340,12 @@ def _show_progress(progress): for server in parsed_args.server: server_obj = utils.find_resource( compute_client.servers, server) - compute_client.servers.delete(server_obj.id) + + if parsed_args.force: + compute_client.servers.force_delete(server_obj.id) + else: + compute_client.servers.delete(server_obj.id) + if parsed_args.wait: if not utils.wait_for_delete(compute_client.servers, server_obj.id, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 0f33dd7047..bde0699b06 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2850,6 +2850,7 @@ def setUp(self): super(TestServerDelete, self).setUp() self.servers_mock.delete.return_value = None + self.servers_mock.force_delete.return_value = None # Get the command object to test self.cmd = server.DeleteServer(self.app, None) @@ -2868,6 +2869,26 @@ def test_server_delete_no_options(self): result = self.cmd.take_action(parsed_args) self.servers_mock.delete.assert_called_with(servers[0].id) + self.servers_mock.force_delete.assert_not_called() + self.assertIsNone(result) + + def test_server_delete_with_force(self): + servers = self.setup_servers_mock(count=1) + + arglist = [ + servers[0].id, + '--force', + ] + verifylist = [ + ('server', [servers[0].id]), + ('force', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.force_delete.assert_called_with(servers[0].id) + self.servers_mock.delete.assert_not_called() self.assertIsNone(result) def test_server_delete_multi_servers(self): diff --git a/releasenotes/notes/add-missing-server-delete-opts-071c3e054e3ce674.yaml b/releasenotes/notes/add-missing-server-delete-opts-071c3e054e3ce674.yaml new file mode 100644 index 0000000000..2c1fc46ed2 --- /dev/null +++ b/releasenotes/notes/add-missing-server-delete-opts-071c3e054e3ce674.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--force`` option to ``server delete`` command, allowing users to + force delete a server. This is admin-only by default. From dfa869ed1dbd54c76a9d7cd7d520f0d21064918d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Dec 2020 17:36:53 +0000 Subject: [PATCH 0354/1120] compute: Improve 'server migration list' options Improve both the '--user' and '--project' options to allow names as well as UUIDs. There's no release note included since this entire command was added in change I15b4a5aca8d0dee59dd293e7b1c7272cdfbeea20, which hasn't been included in a release yet. Change-Id: I7654f3ffc54d38d5cfb03d8d1b2f4dc4fb06fb3d Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 31 +++++++---- .../tests/unit/compute/v2/test_server.py | 51 ++++++++++++------- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index aa4f237208..1e38869e76 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2369,21 +2369,21 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - dest='project_id', help=_( - "Filter migrations by project (ID) " + "Filter migrations by project (name or ID) " "(supported with --os-compute-api-version 2.80 or above)" ), ) + identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--user', metavar='', - dest='user_id', help=_( - "Filter migrations by user (ID) " + "Filter migrations by user (name or ID) " "(supported with --os-compute-api-version 2.80 or above)" ), ) + identity_common.add_user_domain_option_to_parser(parser) return parser def print_migrations(self, parsed_args, compute_client, migrations): @@ -2402,9 +2402,9 @@ def print_migrations(self, parsed_args, compute_client, migrations): columns.insert(len(columns) - 2, "Type") if compute_client.api_version >= api_versions.APIVersion("2.80"): - if parsed_args.project_id: + if parsed_args.project: columns.insert(len(columns) - 2, "Project") - if parsed_args.user_id: + if parsed_args.user: columns.insert(len(columns) - 2, "User") return ( @@ -2414,6 +2414,7 @@ def print_migrations(self, parsed_args, compute_client, migrations): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity search_opts = { 'host': parsed_args.host, @@ -2469,23 +2470,33 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) search_opts['changes_before'] = parsed_args.changes_before - if parsed_args.project_id: + if parsed_args.project: if compute_client.api_version < api_versions.APIVersion('2.80'): msg = _( '--os-compute-api-version 2.80 or greater is required to ' 'support the --project option' ) raise exceptions.CommandError(msg) - search_opts['project_id'] = parsed_args.project_id - if parsed_args.user_id: + search_opts['project_id'] = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + + if parsed_args.user: if compute_client.api_version < api_versions.APIVersion('2.80'): msg = _( '--os-compute-api-version 2.80 or greater is required to ' 'support the --user option' ) raise exceptions.CommandError(msg) - search_opts['user_id'] = parsed_args.user_id + + search_opts['user_id'] = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ).id migrations = compute_client.migrations.list(**search_opts) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index bde0699b06..ea33463ce8 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -28,6 +28,7 @@ from openstackclient.compute.v2 import server from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +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.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils @@ -4548,9 +4549,21 @@ class TestListMigrationV280(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] + project = identity_fakes.FakeProject.create_one_project() + user = identity_fakes.FakeUser.create_one_user() + def setUp(self): super(TestListMigrationV280, self).setUp() + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + self.projects_mock.get.return_value = self.project + self.users_mock.get.return_value = self.user + self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.80') @@ -4561,7 +4574,7 @@ def test_server_migration_list_with_project(self): '--marker', 'test_kp', '--changes-since', '2019-08-07T08:03:25Z', '--changes-before', '2019-08-09T08:03:25Z', - '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3' + '--project', self.project.id ] verifylist = [ ('status', 'migrating'), @@ -4569,7 +4582,7 @@ def test_server_migration_list_with_project(self): ('marker', 'test_kp'), ('changes_since', '2019-08-07T08:03:25Z'), ('changes_before', '2019-08-09T08:03:25Z'), - ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3') + ('project', self.project.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -4580,7 +4593,7 @@ def test_server_migration_list_with_project(self): 'limit': 1, 'marker': 'test_kp', 'host': None, - 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3', + 'project_id': self.project.id, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", } @@ -4605,7 +4618,7 @@ def test_get_migrations_with_project_pre_v280(self): verifylist = [ ('status', 'migrating'), ('changes_before', '2019-08-09T08:03:25Z'), - ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3') + ('project', '0c2accde-644a-45fa-8c10-e76debc7fbc3') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) ex = self.assertRaises( @@ -4623,7 +4636,7 @@ def test_server_migration_list_with_user(self): '--marker', 'test_kp', '--changes-since', '2019-08-07T08:03:25Z', '--changes-before', '2019-08-09T08:03:25Z', - '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + '--user', self.user.id, ] verifylist = [ ('status', 'migrating'), @@ -4631,7 +4644,7 @@ def test_server_migration_list_with_user(self): ('marker', 'test_kp'), ('changes_since', '2019-08-07T08:03:25Z'), ('changes_before', '2019-08-09T08:03:25Z'), - ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ('user', self.user.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -4642,7 +4655,7 @@ def test_server_migration_list_with_user(self): 'limit': 1, 'marker': 'test_kp', 'host': None, - 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6', + 'user_id': self.user.id, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", } @@ -4662,12 +4675,12 @@ def test_get_migrations_with_user_pre_v280(self): arglist = [ '--status', 'migrating', '--changes-before', '2019-08-09T08:03:25Z', - '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + '--user', self.user.id, ] verifylist = [ ('status', 'migrating'), ('changes_before', '2019-08-09T08:03:25Z'), - ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ('user', self.user.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) ex = self.assertRaises( @@ -4684,16 +4697,16 @@ def test_server_migration_list_with_project_and_user(self): '--limit', '1', '--changes-since', '2019-08-07T08:03:25Z', '--changes-before', '2019-08-09T08:03:25Z', - '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3', - '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + '--project', self.project.id, + '--user', self.user.id, ] verifylist = [ ('status', 'migrating'), ('limit', 1), ('changes_since', '2019-08-07T08:03:25Z'), ('changes_before', '2019-08-09T08:03:25Z'), - ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3'), - ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ('project', self.project.id), + ('user', self.user.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -4703,8 +4716,8 @@ def test_server_migration_list_with_project_and_user(self): 'status': 'migrating', 'limit': 1, 'host': None, - 'project_id': '0c2accde-644a-45fa-8c10-e76debc7fbc3', - 'user_id': 'dd214878-ca12-40fb-b035-fa7d2c1e86d6', + 'project_id': self.project.id, + 'user_id': self.user.id, 'changes_since': '2019-08-07T08:03:25Z', 'changes_before': "2019-08-09T08:03:25Z", } @@ -4727,14 +4740,14 @@ def test_get_migrations_with_project_and_user_pre_v280(self): arglist = [ '--status', 'migrating', '--changes-before', '2019-08-09T08:03:25Z', - '--project', '0c2accde-644a-45fa-8c10-e76debc7fbc3', - '--user', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6' + '--project', self.project.id, + '--user', self.user.id, ] verifylist = [ ('status', 'migrating'), ('changes_before', '2019-08-09T08:03:25Z'), - ('project_id', '0c2accde-644a-45fa-8c10-e76debc7fbc3'), - ('user_id', 'dd214878-ca12-40fb-b035-fa7d2c1e86d6') + ('project', self.project.id), + ('user', self.user.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) ex = self.assertRaises( From 0cc878e5b053765a0d3c13f5588bc160b05a388b Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 17 Dec 2020 17:08:22 +0000 Subject: [PATCH 0355/1120] Add device profile to ``port`` Added device profile parameter to ``port create`` command. Related-Bug: #1906602 Change-Id: I4c222ac334d3a0a0ee568ed1e0bc8518baa375e1 --- lower-constraints.txt | 2 +- openstackclient/network/v2/port.py | 8 +++++ .../tests/unit/network/v2/fakes.py | 1 + .../tests/unit/network/v2/test_port.py | 29 +++++++++++++++++++ .../port-device-profile-4a3bf800da21c778.yaml | 4 +++ requirements.txt | 2 +- 6 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/port-device-profile-4a3bf800da21c778.yaml diff --git a/lower-constraints.txt b/lower-constraints.txt index 25f71e45a2..f93db8aa8c 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.52.0 +openstacksdk==0.53.0 os-client-config==2.1.0 os-service-types==1.7.0 os-testr==1.0.0 diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index cb77759efd..dfdb604d55 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -168,6 +168,9 @@ def _get_attrs(client_manager, parsed_args): parsed_args.numa_policy_legacy): attrs['numa_affinity_policy'] = 'legacy' + if 'device_profile' in parsed_args and parsed_args.device_profile: + attrs['device_profile'] = parsed_args.device_profile + return attrs @@ -443,6 +446,11 @@ def get_parser(self, prog_name): "ip-address=[,mac-address=] " "(repeat option to set multiple allowed-address pairs)") ) + parser.add_argument( + '--device-profile', + metavar='', + help=_('Cyborg port device profile') + ) _tag.add_tag_option_to_parser_for_create(parser, _('port')) return parser diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 798cfd967b..32e09ba845 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -697,6 +697,7 @@ def create_one_port(attrs=None): 'description': 'description-' + uuid.uuid4().hex, 'device_id': 'device-id-' + uuid.uuid4().hex, 'device_owner': 'compute:nova', + 'device_profile': 'cyborg_device_profile_1', 'dns_assignment': [{}], 'dns_domain': 'dns-domain-' + uuid.uuid4().hex, 'dns_name': 'dns-name-' + uuid.uuid4().hex, diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index e21f9d01f1..c8bced71c3 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -54,6 +54,7 @@ def _get_common_cols_data(fake_port): 'description', 'device_id', 'device_owner', + 'device_profile', 'dns_assignment', 'dns_domain', 'dns_name', @@ -86,6 +87,7 @@ def _get_common_cols_data(fake_port): fake_port.description, fake_port.device_id, fake_port.device_owner, + fake_port.device_profile, format_columns.ListDictColumn(fake_port.dns_assignment), fake_port.dns_domain, fake_port.dns_name, @@ -737,6 +739,33 @@ def test_create_with_numa_affinity_policy_legacy(self): def test_create_with_numa_affinity_policy_null(self): self._test_create_with_numa_affinity_policy() + def test_create_with_device_profile(self): + arglist = [ + '--network', self._port.network_id, + '--device-profile', 'cyborg_device_profile_1', + 'test-port', + ] + + verifylist = [ + ('network', self._port.network_id,), + ('device_profile', self._port.device_profile,), + ('name', 'test-port'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + create_args = { + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'name': 'test-port', + 'device_profile': 'cyborg_device_profile_1', + } + self.network.create_port.assert_called_once_with(**create_args) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, data) + class TestDeletePort(TestPort): diff --git a/releasenotes/notes/port-device-profile-4a3bf800da21c778.yaml b/releasenotes/notes/port-device-profile-4a3bf800da21c778.yaml new file mode 100644 index 0000000000..db0d495fd7 --- /dev/null +++ b/releasenotes/notes/port-device-profile-4a3bf800da21c778.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add device profile to ``port create`` command. diff --git a/requirements.txt b/requirements.txt index 21e291013b..5077ad779b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,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.52.0 # Apache-2.0 +openstacksdk>=0.53.0 # Apache-2.0 osc-lib>=2.3.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 From 1a6df700be2507bcec760994e64042d03b09ae16 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 15 Jan 2021 17:06:20 +0000 Subject: [PATCH 0356/1120] compute: Add 'server * --all-projects' option Add an '--all-projects' option to a number of commands: - server delete - server start - server stop This is in addition to 'server list', which already supports this option. This option allows users to request the corresponding action on one or more servers using the server names when that server exists in another project. This is admin-only by default. As part of this work, we also introduce a 'boolenv' helper function that allows us to parse the environment variable as a boolean using 'bool_from_string' helper provided by oslo.utils. This could probably be clever and it has the unfortunate side effect of modifying the help text in environments where this is configured, but it's good enough for now. It also appears to add a new dependency, in the form of oslo.utils, but that dependency was already required by osc-lib and probably more. Change-Id: I4811f8f66dcb14ed99cc1cfb80b00e2d77afe45f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 70 ++++++++++++++++--- .../tests/unit/compute/v2/test_server.py | 66 +++++++++++++++++ ...ver-ops-all-projects-2ce2202cdf617184.yaml | 7 ++ requirements.txt | 1 + 4 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/server-ops-all-projects-2ce2202cdf617184.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 02fc5816b8..15a5084d0d 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -31,6 +31,7 @@ from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from oslo_utils import strutils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -193,6 +194,24 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True): return info +def boolenv(*vars, default=False): + """Search for the first defined of possibly many bool-like env vars. + + Returns the first environment variable defined in vars, or returns the + default. + + :param vars: Arbitrary strings to search for. Case sensitive. + :param default: The default to return if no value found. + :returns: A boolean corresponding to the value found, else the default if + no value found. + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return strutils.bool_from_string(value) + return default + + class AddFixedIP(command.Command): _description = _("Add fixed IP address to server") @@ -1322,6 +1341,15 @@ def get_parser(self, prog_name): action='store_true', help=_('Force delete server(s)'), ) + parser.add_argument( + '--all-projects', + action='store_true', + default=boolenv('ALL_PROJECTS'), + help=_( + 'Delete server(s) in another project by name (admin only)' + '(can be specified using the ALL_PROJECTS envvar)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -1339,7 +1367,8 @@ def _show_progress(progress): compute_client = self.app.client_manager.compute for server in parsed_args.server: server_obj = utils.find_resource( - compute_client.servers, server) + compute_client.servers, server, + all_tenants=parsed_args.all_projects) if parsed_args.force: compute_client.servers.force_delete(server_obj.id) @@ -1347,11 +1376,13 @@ def _show_progress(progress): compute_client.servers.delete(server_obj.id) if parsed_args.wait: - if not utils.wait_for_delete(compute_client.servers, - server_obj.id, - callback=_show_progress): - LOG.error(_('Error deleting server: %s'), - server_obj.id) + if not utils.wait_for_delete( + compute_client.servers, + server_obj.id, + callback=_show_progress, + ): + msg = _('Error deleting server: %s') + LOG.error(msg, server_obj.id) self.app.stdout.write(_('Error deleting server\n')) raise SystemExit @@ -1446,8 +1477,11 @@ def get_parser(self, prog_name): parser.add_argument( '--all-projects', action='store_true', - default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help=_('Include all projects (admin only)'), + default=boolenv('ALL_PROJECTS'), + help=_( + 'Include all projects (admin only) ' + '(can be specified using the ALL_PROJECTS envvar)' + ), ) parser.add_argument( '--project', @@ -3939,6 +3973,15 @@ def get_parser(self, prog_name): nargs="+", help=_('Server(s) to start (name or ID)'), ) + parser.add_argument( + '--all-projects', + action='store_true', + default=boolenv('ALL_PROJECTS'), + help=_( + 'Start server(s) in another project by name (admin only)' + '(can be specified using the ALL_PROJECTS envvar)' + ), + ) return parser def take_action(self, parsed_args): @@ -3947,6 +3990,7 @@ def take_action(self, parsed_args): utils.find_resource( compute_client.servers, server, + all_tenants=parsed_args.all_projects, ).start() @@ -3961,6 +4005,15 @@ def get_parser(self, prog_name): nargs="+", help=_('Server(s) to stop (name or ID)'), ) + parser.add_argument( + '--all-projects', + action='store_true', + default=boolenv('ALL_PROJECTS'), + help=_( + 'Stop server(s) in another project by name (admin only)' + '(can be specified using the ALL_PROJECTS envvar)' + ), + ) return parser def take_action(self, parsed_args): @@ -3969,6 +4022,7 @@ def take_action(self, parsed_args): utils.find_resource( compute_client.servers, server, + all_tenants=parsed_args.all_projects, ).stop() diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 16885eb86a..9a01758c8c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2913,6 +2913,28 @@ def test_server_delete_multi_servers(self): self.servers_mock.delete.assert_has_calls(calls) self.assertIsNone(result) + @mock.patch.object(common_utils, 'find_resource') + def test_server_delete_with_all_projects(self, mock_find_resource): + servers = self.setup_servers_mock(count=1) + mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers( + servers, 0, + ) + + arglist = [ + servers[0].id, + '--all-projects', + ] + verifylist = [ + ('server', [servers[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + mock_find_resource.assert_called_once_with( + mock.ANY, servers[0].id, all_tenants=True, + ) + @mock.patch.object(common_utils, 'wait_for_delete', return_value=True) def test_server_delete_wait_ok(self, mock_wait_for_delete): servers = self.setup_servers_mock(count=1) @@ -6781,6 +6803,28 @@ def test_server_start_one_server(self): def test_server_start_multi_servers(self): self.run_method_with_servers('start', 3) + @mock.patch.object(common_utils, 'find_resource') + def test_server_start_with_all_projects(self, mock_find_resource): + servers = self.setup_servers_mock(count=1) + mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers( + servers, 0, + ) + + arglist = [ + servers[0].id, + '--all-projects', + ] + verifylist = [ + ('server', [servers[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + mock_find_resource.assert_called_once_with( + mock.ANY, servers[0].id, all_tenants=True, + ) + class TestServerStop(TestServer): @@ -6801,6 +6845,28 @@ def test_server_stop_one_server(self): def test_server_stop_multi_servers(self): self.run_method_with_servers('stop', 3) + @mock.patch.object(common_utils, 'find_resource') + def test_server_start_with_all_projects(self, mock_find_resource): + servers = self.setup_servers_mock(count=1) + mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers( + servers, 0, + ) + + arglist = [ + servers[0].id, + '--all-projects', + ] + verifylist = [ + ('server', [servers[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + mock_find_resource.assert_called_once_with( + mock.ANY, servers[0].id, all_tenants=True, + ) + class TestServerSuspend(TestServer): diff --git a/releasenotes/notes/server-ops-all-projects-2ce2202cdf617184.yaml b/releasenotes/notes/server-ops-all-projects-2ce2202cdf617184.yaml new file mode 100644 index 0000000000..b14eb50414 --- /dev/null +++ b/releasenotes/notes/server-ops-all-projects-2ce2202cdf617184.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``server delete``, ``server start`` and ``server stop`` commands now + support the ``--all-projects`` option. This allows you to perform the + specified action on a server in another project using the server name. + This is an admin-only action by default. diff --git a/requirements.txt b/requirements.txt index 21e291013b..ad0c50e50d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ iso8601>=0.1.11 # MIT openstacksdk>=0.52.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 python-keystoneclient>=3.22.0 # Apache-2.0 python-novaclient>=17.0.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 From 5ec4d4c7188f4766d270be32e12b64b709d2b835 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 10 Dec 2020 11:29:01 +0000 Subject: [PATCH 0357/1120] compute: Add missing options for 'server group list' Add pagination parameters, '--limit' and '--offset'. It's unfortunate that we can't use '--marker' like elsewhere but that requires server-side support to be truly effective. Change-Id: I186adc8cdf28e9c540ad22bca6684d9dd892976a Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_group.py | 38 ++++++++++++++- .../unit/compute/v2/test_server_group.py | 47 +++++++++++++++++-- ...rver-group-list-opts-d3c3d98b7f7a56a6.yaml | 5 ++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index 783fdbfea3..32dd19370f 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -177,11 +177,47 @@ def get_parser(self, prog_name): default=False, help=_("List additional fields in output") ) + # TODO(stephenfin): This should really be a --marker option, but alas + # the API doesn't support that for some reason + parser.add_argument( + '--offset', + metavar='', + type=int, + default=None, + help=_( + 'Index from which to start listing servers. This should ' + 'typically be a factor of --limit. Display all servers groups ' + 'if not specified.' + ), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + default=None, + help=_( + "Maximum number of server groups to display. " + "If limit is greater than 'osapi_max_limit' option of Nova " + "API, 'osapi_max_limit' will be used instead." + ), + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - data = compute_client.server_groups.list(parsed_args.all_projects) + + kwargs = {} + + if parsed_args.all_projects: + kwargs['all_projects'] = parsed_args.all_projects + + if parsed_args.offset: + kwargs['offset'] = parsed_args.offset + + if parsed_args.limit: + kwargs['limit'] = parsed_args.limit + + data = compute_client.server_groups.list(**kwargs) policy_key = 'Policies' if compute_client.api_version >= api_versions.APIVersion("2.64"): diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index 732c18810b..3ed19e27d5 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -326,10 +326,13 @@ def test_server_group_list(self): verifylist = [ ('all_projects', False), ('long', False), + ('limit', None), + ('offset', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.server_groups_mock.list.assert_called_once_with(False) + + self.server_groups_mock.list.assert_called_once_with() self.assertCountEqual(self.list_columns, columns) self.assertCountEqual(self.list_data, tuple(data)) @@ -342,14 +345,49 @@ def test_server_group_list_with_all_projects_and_long(self): verifylist = [ ('all_projects', True), ('long', True), + ('limit', None), + ('offset', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.server_groups_mock.list.assert_called_once_with(True) + self.server_groups_mock.list.assert_called_once_with( + all_projects=True) self.assertCountEqual(self.list_columns_long, columns) self.assertCountEqual(self.list_data_long, tuple(data)) + def test_server_group_list_with_limit(self): + arglist = [ + '--limit', '1', + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('limit', 1), + ('offset', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.server_groups_mock.list.assert_called_once_with(limit=1) + + def test_server_group_list_with_offset(self): + arglist = [ + '--offset', '5', + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('limit', None), + ('offset', 5), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.server_groups_mock.list.assert_called_once_with(offset=5) + class TestServerGroupListV264(TestServerGroupV264): @@ -400,7 +438,7 @@ def test_server_group_list(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.server_groups_mock.list.assert_called_once_with(False) + self.server_groups_mock.list.assert_called_once_with() self.assertCountEqual(self.list_columns, columns) self.assertCountEqual(self.list_data, tuple(data)) @@ -416,7 +454,8 @@ def test_server_group_list_with_all_projects_and_long(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.server_groups_mock.list.assert_called_once_with(True) + self.server_groups_mock.list.assert_called_once_with( + all_projects=True) self.assertCountEqual(self.list_columns_long, columns) self.assertCountEqual(self.list_data_long, tuple(data)) diff --git a/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml b/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml new file mode 100644 index 0000000000..b359e77ca2 --- /dev/null +++ b/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--limit`` and ``--offset`` options to ``server group list`` command, + to configure pagination of results. From 7ed4f68c68cb17b02deced203f9a461605a68dfe Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 10 Dec 2020 11:57:20 +0000 Subject: [PATCH 0358/1120] compute: Add missing options for 'server event list' Add pagination parameters, '--limit' and '--offset', and filtering parameters, '--changes-since' and '--changes-before'. Change-Id: Ieca8267c3b204ae2db580502cc8fe72c95eddf09 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server_event.py | 178 +++++++++---- .../unit/compute/v2/test_server_event.py | 235 +++++++++++++++++- ...rver-group-list-opts-ebcf84bfcea07a4b.yaml | 6 + 3 files changed, 375 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-group-list-opts-ebcf84bfcea07a4b.yaml diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py index 4fcc913614..1b971e51df 100644 --- a/openstackclient/compute/v2/server_event.py +++ b/openstackclient/compute/v2/server_event.py @@ -17,7 +17,10 @@ import logging +import iso8601 +from novaclient import api_versions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ @@ -27,10 +30,11 @@ class ListServerEvent(command.Lister): - _description = _( - "List recent events of a server. " - "Specify ``--os-compute-api-version 2.21`` " - "or higher to show events for a deleted server.") + """List recent events of a server. + + Specify ``--os-compute-api-version 2.21`` or higher to show events for a + deleted server. + """ def get_parser(self, prog_name): parser = super(ListServerEvent, self).get_parser(prog_name) @@ -45,60 +49,144 @@ def get_parser(self, prog_name): default=False, help=_("List additional fields in output") ) + parser.add_argument( + '--changes-since', + dest='changes_since', + metavar='', + help=_( + "List only server events changed later or equal to a certain " + "point of time. The provided time should be an ISO 8061 " + "formatted time, e.g. ``2016-03-04T06:27:59Z``. " + "(supported with --os-compute-api-version 2.58 or above)" + ), + ) + parser.add_argument( + '--changes-before', + dest='changes_before', + metavar='', + help=_( + "List only server events changed earlier or equal to a " + "certain point of time. The provided time should be an ISO " + "8061 formatted time, e.g. ``2016-03-04T06:27:59Z``. " + "(supported with --os-compute-api-version 2.66 or above)" + ), + ) + parser.add_argument( + '--marker', + help=_( + 'The last server event ID of the previous page ' + '(supported by --os-compute-api-version 2.58 or above)' + ), + ) + parser.add_argument( + '--limit', + type=int, + help=_( + 'Maximum number of server events to display ' + '(supported by --os-compute-api-version 2.58 or above)' + ), + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - server_id = utils.find_resource(compute_client.servers, - parsed_args.server).id - data = compute_client.instance_action.list(server_id) + + kwargs = {} + + if parsed_args.marker: + if compute_client.api_version < api_versions.APIVersion('2.58'): + msg = _( + '--os-compute-api-version 2.58 or greater is required to ' + 'support the --marker option' + ) + raise exceptions.CommandError(msg) + kwargs['marker'] = parsed_args.marker + + if parsed_args.limit: + if compute_client.api_version < api_versions.APIVersion('2.58'): + msg = _( + '--os-compute-api-version 2.58 or greater is required to ' + 'support the --limit option' + ) + raise exceptions.CommandError(msg) + kwargs['limit'] = parsed_args.limit + + if parsed_args.changes_since: + if compute_client.api_version < api_versions.APIVersion('2.58'): + msg = _( + '--os-compute-api-version 2.58 or greater is required to ' + 'support the --changes-since option' + ) + raise exceptions.CommandError(msg) + + try: + iso8601.parse_date(parsed_args.changes_since) + except (TypeError, iso8601.ParseError): + msg = _('Invalid changes-since value: %s') + raise exceptions.CommandError(msg % parsed_args.changes_since) + + kwargs['changes_since'] = parsed_args.changes_since + + if parsed_args.changes_before: + if compute_client.api_version < api_versions.APIVersion('2.66'): + msg = _( + '--os-compute-api-version 2.66 or greater is required to ' + 'support the --changes-before option' + ) + raise exceptions.CommandError(msg) + + try: + iso8601.parse_date(parsed_args.changes_before) + except (TypeError, iso8601.ParseError): + msg = _('Invalid changes-before value: %s') + raise exceptions.CommandError(msg % parsed_args.changes_before) + + kwargs['changes_before'] = parsed_args.changes_before + + server_id = utils.find_resource( + compute_client.servers, parsed_args.server, + ).id + + data = compute_client.instance_action.list(server_id, **kwargs) + + columns = ( + 'request_id', + 'instance_uuid', + 'action', + 'start_time', + ) + column_headers = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + ) if parsed_args.long: - columns = ( - 'request_id', - 'instance_uuid', - 'action', - 'start_time', + columns += ( 'message', 'project_id', 'user_id', ) - column_headers = ( - 'Request ID', - 'Server ID', - 'Action', - 'Start Time', + column_headers += ( 'Message', 'Project ID', 'User ID', ) - else: - columns = ( - 'request_id', - 'instance_uuid', - 'action', - 'start_time', - ) - column_headers = ( - 'Request ID', - 'Server ID', - 'Action', - 'Start Time', - ) - return (column_headers, - (utils.get_item_properties( - s, columns, - ) for s in data)) + return ( + column_headers, + (utils.get_item_properties(s, columns) for s in data), + ) class ShowServerEvent(command.ShowOne): - _description = _( - "Show server event details. " - "Specify ``--os-compute-api-version 2.21`` " - "or higher to show event details for a deleted server. " - "Specify ``--os-compute-api-version 2.51`` " - "or higher to show event details for non-admin users.") + """Show server event details. + + Specify ``--os-compute-api-version 2.21`` or higher to show event details + for a deleted server. Specify ``--os-compute-api-version 2.51`` or higher + to show event details for non-admin users. + """ def get_parser(self, prog_name): parser = super(ShowServerEvent, self).get_parser(prog_name) @@ -116,9 +204,13 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - server_id = utils.find_resource(compute_client.servers, - parsed_args.server).id + + server_id = utils.find_resource( + compute_client.servers, parsed_args.server, + ).id + action_detail = compute_client.instance_action.get( - server_id, parsed_args.request_id) + server_id, parsed_args.request_id + ) return zip(*sorted(action_detail.to_dict().items())) diff --git a/openstackclient/tests/unit/compute/v2/test_server_event.py b/openstackclient/tests/unit/compute/v2/test_server_event.py index 5c94891a14..058a44d8b6 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_event.py +++ b/openstackclient/tests/unit/compute/v2/test_server_event.py @@ -11,7 +11,12 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# + +from unittest import mock + +import iso8601 +from novaclient import api_versions +from osc_lib import exceptions from openstackclient.compute.v2 import server_event from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -113,6 +118,234 @@ def test_server_event_list_long(self): self.assertEqual(self.long_columns, columns) self.assertEqual(self.long_data, tuple(data)) + def test_server_event_list_with_changes_since(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.58') + + arglist = [ + '--changes-since', '2016-03-04T06:27:59Z', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('changes_since', '2016-03-04T06:27:59Z'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.list.assert_called_once_with( + self.fake_server.id, changes_since='2016-03-04T06:27:59Z') + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) + def test_server_event_list_with_changes_since_invalid( + self, mock_parse_isotime, + ): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.58') + + arglist = [ + '--changes-since', 'Invalid time value', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('changes_since', 'Invalid time value'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + 'Invalid changes-since value:', str(ex)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) + + def test_server_event_list_with_changes_since_pre_v258(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.57') + + arglist = [ + '--changes-since', '2016-03-04T06:27:59Z', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('changes_since', '2016-03-04T06:27:59Z'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.58 or greater is required', str(ex)) + + def test_server_event_list_with_changes_before(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.66') + + arglist = [ + '--changes-before', '2016-03-04T06:27:59Z', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('changes_before', '2016-03-04T06:27:59Z'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.list.assert_called_once_with( + self.fake_server.id, changes_before='2016-03-04T06:27:59Z') + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) + def test_server_event_list_with_changes_before_invalid( + self, mock_parse_isotime, + ): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.66') + + arglist = [ + '--changes-before', 'Invalid time value', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('changes_before', 'Invalid time value'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + 'Invalid changes-before value:', str(ex)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) + + def test_server_event_list_with_changes_before_pre_v266(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.65') + + arglist = [ + '--changes-before', '2016-03-04T06:27:59Z', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('changes_before', '2016-03-04T06:27:59Z'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.66 or greater is required', str(ex)) + + def test_server_event_list_with_limit(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.58') + + arglist = [ + '--limit', '1', + self.fake_server.name, + ] + verifylist = [ + ('limit', 1), + ('server', self.fake_server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.events_mock.list.assert_called_once_with( + self.fake_server.id, limit=1) + + def test_server_event_list_with_limit_pre_v258(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.57') + + arglist = [ + '--limit', '1', + self.fake_server.name, + ] + verifylist = [ + ('limit', 1), + ('server', self.fake_server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.58 or greater is required', str(ex)) + + def test_server_event_list_with_marker(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.58') + + arglist = [ + '--marker', 'test_event', + self.fake_server.name, + ] + verifylist = [ + ('marker', 'test_event'), + ('server', self.fake_server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.events_mock.list.assert_called_once_with( + self.fake_server.id, marker='test_event') + + def test_server_event_list_with_marker_pre_v258(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.57') + + arglist = [ + '--marker', 'test_event', + self.fake_server.name, + ] + verifylist = [ + ('marker', 'test_event'), + ('server', self.fake_server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + self.assertIn( + '--os-compute-api-version 2.58 or greater is required', str(ex)) + class TestShowServerEvent(TestServerEvent): diff --git a/releasenotes/notes/add-missing-server-group-list-opts-ebcf84bfcea07a4b.yaml b/releasenotes/notes/add-missing-server-group-list-opts-ebcf84bfcea07a4b.yaml new file mode 100644 index 0000000000..0533b6185e --- /dev/null +++ b/releasenotes/notes/add-missing-server-group-list-opts-ebcf84bfcea07a4b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--limit``, ``--marker``, ``--change-since`` and ``--changes-before`` + options to ``server group list`` command, to configure pagination of + results and filter results by last modification, respectively. From d6c9b7f198b94ef05c96dc72fc71d34f019e9350 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 10 Dec 2020 14:13:47 +0000 Subject: [PATCH 0359/1120] compute: Shuffle options for 'server create' argparse doesn't sort options by name, meaning we can use the opportunity to group closely related options together. Do that. Change-Id: I6714c8db1a549bd4206d2282d2876a406af65aa2 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 198 ++++++++++++++------------- 1 file changed, 102 insertions(+), 96 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 15a5084d0d..7e17d0d043 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -621,6 +621,12 @@ def get_parser(self, prog_name): metavar='', help=_('New server name'), ) + parser.add_argument( + '--flavor', + metavar='', + required=True, + help=_('Create server with this flavor (name or ID)'), + ) disk_group = parser.add_mutually_exclusive_group( required=True, ) @@ -629,12 +635,17 @@ def get_parser(self, prog_name): metavar='', help=_('Create server boot disk from this image (name or ID)'), ) + # TODO(stephenfin): Is this actually useful? Looks like a straight port + # from 'nova boot --image-with'. Perhaps we should deprecate this. disk_group.add_argument( '--image-property', metavar='', action=parseractions.KeyValueAction, dest='image_properties', - help=_("Image property to be matched"), + help=_( + "Create server using the image that matches the specified " + "property. Property must match exactly one property." + ), ) disk_group.add_argument( '--volume', @@ -649,17 +660,101 @@ def get_parser(self, prog_name): 'volume.' ), ) + parser.add_argument( + '--boot-from-volume', + metavar='', + type=int, + help=_( + 'When used in conjunction with the ``--image`` or ' + '``--image-property`` option, this option automatically ' + 'creates a block device mapping with a boot index of 0 ' + 'and tells the compute service to create a volume of the ' + 'given size (in GB) from the specified image and use it ' + 'as the root disk of the server. The root volume will not ' + 'be deleted when the server is deleted. This option is ' + 'mutually exclusive with the ``--volume`` option.' + ) + ) + parser.add_argument( + '--block-device-mapping', + metavar='', + action=parseractions.KeyValueAction, + default={}, + # NOTE(RuiChen): Add '\n' to the end of line to improve formatting; + # see cliff's _SmartHelpFormatter for more details. + help=_( + 'Create a block device on the server.\n' + 'Block device mapping in the format\n' + '=:::\n' + ': block device name, like: vdb, xvdc ' + '(required)\n' + ': Name or ID of the volume, volume snapshot or image ' + '(required)\n' + ': volume, snapshot or image; default: volume ' + '(optional)\n' + ': volume size if create from image or snapshot ' + '(optional)\n' + ': true or false; default: false ' + '(optional)\n' + ), + ) + parser.add_argument( + '--network', + metavar="", + action='append', + dest='nic', + type=_prefix_checked_value('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." + ), + ) + parser.add_argument( + '--port', + metavar="", + action='append', + dest='nic', + type=_prefix_checked_value('port-id='), + help=_( + "Create a NIC on the server and connect it to port. " + "Specify option multiple times to create multiple NICs. " + "This is a wrapper for the '--nic port-id=' " + "parameter that provides simple syntax for the standard " + "use case of connecting a new server to a given port. For " + "more advanced use cases, refer to the '--nic' parameter." + ), + ) + parser.add_argument( + '--nic', + metavar="", + action='append', + help=_( + "Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "Either net-id or port-id must be provided, but not both. " + "net-id: attach NIC to network with this UUID, " + "port-id: attach NIC to port with this UUID, " + "v4-fixed-ip: IPv4 fixed address for NIC (optional), " + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "none: (v2.37+) no network is attached, " + "auto: (v2.37+) the compute service will automatically " + "allocate a network. Specifying a --nic of auto or none " + "cannot be used with any other --nic value." + ), + ) parser.add_argument( '--password', metavar='', help=_("Set the password to this server"), ) - parser.add_argument( - '--flavor', - metavar='', - required=True, - help=_('Create server with this flavor (name or ID)'), - ) parser.add_argument( '--security-group', metavar='', @@ -736,95 +831,6 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.74 or above)' ), ) - parser.add_argument( - '--boot-from-volume', - metavar='', - type=int, - help=_( - 'When used in conjunction with the ``--image`` or ' - '``--image-property`` option, this option automatically ' - 'creates a block device mapping with a boot index of 0 ' - 'and tells the compute service to create a volume of the ' - 'given size (in GB) from the specified image and use it ' - 'as the root disk of the server. The root volume will not ' - 'be deleted when the server is deleted. This option is ' - 'mutually exclusive with the ``--volume`` option.' - ) - ) - parser.add_argument( - '--block-device-mapping', - metavar='', - action=parseractions.KeyValueAction, - default={}, - # NOTE(RuiChen): Add '\n' at the end of line to put each item in - # the separated line, avoid the help message looks - # messy, see _SmartHelpFormatter in cliff. - help=_( - 'Create a block device on the server.\n' - 'Block device mapping in the format\n' - '=:::\n' - ': block device name, like: vdb, xvdc ' - '(required)\n' - ': Name or ID of the volume, volume snapshot or image ' - '(required)\n' - ': volume, snapshot or image; default: volume ' - '(optional)\n' - ': volume size if create from image or snapshot ' - '(optional)\n' - ': true or false; default: false ' - '(optional)\n' - ), - ) - parser.add_argument( - '--nic', - metavar="", - action='append', - help=_( - "Create a NIC on the server. " - "Specify option multiple times to create multiple NICs. " - "Either net-id or port-id must be provided, but not both. " - "net-id: attach NIC to network with this UUID, " - "port-id: attach NIC to port with this UUID, " - "v4-fixed-ip: IPv4 fixed address for NIC (optional), " - "v6-fixed-ip: IPv6 fixed address for NIC (optional), " - "none: (v2.37+) no network is attached, " - "auto: (v2.37+) the compute service will automatically " - "allocate a network. Specifying a --nic of auto or none " - "cannot be used with any other --nic value." - ), - ) - parser.add_argument( - '--network', - metavar="", - action='append', - dest='nic', - type=_prefix_checked_value('net-id='), - help=_( - "Create a NIC on the server and connect it to network. " - "Specify option multiple times to create multiple NICs. " - "This is a wrapper for the '--nic net-id=' " - "parameter that provides simple syntax for the standard " - "use case of connecting a new server to a given network. " - "For more advanced use cases, refer to the '--nic' " - "parameter." - ), - ) - parser.add_argument( - '--port', - metavar="", - action='append', - dest='nic', - type=_prefix_checked_value('port-id='), - help=_( - "Create a NIC on the server and connect it to port. " - "Specify option multiple times to create multiple NICs. " - "This is a wrapper for the '--nic port-id=' " - "parameter that provides simple syntax for the standard " - "use case of connecting a new server to a given port. For " - "more advanced use cases, refer to the '--nic' parameter." - ), - ) parser.add_argument( '--hint', metavar='', From c7d582379ad6b22c6dd8b7334b34a51ec59b69d4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 10 Dec 2020 15:06:03 +0000 Subject: [PATCH 0360/1120] compute: Improve 'server create --nic' option parsing Simplify the parsing of this option by making use of a custom action. Change-Id: I670ff5109522d533ef4e62a79116e49a35c4e8fa Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 221 +++++++++++------- .../tests/unit/compute/v2/test_server.py | 104 ++++----- 2 files changed, 185 insertions(+), 140 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 7e17d0d043..1277c34130 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -98,16 +98,6 @@ def _get_ip_address(addresses, address_type, ip_address_family): ) -def _prefix_checked_value(prefix): - def func(value): - if ',' in value or '=' in value: - msg = _("Invalid argument %s, " - "characters ',' and '=' are not allowed") % value - raise argparse.ArgumentTypeError(msg) - return prefix + value - return func - - def _prep_server_detail(compute_client, image_client, server, refresh=True): """Prepare the detailed server dict for printing @@ -611,6 +601,81 @@ def take_action(self, parsed_args): ) +# TODO(stephenfin): Replace with 'MultiKeyValueAction' when we no longer +# support '--nic=auto' and '--nic=none' +class NICAction(argparse.Action): + + def __init__( + self, + option_strings, + dest, + nargs=None, + const=None, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None, + key=None, + ): + self.key = key + super().__init__( + option_strings=option_strings, dest=dest, nargs=nargs, const=const, + default=default, type=type, choices=choices, required=required, + help=help, metavar=metavar, + ) + + def __call__(self, parser, namespace, values, option_string=None): + # Make sure we have an empty dict rather than None + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + + # Handle the special auto/none cases + if values in ('auto', 'none'): + getattr(namespace, self.dest).append(values) + return + + if self.key: + if ',' in values or '=' in values: + msg = _( + "Invalid argument %s; characters ',' and '=' are not " + "allowed" + ) + raise argparse.ArgumentTypeError(msg % values) + + values = '='.join([self.key, values]) + + info = { + 'net-id': '', + 'port-id': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', + } + + for kv_str in values.split(','): + k, sep, v = kv_str.partition("=") + + if k not in info or not v: + msg = _( + "Invalid argument %s; argument must be of form " + "'net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr," + "port-id=port-uuid'" + ) + raise argparse.ArgumentTypeError(msg % values) + + info[k] = v + + if info['net-id'] and info['port-id']: + msg = _( + 'Invalid argument %s; either network or port should be ' + 'specified but not both' + ) + raise argparse.ArgumentTypeError(msg % values) + + getattr(namespace, self.dest).append(info) + + class CreateServer(command.ShowOne): _description = _("Create a new server") @@ -701,9 +766,10 @@ def get_parser(self, prog_name): parser.add_argument( '--network', metavar="", - action='append', - dest='nic', - type=_prefix_checked_value('net-id='), + 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=_( @@ -719,9 +785,10 @@ def get_parser(self, prog_name): parser.add_argument( '--port', metavar="", - action='append', - dest='nic', - type=_prefix_checked_value('port-id='), + dest='nics', + default=[], + action=NICAction, + key='port-id', help=_( "Create a NIC on the server and connect it to port. " "Specify option multiple times to create multiple NICs. " @@ -733,21 +800,28 @@ def get_parser(self, prog_name): ) parser.add_argument( '--nic', - metavar="", - action='append', + metavar="", + action=NICAction, + dest='nics', + default=[], help=_( - "Create a NIC on the server. " - "Specify option multiple times to create multiple NICs. " - "Either net-id or port-id must be provided, but not both. " - "net-id: attach NIC to network with this UUID, " - "port-id: attach NIC to port with this UUID, " - "v4-fixed-ip: IPv4 fixed address for NIC (optional), " - "v6-fixed-ip: IPv6 fixed address for NIC (optional), " - "none: (v2.37+) no network is attached, " + "Create a NIC on the server.\n" + "NIC in the format:\n" + "net-id=: attach NIC to network with this UUID,\n" + "port-id=: attach NIC to port with this UUID,\n" + "v4-fixed-ip=: IPv4 fixed address for NIC (optional)," + "\n" + "v6-fixed-ip=: IPv6 fixed address for NIC (optional)," + "\n" + "none: (v2.37+) no network is attached,\n" "auto: (v2.37+) the compute service will automatically " - "allocate a network. Specifying a --nic of auto or none " - "cannot be used with any other --nic value." + "allocate a network.\n" + "\n" + "Specify option multiple times to create multiple NICs.\n" + "Specifying a --nic of auto or none cannot be used with any " + "other --nic value.\n" + "Either net-id or port-id must be provided, but not both." ), ) parser.add_argument( @@ -1103,84 +1177,55 @@ def _match_image(image_api, wanted_properties): raise exceptions.CommandError(msg) block_device_mapping_v2.append(mapping) - nics = [] - auto_or_none = False - if parsed_args.nic is None: - parsed_args.nic = [] - for nic_str in parsed_args.nic: - # Handle the special auto/none cases - if nic_str in ('auto', 'none'): - auto_or_none = True - nics.append(nic_str) - else: - nic_info = { - 'net-id': '', - 'v4-fixed-ip': '', - 'v6-fixed-ip': '', - 'port-id': '', - } - for kv_str in nic_str.split(","): - k, sep, v = kv_str.partition("=") - if k in nic_info and v: - nic_info[k] = v - else: - msg = _( - "Invalid nic argument '%s'. Nic arguments " - "must be of the form --nic ." - ) - raise exceptions.CommandError(msg % k) + nics = parsed_args.nics - if bool(nic_info["net-id"]) == bool(nic_info["port-id"]): - msg = _( - 'Either network or port should be specified ' - 'but not both' - ) - raise exceptions.CommandError(msg) + if 'auto' in nics or 'none' in nics: + if len(nics) > 1: + msg = _( + 'Specifying a --nic of auto or none cannot ' + 'be used with any other --nic, --network ' + 'or --port value.' + ) + raise exceptions.CommandError(msg) + nics = nics[0] + else: + for nic in nics: if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network - if nic_info["net-id"]: + + if nic['net-id']: net = network_client.find_network( - nic_info["net-id"], ignore_missing=False) - nic_info["net-id"] = net.id - if nic_info["port-id"]: + nic['net-id'], ignore_missing=False, + ) + nic['net-id'] = net.id + + if nic['port-id']: port = network_client.find_port( - nic_info["port-id"], ignore_missing=False) - nic_info["port-id"] = port.id + nic['port-id'], ignore_missing=False, + ) + nic['port-id'] = port.id else: - if nic_info["net-id"]: - nic_info["net-id"] = compute_client.api.network_find( - nic_info["net-id"] + if nic['net-id']: + nic['net-id'] = compute_client.api.network_find( + nic['net-id'], )['id'] - if nic_info["port-id"]: + + if nic['port-id']: msg = _( "Can't create server with port specified " "since network endpoint not enabled" ) raise exceptions.CommandError(msg) - nics.append(nic_info) - - if nics: - if auto_or_none: - if len(nics) > 1: - msg = _( - 'Specifying a --nic of auto or none cannot ' - 'be used with any other --nic, --network ' - 'or --port value.' - ) - raise exceptions.CommandError(msg) - nics = nics[0] - else: + if not nics: # Compute API version >= 2.37 requires a value, so default to # 'auto' to maintain legacy behavior if a nic wasn't specified. if compute_client.api_version >= api_versions.APIVersion('2.37'): nics = 'auto' else: - # Default to empty list if nothing was specified, let nova - # side to decide the default behavior. + # Default to empty list if nothing was specified and let nova + # decide the default behavior. nics = [] # Check security group exist and convert ID to name diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9a01758c8c..e8db7f596f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1311,8 +1311,28 @@ def test_server_create_with_network(self): verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), - ('nic', ['net-id=net1', 'net-id=net1,v4-fixed-ip=10.0.0.2', - 'port-id=port1', 'net-id=net1', 'port-id=port2']), + ('nics', [ + { + 'net-id': 'net1', 'port-id': '', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + }, + { + 'net-id': 'net1', 'port-id': '', + 'v4-fixed-ip': '10.0.0.2', 'v6-fixed-ip': '', + }, + { + 'net-id': '', 'port-id': 'port1', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + }, + { + 'net-id': 'net1', 'port-id': '', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + }, + { + 'net-id': '', 'port-id': 'port2', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + }, + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -1413,7 +1433,7 @@ def test_server_create_with_auto_network(self): verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), - ('nic', ['auto']), + ('nics', ['auto']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -1509,7 +1529,7 @@ def test_server_create_with_none_network(self): verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), - ('nic', ['none']), + ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -1557,7 +1577,14 @@ def test_server_create_with_conflict_network_options(self): verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), - ('nic', ['none', 'auto', 'port-id=port1']), + ('nics', [ + 'none', + 'auto', + { + 'net-id': '', 'port-id': 'port1', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + }, + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -1587,17 +1614,10 @@ def test_server_create_with_invalid_network_options(self): '--nic', 'abcdefgh', self.new_server.name, ] - verifylist = [ - ('image', 'image1'), - ('flavor', 'flavor1'), - ('nic', ['abcdefgh']), - ('config_drive', False), - ('server_name', self.new_server.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) self.assertNotCalled(self.servers_mock.create) def test_server_create_with_invalid_network_key(self): @@ -1607,17 +1627,10 @@ def test_server_create_with_invalid_network_key(self): '--nic', 'abcdefgh=12324', self.new_server.name, ] - verifylist = [ - ('image', 'image1'), - ('flavor', 'flavor1'), - ('nic', ['abcdefgh=12324']), - ('config_drive', False), - ('server_name', self.new_server.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) self.assertNotCalled(self.servers_mock.create) def test_server_create_with_empty_network_key_value(self): @@ -1627,17 +1640,10 @@ def test_server_create_with_empty_network_key_value(self): '--nic', 'net-id=', self.new_server.name, ] - verifylist = [ - ('image', 'image1'), - ('flavor', 'flavor1'), - ('nic', ['net-id=']), - ('config_drive', False), - ('server_name', self.new_server.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) self.assertNotCalled(self.servers_mock.create) def test_server_create_with_only_network_key(self): @@ -1647,17 +1653,11 @@ def test_server_create_with_only_network_key(self): '--nic', 'net-id', self.new_server.name, ] - verifylist = [ - ('image', 'image1'), - ('flavor', 'flavor1'), - ('nic', ['net-id']), - ('config_drive', False), - ('server_name', self.new_server.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) self.assertNotCalled(self.servers_mock.create) @mock.patch.object(common_utils, 'wait_for_status', return_value=True) @@ -2230,7 +2230,7 @@ def test_server_create_image_property(self): verifylist = [ ('image_properties', {'hypervisor_type': 'qemu'}), ('flavor', 'flavor1'), - ('nic', ['none']), + ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2286,7 +2286,7 @@ def test_server_create_image_property_multi(self): ('image_properties', {'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide'}), ('flavor', 'flavor1'), - ('nic', ['none']), + ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2342,7 +2342,7 @@ def test_server_create_image_property_missed(self): ('image_properties', {'hypervisor_type': 'qemu', 'hw_disk_bus': 'virtio'}), ('flavor', 'flavor1'), - ('nic', ['none']), + ('nics', ['none']), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2374,7 +2374,7 @@ def test_server_create_image_property_with_image_list(self): ('image_properties', {'owner_specified.openstack.object': 'image/cirros'}), ('flavor', 'flavor1'), - ('nic', ['none']), + ('nics', ['none']), ('server_name', self.new_server.name), ] # create a image_info as the side_effect of the fake image_list() From 9ed34aac0a172ece4cd856319486208fcdba095d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 19 Jan 2021 16:55:14 +0000 Subject: [PATCH 0361/1120] compute: Add support for 'server boot --nic ...,tag=' This has been around for a long time but was not exposed via OSC. Close this gap. Change-Id: I71aabf10f791f68ee7405ffb5e8317cc96cb3b38 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 23 +++- .../tests/unit/compute/v2/test_server.py | 107 ++++++++++++++++++ ...-nic-tagging-support-f77b4247e87771d5.yaml | 7 ++ 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add-server-nic-tagging-support-f77b4247e87771d5.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1277c34130..9294288305 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -646,6 +646,8 @@ def __call__(self, parser, namespace, values, option_string=None): values = '='.join([self.key, values]) + # We don't include 'tag' here by default since that requires a + # particular microversion info = { 'net-id': '', 'port-id': '', @@ -656,11 +658,11 @@ def __call__(self, parser, namespace, values, option_string=None): for kv_str in values.split(','): k, sep, v = kv_str.partition("=") - if k not in info or not v: + if k not in list(info) + ['tag'] or not v: msg = _( "Invalid argument %s; argument must be of form " - "'net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr," - "port-id=port-uuid'" + "'net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr," + "v6-fixed-ip=ip-addr,tag=tag'" ) raise argparse.ArgumentTypeError(msg % values) @@ -801,7 +803,7 @@ def get_parser(self, prog_name): parser.add_argument( '--nic', metavar="", + "v6-fixed-ip=ip-addr,tag=tag,auto,none>", action=NICAction, dest='nics', default=[], @@ -814,6 +816,8 @@ def get_parser(self, prog_name): "\n" "v6-fixed-ip=: IPv6 fixed address for NIC (optional)," "\n" + "tag: interface metadata tag (optional) " + "(supported by --os-compute-api-version 2.43 or above),\n" "none: (v2.37+) no network is attached,\n" "auto: (v2.37+) the compute service will automatically " "allocate a network.\n" @@ -1191,6 +1195,17 @@ def _match_image(image_api, wanted_properties): nics = nics[0] else: for nic in nics: + if 'tag' in nic: + if ( + compute_client.api_version < + api_versions.APIVersion('2.43') + ): + msg = _( + '--os-compute-api-version 2.43 or greater is ' + 'required to support the --nic tag field' + ) + raise exceptions.CommandError(msg) + if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index e8db7f596f..5cf76c887e 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1423,6 +1423,113 @@ def test_server_create_with_network(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_network_tag(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.43') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'net-id=net1,tag=foo', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nics', [ + { + 'net-id': 'net1', 'port-id': '', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + 'tag': 'foo', + }, + ]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_network = mock.Mock() + network_client = self.app.client_manager.network + network_client.find_network = find_network + network_resource = mock.Mock(id='net1_uuid') + find_network.return_value = network_resource + + # Mock sdk APIs. + _network = mock.Mock(id='net1_uuid') + find_network = mock.Mock() + find_network.return_value = _network + self.app.client_manager.network.find_network = find_network + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + admin_pass=None, + block_device_mapping_v2=[], + nics=[ + { + 'net-id': 'net1_uuid', + 'v4-fixed-ip': '', + 'v6-fixed-ip': '', + 'port-id': '', + 'tag': 'foo', + }, + ], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + network_client.find_network.assert_called_once() + self.app.client_manager.network.find_network.assert_called_once() + + def test_server_create_with_network_tag_pre_v243(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.42') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'net-id=net1,tag=foo', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nics', [ + { + 'net-id': 'net1', 'port-id': '', + 'v4-fixed-ip': '', 'v6-fixed-ip': '', + 'tag': 'foo', + }, + ]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_create_with_auto_network(self): arglist = [ '--image', 'image1', diff --git a/releasenotes/notes/add-server-nic-tagging-support-f77b4247e87771d5.yaml b/releasenotes/notes/add-server-nic-tagging-support-f77b4247e87771d5.yaml new file mode 100644 index 0000000000..e5f4fd5255 --- /dev/null +++ b/releasenotes/notes/add-server-nic-tagging-support-f77b4247e87771d5.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``--nic`` option of the ``server create`` command now supports an + optional ``tag=`` key-value pair. This can be used to set a tag for + the interface in server metadata, which can be useful to maintain + persistent references to interfaces during various operations. From 32ae1857d12fc2d5b4292e2d98caed0238959081 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 19 Jan 2021 17:11:48 +0000 Subject: [PATCH 0362/1120] Rename FakeServerMigration to FakeMigration Server migrations are (confusingly) a different thing returned by a different API. Change-Id: Ib6b7c8f9cc3d1521a993616f832d41651dc46f73 Signed-off-by: Stephen Finucane --- .../tests/unit/compute/v2/fakes.py | 20 +++++++++---------- .../tests/unit/compute/v2/test_server.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index e4cf10454e..7f3dcae4b1 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1566,12 +1566,12 @@ def __init__(self, verb, uri, value, remain, self.next_available = next_available -class FakeServerMigration(object): - """Fake one or more server migrations.""" +class FakeMigration(object): + """Fake one or more migrations.""" @staticmethod - def create_one_server_migration(attrs=None, methods=None): - """Create a fake server migration. + def create_one_migration(attrs=None, methods=None): + """Create a fake migration. :param Dictionary attrs: A dictionary with all attributes @@ -1612,22 +1612,22 @@ def create_one_server_migration(attrs=None, methods=None): return migration @staticmethod - def create_server_migrations(attrs=None, methods=None, count=2): - """Create multiple fake server migrations. + def create_migrations(attrs=None, methods=None, count=2): + """Create multiple fake migrations. :param Dictionary attrs: A dictionary with all attributes :param Dictionary methods: A dictionary with all methods :param int count: - The number of server migrations to fake + The number of migrations to fake :return: - A list of FakeResource objects faking the server migrations + A list of FakeResource objects faking the migrations """ migrations = [] for i in range(0, count): migrations.append( - FakeServerMigration.create_one_server_migration( + FakeMigration.create_one_migration( attrs, methods)) return migrations @@ -1680,7 +1680,7 @@ def create_volume_attachments(attrs=None, methods=None, count=2): :param Dictionary methods: A dictionary with all methods :param int count: - The number of server migrations to fake + The number of volume attachments to fake :return: A list of FakeResource objects faking the volume attachments. """ diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5cf76c887e..5025f020c2 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4396,8 +4396,8 @@ def setUp(self): self.server = compute_fakes.FakeServer.create_one_server() self.servers_mock.get.return_value = self.server - self.migrations = compute_fakes.FakeServerMigration\ - .create_server_migrations(count=3) + self.migrations = compute_fakes.FakeMigration.create_migrations( + count=3) self.migrations_mock.list.return_value = self.migrations self.data = (common_utils.get_item_properties( From f80fe2d8cf50f85601e2895a5f878764fa77c154 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Jan 2021 10:11:37 +0000 Subject: [PATCH 0363/1120] compute: Add 'server migration show' command This replaces the 'server-migration-show' command provided by novaclient. Change-Id: I413310b481cc13b70853eb579417f6e6fad10d98 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 64 ++++++++++ .../tests/unit/compute/v2/fakes.py | 53 ++++++++ .../tests/unit/compute/v2/test_server.py | 118 ++++++++++++++++++ ...gration-show-command-2e3a25e383dc5d70.yaml | 5 + setup.cfg | 3 +- 5 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-server-migration-show-command-2e3a25e383dc5d70.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 9294288305..3c598981f8 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2603,6 +2603,70 @@ def take_action(self, parsed_args): return self.print_migrations(parsed_args, compute_client, migrations) +class ShowMigration(command.Command): + """Show a migration for a given server.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + parser.add_argument( + 'migration', + metavar='', + help=_("Migration (ID)"), + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + + if compute_client.api_version < api_versions.APIVersion('2.24'): + msg = _( + '--os-compute-api-version 2.24 or greater is required to ' + 'support the server migration show command' + ) + raise exceptions.CommandError(msg) + + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + server_migration = compute_client.server_migrations.get( + server.id, parsed_args.migration, + ) + + columns = ( + 'ID', + 'Server UUID', + 'Status', + 'Source Compute', + 'Source Node', + 'Dest Compute', + 'Dest Host', + 'Dest Node', + 'Memory Total Bytes', + 'Memory Processed Bytes', + 'Memory Remaining Bytes', + 'Disk Total Bytes', + 'Disk Processed Bytes', + 'Disk Remaining Bytes', + 'Created At', + 'Updated At', + ) + + if compute_client.api_version >= api_versions.APIVersion('2.59'): + columns += ('UUID',) + + if compute_client.api_version >= api_versions.APIVersion('2.80'): + columns += ('User ID', 'Project ID') + + data = utils.get_item_properties(server_migration, columns) + return columns, data + + class AbortMigration(command.Command): """Cancel an ongoing live migration. diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 7f3dcae4b1..4a2a44de12 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1633,6 +1633,59 @@ def create_migrations(attrs=None, methods=None, count=2): return migrations +class FakeServerMigration(object): + """Fake one or more server migrations.""" + + @staticmethod + def create_one_server_migration(attrs=None, methods=None): + """Create a fake server migration. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object, with id, type, and so on + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + + migration_info = { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": random.randint(1, 999), + "server_uuid": uuid.uuid4().hex, + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": random.randint(1, 99999), + "memory_processed_bytes": random.randint(1, 99999), + "memory_remaining_bytes": random.randint(1, 99999), + "disk_total_bytes": random.randint(1, 99999), + "disk_processed_bytes": random.randint(1, 99999), + "disk_remaining_bytes": random.randint(1, 99999), + "updated_at": "2016-01-29T13:42:02.000000", + # added in 2.59 + "uuid": uuid.uuid4().hex, + # added in 2.80 + "user_id": uuid.uuid4().hex, + "project_id": uuid.uuid4().hex, + } + + # Overwrite default attributes. + migration_info.update(attrs) + + migration = fakes.FakeResource( + info=copy.deepcopy(migration_info), + methods=methods, + loaded=True) + return migration + + class FakeVolumeAttachment(object): """Fake one or more volume attachments (BDMs).""" diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5025f020c2..8d040472f0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4888,6 +4888,124 @@ def test_get_migrations_with_project_and_user_pre_v280(self): str(ex)) +class TestServerMigrationShow(TestServer): + + def setUp(self): + super().setUp() + + self.server = compute_fakes.FakeServer.create_one_server() + self.servers_mock.get.return_value = self.server + + self.server_migration = compute_fakes.FakeServerMigration\ + .create_one_server_migration() + self.server_migrations_mock.get.return_value = self.server_migration + + self.columns = ( + 'ID', + 'Server UUID', + 'Status', + 'Source Compute', + 'Source Node', + 'Dest Compute', + 'Dest Host', + 'Dest Node', + 'Memory Total Bytes', + 'Memory Processed Bytes', + 'Memory Remaining Bytes', + 'Disk Total Bytes', + 'Disk Processed Bytes', + 'Disk Remaining Bytes', + 'Created At', + 'Updated At', + ) + + self.data = ( + self.server_migration.id, + self.server_migration.server_uuid, + self.server_migration.status, + self.server_migration.source_compute, + self.server_migration.source_node, + self.server_migration.dest_compute, + self.server_migration.dest_host, + self.server_migration.dest_node, + self.server_migration.memory_total_bytes, + self.server_migration.memory_processed_bytes, + self.server_migration.memory_remaining_bytes, + self.server_migration.disk_total_bytes, + self.server_migration.disk_processed_bytes, + self.server_migration.disk_remaining_bytes, + self.server_migration.created_at, + self.server_migration.updated_at, + ) + + # Get the command object to test + self.cmd = server.ShowMigration(self.app, None) + + def _test_server_migration_show(self): + arglist = [ + self.server.id, + '2', # arbitrary migration ID + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server_migrations_mock.get.assert_called_with( + self.server.id, '2',) + + def test_server_migration_show(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.24') + + self._test_server_migration_show() + + def test_server_migration_show_v259(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.59') + + self.columns += ('UUID',) + self.data += (self.server_migration.uuid,) + + self._test_server_migration_show() + + def test_server_migration_show_v280(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.80') + + self.columns += ('UUID', 'User ID', 'Project ID') + self.data += ( + self.server_migration.uuid, + self.server_migration.user_id, + self.server_migration.project_id, + ) + + self._test_server_migration_show() + + def test_server_migration_show_pre_v224(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.23') + + arglist = [ + self.server.id, + '2', # arbitrary migration ID + ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.24 or greater is required', + str(ex)) + + class TestServerMigrationAbort(TestServer): def setUp(self): diff --git a/releasenotes/notes/add-server-migration-show-command-2e3a25e383dc5d70.yaml b/releasenotes/notes/add-server-migration-show-command-2e3a25e383dc5d70.yaml new file mode 100644 index 0000000000..28ab2759a6 --- /dev/null +++ b/releasenotes/notes/add-server-migration-show-command-2e3a25e383dc5d70.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``server migration show`` commands. This can be used to show detailed + information about an ongoing server migration. diff --git a/setup.cfg b/setup.cfg index 48384897a0..ee412c050a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -110,9 +110,10 @@ openstack.compute.v2 = server_migrate = openstackclient.compute.v2.server:MigrateServer server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert - server_migration_list = openstackclient.compute.v2.server:ListMigration server_migration_abort = openstackclient.compute.v2.server:AbortMigration server_migration_force_complete = openstackclient.compute.v2.server:ForceCompleteMigration + server_migration_list = openstackclient.compute.v2.server:ListMigration + server_migration_show = openstackclient.compute.v2.server:ShowMigration server_pause = openstackclient.compute.v2.server:PauseServer server_reboot = openstackclient.compute.v2.server:RebootServer server_rebuild = openstackclient.compute.v2.server:RebuildServer From a52beacaa6fcc11d48f5b742c73aa2c0f87634ce Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Jan 2021 10:19:19 +0000 Subject: [PATCH 0364/1120] compute: Rename 'server migrate (confirm|revert)' We're confirming or reverting a server migration, not a server migrate. We've a number of 'server migration *' commands now so it makes sense to move them under here. Change-Id: Ib95bb36511dad1aafe75f0c88d10ded382e4fa5c Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 41 ++++- .../tests/unit/compute/v2/test_server.py | 152 ++++++++++++++++++ ...firm-revert-commands-84fcb937721f5c4a.yaml | 7 + setup.cfg | 2 + 4 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/rename-server-migrate-confirm-revert-commands-84fcb937721f5c4a.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 3c598981f8..419ae4a5dd 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3559,10 +3559,27 @@ def take_action(self, parsed_args): server.confirm_resize() +# TODO(stephenfin): Remove in OSC 7.0 class MigrateConfirm(ResizeConfirm): - _description = _("""Confirm server migrate. + _description = _("""DEPRECATED: Confirm server migration. -Confirm (verify) success of migrate operation and release the old server.""") +Use 'server migration confirm' instead.""") + + def take_action(self, parsed_args): + msg = _( + "The 'server migrate confirm' command has been deprecated in " + "favour of the 'server migration confirm' command." + ) + self.log.warning(msg) + + super().take_action(parsed_args) + + +class ConfirmMigration(ResizeConfirm): + _description = _("""Confirm server migration. + +Confirm (verify) success of the migration operation and release the old +server.""") class ResizeRevert(command.Command): @@ -3590,10 +3607,26 @@ def take_action(self, parsed_args): server.revert_resize() +# TODO(stephenfin): Remove in OSC 7.0 class MigrateRevert(ResizeRevert): - _description = _("""Revert server migrate. + _description = _("""Revert server migration. + +Use 'server migration revert' instead.""") + + def take_action(self, parsed_args): + msg = _( + "The 'server migrate revert' command has been deprecated in " + "favour of the 'server migration revert' command." + ) + self.log.warning(msg) + + super().take_action(parsed_args) + + +class RevertMigration(ResizeRevert): + _description = _("""Revert server migration. -Revert the migrate operation. Release the new server and restart the old +Revert the migration operation. Release the new server and restart the old one.""") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 8d040472f0..ce04ea4ccd 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6401,6 +6401,82 @@ def test_resize_confirm(self): self.server.confirm_resize.assert_called_with() +# TODO(stephenfin): Remove in OSC 7.0 +class TestServerMigrateConfirm(TestServer): + + def setUp(self): + super().setUp() + + methods = { + 'confirm_resize': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.confirm_resize.return_value = None + + # Get the command object to test + self.cmd = server.MigrateConfirm(self.app, None) + + def test_migrate_confirm(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.confirm_resize.assert_called_with() + + mock_warning.assert_called_once() + self.assertIn( + "The 'server migrate confirm' command has been deprecated", + str(mock_warning.call_args[0][0]) + ) + + +class TestServerConfirmMigration(TestServerResizeConfirm): + + def setUp(self): + super().setUp() + + methods = { + 'confirm_resize': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.confirm_resize.return_value = None + + # Get the command object to test + self.cmd = server.ConfirmMigration(self.app, None) + + def test_migration_confirm(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.confirm_resize.assert_called_with() + + class TestServerResizeRevert(TestServer): def setUp(self): @@ -6435,6 +6511,82 @@ def test_resize_revert(self): self.server.revert_resize.assert_called_with() +# TODO(stephenfin): Remove in OSC 7.0 +class TestServerMigrateRevert(TestServer): + + def setUp(self): + super().setUp() + + methods = { + 'revert_resize': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.revert_resize.return_value = None + + # Get the command object to test + self.cmd = server.MigrateRevert(self.app, None) + + def test_migrate_revert(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.revert_resize.assert_called_with() + + mock_warning.assert_called_once() + self.assertIn( + "The 'server migrate revert' command has been deprecated", + str(mock_warning.call_args[0][0]) + ) + + +class TestServerRevertMigration(TestServer): + + def setUp(self): + super().setUp() + + methods = { + 'revert_resize': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.revert_resize.return_value = None + + # Get the command object to test + self.cmd = server.RevertMigration(self.app, None) + + def test_migration_revert(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.revert_resize.assert_called_with() + + class TestServerRestore(TestServer): def setUp(self): diff --git a/releasenotes/notes/rename-server-migrate-confirm-revert-commands-84fcb937721f5c4a.yaml b/releasenotes/notes/rename-server-migrate-confirm-revert-commands-84fcb937721f5c4a.yaml new file mode 100644 index 0000000000..4c24d5f229 --- /dev/null +++ b/releasenotes/notes/rename-server-migrate-confirm-revert-commands-84fcb937721f5c4a.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The ``server migrate confirm`` and ``server migrate revert`` commands, + introduced in OSC 5.0, have been deprecated in favour of + ``server migration confirm`` and ``server migration revert`` respectively. + The deprecated commands will be removed in a future major version. diff --git a/setup.cfg b/setup.cfg index ee412c050a..7c3eef8570 100644 --- a/setup.cfg +++ b/setup.cfg @@ -111,8 +111,10 @@ openstack.compute.v2 = server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert server_migration_abort = openstackclient.compute.v2.server:AbortMigration + server_migration_confirm = openstackclient.compute.v2.server:ConfirmMigration server_migration_force_complete = openstackclient.compute.v2.server:ForceCompleteMigration server_migration_list = openstackclient.compute.v2.server:ListMigration + server_migration_revert = openstackclient.compute.v2.server:RevertMigration server_migration_show = openstackclient.compute.v2.server:ShowMigration server_pause = openstackclient.compute.v2.server:PauseServer server_reboot = openstackclient.compute.v2.server:RebootServer From 074e045c6938189c77fd13ce4dd9fccf69d3a12a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Jan 2021 15:34:25 +0000 Subject: [PATCH 0365/1120] compute: Improve 'server create --block-device-mapping' option parsing Once again, custom actions to the rescue. Change-Id: I6b4f80882dbbeb6a2a7e877f63becae7211b7f9a Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 134 ++++++++++-------- .../tests/unit/compute/v2/test_server.py | 105 ++++++++++---- 2 files changed, 153 insertions(+), 86 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 419ae4a5dd..7edbb18e83 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -678,6 +678,51 @@ def __call__(self, parser, namespace, values, option_string=None): getattr(namespace, self.dest).append(info) +class BDMLegacyAction(argparse.Action): + + def __call__(self, parser, namespace, values, option_string=None): + # Make sure we have an empty dict rather than None + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + + dev_name, sep, dev_map = values.partition('=') + dev_map = dev_map.split(':') if dev_map else dev_map + if not dev_name or not dev_map or len(dev_map) > 4: + msg = _( + "Invalid argument %s; argument must be of form " + "'dev-name=id[:type[:size[:delete-on-terminate]]]'" + ) + raise argparse.ArgumentTypeError(msg % values) + + mapping = { + 'device_name': dev_name, + # store target; this may be a name and will need verification later + 'uuid': dev_map[0], + 'source_type': 'volume', + 'destination_type': 'volume', + } + + # decide source and destination type + if len(dev_map) > 1 and dev_map[1]: + if dev_map[1] not in ('volume', 'snapshot', 'image'): + msg = _( + "Invalid argument %s; 'type' must be one of: volume, " + "snapshot, image" + ) + raise argparse.ArgumentTypeError(msg % values) + + mapping['source_type'] = dev_map[1] + + # 3. append size and delete_on_termination, if present + if len(dev_map) > 2 and dev_map[2]: + mapping['volume_size'] = dev_map[2] + + if len(dev_map) > 3 and dev_map[3]: + mapping['delete_on_termination'] = dev_map[3] + + getattr(namespace, self.dest).append(mapping) + + class CreateServer(command.ShowOne): _description = _("Create a new server") @@ -745,8 +790,8 @@ def get_parser(self, prog_name): parser.add_argument( '--block-device-mapping', metavar='', - action=parseractions.KeyValueAction, - default={}, + action=BDMLegacyAction, + default=[], # NOTE(RuiChen): Add '\n' to the end of line to improve formatting; # see cliff's _SmartHelpFormatter for more details. help=_( @@ -1123,62 +1168,35 @@ def _match_image(image_api, wanted_properties): # If booting from volume we do not pass an image to compute. image = None - boot_args = [parsed_args.server_name, image, flavor] - # Handle block device by device name order, like: vdb -> vdc -> vdd - for dev_name in sorted(parsed_args.block_device_mapping): - dev_map = parsed_args.block_device_mapping[dev_name] - dev_map = dev_map.split(':') - if dev_map[0]: - mapping = {'device_name': dev_name} - - # 1. decide source and destination type - if (len(dev_map) > 1 and - dev_map[1] in ('volume', 'snapshot', 'image')): - mapping['source_type'] = dev_map[1] - else: - mapping['source_type'] = 'volume' - - mapping['destination_type'] = 'volume' - - # 2. check target exist, update target uuid according by - # source type - if mapping['source_type'] == 'volume': - volume_id = utils.find_resource( - volume_client.volumes, dev_map[0]).id - mapping['uuid'] = volume_id - elif mapping['source_type'] == 'snapshot': - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, dev_map[0]).id - mapping['uuid'] = snapshot_id - elif mapping['source_type'] == 'image': - # NOTE(mriedem): In case --image is specified with the same - # image, that becomes the root disk for the server. If the - # block device is specified with a root device name, e.g. - # vda, then the compute API will likely fail complaining - # that there is a conflict. So if using the same image ID, - # which doesn't really make sense but it's allowed, the - # device name would need to be a non-root device, e.g. vdb. - # Otherwise if the block device image is different from the - # one specified by --image, then the compute service will - # create a volume from the image and attach it to the - # server as a non-root volume. - image_id = image_client.find_image(dev_map[0], - ignore_missing=False).id - mapping['uuid'] = image_id - - # 3. append size and delete_on_termination if exist - if len(dev_map) > 2 and dev_map[2]: - mapping['volume_size'] = dev_map[2] - - if len(dev_map) > 3 and dev_map[3]: - mapping['delete_on_termination'] = dev_map[3] - else: - msg = _( - 'Volume, volume snapshot or image (name or ID) must ' - 'be specified if --block-device-mapping is specified' - ) - raise exceptions.CommandError(msg) + for mapping in parsed_args.block_device_mapping: + if mapping['source_type'] == 'volume': + volume_id = utils.find_resource( + volume_client.volumes, mapping['uuid'], + ).id + mapping['uuid'] = volume_id + elif mapping['source_type'] == 'snapshot': + snapshot_id = utils.find_resource( + volume_client.volume_snapshots, mapping['uuid'], + ).id + mapping['uuid'] = snapshot_id + elif mapping['source_type'] == 'image': + # NOTE(mriedem): In case --image is specified with the same + # image, that becomes the root disk for the server. If the + # block device is specified with a root device name, e.g. + # vda, then the compute API will likely fail complaining + # that there is a conflict. So if using the same image ID, + # which doesn't really make sense but it's allowed, the + # device name would need to be a non-root device, e.g. vdb. + # Otherwise if the block device image is different from the + # one specified by --image, then the compute service will + # create a volume from the image and attach it to the + # server as a non-root volume. + image_id = image_client.find_image( + mapping['uuid'], ignore_missing=False, + ).id + mapping['uuid'] = image_id + block_device_mapping_v2.append(mapping) nics = parsed_args.nics @@ -1281,6 +1299,8 @@ def _match_image(image_api, wanted_properties): else: config_drive = parsed_args.config_drive + boot_args = [parsed_args.server_name, image, flavor] + boot_kwargs = dict( meta=parsed_args.properties, files=files, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index ce04ea4ccd..58daf531ec 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1935,7 +1935,15 @@ def test_server_create_with_block_device_mapping(self): verifylist = [ ('image', 'image1'), ('flavor', self.flavor.id), - ('block_device_mapping', {'vda': self.volume.name + ':::false'}), + ('block_device_mapping', [ + { + 'device_name': 'vda', + 'uuid': self.volume.name, + 'source_type': 'volume', + 'destination_type': 'volume', + 'delete_on_termination': 'false', + } + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -1988,7 +1996,14 @@ def test_server_create_with_block_device_mapping_min_input(self): verifylist = [ ('image', 'image1'), ('flavor', self.flavor.id), - ('block_device_mapping', {'vdf': self.volume.name}), + ('block_device_mapping', [ + { + 'device_name': 'vdf', + 'uuid': self.volume.name, + 'source_type': 'volume', + 'destination_type': 'volume', + } + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2040,7 +2055,14 @@ def test_server_create_with_block_device_mapping_default_input(self): verifylist = [ ('image', 'image1'), ('flavor', self.flavor.id), - ('block_device_mapping', {'vdf': self.volume.name + ':::'}), + ('block_device_mapping', [ + { + 'device_name': 'vdf', + 'uuid': self.volume.name, + 'source_type': 'volume', + 'destination_type': 'volume', + } + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2093,8 +2115,16 @@ def test_server_create_with_block_device_mapping_full_input(self): verifylist = [ ('image', 'image1'), ('flavor', self.flavor.id), - ('block_device_mapping', - {'vde': self.volume.name + ':volume:3:true'}), + ('block_device_mapping', [ + { + 'device_name': 'vde', + 'uuid': self.volume.name, + 'source_type': 'volume', + 'destination_type': 'volume', + 'volume_size': '3', + 'delete_on_termination': 'true', + } + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2149,8 +2179,16 @@ def test_server_create_with_block_device_mapping_snapshot(self): verifylist = [ ('image', 'image1'), ('flavor', self.flavor.id), - ('block_device_mapping', - {'vds': self.volume.name + ':snapshot:5:true'}), + ('block_device_mapping', [ + { + 'device_name': 'vds', + 'uuid': self.volume.name, + 'source_type': 'snapshot', + 'volume_size': '5', + 'destination_type': 'volume', + 'delete_on_termination': 'true', + } + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2205,8 +2243,22 @@ def test_server_create_with_block_device_mapping_multiple(self): verifylist = [ ('image', 'image1'), ('flavor', self.flavor.id), - ('block_device_mapping', {'vdb': self.volume.name + ':::false', - 'vdc': self.volume.name + ':::true'}), + ('block_device_mapping', [ + { + 'device_name': 'vdb', + 'uuid': self.volume.name, + 'source_type': 'volume', + 'destination_type': 'volume', + 'delete_on_termination': 'false', + }, + { + 'device_name': 'vdc', + 'uuid': self.volume.name, + 'source_type': 'volume', + 'destination_type': 'volume', + 'delete_on_termination': 'true', + }, + ]), ('config_drive', False), ('server_name', self.new_server.name), ] @@ -2259,26 +2311,29 @@ def test_server_create_with_block_device_mapping_multiple(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device_mapping_invalid_format(self): - # 1. block device mapping don't contain equal sign "=" + # block device mapping don't contain equal sign "=" arglist = [ '--image', 'image1', '--flavor', self.flavor.id, '--block-device-mapping', 'not_contain_equal_sign', self.new_server.name, ] - self.assertRaises(argparse.ArgumentTypeError, - self.check_parser, - self.cmd, arglist, []) - # 2. block device mapping don't contain device name "=uuid:::true" + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) + + # block device mapping don't contain device name "=uuid:::true" arglist = [ '--image', 'image1', '--flavor', self.flavor.id, '--block-device-mapping', '=uuid:::true', self.new_server.name, ] - self.assertRaises(argparse.ArgumentTypeError, - self.check_parser, - self.cmd, arglist, []) + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) def test_server_create_with_block_device_mapping_no_uuid(self): arglist = [ @@ -2287,18 +2342,10 @@ def test_server_create_with_block_device_mapping_no_uuid(self): '--block-device-mapping', 'vdb=', self.new_server.name, ] - verifylist = [ - ('image', 'image1'), - ('flavor', self.flavor.id), - ('block_device_mapping', {'vdb': ''}), - ('config_drive', False), - ('server_name', self.new_server.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) def test_server_create_volume_boot_from_volume_conflict(self): # Tests that specifying --volume and --boot-from-volume results in From 4da4b96296c6b6d4351ebd47e32d5049a88211f1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 10 Dec 2020 12:40:59 +0000 Subject: [PATCH 0366/1120] compute: Add missing 'server create' options Add some volume-related options, namely '--snapshot', '--swap', and '--ephemeral'. All are shortcuts to avoid having to use '--block-device-mapping'. Change-Id: I450e429ade46a7103740150c90e3ba9f2894e1a5 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 108 ++++++-- .../tests/unit/compute/v2/test_server.py | 233 ++++++++++++++++++ ...g-server-create-opts-d5e32bd743e9e132.yaml | 8 + 3 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 7edbb18e83..1e4f19361f 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -772,6 +772,19 @@ def get_parser(self, prog_name): 'volume.' ), ) + disk_group.add_argument( + '--snapshot', + metavar='', + help=_( + 'Create server using this snapshot as the boot disk (name or ' + 'ID)\n' + 'This option automatically creates a block device mapping ' + 'with a boot index of 0. On many hypervisors (libvirt/kvm ' + 'for example) this will be device vda. Do not create a ' + 'duplicate mapping using --block-device-mapping for this ' + 'volume.' + ), + ) parser.add_argument( '--boot-from-volume', metavar='', @@ -784,7 +797,8 @@ def get_parser(self, prog_name): 'given size (in GB) from the specified image and use it ' 'as the root disk of the server. The root volume will not ' 'be deleted when the server is deleted. This option is ' - 'mutually exclusive with the ``--volume`` option.' + 'mutually exclusive with the ``--volume`` and ``--snapshot`` ' + 'options.' ) ) parser.add_argument( @@ -810,6 +824,28 @@ def get_parser(self, prog_name): '(optional)\n' ), ) + parser.add_argument( + '--swap', + metavar='', + type=int, + help=( + "Create and attach a local swap block device of " + "MiB." + ), + ) + parser.add_argument( + '--ephemeral', + metavar='', + action=parseractions.MultiKeyValueAction, + dest='ephemerals', + default=[], + required_keys=['size'], + optional_keys=['format'], + help=( + "Create and attach a local ephemeral block device of " + "GiB and format it to ." + ), + ) parser.add_argument( '--network', metavar="", @@ -929,12 +965,14 @@ def get_parser(self, prog_name): parser.add_argument( '--availability-zone', metavar='', - help=_('Select an availability zone for the server. ' - 'Host and node are optional parameters. ' - 'Availability zone in the format ' - '::, ' - '::, : ' - 'or '), + help=_( + 'Select an availability zone for the server. ' + 'Host and node are optional parameters. ' + 'Availability zone in the format ' + '::, ' + '::, : ' + 'or ' + ), ) parser.add_argument( '--host', @@ -1000,11 +1038,6 @@ def get_parser(self, prog_name): default=1, help=_('Maximum number of servers to launch (default=1)'), ) - parser.add_argument( - '--wait', - action='store_true', - help=_('Wait for build to complete'), - ) parser.add_argument( '--tag', metavar='', @@ -1017,6 +1050,11 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.52 or above)' ), ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait for build to complete'), + ) return parser def take_action(self, parsed_args): @@ -1092,7 +1130,6 @@ def _match_image(image_api, wanted_properties): ) raise exceptions.CommandError(msg) - # Lookup parsed_args.volume volume = None if parsed_args.volume: # --volume and --boot-from-volume are mutually exclusive. @@ -1105,7 +1142,18 @@ def _match_image(image_api, wanted_properties): parsed_args.volume, ).id - # Lookup parsed_args.flavor + snapshot = None + if parsed_args.snapshot: + # --snapshot and --boot-from-volume are mutually exclusive. + if parsed_args.boot_from_volume: + msg = _('--snapshot is not allowed with --boot-from-volume') + raise exceptions.CommandError(msg) + + snapshot = utils.find_resource( + volume_client.volume_snapshots, + parsed_args.snapshot, + ).id + flavor = utils.find_resource( compute_client.flavors, parsed_args.flavor) @@ -1156,6 +1204,14 @@ def _match_image(image_api, wanted_properties): 'source_type': 'volume', 'destination_type': 'volume' }] + elif snapshot: + block_device_mapping_v2 = [{ + 'uuid': snapshot, + 'boot_index': '0', + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'delete_on_termination': False + }] elif parsed_args.boot_from_volume: # Tell nova to create a root volume from the image provided. block_device_mapping_v2 = [{ @@ -1168,6 +1224,30 @@ def _match_image(image_api, wanted_properties): # If booting from volume we do not pass an image to compute. image = None + if parsed_args.swap: + block_device_mapping_v2.append({ + 'boot_index': -1, + 'source_type': 'blank', + 'destination_type': 'local', + 'guest_format': 'swap', + 'volume_size': parsed_args.swap, + 'delete_on_termination': True, + }) + + for mapping in parsed_args.ephemerals: + block_device_mapping_dict = { + 'boot_index': -1, + 'source_type': 'blank', + 'destination_type': 'local', + 'delete_on_termination': True, + 'volume_size': mapping['size'], + } + + if 'format' in mapping: + block_device_mapping_dict['guest_format'] = mapping['format'] + + block_device_mapping_v2.append(block_device_mapping_dict) + # Handle block device by device name order, like: vdb -> vdc -> vdd for mapping in parsed_args.block_device_mapping: if mapping['source_type'] == 'volume': diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 58daf531ec..ce93f21ea3 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1925,6 +1925,109 @@ def test_server_create_userdata(self, mock_open): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_volume(self): + arglist = [ + '--flavor', self.flavor.id, + '--volume', self.volume.name, + self.new_server.name, + ] + verifylist = [ + ('flavor', self.flavor.id), + ('volume', self.volume.name), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'uuid': self.volume.id, + 'boot_index': '0', + 'source_type': 'volume', + 'destination_type': 'volume', + }], + 'nics': [], + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + None, + self.flavor, + **kwargs + ) + self.volumes_mock.get.assert_called_once_with( + self.volume.name) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_snapshot(self): + arglist = [ + '--flavor', self.flavor.id, + '--snapshot', self.snapshot.name, + self.new_server.name, + ] + verifylist = [ + ('flavor', self.flavor.id), + ('snapshot', self.snapshot.name), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'uuid': self.snapshot.id, + 'boot_index': '0', + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'delete_on_termination': False, + }], + 'nics': [], + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + None, + self.flavor, + **kwargs + ) + self.snapshots_mock.get.assert_called_once_with( + self.snapshot.name) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + def test_server_create_with_block_device_mapping(self): arglist = [ '--image', 'image1', @@ -2575,6 +2678,136 @@ def test_server_create_image_property_with_image_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_swap(self): + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--swap', '1024', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', self.flavor.id), + ('swap', 1024), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'boot_index': -1, + 'source_type': 'blank', + 'destination_type': 'local', + 'guest_format': 'swap', + 'volume_size': 1024, + 'delete_on_termination': True, + }], + 'nics': [], + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_ephemeral(self): + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--ephemeral', 'size=1024,format=ext4', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', self.flavor.id), + ('ephemerals', [{'size': '1024', 'format': 'ext4'}]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'boot_index': -1, + 'source_type': 'blank', + 'destination_type': 'local', + 'guest_format': 'ext4', + 'volume_size': '1024', + 'delete_on_termination': True, + }], + 'nics': [], + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_ephemeral_missing_key(self): + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--ephemeral', 'format=ext3', + self.new_server.name, + ] + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) + + def test_server_create_with_ephemeral_invalid_key(self): + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--ephemeral', 'size=1024,foo=bar', + self.new_server.name, + ] + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) + def test_server_create_invalid_hint(self): # Not a key-value pair arglist = [ diff --git a/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml b/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml new file mode 100644 index 0000000000..b82a9d30d1 --- /dev/null +++ b/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add a number of additional options to the ``server create`` command: + + - ``--snapshot`` + - ``--ephemeral`` + - ``--swap`` From f2deabb136efdfca02a51b503f97cbb436ddfb45 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Jan 2021 16:38:52 +0000 Subject: [PATCH 0367/1120] compute: Remove references to optional extensions This is no longer a thing in nova. Change-Id: I2413b826385792a4f33ff70e75621b48de65c799 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1e4f19361f..3db36f7262 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -997,7 +997,7 @@ def get_parser(self, prog_name): metavar='', action=parseractions.KeyValueAppendAction, default={}, - help=_('Hints for the scheduler (optional extension)'), + help=_('Hints for the scheduler'), ) config_drive_group = parser.add_mutually_exclusive_group() config_drive_group.add_argument( From ace4bfb6404b7b39c597c4884c56e26a47a94fc4 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 20 Jan 2021 17:42:42 +0000 Subject: [PATCH 0368/1120] compute: Add 'server create --block-device' option One of the last big gaps with novaclient. As noted in the release note, the current '--block-device-mapping' format is based on the old BDM v1 format, even though it actually results in BDM v2-style requests to the server. It's time to replace that. Change-Id: If4eba38ccfb208ee186b90a0eec95e5fe6cf8415 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 119 +++++++- .../tests/unit/compute/v2/test_server.py | 256 ++++++++++++++++++ ...g-server-create-opts-d5e32bd743e9e132.yaml | 9 + 3 files changed, 383 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 3db36f7262..b1a86a23df 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -801,6 +801,7 @@ def get_parser(self, prog_name): 'options.' ) ) + # TODO(stephenfin): Remove this in the v7.0 parser.add_argument( '--block-device-mapping', metavar='', @@ -809,7 +810,7 @@ def get_parser(self, prog_name): # NOTE(RuiChen): Add '\n' to the end of line to improve formatting; # see cliff's _SmartHelpFormatter for more details. help=_( - 'Create a block device on the server.\n' + '**Deprecated** Create a block device on the server.\n' 'Block device mapping in the format\n' '=:::\n' ': block device name, like: vdb, xvdc ' @@ -822,6 +823,49 @@ def get_parser(self, prog_name): '(optional)\n' ': true or false; default: false ' '(optional)\n' + 'Replaced by --block-device' + ), + ) + parser.add_argument( + '--block-device', + metavar='', + action=parseractions.MultiKeyValueAction, + dest='block_devices', + default=[], + required_keys=[ + 'boot_index', + ], + optional_keys=[ + 'uuid', 'source_type', 'destination_type', + 'disk_bus', 'device_type', 'device_name', 'guest_format', + 'volume_size', 'volume_type', 'delete_on_termination', 'tag', + ], + help=_( + 'Create a block device on the server.\n' + 'Block device in the format:\n' + 'uuid=: UUID of the volume, snapshot or ID ' + '(required if using source image, snapshot or volume),\n' + 'source_type=: source type ' + '(one of: image, snapshot, volume, blank),\n' + 'destination_typ=: destination type ' + '(one of: volume, local) (optional),\n' + 'disk_bus=: device bus ' + '(one of: uml, lxc, virtio, ...) (optional),\n' + 'device_type=: device type ' + '(one of: disk, cdrom, etc. (optional),\n' + 'device_name=: name of the device (optional),\n' + 'volume_size=: size of the block device in MiB ' + '(for swap) or GiB (for everything else) (optional),\n' + 'guest_format=: format of device (optional),\n' + 'boot_index=: index of disk used to order boot ' + 'disk ' + '(required for volume-backed instances),\n' + 'delete_on_termination=: whether to delete the ' + 'volume upon deletion of server (optional),\n' + 'tag=: device metadata tag (optional),\n' + 'volume_type=: type of volume to create (name or ' + 'ID) when source if blank, image or snapshot and dest is ' + 'volume (optional)' ), ) parser.add_argument( @@ -1250,6 +1294,8 @@ def _match_image(image_api, wanted_properties): # Handle block device by device name order, like: vdb -> vdc -> vdd for mapping in parsed_args.block_device_mapping: + # The 'uuid' field isn't necessarily a UUID yet; let's validate it + # just in case if mapping['source_type'] == 'volume': volume_id = utils.find_resource( volume_client.volumes, mapping['uuid'], @@ -1279,6 +1325,77 @@ def _match_image(image_api, wanted_properties): block_device_mapping_v2.append(mapping) + for mapping in parsed_args.block_devices: + try: + mapping['boot_index'] = int(mapping['boot_index']) + except ValueError: + msg = _( + 'The boot_index key of --block-device should be an ' + 'integer' + ) + raise exceptions.CommandError(msg) + + if 'tag' in mapping and ( + compute_client.api_version < api_versions.APIVersion('2.42') + ): + msg = _( + '--os-compute-api-version 2.42 or greater is ' + 'required to support the tag key of --block-device' + ) + raise exceptions.CommandError(msg) + + if 'volume_type' in mapping and ( + compute_client.api_version < api_versions.APIVersion('2.67') + ): + msg = _( + '--os-compute-api-version 2.67 or greater is ' + 'required to support the volume_type key of --block-device' + ) + raise exceptions.CommandError(msg) + + if 'source_type' in mapping: + if mapping['source_type'] not in ( + 'volume', 'image', 'snapshot', 'blank', + ): + msg = _( + 'The source_type key of --block-device should be one ' + 'of: volume, image, snapshot, blank' + ) + raise exceptions.CommandError(msg) + else: + mapping['source_type'] = 'blank' + + if 'destination_type' in mapping: + if mapping['destination_type'] not in ('local', 'volume'): + msg = _( + 'The destination_type key of --block-device should be ' + 'one of: local, volume' + ) + raise exceptions.CommandError(msg) + else: + if mapping['source_type'] in ('image', 'blank'): + mapping['destination_type'] = 'local' + else: # volume, snapshot + mapping['destination_type'] = 'volume' + + if 'delete_on_termination' in mapping: + try: + value = strutils.bool_from_string( + mapping['delete_on_termination'], strict=True) + except ValueError: + msg = _( + 'The delete_on_termination key of --block-device ' + 'should be a boolean-like value' + ) + raise exceptions.CommandError(msg) + + mapping['delete_on_termination'] = value + else: + if mapping['destination_type'] == 'local': + mapping['delete_on_termination'] = True + + block_device_mapping_v2.append(mapping) + nics = parsed_args.nics if 'auto' in nics or 'none' in nics: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index ce93f21ea3..0548924d27 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2028,6 +2028,262 @@ def test_server_create_with_snapshot(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_block_device(self): + block_device = f'uuid={self.volume.id},source_type=volume,boot_index=1' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', self.flavor.id), + ('block_devices', [ + { + 'uuid': self.volume.id, + 'source_type': 'volume', + 'boot_index': '1', + }, + ]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'uuid': self.volume.id, + 'source_type': 'volume', + 'destination_type': 'volume', + 'boot_index': 1, + }], + 'nics': [], + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_block_device_full(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.67') + + block_device = ( + f'uuid={self.volume.id},source_type=volume,' + f'destination_type=volume,disk_bus=ide,device_type=disk,' + f'device_name=sdb,guest_format=ext4,volume_size=64,' + f'volume_type=foo,boot_index=1,delete_on_termination=true,' + f'tag=foo' + ) + + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', self.flavor.id), + ('block_devices', [ + { + 'uuid': self.volume.id, + 'source_type': 'volume', + 'destination_type': 'volume', + 'disk_bus': 'ide', + 'device_type': 'disk', + 'device_name': 'sdb', + 'guest_format': 'ext4', + 'volume_size': '64', + 'volume_type': 'foo', + 'boot_index': '1', + 'delete_on_termination': 'true', + 'tag': 'foo', + }, + ]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'uuid': self.volume.id, + 'source_type': 'volume', + 'destination_type': 'volume', + 'disk_bus': 'ide', + 'device_name': 'sdb', + 'volume_size': '64', + 'guest_format': 'ext4', + 'boot_index': 1, + 'device_type': 'disk', + 'delete_on_termination': True, + 'tag': 'foo', + 'volume_type': 'foo', + }], + 'nics': 'auto', + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_block_device_no_boot_index(self): + block_device = \ + f'uuid={self.volume.name},source_type=volume' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + self.assertRaises( + argparse.ArgumentTypeError, + self.check_parser, + self.cmd, arglist, []) + + def test_server_create_with_block_device_invalid_boot_index(self): + block_device = \ + f'uuid={self.volume.name},source_type=volume,boot_index=foo' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertIn('The boot_index key of --block-device ', str(ex)) + + def test_server_create_with_block_device_invalid_source_type(self): + block_device = f'uuid={self.volume.name},source_type=foo,boot_index=1' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertIn('The source_type key of --block-device ', str(ex)) + + def test_server_create_with_block_device_invalid_destination_type(self): + block_device = \ + f'uuid={self.volume.name},destination_type=foo,boot_index=1' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertIn('The destination_type key of --block-device ', str(ex)) + + def test_server_create_with_block_device_invalid_shutdown(self): + block_device = \ + f'uuid={self.volume.name},delete_on_termination=foo,boot_index=1' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertIn( + 'The delete_on_termination key of --block-device ', str(ex)) + + def test_server_create_with_block_device_tag_pre_v242(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.41') + + block_device = \ + f'uuid={self.volume.name},tag=foo,boot_index=1' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertIn( + '--os-compute-api-version 2.42 or greater is required', + str(ex)) + + def test_server_create_with_block_device_volume_type_pre_v267(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.66') + + block_device = f'uuid={self.volume.name},volume_type=foo,boot_index=1' + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', block_device, + self.new_server.name, + ] + parsed_args = self.check_parser(self.cmd, arglist, []) + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertIn( + '--os-compute-api-version 2.67 or greater is required', + str(ex)) + def test_server_create_with_block_device_mapping(self): arglist = [ '--image', 'image1', diff --git a/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml b/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml index b82a9d30d1..951d944dd4 100644 --- a/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml +++ b/releasenotes/notes/add-missing-server-create-opts-d5e32bd743e9e132.yaml @@ -6,3 +6,12 @@ features: - ``--snapshot`` - ``--ephemeral`` - ``--swap`` + - ``--block-device`` +deprecations: + - | + The ``--block-device-mapping`` option of the ``server create`` command + has been deprecated in favour of ``--block-device``. The format of the + ``--block-device-mapping`` option is based on the limited "BDM v1" + format for block device maps introduced way back in the v1 nova API. The + ``--block-device`` option instead exposes the richer key-value based + "BDM v2" format. From 2bdf34dcc3f8957ff78709467197b5fcb44e07c3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 21 Jan 2021 11:25:08 +0000 Subject: [PATCH 0369/1120] compute: Auto-configure shared/block live migration API microversion 2.25 introduced the 'block_migration=auto' value for the os-migrateLive server action. This is a sensible default that we should use, allowing users to avoid stating one of the '--block-migration' or '--shared-migration' parameters explicitly. While we're here, we take the opportunity to fix up some formatting in the function, which is really rather messy. Change-Id: Ieedc77d6dc3d4a3cd93b29672faa97dd4e8c1185 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 87 +++++++++++++------ .../tests/unit/compute/v2/test_server.py | 45 +++++----- ...block-live-migration-437d461c914f8f2f.yaml | 6 ++ 3 files changed, 90 insertions(+), 48 deletions(-) create mode 100644 releasenotes/notes/auto-configure-block-live-migration-437d461c914f8f2f.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b1a86a23df..c42486d9c7 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2457,9 +2457,11 @@ def get_parser(self, prog_name): '--live-migration', dest='live_migration', action='store_true', - help=_('Live migrate the server. Use the ``--host`` option to ' - 'specify a target host for the migration which will be ' - 'validated by the scheduler.'), + help=_( + 'Live migrate the server; use the ``--host`` option to ' + 'specify a target host for the migration which will be ' + 'validated by the scheduler' + ), ) # The --live and --host options are mutually exclusive ways of asking # for a target host during a live migration. @@ -2469,37 +2471,48 @@ def get_parser(self, prog_name): host_group.add_argument( '--live', metavar='', - help=_('**Deprecated** This option is problematic in that it ' - 'requires a host and prior to compute API version 2.30, ' - 'specifying a host during live migration will bypass ' - 'validation by the scheduler which could result in ' - 'failures to actually migrate the server to the specified ' - 'host or over-subscribe the host. Use the ' - '``--live-migration`` option instead. If both this option ' - 'and ``--live-migration`` are used, ``--live-migration`` ' - 'takes priority.'), + help=_( + '**Deprecated** This option is problematic in that it ' + 'requires a host and prior to compute API version 2.30, ' + 'specifying a host during live migration will bypass ' + 'validation by the scheduler which could result in ' + 'failures to actually migrate the server to the specified ' + 'host or over-subscribe the host. Use the ' + '``--live-migration`` option instead. If both this option ' + 'and ``--live-migration`` are used, ``--live-migration`` ' + 'takes priority.' + ), ) host_group.add_argument( '--host', metavar='', - help=_('Migrate the server to the specified host. Requires ' - '``--os-compute-api-version`` 2.30 or greater when used ' - 'with the ``--live-migration`` option, otherwise requires ' - '``--os-compute-api-version`` 2.56 or greater.'), + help=_( + 'Migrate the server to the specified host. ' + '(supported with --os-compute-api-version 2.30 or above ' + 'when used with the --live-migration option) ' + '(supported with --os-compute-api-version 2.56 or above ' + 'when used without the --live-migration option)' + ), ) migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( '--shared-migration', dest='block_migration', action='store_false', - default=False, - help=_('Perform a shared live migration (default)'), + default=None, + help=_( + 'Perform a shared live migration ' + '(default before --os-compute-api-version 2.25, auto after)' + ), ) migration_group.add_argument( '--block-migration', dest='block_migration', action='store_true', - help=_('Perform a block live migration'), + help=_( + 'Perform a block live migration ' + '(auto-configured from --os-compute-api-version 2.25)' + ), ) disk_group = parser.add_mutually_exclusive_group() disk_group.add_argument( @@ -2513,8 +2526,9 @@ def get_parser(self, prog_name): dest='disk_overcommit', action='store_false', default=False, - help=_('Do not over-commit disk on the' - ' destination host (default)'), + help=_( + 'Do not over-commit disk on the destination host (default)' + ), ) parser.add_argument( '--wait', @@ -2549,9 +2563,21 @@ def _show_progress(progress): if parsed_args.live or parsed_args.live_migration: # Always log a warning if --live is used. self._log_warning_for_live(parsed_args) - kwargs = { - 'block_migration': parsed_args.block_migration - } + + kwargs = {} + + block_migration = parsed_args.block_migration + if block_migration is None: + if ( + compute_client.api_version < + api_versions.APIVersion('2.25') + ): + block_migration = False + else: + block_migration = 'auto' + + kwargs['block_migration'] = block_migration + # Prefer --live-migration over --live if both are specified. if parsed_args.live_migration: # Technically we could pass a non-None host with @@ -2560,12 +2586,16 @@ def _show_progress(progress): # want to support, so if the user is using --live-migration # and --host, we want to enforce that they are using version # 2.30 or greater. - if (parsed_args.host and - compute_client.api_version < - api_versions.APIVersion('2.30')): + if ( + parsed_args.host and + compute_client.api_version < + api_versions.APIVersion('2.30') + ): raise exceptions.CommandError( '--os-compute-api-version 2.30 or greater is required ' - 'when using --host') + 'when using --host' + ) + # The host parameter is required in the API even if None. kwargs['host'] = parsed_args.host else: @@ -2573,6 +2603,7 @@ def _show_progress(progress): if compute_client.api_version < api_versions.APIVersion('2.25'): kwargs['disk_over_commit'] = parsed_args.disk_overcommit + server.live_migrate(**kwargs) else: if parsed_args.block_migration or parsed_args.disk_overcommit: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 0548924d27..5de6d006ca 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4524,7 +4524,7 @@ def test_server_migrate_no_options(self): ] verifylist = [ ('live', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4547,7 +4547,7 @@ def test_server_migrate_with_host_2_56(self): ('live', None), ('live_migration', False), ('host', 'fakehost'), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4588,7 +4588,7 @@ def test_server_migrate_with_disk_overcommit(self): ] verifylist = [ ('live', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', True), ('wait', False), ] @@ -4611,7 +4611,7 @@ def test_server_migrate_with_host_pre_2_56(self): ('live', None), ('live_migration', False), ('host', 'fakehost'), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4637,7 +4637,7 @@ def test_server_live_migrate(self): ('live', 'fakehost'), ('live_migration', False), ('host', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4670,7 +4670,7 @@ def test_server_live_migrate_host_pre_2_30(self): ('live', None), ('live_migration', True), ('host', 'fakehost'), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4696,7 +4696,7 @@ def test_server_live_migrate_no_host(self): ('live', None), ('live_migration', True), ('host', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4724,7 +4724,7 @@ def test_server_live_migrate_with_host(self): ('live', None), ('live_migration', True), ('host', 'fakehost'), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4736,9 +4736,10 @@ def test_server_live_migrate_with_host(self): result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - # No disk_overcommit with microversion >= 2.25. - self.server.live_migrate.assert_called_with(block_migration=False, - host='fakehost') + # No disk_overcommit and block_migration defaults to auto with + # microversion >= 2.25 + self.server.live_migrate.assert_called_with( + block_migration='auto', host='fakehost') self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) @@ -4753,7 +4754,7 @@ def test_server_live_migrate_without_host_override_live(self): ('live', 'fakehost'), ('live_migration', True), ('host', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] @@ -4779,8 +4780,9 @@ def test_server_live_migrate_live_and_host_mutex(self): arglist = [ '--live', 'fakehost', '--host', 'fakehost', self.server.id, ] - self.assertRaises(utils.ParserException, - self.check_parser, self.cmd, arglist, verify_args=[]) + self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verify_args=[]) def test_server_block_live_migrate(self): arglist = [ @@ -4812,7 +4814,7 @@ def test_server_live_migrate_with_disk_overcommit(self): ] verifylist = [ ('live', 'fakehost'), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', True), ('wait', False), ] @@ -4861,7 +4863,7 @@ def test_server_live_migrate_225(self): ] verifylist = [ ('live', 'fakehost'), - ('block_migration', False), + ('block_migration', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4872,8 +4874,11 @@ def test_server_live_migrate_225(self): result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=False, - host='fakehost') + # No disk_overcommit and block_migration defaults to auto with + # microversion >= 2.25 + self.server.live_migrate.assert_called_with( + block_migration='auto', + host='fakehost') self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) @@ -4884,7 +4889,7 @@ def test_server_migrate_with_wait(self, mock_wait_for_status): ] verifylist = [ ('live', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', True), ] @@ -4904,7 +4909,7 @@ def test_server_migrate_with_wait_fails(self, mock_wait_for_status): ] verifylist = [ ('live', None), - ('block_migration', False), + ('block_migration', None), ('disk_overcommit', False), ('wait', True), ] diff --git a/releasenotes/notes/auto-configure-block-live-migration-437d461c914f8f2f.yaml b/releasenotes/notes/auto-configure-block-live-migration-437d461c914f8f2f.yaml new file mode 100644 index 0000000000..e1ff187e62 --- /dev/null +++ b/releasenotes/notes/auto-configure-block-live-migration-437d461c914f8f2f.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``server migrate`` command will now automatically determine whether + to use block or shared migration during a live migration operation. This + requires Compute API microversion 2.25 or greater. From 8868c77a201703edaded5d06aa1734265431f786 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 21 Jan 2021 11:45:38 +0000 Subject: [PATCH 0370/1120] compute: Stop silently ignore --(no-)disk-overcommit These options are not supported from Nova API microversion 2.25 and above. This can be a source of confusion. Start warning, with an eye on erroring out in the future. Change-Id: I53f27eb3e3c1a84d0d77a1672c008d0e8bb8536f Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 15 +++++++- .../tests/unit/compute/v2/test_server.py | 34 +++++++++++++++++++ ...n-on-disk-overcommit-087ae46f12d74693.yaml | 9 +++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/warn-on-disk-overcommit-087ae46f12d74693.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c42486d9c7..111c4a6b04 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2519,7 +2519,10 @@ def get_parser(self, prog_name): '--disk-overcommit', action='store_true', default=False, - help=_('Allow disk over-commit on the destination host'), + help=_( + 'Allow disk over-commit on the destination host' + '(supported with --os-compute-api-version 2.24 or below)' + ), ) disk_group.add_argument( '--no-disk-overcommit', @@ -2528,6 +2531,7 @@ def get_parser(self, prog_name): default=False, help=_( 'Do not over-commit disk on the destination host (default)' + '(supported with --os-compute-api-version 2.24 or below)' ), ) parser.add_argument( @@ -2603,6 +2607,15 @@ def _show_progress(progress): if compute_client.api_version < api_versions.APIVersion('2.25'): kwargs['disk_over_commit'] = parsed_args.disk_overcommit + elif parsed_args.disk_overcommit is not None: + # TODO(stephenfin): Raise an error here in OSC 7.0 + msg = _( + 'The --disk-overcommit and --no-disk-overcommit ' + 'options are only supported by ' + '--os-compute-api-version 2.24 or below; this will ' + 'be an error in a future release' + ) + self.log.warning(msg) server.live_migrate(**kwargs) else: diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5de6d006ca..9afc4cebe8 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4832,6 +4832,40 @@ def test_server_live_migrate_with_disk_overcommit(self): self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) + def test_server_live_migrate_with_disk_overcommit_post_v224(self): + arglist = [ + '--live-migration', + '--disk-overcommit', + self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('block_migration', None), + ('disk_overcommit', True), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.25') + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + # There should be no 'disk_over_commit' value present + self.server.live_migrate.assert_called_with( + block_migration='auto', + host=None) + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + # A warning should have been logged for using --disk-overcommit. + mock_warning.assert_called_once() + self.assertIn( + 'The --disk-overcommit and --no-disk-overcommit options ', + str(mock_warning.call_args[0][0])) + def test_server_live_migrate_with_false_value_options(self): arglist = [ '--live', 'fakehost', '--no-disk-overcommit', diff --git a/releasenotes/notes/warn-on-disk-overcommit-087ae46f12d74693.yaml b/releasenotes/notes/warn-on-disk-overcommit-087ae46f12d74693.yaml new file mode 100644 index 0000000000..766dbab320 --- /dev/null +++ b/releasenotes/notes/warn-on-disk-overcommit-087ae46f12d74693.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + A warning will now be emitted when using the ``--disk-overcommit`` + or ``--no-disk-overcommit`` flags of the ``server migrate`` command on + Compute API microversion 2.25 or greater. This feature is only supported + before this microversion and previously the flag was silently ignored on + newer microversions. This warning will become an error in a future + release. From 6f3969a0c8a608236a6f7258aa213c12af060a9d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 21 Jan 2021 12:06:57 +0000 Subject: [PATCH 0371/1120] compute: Deprecate 'server create --file' The parameter isn't actually deprecated, since we need to support older API microversion, however, we now emit an error if someone attempts to boot a server with the wrong microversion. This would happen server-side anyway since this parameter was removed entirely in API microversion 2.57. Change-Id: I73864ccbf5bf181fecf505ca168c1a35a8b0af3a Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 12 +++++++++++- ...d-server-create-file-option-80246b13bd3c1b43.yaml | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/deprecated-server-create-file-option-80246b13bd3c1b43.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 111c4a6b04..48f5b7cf16 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -989,8 +989,9 @@ def get_parser(self, prog_name): action='append', default=[], help=_( - 'File to inject into image before boot ' + 'File(s) to inject into image before boot ' '(repeat option to set multiple files)' + '(supported by --os-compute-api-version 2.57 or below)' ), ) parser.add_argument( @@ -1201,6 +1202,15 @@ def _match_image(image_api, wanted_properties): flavor = utils.find_resource( compute_client.flavors, parsed_args.flavor) + if parsed_args.file: + if compute_client.api_version >= api_versions.APIVersion('2.57'): + msg = _( + 'Personality files are deprecated and are not supported ' + 'for --os-compute-api-version greater than 2.56; use ' + 'user data instead' + ) + raise exceptions.CommandError(msg) + files = {} for f in parsed_args.file: dst, src = f.split('=', 1) diff --git a/releasenotes/notes/deprecated-server-create-file-option-80246b13bd3c1b43.yaml b/releasenotes/notes/deprecated-server-create-file-option-80246b13bd3c1b43.yaml new file mode 100644 index 0000000000..93de724461 --- /dev/null +++ b/releasenotes/notes/deprecated-server-create-file-option-80246b13bd3c1b43.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The ``server create`` command will now error out if the ``--file`` option + is specified alongside ``--os-compute-api-version`` of ``2.57`` or greater. + This reflects the removal of this feature from the compute service in this + microversion. From 70480fa86236f7de583c7b098cc53f0acedfd91d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 21 Jan 2021 12:26:13 +0000 Subject: [PATCH 0372/1120] compute: Remove deprecated 'server migrate --live' option It's been long enough. Time to remove this. Change-Id: I37ef09eca0db9286544a4b0bb33f845311baa9b2 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 75 ++----- .../tests/unit/compute/v2/test_server.py | 203 ++++-------------- ...-migrate-live-option-28ec43ee210124dc.yaml | 8 + 3 files changed, 67 insertions(+), 219 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-server-migrate-live-option-28ec43ee210124dc.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 48f5b7cf16..9838ed5480 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2473,27 +2473,7 @@ def get_parser(self, prog_name): 'validated by the scheduler' ), ) - # The --live and --host options are mutually exclusive ways of asking - # for a target host during a live migration. - host_group = parser.add_mutually_exclusive_group() - # TODO(mriedem): Remove --live in the next major version bump after - # the Train release. - host_group.add_argument( - '--live', - metavar='', - help=_( - '**Deprecated** This option is problematic in that it ' - 'requires a host and prior to compute API version 2.30, ' - 'specifying a host during live migration will bypass ' - 'validation by the scheduler which could result in ' - 'failures to actually migrate the server to the specified ' - 'host or over-subscribe the host. Use the ' - '``--live-migration`` option instead. If both this option ' - 'and ``--live-migration`` are used, ``--live-migration`` ' - 'takes priority.' - ), - ) - host_group.add_argument( + parser.add_argument( '--host', metavar='', help=_( @@ -2551,15 +2531,6 @@ def get_parser(self, prog_name): ) return parser - def _log_warning_for_live(self, parsed_args): - if parsed_args.live: - # NOTE(mriedem): The --live option requires a host and if - # --os-compute-api-version is less than 2.30 it will forcefully - # bypass the scheduler which is dangerous. - self.log.warning(_( - 'The --live option has been deprecated. Please use the ' - '--live-migration option instead.')) - def take_action(self, parsed_args): def _show_progress(progress): @@ -2573,11 +2544,8 @@ def _show_progress(progress): compute_client.servers, parsed_args.server, ) - # Check for live migration. - if parsed_args.live or parsed_args.live_migration: - # Always log a warning if --live is used. - self._log_warning_for_live(parsed_args) + if parsed_args.live_migration: kwargs = {} block_migration = parsed_args.block_migration @@ -2592,28 +2560,23 @@ def _show_progress(progress): kwargs['block_migration'] = block_migration - # Prefer --live-migration over --live if both are specified. - if parsed_args.live_migration: - # Technically we could pass a non-None host with - # --os-compute-api-version < 2.30 but that is the same thing - # as the --live option bypassing the scheduler which we don't - # want to support, so if the user is using --live-migration - # and --host, we want to enforce that they are using version - # 2.30 or greater. - if ( - parsed_args.host and - compute_client.api_version < - api_versions.APIVersion('2.30') - ): - raise exceptions.CommandError( - '--os-compute-api-version 2.30 or greater is required ' - 'when using --host' - ) + # Technically we could pass a non-None host with + # --os-compute-api-version < 2.30 but that is the same thing + # as the --live option bypassing the scheduler which we don't + # want to support, so if the user is using --live-migration + # and --host, we want to enforce that they are using version + # 2.30 or greater. + if ( + parsed_args.host and + compute_client.api_version < api_versions.APIVersion('2.30') + ): + raise exceptions.CommandError( + '--os-compute-api-version 2.30 or greater is required ' + 'when using --host' + ) - # The host parameter is required in the API even if None. - kwargs['host'] = parsed_args.host - else: - kwargs['host'] = parsed_args.live + # The host parameter is required in the API even if None. + kwargs['host'] = parsed_args.host if compute_client.api_version < api_versions.APIVersion('2.25'): kwargs['disk_over_commit'] = parsed_args.disk_overcommit @@ -2628,7 +2591,7 @@ def _show_progress(progress): self.log.warning(msg) server.live_migrate(**kwargs) - else: + else: # cold migration if parsed_args.block_migration or parsed_args.disk_overcommit: raise exceptions.CommandError( "--live-migration must be specified if " diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9afc4cebe8..ced5d458c2 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4523,7 +4523,7 @@ def test_server_migrate_no_options(self): self.server.id, ] verifylist = [ - ('live', None), + ('live_migration', False), ('block_migration', None), ('disk_overcommit', False), ('wait', False), @@ -4544,7 +4544,6 @@ def test_server_migrate_with_host_2_56(self): '--host', 'fakehost', self.server.id, ] verifylist = [ - ('live', None), ('live_migration', False), ('host', 'fakehost'), ('block_migration', None), @@ -4568,7 +4567,7 @@ def test_server_migrate_with_block_migration(self): '--block-migration', self.server.id, ] verifylist = [ - ('live', None), + ('live_migration', False), ('block_migration', True), ('disk_overcommit', False), ('wait', False), @@ -4587,7 +4586,7 @@ def test_server_migrate_with_disk_overcommit(self): '--disk-overcommit', self.server.id, ] verifylist = [ - ('live', None), + ('live_migration', False), ('block_migration', None), ('disk_overcommit', True), ('wait', False), @@ -4608,7 +4607,6 @@ def test_server_migrate_with_host_pre_2_56(self): '--host', 'fakehost', self.server.id, ] verifylist = [ - ('live', None), ('live_migration', False), ('host', 'fakehost'), ('block_migration', None), @@ -4630,70 +4628,11 @@ def test_server_migrate_with_host_pre_2_56(self): self.assertNotCalled(self.servers_mock.migrate) def test_server_live_migrate(self): - arglist = [ - '--live', 'fakehost', self.server.id, - ] - verifylist = [ - ('live', 'fakehost'), - ('live_migration', False), - ('host', None), - ('block_migration', None), - ('disk_overcommit', False), - ('wait', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.24') - - with mock.patch.object(self.cmd.log, 'warning') as mock_warning: - result = self.cmd.take_action(parsed_args) - - self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=False, - disk_over_commit=False, - host='fakehost') - self.assertNotCalled(self.servers_mock.migrate) - self.assertIsNone(result) - # A warning should have been logged for using --live. - mock_warning.assert_called_once() - self.assertIn('The --live option has been deprecated.', - str(mock_warning.call_args[0][0])) - - def test_server_live_migrate_host_pre_2_30(self): - # Tests that the --host option is not supported for --live-migration - # before microversion 2.30 (the test defaults to 2.1). - arglist = [ - '--live-migration', '--host', 'fakehost', self.server.id, - ] - verifylist = [ - ('live', None), - ('live_migration', True), - ('host', 'fakehost'), - ('block_migration', None), - ('disk_overcommit', False), - ('wait', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - ex = self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) - - # Make sure it's the error we expect. - self.assertIn('--os-compute-api-version 2.30 or greater is required ' - 'when using --host', str(ex)) - - self.servers_mock.get.assert_called_with(self.server.id) - self.assertNotCalled(self.servers_mock.live_migrate) - self.assertNotCalled(self.servers_mock.migrate) - - def test_server_live_migrate_no_host(self): # Tests the --live-migration option without --host or --live. arglist = [ '--live-migration', self.server.id, ] verifylist = [ - ('live', None), ('live_migration', True), ('host', None), ('block_migration', None), @@ -4702,26 +4641,22 @@ def test_server_live_migrate_no_host(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(self.cmd.log, 'warning') as mock_warning: - result = self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=False, - disk_over_commit=False, - host=None) + self.server.live_migrate.assert_called_with( + block_migration=False, + disk_over_commit=False, + host=None) self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) - # Since --live wasn't used a warning shouldn't have been logged. - mock_warning.assert_not_called() def test_server_live_migrate_with_host(self): - # Tests the --live-migration option with --host but no --live. # This requires --os-compute-api-version >= 2.30 so the test uses 2.30. arglist = [ '--live-migration', '--host', 'fakehost', self.server.id, ] verifylist = [ - ('live', None), ('live_migration', True), ('host', 'fakehost'), ('block_migration', None), @@ -4743,53 +4678,42 @@ def test_server_live_migrate_with_host(self): self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) - def test_server_live_migrate_without_host_override_live(self): - # Tests the --live-migration option without --host and with --live. - # The --live-migration option will take precedence and a warning is - # logged for using --live. + def test_server_live_migrate_with_host_pre_v230(self): + # Tests that the --host option is not supported for --live-migration + # before microversion 2.30 (the test defaults to 2.1). arglist = [ - '--live', 'fakehost', '--live-migration', self.server.id, + '--live-migration', '--host', 'fakehost', self.server.id, ] verifylist = [ - ('live', 'fakehost'), ('live_migration', True), - ('host', None), + ('host', 'fakehost'), ('block_migration', None), ('disk_overcommit', False), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(self.cmd.log, 'warning') as mock_warning: - result = self.cmd.take_action(parsed_args) + ex = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # Make sure it's the error we expect. + self.assertIn( + '--os-compute-api-version 2.30 or greater is required ' + 'when using --host', str(ex)) self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=False, - disk_over_commit=False, - host=None) + self.assertNotCalled(self.servers_mock.live_migrate) self.assertNotCalled(self.servers_mock.migrate) - self.assertIsNone(result) - # A warning should have been logged for using --live. - mock_warning.assert_called_once() - self.assertIn('The --live option has been deprecated.', - str(mock_warning.call_args[0][0])) - - def test_server_live_migrate_live_and_host_mutex(self): - # Tests specifying both the --live and --host options which are in a - # mutex group so argparse should fail. - arglist = [ - '--live', 'fakehost', '--host', 'fakehost', self.server.id, - ] - self.assertRaises( - utils.ParserException, - self.check_parser, self.cmd, arglist, verify_args=[]) def test_server_block_live_migrate(self): arglist = [ - '--live', 'fakehost', '--block-migration', self.server.id, + '--live-migration', + '--block-migration', + self.server.id, ] verifylist = [ - ('live', 'fakehost'), + ('live_migration', True), ('block_migration', True), ('disk_overcommit', False), ('wait', False), @@ -4802,18 +4726,21 @@ def test_server_block_live_migrate(self): result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=True, - disk_over_commit=False, - host='fakehost') + self.server.live_migrate.assert_called_with( + block_migration=True, + disk_over_commit=False, + host=None) self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) def test_server_live_migrate_with_disk_overcommit(self): arglist = [ - '--live', 'fakehost', '--disk-overcommit', self.server.id, + '--live-migration', + '--disk-overcommit', + self.server.id, ] verifylist = [ - ('live', 'fakehost'), + ('live_migration', True), ('block_migration', None), ('disk_overcommit', True), ('wait', False), @@ -4826,9 +4753,10 @@ def test_server_live_migrate_with_disk_overcommit(self): result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=False, - disk_over_commit=True, - host='fakehost') + self.server.live_migrate.assert_called_with( + block_migration=False, + disk_over_commit=True, + host=None) self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) @@ -4839,7 +4767,6 @@ def test_server_live_migrate_with_disk_overcommit_post_v224(self): self.server.id, ] verifylist = [ - ('live', None), ('live_migration', True), ('block_migration', None), ('disk_overcommit', True), @@ -4866,63 +4793,13 @@ def test_server_live_migrate_with_disk_overcommit_post_v224(self): 'The --disk-overcommit and --no-disk-overcommit options ', str(mock_warning.call_args[0][0])) - def test_server_live_migrate_with_false_value_options(self): - arglist = [ - '--live', 'fakehost', '--no-disk-overcommit', - '--shared-migration', self.server.id, - ] - verifylist = [ - ('live', 'fakehost'), - ('block_migration', False), - ('disk_overcommit', False), - ('wait', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.24') - - result = self.cmd.take_action(parsed_args) - - self.servers_mock.get.assert_called_with(self.server.id) - self.server.live_migrate.assert_called_with(block_migration=False, - disk_over_commit=False, - host='fakehost') - self.assertNotCalled(self.servers_mock.migrate) - self.assertIsNone(result) - - def test_server_live_migrate_225(self): - arglist = [ - '--live', 'fakehost', self.server.id, - ] - verifylist = [ - ('live', 'fakehost'), - ('block_migration', None), - ('wait', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.25') - - result = self.cmd.take_action(parsed_args) - - self.servers_mock.get.assert_called_with(self.server.id) - # No disk_overcommit and block_migration defaults to auto with - # microversion >= 2.25 - self.server.live_migrate.assert_called_with( - block_migration='auto', - host='fakehost') - self.assertNotCalled(self.servers_mock.migrate) - self.assertIsNone(result) - @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_server_migrate_with_wait(self, mock_wait_for_status): arglist = [ '--wait', self.server.id, ] verifylist = [ - ('live', None), + ('live_migration', False), ('block_migration', None), ('disk_overcommit', False), ('wait', True), @@ -4942,7 +4819,7 @@ def test_server_migrate_with_wait_fails(self, mock_wait_for_status): '--wait', self.server.id, ] verifylist = [ - ('live', None), + ('live_migration', False), ('block_migration', None), ('disk_overcommit', False), ('wait', True), diff --git a/releasenotes/notes/remove-deprecated-server-migrate-live-option-28ec43ee210124dc.yaml b/releasenotes/notes/remove-deprecated-server-migrate-live-option-28ec43ee210124dc.yaml new file mode 100644 index 0000000000..bc31aa2699 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-server-migrate-live-option-28ec43ee210124dc.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The deprecated ``--live`` option of the ``server migrate`` command has + been removed. This was problematic as it required a host argument and + would result in a forced live migration to a host, bypassing the + scheduler. It has been replaced by a ``--live-migration`` option and + optional ``--host`` option. From 119d2fae2567285b9149b2c737d7d4452b59288c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 9 Jun 2020 11:35:46 +0200 Subject: [PATCH 0373/1120] project cleanup New implementation of the project cleanup based on the sdk.project_cleanup. It is implemented as an additional OSC operation and will ideally obsolete the `openstack project purge` giving flexibility to extend services support, parallelization, filters, etc. Change-Id: Ie08877f182379f73e5ec5ad4daaf84b3092c829c --- .../cli/command-objects/project-cleanup.rst | 12 ++ doc/source/cli/commands.rst | 1 + openstackclient/common/project_cleanup.py | 140 ++++++++++++++ .../tests/unit/common/test_project_cleanup.py | 183 ++++++++++++++++++ .../add-project-cleanup-beb08c9df3c95b24.yaml | 6 + setup.cfg | 1 + 6 files changed, 343 insertions(+) create mode 100644 doc/source/cli/command-objects/project-cleanup.rst create mode 100644 openstackclient/common/project_cleanup.py create mode 100644 openstackclient/tests/unit/common/test_project_cleanup.py create mode 100644 releasenotes/notes/add-project-cleanup-beb08c9df3c95b24.yaml diff --git a/doc/source/cli/command-objects/project-cleanup.rst b/doc/source/cli/command-objects/project-cleanup.rst new file mode 100644 index 0000000000..e76e538948 --- /dev/null +++ b/doc/source/cli/command-objects/project-cleanup.rst @@ -0,0 +1,12 @@ +=============== +project cleanup +=============== + +Clean resources associated with a specific project based on OpenStackSDK +implementation + +Block Storage v2, v3; Compute v2; Network v2; DNS v2; Orchestrate v1 + + +.. autoprogram-cliff:: openstack.common + :command: project cleanup diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 0dfac00bdc..94a0b5a638 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -270,6 +270,7 @@ Those actions with an opposite action are noted in parens if applicable. * ``pause`` (``unpause``) - stop one or more servers and leave them in memory * ``query`` - Query resources by Elasticsearch query string or json format DSL. * ``purge`` - clean resources associated with a specific project +* ``cleanup`` - flexible clean resources associated with a specific project * ``reboot`` - forcibly reboot a server * ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create * ``remove`` (``add``) - remove an object from a group of objects diff --git a/openstackclient/common/project_cleanup.py b/openstackclient/common/project_cleanup.py new file mode 100644 index 0000000000..f253635495 --- /dev/null +++ b/openstackclient/common/project_cleanup.py @@ -0,0 +1,140 @@ +# Copyright 2020 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. +# + +import getpass +import logging +import os +import queue + +from cliff.formatters import table +from osc_lib.command import command + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +LOG = logging.getLogger(__name__) + + +def ask_user_yesno(msg, default=True): + """Ask user Y/N question + + :param str msg: question text + :param bool default: default value + :return bool: User choice + """ + while True: + answer = getpass._raw_input( + '{} [{}]: '.format(msg, 'y/N' if not default else 'Y/n')) + if answer in ('y', 'Y', 'yes'): + return True + elif answer in ('n', 'N', 'no'): + return False + + +class ProjectCleanup(command.Command): + _description = _("Clean resources associated with a project") + + def get_parser(self, prog_name): + parser = super(ProjectCleanup, self).get_parser(prog_name) + parser.add_argument( + '--dry-run', + action='store_true', + help=_("List a project's resources") + ) + project_group = parser.add_mutually_exclusive_group(required=True) + project_group.add_argument( + '--auth-project', + action='store_true', + help=_('Delete resources of the project used to authenticate') + ) + project_group.add_argument( + '--project', + metavar='', + help=_('Project to clean (name or ID)') + ) + parser.add_argument( + '--created-before', + metavar='', + help=_('Drop resources created before the given time') + ) + parser.add_argument( + '--updated-before', + metavar='', + help=_('Drop resources updated before the given time') + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + sdk = self.app.client_manager.sdk_connection + + if parsed_args.auth_project: + project_connect = sdk + elif parsed_args.project: + project = sdk.identity.find_project( + name_or_id=parsed_args.project, + ignore_missing=False) + project_connect = sdk.connect_as_project(project) + + if project_connect: + status_queue = queue.Queue() + parsed_args.max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH', + 0)) + parsed_args.fit_width = bool(int(os.environ.get('CLIFF_FIT_WIDTH', + 0))) + parsed_args.print_empty = False + table_fmt = table.TableFormatter() + + self.log.info('Searching resources...') + + filters = {} + if parsed_args.created_before: + filters['created_at'] = parsed_args.created_before + + if parsed_args.updated_before: + filters['updated_at'] = parsed_args.updated_before + + project_connect.project_cleanup(dry_run=True, + status_queue=status_queue, + filters=filters) + + data = [] + while not status_queue.empty(): + resource = status_queue.get_nowait() + data.append( + (type(resource).__name__, resource.id, resource.name)) + status_queue.task_done() + status_queue.join() + table_fmt.emit_list( + ('Type', 'ID', 'Name'), + data, + self.app.stdout, + parsed_args + ) + + if parsed_args.dry_run: + return + + confirm = ask_user_yesno( + _("These resources will be deleted. Are you sure"), + default=False) + + if confirm: + self.log.warning(_('Deleting resources')) + + project_connect.project_cleanup(dry_run=False, + status_queue=status_queue, + filters=filters) diff --git a/openstackclient/tests/unit/common/test_project_cleanup.py b/openstackclient/tests/unit/common/test_project_cleanup.py new file mode 100644 index 0000000000..d235aeb063 --- /dev/null +++ b/openstackclient/tests/unit/common/test_project_cleanup.py @@ -0,0 +1,183 @@ +# 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 io import StringIO +from unittest import mock + +from openstackclient.common import project_cleanup +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestProjectCleanupBase(tests_utils.TestCommand): + + def setUp(self): + super(TestProjectCleanupBase, self).setUp() + + self.app.client_manager.sdk_connection = mock.Mock() + + +class TestProjectCleanup(TestProjectCleanupBase): + + project = identity_fakes.FakeProject.create_one_project() + + def setUp(self): + super(TestProjectCleanup, self).setUp() + self.cmd = project_cleanup.ProjectCleanup(self.app, None) + + self.project_cleanup_mock = mock.Mock() + self.sdk_connect_as_project_mock = \ + mock.Mock(return_value=self.app.client_manager.sdk_connection) + self.app.client_manager.sdk_connection.project_cleanup = \ + self.project_cleanup_mock + self.app.client_manager.sdk_connection.identity.find_project = \ + mock.Mock(return_value=self.project) + self.app.client_manager.sdk_connection.connect_as_project = \ + self.sdk_connect_as_project_mock + + def test_project_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_project_cleanup_with_filters(self): + arglist = [ + '--project', self.project.id, + '--created-before', '2200-01-01', + '--updated-before', '2200-01-02' + ] + verifylist = [ + ('dry_run', False), + ('auth_project', False), + ('project', self.project.id), + ('created_before', '2200-01-01'), + ('updated_before', '2200-01-02') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = None + + with mock.patch('sys.stdin', StringIO('y')): + result = self.cmd.take_action(parsed_args) + + self.sdk_connect_as_project_mock.assert_called_with( + self.project) + filters = { + 'created_at': '2200-01-01', + 'updated_at': '2200-01-02' + } + + calls = [ + mock.call(dry_run=True, status_queue=mock.ANY, filters=filters), + mock.call(dry_run=False, status_queue=mock.ANY, filters=filters) + ] + self.project_cleanup_mock.assert_has_calls(calls) + + self.assertIsNone(result) + + def test_project_cleanup_with_project(self): + arglist = [ + '--project', self.project.id, + ] + verifylist = [ + ('dry_run', False), + ('auth_project', False), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = None + + with mock.patch('sys.stdin', StringIO('y')): + result = self.cmd.take_action(parsed_args) + + self.sdk_connect_as_project_mock.assert_called_with( + self.project) + calls = [ + mock.call(dry_run=True, status_queue=mock.ANY, filters={}), + mock.call(dry_run=False, status_queue=mock.ANY, filters={}) + ] + self.project_cleanup_mock.assert_has_calls(calls) + + self.assertIsNone(result) + + def test_project_cleanup_with_project_abort(self): + arglist = [ + '--project', self.project.id, + ] + verifylist = [ + ('dry_run', False), + ('auth_project', False), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = None + + with mock.patch('sys.stdin', StringIO('n')): + result = self.cmd.take_action(parsed_args) + + self.sdk_connect_as_project_mock.assert_called_with( + self.project) + calls = [ + mock.call(dry_run=True, status_queue=mock.ANY, filters={}), + ] + self.project_cleanup_mock.assert_has_calls(calls) + + self.assertIsNone(result) + + def test_project_cleanup_with_dry_run(self): + arglist = [ + '--dry-run', + '--project', self.project.id, + ] + verifylist = [ + ('dry_run', True), + ('auth_project', False), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = None + + result = self.cmd.take_action(parsed_args) + + self.sdk_connect_as_project_mock.assert_called_with( + self.project) + self.project_cleanup_mock.assert_called_once_with( + dry_run=True, status_queue=mock.ANY, filters={}) + + self.assertIsNone(result) + + def test_project_cleanup_with_auth_project(self): + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.project_id = self.project.id + arglist = [ + '--auth-project', + ] + verifylist = [ + ('dry_run', False), + ('auth_project', True), + ('project', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = None + + with mock.patch('sys.stdin', StringIO('y')): + result = self.cmd.take_action(parsed_args) + + self.sdk_connect_as_project_mock.assert_not_called() + calls = [ + mock.call(dry_run=True, status_queue=mock.ANY, filters={}), + mock.call(dry_run=False, status_queue=mock.ANY, filters={}) + ] + self.project_cleanup_mock.assert_has_calls(calls) + + self.assertIsNone(result) diff --git a/releasenotes/notes/add-project-cleanup-beb08c9df3c95b24.yaml b/releasenotes/notes/add-project-cleanup-beb08c9df3c95b24.yaml new file mode 100644 index 0000000000..58d4223d2b --- /dev/null +++ b/releasenotes/notes/add-project-cleanup-beb08c9df3c95b24.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for project cleanup based on the OpenStackSDK with + create/update time filters. In the long run this will replace + `openstack project purge` command. diff --git a/setup.cfg b/setup.cfg index 48384897a0..9deb0f5313 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,7 @@ openstack.common = extension_list = openstackclient.common.extension:ListExtension extension_show = openstackclient.common.extension:ShowExtension limits_show = openstackclient.common.limits:ShowLimits + project_cleanup = openstackclient.common.project_cleanup:ProjectCleanup project_purge = openstackclient.common.project_purge:ProjectPurge quota_list = openstackclient.common.quota:ListQuota quota_set = openstackclient.common.quota:SetQuota From e8509d81ee0b541033411e744f633e769073530b Mon Sep 17 00:00:00 2001 From: Miguel Lavalle Date: Wed, 10 Feb 2021 17:40:39 -0600 Subject: [PATCH 0374/1120] Add 'address_group' type support to rbac commands Depends-On: https://review.opendev.org/c/openstack/neutron/+/772460 Change-Id: Icd5e96d180364b979d1e93fcb39f9133a41a06e5 --- openstackclient/network/v2/network_rbac.py | 22 ++++++++++++------- .../unit/network/v2/test_network_rbac.py | 6 ++++- ...ac-add-address-group-f9bb83238b5a7c1f.yaml | 4 ++++ 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/rbac-add-address-group-f9bb83238b5a7c1f.yaml diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index b88ef019e0..4984e89d56 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -60,6 +60,10 @@ def _get_attrs(client_manager, parsed_args): object_id = network_client.find_subnet_pool( parsed_args.rbac_object, ignore_missing=False).id + if parsed_args.type == 'address_group': + object_id = network_client.find_address_group( + parsed_args.rbac_object, + ignore_missing=False).id attrs['object_id'] = object_id @@ -100,11 +104,12 @@ def get_parser(self, prog_name): '--type', metavar="", required=True, - choices=['address_scope', 'security_group', 'subnetpool', - 'qos_policy', 'network'], + choices=['address_group', 'address_scope', 'security_group', + 'subnetpool', 'qos_policy', 'network'], help=_('Type of the object that RBAC policy ' - 'affects ("address_scope", "security_group", "subnetpool",' - ' "qos_policy" or "network")') + 'affects ("address_group", "address_scope", ' + '"security_group", "subnetpool", "qos_policy" or ' + '"network")') ) parser.add_argument( '--action', @@ -193,11 +198,12 @@ def get_parser(self, prog_name): parser.add_argument( '--type', metavar='', - choices=['address_scope', 'security_group', 'subnetpool', - 'qos_policy', 'network'], + choices=['address_group', 'address_scope', 'security_group', + 'subnetpool', 'qos_policy', 'network'], help=_('List network RBAC policies according to ' - 'given object type ("address_scope", "security_group", ' - '"subnetpool", "qos_policy" or "network")') + 'given object type ("address_group", "address_scope", ' + '"security_group", "subnetpool", "qos_policy" or ' + '"network")') ) parser.add_argument( '--action', diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index d7c71ea7d0..08be64c516 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -42,6 +42,7 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): sg_object = network_fakes.FakeNetworkSecGroup.create_one_security_group() as_object = network_fakes.FakeAddressScope.create_one_address_scope() snp_object = network_fakes.FakeSubnetPool.create_one_subnet_pool() + ag_object = network_fakes.FakeAddressGroup.create_one_address_group() project = identity_fakes_v3.FakeProject.create_one_project() rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac( attrs={'tenant_id': project.id, @@ -85,6 +86,8 @@ def setUp(self): return_value=self.as_object) self.network.find_subnet_pool = mock.Mock( return_value=self.snp_object) + self.network.find_address_group = mock.Mock( + return_value=self.ag_object) self.projects_mock.get.return_value = self.project def test_network_rbac_create_no_type(self): @@ -236,7 +239,8 @@ def test_network_rbac_create_all_options(self): ('qos_policy', "qos_object"), ('security_group', "sg_object"), ('subnetpool', "snp_object"), - ('address_scope', "as_object") + ('address_scope', "as_object"), + ('address_group', "ag_object") ) @ddt.unpack def test_network_rbac_create_object(self, obj_type, obj_fake_attr): diff --git a/releasenotes/notes/rbac-add-address-group-f9bb83238b5a7c1f.yaml b/releasenotes/notes/rbac-add-address-group-f9bb83238b5a7c1f.yaml new file mode 100644 index 0000000000..d25da2f005 --- /dev/null +++ b/releasenotes/notes/rbac-add-address-group-f9bb83238b5a7c1f.yaml @@ -0,0 +1,4 @@ +features: + - | + Add ``address_group`` as a valid ``--type`` value for the + ``network rbac create`` and ``network rbac list`` commands. From 16c72f8642c24e4e2d8af93698a84aced54be97a Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Tue, 23 Feb 2021 18:58:24 -0500 Subject: [PATCH 0375/1120] Add --name to port list The neutron API supports filtering ports by name, but the CLI was missing support for it like it does for other networking resources. Change-Id: I4ff339e18656013218a26f045b205cb7a02dd2fb Story: #2008654 --- openstackclient/network/v2/port.py | 7 +++++++ .../tests/unit/network/v2/test_port.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index dfdb604d55..6885e147e0 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -600,6 +600,11 @@ def get_parser(self, prog_name): metavar='', help=_("List ports according to their project (name or ID)") ) + parser.add_argument( + '--name', + metavar='', + help=_("List ports according to their name") + ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--fixed-ip', @@ -667,6 +672,8 @@ def take_action(self, parsed_args): ).id filters['tenant_id'] = project_id filters['project_id'] = project_id + if parsed_args.name: + filters['name'] = parsed_args.name if parsed_args.fixed_ip: filters['fixed_ips'] = _prepare_filter_fixed_ips( self.app.client_manager, parsed_args) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index c8bced71c3..8c5158d7c8 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -1250,6 +1250,26 @@ def test_port_list_project_domain(self): self.assertEqual(self.columns, columns) self.assertItemsEqual(self.data, list(data)) + def test_port_list_name(self): + test_name = "fakename" + arglist = [ + '--name', test_name, + ] + verifylist = [ + ('name', test_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = { + 'name': test_name, + 'fields': LIST_FIELDS_TO_RETRIEVE, + } + + self.network.ports.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + def test_list_with_tag_options(self): arglist = [ '--tags', 'red,blue', From 7c1d6f769c4f0d2afe61410fefd8bc8f26a22980 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 4 Mar 2021 18:34:00 +0000 Subject: [PATCH 0376/1120] compute: Add functional tests for --block-device This mostly reuses the existing tests for '--block-device-mapping', which can hopefully be removed at some point in the future. This highlights two issues with the implementation of this option. Firstly, the 'boot_index' parameter is not required so don't mandate it. Secondly, and more significantly, we were defaulting the destination type for the 'image' source type to 'local'. Nova only allows you to attach a single image to local mapping [1], which means this default would only make sense if you were expecting users to use the '--block-device' option exclusively and omit the '--image' option. This is the *less common* case so this is a bad default. Default instead to a destination type of 'volume' like everything else, and require users specifying '--block-device' alone to pass 'destination_type=local' explicitly. [1] https://github.com/openstack/nova/blob/c8a6f8d2e/nova/block_device.py#L193-L206 Change-Id: I1718be965f57c3bbdb8a14f3cfac967dd4c55b4d Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 30 ++-- .../functional/compute/v2/test_server.py | 164 ++++++++++++++++-- .../tests/unit/compute/v2/test_server.py | 28 +-- 3 files changed, 169 insertions(+), 53 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 9838ed5480..b2ae93c807 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -832,13 +832,12 @@ def get_parser(self, prog_name): action=parseractions.MultiKeyValueAction, dest='block_devices', default=[], - required_keys=[ - 'boot_index', - ], + required_keys=[], optional_keys=[ 'uuid', 'source_type', 'destination_type', - 'disk_bus', 'device_type', 'device_name', 'guest_format', - 'volume_size', 'volume_type', 'delete_on_termination', 'tag', + 'disk_bus', 'device_type', 'device_name', 'volume_size', + 'guest_format', 'boot_index', 'delete_on_termination', 'tag', + 'volume_type', ], help=_( 'Create a block device on the server.\n' @@ -1336,14 +1335,15 @@ def _match_image(image_api, wanted_properties): block_device_mapping_v2.append(mapping) for mapping in parsed_args.block_devices: - try: - mapping['boot_index'] = int(mapping['boot_index']) - except ValueError: - msg = _( - 'The boot_index key of --block-device should be an ' - 'integer' - ) - raise exceptions.CommandError(msg) + if 'boot_index' in mapping: + try: + mapping['boot_index'] = int(mapping['boot_index']) + except ValueError: + msg = _( + 'The boot_index key of --block-device should be an ' + 'integer' + ) + raise exceptions.CommandError(msg) if 'tag' in mapping and ( compute_client.api_version < api_versions.APIVersion('2.42') @@ -1383,9 +1383,9 @@ def _match_image(image_api, wanted_properties): ) raise exceptions.CommandError(msg) else: - if mapping['source_type'] in ('image', 'blank'): + if mapping['source_type'] in ('blank',): mapping['destination_type'] = 'local' - else: # volume, snapshot + else: # volume, image, snapshot mapping['destination_type'] = 'volume' if 'delete_on_termination' in mapping: diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index bad3f93d47..9cf2fc7f0f 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -574,7 +574,90 @@ def test_server_boot_from_volume(self): cmd_output['status'], ) - def test_server_boot_with_bdm_snapshot(self): + def _test_server_boot_with_bdm_volume(self, use_legacy): + """Test server create from volume, server delete""" + # get volume status wait function + volume_wait_for = volume_common.BaseVolumeTests.wait_for_status + + # create source empty volume + volume_name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + volume_id = cmd_output["id"] + self.assertIsNotNone(volume_id) + self.addCleanup(self.openstack, 'volume delete ' + volume_name) + self.assertEqual(volume_name, cmd_output['name']) + volume_wait_for("volume", volume_name, "available") + + if use_legacy: + bdm_arg = f'--block-device-mapping vdb={volume_name}' + else: + bdm_arg = ( + f'--block-device ' + f'device_name=vdb,source_type=volume,boot_index=1,' + f'uuid={volume_id}' + ) + + # create server + server_name = uuid.uuid4().hex + server = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + bdm_arg + ' ' + + self.network_arg + ' ' + + '--wait ' + + server_name + )) + self.assertIsNotNone(server["id"]) + self.addCleanup(self.openstack, 'server delete --wait ' + server_name) + self.assertEqual( + server_name, + server['name'], + ) + + # check server volumes_attached, format is + # {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",} + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + server_name + )) + volumes_attached = cmd_output['volumes_attached'] + self.assertIsNotNone(volumes_attached) + + # check volumes + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + volume_name + )) + attachments = cmd_output['attachments'] + self.assertEqual( + 1, + len(attachments), + ) + self.assertEqual( + server['id'], + attachments[0]['server_id'], + ) + self.assertEqual( + "in-use", + cmd_output['status'], + ) + + def test_server_boot_with_bdm_volume(self): + """Test server create from image with bdm volume, server delete""" + self._test_server_boot_with_bdm_volume(use_legacy=False) + + # TODO(stephenfin): Remove when we drop support for the + # '--block-device-mapping' option + def test_server_boot_with_bdm_volume_legacy(self): + """Test server create from image with bdm volume, server delete""" + self._test_server_boot_with_bdm_volume(use_legacy=True) + + def _test_server_boot_with_bdm_snapshot(self, use_legacy): """Test server create from image with bdm snapshot, server delete""" # get volume status wait function volume_wait_for = volume_common.BaseVolumeTests.wait_for_status @@ -588,12 +671,8 @@ def test_server_boot_with_bdm_snapshot(self): empty_volume_name )) self.assertIsNotNone(cmd_output["id"]) - self.addCleanup(self.openstack, - 'volume delete ' + empty_volume_name) - self.assertEqual( - empty_volume_name, - cmd_output['name'], - ) + self.addCleanup(self.openstack, 'volume delete ' + empty_volume_name) + self.assertEqual(empty_volume_name, cmd_output['name']) volume_wait_for("volume", empty_volume_name, "available") # create snapshot of source empty volume @@ -603,7 +682,8 @@ def test_server_boot_with_bdm_snapshot(self): '--volume ' + empty_volume_name + ' ' + empty_snapshot_name )) - self.assertIsNotNone(cmd_output["id"]) + empty_snapshot_id = cmd_output["id"] + self.assertIsNotNone(empty_snapshot_id) # Deleting volume snapshot take time, so we need to wait until the # snapshot goes. Entries registered by self.addCleanup will be called # in the reverse order, so we need to register wait_for_delete first. @@ -617,14 +697,26 @@ def test_server_boot_with_bdm_snapshot(self): ) volume_wait_for("volume snapshot", empty_snapshot_name, "available") + if use_legacy: + bdm_arg = ( + f'--block-device-mapping ' + f'vdb={empty_snapshot_name}:snapshot:1:true' + ) + else: + bdm_arg = ( + f'--block-device ' + f'device_name=vdb,uuid={empty_snapshot_id},' + f'source_type=snapshot,volume_size=1,' + f'delete_on_termination=true,boot_index=1' + ) + # create server with bdm snapshot server_name = uuid.uuid4().hex server = json.loads(self.openstack( 'server create -f json ' + '--flavor ' + self.flavor_name + ' ' + '--image ' + self.image_name + ' ' + - '--block-device-mapping ' - 'vdb=' + empty_snapshot_name + ':snapshot:1:true ' + + bdm_arg + ' ' + self.network_arg + ' ' + '--wait ' + server_name @@ -681,7 +773,17 @@ def test_server_boot_with_bdm_snapshot(self): # the attached volume had been deleted pass - def test_server_boot_with_bdm_image(self): + def test_server_boot_with_bdm_snapshot(self): + """Test server create from image with bdm snapshot, server delete""" + self._test_server_boot_with_bdm_snapshot(use_legacy=False) + + # TODO(stephenfin): Remove when we drop support for the + # '--block-device-mapping' option + def test_server_boot_with_bdm_snapshot_legacy(self): + """Test server create from image with bdm snapshot, server delete""" + self._test_server_boot_with_bdm_snapshot(use_legacy=True) + + def _test_server_boot_with_bdm_image(self, use_legacy): # Tests creating a server where the root disk is backed by the given # --image but a --block-device-mapping with type=image is provided so # that the compute service creates a volume from that image and @@ -689,6 +791,32 @@ def test_server_boot_with_bdm_image(self): # marked as delete_on_termination=True so it will be automatically # deleted when the server is deleted. + if use_legacy: + # This means create a 1GB volume from the specified image, attach + # it to the server at /dev/vdb and delete the volume when the + # server is deleted. + bdm_arg = ( + f'--block-device-mapping ' + f'vdb={self.image_name}:image:1:true ' + ) + else: + # get image ID + cmd_output = json.loads(self.openstack( + 'image show -f json ' + + self.image_name + )) + image_id = cmd_output['id'] + + # This means create a 1GB volume from the specified image, attach + # it to the server at /dev/vdb and delete the volume when the + # server is deleted. + bdm_arg = ( + f'--block-device ' + f'device_name=vdb,uuid={image_id},' + f'source_type=image,volume_size=1,' + f'delete_on_termination=true,boot_index=1' + ) + # create server with bdm type=image # NOTE(mriedem): This test is a bit unrealistic in that specifying the # same image in the block device as the --image option does not really @@ -700,11 +828,7 @@ def test_server_boot_with_bdm_image(self): 'server create -f json ' + '--flavor ' + self.flavor_name + ' ' + '--image ' + self.image_name + ' ' + - '--block-device-mapping ' - # This means create a 1GB volume from the specified image, attach - # it to the server at /dev/vdb and delete the volume when the - # server is deleted. - 'vdb=' + self.image_name + ':image:1:true ' + + bdm_arg + ' ' + self.network_arg + ' ' + '--wait ' + server_name @@ -768,6 +892,14 @@ def test_server_boot_with_bdm_image(self): # the attached volume had been deleted pass + def test_server_boot_with_bdm_image(self): + self._test_server_boot_with_bdm_image(use_legacy=False) + + # TODO(stephenfin): Remove when we drop support for the + # '--block-device-mapping' option + def test_server_boot_with_bdm_image_legacy(self): + self._test_server_boot_with_bdm_image(use_legacy=True) + def test_boot_from_volume(self): # Tests creating a server using --image and --boot-from-volume where # the compute service will create a root volume of the specified size diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index ced5d458c2..9666c5fdba 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2029,7 +2029,7 @@ def test_server_create_with_snapshot(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device(self): - block_device = f'uuid={self.volume.id},source_type=volume,boot_index=1' + block_device = f'uuid={self.volume.id},source_type=volume' arglist = [ '--image', 'image1', '--flavor', self.flavor.id, @@ -2043,7 +2043,6 @@ def test_server_create_with_block_device(self): { 'uuid': self.volume.id, 'source_type': 'volume', - 'boot_index': '1', }, ]), ('server_name', self.new_server.name), @@ -2069,7 +2068,6 @@ def test_server_create_with_block_device(self): 'uuid': self.volume.id, 'source_type': 'volume', 'destination_type': 'volume', - 'boot_index': 1, }], 'nics': [], 'scheduler_hints': {}, @@ -2171,20 +2169,6 @@ def test_server_create_with_block_device_full(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) - def test_server_create_with_block_device_no_boot_index(self): - block_device = \ - f'uuid={self.volume.name},source_type=volume' - arglist = [ - '--image', 'image1', - '--flavor', self.flavor.id, - '--block-device', block_device, - self.new_server.name, - ] - self.assertRaises( - argparse.ArgumentTypeError, - self.check_parser, - self.cmd, arglist, []) - def test_server_create_with_block_device_invalid_boot_index(self): block_device = \ f'uuid={self.volume.name},source_type=volume,boot_index=foo' @@ -2201,7 +2185,7 @@ def test_server_create_with_block_device_invalid_boot_index(self): self.assertIn('The boot_index key of --block-device ', str(ex)) def test_server_create_with_block_device_invalid_source_type(self): - block_device = f'uuid={self.volume.name},source_type=foo,boot_index=1' + block_device = f'uuid={self.volume.name},source_type=foo' arglist = [ '--image', 'image1', '--flavor', self.flavor.id, @@ -2216,7 +2200,7 @@ def test_server_create_with_block_device_invalid_source_type(self): def test_server_create_with_block_device_invalid_destination_type(self): block_device = \ - f'uuid={self.volume.name},destination_type=foo,boot_index=1' + f'uuid={self.volume.name},destination_type=foo' arglist = [ '--image', 'image1', '--flavor', self.flavor.id, @@ -2231,7 +2215,7 @@ def test_server_create_with_block_device_invalid_destination_type(self): def test_server_create_with_block_device_invalid_shutdown(self): block_device = \ - f'uuid={self.volume.name},delete_on_termination=foo,boot_index=1' + f'uuid={self.volume.name},delete_on_termination=foo' arglist = [ '--image', 'image1', '--flavor', self.flavor.id, @@ -2250,7 +2234,7 @@ def test_server_create_with_block_device_tag_pre_v242(self): '2.41') block_device = \ - f'uuid={self.volume.name},tag=foo,boot_index=1' + f'uuid={self.volume.name},tag=foo' arglist = [ '--image', 'image1', '--flavor', self.flavor.id, @@ -2269,7 +2253,7 @@ def test_server_create_with_block_device_volume_type_pre_v267(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.66') - block_device = f'uuid={self.volume.name},volume_type=foo,boot_index=1' + block_device = f'uuid={self.volume.name},volume_type=foo' arglist = [ '--image', 'image1', '--flavor', self.flavor.id, From d3bd0146ae64ebc3a7c5d7a3a2fc90efe4071495 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 5 Mar 2021 12:39:37 +0000 Subject: [PATCH 0377/1120] compute: Add support for loading BDMs from files The syntax of the '--block-device' parameter is complex and easily screwed up. Allow users to load a block device config from a file. For example: $ openstack server create ... --block-device file:///tmp/bdm.json ... This should alleviate the pain that is BDMv2 somewhat. No functional tests are provided since we already have tests for the CSV style of passing parameters and the unit tests show that the net result is the same. Change-Id: I3e3299bbdbbb343863b4c14fb4d9196ff3e1698d Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 80 +++++++++++++++--- .../tests/unit/compute/v2/test_server.py | 83 +++++++++++++++++++ 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b2ae93c807..cd6e2c9165 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -18,8 +18,10 @@ import argparse import getpass import io +import json import logging import os +import urllib.parse from cliff import columns as cliff_columns import iso8601 @@ -681,7 +683,7 @@ def __call__(self, parser, namespace, values, option_string=None): class BDMLegacyAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): - # Make sure we have an empty dict rather than None + # Make sure we have an empty list rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) @@ -723,6 +725,68 @@ def __call__(self, parser, namespace, values, option_string=None): getattr(namespace, self.dest).append(mapping) +class BDMAction(parseractions.MultiKeyValueAction): + + def __init__(self, option_strings, dest, **kwargs): + required_keys = [] + optional_keys = [ + 'uuid', 'source_type', 'destination_type', + 'disk_bus', 'device_type', 'device_name', 'volume_size', + 'guest_format', 'boot_index', 'delete_on_termination', 'tag', + 'volume_type', + ] + super().__init__( + option_strings, dest, required_keys=required_keys, + optional_keys=optional_keys, **kwargs, + ) + + # TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b + # or similar lands in a release + def validate_keys(self, keys): + """Validate the provided keys. + + :param keys: A list of keys to validate. + """ + valid_keys = self.required_keys | self.optional_keys + invalid_keys = [k for k in keys if k not in valid_keys] + if invalid_keys: + msg = _( + "Invalid keys %(invalid_keys)s specified.\n" + "Valid keys are: %(valid_keys)s" + ) + raise argparse.ArgumentTypeError(msg % { + 'invalid_keys': ', '.join(invalid_keys), + 'valid_keys': ', '.join(valid_keys), + }) + + missing_keys = [k for k in self.required_keys if k not in keys] + if missing_keys: + msg = _( + "Missing required keys %(missing_keys)s.\n" + "Required keys are: %(required_keys)s" + ) + raise argparse.ArgumentTypeError(msg % { + 'missing_keys': ', '.join(missing_keys), + 'required_keys': ', '.join(self.required_keys), + }) + + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + + if values.startswith('file://'): + path = urllib.parse.urlparse(values).path + with open(path) as fh: + data = json.load(fh) + + # Validate the keys - other validation is left to later + self.validate_keys(list(data)) + + getattr(namespace, self.dest, []).append(data) + else: + super().__call__(parser, namespace, values, option_string) + + class CreateServer(command.ShowOne): _description = _("Create a new server") @@ -829,19 +893,15 @@ def get_parser(self, prog_name): parser.add_argument( '--block-device', metavar='', - action=parseractions.MultiKeyValueAction, + action=BDMAction, dest='block_devices', default=[], - required_keys=[], - optional_keys=[ - 'uuid', 'source_type', 'destination_type', - 'disk_bus', 'device_type', 'device_name', 'volume_size', - 'guest_format', 'boot_index', 'delete_on_termination', 'tag', - 'volume_type', - ], help=_( 'Create a block device on the server.\n' - 'Block device in the format:\n' + 'Either a URI-style path (\'file:\\\\{path}\') to a JSON file ' + 'or a CSV-serialized string describing the block device ' + 'mapping.\n' + 'The following keys are accepted:\n' 'uuid=: UUID of the volume, snapshot or ID ' '(required if using source image, snapshot or volume),\n' 'source_type=: source type ' diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9666c5fdba..775ad0d911 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -16,6 +16,8 @@ import collections import copy import getpass +import json +import tempfile from unittest import mock from unittest.mock import call @@ -2169,6 +2171,87 @@ def test_server_create_with_block_device_full(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_block_device_from_file(self): + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.67') + + block_device = { + 'uuid': self.volume.id, + 'source_type': 'volume', + 'destination_type': 'volume', + 'disk_bus': 'ide', + 'device_type': 'disk', + 'device_name': 'sdb', + 'guest_format': 'ext4', + 'volume_size': 64, + 'volume_type': 'foo', + 'boot_index': 1, + 'delete_on_termination': True, + 'tag': 'foo', + } + + with tempfile.NamedTemporaryFile(mode='w+') as fp: + json.dump(block_device, fp=fp) + fp.flush() + + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device', f'file://{fp.name}', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', self.flavor.id), + ('block_devices', [block_device]), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'meta': None, + 'files': {}, + 'reservation_id': None, + 'min_count': 1, + 'max_count': 1, + 'security_groups': [], + 'userdata': None, + 'key_name': None, + 'availability_zone': None, + 'admin_pass': None, + 'block_device_mapping_v2': [{ + 'uuid': self.volume.id, + 'source_type': 'volume', + 'destination_type': 'volume', + 'disk_bus': 'ide', + 'device_name': 'sdb', + 'volume_size': 64, + 'guest_format': 'ext4', + 'boot_index': 1, + 'device_type': 'disk', + 'delete_on_termination': True, + 'tag': 'foo', + 'volume_type': 'foo', + }], + 'nics': 'auto', + 'scheduler_hints': {}, + 'config_drive': None, + } + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + def test_server_create_with_block_device_invalid_boot_index(self): block_device = \ f'uuid={self.volume.name},source_type=volume,boot_index=foo' From ed731d6cd9254eae047d14ccd55132229e13e7d1 Mon Sep 17 00:00:00 2001 From: Bharat Kunwar Date: Fri, 5 Mar 2021 12:52:26 +0000 Subject: [PATCH 0378/1120] network: Add missing subnet unset --gateway Story: 2008695 Task: 42003 Change-Id: I9486a09531b11f27a9ff0d68fd4ad8c68a65cccf --- openstackclient/network/v2/subnet.py | 7 +++++++ openstackclient/tests/unit/network/v2/test_subnet.py | 4 ++++ .../notes/subnet-unset-gateway-20239d5910e10778.yaml | 4 ++++ 3 files changed, 15 insertions(+) create mode 100644 releasenotes/notes/subnet-unset-gateway-20239d5910e10778.yaml diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index f87f7abe1b..b98f86412c 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -672,6 +672,11 @@ def get_parser(self, prog_name): 'subnet e.g.: start=192.168.199.2,end=192.168.199.254 ' '(repeat option to unset multiple allocation pools)') ) + parser.add_argument( + '--gateway', + action='store_true', + help=_("Remove gateway IP from this subnet") + ) parser.add_argument( '--dns-nameserver', metavar='', @@ -715,6 +720,8 @@ def take_action(self, parsed_args): obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) attrs = {} + if parsed_args.gateway: + attrs['gateway_ip'] = None if parsed_args.dns_nameservers: attrs['dns_nameservers'] = copy.deepcopy(obj.dns_nameservers) _update_arguments(attrs['dns_nameservers'], diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 1b4bfdad2f..6085cda8a1 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -1264,6 +1264,7 @@ def setUp(self): 'end': '8.8.8.170'}], 'service_types': ['network:router_gateway', 'network:floatingip_agent_gateway'], + 'gateway_ip': 'fe80::a00a:0:c0de:0:1', 'tags': ['green', 'red'], }) self.network.find_subnet = mock.Mock(return_value=self._testsubnet) self.network.update_subnet = mock.Mock(return_value=None) @@ -1277,6 +1278,7 @@ def test_unset_subnet_params(self): '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', '--service-type', 'network:router_gateway', + '--gateway', self._testsubnet.name, ] verifylist = [ @@ -1286,6 +1288,7 @@ def test_unset_subnet_params(self): ('allocation_pools', [{ 'start': '8.8.8.100', 'end': '8.8.8.150'}]), ('service_types', ['network:router_gateway']), + ('gateway', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1297,6 +1300,7 @@ def test_unset_subnet_params(self): "destination": "10.20.20.0/24", "nexthop": "10.20.20.1"}], 'allocation_pools': [{'start': '8.8.8.160', 'end': '8.8.8.170'}], 'service_types': ['network:floatingip_agent_gateway'], + 'gateway_ip': None, } self.network.update_subnet.assert_called_once_with( self._testsubnet, **attrs) diff --git a/releasenotes/notes/subnet-unset-gateway-20239d5910e10778.yaml b/releasenotes/notes/subnet-unset-gateway-20239d5910e10778.yaml new file mode 100644 index 0000000000..55be3e4a34 --- /dev/null +++ b/releasenotes/notes/subnet-unset-gateway-20239d5910e10778.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Add missing ``openstack subnet unset --gateway ``. From 46bd6ef91f297aa84fb761b0e7fd983668da56de Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Tue, 9 Mar 2021 14:49:38 +0100 Subject: [PATCH 0379/1120] Update volume create documentation Change I94aa7a9824e44f9585ffb45e5e7637b9588539b4 removed these options. Change-Id: I43d84b5532ae6570e1486867c03b8ebec81e38e4 --- doc/source/cli/command-objects/volume.rst | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/doc/source/cli/command-objects/volume.rst b/doc/source/cli/command-objects/volume.rst index fc6188c0a8..ac414110a0 100644 --- a/doc/source/cli/command-objects/volume.rst +++ b/doc/source/cli/command-objects/volume.rst @@ -17,13 +17,10 @@ Create new volume [--type ] [--image | --snapshot | --source ] [--description ] - [--user ] - [--project ] [--availability-zone ] [--consistency-group ] [--property [...] ] [--hint [...] ] - [--multi-attach] [--bootable | --non-bootable] [--read-only | --read-write] @@ -58,14 +55,6 @@ Create new volume Volume description -.. option:: --user - - Specify an alternate user (name or ID) - -.. option:: --project - - Specify an alternate project (name or ID) - .. option:: --availability-zone Create volume in ```` @@ -83,10 +72,6 @@ Create new volume Arbitrary scheduler hint key-value pairs to help boot an instance (repeat option to set multiple hints) -.. option:: --multi-attach - - Allow volume to be attached more than once (default to False) - .. option:: --bootable Mark volume as bootable @@ -108,10 +93,6 @@ Create new volume Volume name -The :option:`--project` and :option:`--user` options are typically only -useful for admin users, but may be allowed for other users depending on -the policy of the cloud and the roles granted to the user. - volume delete ------------- From 2ccf7727a6c06214956b7067e7932015bcdfb5a5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Mar 2021 15:53:39 +0000 Subject: [PATCH 0380/1120] compute: Remove 'file://' prefix from '--block-device' There are a couple of other (networking-related) options which accept paths, none of which insist on a URI-style path. Let's just drop this bit of complexity before we release the feature. Change-Id: Ia7f781d82f3f4695b49b55a39abbb6e582cd879c Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 13 +++++-------- .../tests/unit/compute/v2/test_server.py | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index cd6e2c9165..b81d2a181c 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -21,7 +21,6 @@ import json import logging import os -import urllib.parse from cliff import columns as cliff_columns import iso8601 @@ -774,9 +773,8 @@ def __call__(self, parser, namespace, values, option_string=None): if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) - if values.startswith('file://'): - path = urllib.parse.urlparse(values).path - with open(path) as fh: + if os.path.exists(values): + with open(values) as fh: data = json.load(fh) # Validate the keys - other validation is left to later @@ -898,10 +896,9 @@ def get_parser(self, prog_name): default=[], help=_( 'Create a block device on the server.\n' - 'Either a URI-style path (\'file:\\\\{path}\') to a JSON file ' - 'or a CSV-serialized string describing the block device ' - 'mapping.\n' - 'The following keys are accepted:\n' + 'Either a path to a JSON file or a CSV-serialized string ' + 'describing the block device mapping.\n' + 'The following keys are accepted for both:\n' 'uuid=: UUID of the volume, snapshot or ID ' '(required if using source image, snapshot or volume),\n' 'source_type=: source type ' diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 775ad0d911..c6dff5a8a2 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -2197,7 +2197,7 @@ def test_server_create_with_block_device_from_file(self): arglist = [ '--image', 'image1', '--flavor', self.flavor.id, - '--block-device', f'file://{fp.name}', + '--block-device', fp.name, self.new_server.name, ] verifylist = [ From 87e682867886bddd10f1dc9c4a174603414e0480 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Mar 2021 16:20:15 +0000 Subject: [PATCH 0381/1120] Add pre-commit This is helpful to automate code style checks at runtime. We include documentation on how to run this as well as a general overview of style guidelines in OSC. Change-Id: I2dc5a0f760ce53269ae25677560b2611cc6bfd91 Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 29 +++++++++++++++ doc/source/contributor/developing.rst | 53 ++++++++++++++++++++------- 2 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..f91d10b7be --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +--- +default_language_version: + # force all unspecified python hooks to run python3 + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: mixed-line-ending + args: ['--fix', 'lf'] + exclude: '.*\.(svg)$' + - id: check-byte-order-marker + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: debug-statements + - id: check-yaml + files: .*\.(yaml|yml)$ + - repo: local + hooks: + - id: flake8 + name: flake8 + additional_dependencies: + - hacking>=2.0.0 + - flake8-import-order>=0.13 + language: python + entry: flake8 + files: '^.*\.py$' + exclude: '^(doc|releasenotes|tools)/.*$' diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index 9142edb86c..a70b33268a 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -6,16 +6,18 @@ Communication ------------- IRC Channel -=========== +~~~~~~~~~~~ + The OpenStackClient team doesn't have regular meetings so if you have questions or anything you want to discuss, come to our channel: #openstack-sdks + Testing ------- Tox prerequisites and installation -================================== +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Install the prerequisites for Tox: @@ -23,23 +25,22 @@ Install the prerequisites for Tox: .. code-block:: bash - $ apt-get install gcc gettext python-dev libxml2-dev libxslt1-dev \ + $ apt-get install gcc gettext python3-dev libxml2-dev libxslt1-dev \ zlib1g-dev You may need to use pip install for some packages. - * On RHEL or CentOS including Fedora: .. code-block:: bash - $ yum install gcc python-devel libxml2-devel libxslt-devel + $ yum install gcc python3-devel libxml2-devel libxslt-devel * On openSUSE or SUSE linux Enterprise: .. code-block:: bash - $ zypper install gcc python-devel libxml2-devel libxslt-devel + $ zypper install gcc python3-devel libxml2-devel libxslt-devel Install python-tox: @@ -59,7 +60,7 @@ To run the full suite of tests maintained within OpenStackClient. virtualenvs. You can later use the ``-r`` option with ``tox`` to rebuild your virtualenv in a similar manner. -To run tests for one or more specific test environments(for example, the most +To run tests for one or more specific test environments (for example, the most common configuration of the latest Python version and PEP-8), list the environments with the ``-e`` option, separated by spaces: @@ -70,7 +71,7 @@ environments with the ``-e`` option, separated by spaces: See ``tox.ini`` for the full list of available test environments. Running functional tests -======================== +~~~~~~~~~~~~~~~~~~~~~~~~ OpenStackClient also maintains a set of functional tests that are optimally designed to be run against OpenStack's gate. Optionally, a developer may @@ -90,7 +91,7 @@ To run a specific functional test: $ tox -e functional -- --regex functional.tests.compute.v2.test_server Running with PDB -================ +~~~~~~~~~~~~~~~~ Using PDB breakpoints with ``tox`` and ``testr`` normally does not work since the tests fail with a `BdbQuit` exception rather than stopping at the @@ -109,8 +110,32 @@ For reference, the `debug`_ ``tox`` environment implements the instructions .. _`debug`: https://wiki.openstack.org/wiki/Testr#Debugging_.28pdb.29_Tests -Building the Documentation --------------------------- +Coding Style +------------ + +OpenStackClient uses `flake8`__ along with `hacking`__, an OpenStack-specific +superset of ``flake8`` rules, to enforce coding style. This can be run manually +using ``tox``: + +.. code-block:: bash + + $ tox -e pep8 + +Alternatively, you can use the `pre-commit framework`__ to allow running of +some linters on each commit. This must be enabled locally to function: + +.. code-block:: bash + + $ pip install --user pre-commit + $ pre-commit install --allow-missing-config + +.. __: https://flake8.pycqa.org/en/latest/ +.. __: https://docs.openstack.org/hacking/latest/user/hacking.html +.. __: https://pre-commit.com/ + + +Documentation +------------- The documentation is generated with Sphinx using the ``tox`` command. To create HTML docs, run the commands: @@ -121,6 +146,7 @@ create HTML docs, run the commands: The resultant HTML will be in the ``doc/build/html`` directory. + Release Notes ------------- @@ -156,6 +182,7 @@ To run the commands and see results: At last, look at the generated release notes files in ``releasenotes/build/html`` in your browser. + Testing new code ---------------- @@ -174,7 +201,7 @@ or $ pip install -e . Standardize Import Format -========================= +~~~~~~~~~~~~~~~~~~~~~~~~~ More information about Import Format, see `Import Order Guide `__. @@ -193,7 +220,7 @@ The import order shows below: {{begin your code}} Example -~~~~~~~ +^^^^^^^ .. code-block:: python From 791bed6dd2272138b619e7c7ab26ca0692defff8 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 15 Mar 2021 23:29:53 +0900 Subject: [PATCH 0382/1120] Update the file paths mentioned in README.rst This change fixes the outdated file paths, which were renamed by commit 9599ffe65d9dcd4b3aa780d346eccd1e760890bf . Change-Id: I9ec4c49711a2fde24f5527086e495c86af9ef1ce --- doc/source/contributor/plugins.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/contributor/plugins.rst b/doc/source/contributor/plugins.rst index 067c1b9912..c2a08c5d63 100644 --- a/doc/source/contributor/plugins.rst +++ b/doc/source/contributor/plugins.rst @@ -226,15 +226,15 @@ Add the command checker to your CI Changes to python-openstackclient --------------------------------- -#. In ``doc/source/plugins.rst``, update the `Adoption` section to reflect the - status of the project. +#. In ``doc/source/contributor/plugins.rst``, update the `Adoption` section to + reflect the status of the project. -#. Update ``doc/source/commands.rst`` to include objects that are defined by - fooclient's new plugin. +#. Update ``doc/source/contributor/commands.rst`` to include objects that are + defined by fooclient's new plugin. -#. Update ``doc/source/plugin-commands.rst`` to include the entry point defined - in fooclient. We use `sphinxext`_ to automatically document commands that - are used. +#. Update ``doc/source/contributor/plugin-commands.rst`` to include the entry + point defined in fooclient. We use `sphinxext`_ to automatically document + commands that are used. #. Update ``test-requirements.txt`` to include fooclient. This is necessary to auto-document the commands in the previous step. From e4e9fb594d003ea6c3ec29aab0bccf72ffab6781 Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Wed, 3 Mar 2021 12:46:02 -0500 Subject: [PATCH 0383/1120] Add --subnet-pool to subnet list The neutron API supports filtering subnets by subnet pool id, but the CLI was missing support for it. Change-Id: Ic230c2c5cda8255d8f2c422880aeac81670b2df3 --- openstackclient/network/v2/subnet.py | 10 +++++ .../tests/unit/network/v2/test_subnet.py | 42 +++++++++++++++++++ ...st-subnet-by-pool-id-a642efc13d04fa08.yaml | 5 +++ 3 files changed, 57 insertions(+) create mode 100644 releasenotes/notes/list-subnet-by-pool-id-a642efc13d04fa08.yaml diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index f87f7abe1b..92a9e750c5 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -488,6 +488,12 @@ def get_parser(self, prog_name): "(in CIDR notation) in output " "e.g.: --subnet-range 10.10.0.0/16") ) + parser.add_argument( + '--subnet-pool', + metavar='', + help=_("List only subnets which belong to a given subnet pool " + "in output (Name or ID)") + ) _tag.add_tag_filtering_option_to_parser(parser, _('subnets')) return parser @@ -523,6 +529,10 @@ def take_action(self, parsed_args): filters['name'] = parsed_args.name if parsed_args.subnet_range: filters['cidr'] = parsed_args.subnet_range + if parsed_args.subnet_pool: + subnetpool_id = network_client.find_subnet_pool( + parsed_args.subnet_pool, ignore_missing=False).id + filters['subnetpool_id'] = subnetpool_id _tag.get_tag_filtering_args(parsed_args, filters) data = network_client.subnets(**filters) diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 1b4bfdad2f..06096f4b63 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -899,6 +899,48 @@ def test_subnet_list_subnet_range(self): self.assertEqual(self.columns, columns) self.assertItemsEqual(self.data, list(data)) + def test_subnet_list_subnetpool_by_name(self): + subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() + subnet = network_fakes.FakeSubnet.create_one_subnet( + {'subnetpool_id': subnet_pool.id}) + self.network.find_network = mock.Mock(return_value=subnet) + self.network.find_subnet_pool = mock.Mock(return_value=subnet_pool) + arglist = [ + '--subnet-pool', subnet_pool.name, + ] + verifylist = [ + ('subnet_pool', subnet_pool.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'subnetpool_id': subnet_pool.id} + + self.network.subnets.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_subnet_list_subnetpool_by_id(self): + subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() + subnet = network_fakes.FakeSubnet.create_one_subnet( + {'subnetpool_id': subnet_pool.id}) + self.network.find_network = mock.Mock(return_value=subnet) + self.network.find_subnet_pool = mock.Mock(return_value=subnet_pool) + arglist = [ + '--subnet-pool', subnet_pool.id, + ] + verifylist = [ + ('subnet_pool', subnet_pool.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'subnetpool_id': subnet_pool.id} + + self.network.subnets.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + def test_list_with_tag_options(self): arglist = [ '--tags', 'red,blue', diff --git a/releasenotes/notes/list-subnet-by-pool-id-a642efc13d04fa08.yaml b/releasenotes/notes/list-subnet-by-pool-id-a642efc13d04fa08.yaml new file mode 100644 index 0000000000..d784a9aadb --- /dev/null +++ b/releasenotes/notes/list-subnet-by-pool-id-a642efc13d04fa08.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--subnet-pool`` option to ``subnet list`` to filter + by subnets by subnet pool. From 6f821659795fdc82c694bb9fdddd5d3c61702e21 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Wed, 3 Mar 2021 20:20:00 +0000 Subject: [PATCH 0384/1120] network: Add support for vnic-type vdpa Extend 'port create' to support vinc-type vdpa as introduced by neutron in [1]. [1] https://review.opendev.org/c/openstack/neutron/+/760047 Change-Id: I635c5269f4e8fc55f234c98e85fced87b39fce81 --- openstackclient/network/v2/port.py | 14 +++++++++----- ...ort-create-vnic-type-vdpa-fc02516cfb919941.yaml | 4 ++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/network-port-create-vnic-type-vdpa-fc02516cfb919941.yaml diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 6885e147e0..4feffc1df4 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -255,11 +255,15 @@ def _add_updatable_args(parser): parser.add_argument( '--vnic-type', metavar='', - choices=['direct', 'direct-physical', 'macvtap', - 'normal', 'baremetal', 'virtio-forwarder'], - help=_("VNIC type for this port (direct | direct-physical | " - "macvtap | normal | baremetal | virtio-forwarder, " - "default: normal)") + choices=( + 'direct', 'direct-physical', 'macvtap', + 'normal', 'baremetal', 'virtio-forwarder', 'vdpa' + ), + help=_( + "VNIC type for this port (direct | direct-physical | " + "macvtap | normal | baremetal | virtio-forwarder | vdpa, " + "default: normal)" + ), ) parser.add_argument( '--host', diff --git a/releasenotes/notes/network-port-create-vnic-type-vdpa-fc02516cfb919941.yaml b/releasenotes/notes/network-port-create-vnic-type-vdpa-fc02516cfb919941.yaml new file mode 100644 index 0000000000..4821d2a647 --- /dev/null +++ b/releasenotes/notes/network-port-create-vnic-type-vdpa-fc02516cfb919941.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + The ``port create --vnic-type`` option now accepts a ``vdpa`` value. From bf97b5f2878884416613d104f7168edf26f64ffa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 19 Mar 2021 15:55:30 +0000 Subject: [PATCH 0385/1120] volume: Re-add accidentally deleted test This is essentially a partial revert of change I94aa7a9824e44f9585ffb45e5e7637b9588539b4, which removed some deprecated commands like 'openstack snapshot *' in favour of 'openstack volume snapshot *'. Unfortunately the latter appeared to have no test coverage and were relying on tests for the former to validate behavior. Re-add the tests removed back then. Change-Id: Ib2cd975221034c8997d272d43cfb18acefc319fe Signed-off-by: Stephen Finucane --- .../unit/volume/v2/test_volume_snapshot.py | 742 ++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 openstackclient/tests/unit/volume/v2/test_volume_snapshot.py diff --git a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py new file mode 100644 index 0000000000..3830f458d6 --- /dev/null +++ b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py @@ -0,0 +1,742 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import argparse +from unittest import mock + +from osc_lib.cli import format_columns +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.tests.unit.identity.v3 import fakes as project_fakes +from openstackclient.tests.unit import utils as tests_utils +from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes +from openstackclient.volume.v2 import volume_snapshot + + +class TestVolumeSnapshot(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.snapshots_mock = self.app.client_manager.volume.volume_snapshots + self.snapshots_mock.reset_mock() + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() + self.project_mock = self.app.client_manager.identity.projects + self.project_mock.reset_mock() + + +class TestVolumeSnapshotCreate(TestVolumeSnapshot): + + columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) + + def setUp(self): + super().setUp() + + self.volume = volume_fakes.FakeVolume.create_one_volume() + self.new_snapshot = volume_fakes.FakeSnapshot.create_one_snapshot( + attrs={'volume_id': self.volume.id}) + + self.data = ( + self.new_snapshot.created_at, + self.new_snapshot.description, + self.new_snapshot.id, + self.new_snapshot.name, + format_columns.DictColumn(self.new_snapshot.metadata), + self.new_snapshot.size, + self.new_snapshot.status, + self.new_snapshot.volume_id, + ) + + self.volumes_mock.get.return_value = self.volume + self.snapshots_mock.create.return_value = self.new_snapshot + self.snapshots_mock.manage.return_value = self.new_snapshot + # Get the command object to test + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) + + def test_snapshot_create(self): + arglist = [ + "--volume", self.new_snapshot.volume_id, + "--description", self.new_snapshot.description, + "--force", + '--property', 'Alpha=a', + '--property', 'Beta=b', + self.new_snapshot.name, + ] + verifylist = [ + ("volume", self.new_snapshot.volume_id), + ("description", self.new_snapshot.description), + ("force", True), + ('property', {'Alpha': 'a', 'Beta': 'b'}), + ("snapshot_name", self.new_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.create.assert_called_with( + self.new_snapshot.volume_id, + force=True, + name=self.new_snapshot.name, + description=self.new_snapshot.description, + metadata={'Alpha': 'a', 'Beta': 'b'}, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_snapshot_create_without_name(self): + arglist = [ + "--volume", self.new_snapshot.volume_id, + ] + verifylist = [ + ("volume", self.new_snapshot.volume_id), + ] + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_snapshot_create_without_volume(self): + arglist = [ + "--description", self.new_snapshot.description, + "--force", + self.new_snapshot.name + ] + verifylist = [ + ("description", self.new_snapshot.description), + ("force", True), + ("snapshot_name", self.new_snapshot.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.get.assert_called_once_with( + self.new_snapshot.name) + self.snapshots_mock.create.assert_called_once_with( + self.new_snapshot.volume_id, + force=True, + name=self.new_snapshot.name, + description=self.new_snapshot.description, + metadata=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_snapshot_create_with_remote_source(self): + arglist = [ + '--remote-source', 'source-name=test_source_name', + '--remote-source', 'source-id=test_source_id', + '--volume', self.new_snapshot.volume_id, + self.new_snapshot.name, + ] + ref_dict = {'source-name': 'test_source_name', + 'source-id': 'test_source_id'} + verifylist = [ + ('remote_source', ref_dict), + ('volume', self.new_snapshot.volume_id), + ("snapshot_name", self.new_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.manage.assert_called_with( + volume_id=self.new_snapshot.volume_id, + ref=ref_dict, + name=self.new_snapshot.name, + description=None, + metadata=None, + ) + self.snapshots_mock.create.assert_not_called() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestVolumeSnapshotDelete(TestVolumeSnapshot): + + snapshots = volume_fakes.FakeSnapshot.create_snapshots(count=2) + + def setUp(self): + super().setUp() + + self.snapshots_mock.get = ( + volume_fakes.FakeSnapshot.get_snapshots(self.snapshots)) + self.snapshots_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) + + def test_snapshot_delete(self): + arglist = [ + self.snapshots[0].id + ] + verifylist = [ + ("snapshots", [self.snapshots[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.delete.assert_called_with( + self.snapshots[0].id, False) + self.assertIsNone(result) + + def test_snapshot_delete_with_force(self): + arglist = [ + '--force', + self.snapshots[0].id + ] + verifylist = [ + ('force', True), + ("snapshots", [self.snapshots[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.delete.assert_called_with( + self.snapshots[0].id, True) + self.assertIsNone(result) + + def test_delete_multiple_snapshots(self): + arglist = [] + for s in self.snapshots: + arglist.append(s.id) + verifylist = [ + ('snapshots', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for s in self.snapshots: + calls.append(mock.call(s.id, False)) + self.snapshots_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_snapshots_with_exception(self): + arglist = [ + self.snapshots[0].id, + 'unexist_snapshot', + ] + verifylist = [ + ('snapshots', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.snapshots[0], exceptions.CommandError] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 snapshots failed to delete.', + str(e)) + + find_mock.assert_any_call( + self.snapshots_mock, self.snapshots[0].id) + find_mock.assert_any_call(self.snapshots_mock, 'unexist_snapshot') + + self.assertEqual(2, find_mock.call_count) + self.snapshots_mock.delete.assert_called_once_with( + self.snapshots[0].id, False + ) + + +class TestVolumeSnapshotList(TestVolumeSnapshot): + + volume = volume_fakes.FakeVolume.create_one_volume() + project = project_fakes.FakeProject.create_one_project() + snapshots = volume_fakes.FakeSnapshot.create_snapshots( + attrs={'volume_id': volume.name}, count=3) + + columns = [ + "ID", + "Name", + "Description", + "Status", + "Size" + ] + columns_long = columns + [ + "Created At", + "Volume", + "Properties" + ] + + data = [] + for s in snapshots: + data.append(( + s.id, + s.name, + s.description, + s.status, + s.size, + )) + data_long = [] + for s in snapshots: + data_long.append(( + s.id, + s.name, + s.description, + s.status, + s.size, + s.created_at, + volume_snapshot.VolumeIdColumn( + s.volume_id, volume_cache={volume.id: volume}), + format_columns.DictColumn(s.metadata), + )) + + def setUp(self): + super().setUp() + + self.volumes_mock.list.return_value = [self.volume] + self.volumes_mock.get.return_value = self.volume + self.project_mock.get.return_value = self.project + self.snapshots_mock.list.return_value = self.snapshots + # Get the command to test + self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) + + def test_snapshot_list_without_options(self): + arglist = [] + verifylist = [ + ('all_projects', False), + ('long', False) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': None, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_with_options(self): + arglist = [ + "--long", + "--limit", "2", + "--project", self.project.id, + "--marker", self.snapshots[0].id, + ] + verifylist = [ + ("long", True), + ("limit", 2), + ("project", self.project.id), + ("marker", self.snapshots[0].id), + ('all_projects', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=2, + marker=self.snapshots[0].id, + search_opts={ + 'all_tenants': True, + 'project_id': self.project.id, + 'name': None, + 'status': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_snapshot_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': True, + 'name': None, + 'status': None, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_name_option(self): + arglist = [ + '--name', self.snapshots[0].name, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('name', self.snapshots[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': self.snapshots[0].name, + 'status': None, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_status_option(self): + arglist = [ + '--status', self.snapshots[0].status, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('status', self.snapshots[0].status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': self.snapshots[0].status, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_volumeid_option(self): + arglist = [ + '--volume', self.volume.id, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': None, + 'project_id': None, + 'volume_id': self.volume.id + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_negative_limit(self): + arglist = [ + "--limit", "-2", + ] + verifylist = [ + ("limit", -2), + ] + self.assertRaises(argparse.ArgumentTypeError, self.check_parser, + self.cmd, arglist, verifylist) + + +class TestVolumeSnapshotSet(TestVolumeSnapshot): + + snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + + def setUp(self): + super().setUp() + + self.snapshots_mock.get.return_value = self.snapshot + self.snapshots_mock.set_metadata.return_value = None + self.snapshots_mock.update.return_value = None + # Get the command object to mock + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) + + def test_snapshot_set_no_option(self): + arglist = [ + self.snapshot.id, + ] + verifylist = [ + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) + self.assertNotCalled(self.snapshots_mock.reset_state) + self.assertNotCalled(self.snapshots_mock.update) + self.assertNotCalled(self.snapshots_mock.set_metadata) + self.assertIsNone(result) + + def test_snapshot_set_name_and_property(self): + arglist = [ + "--name", "new_snapshot", + "--property", "x=y", + "--property", "foo=foo", + self.snapshot.id, + ] + new_property = {"x": "y", "foo": "foo"} + verifylist = [ + ("name", "new_snapshot"), + ("property", new_property), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + "name": "new_snapshot", + } + self.snapshots_mock.update.assert_called_with( + self.snapshot.id, **kwargs) + self.snapshots_mock.set_metadata.assert_called_with( + self.snapshot.id, new_property + ) + self.assertIsNone(result) + + def test_snapshot_set_with_no_property(self): + arglist = [ + "--no-property", + self.snapshot.id, + ] + verifylist = [ + ("no_property", True), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) + self.assertNotCalled(self.snapshots_mock.reset_state) + self.assertNotCalled(self.snapshots_mock.update) + self.assertNotCalled(self.snapshots_mock.set_metadata) + self.snapshots_mock.delete_metadata.assert_called_with( + self.snapshot.id, ["foo"] + ) + self.assertIsNone(result) + + def test_snapshot_set_with_no_property_and_property(self): + arglist = [ + "--no-property", + "--property", "foo_1=bar_1", + self.snapshot.id, + ] + verifylist = [ + ("no_property", True), + ("property", {"foo_1": "bar_1"}), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) + self.assertNotCalled(self.snapshots_mock.reset_state) + self.assertNotCalled(self.snapshots_mock.update) + self.snapshots_mock.delete_metadata.assert_called_with( + self.snapshot.id, ["foo"] + ) + self.snapshots_mock.set_metadata.assert_called_once_with( + self.snapshot.id, {"foo_1": "bar_1"}) + self.assertIsNone(result) + + def test_snapshot_set_state_to_error(self): + arglist = [ + "--state", "error", + self.snapshot.id + ] + verifylist = [ + ("state", "error"), + ("snapshot", self.snapshot.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.reset_state.assert_called_with( + self.snapshot.id, "error") + self.assertIsNone(result) + + def test_volume_set_state_failed(self): + self.snapshots_mock.reset_state.side_effect = exceptions.CommandError() + arglist = [ + '--state', 'error', + self.snapshot.id + ] + verifylist = [ + ('state', 'error'), + ('snapshot', self.snapshot.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('One or more of the set operations failed', + str(e)) + self.snapshots_mock.reset_state.assert_called_once_with( + self.snapshot.id, 'error') + + def test_volume_set_name_and_state_failed(self): + self.snapshots_mock.reset_state.side_effect = exceptions.CommandError() + arglist = [ + '--state', 'error', + "--name", "new_snapshot", + self.snapshot.id + ] + verifylist = [ + ('state', 'error'), + ("name", "new_snapshot"), + ('snapshot', self.snapshot.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('One or more of the set operations failed', + str(e)) + kwargs = { + "name": "new_snapshot", + } + self.snapshots_mock.update.assert_called_once_with( + self.snapshot.id, **kwargs) + self.snapshots_mock.reset_state.assert_called_once_with( + self.snapshot.id, 'error') + + +class TestVolumeSnapshotShow(TestVolumeSnapshot): + + columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) + + def setUp(self): + super().setUp() + + self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + + self.data = ( + self.snapshot.created_at, + self.snapshot.description, + self.snapshot.id, + self.snapshot.name, + format_columns.DictColumn(self.snapshot.metadata), + self.snapshot.size, + self.snapshot.status, + self.snapshot.volume_id, + ) + + self.snapshots_mock.get.return_value = self.snapshot + # Get the command object to test + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) + + def test_snapshot_show(self): + arglist = [ + self.snapshot.id + ] + verifylist = [ + ("snapshot", self.snapshot.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_with(self.snapshot.id) + + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, data) + + +class TestVolumeSnapshotUnset(TestVolumeSnapshot): + + snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() + + def setUp(self): + super().setUp() + + self.snapshots_mock.get.return_value = self.snapshot + self.snapshots_mock.delete_metadata.return_value = None + # Get the command object to mock + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) + + def test_snapshot_unset(self): + arglist = [ + "--property", "foo", + self.snapshot.id, + ] + verifylist = [ + ("property", ["foo"]), + ("snapshot", self.snapshot.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.delete_metadata.assert_called_with( + self.snapshot.id, ["foo"] + ) + self.assertIsNone(result) From c58f0277a74a0f94638689de7db297f48243048e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 19 Mar 2021 18:36:44 +0000 Subject: [PATCH 0386/1120] network: Make 'network qos rule create --type' option required When we create a network qos rule we need specify the type so that we can call the corresponding API. It's not possible to use the command without the type so mark it as required. This was already being done but inline. Change-Id: I559f884bac198d2c69e800620aef66b200473418 Signed-off-by: Stephen Finucane --- openstackclient/network/v2/network_qos_rule.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index 28c5600aa6..2e4b385d2e 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -84,9 +84,6 @@ def _get_attrs(network_client, parsed_args, is_create=False): {'rule_id': parsed_args.id}) raise exceptions.CommandError(msg) else: - if not parsed_args.type: - msg = _('"Create" rule command requires argument "type"') - raise exceptions.CommandError(msg) rule_type = parsed_args.type if parsed_args.max_kbps is not None: attrs['max_kbps'] = parsed_args.max_kbps @@ -188,6 +185,7 @@ def get_parser(self, prog_name): parser.add_argument( '--type', metavar='', + required=True, choices=[RULE_TYPE_MINIMUM_BANDWIDTH, RULE_TYPE_DSCP_MARKING, RULE_TYPE_BANDWIDTH_LIMIT], From d769ff4393661ebfde9d683c04cc271ddb550140 Mon Sep 17 00:00:00 2001 From: James Denton Date: Mon, 14 Jan 2019 15:22:45 +0000 Subject: [PATCH 0387/1120] Hides prefix_length column in subnet show output When the openstacksdk is patched to properly support defining prefix lengths when creating subnets, the resulting subnet show output reveals a prefix_length column with a value of 'none'. This patch hides the prefix_length column. Change-Id: I59dfb0b1585ed624f9d82b3557df2ff5ff9d1b3e Partial-Bug: 1754062 Depends-On: https://review.openstack.org/#/c/550558/ --- openstackclient/network/v2/subnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index b98f86412c..eccdd4e460 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -141,7 +141,7 @@ def _get_columns(item): 'tenant_id': 'project_id', } # Do not show this column when displaying a subnet - invisible_columns = ['use_default_subnet_pool'] + invisible_columns = ['use_default_subnet_pool', 'prefix_length'] return sdk_utils.get_osc_show_columns_for_sdk_resource( item, column_map, From 32151b099c9b237adadbb8123a885eb46bcc2a30 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Sat, 20 Mar 2021 09:16:50 +0000 Subject: [PATCH 0388/1120] Update master for stable/wallaby Add file to the reno documentation build to show release notes for stable/wallaby. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/wallaby. Sem-Ver: feature Change-Id: I848e21bf2d2ea1a1a132525070e5f20d7ff8478d --- releasenotes/source/index.rst | 1 + releasenotes/source/wallaby.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/wallaby.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 8c276de2d8..cdbb595ce8 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + wallaby victoria ussuri train diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 0000000000..d77b565995 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: stable/wallaby From 449b30981a5151fb19174e61f51f5f6dee142af1 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Sat, 20 Mar 2021 09:16:55 +0000 Subject: [PATCH 0389/1120] Add Python3 xena unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for xena. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I2415ec61c3580fcd43ee7d8f2a90b698ac156593 --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index 42c29d162d..404c005a41 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -222,7 +222,7 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-wallaby-jobs + - openstack-python3-xena-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 From 383289edd8ea222ce1a6e77ed0298ecdb21608a1 Mon Sep 17 00:00:00 2001 From: Valery Tschopp Date: Tue, 2 Feb 2021 12:56:55 +0100 Subject: [PATCH 0390/1120] Implements hide image openstack image set [--hidden|--unhidden] IMAGE openstack image list --hidden Task: 41734 Story: 2008581 Change-Id: Ie84f10c0f7aa2e7b7f78bfadc70132a10673866e --- openstackclient/image/v2/image.py | 27 ++++++++ .../tests/unit/image/v2/test_image.py | 68 +++++++++++++++++++ ...mplements-hide-image-4c726a61c336ebaa.yaml | 8 +++ 3 files changed, 103 insertions(+) create mode 100644 releasenotes/notes/implements-hide-image-4c726a61c336ebaa.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index ad38c01ec2..644fbbb4f7 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -615,6 +615,12 @@ def get_parser(self, prog_name): default=None, help=_('Filter images based on tag.'), ) + parser.add_argument( + '--hidden', + action='store_true', + default=False, + help=_('List hidden images'), + ) parser.add_argument( '--long', action='store_true', @@ -686,6 +692,8 @@ def take_action(self, parsed_args): parsed_args.project_domain, ).id kwargs['owner'] = project_id + if parsed_args.hidden: + kwargs['is_hidden'] = True if parsed_args.long: columns = ( 'ID', @@ -1016,6 +1024,22 @@ def get_parser(self, prog_name): action="store_true", help=_("Reset the image membership to 'pending'"), ) + + hidden_group = parser.add_mutually_exclusive_group() + hidden_group.add_argument( + "--hidden", + dest='hidden', + default=None, + action="store_true", + help=_("Hide the image"), + ) + hidden_group.add_argument( + "--unhidden", + dest='hidden', + default=None, + action="store_false", + help=_("Unhide the image"), + ) return parser def take_action(self, parsed_args): @@ -1106,6 +1130,9 @@ def take_action(self, parsed_args): # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) + if parsed_args.hidden is not None: + kwargs['is_hidden'] = parsed_args.hidden + try: image = image_client.update_image(image.id, **kwargs) except Exception: diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 87dfdbeab9..c44c767b70 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -837,6 +837,20 @@ def test_image_list_status_option(self): status='active' ) + def test_image_list_hidden_option(self): + arglist = [ + '--hidden', + ] + verifylist = [ + ('hidden', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.client.images.assert_called_with( + is_hidden=True + ) + def test_image_list_tag_option(self): arglist = [ '--tag', 'abc', @@ -1439,6 +1453,60 @@ def test_image_set_numeric_options_to_zero(self): ) self.assertIsNone(result) + def test_image_set_hidden(self): + arglist = [ + '--hidden', + '--public', + image_fakes.image_name, + ] + verifylist = [ + ('hidden', True), + ('public', True), + ('private', False), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + 'is_hidden': True, + 'visibility': 'public', + } + # ImageManager.update(image, **kwargs) + self.client.update_image.assert_called_with( + self._image.id, + **kwargs + ) + self.assertIsNone(result) + + def test_image_set_unhidden(self): + arglist = [ + '--unhidden', + '--public', + image_fakes.image_name, + ] + verifylist = [ + ('hidden', False), + ('public', True), + ('private', False), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + 'is_hidden': False, + 'visibility': 'public', + } + # ImageManager.update(image, **kwargs) + self.client.update_image.assert_called_with( + self._image.id, + **kwargs + ) + self.assertIsNone(result) + class TestImageShow(TestImage): diff --git a/releasenotes/notes/implements-hide-image-4c726a61c336ebaa.yaml b/releasenotes/notes/implements-hide-image-4c726a61c336ebaa.yaml new file mode 100644 index 0000000000..718ac2920d --- /dev/null +++ b/releasenotes/notes/implements-hide-image-4c726a61c336ebaa.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add mutually exclusive options ``--hidden`` and ``--unhidden`` to + ``image set`` command to hide or unhide an image (``is_hidden`` + attribute). + - | + Add option ``--hidden`` to ``image list`` command to list hidden images. From f00e14f400193e35c8040bc93ac6c509fa2bb4dc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 31 Mar 2021 18:18:55 +0100 Subject: [PATCH 0391/1120] hacking: Remove references to encoding This is no longer an issue in our new Python 3-only world. Change-Id: I25c31a0b7f76a253499d9713ba48fd7ba7168450 Signed-off-by: Stephen Finucane --- HACKING.rst | 48 +----------------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index b5fbad3ca1..432d19f4e4 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -32,7 +32,7 @@ use the alternate 4 space indent. With the first argument on the succeeding line all arguments will then be vertically aligned. Use the same convention used with other data structure literals and terminate the method call with the last argument line ending with a comma and the closing paren on its own -line indented to the starting line level. +line indented to the starting line level. :: unnecessarily_long_function_name( 'string one', @@ -40,49 +40,3 @@ line indented to the starting line level. kwarg1=constants.ACTIVE, kwarg2=['a', 'b', 'c'], ) - -Text encoding -------------- - -Note: this section clearly has not been implemented in this project yet, it is -the intention to do so. - -All text within python code should be of type 'unicode'. - - WRONG: - - >>> s = 'foo' - >>> s - 'foo' - >>> type(s) - - - RIGHT: - - >>> u = u'foo' - >>> u - u'foo' - >>> type(u) - - -Transitions between internal unicode and external strings should always -be immediately and explicitly encoded or decoded. - -All external text that is not explicitly encoded (database storage, -commandline arguments, etc.) should be presumed to be encoded as utf-8. - - WRONG: - - infile = open('testfile', 'r') - mystring = infile.readline() - myreturnstring = do_some_magic_with(mystring) - outfile.write(myreturnstring) - - RIGHT: - - infile = open('testfile', 'r') - mystring = infile.readline() - mytext = mystring.decode('utf-8') - returntext = do_some_magic_with(mytext) - returnstring = returntext.encode('utf-8') - outfile.write(returnstring) From 168a4e73904f15c2fb34e98f8884ff76291ee287 Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Thu, 8 Apr 2021 14:22:49 +0800 Subject: [PATCH 0392/1120] requirements: Drop os-testr os-testr has been decrepated [1], it's not necessary in a world with stestr. [1]https://opendev.org/openstack/os-testr/src/branch/master/README.rst Change-Id: Id2382f2c559ea7f4d4a629d137f07f0ce8841abc --- lower-constraints.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index f93db8aa8c..09aabede1f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -41,7 +41,6 @@ netifaces==0.10.4 openstacksdk==0.53.0 os-client-config==2.1.0 os-service-types==1.7.0 -os-testr==1.0.0 osc-lib==2.3.0 oslo.concurrency==3.26.0 oslo.config==5.2.0 From e82a05864f482acc485d1bd35a4db23452f8b2ac Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Mon, 3 May 2021 22:07:39 +0200 Subject: [PATCH 0393/1120] Replace assertItemsEqual with assertCountEqual assertItemsEqual was removed from Python's unittest.TestCase in Python 3.3 [1][2]. We have been able to use them since then, because testtools required unittest2, which still included it. With testtools removing Python 2.7 support [3][4], we will lose support for assertItemsEqual, so we should switch to use assertCountEqual. [1] - https://bugs.python.org/issue17866 [2] - https://hg.python.org/cpython/rev/d9921cb6e3cd [3] - testing-cabal/testtools#286 [4] - testing-cabal/testtools#277 Change-Id: I0bbffbec8889b8b3067cfe17d258f5cb16624f38 --- .../tests/unit/compute/v2/test_aggregate.py | 16 ++--- .../tests/unit/compute/v2/test_flavor.py | 14 ++-- .../tests/unit/compute/v2/test_hypervisor.py | 6 +- .../unit/compute/v2/test_server_backup.py | 6 +- .../unit/compute/v2/test_server_image.py | 6 +- .../tests/unit/identity/v2_0/test_catalog.py | 6 +- .../tests/unit/identity/v2_0/test_project.py | 2 +- .../tests/unit/identity/v2_0/test_user.py | 8 +-- .../tests/unit/identity/v3/test_catalog.py | 4 +- .../identity/v3/test_identity_provider.py | 24 +++---- .../tests/unit/image/v1/test_image.py | 10 +-- .../tests/unit/image/v2/test_image.py | 24 +++---- .../unit/network/v2/test_address_group.py | 14 ++-- .../unit/network/v2/test_ip_availability.py | 8 +-- .../tests/unit/network/v2/test_network.py | 46 ++++++------- .../unit/network/v2/test_network_agent.py | 14 ++-- .../tests/unit/network/v2/test_port.py | 68 +++++++++---------- .../tests/unit/network/v2/test_router.py | 32 ++++----- .../network/v2/test_security_group_compute.py | 10 +-- .../network/v2/test_security_group_network.py | 16 ++--- .../tests/unit/network/v2/test_subnet.py | 42 ++++++------ .../tests/unit/network/v2/test_subnet_pool.py | 40 +++++------ .../tests/unit/volume/v1/test_qos_specs.py | 12 ++-- .../tests/unit/volume/v1/test_type.py | 14 ++-- .../tests/unit/volume/v1/test_volume.py | 36 +++++----- .../unit/volume/v1/test_volume_backup.py | 10 +-- .../unit/volume/v2/test_consistency_group.py | 14 ++-- .../tests/unit/volume/v2/test_qos_specs.py | 12 ++-- .../tests/unit/volume/v2/test_type.py | 24 +++---- .../tests/unit/volume/v2/test_volume.py | 40 +++++------ .../unit/volume/v2/test_volume_backup.py | 4 +- .../unit/volume/v2/test_volume_snapshot.py | 2 +- 32 files changed, 292 insertions(+), 292 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index 8563f98811..7c4fe5cbd3 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -88,7 +88,7 @@ def test_aggregate_add_host(self): self.sdk_client.add_host_to_aggregate.assert_called_once_with( self.fake_ag.id, parsed_args.host) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestAggregateCreate(TestAggregate): @@ -112,7 +112,7 @@ def test_aggregate_create(self): self.sdk_client.create_aggregate.assert_called_once_with( name=parsed_args.name) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_aggregate_create_with_zone(self): arglist = [ @@ -129,7 +129,7 @@ def test_aggregate_create_with_zone(self): self.sdk_client.create_aggregate.assert_called_once_with( name=parsed_args.name, availability_zone=parsed_args.zone) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_aggregate_create_with_property(self): arglist = [ @@ -148,7 +148,7 @@ def test_aggregate_create_with_property(self): self.sdk_client.set_aggregate_metadata.assert_called_once_with( self.fake_ag.id, parsed_args.properties) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestAggregateDelete(TestAggregate): @@ -265,7 +265,7 @@ def test_aggregate_list(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.list_columns, columns) - self.assertItemsEqual(self.list_data, tuple(data)) + self.assertCountEqual(self.list_data, tuple(data)) def test_aggregate_list_with_long(self): arglist = [ @@ -278,7 +278,7 @@ def test_aggregate_list_with_long(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.list_columns_long, columns) - self.assertItemsEqual(self.list_data_long, tuple(data)) + self.assertCountEqual(self.list_data_long, tuple(data)) class TestAggregateRemoveHost(TestAggregate): @@ -306,7 +306,7 @@ def test_aggregate_remove_host(self): self.sdk_client.remove_host_from_aggregate.assert_called_once_with( self.fake_ag.id, parsed_args.host) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestAggregateSet(TestAggregate): @@ -492,7 +492,7 @@ def test_aggregate_show(self): parsed_args.aggregate, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, tuple(data)) + self.assertCountEqual(self.data, tuple(data)) class TestAggregateUnset(TestAggregate): diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index ee4479b009..14dd3df20a 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -133,7 +133,7 @@ def test_flavor_create_default_options(self): self.sdk_client.create_flavor.assert_called_once_with(**default_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_flavor_create_all_options(self): @@ -202,7 +202,7 @@ def test_flavor_create_all_options(self): self.sdk_client.get_flavor_access.assert_not_called() self.assertEqual(self.columns, columns) - self.assertItemsEqual(tuple(cmp_data), data) + self.assertCountEqual(tuple(cmp_data), data) def test_flavor_create_other_options(self): @@ -277,7 +277,7 @@ def test_flavor_create_other_options(self): self.sdk_client.create_flavor_extra_specs.assert_called_with( create_flavor, props) self.assertEqual(self.columns, columns) - self.assertItemsEqual(cmp_data, data) + self.assertCountEqual(cmp_data, data) def test_public_flavor_create_with_project(self): arglist = [ @@ -350,7 +350,7 @@ def test_flavor_create_with_description_api_newer(self): self.sdk_client.create_flavor.assert_called_once_with(**args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data_private, data) + self.assertCountEqual(self.data_private, data) def test_flavor_create_with_description_api_older(self): arglist = [ @@ -633,7 +633,7 @@ def test_flavor_list_long(self): ) self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, tuple(data)) + self.assertCountEqual(self.data_long, tuple(data)) def test_flavor_list_min_disk_min_ram(self): arglist = [ @@ -951,7 +951,7 @@ def test_public_flavor_show(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_private_flavor_show(self): private_flavor = compute_fakes.FakeFlavor.create_one_flavor( @@ -991,7 +991,7 @@ def test_private_flavor_show(self): self.sdk_client.get_flavor_access.assert_called_with( flavor=private_flavor.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(data_with_project, data) + self.assertCountEqual(data_with_project, data) class TestFlavorUnset(TestFlavor): diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor.py b/openstackclient/tests/unit/compute/v2/test_hypervisor.py index 3220a76450..7dbd6e1983 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor.py @@ -394,7 +394,7 @@ def test_hypervisor_show(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_hypervisor_show_pre_v228(self): self.app.client_manager.compute.api_version = \ @@ -420,7 +420,7 @@ def test_hypervisor_show_pre_v228(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_hypervisor_show_uptime_not_implemented(self): self.app.client_manager.compute.api_version = \ @@ -492,4 +492,4 @@ def test_hypervisor_show_uptime_not_implemented(self): ) self.assertEqual(expected_columns, columns) - self.assertItemsEqual(expected_data, data) + self.assertCountEqual(expected_data, data) diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 753db9cd70..0012d70062 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -139,7 +139,7 @@ def test_server_backup_defaults(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemsEqual(self.image_data(images[0]), data) + self.assertCountEqual(self.image_data(images[0]), data) def test_server_backup_create_options(self): servers = self.setup_servers_mock(count=1) @@ -173,7 +173,7 @@ def test_server_backup_create_options(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemsEqual(self.image_data(images[0]), data) + self.assertCountEqual(self.image_data(images[0]), data) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_backup_wait_fail(self, mock_wait_for_status): @@ -269,4 +269,4 @@ def test_server_backup_wait_ok(self, mock_wait_for_status): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemsEqual(self.image_data(images[0]), data) + self.assertCountEqual(self.image_data(images[0]), data) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 66452a8bb8..9b14428a27 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -134,7 +134,7 @@ def test_server_image_create_defaults(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemsEqual(self.image_data(images[0]), data) + self.assertCountEqual(self.image_data(images[0]), data) def test_server_image_create_options(self): servers = self.setup_servers_mock(count=1) @@ -165,7 +165,7 @@ def test_server_image_create_options(self): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemsEqual(self.image_data(images[0]), data) + self.assertCountEqual(self.image_data(images[0]), data) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) def test_server_create_image_wait_fail(self, mock_wait_for_status): @@ -235,4 +235,4 @@ def test_server_create_image_wait_ok(self, mock_wait_for_status): ) self.assertEqual(self.image_columns(images[0]), columns) - self.assertItemsEqual(self.image_data(images[0]), data) + self.assertCountEqual(self.image_data(images[0]), data) diff --git a/openstackclient/tests/unit/identity/v2_0/test_catalog.py b/openstackclient/tests/unit/identity/v2_0/test_catalog.py index e2c56ba1ec..bfb28f6962 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_catalog.py +++ b/openstackclient/tests/unit/identity/v2_0/test_catalog.py @@ -74,7 +74,7 @@ def test_catalog_list(self): catalog.EndpointsColumn( auth_ref.service_catalog.catalog[0]['endpoints']), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_catalog_list_with_endpoint_url(self): attr = { @@ -117,7 +117,7 @@ def test_catalog_list_with_endpoint_url(self): catalog.EndpointsColumn( auth_ref.service_catalog.catalog[0]['endpoints']), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) class TestCatalogShow(TestCatalog): @@ -158,7 +158,7 @@ def test_catalog_show(self): 'supernova', 'compute', ) - self.assertItemsEqual(datalist, data) + self.assertCountEqual(datalist, data) class TestFormatColumns(TestCatalog): diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index 766d5dab55..496214aaee 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -643,7 +643,7 @@ def test_project_show(self): self.fake_proj_show.name, format_columns.DictColumn({}), ) - self.assertItemsEqual(datalist, data) + self.assertCountEqual(datalist, data) class TestProjectUnset(TestProject): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index dd30047814..c3f5f1d7a1 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -482,7 +482,7 @@ def test_user_list_no_options(self): self.users_mock.list.assert_called_with(tenant_id=None) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_user_list_project(self): arglist = [ @@ -502,7 +502,7 @@ def test_user_list_project(self): self.users_mock.list.assert_called_with(tenant_id=project_id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_user_list_long(self): arglist = [ @@ -531,7 +531,7 @@ def test_user_list_long(self): self.fake_user_l.email, True, ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) class TestUserSet(TestUser): @@ -819,4 +819,4 @@ def test_user_show(self): self.fake_user.name, self.fake_project.id, ) - self.assertItemsEqual(datalist, data) + self.assertCountEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_catalog.py b/openstackclient/tests/unit/identity/v3/test_catalog.py index 97ce48f6a5..802a9017e2 100644 --- a/openstackclient/tests/unit/identity/v3/test_catalog.py +++ b/openstackclient/tests/unit/identity/v3/test_catalog.py @@ -94,7 +94,7 @@ def test_catalog_list(self): catalog.EndpointsColumn( auth_ref.service_catalog.catalog[0]['endpoints']), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) class TestCatalogShow(TestCatalog): @@ -135,7 +135,7 @@ def test_catalog_show(self): 'supernova', 'compute', ) - self.assertItemsEqual(datalist, data) + self.assertCountEqual(datalist, data) class TestFormatColumns(TestCatalog): diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index 5aff2b1be2..1a9a799123 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -89,7 +89,7 @@ def test_create_identity_provider_no_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_description(self): arglist = [ @@ -117,7 +117,7 @@ def test_create_identity_provider_description(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_remote_id(self): arglist = [ @@ -145,7 +145,7 @@ def test_create_identity_provider_remote_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_remote_ids_multiple(self): arglist = [ @@ -174,7 +174,7 @@ def test_create_identity_provider_remote_ids_multiple(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_remote_ids_file(self): arglist = [ @@ -207,7 +207,7 @@ def test_create_identity_provider_remote_ids_file(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_disabled(self): @@ -250,7 +250,7 @@ def test_create_identity_provider_disabled(self): identity_fakes.idp_id, identity_fakes.formatted_idp_remote_ids ) - self.assertItemsEqual(datalist, data) + self.assertCountEqual(datalist, data) def test_create_identity_provider_domain_name(self): arglist = [ @@ -278,7 +278,7 @@ def test_create_identity_provider_domain_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_domain_id(self): arglist = [ @@ -306,7 +306,7 @@ def test_create_identity_provider_domain_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) class TestIdentityProviderDelete(TestIdentityProvider): @@ -382,7 +382,7 @@ def test_identity_provider_list_no_options(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_identity_provider_list_ID_option(self): arglist = ['--id', @@ -410,7 +410,7 @@ def test_identity_provider_list_ID_option(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_identity_provider_list_enabled_option(self): arglist = ['--enabled'] @@ -437,7 +437,7 @@ def test_identity_provider_list_enabled_option(self): identity_fakes.domain_id, identity_fakes.idp_description, ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) class TestIdentityProviderSet(TestIdentityProvider): @@ -722,4 +722,4 @@ def test_identity_provider_show(self): identity_fakes.idp_id, identity_fakes.formatted_idp_remote_ids ) - self.assertItemsEqual(datalist, data) + self.assertCountEqual(datalist, data) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index db64983c97..5c69bf0f87 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -100,7 +100,7 @@ def test_image_reserve_no_options(self, raw_input): self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) @mock.patch('sys.stdin', side_effect=[None]) def test_image_reserve_options(self, raw_input): @@ -149,7 +149,7 @@ def test_image_reserve_options(self, raw_input): self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) @mock.patch('openstackclient.image.v1.image.io.open', name='Open') def test_image_create_file(self, mock_open): @@ -205,7 +205,7 @@ def test_image_create_file(self, mock_open): self.assertEqual(self.client.update_image.call_args_list, []) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestImageDelete(TestImage): @@ -386,7 +386,7 @@ def test_image_list_long_option(self): format_columns.DictColumn( {'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): @@ -737,7 +737,7 @@ def test_image_show(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_image_show_human_readable(self): arglist = [ diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index c44c767b70..35af6799f6 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -111,7 +111,7 @@ def test_image_reserve_no_options(self, raw_input): self.assertEqual( self.expected_columns, columns) - self.assertItemsEqual( + self.assertCountEqual( self.expected_data, data) @@ -166,7 +166,7 @@ def test_image_reserve_options(self, raw_input): self.assertEqual( self.expected_columns, columns) - self.assertItemsEqual( + self.assertCountEqual( self.expected_data, data) @@ -255,7 +255,7 @@ def test_image_create_file(self): self.assertEqual( self.expected_columns, columns) - self.assertItemsEqual( + self.assertCountEqual( self.expected_data, data) @@ -513,7 +513,7 @@ def test_image_list_no_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_image_list_public_option(self): arglist = [ @@ -537,7 +537,7 @@ def test_image_list_public_option(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_image_list_private_option(self): arglist = [ @@ -561,7 +561,7 @@ def test_image_list_private_option(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_image_list_community_option(self): arglist = [ @@ -609,7 +609,7 @@ def test_image_list_shared_option(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_image_list_shared_member_status_option(self): arglist = [ @@ -697,7 +697,7 @@ def test_image_list_long_option(self): self._image.owner_id, format_columns.ListColumn(self._image.tags), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): @@ -725,7 +725,7 @@ def test_image_list_property_option(self, sf_mock): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) @mock.patch('osc_lib.utils.sort_items') def test_image_list_sort_option(self, si_mock): @@ -747,7 +747,7 @@ def test_image_list_sort_option(self, si_mock): str, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_image_list_limit_option(self): ret_limit = 1 @@ -782,7 +782,7 @@ def test_image_list_project_option(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) @mock.patch('osc_lib.utils.find_resource') def test_image_list_marker_option(self, fr_mock): @@ -1555,7 +1555,7 @@ def test_image_show(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_image_show_human_readable(self): self.client.find_image.return_value = self.new_image diff --git a/openstackclient/tests/unit/network/v2/test_address_group.py b/openstackclient/tests/unit/network/v2/test_address_group.py index e4fa8ab3dc..a5ee83cb56 100644 --- a/openstackclient/tests/unit/network/v2/test_address_group.py +++ b/openstackclient/tests/unit/network/v2/test_address_group.py @@ -99,7 +99,7 @@ def test_create_default_options(self): 'addresses': [], }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -127,7 +127,7 @@ def test_create_all_options(self): 'description': self.new_address_group.description, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestDeleteAddressGroup(TestAddressGroup): @@ -252,7 +252,7 @@ def test_address_group_list(self): self.network.address_groups.assert_called_once_with(**{}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_address_group_list_name(self): arglist = [ @@ -267,7 +267,7 @@ def test_address_group_list_name(self): self.network.address_groups.assert_called_once_with( **{'name': self.address_groups[0].name}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_address_group_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -284,7 +284,7 @@ def test_address_group_list_project(self): self.network.address_groups.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_address_group_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -302,7 +302,7 @@ def test_address_group_project_domain(self): self.network.address_groups.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)) class TestSetAddressGroup(TestAddressGroup): @@ -438,7 +438,7 @@ def test_show_all_options(self): self.network.find_address_group.assert_called_once_with( self._address_group.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestUnsetAddressGroup(TestAddressGroup): diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index ade5783700..a722a02391 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -75,7 +75,7 @@ def test_list_no_options(self): self.network.network_ip_availabilities.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_ip_version(self): arglist = [ @@ -93,7 +93,7 @@ def test_list_ip_version(self): self.network.network_ip_availabilities.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_project(self): arglist = [ @@ -113,7 +113,7 @@ def test_list_project(self): self.network.network_ip_availabilities.assert_called_once_with( **filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestShowIPAvailability(TestIPAvailability): @@ -176,4 +176,4 @@ def test_show_all_options(self): self._ip_availability.network_name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index e29b72c769..127d82b015 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -146,7 +146,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -211,7 +211,7 @@ def test_create_all_options(self): 'dns_domain': 'example.org.', }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_other_options(self): arglist = [ @@ -238,7 +238,7 @@ def test_create_other_options(self): 'port_security_enabled': False, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self._network.name] @@ -270,7 +270,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -385,7 +385,7 @@ def test_create_with_project_identityv2(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_domain_identityv2(self): arglist = [ @@ -577,7 +577,7 @@ def test_network_list_no_options(self): self.network.networks.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_list_external(self): arglist = [ @@ -598,7 +598,7 @@ def test_list_external(self): **{'router:external': True, 'is_router_external': True} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_list_internal(self): arglist = [ @@ -615,7 +615,7 @@ def test_list_internal(self): **{'router:external': False, 'is_router_external': False} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_long(self): arglist = [ @@ -634,7 +634,7 @@ def test_network_list_long(self): self.network.networks.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_list_name(self): test_name = "fakename" @@ -653,7 +653,7 @@ def test_list_name(self): **{'name': test_name} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_enable(self): arglist = [ @@ -671,7 +671,7 @@ def test_network_list_enable(self): **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_disable(self): arglist = [ @@ -689,7 +689,7 @@ def test_network_list_disable(self): **{'admin_state_up': False, 'is_admin_state_up': False} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -708,7 +708,7 @@ def test_network_list_project(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -727,7 +727,7 @@ def test_network_list_project_domain(self): self.network.networks.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_share(self): arglist = [ @@ -744,7 +744,7 @@ def test_network_list_share(self): **{'shared': True, 'is_shared': True} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_no_share(self): arglist = [ @@ -761,7 +761,7 @@ def test_network_list_no_share(self): **{'shared': False, 'is_shared': False} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_status(self): choices = ['ACTIVE', 'BUILD', 'DOWN', 'ERROR'] @@ -780,7 +780,7 @@ def test_network_list_status(self): **{'status': test_status} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_provider_network_type(self): network_type = self._network[0].provider_network_type @@ -798,7 +798,7 @@ def test_network_list_provider_network_type(self): 'provider_network_type': network_type} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_provider_physical_network(self): physical_network = self._network[0].provider_physical_network @@ -816,7 +816,7 @@ def test_network_list_provider_physical_network(self): 'provider_physical_network': physical_network} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_provider_segment(self): segmentation_id = self._network[0].provider_segmentation_id @@ -834,7 +834,7 @@ def test_network_list_provider_segment(self): 'provider_segmentation_id': segmentation_id} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_list_dhcp_agent(self): arglist = [ @@ -853,7 +853,7 @@ def test_network_list_dhcp_agent(self): *attrs) self.assertEqual(self.columns, columns) - self.assertItemsEqual(list(data), list(self.data)) + self.assertCountEqual(list(data), list(self.data)) def test_list_with_tag_options(self): arglist = [ @@ -878,7 +878,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestSetNetwork(TestNetwork): @@ -1111,7 +1111,7 @@ def test_show_all_options(self): self._network.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestUnsetNetwork(TestNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index fceac68ea0..734a36ee94 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -246,7 +246,7 @@ def test_network_agents_list(self): self.network.agents.assert_called_once_with(**{}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_agents_list_agent_type(self): arglist = [ @@ -263,7 +263,7 @@ def test_network_agents_list_agent_type(self): 'agent_type': 'DHCP agent', }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_agents_list_host(self): arglist = [ @@ -280,7 +280,7 @@ def test_network_agents_list_host(self): 'host': self.network_agents[0].host, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_agents_list_networks(self): arglist = [ @@ -298,7 +298,7 @@ def test_network_agents_list_networks(self): self.network.network_hosting_dhcp_agents.assert_called_once_with( *attrs) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_agents_list_routers(self): arglist = [ @@ -318,7 +318,7 @@ def test_network_agents_list_routers(self): *attrs) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_network_agents_list_routers_with_long_option(self): arglist = [ @@ -343,7 +343,7 @@ def test_network_agents_list_routers_with_long_option(self): router_agent_data = [d + ('',) for d in self.data] self.assertEqual(router_agent_columns, columns) - self.assertItemsEqual(router_agent_data, list(data)) + self.assertCountEqual(router_agent_data, list(data)) class TestRemoveNetworkFromAgent(TestNetworkAgent): @@ -571,4 +571,4 @@ def test_show_all_options(self): self.network.get_agent.assert_called_once_with( self._network_agent.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(list(self.data), list(data)) + self.assertCountEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 8c5158d7c8..5f2a12836a 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -153,7 +153,7 @@ def test_create_default_options(self): self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_full_options(self): arglist = [ @@ -211,7 +211,7 @@ def test_create_full_options(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_invalid_json_binding_profile(self): arglist = [ @@ -262,7 +262,7 @@ def test_create_json_binding_profile(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_security_group(self): secgroup = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -291,7 +291,7 @@ def test_create_with_security_group(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_port_with_dns_name(self): arglist = [ @@ -317,7 +317,7 @@ def test_create_port_with_dns_name(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_security_groups(self): sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -347,7 +347,7 @@ def test_create_with_security_groups(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_no_security_groups(self): arglist = [ @@ -373,7 +373,7 @@ def test_create_with_no_security_groups(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_no_fixed_ips(self): arglist = [ @@ -399,7 +399,7 @@ def test_create_with_no_fixed_ips(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_port_with_allowed_address_pair_ipaddr(self): pairs = [{'ip_address': '192.168.1.123'}, @@ -429,7 +429,7 @@ def test_create_port_with_allowed_address_pair_ipaddr(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_port_with_allowed_address_pair(self): pairs = [{'ip_address': '192.168.1.123', @@ -465,7 +465,7 @@ def test_create_port_with_allowed_address_pair(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_port_with_qos(self): qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() @@ -493,7 +493,7 @@ def test_create_port_with_qos(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_port_security_enabled(self): arglist = [ @@ -602,7 +602,7 @@ def _test_create_with_tag(self, add_tags=True, add_tags_in_post=True): self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True, add_tags_in_post=True) @@ -645,7 +645,7 @@ def _test_create_with_uplink_status_propagation(self, enable=True): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_uplink_status_propagation_enabled(self): self._test_create_with_uplink_status_propagation(enable=True) @@ -725,7 +725,7 @@ def _test_create_with_numa_affinity_policy(self, policy=None): self.network.create_port.assert_called_once_with(**create_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_numa_affinity_policy_required(self): self._test_create_with_numa_affinity_policy(policy='required') @@ -764,7 +764,7 @@ def test_create_with_device_profile(self): } self.network.create_port.assert_called_once_with(**create_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestDeletePort(TestPort): @@ -919,7 +919,7 @@ def test_port_list_no_options(self): self.network.ports.assert_called_once_with( fields=LIST_FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_router_opt(self): arglist = [ @@ -939,7 +939,7 @@ def test_port_list_router_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) @mock.patch.object(utils, 'find_resource') def test_port_list_with_server_option(self, mock_find): @@ -960,7 +960,7 @@ def test_port_list_with_server_option(self, mock_find): fields=LIST_FIELDS_TO_RETRIEVE) mock_find.assert_called_once_with(mock.ANY, 'fake-server-name') self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_device_id_opt(self): arglist = [ @@ -980,7 +980,7 @@ def test_port_list_device_id_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_device_owner_opt(self): arglist = [ @@ -1000,7 +1000,7 @@ def test_port_list_device_owner_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_all_opt(self): arglist = [ @@ -1029,7 +1029,7 @@ def test_port_list_all_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_mac_address_opt(self): arglist = [ @@ -1049,7 +1049,7 @@ def test_port_list_mac_address_opt(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_ip_address(self): ip_address = self._ports[0].fixed_ips[0]['ip_address'] @@ -1069,7 +1069,7 @@ def test_port_list_fixed_ip_opt_ip_address(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_ip_address_substr(self): ip_address_ss = self._ports[0].fixed_ips[0]['ip_address'][:-1] @@ -1089,7 +1089,7 @@ def test_port_list_fixed_ip_opt_ip_address_substr(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_fixed_ip_opt_subnet_id(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -1111,7 +1111,7 @@ def test_port_list_fixed_ip_opt_subnet_id(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_fixed_ip_opts(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -1137,7 +1137,7 @@ def test_port_list_fixed_ip_opts(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_fixed_ips(self): subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] @@ -1165,7 +1165,7 @@ def test_port_list_fixed_ips(self): 'fields': LIST_FIELDS_TO_RETRIEVE, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_list_port_with_long(self): arglist = [ @@ -1183,7 +1183,7 @@ def test_list_port_with_long(self): self.network.ports.assert_called_once_with( fields=LIST_FIELDS_TO_RETRIEVE + LIST_FIELDS_TO_RETRIEVE_LONG) self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_port_list_host(self): arglist = [ @@ -1202,7 +1202,7 @@ def test_port_list_host(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_project(self): project = identity_fakes.FakeProject.create_one_project() @@ -1224,7 +1224,7 @@ def test_port_list_project(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_project_domain(self): project = identity_fakes.FakeProject.create_one_project() @@ -1248,7 +1248,7 @@ def test_port_list_project_domain(self): self.network.ports.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_port_list_name(self): test_name = "fakename" @@ -1268,7 +1268,7 @@ def test_port_list_name(self): self.network.ports.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 = [ @@ -1294,7 +1294,7 @@ def test_list_with_tag_options(self): 'fields': LIST_FIELDS_TO_RETRIEVE} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestSetPort(TestPort): @@ -1894,7 +1894,7 @@ def test_show_all_options(self): self._port.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestUnsetPort(TestPort): diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 323c919828..0324674817 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -184,7 +184,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def _test_create_with_ha_options(self, option, ha): arglist = [ @@ -208,7 +208,7 @@ def _test_create_with_ha_options(self, option, ha): 'ha': ha, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_ha_option(self): self._test_create_with_ha_options('--ha', True) @@ -237,7 +237,7 @@ def _test_create_with_distributed_options(self, option, distributed): 'distributed': distributed, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_distributed_option(self): self._test_create_with_distributed_options('--distributed', True) @@ -268,7 +268,7 @@ def test_create_with_AZ_hints(self): }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self.new_router.name] @@ -301,7 +301,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -494,7 +494,7 @@ def test_router_list_no_options(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_router_list_no_ha_no_distributed(self): _routers = network_fakes.FakeRouter.create_routers({ @@ -531,7 +531,7 @@ def test_router_list_long(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_router_list_long_no_az(self): arglist = [ @@ -552,7 +552,7 @@ def test_router_list_long_no_az(self): self.network.routers.assert_called_once_with() self.assertEqual(self.columns_long_no_az, columns) - self.assertItemsEqual(self.data_long_no_az, list(data)) + self.assertCountEqual(self.data_long_no_az, list(data)) def test_list_name(self): test_name = "fakename" @@ -570,7 +570,7 @@ def test_list_name(self): **{'name': test_name} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_router_list_enable(self): arglist = [ @@ -587,7 +587,7 @@ def test_router_list_enable(self): **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_router_list_disable(self): arglist = [ @@ -605,7 +605,7 @@ def test_router_list_disable(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_router_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -623,7 +623,7 @@ def test_router_list_project(self): self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_router_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -643,7 +643,7 @@ def test_router_list_project_domain(self): self.network.routers.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_router_list_agents_no_args(self): arglist = [ @@ -671,7 +671,7 @@ def test_router_list_agents(self): self.network.agent_hosted_routers( *attrs) 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 = [ @@ -696,7 +696,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestRemovePortFromRouter(TestRouter): @@ -1403,7 +1403,7 @@ def test_show_all_options(self): 'device_id': self._router.id }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_show_no_ha_no_distributed(self): _router = network_fakes.FakeRouter.create_one_router({ diff --git a/openstackclient/tests/unit/network/v2/test_security_group_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_compute.py index 837c9b21af..4f1ddce590 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_compute.py @@ -88,7 +88,7 @@ def test_security_group_create_min_options(self, sg_mock): self._security_group['name'], ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_security_group_create_all_options(self, sg_mock): sg_mock.return_value = self._security_group @@ -109,7 +109,7 @@ def test_security_group_create_all_options(self, sg_mock): self._security_group['description'], ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) @mock.patch( @@ -255,7 +255,7 @@ def test_security_group_list_no_options(self, sg_mock): kwargs = {'search_opts': {'all_tenants': False}} sg_mock.assert_called_once_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_security_group_list_all_projects(self, sg_mock): sg_mock.return_value = self._security_groups @@ -272,7 +272,7 @@ def test_security_group_list_all_projects(self, sg_mock): kwargs = {'search_opts': {'all_tenants': True}} sg_mock.assert_called_once_with(**kwargs) self.assertEqual(self.columns_all_projects, columns) - self.assertItemsEqual(self.data_all_projects, list(data)) + self.assertCountEqual(self.data_all_projects, list(data)) @mock.patch( @@ -401,4 +401,4 @@ def test_security_group_show_all_options(self, sg_mock): sg_mock.assert_called_once_with(self._security_group['id']) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group_network.py index fe37778598..569c0cd5f0 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_network.py @@ -96,7 +96,7 @@ def test_create_min_options(self): 'name': self._security_group.name, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_all_options(self): arglist = [ @@ -124,7 +124,7 @@ def test_create_all_options(self): 'tenant_id': self.project.id, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [self._security_group.name] @@ -155,7 +155,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -293,7 +293,7 @@ def test_security_group_list_no_options(self): self.network.security_groups.assert_called_once_with( fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_security_group_list_all_projects(self): arglist = [ @@ -309,7 +309,7 @@ def test_security_group_list_all_projects(self): self.network.security_groups.assert_called_once_with( fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_security_group_list_project(self): project = identity_fakes.FakeProject.create_one_project() @@ -329,7 +329,7 @@ def test_security_group_list_project(self): self.network.security_groups.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_security_group_list_project_domain(self): project = identity_fakes.FakeProject.create_one_project() @@ -351,7 +351,7 @@ def test_security_group_list_project_domain(self): self.network.security_groups.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 = [ @@ -539,7 +539,7 @@ def test_show_all_options(self): self.network.find_security_group.assert_called_once_with( self._security_group.id, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestUnsetSecurityGroupNetwork(TestSecurityGroupNetwork): diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 6085cda8a1..5147b64d2a 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -255,7 +255,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_from_subnet_pool_options(self): # Mock SDK calls for this test. @@ -317,7 +317,7 @@ def test_create_from_subnet_pool_options(self): 'service_types': self._subnet_from_pool.service_types, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data_subnet_pool, data) + self.assertCountEqual(self.data_subnet_pool, data) def test_create_options_subnet_range_ipv6(self): # Mock SDK calls for this test. @@ -390,7 +390,7 @@ def test_create_options_subnet_range_ipv6(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data_ipv6, data) + self.assertCountEqual(self.data_ipv6, data) def test_create_with_network_segment(self): # Mock SDK calls for this test. @@ -424,7 +424,7 @@ def test_create_with_network_segment(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_description(self): # Mock SDK calls for this test. @@ -458,7 +458,7 @@ def test_create_with_description(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def _test_create_with_dns(self, publish_dns=True): arglist = [ @@ -490,7 +490,7 @@ def _test_create_with_dns(self, publish_dns=True): dns_publish_fixed_ip=publish_dns, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_dns(self): self._test_create_with_dns(publish_dns=True) @@ -535,7 +535,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -691,7 +691,7 @@ def test_subnet_list_no_options(self): self.network.subnets.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_subnet_list_long(self): arglist = [ @@ -706,7 +706,7 @@ def test_subnet_list_long(self): self.network.subnets.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_subnet_list_ip_version(self): arglist = [ @@ -722,7 +722,7 @@ def test_subnet_list_ip_version(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_dhcp(self): arglist = [ @@ -738,7 +738,7 @@ def test_subnet_list_dhcp(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_no_dhcp(self): arglist = [ @@ -754,7 +754,7 @@ def test_subnet_list_no_dhcp(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_service_type(self): arglist = [ @@ -769,7 +769,7 @@ def test_subnet_list_service_type(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_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -787,7 +787,7 @@ def test_subnet_list_project(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_service_type_multiple(self): arglist = [ @@ -805,7 +805,7 @@ def test_subnet_list_service_type_multiple(self): 'network:floatingip_agent_gateway']} 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_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -825,7 +825,7 @@ def test_subnet_list_project_domain(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_network(self): network = network_fakes.FakeNetwork.create_one_network() @@ -843,7 +843,7 @@ def test_subnet_list_network(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_gateway(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -861,7 +861,7 @@ def test_subnet_list_gateway(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_name(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -879,7 +879,7 @@ def test_subnet_list_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_subnet_range(self): subnet = network_fakes.FakeSubnet.create_one_subnet() @@ -897,7 +897,7 @@ def test_subnet_list_subnet_range(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 = [ @@ -1244,7 +1244,7 @@ def test_show_all_options(self): self._subnet.name, ignore_missing=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestUnsetSubnet(TestSubnet): diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 243fc76df4..4d18dc99b4 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -133,7 +133,7 @@ def test_create_default_options(self): }) self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_prefixlen_options(self): arglist = [ @@ -163,7 +163,7 @@ def test_create_prefixlen_options(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_len_negative(self): arglist = [ @@ -201,7 +201,7 @@ def test_create_project_domain(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_address_scope_option(self): arglist = [ @@ -224,7 +224,7 @@ def test_create_address_scope_option(self): 'name': self._subnet_pool.name, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_default_and_shared_options(self): arglist = [ @@ -250,7 +250,7 @@ def test_create_default_and_shared_options(self): 'shared': True, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_description(self): arglist = [ @@ -273,7 +273,7 @@ def test_create_with_description(self): 'description': self._subnet_pool.description, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_default_quota(self): arglist = [ @@ -294,7 +294,7 @@ def test_create_with_default_quota(self): 'default_quota': 10, }) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def _test_create_with_tag(self, add_tags=True): arglist = [ @@ -328,7 +328,7 @@ def _test_create_with_tag(self, add_tags=True): else: self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_create_with_tags(self): self._test_create_with_tag(add_tags=True) @@ -476,7 +476,7 @@ def test_subnet_pool_list_no_option(self): self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_subnet_pool_list_long(self): arglist = [ @@ -491,7 +491,7 @@ def test_subnet_pool_list_long(self): self.network.subnet_pools.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_subnet_pool_list_no_share(self): arglist = [ @@ -507,7 +507,7 @@ def test_subnet_pool_list_no_share(self): self.network.subnet_pools.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_pool_list_share(self): arglist = [ @@ -523,7 +523,7 @@ def test_subnet_pool_list_share(self): self.network.subnet_pools.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_pool_list_no_default(self): arglist = [ @@ -539,7 +539,7 @@ def test_subnet_pool_list_no_default(self): self.network.subnet_pools.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_pool_list_default(self): arglist = [ @@ -555,7 +555,7 @@ def test_subnet_pool_list_default(self): self.network.subnet_pools.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_pool_list_project(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -573,7 +573,7 @@ def test_subnet_pool_list_project(self): self.network.subnet_pools.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_pool_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() @@ -593,7 +593,7 @@ def test_subnet_pool_list_project_domain(self): self.network.subnet_pools.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_pool_list_name(self): subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() @@ -611,7 +611,7 @@ def test_subnet_pool_list_name(self): self.network.subnet_pools.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_pool_list_address_scope(self): addr_scope = network_fakes.FakeAddressScope.create_one_address_scope() @@ -629,7 +629,7 @@ def test_subnet_pool_list_address_scope(self): self.network.subnet_pools.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 = [ @@ -654,7 +654,7 @@ def test_list_with_tag_options(self): 'not_any_tags': 'black,white'} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestSetSubnetPool(TestSubnetPool): @@ -1008,7 +1008,7 @@ def test_show_all_options(self): ignore_missing=False ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestUnsetSubnetPool(TestSubnetPool): diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index 5500438bd6..15c20561e2 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -109,7 +109,7 @@ def test_qos_create_without_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_qos_create_with_consumer(self): arglist = [ @@ -129,7 +129,7 @@ def test_qos_create_with_consumer(self): {'consumer': self.new_qos_spec.consumer} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_qos_create_with_properties(self): arglist = [ @@ -155,7 +155,7 @@ def test_qos_create_with_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) class TestQosDelete(TestQos): @@ -350,7 +350,7 @@ def test_qos_list(self): self.qos_mock.list.assert_called_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_qos_list_no_association(self): self.qos_mock.reset_mock() @@ -377,7 +377,7 @@ def test_qos_list_no_association(self): format_columns.ListColumn(None), format_columns.DictColumn(self.qos_specs[1].specs), ) - self.assertItemsEqual(ex_data, list(data)) + self.assertCountEqual(ex_data, list(data)) class TestQosSet(TestQos): @@ -454,7 +454,7 @@ def test_qos_show(self): self.qos_spec.name, format_columns.DictColumn(self.qos_spec.specs), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) class TestQosUnset(TestQos): diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index f1d469140b..be47f5db59 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -78,7 +78,7 @@ def test_type_create(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_type_create_with_encryption(self): encryption_info = { @@ -139,7 +139,7 @@ def test_type_create_with_encryption(self): body, ) self.assertEqual(encryption_columns, columns) - self.assertItemsEqual(encryption_data, data) + self.assertCountEqual(encryption_data, data) class TestTypeDelete(TestType): @@ -270,7 +270,7 @@ def test_type_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = [ @@ -284,7 +284,7 @@ def test_type_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with() self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_type_list_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type( @@ -328,7 +328,7 @@ def test_type_list_with_encryption(self): self.encryption_types_mock.list.assert_called_once_with() self.types_mock.list.assert_called_once_with() self.assertEqual(encryption_columns, columns) - self.assertItemsEqual(encryption_data, list(data)) + self.assertCountEqual(encryption_data, list(data)) class TestTypeSet(TestType): @@ -469,7 +469,7 @@ def test_type_show(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_type_show_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type() @@ -513,7 +513,7 @@ def test_type_show_with_encryption(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.encryption_types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(encryption_columns, columns) - self.assertItemsEqual(encryption_data, data) + self.assertCountEqual(encryption_data, data) class TestTypeUnset(TestType): diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 704a66da71..de0c99c291 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -135,7 +135,7 @@ def test_volume_create_min_options(self): None, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_options(self): arglist = [ @@ -179,7 +179,7 @@ def test_volume_create_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_user_project_id(self): # Return a project @@ -226,7 +226,7 @@ def test_volume_create_user_project_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_user_project_name(self): # Return a project @@ -273,7 +273,7 @@ def test_volume_create_user_project_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -314,7 +314,7 @@ def test_volume_create_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() @@ -357,7 +357,7 @@ def test_volume_create_image_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() @@ -400,7 +400,7 @@ def test_volume_create_image_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_with_source(self): self.volumes_mock.get.return_value = self.new_volume @@ -430,7 +430,7 @@ def test_volume_create_with_source(self): None, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_with_bootable_and_readonly(self): arglist = [ @@ -468,7 +468,7 @@ def test_volume_create_with_bootable_and_readonly(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -510,7 +510,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, False) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -562,7 +562,7 @@ def test_volume_create_with_bootable_and_readonly_fail( self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -765,7 +765,7 @@ def test_volume_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_volume_list_name(self): arglist = [ @@ -782,7 +782,7 @@ def test_volume_list_name(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_volume_list_status(self): arglist = [ @@ -799,7 +799,7 @@ def test_volume_list_status(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_volume_list_all_projects(self): arglist = [ @@ -816,7 +816,7 @@ def test_volume_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, tuple(columns)) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_volume_list_long(self): arglist = [ @@ -856,7 +856,7 @@ def test_volume_list_long(self): volume.AttachmentsColumn(self._volume.attachments), format_columns.DictColumn(self._volume.metadata), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_with_limit(self): arglist = [ @@ -881,7 +881,7 @@ def test_volume_list_with_limit(self): 'all_tenants': False, } ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, tuple(data)) + self.assertCountEqual(self.datalist, tuple(data)) def test_volume_list_negative_limit(self): arglist = [ @@ -1272,7 +1272,7 @@ def test_volume_show(self): self.volumes_mock.get.assert_called_with(self._volume.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_show_backward_compatibility(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v1/test_volume_backup.py b/openstackclient/tests/unit/volume/v1/test_volume_backup.py index a713155092..f25a5ffa6d 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_volume_backup.py @@ -100,7 +100,7 @@ def test_backup_create(self): self.new_backup.description, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_backup_create_without_name(self): arglist = [ @@ -124,7 +124,7 @@ def test_backup_create_without_name(self): self.new_backup.description, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestBackupDelete(TestBackup): @@ -277,7 +277,7 @@ def test_backup_list_without_options(self): search_opts=search_opts, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = [ @@ -309,7 +309,7 @@ def test_backup_list_with_options(self): search_opts=search_opts, ) self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): @@ -391,4 +391,4 @@ def test_backup_show(self): self.backups_mock.get.assert_called_with(self.backup.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index 6bb6c02978..7fd5187170 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -251,7 +251,7 @@ def test_consistency_group_create_without_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_consistency_group_create_from_source(self): arglist = [ @@ -279,7 +279,7 @@ def test_consistency_group_create_from_source(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_consistency_group_create_from_snapshot(self): arglist = [ @@ -307,7 +307,7 @@ def test_consistency_group_create_from_snapshot(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestConsistencyGroupDelete(TestConsistencyGroup): @@ -463,7 +463,7 @@ def test_consistency_group_list_without_options(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_consistency_group_list_with_all_project(self): arglist = [ @@ -480,7 +480,7 @@ def test_consistency_group_list_with_all_project(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': True}) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_consistency_group_list_with_long(self): arglist = [ @@ -497,7 +497,7 @@ def test_consistency_group_list_with_long(self): self.consistencygroups_mock.list.assert_called_once_with( detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): @@ -705,4 +705,4 @@ def test_consistency_group_show(self): self.consistencygroups_mock.get.assert_called_once_with( self.consistency_group.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index bc4cee8b40..8f8d26c864 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -112,7 +112,7 @@ def test_qos_create_without_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_qos_create_with_consumer(self): arglist = [ @@ -133,7 +133,7 @@ def test_qos_create_with_consumer(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_qos_create_with_properties(self): arglist = [ @@ -159,7 +159,7 @@ def test_qos_create_with_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestQosDelete(TestQos): @@ -342,7 +342,7 @@ def test_qos_list(self): self.qos_mock.list.assert_called_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_qos_list_no_association(self): self.qos_mock.reset_mock() @@ -369,7 +369,7 @@ def test_qos_list_no_association(self): format_columns.ListColumn(None), format_columns.DictColumn(self.qos_specs[1].specs), ) - self.assertItemsEqual(ex_data, list(data)) + self.assertCountEqual(ex_data, list(data)) class TestQosSet(TestQos): @@ -449,7 +449,7 @@ def test_qos_show(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, tuple(data)) + self.assertCountEqual(self.data, tuple(data)) class TestQosUnset(TestQos): diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 000464c5d8..f718f4c4b8 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -93,7 +93,7 @@ def test_type_create_public(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_type_create_private(self): arglist = [ @@ -119,7 +119,7 @@ def test_type_create_private(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_public_type_create_with_project(self): arglist = [ @@ -196,7 +196,7 @@ def test_type_create_with_encryption(self): body, ) self.assertEqual(encryption_columns, columns) - self.assertItemsEqual(encryption_data, data) + self.assertCountEqual(encryption_data, data) class TestTypeDelete(TestType): @@ -330,7 +330,7 @@ def test_type_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=None) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_type_list_with_options(self): arglist = [ @@ -348,7 +348,7 @@ def test_type_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=True) self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) def test_type_list_with_private_option(self): arglist = [ @@ -365,7 +365,7 @@ def test_type_list_with_private_option(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.list.assert_called_once_with(is_public=False) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_type_list_with_default_option(self): arglist = [ @@ -383,7 +383,7 @@ def test_type_list_with_default_option(self): columns, data = self.cmd.take_action(parsed_args) self.types_mock.default.assert_called_once_with() self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data_with_default_type, list(data)) + self.assertCountEqual(self.data_with_default_type, list(data)) def test_type_list_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type( @@ -427,7 +427,7 @@ def test_type_list_with_encryption(self): self.encryption_types_mock.list.assert_called_once_with() self.types_mock.list.assert_called_once_with(is_public=None) self.assertEqual(encryption_columns, columns) - self.assertItemsEqual(encryption_data, list(data)) + self.assertCountEqual(encryption_data, list(data)) class TestTypeSet(TestType): @@ -713,7 +713,7 @@ def test_type_show(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) def test_type_show_with_access(self): arglist = [ @@ -746,7 +746,7 @@ def test_type_show_with_access(self): private_type.name, format_columns.DictColumn(private_type.extra_specs) ) - self.assertItemsEqual(private_type_data, data) + self.assertCountEqual(private_type_data, data) def test_type_show_with_list_access_exec(self): arglist = [ @@ -778,7 +778,7 @@ def test_type_show_with_list_access_exec(self): private_type.name, format_columns.DictColumn(private_type.extra_specs) ) - self.assertItemsEqual(private_type_data, data) + self.assertCountEqual(private_type_data, data) def test_type_show_with_encryption(self): encryption_type = volume_fakes.FakeType.create_one_encryption_type() @@ -824,7 +824,7 @@ def test_type_show_with_encryption(self): self.types_mock.get.assert_called_with(self.volume_type.id) self.encryption_types_mock.get.assert_called_with(self.volume_type.id) self.assertEqual(encryption_columns, columns) - self.assertItemsEqual(encryption_data, data) + self.assertCountEqual(encryption_data, data) class TestTypeUnset(TestType): diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index b9fe4e834c..31ba6036c4 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -136,7 +136,7 @@ def test_volume_create_min_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_options(self): consistency_group = ( @@ -182,7 +182,7 @@ def test_volume_create_options(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -218,7 +218,7 @@ def test_volume_create_properties(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.FakeImage.create_one_image() @@ -256,7 +256,7 @@ def test_volume_create_image_id(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.FakeImage.create_one_image() @@ -294,7 +294,7 @@ def test_volume_create_image_name(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_with_snapshot(self): snapshot = volume_fakes.FakeSnapshot.create_one_snapshot() @@ -331,7 +331,7 @@ def test_volume_create_with_snapshot(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) def test_volume_create_with_bootable_and_readonly(self): arglist = [ @@ -369,7 +369,7 @@ def test_volume_create_with_bootable_and_readonly(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -411,7 +411,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self): ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, False) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -463,7 +463,7 @@ def test_volume_create_with_bootable_and_readonly_fail( self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.datalist, data) + self.assertCountEqual(self.datalist, data) self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, True) self.volumes_mock.update_readonly_flag.assert_called_with( @@ -680,7 +680,7 @@ def test_volume_list_no_options(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_project(self): arglist = [ @@ -720,7 +720,7 @@ def test_volume_list_project(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_project_domain(self): arglist = [ @@ -762,7 +762,7 @@ def test_volume_list_project_domain(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_user(self): arglist = [ @@ -801,7 +801,7 @@ def test_volume_list_user(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_user_domain(self): arglist = [ @@ -843,7 +843,7 @@ def test_volume_list_user_domain(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_name(self): arglist = [ @@ -883,7 +883,7 @@ def test_volume_list_name(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_status(self): arglist = [ @@ -923,7 +923,7 @@ def test_volume_list_status(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_all_projects(self): arglist = [ @@ -963,7 +963,7 @@ def test_volume_list_all_projects(self): self.mock_volume.size, volume.AttachmentsColumn(self.mock_volume.attachments), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_long(self): arglist = [ @@ -1017,7 +1017,7 @@ def test_volume_list_long(self): volume.AttachmentsColumn(self.mock_volume.attachments), format_columns.DictColumn(self.mock_volume.metadata), ), ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_with_marker_and_limit(self): arglist = [ @@ -1056,7 +1056,7 @@ def test_volume_list_with_marker_and_limit(self): 'name': None, 'all_tenants': False, } ) - self.assertItemsEqual(datalist, tuple(data)) + self.assertCountEqual(datalist, tuple(data)) def test_volume_list_negative_limit(self): arglist = [ @@ -1450,7 +1450,7 @@ def test_volume_show(self): volume_fakes.FakeVolume.get_volume_columns(self._volume), columns) - self.assertItemsEqual( + self.assertCountEqual( volume_fakes.FakeVolume.get_volume_data(self._volume), data) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index 13513ed8ad..97f64ce7ad 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -314,7 +314,7 @@ def test_backup_list_without_options(self): limit=None, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = [ @@ -353,7 +353,7 @@ def test_backup_list_with_options(self): limit=3, ) self.assertEqual(self.columns_long, columns) - self.assertItemsEqual(self.data_long, list(data)) + self.assertCountEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): diff --git a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py index 3830f458d6..33a5a98a35 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py @@ -707,7 +707,7 @@ def test_snapshot_show(self): self.snapshots_mock.get.assert_called_with(self.snapshot.id) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, data) + self.assertCountEqual(self.data, data) class TestVolumeSnapshotUnset(TestVolumeSnapshot): From 3918622968073738e8fa17eec8bf5512ed609af9 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Wed, 5 May 2021 01:26:51 +0200 Subject: [PATCH 0394/1120] openstack image create: honor protection/visibility flags The --protected, --unprotected, --public, --shared, --community, --private flags were ignored when using --volume. Change-Id: Id5c05ef7d7bb0a04b9d7a9d821e544e1ff7b3d28 Story: 2008882 --- openstackclient/image/v2/image.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 644fbbb4f7..c1f46d2d99 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -490,6 +490,8 @@ def take_action(self, parsed_args): parsed_args.name, parsed_args.container_format, parsed_args.disk_format, + visibility=kwargs.get('visibility', 'private'), + protected=True if parsed_args.protected else False ) info = body['os-volume_upload_image'] try: From 1f0fcbcd1d480be8c097a5a663dd97b0bdcac2bc Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Tue, 11 May 2021 11:22:59 +0000 Subject: [PATCH 0395/1120] Fix the functional-tips tox environment The egg for the keystoneauth project is actually called keystonauth1. Seems newer pip actually complains about the difference and fails. Change-Id: I1602832d33cd467745a03b36c9b1545cd069ba1d --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ebcdc2d5d9..5f9bf73b20 100644 --- a/tox.ini +++ b/tox.ini @@ -84,7 +84,7 @@ setenv = OS_TEST_PATH=./openstackclient/tests/functional passenv = OS_* commands = python -m pip install -q -U -e "git+file://{toxinidir}/../cliff#egg=cliff" - python -m pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth" + python -m pip install -q -U -e "git+file://{toxinidir}/../keystoneauth#egg=keystoneauth1" python -m pip install -q -U -e "git+file://{toxinidir}/../osc-lib#egg=osc_lib" python -m pip install -q -U -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" python -m pip freeze From 05807ee0dba1eb4d87943817a0efd234278589eb Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Thu, 13 May 2021 22:37:38 +0200 Subject: [PATCH 0396/1120] Set ML2/OVS backend explicitly in the devstack jobs Neutron team recently switched default backend used in Neutron by Devstack to OVN. With that backend some tests, like e.g. related to DHCP or L3 agents aren't working fine. So to have still the same test coverage as we had before, let's explicitly set ML2/OVS as a Neutron's backend in those CI jobs. Change-Id: Idf6466a59c6cf96be2f1d53e696f0564584fa233 --- .zuul.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 404c005a41..e7b01da409 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -88,11 +88,28 @@ # NOTE(amotoki): Some neutron features are enabled by devstack plugin neutron: https://opendev.org/openstack/neutron devstack_services: + # Disable OVN services + br-ex-tcpdump: false + br-int-flows: false + ovn-controller: false + ovn-northd: false + ovs-vswitchd: false + ovsdb-server: false + q-ovn-metadata-agent: false + # Neutron services + q-agt: true + q-dhcp: true + q-l3: true + q-meta: true neutron-network-segment-range: true neutron-segments: true q-metering: true q-qos: true neutron-tag-ports-during-bulk-creation: true + devstack_localrc: + Q_AGENT: openvswitch + Q_ML2_TENANT_NETWORK_TYPE: vxlan + Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch tox_envlist: functional - job: From b019a561876b51523021654f3b499c962ce54fb6 Mon Sep 17 00:00:00 2001 From: Brian Rosmaita Date: Thu, 20 May 2021 09:18:26 -0400 Subject: [PATCH 0397/1120] Add check for cinderclient.v2 support Block Storage API v2 support is being removed from the cinderclient during the Xena development cycle [0], so add a check to determine whether the available cinderclient has v2 support. [0] https://wiki.openstack.org/wiki/CinderXenaPTGSummary#Removing_the_Block_Storage_API_v2 Change-Id: Id54da1704d94526071f500c36a6e38d6d84aa7b8 --- openstackclient/volume/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 64e8b9f34d..37cb41685c 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -42,11 +42,15 @@ def make_client(instance): from cinderclient.v3 import volume_snapshots from cinderclient.v3 import volumes - # Try a small import to check if cinderclient v1 is supported + # Check whether the available cinderclient supports v1 or v2 try: from cinderclient.v1 import services # noqa except Exception: del API_VERSIONS['1'] + try: + from cinderclient.v2 import services # noqa + except Exception: + del API_VERSIONS['2'] version = instance._api_version[API_NAME] from cinderclient import api_versions From b1a41904c367872727d3daf4caf58756ae07c417 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 20 May 2021 16:58:57 +0100 Subject: [PATCH 0398/1120] compute: Update 'server resize --revert', '--confirm' help Update the help strings for these two arguments to indicate their deprecated nature. This was previously flagged via a deprecation warning but users would only see that if they were to run the command. Change-Id: I31a5e27ac8bd2625a6073b54a51bf3e8d6126c8c Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b81d2a181c..468c6b1b3e 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3769,12 +3769,20 @@ def get_parser(self, prog_name): phase_group.add_argument( '--confirm', action="store_true", - help=_('Confirm server resize is complete'), + help=_( + "**Deprecated** Confirm server resize is complete. " + "Replaced by the 'openstack server resize confirm' and " + "'openstack server migration confirm' commands" + ), ) phase_group.add_argument( '--revert', action="store_true", - help=_('Restore server state before resize'), + help=_( + '**Deprecated** Restore server state before resize' + "Replaced by the 'openstack server resize revert' and " + "'openstack server migration revert' commands" + ), ) parser.add_argument( '--wait', From b26b7f3440d4f756c0b7906b93751d7e83a733f7 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Tue, 22 Dec 2020 15:31:44 +0100 Subject: [PATCH 0399/1120] Allow to send extra attributes in Neutron related commands To deprecate and drop support for neutronclient CLI and use only OSC we need feature parity between OSC and neutronclient. Last missing piece here is possibility to send in POST/PUT requests unknown parameters to the Neutron server. This patch adds such possibility to the OSC. Change-Id: Iba09297c2be9fb9fa0be1b3dc65755277b79230e --- lower-constraints.txt | 2 +- openstackclient/network/common.py | 74 +++++++++- openstackclient/network/utils.py | 42 ++++++ openstackclient/network/v2/address_group.py | 11 +- openstackclient/network/v2/address_scope.py | 9 +- openstackclient/network/v2/floating_ip.py | 15 +- .../network/v2/floating_ip_port_forwarding.py | 12 +- openstackclient/network/v2/network.py | 17 ++- openstackclient/network/v2/network_flavor.py | 9 +- .../network/v2/network_flavor_profile.py | 10 +- openstackclient/network/v2/network_meter.py | 5 +- .../network/v2/network_meter_rule.py | 5 +- .../network/v2/network_qos_policy.py | 10 +- .../network/v2/network_qos_rule.py | 10 +- openstackclient/network/v2/network_rbac.py | 9 +- openstackclient/network/v2/network_segment.py | 10 +- .../network/v2/network_segment_range.py | 12 +- openstackclient/network/v2/port.py | 15 +- openstackclient/network/v2/router.py | 18 ++- openstackclient/network/v2/security_group.py | 10 +- .../network/v2/security_group_rule.py | 6 +- openstackclient/network/v2/subnet.py | 14 +- openstackclient/network/v2/subnet_pool.py | 10 +- .../tests/unit/network/test_common.py | 139 ++++++++++++++++++ .../tests/unit/network/test_utils.py | 59 ++++++++ requirements.txt | 2 +- 26 files changed, 486 insertions(+), 49 deletions(-) create mode 100644 openstackclient/tests/unit/network/test_utils.py diff --git a/lower-constraints.txt b/lower-constraints.txt index 09aabede1f..861749b738 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -38,7 +38,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.53.0 +openstacksdk==0.56.0 os-client-config==2.1.0 os-service-types==1.7.0 osc-lib==2.3.0 diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index 47ffbe77a6..b1902a6c9c 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -16,10 +16,12 @@ import logging import openstack.exceptions +from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from openstackclient.i18n import _ +from openstackclient.network import utils LOG = logging.getLogger(__name__) @@ -75,7 +77,6 @@ def _network_type(self): """ # Have we set it up yet for this command? if not hasattr(self, '_net_type'): - # import pdb; pdb.set_trace() try: if self.app.client_manager.is_network_endpoint_enabled(): net_type = _NET_TYPE_NEUTRON @@ -255,3 +256,74 @@ def take_action(self, parsed_args): if exc.details: msg += ", " + str(exc.details) raise exceptions.CommandError(msg) + + +class NeutronCommandWithExtraArgs(command.Command): + """Create and Update commands with additional extra properties. + + Extra properties can be passed to the command and are then send to the + Neutron as given to the command. + """ + + # dict of allowed types + _allowed_types_dict = { + 'bool': utils.str2bool, + 'dict': utils.str2dict, + 'list': utils.str2list, + 'int': int, + 'str': str, + } + + def _get_property_converter(self, _property): + if 'type' not in _property: + converter = str + else: + converter = self._allowed_types_dict.get(_property['type']) + + if not converter: + raise exceptions.CommandError( + _("Type {property_type} of property {name} " + "is not supported").format( + property_type=_property['type'], + name=_property['name'])) + return converter + + def _parse_extra_properties(self, extra_properties): + result = {} + if extra_properties: + for _property in extra_properties: + converter = self._get_property_converter(_property) + result[_property['name']] = converter(_property['value']) + return result + + def get_parser(self, prog_name): + parser = super(NeutronCommandWithExtraArgs, self).get_parser(prog_name) + parser.add_argument( + '--extra-property', + metavar='type=,name=,' + 'value=', + dest='extra_properties', + action=parseractions.MultiKeyValueAction, + required_keys=['name', 'value'], + optional_keys=['type'], + help=_("Additional parameters can be passed using this property. " + "Default type of the extra property is string ('str'), but " + "other types can be used as well. Available types are: " + "'dict', 'list', 'str', 'bool', 'int'. " + "In case of 'list' type, 'value' can be " + "semicolon-separated list of values. " + "For 'dict' value is semicolon-separated list of the " + "key:value pairs.") + ) + return parser + + +class NeutronUnsetCommandWithExtraArgs(NeutronCommandWithExtraArgs): + + def _parse_extra_properties(self, extra_properties): + result = {} + if extra_properties: + for _property in extra_properties: + result[_property['name']] = None + + return result diff --git a/openstackclient/network/utils.py b/openstackclient/network/utils.py index 287f027163..4d4d18e470 100644 --- a/openstackclient/network/utils.py +++ b/openstackclient/network/utils.py @@ -11,6 +11,10 @@ # under the License. # +from osc_lib import exceptions + +from openstackclient.i18n import _ + # Transform compute security group rule for display. def transform_compute_security_group_rule(sg_rule): @@ -39,3 +43,41 @@ def transform_compute_security_group_rule(sg_rule): else: info['remote_security_group'] = '' return info + + +def str2bool(strbool): + if strbool is None: + return None + return strbool.lower() == 'true' + + +def str2list(strlist): + result = [] + if strlist: + result = strlist.split(';') + return result + + +def str2dict(strdict): + """Convert key1:value1;key2:value2;... string into dictionary. + + :param strdict: string in the form of key1:value1;key2:value2 + """ + result = {} + if not strdict: + return result + i = 0 + kvlist = [] + for kv in strdict.split(';'): + if ':' in kv: + kvlist.append(kv) + i += 1 + elif i == 0: + msg = _("missing value for key '%s'") + raise exceptions.CommandError(msg % kv) + else: + kvlist[i - 1] = "%s;%s" % (kvlist[i - 1], kv) + for kv in kvlist: + key, sep, value = kv.partition(':') + result[key] = value + return result diff --git a/openstackclient/network/v2/address_group.py b/openstackclient/network/v2/address_group.py index c5b2f12606..fc83470053 100644 --- a/openstackclient/network/v2/address_group.py +++ b/openstackclient/network/v2/address_group.py @@ -22,6 +22,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -57,7 +58,7 @@ def _get_attrs(client_manager, parsed_args): return attrs -class CreateAddressGroup(command.ShowOne): +class CreateAddressGroup(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new Address Group") def get_parser(self, prog_name): @@ -93,6 +94,9 @@ def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + obj = client.create_address_group(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) @@ -191,7 +195,7 @@ def take_action(self, parsed_args): ) for s in data)) -class SetAddressGroup(command.Command): +class SetAddressGroup(common.NeutronCommandWithExtraArgs): _description = _("Set address group properties") def get_parser(self, prog_name): @@ -231,6 +235,9 @@ def take_action(self, parsed_args): attrs['name'] = parsed_args.name if parsed_args.description is not None: attrs['description'] = parsed_args.description + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_address_group(obj, **attrs) if parsed_args.address: diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index 71c1a9afb7..cd27678ee9 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -21,6 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -57,7 +58,7 @@ def _get_attrs(client_manager, parsed_args): # TODO(rtheis): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateAddressScope(command.ShowOne): +class CreateAddressScope(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new Address Scope") def get_parser(self, prog_name): @@ -98,6 +99,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_address_scope(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) @@ -226,7 +229,7 @@ def take_action(self, parsed_args): # TODO(rtheis): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetAddressScope(command.Command): +class SetAddressScope(common.NeutronCommandWithExtraArgs): _description = _("Set address scope properties") def get_parser(self, prog_name): @@ -267,6 +270,8 @@ def take_action(self, parsed_args): attrs['shared'] = True if parsed_args.no_share: attrs['shared'] = False + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) client.update_address_scope(obj, **attrs) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index a2765cd1ef..25b2a1baf6 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -13,7 +13,6 @@ """IP Floating action implementations""" -from osc_lib.command import command from osc_lib import utils from osc_lib.utils import tags as _tag @@ -94,7 +93,8 @@ def _get_attrs(client_manager, parsed_args): return attrs -class CreateFloatingIP(common.NetworkAndComputeShowOne): +class CreateFloatingIP(common.NetworkAndComputeShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create floating IP") def update_parser_common(self, parser): @@ -175,6 +175,8 @@ def update_parser_network(self, parser): def take_action_network(self, client, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) with common.check_missing_extension_if_error( self.app.client_manager.network, attrs): obj = client.create_ip(**attrs) @@ -390,7 +392,7 @@ def take_action_compute(self, client, parsed_args): ) for s in data)) -class SetFloatingIP(command.Command): +class SetFloatingIP(common.NeutronCommandWithExtraArgs): _description = _("Set floating IP Properties") def get_parser(self, prog_name): @@ -456,6 +458,9 @@ def take_action(self, parsed_args): if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: attrs['qos_policy_id'] = None + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_ip(obj, **attrs) @@ -490,7 +495,7 @@ def take_action_compute(self, client, parsed_args): return (columns, data) -class UnsetFloatingIP(command.Command): +class UnsetFloatingIP(common.NeutronCommandWithExtraArgs): _description = _("Unset floating IP Properties") def get_parser(self, prog_name): @@ -526,6 +531,8 @@ def take_action(self, parsed_args): attrs['port_id'] = None if parsed_args.qos_policy: attrs['qos_policy_id'] = None + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) if attrs: client.update_ip(obj, **attrs) diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py index 06b3df8bcd..71b0b7da63 100644 --- a/openstackclient/network/v2/floating_ip_port_forwarding.py +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -19,6 +19,7 @@ from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -32,7 +33,8 @@ def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) -class CreateFloatingIPPortForwarding(command.ShowOne): +class CreateFloatingIPPortForwarding(command.ShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create floating IP port forwarding") def get_parser(self, prog_name): @@ -122,6 +124,9 @@ def take_action(self, parsed_args): if parsed_args.description is not None: attrs['description'] = parsed_args.description + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + obj = client.create_floating_ip_port_forwarding( floating_ip.id, **attrs @@ -258,7 +263,7 @@ def take_action(self, parsed_args): ) for s in data)) -class SetFloatingIPPortForwarding(command.Command): +class SetFloatingIPPortForwarding(common.NeutronCommandWithExtraArgs): _description = _("Set floating IP Port Forwarding Properties") def get_parser(self, prog_name): @@ -352,6 +357,9 @@ def take_action(self, parsed_args): if parsed_args.description is not None: attrs['description'] = parsed_args.description + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + client.update_floating_ip_port_forwarding( floating_ip.id, parsed_args.port_forwarding_id, **attrs) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 7a12d523e2..b8eb9f014b 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -15,7 +15,6 @@ from cliff import columns as cliff_columns from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import utils from osc_lib.utils import tags as _tag @@ -189,7 +188,8 @@ def _add_additional_network_options(parser): # TODO(sindhu): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateNetwork(common.NetworkAndComputeShowOne): +class CreateNetwork(common.NetworkAndComputeShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create new network") def update_parser_common(self, parser): @@ -334,6 +334,8 @@ def take_action_network(self, client, parsed_args): attrs['vlan_transparent'] = True if parsed_args.no_transparent_vlan: attrs['vlan_transparent'] = False + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) with common.check_missing_extension_if_error( self.app.client_manager.network, attrs): obj = client.create_network(**attrs) @@ -623,7 +625,7 @@ def take_action_compute(self, client, parsed_args): # TODO(sindhu): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetNetwork(command.Command): +class SetNetwork(common.NeutronCommandWithExtraArgs): _description = _("Set network properties") def get_parser(self, prog_name): @@ -728,6 +730,8 @@ def take_action(self, parsed_args): obj = client.find_network(parsed_args.network, ignore_missing=False) attrs = _get_attrs_network(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) if attrs: with common.check_missing_extension_if_error( self.app.client_manager.network, attrs): @@ -761,7 +765,7 @@ def take_action_compute(self, client, parsed_args): return (display_columns, data) -class UnsetNetwork(command.Command): +class UnsetNetwork(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset network properties") def get_parser(self, prog_name): @@ -778,8 +782,9 @@ def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_network(parsed_args.network, ignore_missing=False) - # NOTE: As of now, UnsetNetwork has no attributes which need - # to be updated by update_network(). + attrs = self._parse_extra_properties(parsed_args.extra_properties) + if attrs: + client.update_network(obj, **attrs) # tags is a subresource and it needs to be updated separately. _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py index c9d368bfc1..9e758ae29c 100644 --- a/openstackclient/network/v2/network_flavor.py +++ b/openstackclient/network/v2/network_flavor.py @@ -21,6 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -88,7 +89,7 @@ def take_action(self, parsed_args): # TODO(dasanind): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateNetworkFlavor(command.ShowOne): +class CreateNetworkFlavor(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create new network flavor") def get_parser(self, prog_name): @@ -134,6 +135,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_flavor(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) @@ -234,7 +237,7 @@ def take_action(self, parsed_args): # TODO(dasanind): Use only the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetNetworkFlavor(command.Command): +class SetNetworkFlavor(common.NeutronCommandWithExtraArgs): _description = _("Set network flavor properties") def get_parser(self, prog_name): @@ -281,6 +284,8 @@ def take_action(self, parsed_args): attrs['enabled'] = True if parsed_args.disable: attrs['enabled'] = False + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) client.update_flavor(obj, **attrs) diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py index 6cf0c4124d..0212e0d9ba 100644 --- a/openstackclient/network/v2/network_flavor_profile.py +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -19,6 +19,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -60,7 +61,8 @@ def _get_attrs(client_manager, parsed_args): # TODO(ndahiwade): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateNetworkFlavorProfile(command.ShowOne): +class CreateNetworkFlavorProfile(command.ShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create new network flavor profile") def get_parser(self, prog_name): @@ -103,6 +105,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) if parsed_args.driver is None and parsed_args.metainfo is None: msg = _("Either --driver or --metainfo or both are required") @@ -180,7 +184,7 @@ def take_action(self, parsed_args): # TODO(ndahiwade): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetNetworkFlavorProfile(command.Command): +class SetNetworkFlavorProfile(common.NeutronCommandWithExtraArgs): _description = _("Set network flavor profile properties") def get_parser(self, prog_name): @@ -225,6 +229,8 @@ def take_action(self, parsed_args): obj = client.find_service_profile(parsed_args.flavor_profile, ignore_missing=False) attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) client.update_service_profile(obj, **attrs) diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py index df0e1da119..f8f188a806 100644 --- a/openstackclient/network/v2/network_meter.py +++ b/openstackclient/network/v2/network_meter.py @@ -21,6 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -59,7 +60,7 @@ def _get_attrs(client_manager, parsed_args): # TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateMeter(command.ShowOne): +class CreateMeter(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create network meter") def get_parser(self, prog_name): @@ -100,6 +101,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_metering_label(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index 1cf0395f44..06362fa14c 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -21,6 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -64,7 +65,7 @@ def _get_attrs(client_manager, parsed_args): return attrs -class CreateMeterRule(command.ShowOne): +class CreateMeterRule(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new meter rule") def get_parser(self, prog_name): @@ -130,6 +131,8 @@ def take_action(self, parsed_args): ignore_missing=False) parsed_args.meter = _meter.id attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_metering_label_rule(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index fd5ff93771..7300a5c0ac 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -21,6 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -67,7 +68,8 @@ def _get_attrs(client_manager, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateNetworkQosPolicy(command.ShowOne): +class CreateNetworkQosPolicy(command.ShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create a QoS policy") def get_parser(self, prog_name): @@ -117,6 +119,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_qos_policy(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) @@ -209,7 +213,7 @@ def take_action(self, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetNetworkQosPolicy(command.Command): +class SetNetworkQosPolicy(common.NeutronCommandWithExtraArgs): _description = _("Set QoS policy properties") def get_parser(self, prog_name): @@ -259,6 +263,8 @@ def take_action(self, parsed_args): parsed_args.policy, ignore_missing=False) attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) client.update_qos_policy(obj, **attrs) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index 2e4b385d2e..f30a5aeb1f 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -20,6 +20,7 @@ from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -171,7 +172,8 @@ def _add_rule_arguments(parser): ) -class CreateNetworkQosRule(command.ShowOne): +class CreateNetworkQosRule(command.ShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create new Network QoS rule") def get_parser(self, prog_name): @@ -198,6 +200,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): network_client = self.app.client_manager.network attrs = _get_attrs(network_client, parsed_args, is_create=True) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) try: obj = _rule_action_call( network_client, ACTION_CREATE, parsed_args.type)( @@ -285,7 +289,7 @@ def take_action(self, parsed_args): (_get_item_properties(s, columns) for s in data)) -class SetNetworkQosRule(command.Command): +class SetNetworkQosRule(common.NeutronCommandWithExtraArgs): _description = _("Set Network QoS rule properties") def get_parser(self, prog_name): @@ -312,6 +316,8 @@ def take_action(self, parsed_args): if not rule_type: raise Exception('Rule not found') attrs = _get_attrs(network_client, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) qos_id = attrs.pop('qos_policy_id') qos_rule = _rule_action_call(network_client, ACTION_FIND, rule_type)(attrs.pop('id'), qos_id) diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 4984e89d56..692a43857d 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -21,6 +21,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -90,7 +91,7 @@ def _get_attrs(client_manager, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateNetworkRBAC(command.ShowOne): +class CreateNetworkRBAC(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create network RBAC policy") def get_parser(self, prog_name): @@ -150,6 +151,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_rbac_policy(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) @@ -253,7 +256,7 @@ def take_action(self, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetNetworkRBAC(command.Command): +class SetNetworkRBAC(common.NeutronCommandWithExtraArgs): _description = _("Set network RBAC policy properties") def get_parser(self, prog_name): @@ -291,6 +294,8 @@ def take_action(self, parsed_args): parsed_args.target_project_domain, ).id attrs['target_tenant'] = project_id + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) client.update_rbac_policy(obj, **attrs) diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index c1a672e2d5..14a8edabe5 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -20,6 +20,7 @@ from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -30,7 +31,8 @@ def _get_columns(item): return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) -class CreateNetworkSegment(command.ShowOne): +class CreateNetworkSegment(command.ShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create new network segment") def get_parser(self, prog_name): @@ -88,6 +90,8 @@ def take_action(self, parsed_args): attrs['physical_network'] = parsed_args.physical_network if parsed_args.segment is not None: attrs['segmentation_id'] = parsed_args.segment + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_segment(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) @@ -189,7 +193,7 @@ def take_action(self, parsed_args): ) for s in data)) -class SetNetworkSegment(command.Command): +class SetNetworkSegment(common.NeutronCommandWithExtraArgs): _description = _("Set network segment properties") def get_parser(self, prog_name): @@ -220,6 +224,8 @@ def take_action(self, parsed_args): attrs['description'] = parsed_args.description if parsed_args.name is not None: attrs['name'] = parsed_args.name + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) client.update_segment(obj, **attrs) diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index 6229995aab..ee414407ee 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -25,6 +25,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -87,7 +88,8 @@ def _update_additional_fields_from_props(columns, props): return props -class CreateNetworkSegmentRange(command.ShowOne): +class CreateNetworkSegmentRange(command.ShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create new network segment range") def get_parser(self, prog_name): @@ -209,6 +211,10 @@ def take_action(self, parsed_args): if parsed_args.physical_network: attrs['physical_network'] = parsed_args.physical_network + + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + obj = network_client.create_network_segment_range(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) @@ -365,7 +371,7 @@ def take_action(self, parsed_args): return headers, display_props -class SetNetworkSegmentRange(command.Command): +class SetNetworkSegmentRange(common.NeutronCommandWithExtraArgs): _description = _("Set network segment range properties") def get_parser(self, prog_name): @@ -419,6 +425,8 @@ def take_action(self, parsed_args): attrs['minimum'] = parsed_args.minimum if parsed_args.maximum: attrs['maximum'] = parsed_args.maximum + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) network_client.update_network_segment_range(obj, **attrs) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 4feffc1df4..ecb2382a85 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -326,7 +326,7 @@ def _convert_extra_dhcp_options(parsed_args): return dhcp_options -class CreatePort(command.ShowOne): +class CreatePort(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new port") def get_parser(self, prog_name): @@ -501,6 +501,9 @@ def take_action(self, parsed_args): if parsed_args.tags: attrs['tags'] = list(set(parsed_args.tags)) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + with common.check_missing_extension_if_error( self.app.client_manager.network, attrs): obj = client.create_port(**attrs) @@ -697,7 +700,7 @@ def take_action(self, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetPort(command.Command): +class SetPort(common.NeutronCommandWithExtraArgs): _description = _("Set port properties") def get_parser(self, prog_name): @@ -871,6 +874,9 @@ def take_action(self, parsed_args): if parsed_args.data_plane_status: attrs['data_plane_status'] = parsed_args.data_plane_status + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: with common.check_missing_extension_if_error( self.app.client_manager.network, attrs): @@ -902,7 +908,7 @@ def take_action(self, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class UnsetPort(command.Command): +class UnsetPort(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset port properties") def get_parser(self, prog_name): @@ -1023,6 +1029,9 @@ def take_action(self, parsed_args): if parsed_args.numa_policy: attrs['numa_affinity_policy'] = None + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_port(obj, **attrs) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index e3e8accd21..d15300a0b7 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -27,6 +27,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -256,7 +257,7 @@ def take_action(self, parsed_args): # TODO(yanxing'an): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateRouter(command.ShowOne): +class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new router") def get_parser(self, prog_name): @@ -332,6 +333,9 @@ def take_action(self, parsed_args): attrs['ha'] = True if parsed_args.no_ha: attrs['ha'] = False + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + obj = client.create_router(**attrs) # tags cannot be set when created, so tags need to be set later. _tag.update_tags_for_set(client, obj, parsed_args) @@ -576,7 +580,7 @@ def take_action(self, parsed_args): # TODO(yanxing'an): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetRouter(command.Command): +class SetRouter(common.NeutronCommandWithExtraArgs): _description = _("Set router properties") def get_parser(self, prog_name): @@ -767,6 +771,10 @@ def take_action(self, parsed_args): if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: attrs['external_gateway_info']['qos_policy_id'] = None + + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_router(obj, **attrs) # tags is a subresource and it needs to be updated separately. @@ -809,7 +817,7 @@ def take_action(self, parsed_args): return (display_columns, data) -class UnsetRouter(command.Command): +class UnsetRouter(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset router properties") def get_parser(self, prog_name): @@ -875,6 +883,10 @@ def take_action(self, parsed_args): if parsed_args.external_gateway: attrs['external_gateway_info'] = {} + + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_router(obj, **attrs) # tags is a subresource and it needs to be updated separately. diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 0732c23edc..49dc14e403 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -95,7 +95,8 @@ def _get_columns(item): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateSecurityGroup(common.NetworkAndComputeShowOne): +class CreateSecurityGroup(common.NetworkAndComputeShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create a new security group") def update_parser_common(self, parser): @@ -160,6 +161,8 @@ def take_action_network(self, client, parsed_args): parsed_args.project_domain, ).id attrs['tenant_id'] = project_id + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) # Create the security group and display the results. obj = client.create_security_group(**attrs) @@ -310,7 +313,8 @@ def take_action_compute(self, client, parsed_args): ) for s in data)) -class SetSecurityGroup(common.NetworkAndComputeCommand): +class SetSecurityGroup(common.NetworkAndComputeCommand, + common.NeutronCommandWithExtraArgs): _description = _("Set security group properties") def update_parser_common(self, parser): @@ -362,6 +366,8 @@ def take_action_network(self, client, parsed_args): attrs['stateful'] = True if parsed_args.stateless: attrs['stateful'] = False + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) # NOTE(rtheis): Previous behavior did not raise a CommandError # if there were no updates. Maintain this behavior and issue # the update. diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 17241ed256..e273ded3d6 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -104,7 +104,8 @@ def _is_icmp_protocol(protocol): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): +class CreateSecurityGroupRule(common.NetworkAndComputeShowOne, + common.NeutronCommandWithExtraArgs): _description = _("Create a new security group rule") def update_parser_common(self, parser): @@ -355,6 +356,9 @@ def take_action_network(self, client, parsed_args): ).id attrs['tenant_id'] = project_id + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + # Create and show the security group rule. obj = client.create_security_group_rule(**attrs) display_columns, columns = _get_columns(obj) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index eccdd4e460..09fd7c7c39 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -26,6 +26,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -250,7 +251,7 @@ def _get_attrs(client_manager, parsed_args, is_create=True): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateSubnet(command.ShowOne): +class CreateSubnet(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a subnet") def get_parser(self, prog_name): @@ -373,6 +374,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_subnet(**attrs) # tags cannot be set when created, so tags need to be set later. _tag.update_tags_for_set(client, obj, parsed_args) @@ -545,7 +548,7 @@ def take_action(self, parsed_args): # TODO(abhiraut): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetSubnet(command.Command): +class SetSubnet(common.NeutronCommandWithExtraArgs): _description = _("Set subnet properties") def get_parser(self, prog_name): @@ -630,6 +633,8 @@ def take_action(self, parsed_args): attrs['allocation_pools'] = [] if 'service_types' in attrs: attrs['service_types'] += obj.service_types + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) if attrs: client.update_subnet(obj, **attrs) # tags is a subresource and it needs to be updated separately. @@ -657,7 +662,7 @@ def take_action(self, parsed_args): return (display_columns, data) -class UnsetSubnet(command.Command): +class UnsetSubnet(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset subnet properties") def get_parser(self, prog_name): @@ -744,6 +749,9 @@ def take_action(self, parsed_args): _update_arguments(attrs['service_types'], parsed_args.service_types, 'service-type') + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_subnet(obj, **attrs) diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 56cf61526c..bdf7aba805 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -24,6 +24,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import common from openstackclient.network import sdk_utils @@ -146,7 +147,7 @@ def _add_default_options(parser): # TODO(rtheis): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class CreateSubnetPool(command.ShowOne): +class CreateSubnetPool(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create subnet pool") def get_parser(self, prog_name): @@ -203,6 +204,8 @@ def take_action(self, parsed_args): # NeutronServer expects prefixes to be a List if "prefixes" not in attrs: attrs['prefixes'] = [] + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) obj = client.create_subnet_pool(**attrs) # tags cannot be set when created, so tags need to be set later. _tag.update_tags_for_set(client, obj, parsed_args) @@ -351,7 +354,7 @@ def take_action(self, parsed_args): # TODO(rtheis): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. -class SetSubnetPool(command.Command): +class SetSubnetPool(common.NeutronCommandWithExtraArgs): _description = _("Set subnet pool properties") def get_parser(self, prog_name): @@ -408,6 +411,9 @@ def take_action(self, parsed_args): if 'prefixes' in attrs: attrs['prefixes'].extend(obj.prefixes) + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + if attrs: client.update_subnet_pool(obj, **attrs) # tags is a subresource and it needs to be updated separately. diff --git a/openstackclient/tests/unit/network/test_common.py b/openstackclient/tests/unit/network/test_common.py index cde321aa23..4dde1b2bea 100644 --- a/openstackclient/tests/unit/network/test_common.py +++ b/openstackclient/tests/unit/network/test_common.py @@ -102,6 +102,27 @@ def take_action_compute(self, client, parsed_args): return client.compute_action(parsed_args) +class FakeCreateNeutronCommandWithExtraArgs( + common.NeutronCommandWithExtraArgs): + + def get_parser(self, prog_name): + parser = super(FakeCreateNeutronCommandWithExtraArgs, + self).get_parser(prog_name) + parser.add_argument( + '--known-attribute', + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = {} + if 'known_attribute' in parsed_args: + attrs['known_attribute'] = parsed_args.known_attribute + attrs.update( + self._parse_extra_properties(parsed_args.extra_properties)) + client.test_create_action(**attrs) + + class TestNetworkAndCompute(utils.TestCommand): def setUp(self): @@ -187,3 +208,121 @@ def test_take_action_with_http_exception(self): m_action.side_effect = openstack.exceptions.HttpException("bar") self.assertRaisesRegex(exceptions.CommandError, "bar", self.cmd.take_action, mock.Mock()) + + +class TestNeutronCommandWithExtraArgs(utils.TestCommand): + + def setUp(self): + super(TestNeutronCommandWithExtraArgs, self).setUp() + + self.namespace = argparse.Namespace() + + self.app.client_manager.network = mock.Mock() + self.network = self.app.client_manager.network + self.network.test_create_action = mock.Mock() + + # Subclasses can override the command object to test. + self.cmd = FakeCreateNeutronCommandWithExtraArgs( + self.app, self.namespace) + + def test_create_extra_attributes_default_type(self): + arglist = [ + '--known-attribute', 'known-value', + '--extra-property', 'name=extra_name,value=extra_value' + ] + verifylist = [ + ('known_attribute', 'known-value'), + ('extra_properties', [{'name': 'extra_name', + 'value': 'extra_value'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.network.test_create_action.assert_called_with( + known_attribute='known-value', extra_name='extra_value') + + def test_create_extra_attributes_string(self): + arglist = [ + '--known-attribute', 'known-value', + '--extra-property', 'type=str,name=extra_name,value=extra_value' + ] + verifylist = [ + ('known_attribute', 'known-value'), + ('extra_properties', [{'name': 'extra_name', + 'type': 'str', + 'value': 'extra_value'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.network.test_create_action.assert_called_with( + known_attribute='known-value', extra_name='extra_value') + + def test_create_extra_attributes_bool(self): + arglist = [ + '--known-attribute', 'known-value', + '--extra-property', 'type=bool,name=extra_name,value=TrUe' + ] + verifylist = [ + ('known_attribute', 'known-value'), + ('extra_properties', [{'name': 'extra_name', + 'type': 'bool', + 'value': 'TrUe'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.network.test_create_action.assert_called_with( + known_attribute='known-value', extra_name=True) + + def test_create_extra_attributes_int(self): + arglist = [ + '--known-attribute', 'known-value', + '--extra-property', 'type=int,name=extra_name,value=8' + ] + verifylist = [ + ('known_attribute', 'known-value'), + ('extra_properties', [{'name': 'extra_name', + 'type': 'int', + 'value': '8'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.network.test_create_action.assert_called_with( + known_attribute='known-value', extra_name=8) + + def test_create_extra_attributes_list(self): + arglist = [ + '--known-attribute', 'known-value', + '--extra-property', 'type=list,name=extra_name,value=v_1;v_2' + ] + verifylist = [ + ('known_attribute', 'known-value'), + ('extra_properties', [{'name': 'extra_name', + 'type': 'list', + 'value': 'v_1;v_2'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.network.test_create_action.assert_called_with( + known_attribute='known-value', extra_name=['v_1', 'v_2']) + + def test_create_extra_attributes_dict(self): + arglist = [ + '--known-attribute', 'known-value', + '--extra-property', 'type=dict,name=extra_name,value=n1:v1;n2:v2' + ] + verifylist = [ + ('known_attribute', 'known-value'), + ('extra_properties', [{'name': 'extra_name', + 'type': 'dict', + 'value': 'n1:v1;n2:v2'}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.network.test_create_action.assert_called_with( + known_attribute='known-value', + extra_name={'n1': 'v1', 'n2': 'v2'}) diff --git a/openstackclient/tests/unit/network/test_utils.py b/openstackclient/tests/unit/network/test_utils.py new file mode 100644 index 0000000000..6252d7f766 --- /dev/null +++ b/openstackclient/tests/unit/network/test_utils.py @@ -0,0 +1,59 @@ +# 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 import exceptions + +from openstackclient.network import utils +from openstackclient.tests.unit import utils as tests_utils + + +class TestUtils(tests_utils.TestCase): + + def test_str2bool(self): + self.assertTrue(utils.str2bool("true")) + self.assertTrue(utils.str2bool("True")) + self.assertTrue(utils.str2bool("TRUE")) + self.assertTrue(utils.str2bool("TrUe")) + + self.assertFalse(utils.str2bool("false")) + self.assertFalse(utils.str2bool("False")) + self.assertFalse(utils.str2bool("FALSE")) + self.assertFalse(utils.str2bool("FaLsE")) + self.assertFalse(utils.str2bool("Something else")) + self.assertFalse(utils.str2bool("")) + + self.assertIsNone(utils.str2bool(None)) + + def test_str2list(self): + self.assertEqual( + ['a', 'b', 'c'], utils.str2list("a;b;c")) + self.assertEqual( + ['abc'], utils.str2list("abc")) + + self.assertEqual([], utils.str2list("")) + self.assertEqual([], utils.str2list(None)) + + def test_str2dict(self): + self.assertEqual( + {'a': 'aaa', 'b': '2'}, + utils.str2dict('a:aaa;b:2')) + self.assertEqual( + {'a': 'aaa;b;c', 'd': 'ddd'}, + utils.str2dict('a:aaa;b;c;d:ddd')) + + self.assertEqual({}, utils.str2dict("")) + self.assertEqual({}, utils.str2dict(None)) + + self.assertRaises( + exceptions.CommandError, + utils.str2dict, "aaa;b:2") diff --git a/requirements.txt b/requirements.txt index d3a17e9a48..0ac991da20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,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.53.0 # Apache-2.0 +openstacksdk>=0.56.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 From a2375b878785d739b4fdc49f563c00208a6d18ae Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Wed, 26 May 2021 12:26:40 +0200 Subject: [PATCH 0400/1120] Make functional Neutron tests running fine on ML2/OVN environments Devstack recently switched default Neutron's backend from ML2/OVS to ML2/OVN. As OVN backend has some parity gaps and differences in some APIs, functional tests job was failing with ML2/OVN as some tests weren't properly skipped in case of missing some Neutron API extensions. This patch fixes that by doing some small changes in the functional tests: - skip DHCP/L3 agent tests when dhcp/l3 agent scheduler extensions aren't available, - skip updating neutron agent as OVN agents don't allows that, - skip service providers tests when there is no Neutron L3 agent available, - skip setting router as distributed as OVN backend don't supports that router's attribute at all. Depends-On: https://review.opendev.org/c/openstack/neutron/+/793141 Change-Id: I29a8db202086b0b49fed865409fa8ca244b98439 --- .../functional/network/v2/test_network.py | 2 ++ .../network/v2/test_network_agent.py | 12 +++++++ .../v2/test_network_service_provider.py | 8 +++++ .../functional/network/v2/test_router.py | 36 ++++++++++++------- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py index 71b533ed22..f68b314382 100644 --- a/openstackclient/tests/functional/network/v2/test_network.py +++ b/openstackclient/tests/functional/network/v2/test_network.py @@ -335,6 +335,8 @@ def test_network_list(self): def test_network_dhcp_agent(self): if not self.haz_network: self.skipTest("No Network service present") + if not self.is_extension_enabled("dhcp_agent_scheduler"): + self.skipTest("No dhcp_agent_scheduler extension present") name1 = uuid.uuid4().hex cmd_output1 = json.loads(self.openstack( diff --git a/openstackclient/tests/functional/network/v2/test_network_agent.py b/openstackclient/tests/functional/network/v2/test_network_agent.py index 963227de48..e558094598 100644 --- a/openstackclient/tests/functional/network/v2/test_network_agent.py +++ b/openstackclient/tests/functional/network/v2/test_network_agent.py @@ -49,6 +49,11 @@ def test_network_agent_list_show_set(self): cmd_output['id'], ) + if 'ovn' in agent_list[0]['Agent Type'].lower(): + # NOTE(slaweq): OVN Neutron agents can't be updated so test can be + # finished here + return + # agent set raw_output = self.openstack( 'network agent set --disable %s' % agent_ids[0] @@ -89,6 +94,9 @@ def setUp(self): def test_network_dhcp_agent_list(self): """Test network agent list""" + if not self.is_extension_enabled("dhcp_agent_scheduler"): + self.skipTest("No dhcp_agent_scheduler extension present") + name1 = uuid.uuid4().hex cmd_output1 = json.loads(self.openstack( 'network create -f json --description aaaa %s' % name1 @@ -131,6 +139,10 @@ def test_network_dhcp_agent_list(self): def test_network_agent_list_routers(self): """Add agent to router, list agents on router, delete.""" + + if not self.is_extension_enabled("l3_agent_scheduler"): + self.skipTest("No l3_agent_scheduler extension present") + name = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'router create -f json %s' % name)) diff --git a/openstackclient/tests/functional/network/v2/test_network_service_provider.py b/openstackclient/tests/functional/network/v2/test_network_service_provider.py index 999b7eb713..c571a75640 100644 --- a/openstackclient/tests/functional/network/v2/test_network_service_provider.py +++ b/openstackclient/tests/functional/network/v2/test_network_service_provider.py @@ -26,6 +26,14 @@ def setUp(self): # Nothing in this class works with Nova Network if not self.haz_network: self.skipTest("No Network service present") + # NOTE(slaweq): + # that tests should works only when "standard" Neutron L3 agent is + # used, as e.g. OVN L3 plugin don't supports that. + l3_agent_list = json.loads(self.openstack( + 'network agent list -f json --agent-type l3 -c ID' + )) + if not l3_agent_list: + self.skipTest("No Neutron L3 Agents present") def test_network_service_provider_list(self): cmd_output = json.loads(self.openstack( diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 0769dca6ff..2464b681f6 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -155,6 +155,10 @@ def test_router_list(self): def test_router_list_l3_agent(self): """Tests create router, add l3 agent, list, delete""" + + if not self.is_extension_enabled("l3_agent_scheduler"): + self.skipTest("No l3_agent_scheduler extension present") + name = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'router create -f json ' + name)) @@ -235,32 +239,38 @@ def test_router_set_show_unset(self): ) # Test set --ha --distributed + self._test_set_router_distributed(new_name) + + # Test unset cmd_output = self.openstack( - 'router set ' + - '--distributed ' + - '--external-gateway public ' + + 'router unset ' + + '--external-gateway ' + new_name ) - self.assertOutput('', cmd_output) - cmd_output = json.loads(self.openstack( 'router show -f json ' + new_name )) - self.assertTrue(cmd_output["distributed"]) - self.assertIsNotNone(cmd_output["external_gateway_info"]) + self.assertIsNone(cmd_output["external_gateway_info"]) + + def _test_set_router_distributed(self, router_name): + if not self.is_extension_enabled("dvr"): + return - # Test unset cmd_output = self.openstack( - 'router unset ' + - '--external-gateway ' + - new_name + 'router set ' + + '--distributed ' + + '--external-gateway public ' + + router_name ) + self.assertOutput('', cmd_output) + cmd_output = json.loads(self.openstack( 'router show -f json ' + - new_name + router_name )) - self.assertIsNone(cmd_output["external_gateway_info"]) + self.assertTrue(cmd_output["distributed"]) + self.assertIsNotNone(cmd_output["external_gateway_info"]) def test_router_add_remove_route(self): network_name = uuid.uuid4().hex From fe6a4fa8fc82a05785671c546345719bae39bbb3 Mon Sep 17 00:00:00 2001 From: YuehuiLei Date: Wed, 5 May 2021 14:56:50 +0800 Subject: [PATCH 0401/1120] setup.cfg: Replace dashes with underscores Setuptools v54.1.0 introduces a warning that the use of dash-separated options in 'setup.cfg' will not be supported in a future version [1]. Get ahead of the issue by replacing the dashes with underscores. Without this, we see 'UserWarning' messages like the following on new enough versions of setuptools: UserWarning: Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead [1] https://github.com/pypa/setuptools/commit/a2e9ae4cb Change-Id: I7e43e43bc5a24f49aa7b225502e5d0176fef3783 --- setup.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index c61b3bac35..d6d7a3d2b0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] name = python-openstackclient summary = OpenStack Command-line Client -description-file = +description_file = README.rst author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://docs.openstack.org/python-openstackclient/latest/ -python-requires = >=3.6 +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/python-openstackclient/latest/ +python_requires = >=3.6 classifier = Environment :: OpenStack Intended Audience :: Information Technology From d08795271783cc6fc912ce99aef932d93dc60c47 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 2 Jun 2021 12:17:35 +0100 Subject: [PATCH 0402/1120] compute: Fix typo Change-Id: I3795142318b63b7c8f836d78a415a2161f61164d Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b81d2a181c..9670a6205d 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -903,7 +903,7 @@ def get_parser(self, prog_name): '(required if using source image, snapshot or volume),\n' 'source_type=: source type ' '(one of: image, snapshot, volume, blank),\n' - 'destination_typ=: destination type ' + 'destination_type=: destination type ' '(one of: volume, local) (optional),\n' 'disk_bus=: device bus ' '(one of: uml, lxc, virtio, ...) (optional),\n' From 1169a114e7388e4ee622dca98b9fb8e2a06c8ec3 Mon Sep 17 00:00:00 2001 From: "wu.shiming" Date: Thu, 3 Jun 2021 14:49:23 +0800 Subject: [PATCH 0403/1120] Changed minversion in tox to 3.18.0 The patch bumps min version of tox to 3.18.0 in order to replace tox's whitelist_externals by allowlist_externals option: https://github.com/tox-dev/tox/blob/master/docs/changelog.rst#v3180-2020-07-23 Change-Id: Ibb77fa2afad3f09e95f0dba243d3a096daedd787 --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index ebcdc2d5d9..5e0be38422 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.2.0 +minversion = 3.18.0 envlist = py38,pep8 skipdist = True # Automatic envs (pyXX) will only use the python version appropriate to that @@ -18,7 +18,7 @@ deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run {posargs} -whitelist_externals = stestr +allowlist_externals = stestr [testenv:fast8] # Use same environment directory as pep8 env to save space and install time @@ -71,7 +71,7 @@ commands = pythom -m pip install -q -e "git+file://{toxinidir}/../openstacksdk#egg=openstacksdk" python -m pip freeze stestr run {posargs} -whitelist_externals = stestr +allowlist_externals = stestr [testenv:functional] setenv = OS_TEST_PATH=./openstackclient/tests/functional From eca1fcd65f22ead444ec39fe20f48c83f8d7c1cf Mon Sep 17 00:00:00 2001 From: David Caro Date: Wed, 2 Jun 2021 16:33:53 +0200 Subject: [PATCH 0404/1120] Include hosts in aggregate list --long This makes it easier to get the total list of aggregates and the hosts belonging to each of them (specially for scripting purposes). Change-Id: I94833c15075ae655bc11e7c0fc47c0abad5846fc Signed-off-by: David Caro --- openstackclient/compute/v2/aggregate.py | 2 ++ openstackclient/tests/unit/compute/v2/test_aggregate.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index e39eb2d272..37522a788a 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -193,12 +193,14 @@ def take_action(self, parsed_args): "Name", "Availability Zone", "Properties", + "Hosts", ) columns = ( "ID", "Name", "Availability Zone", "Metadata", + "Hosts", ) else: column_headers = columns = ( diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index 8563f98811..92ff607f94 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -234,6 +234,7 @@ class TestAggregateList(TestAggregate): "Name", "Availability Zone", "Properties", + "Hosts", ) list_data = (( @@ -251,6 +252,7 @@ class TestAggregateList(TestAggregate): for key, value in TestAggregate.fake_ag.metadata.items() if key != 'availability_zone' }), + format_columns.ListColumn(TestAggregate.fake_ag.hosts), ), ) def setUp(self): From 8dc2a7e9f79ef22c53a6f71187d47c40b95631f6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Apr 2021 15:46:36 +0100 Subject: [PATCH 0405/1120] docs: Update cinderclient comparison doc Done manually by looking at the help text for the 'cinder' client (version 7.0.0) and identifying gaps. Change-Id: Ib16c7e9dfa47a93d8b077f0e3e5bbd5bf8984ec3 Signed-off-by: Stephen Finucane --- doc/source/cli/data/cinder.csv | 250 +++++++++++++++++++-------------- 1 file changed, 146 insertions(+), 104 deletions(-) diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 22fb84cffa..27141494a2 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -1,104 +1,146 @@ -absolute-limits,limits show --absolute,Lists absolute limits for a user. -availability-zone-list,availability zone list --volume,Lists all availability zones. -backup-create,volume backup create,Creates a volume backup. -backup-delete,volume backup delete,Removes a backup. -backup-export,volume backup record export,Export backup metadata record. -backup-import,volume backup record import,Import backup metadata record. -backup-list,volume backup list,Lists all backups. -backup-reset-state,volume backup set --state,Explicitly updates the backup state. -backup-restore,volume backup restore,Restores a backup. -backup-show,volume backup show,Show backup details. -cgsnapshot-create,consistency group snapshot create,Creates a cgsnapshot. -cgsnapshot-delete,consistency group snapshot delete,Removes one or more cgsnapshots. -cgsnapshot-list,consistency group snapshot list,Lists all cgsnapshots. -cgsnapshot-show,consistency group snapshot show,Shows cgsnapshot details. -consisgroup-create,consistency group create,Creates a consistency group. -consisgroup-create-from-src,consistency group create --consistency-group-snapshot,Creates a consistency group from a cgsnapshot or a source CG -consisgroup-delete,consistency group delete,Removes one or more consistency groups. -consisgroup-list,consistency group list,Lists all consistencygroups. -consisgroup-show,consistency group show,Shows details of a consistency group. -consisgroup-update,consistency group set,Updates a consistencygroup. -create,volume create,Creates a volume. -credentials,WONTFIX,Shows user credentials returned from auth. -delete,volume delete,Removes one or more volumes. -encryption-type-create,volume type create --encryption-provider --enc..,Creates encryption type for a volume type. Admin only. -encryption-type-delete,volume type delete,Deletes encryption type for a volume type. Admin only. -encryption-type-list,volume type list --encryption-type,Shows encryption type details for volume types. Admin only. -encryption-type-show,volume type list --encryption-show,Shows encryption type details for volume type. Admin only. -encryption-type-update,volume type set --encryption-provider --enc..,Update encryption type information for a volume type (Admin Only). -endpoints,catalog list,Discovers endpoints registered by authentication service. -extend,volume set --size,Attempts to extend size of an existing volume. -extra-specs-list,volume type list --long,Lists current volume types and extra specs. -failover-host,volume host failover,Failover a replicating cinder-volume host. -force-delete,volume delete --force,"Attempts force-delete of volume, regardless of state." -freeze-host,volume host set --disable,Freeze and disable the specified cinder-volume host. -get-capabilities,volume backend capability show,Show capabilities of a volume backend. Admin only. -get-pools,volume backend pool list,Show pool information for backends. Admin only. -image-metadata,volume set --image-property,Sets or deletes volume image metadata. -image-metadata-show,volume show,Shows volume image metadata. -list,volume list,Lists all volumes. -manage,volume create --remote-source k=v,Manage an existing volume. -metadata,volume set --property k=v / volume unset --property k,Sets or deletes volume metadata. -metadata-show,volume show,Shows volume metadata. -metadata-update-all,volume set --property k=v,Updates volume metadata. -migrate,volume migrate --host --force-copy --lock-volume ,Migrates volume to a new host. -qos-associate,volume qos associate,Associates qos specs with specified volume type. -qos-create,volume qos create,Creates a qos specs. -qos-delete,volume qos delete,Deletes a specified qos specs. -qos-disassociate,volume qos disassociate,Disassociates qos specs from specified volume type. -qos-disassociate-all,volume qos disassociate --all,Disassociates qos specs from all associations. -qos-get-association,volume qos show,Gets all associations for specified qos specs. -qos-key,volume qos set --property k=v / volume qos unset --property k,Sets or unsets specifications for a qos spec -qos-list,volume qos list,Lists qos specs. -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-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. -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. -replication-promote,WONTFIX,Promote a secondary volume to primary for a relationship -replication-reenable,WONTFIX,Sync the secondary volume with primary for a relationship -reset-state,volume set --state,Explicitly updates the volume state. -retype,volume type set --type,Changes the volume type for a volume. -service-disable,volume service set --disable,Disables the service. -service-enable,volume service set --enable,Enables the service. -service-list,volume service list,Lists all services. Filter by host and service binary. -set-bootable,volume set --bootable / --not-bootable,Update bootable status of a volume. -show,volume show,Shows volume details. -snapshot-create,snapshot create,Creates a snapshot. -snapshot-delete,snapshot delete,Remove one or more snapshots. -snapshot-list,snapshot list,Lists all snapshots. -snapshot-manage,volume snapshot create --remote-source ,Manage an existing snapshot. -snapshot-metadata,snapshot set --property k=v / snapshot unset --property k,Sets or deletes snapshot metadata. -snapshot-metadata-show,snapshot show,Shows snapshot metadata. -snapshot-metadata-update-all,snapshot set --property k=v,Updates snapshot metadata. -snapshot-rename,snapshot set --name,Renames a snapshot. -snapshot-reset-state,snapshot set --state,Explicitly updates the snapshot state. -snapshot-show,snapshot show,Shows snapshot details. -snapshot-unmanage,volume snapshot delete --remote,Stop managing a snapshot. -thaw-host,volume host set --enable,Thaw and enable the specified cinder-volume host. -transfer-accept,volume transfer accept,Accepts a volume transfer. -transfer-create,volume transfer create,Creates a volume transfer. -transfer-delete,volume transfer delete,Undoes a transfer. -transfer-list,volume transfer list,Lists all transfers. -transfer-show,volume transfer show,Show transfer details. -type-access-add,volume type set --project,Adds volume type access for the given project. -type-access-list,volume type show,Print access information about the given volume type. -type-access-remove,volume type unset --project,Removes volume type access for the given project. -type-create,volume type create,Creates a volume type. -type-default,volume type list --default,List the default volume type. -type-delete,volume type delete,Deletes a specified volume type. -type-key,volume type set --property k=v / volume type unset --property k,Sets or unsets extra_spec for a volume type. -type-list,volume type list,Lists available 'volume types'. -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. -bash-completion,complete,Prints arguments for bash_completion. -help,help,Shows help about this program or one of its subcommands. -list-extensions,extension list --volume,Lists all available os-api extensions. +absolute-limits,limits show --absolute,Lists absolute limits for a user. +api-version,WONTFIX,Display the server API version information. +availability-zone-list,availability zone list --volume,Lists all availability zones. +attachment-complete,,Complete an attachment for a cinder volume. (Supported by API versions 3.44 - 3.latest) +attachment-create,,Create an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) +attachment-delete,,Delete an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) +attachment-list,,Lists all attachments. (Supported by API versions 3.27 - 3.latest) +attachment-show,,Show detailed information for attachment. (Supported by API versions 3.27 - 3.latest) +attachment-update,,Update an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) +backup-create,volume backup create,Creates a volume backup. +backup-delete,volume backup delete,Removes a backup. +backup-export,volume backup record export,Export backup metadata record. +backup-import,volume backup record import,Import backup metadata record. +backup-list,volume backup list,Lists all backups. +backup-reset-state,volume backup set --state,Explicitly updates the backup state. +backup-restore,volume backup restore,Restores a backup. +backup-show,volume backup show,Show backup details. +backup-update,,Updates a backup. (Supported by API versions 3.9 - 3.latest) +cgsnapshot-create,consistency group snapshot create,Creates a cgsnapshot. +cgsnapshot-delete,consistency group snapshot delete,Removes one or more cgsnapshots. +cgsnapshot-list,consistency group snapshot list,Lists all cgsnapshots. +cgsnapshot-show,consistency group snapshot show,Shows cgsnapshot details. +cluster-disable,,Disables clustered services. (Supported by API versions 3.7 - 3.latest) +cluster-enable,,Enables clustered services. (Supported by API versions 3.7 - 3.latest) +cluster-list,,Lists clustered services with optional filtering. (Supported by API versions 3.7 - 3.latest) +cluster-show,,Show detailed information on a clustered service. (Supported by API versions 3.7 - 3.latest) +consisgroup-create,consistency group create,Creates a consistency group. +consisgroup-create-from-src,consistency group create --consistency-group-snapshot,Creates a consistency group from a cgsnapshot or a source CG +consisgroup-delete,consistency group delete,Removes one or more consistency groups. +consisgroup-list,consistency group list,Lists all consistencygroups. +consisgroup-show,consistency group show,Shows details of a consistency group. +consisgroup-update,consistency group set,Updates a consistencygroup. +create,volume create,Creates a volume. +delete,volume delete,Removes one or more volumes. +encryption-type-create,volume type create --encryption-provider --enc..,Creates encryption type for a volume type. Admin only. +encryption-type-delete,volume type delete,Deletes encryption type for a volume type. Admin only. +encryption-type-list,volume type list --encryption-type,Shows encryption type details for volume types. Admin only. +encryption-type-show,volume type list --encryption-show,Shows encryption type details for volume type. Admin only. +encryption-type-update,volume type set --encryption-provider --enc..,Update encryption type information for a volume type (Admin Only). +extend,volume set --size,Attempts to extend size of an existing volume. +extra-specs-list,volume type list --long,Lists current volume types and extra specs. +failover-host,volume host failover,Failover a replicating cinder-volume host. +force-delete,volume delete --force,"Attempts force-delete of volume regardless of state." +freeze-host,volume host set --disable,Freeze and disable the specified cinder-volume host. +get-capabilities,volume backend capability show,Show capabilities of a volume backend. Admin only. +get-pools,volume backend pool list,Show pool information for backends. Admin only. +group-create,,Creates a group. (Supported by API versions 3.13 - 3.latest) +group-create-from-src,,Creates a group from a group snapshot or a source group. (Supported by API versions 3.14 - 3.latest) +group-delete,,Removes one or more groups. (Supported by API versions 3.13 - 3.latest) +group-disable-replication,,Disables replication for group. (Supported by API versions 3.38 - 3.latest) +group-enable-replication,,Enables replication for group. (Supported by API versions 3.38 - 3.latest) +group-failover-replication,,Fails over replication for group. (Supported by API versions 3.38 - 3.latest) +group-list,,Lists all groups. (Supported by API versions 3.13 - 3.latest) +group-list-replication-targets,,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest) +group-show,,Shows details of a group. (Supported by API versions 3.13 - 3.latest) +group-snapshot-create,,Creates a group snapshot. (Supported by API versions 3.14 - 3.latest) +group-snapshot-delete,,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest) +group-snapshot-list,,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest) +group-snapshot-show,,Shows group snapshot details. (Supported by API versions 3.14 - 3.latest) +group-specs-list,,Lists current group types and specs. (Supported by API versions 3.11 - 3.latest) +group-type-create,,Creates a group type. (Supported by API versions 3.11 - 3.latest) +group-type-default,,List the default group type. (Supported by API versions 3.11 - 3.latest) +group-type-delete,,Deletes group type or types. (Supported by API versions 3.11 - 3.latest) +group-type-key,,Sets or unsets group_spec for a group type. (Supported by API versions 3.11 - 3.latest) +group-type-list,,Lists available 'group types'. (Admin only will see private types) (Supported by API versions 3.11 - 3.latest) +group-type-show,,Show group type details. (Supported by API versions 3.11 - 3.latest) +group-type-update,,Updates group type name description and/or is_public. (Supported by API versions 3.11 - 3.latest) +group-update,,Updates a group. (Supported by API versions 3.13 - 3.latest) +image-metadata,volume set --image-property,Sets or deletes volume image metadata. +image-metadata-show,volume show,Shows volume image metadata. +list,volume list,Lists all volumes. +list-filters,,List enabled filters. (Supported by API versions 3.33 - 3.latest) +manage,volume create --remote-source k=v,Manage an existing volume. +manageable-list,,Lists all manageable volumes. (Supported by API versions 3.8 - 3.latest) +message-delete,,Removes one or more messages. (Supported by API versions 3.3 - 3.latest) +message-list,,Lists all messages. (Supported by API versions 3.3 - 3.latest) +message-show,,Shows message details. (Supported by API versions 3.3 - 3.latest) +metadata,volume set --property k=v / volume unset --property k,Sets or deletes volume metadata. +metadata-show,volume show,Shows volume metadata. +metadata-update-all,volume set --property k=v,Updates volume metadata. +migrate,volume migrate --host --force-copy --lock-volume ,Migrates volume to a new host. +qos-associate,volume qos associate,Associates qos specs with specified volume type. +qos-create,volume qos create,Creates a qos specs. +qos-delete,volume qos delete,Deletes a specified qos specs. +qos-disassociate,volume qos disassociate,Disassociates qos specs from specified volume type. +qos-disassociate-all,volume qos disassociate --all,Disassociates qos specs from all associations. +qos-get-association,volume qos show,Gets all associations for specified qos specs. +qos-key,volume qos set --property k=v / volume qos unset --property k,Sets or unsets specifications for a qos spec +qos-list,volume qos list,Lists qos specs. +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-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. +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. +reset-state,volume set --state,Explicitly updates the volume state. +retype,volume type set --type,Changes the volume type for a volume. +revert-to-snapshot,,Revert a volume to the specified snapshot. (Supported by API versions 3.40 - 3.latest) +service-disable,volume service set --disable,Disables the service. +service-enable,volume service set --enable,Enables the service. +service-get-log,,(Supported by API versions 3.32 - 3.latest) +service-list,volume service list,Lists all services. Filter by host and service binary. +service-set-log,,(Supported by API versions 3.32 - 3.latest) +set-bootable,volume set --bootable / --not-bootable,Update bootable status of a volume. +show,volume show,Shows volume details. +snapshot-create,snapshot create,Creates a snapshot. +snapshot-delete,snapshot delete,Remove one or more snapshots. +snapshot-list,snapshot list,Lists all snapshots. +snapshot-manage,volume snapshot create --remote-source ,Manage an existing snapshot. +snapshot-manageable-list,,Lists all manageable snapshots. (Supported by API versions 3.8 - 3.latest) +snapshot-metadata,snapshot set --property k=v / snapshot unset --property k,Sets or deletes snapshot metadata. +snapshot-metadata-show,snapshot show,Shows snapshot metadata. +snapshot-metadata-update-all,snapshot set --property k=v,Updates snapshot metadata. +snapshot-rename,snapshot set --name,Renames a snapshot. +snapshot-reset-state,snapshot set --state,Explicitly updates the snapshot state. +snapshot-show,snapshot show,Shows snapshot details. +snapshot-unmanage,volume snapshot delete --remote,Stop managing a snapshot. +summary,,Get volumes summary. (Supported by API versions 3.12 - 3.latest) +thaw-host,volume host set --enable,Thaw and enable the specified cinder-volume host. +transfer-accept,volume transfer accept,Accepts a volume transfer. +transfer-create,volume transfer create,Creates a volume transfer. +transfer-delete,volume transfer delete,Undoes a transfer. +transfer-list,volume transfer list,Lists all transfers. +transfer-show,volume transfer show,Show transfer details. +type-access-add,volume type set --project,Adds volume type access for the given project. +type-access-list,volume type show,Print access information about the given volume type. +type-access-remove,volume type unset --project,Removes volume type access for the given project. +type-create,volume type create,Creates a volume type. +type-default,volume type list --default,List the default volume type. +type-delete,volume type delete,Deletes a specified volume type. +type-key,volume type set --property k=v / volume type unset --property k,Sets or unsets extra_spec for a volume type. +type-list,volume type list,Lists available 'volume types'. +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) +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. +list-extensions,extension list --volume,Lists all available os-api extensions. From 3751f1fdb60081d0ef72bf41b027c1c05f0a89b9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Apr 2021 17:26:27 +0100 Subject: [PATCH 0406/1120] docs: Update novaclient comparison doc Done manually by looking at the help text for the 'nova' client (version 17.0.0) and identifying gaps. Change-Id: I23a4947a13d5e576c5aa66902686df60379ffda0 Signed-off-by: Stephen Finucane --- doc/source/cli/data/nova.csv | 37 +++++++++++------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index c319a4a69d..83911f1237 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -1,10 +1,10 @@ -add-fixed-ip,server add fixed ip,Add new IP address on a network to server. add-secgroup,server add security group,Add a Security Group to a server. agent-create,compute agent create,Create new agent build. 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-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. @@ -15,12 +15,7 @@ aggregate-update,aggregate set / unset,Update the aggregate's name and optionall availability-zone-list,availability zone list,List all the availability zones. backup,server backup create,Backup a server by creating a 'backup' type snapshot. boot,server create,Boot a new server. -cell-capacities,,Get cell capacities for all cells or a given cell. -cell-show,,Show details of a given cell. -clear-password,server set --root-password,Clear the admin password for a server from the metadata server. -cloudpipe-configure,WONTFIX,Update the VPN IP/port of a cloudpipe instance. -cloudpipe-create,WONTFIX,Create a cloudpipe instance for the given project. -cloudpipe-list,WONTFIX,Print a list of all cloudpipe instances. +clear-password,server set --root-password,Clear the admin password for a server from the metadata server. This action does not actually change the instance server password. console-log,console log show,Get console log output of a server. delete,server delete,Immediately shut down and delete specified server(s). diagnostics,openstack server show --diagnostics,Retrieve server diagnostics. @@ -33,24 +28,19 @@ flavor-delete,flavor delete,Delete a specific flavor flavor-key,flavor set / unset,Set or unset extra_spec for a flavor. flavor-list,flavor list,Print a list of available 'flavors' flavor-show,flavor show,Show details about the given flavor. -floating-ip-associate,server add floating ip,Associate a floating IP address to a server. -floating-ip-disassociate,server remove floating ip,Disassociate a floating IP address from a server. +flavor-update,,Update the description of an existing flavor. (Supported by API versions '2.55' - '2.latest') [hint: use '--os-compute-api- version' flag to show help message for proper version] force-delete,server delete,Force delete a server. -get-mks-console,console url show --mks,Get an MKS console to a server. -get-password,WONTFIX,Get the admin password for a server. +get-mks-console,console url show --mks,Get an MKS console to a server. (Supported by API versions '2.8' - '2.latest') [hint: use ' --os-compute-api-version' flag to show help message for proper version] +get-password,WONTFIX,Get the admin password for a server. This operation calls the metadata service to query metadata information and does not read password information from the server itself. get-rdp-console,console url show --rdp,Get a rdp console to a server. get-serial-console,console url show --serial,Get a serial console to a server. get-spice-console,console url show --spice,Get a spice console to a server. -get-vnc-console,console url show --novnc | --xvpvnc,Get a vnc console to a server. -host-action,,Perform a power action on a host. -host-describe,host show,Describe a specific host. +get-vnc-console,console url show --novnc,Get a vnc console to a server. host-evacuate,,Evacuate all instances from failed host. host-evacuate-live,,Live migrate all instances off the specified host to other available hosts. -host-list,host list,List all hosts by service. host-meta,,Set or Delete metadata on all instances of a host. host-servers-migrate,,Cold migrate all instances off the specified host to other available hosts. -host-update,host set,Update host settings. -hypervisor-list,hypervisor list,List hypervisors. +hypervisor-list,hypervisor list,List hypervisors. (Supported by API versions '2.0' - '2.latest') 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. @@ -58,6 +48,7 @@ hypervisor-uptime,,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,,Show an action. instance-action-list,,List actions on a server. +instance-usage-audit-log,,List/Get server usage audits. interface-attach,,Attach a network interface to a server. interface-detach,,Detach a network interface from a server. interface-list,port list --server,List interfaces attached to a server. @@ -67,7 +58,6 @@ keypair-list,keypair list,Print a list of keypairs for a user keypair-show,keypair show,Show details about the given keypair. limits,limits show,Print rate and absolute limits. list,server list,List active servers. -list-extensions,extension list,List all the os-api extensions that are available. list-secgroup,security group list,List Security Group(s) of a server. live-migration,,Migrate running server to a new machine. live-migration-abort,,Abort an on-going live migration. @@ -86,7 +76,6 @@ quota-update,quota set,Update the quotas for a tenant/user. reboot,server reboot,Reboot a server. rebuild,server rebuild,"Shutdown, re-image, and re-boot a server." refresh-network,WONTFIX,Refresh server network information. -remove-fixed-ip,server remove fixed ip,Remove an IP address from a server. remove-secgroup,server remove security group,Remove a Security Group from a server. rescue,server rescue,Reboots a server into rescue mode. reset-network,WONTFIX,Reset network of a server. @@ -107,6 +96,7 @@ server-tag-delete,,Delete one or more tags from a server. server-tag-delete-all,,Delete all tags from a server. server-tag-list,,Get list of tags from a server. server-tag-set,,Set list of tags to a server. +server-topology,openstack server show --topology,Retrieve server topology. (Supported by API versions '2.78' - '2.latest') [hint: use '-- os-compute-api-version' flag to show help message for proper version] service-delete,compute service delete,Delete the service. service-disable,compute service set --disable,Disable the service. service-enable,compute service set --enable,Enable the service. @@ -121,22 +111,17 @@ start,server start,Start the server(s). stop,server stop,Stop the server(s). suspend,server suspend,Suspend a server. trigger-crash-dump,server dump create,Trigger crash dump in an instance. -topology,openstack server show --topology,Retrieve server NUMA topology. unlock,server unlock,Unlock a server. unpause,server unpause,Unpause a server. unrescue,server unrescue,Restart the server from normal boot disk again. unshelve,server unshelve,Unshelve a server. -update,server set / unset --description,Update or unset the description for a server. -update,server set --name,Update the name for 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. -virtual-interface-list,,Show virtual interface info about the given server. 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. volume-update,,Update volume attachment. -x509-create-cert,WONTFIX,Create x509 cert for a user in tenant. -x509-get-root-cert,WONTFIX,Fetch the x509 root cert. -bash-completion,complete,Prints all of the commands and options to +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 83e7e4ab2e84169241651e504be164939a11b417 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Apr 2021 17:33:42 +0100 Subject: [PATCH 0407/1120] docs: Update glanceclient comparison doc Done manually by looking at the help text for the 'glance' client (version 3.1.1) and identifying gaps. Change-Id: Ic46bbdef7182e5f707cd5083868886ce60c7eb47 Signed-off-by: Stephen Finucane --- doc/source/cli/data/glance.csv | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index 994a8fdaae..27585b9510 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -1,22 +1,58 @@ explain,WONTFIX,Describe a specific model. image-create,image create,Create a new image. +image-create-via-import,,EXPERIMENTAL: Create a new image via image import. image-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-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-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. image-upload,,Upload data for a specific image. +import-info,,Print import methods available from Glance. location-add,,Add a location (and related metadata) to an image. location-delete,,Remove locations (and related metadata) from an image. location-update,,Update metadata of an image's location. +md-namespace-create,,Create a new metadata definitions namespace. +md-namespace-delete,,Delete specified metadata definitions namespace with its contents. +md-namespace-import,,Import a metadata definitions namespace from file or standard input. +md-namespace-list,,List metadata definitions namespaces. +md-namespace-objects-delete,,Delete all metadata definitions objects inside a specific namespace. +md-namespace-properties-delete,,Delete all metadata definitions property inside a specific namespace. +md-namespace-resource-type-list,,List resource types associated to specific namespace. +md-namespace-show,,Describe a specific metadata definitions namespace. +md-namespace-tags-delete,,Delete all metadata definitions tags inside a specific namespace. +md-namespace-update,,Update an existing metadata definitions namespace. +md-object-create,,Create a new metadata definitions object inside a namespace. +md-object-delete,,Delete a specific metadata definitions object inside a namespace. +md-object-list,,List metadata definitions objects inside a specific namespace. +md-object-property-show,,Describe a specific metadata definitions property inside an object. +md-object-show,,Describe a specific metadata definitions object inside a namespace. +md-object-update,,Update metadata definitions object inside a namespace. +md-property-create,,Create a new metadata definitions property inside a namespace. +md-property-delete,,Delete a specific metadata definitions property inside a namespace. +md-property-list,,List metadata definitions properties inside a specific namespace. +md-property-show,,Describe a specific metadata definitions property inside a namespace. +md-property-update,,Update metadata definitions property inside a namespace. +md-resource-type-associate,,Associate resource type with a metadata definitions namespace. +md-resource-type-deassociate,,Deassociate resource type with a metadata definitions namespace. +md-resource-type-list,,List available resource type names. +md-tag-create,,Add a new metadata definitions tag inside a namespace. +md-tag-create-multiple,,Create new metadata definitions tags inside a namespace. +md-tag-delete,,Delete a specific metadata definitions tag inside a namespace. +md-tag-list,,List metadata definitions tags inside a specific namespace. +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-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-show,,Describe a specific task. From 95f914769a1ba8724ff16bbd06477783bceea157 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Apr 2021 17:45:18 +0100 Subject: [PATCH 0408/1120] docs: Update neutronclient comparison doc Done manually by looking at the help text for the 'neutron' client (version 7.1.1) and identifying gaps. Change-Id: Ib029b2c236f79a0ca6f64834f069db2be4332ea8 Signed-off-by: Stephen Finucane --- doc/source/cli/data/neutron.csv | 34 +++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/doc/source/cli/data/neutron.csv b/doc/source/cli/data/neutron.csv index 080114e241..402f4064ee 100644 --- a/doc/source/cli/data/neutron.csv +++ b/doc/source/cli/data/neutron.csv @@ -35,6 +35,23 @@ dhcp-agent-network-add,network agent add network,Add a network to a DHCP agent. dhcp-agent-network-remove,network agent remove network,Remove a network from a DHCP agent. ext-list,extension list,List all extensions. ext-show,extension show,Show information of a given resource. +firewall-create,,Create a firewall. +firewall-delete,,Delete a given firewall. +firewall-list,,List firewalls that belong to a given tenant. +firewall-policy-create,,Create a firewall policy. +firewall-policy-delete,,Delete a given firewall policy. +firewall-policy-insert-rule,,Insert a rule into a given firewall policy. +firewall-policy-list,,List firewall policies that belong to a given tenant. +firewall-policy-remove-rule,,Remove a rule from a given firewall policy. +firewall-policy-show,,Show information of a given firewall policy. +firewall-policy-update,,Update a given firewall policy. +firewall-rule-create,,Create a firewall rule. +firewall-rule-delete,,Delete a given firewall rule. +firewall-rule-list,,List firewall rules that belong to a given tenant. +firewall-rule-show,,Show information of a given firewall rule. +firewall-rule-update,,Update a given firewall rule. +firewall-show,,Show information of a given firewall. +firewall-update,,Update a given firewall. flavor-associate,network flavor add profile,Add a Neutron service flavor with a flavor profile. flavor-create,network flavor create,Create a Neutron service flavor. flavor-delete,network flavor delete,Delete a given Neutron service flavor. @@ -214,14 +231,6 @@ subnetpool-update,subnet pool set / subnet pool unset,Update subnetpool's inform 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. -tap-flow-create,tapflow create,Create a tap flow -tap-flow-delete,tapflow delete,Delete a tap flow -tap-flow-list,tapflow list,List all tap flows -tap-flow-show,tapflow show,Show details of the tap flow -tap-service-create,tapservice create,Create a tap service -tap-service-delete,tapservice delete,Delete a tap service -tap-service-list,tapservice list,List all tap services -tap-service-show,tapservice show,Show details of the tap service 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. @@ -242,3 +251,12 @@ vpn-service-delete,,Delete a given VPN service. vpn-service-list,,List VPN service configurations that belong to a given tenant. vpn-service-show,,Show information of a given VPN service. vpn-service-update,,Update a given VPN service. + +tap-flow-create,tapflow create,Create a tap flow +tap-flow-delete,tapflow delete,Delete a tap flow +tap-flow-list,tapflow list,List all tap flows +tap-flow-show,tapflow show,Show details of the tap flow +tap-service-create,tapservice create,Create a tap service +tap-service-delete,tapservice delete,Delete a tap service +tap-service-list,tapservice list,List all tap services +tap-service-show,tapservice show,Show details of the tap service From 0f28588e48c1e296f834e8684f293c2cdf4afc33 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 May 2021 20:37:33 +0100 Subject: [PATCH 0409/1120] volume: Allow more versions Copy the API version checks from the 'openstackclient.compute.client' module. These will only be necessary until we migrate everything to SDK but it's very helpful until then. Change-Id: I2d9c68db5bf891ffa25fd5a7fc9e8953e44b73ab Signed-off-by: Stephen Finucane --- openstackclient/volume/client.py | 55 +++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 37cb41685c..0712fa7b00 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -15,6 +15,7 @@ import logging +from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ @@ -29,9 +30,11 @@ "1": "cinderclient.v1.client.Client", "2": "cinderclient.v2.client.Client", "3": "cinderclient.v3.client.Client", - "3.42": "cinderclient.v3.client.Client", } +# Save the microversion if in use +_volume_api_version = None + def make_client(instance): """Returns a volume service client.""" @@ -52,10 +55,13 @@ def make_client(instance): except Exception: del API_VERSIONS['2'] - version = instance._api_version[API_NAME] - from cinderclient import api_versions - # convert to APIVersion object - version = api_versions.get_api_version(version) + if _volume_api_version is not None: + version = _volume_api_version + else: + version = instance._api_version[API_NAME] + from cinderclient import api_versions + # convert to APIVersion object + version = api_versions.get_api_version(version) if version.ver_major == '1': # Monkey patch for v1 cinderclient @@ -103,3 +109,42 @@ def build_option_parser(parser): '(Env: OS_VOLUME_API_VERSION)') % DEFAULT_API_VERSION ) return parser + + +def check_api_version(check_version): + """Validate version supplied by user + + Returns: + + * True if version is OK + * False if the version has not been checked and the previous plugin + check should be performed + * throws an exception if the version is no good + """ + + # Defer client imports until we actually need them + from cinderclient import api_versions + + global _volume_api_version + + # Copy some logic from novaclient 3.3.0 for basic version detection + # NOTE(dtroyer): This is only enough to resume operations using API + # version 3.0 or any valid version supplied by the user. + _volume_api_version = api_versions.get_api_version(check_version) + + # Bypass X.latest format microversion + if not _volume_api_version.is_latest(): + if _volume_api_version > api_versions.APIVersion("3.0"): + if not _volume_api_version.matches( + api_versions.MIN_VERSION, + api_versions.MAX_VERSION, + ): + msg = _("versions supported by client: %(min)s - %(max)s") % { + "min": api_versions.MIN_VERSION, + "max": api_versions.MAX_VERSION, + } + raise exceptions.CommandError(msg) + + return True + + return False From 6dc94e1fb85595653dcdd24185c914b9df1741df Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 24 May 2021 15:56:27 +0100 Subject: [PATCH 0410/1120] volume: Add 'volume attachment *' commands These mirror the 'cinder attachment-*' commands, with arguments copied across essentially verbatim. The only significant departure is the replacement of "tenant" terminology with "project". volume attachment create volume attachment delete volume attachment list volume attachment complete volume attachment set volume attachment show Full support for filtering is deferred for now since that's a more complicated change that requires additional commands be added first. TODOs are included to this effect. Change-Id: If47c2b56fe65ee2cee07c000d6ae3688d5ef3b42 Signed-off-by: Stephen Finucane --- .../cli/command-objects/volume-attachment.rst | 8 + doc/source/cli/commands.rst | 7 +- doc/source/cli/data/cinder.csv | 12 +- openstackclient/tests/unit/volume/v3/fakes.py | 155 +++++ .../unit/volume/v3/test_volume_attachment.py | 560 ++++++++++++++++++ .../volume/v3/volume_attachment.py | 511 ++++++++++++++++ ...-attachment-commands-db2974c6460fa3bc.yaml | 8 + setup.cfg | 7 + 8 files changed, 1259 insertions(+), 9 deletions(-) create mode 100644 doc/source/cli/command-objects/volume-attachment.rst create mode 100644 openstackclient/tests/unit/volume/v3/fakes.py create mode 100644 openstackclient/tests/unit/volume/v3/test_volume_attachment.py create mode 100644 openstackclient/volume/v3/volume_attachment.py create mode 100644 releasenotes/notes/add-volume-attachment-commands-db2974c6460fa3bc.yaml diff --git a/doc/source/cli/command-objects/volume-attachment.rst b/doc/source/cli/command-objects/volume-attachment.rst new file mode 100644 index 0000000000..5622444638 --- /dev/null +++ b/doc/source/cli/command-objects/volume-attachment.rst @@ -0,0 +1,8 @@ +================= +volume attachment +================= + +Block Storage v3 + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume attachment * diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 94a0b5a638..00f6b23a77 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -153,11 +153,12 @@ referring to both Compute and Volume quotas. * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user * ``volume``: (**Volume**) block volumes +* ``volume attachment``: (**Volume**) an attachment of a volumes to a server * ``volume backup``: (**Volume**) backup for volumes -* ``volume backend capability``: (**volume**) volume backend storage capabilities -* ``volume backend pool``: (**volume**) volume backend storage pools +* ``volume backend capability``: (**Volume**) volume backend storage capabilities +* ``volume backend pool``: (**Volume**) volume backend storage pools * ``volume backup record``: (**Volume**) volume record that can be imported or exported -* ``volume backend``: (**volume**) volume backend storage +* ``volume backend``: (**Volume**) volume backend storage * ``volume host``: (**Volume**) the physical computer for volumes * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes * ``volume snapshot``: (**Volume**) a point-in-time copy of a volume diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 27141494a2..dc43ab5b1a 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -1,12 +1,12 @@ absolute-limits,limits show --absolute,Lists absolute limits for a user. api-version,WONTFIX,Display the server API version information. availability-zone-list,availability zone list --volume,Lists all availability zones. -attachment-complete,,Complete an attachment for a cinder volume. (Supported by API versions 3.44 - 3.latest) -attachment-create,,Create an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) -attachment-delete,,Delete an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) -attachment-list,,Lists all attachments. (Supported by API versions 3.27 - 3.latest) -attachment-show,,Show detailed information for attachment. (Supported by API versions 3.27 - 3.latest) -attachment-update,,Update an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) +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) +attachment-delete,volume attachment delete,Delete an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) +attachment-list,volume attachment list,Lists all attachments. (Supported by API versions 3.27 - 3.latest) +attachment-show,volume attachment show,Show detailed information for attachment. (Supported by API versions 3.27 - 3.latest) +attachment-update,volume attachment update,Update an attachment for a cinder volume. (Supported by API versions 3.27 - 3.latest) backup-create,volume backup create,Creates a volume backup. backup-delete,volume backup delete,Removes a backup. backup-export,volume backup record export,Export backup metadata record. diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py new file mode 100644 index 0000000000..fb3b1b74e1 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -0,0 +1,155 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random +from unittest import mock +import uuid + +from cinderclient import api_versions + +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit import utils +from openstackclient.tests.unit.volume.v2 import fakes as volume_v2_fakes + + +class FakeVolumeClient(object): + + def __init__(self, **kwargs): + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + self.api_version = api_versions.APIVersion('3.0') + + self.attachments = mock.Mock() + self.attachments.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.compute = compute_fakes.FakeComputev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + +# TODO(stephenfin): Check if the responses are actually the same +FakeVolume = volume_v2_fakes.FakeVolume + + +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([ + 'attached', + 'attaching', + 'detached', + 'reserved', + '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) + + attachment = fakes.FakeResource( + None, + attachment_info, + loaded=True) + return attachment + + @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) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py new file mode 100644 index 0000000000..09f698e7fd --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py @@ -0,0 +1,560 @@ +# 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 cinderclient import api_versions +from osc_lib.cli import format_columns +from osc_lib import exceptions + +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_attachment + + +class TestVolumeAttachment(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() + + self.volume_attachments_mock = \ + self.app.client_manager.volume.attachments + self.volume_attachments_mock.reset_mock() + + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + self.servers_mock = self.app.client_manager.compute.servers + self.servers_mock.reset_mock() + + +class TestVolumeAttachmentCreate(TestVolumeAttachment): + + volume = volume_fakes.FakeVolume.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}) + + columns = ( + 'ID', + 'Volume ID', + 'Instance ID', + 'Status', + 'Attach Mode', + 'Attached At', + 'Detached At', + 'Properties', + ) + data = ( + volume_attachment.id, + volume_attachment.volume_id, + volume_attachment.instance, + volume_attachment.status, + volume_attachment.attach_mode, + volume_attachment.attached_at, + volume_attachment.detached_at, + format_columns.DictColumn(volume_attachment.connection_info), + ) + + def setUp(self): + super().setUp() + + self.volumes_mock.get.return_value = self.volume + self.servers_mock.get.return_value = self.server + self.volume_attachments_mock.create.return_value = \ + self.volume_attachment + + self.cmd = volume_attachment.CreateVolumeAttachment(self.app, None) + + def test_volume_attachment_create(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.27') + + arglist = [ + self.volume.id, + self.server.id, + ] + verifylist = [ + ('volume', self.volume.id), + ('server', self.server.id), + ('connect', False), + ('initiator', None), + ('ip', None), + ('host', None), + ('platform', None), + ('os_type', None), + ('multipath', False), + ('mountpoint', None), + ('mode', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.get.assert_called_once_with(self.volume.id) + self.servers_mock.get.assert_called_once_with(self.server.id) + self.volume_attachments_mock.create.assert_called_once_with( + self.volume.id, {}, self.server.id, None, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_attachment_create_with_connect(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.54') + + arglist = [ + self.volume.id, + self.server.id, + '--connect', + '--initiator', 'iqn.1993-08.org.debian:01:cad181614cec', + '--ip', '192.168.1.20', + '--host', 'my-host', + '--platform', 'x86_64', + '--os-type', 'linux2', + '--multipath', + '--mountpoint', '/dev/vdb', + '--mode', 'null', + ] + verifylist = [ + ('volume', self.volume.id), + ('server', self.server.id), + ('connect', True), + ('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'), + ('ip', '192.168.1.20'), + ('host', 'my-host'), + ('platform', 'x86_64'), + ('os_type', 'linux2'), + ('multipath', True), + ('mountpoint', '/dev/vdb'), + ('mode', 'null'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + connect_info = dict([ + ('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'), + ('ip', '192.168.1.20'), + ('host', 'my-host'), + ('platform', 'x86_64'), + ('os_type', 'linux2'), + ('multipath', True), + ('mountpoint', '/dev/vdb'), + ]) + + self.volumes_mock.get.assert_called_once_with(self.volume.id) + self.servers_mock.get.assert_called_once_with(self.server.id) + self.volume_attachments_mock.create.assert_called_once_with( + self.volume.id, connect_info, self.server.id, 'null', + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_attachment_create_pre_v327(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.26') + + arglist = [ + self.volume.id, + self.server.id, + ] + verifylist = [ + ('volume', self.volume.id), + ('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( + '--os-volume-api-version 3.27 or greater is required', + str(exc)) + + def test_volume_attachment_create_with_mode_pre_v354(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.53') + + arglist = [ + self.volume.id, + self.server.id, + '--mode', 'rw', + ] + verifylist = [ + ('volume', self.volume.id), + ('server', self.server.id), + ('mode', 'rw'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.54 or greater is required', + str(exc)) + + def test_volume_attachment_create_with_connect_missing_arg(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.54') + + arglist = [ + self.volume.id, + self.server.id, + '--initiator', 'iqn.1993-08.org.debian:01:cad181614cec', + ] + verifylist = [ + ('volume', self.volume.id), + ('server', self.server.id), + ('connect', False), + ('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + 'You must specify the --connect option for any', + str(exc)) + + +class TestVolumeAttachmentDelete(TestVolumeAttachment): + + volume_attachment = \ + volume_fakes.FakeVolumeAttachment.create_one_volume_attachment() + + def setUp(self): + super().setUp() + + self.volume_attachments_mock.delete.return_value = None + + self.cmd = volume_attachment.DeleteVolumeAttachment(self.app, None) + + def test_volume_attachment_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.27') + + arglist = [ + self.volume_attachment.id, + ] + verifylist = [ + ('attachment', self.volume_attachment.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_attachments_mock.delete.assert_called_once_with( + self.volume_attachment.id, + ) + self.assertIsNone(result) + + def test_volume_attachment_delete_pre_v327(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.26') + + arglist = [ + self.volume_attachment.id, + ] + verifylist = [ + ('attachment', self.volume_attachment.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.27 or greater is required', + str(exc)) + + +class TestVolumeAttachmentSet(TestVolumeAttachment): + + volume_attachment = \ + volume_fakes.FakeVolumeAttachment.create_one_volume_attachment() + + columns = ( + 'ID', + 'Volume ID', + 'Instance ID', + 'Status', + 'Attach Mode', + 'Attached At', + 'Detached At', + 'Properties', + ) + data = ( + volume_attachment.id, + volume_attachment.volume_id, + volume_attachment.instance, + volume_attachment.status, + volume_attachment.attach_mode, + volume_attachment.attached_at, + volume_attachment.detached_at, + format_columns.DictColumn(volume_attachment.connection_info), + ) + + def setUp(self): + super().setUp() + + self.volume_attachments_mock.update.return_value = \ + self.volume_attachment + + self.cmd = volume_attachment.SetVolumeAttachment(self.app, None) + + def test_volume_attachment_set(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.27') + + arglist = [ + self.volume_attachment.id, + '--initiator', 'iqn.1993-08.org.debian:01:cad181614cec', + '--ip', '192.168.1.20', + '--host', 'my-host', + '--platform', 'x86_64', + '--os-type', 'linux2', + '--multipath', + '--mountpoint', '/dev/vdb', + ] + verifylist = [ + ('attachment', self.volume_attachment.id), + ('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'), + ('ip', '192.168.1.20'), + ('host', 'my-host'), + ('platform', 'x86_64'), + ('os_type', 'linux2'), + ('multipath', True), + ('mountpoint', '/dev/vdb'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + connect_info = dict([ + ('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'), + ('ip', '192.168.1.20'), + ('host', 'my-host'), + ('platform', 'x86_64'), + ('os_type', 'linux2'), + ('multipath', True), + ('mountpoint', '/dev/vdb'), + ]) + + self.volume_attachments_mock.update.assert_called_once_with( + self.volume_attachment.id, connect_info, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_attachment_set_pre_v327(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.26') + + arglist = [ + self.volume_attachment.id, + '--initiator', 'iqn.1993-08.org.debian:01:cad181614cec', + ] + verifylist = [ + ('attachment', self.volume_attachment.id), + ('initiator', 'iqn.1993-08.org.debian:01:cad181614cec'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.27 or greater is required', + str(exc)) + + +class TestVolumeAttachmentComplete(TestVolumeAttachment): + + volume_attachment = \ + volume_fakes.FakeVolumeAttachment.create_one_volume_attachment() + + def setUp(self): + super().setUp() + + self.volume_attachments_mock.complete.return_value = None + + self.cmd = volume_attachment.CompleteVolumeAttachment(self.app, None) + + def test_volume_attachment_complete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.44') + + arglist = [ + self.volume_attachment.id, + ] + verifylist = [ + ('attachment', self.volume_attachment.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_attachments_mock.complete.assert_called_once_with( + self.volume_attachment.id, + ) + self.assertIsNone(result) + + def test_volume_attachment_complete_pre_v344(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.43') + + arglist = [ + self.volume_attachment.id, + ] + verifylist = [ + ('attachment', self.volume_attachment.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.44 or greater is required', + str(exc)) + + +class TestVolumeAttachmentList(TestVolumeAttachment): + + project = identity_fakes.FakeProject.create_one_project() + volume_attachments = \ + volume_fakes.FakeVolumeAttachment.create_volume_attachments() + + columns = ( + 'ID', + 'Volume ID', + 'Server ID', + 'Status', + ) + data = [ + ( + volume_attachment.id, + volume_attachment.volume_id, + volume_attachment.instance, + volume_attachment.status, + ) for volume_attachment in volume_attachments + ] + + def setUp(self): + super().setUp() + + self.projects_mock.get.return_value = self.project + self.volume_attachments_mock.list.return_value = \ + self.volume_attachments + + self.cmd = volume_attachment.ListVolumeAttachment(self.app, None) + + def test_volume_attachment_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.27') + + arglist = [] + verifylist = [ + ('project', None), + ('all_projects', False), + ('volume_id', None), + ('status', None), + ('marker', None), + ('limit', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_attachments_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': False, + 'project_id': None, + 'status': None, + 'volume_id': None, + }, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), data) + + def test_volume_attachment_list_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.27') + + arglist = [ + '--project', self.project.name, + '--volume-id', 'volume-id', + '--status', 'attached', + '--marker', 'volume-attachment-id', + '--limit', '2', + ] + verifylist = [ + ('project', self.project.name), + ('all_projects', False), + ('volume_id', 'volume-id'), + ('status', 'attached'), + ('marker', 'volume-attachment-id'), + ('limit', 2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_attachments_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': True, + 'project_id': self.project.id, + 'status': 'attached', + 'volume_id': 'volume-id', + }, + marker='volume-attachment-id', + limit=2, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), data) + + def test_volume_attachment_list_pre_v327(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.26') + + arglist = [] + verifylist = [ + ('project', None), + ('all_projects', False), + ('volume_id', None), + ('status', None), + ('marker', None), + ('limit', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.27 or greater is required', + str(exc)) diff --git a/openstackclient/volume/v3/volume_attachment.py b/openstackclient/volume/v3/volume_attachment.py new file mode 100644 index 0000000000..39a9c37fdb --- /dev/null +++ b/openstackclient/volume/v3/volume_attachment.py @@ -0,0 +1,511 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from cinderclient import api_versions +from osc_lib.cli import format_columns +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + +_FILTER_DEPRECATED = _( + "This option is deprecated. Consider using the '--filters' option which " + "was introduced in microversion 3.33 instead." +) + + +def _format_attachment(attachment): + columns = ( + 'id', + 'volume_id', + 'instance', + 'status', + 'attach_mode', + 'attached_at', + 'detached_at', + 'connection_info', + ) + column_headers = ( + 'ID', + 'Volume ID', + 'Instance ID', + 'Status', + 'Attach Mode', + 'Attached At', + 'Detached At', + 'Properties', + ) + + # TODO(stephenfin): Improve output with the nested connection_info + # field - cinderclient printed two things but that's equally ugly + return ( + column_headers, + utils.get_item_properties( + attachment, + columns, + formatters={ + 'connection_info': format_columns.DictColumn, + }, + ), + ) + + +class CreateVolumeAttachment(command.ShowOne): + """Create an attachment for a volume. + + This command will only create a volume attachment in the Volume service. It + will not invoke the necessary Compute service actions to actually attach + 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. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'volume', + metavar='', + help=_('Name or ID of volume to attach to server.'), + ) + parser.add_argument( + 'server', + metavar='', + help=_('Name or ID of server to attach volume to.'), + ) + parser.add_argument( + '--connect', + action='store_true', + dest='connect', + default=False, + help=_('Make an active connection using provided connector info'), + ) + parser.add_argument( + '--no-connect', + action='store_false', + dest='connect', + help=_( + 'Do not make an active connection using provided connector ' + 'info' + ), + ) + parser.add_argument( + '--initiator', + metavar='', + help=_('IQN of the initiator attaching to'), + ) + parser.add_argument( + '--ip', + metavar='', + help=_('IP of the system attaching to'), + ) + parser.add_argument( + '--host', + metavar='', + help=_('Name of the host attaching to'), + ) + parser.add_argument( + '--platform', + metavar='', + help=_('Platform type'), + ) + parser.add_argument( + '--os-type', + metavar='', + help=_('OS type'), + ) + parser.add_argument( + '--multipath', + action='store_true', + dest='multipath', + default=False, + help=_('Use multipath'), + ) + parser.add_argument( + '--no-multipath', + action='store_false', + dest='multipath', + help=_('Use multipath'), + ) + parser.add_argument( + '--mountpoint', + metavar='', + help=_('Mountpoint volume will be attached at'), + ) + parser.add_argument( + '--mode', + metavar='', + help=_( + 'Mode of volume attachment, rw, ro and null, where null ' + 'indicates we want to honor any existing admin-metadata ' + 'settings ' + '(supported by --os-volume-api-version 3.54 or later)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.compute + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment create' command" + ) + raise exceptions.CommandError(msg) + + if parsed_args.mode: + if volume_client.api_version < api_versions.APIVersion('3.54'): + msg = _( + "--os-volume-api-version 3.54 or greater is required to " + "support the '--mode' option" + ) + raise exceptions.CommandError(msg) + + connector = {} + if parsed_args.connect: + connector = { + 'initiator': parsed_args.initiator, + 'ip': parsed_args.ip, + 'platform': parsed_args.platform, + 'host': parsed_args.host, + 'os_type': parsed_args.os_type, + 'multipath': parsed_args.multipath, + 'mountpoint': parsed_args.mountpoint, + } + else: + if any({ + parsed_args.initiator, + parsed_args.ip, + parsed_args.platform, + parsed_args.host, + parsed_args.host, + parsed_args.multipath, + parsed_args.mountpoint, + }): + msg = _( + 'You must specify the --connect option for any of the ' + 'connection-specific options such as --initiator to be ' + 'valid' + ) + raise exceptions.CommandError(msg) + + volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ) + server = utils.find_resource( + compute_client.servers, + parsed_args.server, + ) + + attachment = volume_client.attachments.create( + volume.id, connector, server.id, parsed_args.mode) + + return _format_attachment(attachment) + + +class DeleteVolumeAttachment(command.Command): + """Delete an attachment for a volume. + + Similarly to the 'volume attachment create' command, this command will only + delete the volume attachment record in the Volume service. It will not + invoke the necessary Compute service actions to actually attach 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 + remove' command should be preferred. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='', + help=_('ID of volume attachment to delete'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment delete' command" + ) + raise exceptions.CommandError(msg) + + volume_client.attachments.delete(parsed_args.attachment) + + +class SetVolumeAttachment(command.ShowOne): + """Update an attachment for a volume. + + This call is designed to be more of an volume attachment completion than + anything else. It expects the value of a connector object to notify the + driver that the volume is going to be connected and where it's being + connected to. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='', + help=_('ID of volume attachment.'), + ) + parser.add_argument( + '--initiator', + metavar='', + help=_('IQN of the initiator attaching to'), + ) + parser.add_argument( + '--ip', + metavar='', + help=_('IP of the system attaching to'), + ) + parser.add_argument( + '--host', + metavar='', + help=_('Name of the host attaching to'), + ) + parser.add_argument( + '--platform', + metavar='', + help=_('Platform type'), + ) + parser.add_argument( + '--os-type', + metavar='', + help=_('OS type'), + ) + parser.add_argument( + '--multipath', + action='store_true', + dest='multipath', + default=False, + help=_('Use multipath'), + ) + parser.add_argument( + '--no-multipath', + action='store_false', + dest='multipath', + help=_('Use multipath'), + ) + parser.add_argument( + '--mountpoint', + metavar='', + help=_('Mountpoint volume will be attached at'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment set' command" + ) + raise exceptions.CommandError(msg) + + connector = { + 'initiator': parsed_args.initiator, + 'ip': parsed_args.ip, + 'platform': parsed_args.platform, + 'host': parsed_args.host, + 'os_type': parsed_args.os_type, + 'multipath': parsed_args.multipath, + 'mountpoint': parsed_args.mountpoint, + } + + attachment = volume_client.attachments.update( + parsed_args.attachment, connector) + + return _format_attachment(attachment) + + +class CompleteVolumeAttachment(command.Command): + """Complete an attachment for a volume.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='', + help=_('ID of volume attachment to mark as completed'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.44'): + msg = _( + "--os-volume-api-version 3.44 or greater is required to " + "support the 'volume attachment complete' command" + ) + raise exceptions.CommandError(msg) + + volume_client.attachments.complete(parsed_args.attachment) + + +class ListVolumeAttachment(command.Lister): + """Lists all volume attachments.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--project', + dest='project', + metavar='', + help=_('Filter results by project (name or ID) (admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--all-projects', + dest='all_projects', + action='store_true', + default=utils.env('ALL_PROJECTS', default=False), + help=_('Shows details for all projects (admin only).'), + ) + parser.add_argument( + '--volume-id', + metavar='', + default=None, + help=_('Filters results by a volume ID. ') + _FILTER_DEPRECATED, + ) + parser.add_argument( + '--status', + metavar='', + help=_('Filters results by a status. ') + _FILTER_DEPRECATED, + ) + parser.add_argument( + '--marker', + metavar='', + help=_( + 'Begin returning volume attachments that appear later in ' + 'volume attachment list than that represented by this ID.' + ), + ) + parser.add_argument( + '--limit', + type=int, + metavar='', + help=_('Maximum number of volume attachments to return.'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment list' command" + ) + raise exceptions.CommandError(msg) + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + + search_opts = { + 'all_tenants': True if project_id else parsed_args.all_projects, + 'project_id': project_id, + 'status': parsed_args.status, + 'volume_id': parsed_args.volume_id, + } + # Update search option with `filters` + # if AppendFilters.filters: + # search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) + + # TODO(stephenfin): Implement sorting + attachments = volume_client.attachments.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit) + + column_headers = ( + 'ID', + 'Volume ID', + 'Server ID', + 'Status', + ) + columns = ( + 'id', + 'volume_id', + 'instance', + 'status', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in attachments + ), + ) + + +class ShowVolumeAttachment(command.ShowOne): + """Show detailed information for a volume attachment.""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'attachment', + metavar='', + help=_('ID of volume attachment.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.27'): + msg = _( + "--os-volume-api-version 3.27 or greater is required to " + "support the 'volume attachment show' command" + ) + raise exceptions.CommandError(msg) + + attachment = volume_client.attachments.show(parsed_args.attachment) + + return _format_attachment(attachment) diff --git a/releasenotes/notes/add-volume-attachment-commands-db2974c6460fa3bc.yaml b/releasenotes/notes/add-volume-attachment-commands-db2974c6460fa3bc.yaml new file mode 100644 index 0000000000..8355cea55a --- /dev/null +++ b/releasenotes/notes/add-volume-attachment-commands-db2974c6460fa3bc.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``volume attachment create``, ``volume attachment delete``, + ``volume attachment list``, ``volume attachment complete``, + ``volume attachment set`` and ``volume attachment show`` commands to + create, delete, list, complete, update and show volume attachments, + respectively. diff --git a/setup.cfg b/setup.cfg index d6d7a3d2b0..792e5a8584 100644 --- a/setup.cfg +++ b/setup.cfg @@ -697,6 +697,13 @@ openstack.volume.v3 = volume_show = openstackclient.volume.v2.volume:ShowVolume volume_unset = openstackclient.volume.v2.volume:UnsetVolume + volume_attachment_create = openstackclient.volume.v3.volume_attachment:CreateVolumeAttachment + volume_attachment_delete = openstackclient.volume.v3.volume_attachment:DeleteVolumeAttachment + volume_attachment_list = openstackclient.volume.v3.volume_attachment:ListVolumeAttachment + volume_attachment_complete = openstackclient.volume.v3.volume_attachment:CompleteVolumeAttachment + volume_attachment_set = openstackclient.volume.v3.volume_attachment:SetVolumeAttachment + volume_attachment_show = openstackclient.volume.v3.volume_attachment:ShowVolumeAttachment + volume_backup_create = openstackclient.volume.v2.volume_backup:CreateVolumeBackup volume_backup_delete = openstackclient.volume.v2.volume_backup:DeleteVolumeBackup volume_backup_list = openstackclient.volume.v2.volume_backup:ListVolumeBackup From 0eddab36e52e813e2329ac10044fa4f67830efec Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 19 Mar 2021 11:34:31 +0000 Subject: [PATCH 0411/1120] volume: Add 'volume message *' commands This patch implements the necessary commands to utilize the Messages API introduced in Cinder API version 3.3. Version 3.5 built upon this by implementing pagination support for these commands which is present in this patch as well. volume message get volume message list volume message delete Change-Id: I64aa0b4a8d4468baa8c63e5e30ee31de68df999d --- .../cli/command-objects/volume-message.rst | 8 + doc/source/cli/commands.rst | 1 + doc/source/cli/data/cinder.csv | 6 +- openstackclient/tests/unit/volume/v3/fakes.py | 68 ++++ .../unit/volume/v3/test_volume_message.py | 324 ++++++++++++++++++ openstackclient/volume/v3/volume_message.py | 165 +++++++++ ...ume-message-commands-89a590a1549c333e.yaml | 6 + setup.cfg | 4 + 8 files changed, 579 insertions(+), 3 deletions(-) create mode 100644 doc/source/cli/command-objects/volume-message.rst create mode 100644 openstackclient/tests/unit/volume/v3/test_volume_message.py create mode 100644 openstackclient/volume/v3/volume_message.py create mode 100644 releasenotes/notes/add-volume-message-commands-89a590a1549c333e.yaml diff --git a/doc/source/cli/command-objects/volume-message.rst b/doc/source/cli/command-objects/volume-message.rst new file mode 100644 index 0000000000..5b1a8acef2 --- /dev/null +++ b/doc/source/cli/command-objects/volume-message.rst @@ -0,0 +1,8 @@ +============== +volume message +============== + +Block Storage v3 + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume message * diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 00f6b23a77..cc95b3d121 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -160,6 +160,7 @@ referring to both Compute and Volume quotas. * ``volume backup record``: (**Volume**) volume record that can be imported or exported * ``volume backend``: (**Volume**) volume backend storage * ``volume host``: (**Volume**) the physical computer for volumes +* ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes * ``volume snapshot``: (**Volume**) a point-in-time copy of a volume * ``volume type``: (**Volume**) deployment-specific types of volumes available diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index dc43ab5b1a..f94552ea93 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -72,9 +72,9 @@ list,volume list,Lists all volumes. list-filters,,List enabled filters. (Supported by API versions 3.33 - 3.latest) manage,volume create --remote-source k=v,Manage an existing volume. manageable-list,,Lists all manageable volumes. (Supported by API versions 3.8 - 3.latest) -message-delete,,Removes one or more messages. (Supported by API versions 3.3 - 3.latest) -message-list,,Lists all messages. (Supported by API versions 3.3 - 3.latest) -message-show,,Shows message details. (Supported by API versions 3.3 - 3.latest) +message-delete,volume message delete,Removes one or more messages. (Supported by API versions 3.3 - 3.latest) +message-list,volume message list,Lists all messages. (Supported by API versions 3.3 - 3.latest) +message-show,volume message show,Shows message details. (Supported by API versions 3.3 - 3.latest) metadata,volume set --property k=v / volume unset --property k,Sets or deletes volume metadata. metadata-show,volume show,Shows volume metadata. metadata-update-all,volume set --property k=v,Updates volume metadata. diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index fb3b1b74e1..45cad8c14c 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -32,6 +32,8 @@ def __init__(self, **kwargs): self.attachments = mock.Mock() self.attachments.resource_class = fakes.FakeResource(None, {}) + self.messages = mock.Mock() + self.messages.resource_class = fakes.FakeResource(None, {}) self.volumes = mock.Mock() self.volumes.resource_class = fakes.FakeResource(None, {}) @@ -59,6 +61,72 @@ def setUp(self): FakeVolume = volume_v2_fakes.FakeVolume +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) + + message = fakes.FakeResource( + None, + message_info, + loaded=True) + return message + + @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.""" diff --git a/openstackclient/tests/unit/volume/v3/test_volume_message.py b/openstackclient/tests/unit/volume/v3/test_volume_message.py new file mode 100644 index 0000000000..68becf4418 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_message.py @@ -0,0 +1,324 @@ +# 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 unittest.mock import call + +from cinderclient import api_versions +from osc_lib import exceptions + +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_message + + +class TestVolumeMessage(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + self.volume_messages_mock = self.app.client_manager.volume.messages + self.volume_messages_mock.reset_mock() + + +class TestVolumeMessageDelete(TestVolumeMessage): + + fake_messages = volume_fakes.FakeVolumeMessage.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.delete.return_value = None + + # Get the command object to mock + self.cmd = volume_message.DeleteMessage(self.app, None) + + def test_message_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_messages[0].id, + ] + verifylist = [ + ('message_ids', [self.fake_messages[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_messages_mock.delete.assert_called_with( + self.fake_messages[0].id) + self.assertIsNone(result) + + def test_message_delete_multiple_messages(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_messages[0].id, + self.fake_messages[1].id, + ] + verifylist = [ + ('message_ids', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for m in self.fake_messages: + calls.append(call(m.id)) + self.volume_messages_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_message_delete_multiple_messages_with_exception(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_messages[0].id, + 'invalid_message', + ] + verifylist = [ + ('message_ids', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.volume_messages_mock.delete.side_effect = [ + self.fake_messages[0], exceptions.CommandError] + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertEqual('Failed to delete 1 of 2 messages.', str(exc)) + + self.volume_messages_mock.delete.assert_any_call( + self.fake_messages[0].id) + self.volume_messages_mock.delete.assert_any_call('invalid_message') + + self.assertEqual(2, self.volume_messages_mock.delete.call_count) + + def test_message_delete_pre_v33(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.2') + + arglist = [ + self.fake_messages[0].id, + ] + verifylist = [ + ('message_ids', [self.fake_messages[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.3 or greater is required', + str(exc)) + + +class TestVolumeMessageList(TestVolumeMessage): + + fake_project = identity_fakes.FakeProject.create_one_project() + fake_messages = volume_fakes.FakeVolumeMessage.create_volume_messages( + count=3) + + columns = ( + 'ID', + 'Event ID', + 'Resource Type', + 'Resource UUID', + 'Message Level', + 'User Message', + 'Request ID', + 'Created At', + 'Guaranteed Until', + ) + data = [] + for fake_message in fake_messages: + data.append(( + fake_message.id, + fake_message.event_id, + fake_message.resource_type, + fake_message.resource_uuid, + fake_message.message_level, + fake_message.user_message, + fake_message.request_id, + fake_message.created_at, + fake_message.guaranteed_until, + )) + + def setUp(self): + super().setUp() + + self.projects_mock.get.return_value = self.fake_project + self.volume_messages_mock.list.return_value = self.fake_messages + # Get the command to test + self.cmd = volume_message.ListMessages(self.app, None) + + def test_message_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [] + verifylist = [ + ('project', None), + ('marker', None), + ('limit', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'project_id': None, + } + self.volume_messages_mock.list.assert_called_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_message_list_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + '--project', self.fake_project.name, + '--marker', self.fake_messages[0].id, + '--limit', '3', + ] + verifylist = [ + ('project', self.fake_project.name), + ('marker', self.fake_messages[0].id), + ('limit', 3), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'project_id': self.fake_project.id, + } + self.volume_messages_mock.list.assert_called_with( + search_opts=search_opts, + marker=self.fake_messages[0].id, + limit=3, + ) + self.assertEqual(self.columns, columns) + self.assertItemsEqual(self.data, list(data)) + + def test_message_list_pre_v33(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.2') + + arglist = [] + verifylist = [ + ('project', None), + ('marker', None), + ('limit', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.3 or greater is required', + str(exc)) + + +class TestVolumeMessageShow(TestVolumeMessage): + + fake_message = volume_fakes.FakeVolumeMessage.create_one_volume_message() + + columns = ( + 'created_at', + 'event_id', + 'guaranteed_until', + 'id', + 'message_level', + 'request_id', + 'resource_type', + 'resource_uuid', + 'user_message', + ) + data = ( + fake_message.created_at, + fake_message.event_id, + fake_message.guaranteed_until, + fake_message.id, + fake_message.message_level, + fake_message.request_id, + fake_message.resource_type, + fake_message.resource_uuid, + fake_message.user_message, + ) + + def setUp(self): + super().setUp() + + self.volume_messages_mock.get.return_value = self.fake_message + # Get the command object to test + self.cmd = volume_message.ShowMessage(self.app, None) + + def test_message_show(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.3') + + arglist = [ + self.fake_message.id + ] + verifylist = [ + ('message_id', self.fake_message.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.volume_messages_mock.get.assert_called_with(self.fake_message.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_message_show_pre_v33(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.2') + + arglist = [ + self.fake_message.id + ] + verifylist = [ + ('message_id', self.fake_message.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.3 or greater is required', + str(exc)) diff --git a/openstackclient/volume/v3/volume_message.py b/openstackclient/volume/v3/volume_message.py new file mode 100644 index 0000000000..4fe5ae92f6 --- /dev/null +++ b/openstackclient/volume/v3/volume_message.py @@ -0,0 +1,165 @@ +# +# 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. +# + +"""Volume V3 Messages implementations""" + +import logging as LOG + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +class DeleteMessage(command.Command): + _description = _('Delete a volume failure message') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'message_ids', + metavar='', + nargs='+', + help=_('Message(s) to delete (ID)') + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message delete' command" + ) + raise exceptions.CommandError(msg) + + errors = 0 + for message_id in parsed_args.message_ids: + try: + volume_client.messages.delete(message_id) + except Exception: + LOG.error(_('Failed to delete message: %s'), message_id) + errors += 1 + + if errors > 0: + total = len(parsed_args.message_ids) + msg = _('Failed to delete %(errors)s of %(total)s messages.') % { + 'errors': errors, 'total': total, + } + raise exceptions.CommandError(msg) + + +class ListMessages(command.Lister): + _description = _('List volume failure messages') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + + parser.add_argument( + '--project', + metavar='', + help=_('Filter results by project (name or ID) (admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--marker', + metavar='', + help=_('The last message ID of the previous page'), + default=None, + ) + parser.add_argument( + '--limit', + type=int, + metavar='', + help=_('Maximum number of messages to display'), + default=None, + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message list' command" + ) + raise exceptions.CommandError(msg) + + column_headers = ( + 'ID', + 'Event ID', + 'Resource Type', + 'Resource UUID', + 'Message Level', + 'User Message', + 'Request ID', + 'Created At', + 'Guaranteed Until', + ) + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain).id + + search_opts = { + 'project_id': project_id, + } + data = volume_client.messages.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit) + + return ( + column_headers, + (utils.get_item_properties(s, column_headers) for s in data) + ) + + +class ShowMessage(command.ShowOne): + _description = _('Show a volume failure message') + + def get_parser(self, prog_name): + parser = super(ShowMessage, self).get_parser(prog_name) + parser.add_argument( + 'message_id', + metavar='', + help=_('Message to show (ID).') + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.3'): + msg = _( + "--os-volume-api-version 3.3 or greater is required to " + "support the 'volume message show' command" + ) + raise exceptions.CommandError(msg) + + message = volume_client.messages.get(parsed_args.message_id) + + return zip(*sorted(message._info.items())) diff --git a/releasenotes/notes/add-volume-message-commands-89a590a1549c333e.yaml b/releasenotes/notes/add-volume-message-commands-89a590a1549c333e.yaml new file mode 100644 index 0000000000..eba53524e7 --- /dev/null +++ b/releasenotes/notes/add-volume-message-commands-89a590a1549c333e.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``volume message list``, ``volume message get`` and + ``volume message delete`` commands, to list, get and delete volume + failure messages, respectively. diff --git a/setup.cfg b/setup.cfg index 792e5a8584..d593762ac9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -716,6 +716,10 @@ openstack.volume.v3 = volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost + volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage + volume_message_list = openstackclient.volume.v3.volume_message:ListMessages + volume_message_show = openstackclient.volume.v3.volume_message:ShowMessage + volume_snapshot_create = openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot volume_snapshot_delete = openstackclient.volume.v2.volume_snapshot:DeleteVolumeSnapshot volume_snapshot_list = openstackclient.volume.v2.volume_snapshot:ListVolumeSnapshot From 524af4a23efde62989ad55ecebabff0b50395308 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 26 May 2021 14:42:52 +0100 Subject: [PATCH 0412/1120] volume: Add missing 'volume backup *' options Add a couple of missing options to each command: volume backup create --no-incremental --property --availability-zone volume backup set --property Most of these are version dependent so we add the relevant version checks as part of this work. While we're here, we also make the formatting a little easier on the eye in places. Change-Id: I328d5c981cb32b2ee9a4b1bd43aa36b22347ff63 Signed-off-by: Stephen Finucane --- doc/source/cli/data/cinder.csv | 2 +- .../unit/volume/v2/test_volume_backup.py | 155 ++++++++++++- openstackclient/volume/v2/volume_backup.py | 219 ++++++++++++++---- ...g-volume-backup-opts-b9246aded87427ce.yaml | 15 ++ 4 files changed, 337 insertions(+), 54 deletions(-) create mode 100644 releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index f94552ea93..649cd8f59a 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -15,7 +15,7 @@ backup-list,volume backup list,Lists all backups. backup-reset-state,volume backup set --state,Explicitly updates the backup state. backup-restore,volume backup restore,Restores a backup. backup-show,volume backup show,Show backup details. -backup-update,,Updates a backup. (Supported by API versions 3.9 - 3.latest) +backup-update,volume backup set,Updates a backup. (Supported by API versions 3.9 - 3.latest) cgsnapshot-create,consistency group snapshot create,Creates a cgsnapshot. cgsnapshot-delete,consistency group snapshot delete,Removes one or more cgsnapshots. cgsnapshot-list,consistency group snapshot list,Lists all cgsnapshots. diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index 13513ed8ad..7b5a965e09 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -15,6 +15,7 @@ from unittest import mock from unittest.mock import call +from cinderclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -114,6 +115,104 @@ def test_backup_create(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_backup_create_with_properties(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.43') + + arglist = [ + "--property", "foo=bar", + "--property", "wow=much-cool", + self.new_backup.volume_id, + ] + verifylist = [ + ("properties", {"foo": "bar", "wow": "much-cool"}), + ("volume", self.new_backup.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.backups_mock.create.assert_called_with( + self.new_backup.volume_id, + container=None, + name=None, + description=None, + force=False, + incremental=False, + metadata={"foo": "bar", "wow": "much-cool"}, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_backup_create_with_properties_pre_v343(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.42') + + arglist = [ + "--property", "foo=bar", + "--property", "wow=much-cool", + self.new_backup.volume_id, + ] + verifylist = [ + ("properties", {"foo": "bar", "wow": "much-cool"}), + ("volume", self.new_backup.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.43 or greater", str(exc)) + + def test_backup_create_with_availability_zone(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.51') + + arglist = [ + "--availability-zone", "my-az", + self.new_backup.volume_id, + ] + verifylist = [ + ("availability_zone", "my-az"), + ("volume", self.new_backup.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.backups_mock.create.assert_called_with( + self.new_backup.volume_id, + container=None, + name=None, + description=None, + force=False, + incremental=False, + availability_zone="my-az", + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_backup_create_with_availability_zone_pre_v351(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.50') + + arglist = [ + "--availability-zone", "my-az", + self.new_backup.volume_id, + ] + verifylist = [ + ("availability_zone", "my-az"), + ("volume", self.new_backup.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.51 or greater", str(exc)) + def test_backup_create_without_name(self): arglist = [ "--description", self.new_backup.description, @@ -136,7 +235,6 @@ def test_backup_create_without_name(self): description=self.new_backup.description, force=False, incremental=False, - snapshot_id=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -240,18 +338,18 @@ class TestBackupList(TestBackup): backups = volume_fakes.FakeBackup.create_backups( attrs={'volume_id': volume.name}, count=3) - columns = [ + columns = ( 'ID', 'Name', 'Description', 'Status', 'Size', - ] - columns_long = columns + [ + ) + columns_long = columns + ( 'Availability Zone', 'Volume', 'Container', - ] + ) data = [] for b in backups: @@ -403,6 +501,9 @@ def setUp(self): self.cmd = volume_backup.SetVolumeBackup(self.app, None) def test_backup_set_name(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.9') + arglist = [ '--name', 'new_name', self.backup.id, @@ -420,7 +521,30 @@ def test_backup_set_name(self): self.backup.id, **{'name': 'new_name'}) self.assertIsNone(result) + def test_backup_set_name_pre_v39(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.8') + + arglist = [ + '--name', 'new_name', + self.backup.id, + ] + verifylist = [ + ('name', 'new_name'), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.9 or greater", str(exc)) + def test_backup_set_description(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.9') + arglist = [ '--description', 'new_description', self.backup.id, @@ -444,6 +568,27 @@ def test_backup_set_description(self): ) self.assertIsNone(result) + def test_backup_set_description_pre_v39(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.8') + + arglist = [ + '--description', 'new_description', + self.backup.id, + ] + verifylist = [ + ('name', None), + ('description', 'new_description'), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.9 or greater", str(exc)) + def test_backup_set_state(self): arglist = [ '--state', 'error', diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index c336f6c9ac..a171164e3a 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -14,10 +14,10 @@ """Volume v2 Backup action implementations""" -import copy import functools import logging +from cinderclient import api_versions from cliff import columns as cliff_columns from osc_lib.cli import parseractions from osc_lib.command import command @@ -61,7 +61,7 @@ class CreateVolumeBackup(command.ShowOne): _description = _("Create new volume backup") def get_parser(self, prog_name): - parser = super(CreateVolumeBackup, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "volume", metavar="", @@ -99,16 +99,67 @@ def get_parser(self, prog_name): default=False, help=_("Perform an incremental backup") ) + parser.add_argument( + '--no-incremental', + action='store_false', + help=_("Do not perform an incremental backup") + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Set a property on this backup ' + '(repeat option to remove multiple values) ' + '(supported by --os-volume-api-version 3.43 or above)' + ), + ) + parser.add_argument( + '--availability-zone', + metavar='', + help=_( + 'AZ where the backup should be stored; by default it will be ' + 'the same as the source ' + '(supported by --os-volume-api-version 3.51 or above)' + ), + ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + volume_id = utils.find_resource( - volume_client.volumes, parsed_args.volume).id - snapshot_id = None + volume_client.volumes, parsed_args.volume, + ).id + + kwargs = {} + if parsed_args.snapshot: - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot).id + kwargs['snapshot_id'] = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot, + ).id + + if parsed_args.properties: + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --property option' + ) + raise exceptions.CommandError(msg) + + kwargs['metadata'] = parsed_args.properties + + if parsed_args.availability_zone: + if volume_client.api_version < api_versions.APIVersion('3.51'): + msg = _( + '--os-volume-api-version 3.51 or greater is required to ' + 'support the --availability-zone option' + ) + raise exceptions.CommandError(msg) + + kwargs['availability_zone'] = parsed_args.availability_zone + backup = volume_client.backups.create( volume_id, container=parsed_args.container, @@ -116,7 +167,7 @@ def take_action(self, parsed_args): description=parsed_args.description, force=parsed_args.force, incremental=parsed_args.incremental, - snapshot_id=snapshot_id, + **kwargs, ) backup._info.pop("links", None) return zip(*sorted(backup._info.items())) @@ -148,7 +199,8 @@ def take_action(self, parsed_args): for i in parsed_args.backups: try: backup_id = utils.find_resource( - volume_client.backups, i).id + volume_client.backups, i, + ).id volume_client.backups.delete(backup_id, parsed_args.force) except Exception as e: result += 1 @@ -158,8 +210,9 @@ def take_action(self, parsed_args): if result > 0: total = len(parsed_args.backups) - msg = (_("%(result)s of %(total)s backups failed " - "to delete.") % {'result': result, 'total': total}) + msg = _("%(result)s of %(total)s backups failed to delete.") % { + 'result': result, 'total': total, + } raise exceptions.CommandError(msg) @@ -182,17 +235,22 @@ def get_parser(self, prog_name): parser.add_argument( "--status", metavar="", - choices=['creating', 'available', 'deleting', - 'error', 'restoring', 'error_restoring'], - help=_("Filters results by the backup status " - "('creating', 'available', 'deleting', " - "'error', 'restoring' or 'error_restoring')") + choices=[ + 'creating', 'available', 'deleting', + 'error', 'restoring', 'error_restoring', + ], + help=_( + "Filters results by the backup status, one of: " + "creating, available, deleting, error, restoring or " + "error_restoring" + ), ) parser.add_argument( "--volume", metavar="", - help=_("Filters results by the volume which they " - "backup (name or ID)") + help=_( + "Filters results by the volume which they backup (name or ID)" + ), ) parser.add_argument( '--marker', @@ -212,19 +270,30 @@ def get_parser(self, prog_name): default=False, help=_('Include all projects (admin only)'), ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + columns = ('id', 'name', 'description', 'status', 'size') + column_headers = ('ID', 'Name', 'Description', 'Status', 'Size') if parsed_args.long: - columns = ['ID', 'Name', 'Description', 'Status', 'Size', - 'Availability Zone', 'Volume ID', 'Container'] - column_headers = copy.deepcopy(columns) - column_headers[6] = 'Volume' - else: - columns = ['ID', 'Name', 'Description', 'Status', 'Size'] - column_headers = columns + columns += ('availability_zone', 'volume_id', 'container') + column_headers += ('Availability Zone', 'Volume', 'Container') # Cache the volume list volume_cache = {} @@ -234,17 +303,22 @@ def take_action(self, parsed_args): except Exception: # Just forget it if there's any trouble pass - _VolumeIdColumn = functools.partial(VolumeIdColumn, - volume_cache=volume_cache) + + _VolumeIdColumn = functools.partial( + VolumeIdColumn, volume_cache=volume_cache) filter_volume_id = None if parsed_args.volume: - filter_volume_id = utils.find_resource(volume_client.volumes, - parsed_args.volume).id + filter_volume_id = utils.find_resource( + volume_client.volumes, parsed_args.volume, + ).id + marker_backup_id = None if parsed_args.marker: - marker_backup_id = utils.find_resource(volume_client.backups, - parsed_args.marker).id + marker_backup_id = utils.find_resource( + volume_client.backups, parsed_args.marker, + ).id + search_opts = { 'name': parsed_args.name, 'status': parsed_args.status, @@ -257,11 +331,14 @@ def take_action(self, parsed_args): limit=parsed_args.limit, ) - return (column_headers, - (utils.get_item_properties( - s, columns, - formatters={'Volume ID': _VolumeIdColumn}, - ) for s in data)) + return ( + column_headers, + ( + utils.get_item_properties( + s, columns, formatters={'volume_id': _VolumeIdColumn}, + ) for s in data + ), + ) class RestoreVolumeBackup(command.ShowOne): @@ -295,7 +372,7 @@ class SetVolumeBackup(command.Command): _description = _("Set volume backup properties") def get_parser(self, prog_name): - parser = super(SetVolumeBackup, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "backup", metavar="", @@ -304,28 +381,48 @@ def get_parser(self, prog_name): parser.add_argument( '--name', metavar='', - help=_('New backup name') + help=_( + 'New backup name' + '(supported by --os-volume-api-version 3.9 or above)' + ), ) parser.add_argument( '--description', metavar='', - help=_('New backup description') + help=_( + 'New backup description ' + '(supported by --os-volume-api-version 3.9 or above)' + ), ) parser.add_argument( '--state', metavar='', choices=['available', 'error'], - help=_('New backup state ("available" or "error") (admin only) ' - '(This option simply changes the state of the backup ' - 'in the database with no regard to actual status, ' - 'exercise caution when using)'), + help=_( + 'New backup state ("available" or "error") (admin only) ' + '(This option simply changes the state of the backup ' + 'in the database with no regard to actual status; ' + 'exercise caution when using)' + ), + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Set a property on this backup ' + '(repeat option to set multiple values) ' + '(supported by --os-volume-api-version 3.43 or above)' + ), ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume - backup = utils.find_resource(volume_client.backups, - parsed_args.backup) + backup = utils.find_resource( + volume_client.backups, parsed_args.backup) + result = 0 if parsed_args.state: try: @@ -336,21 +433,47 @@ def take_action(self, parsed_args): result += 1 kwargs = {} + if parsed_args.name: + if volume_client.api_version < api_versions.APIVersion('3.9'): + msg = _( + '--os-volume-api-version 3.9 or greater is required to ' + 'support the --name option' + ) + raise exceptions.CommandError(msg) + kwargs['name'] = parsed_args.name + if parsed_args.description: + if volume_client.api_version < api_versions.APIVersion('3.9'): + msg = _( + '--os-volume-api-version 3.9 or greater is required to ' + 'support the --description option' + ) + raise exceptions.CommandError(msg) + kwargs['description'] = parsed_args.description + + if parsed_args.properties: + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --property option' + ) + raise exceptions.CommandError(msg) + + kwargs['metadata'] = parsed_args.properties + if kwargs: try: volume_client.backups.update(backup.id, **kwargs) except Exception as e: - LOG.error(_("Failed to update backup name " - "or description: %s"), e) + LOG.error("Failed to update backup name or description: %s", e) result += 1 if result > 0: - raise exceptions.CommandError(_("One or more of the " - "set operations failed")) + msg = _("One or more of the set operations failed") + raise exceptions.CommandError(msg) class ShowVolumeBackup(command.ShowOne): diff --git a/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml b/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml new file mode 100644 index 0000000000..883cb0d564 --- /dev/null +++ b/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Add ``--no-incremental``, ``--property`` and ``--availability-zone`` + options to ``volume backup create`` command, allowing users to request a + non-incremental backup, set a metadata property on the created backup, and + set an availability zone on the created backup, respectively. + - | + Add ``--property`` option the ``volume backup set`` command to set a + metadata property on an existing backup. +fixes: + - | + The ``--name`` and ``--description`` options of the ``volume backup set`` + command will now verify the version requested on the client side. + Previously this would fail on the server side. From 5faa9ef8058a161a0637dceb91f213ac5ac39070 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Jun 2021 11:09:47 +0100 Subject: [PATCH 0413/1120] tests: Rename 'FakeType' -> 'FakeVolumeType' There are more types than just volume types. Change-Id: I6af66f966a221437ff79fabcb0b81fd38586fe67 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v1/fakes.py | 30 ++++++------ .../tests/unit/volume/v1/test_qos_specs.py | 4 +- .../tests/unit/volume/v1/test_type.py | 43 +++++++++-------- openstackclient/tests/unit/volume/v2/fakes.py | 46 +++++++++---------- .../unit/volume/v2/test_consistency_group.py | 2 +- .../tests/unit/volume/v2/test_qos_specs.py | 4 +- .../tests/unit/volume/v2/test_type.py | 44 ++++++++++-------- .../tests/unit/volume/v2/test_volume.py | 2 +- 8 files changed, 91 insertions(+), 84 deletions(-) diff --git a/openstackclient/tests/unit/volume/v1/fakes.py b/openstackclient/tests/unit/volume/v1/fakes.py index adb775edf2..438a60ade5 100644 --- a/openstackclient/tests/unit/volume/v1/fakes.py +++ b/openstackclient/tests/unit/volume/v1/fakes.py @@ -400,12 +400,12 @@ def setUp(self): ) -class FakeType(object): +class FakeVolumeType(object): """Fake one or more type.""" @staticmethod - def create_one_type(attrs=None, methods=None): - """Create a fake type. + def create_one_volume_type(attrs=None, methods=None): + """Create a fake volume type. :param Dictionary attrs: A dictionary with all attributes @@ -418,7 +418,7 @@ def create_one_type(attrs=None, methods=None): methods = methods or {} # Set default attributes. - type_info = { + volume_type_info = { "id": 'type-id-' + uuid.uuid4().hex, "name": 'type-name-' + uuid.uuid4().hex, "description": 'type-description-' + uuid.uuid4().hex, @@ -427,16 +427,16 @@ def create_one_type(attrs=None, methods=None): } # Overwrite default attributes. - type_info.update(attrs) + volume_type_info.update(attrs) volume_type = fakes.FakeResource( - info=copy.deepcopy(type_info), + info=copy.deepcopy(volume_type_info), methods=methods, loaded=True) return volume_type @staticmethod - def create_types(attrs=None, count=2): + def create_volume_types(attrs=None, count=2): """Create multiple fake types. :param Dictionary attrs: @@ -448,19 +448,19 @@ def create_types(attrs=None, count=2): """ volume_types = [] for i in range(0, count): - volume_type = FakeType.create_one_type(attrs) + volume_type = FakeVolumeType.create_one_volume_type(attrs) volume_types.append(volume_type) return volume_types @staticmethod - def get_types(types=None, count=2): + 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 types: + :param List volume_types: A list of FakeResource objects faking types :param Integer count: The number of types to be faked @@ -468,14 +468,14 @@ def get_types(types=None, count=2): An iterable Mock object with side_effect set to a list of faked types """ - if types is None: - types = FakeType.create_types(count) + if volume_types is None: + volume_types = FakeVolumeType.create_volume_types(count) - return mock.Mock(side_effect=types) + return mock.Mock(side_effect=volume_types) @staticmethod - def create_one_encryption_type(attrs=None): - """Create a fake encryption type. + def create_one_encryption_volume_type(attrs=None): + """Create a fake encryption volume type. :param Dictionary attrs: A dictionary with all attributes diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index 5500438bd6..5b8e065686 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -39,7 +39,7 @@ def setUp(self): class TestQosAssociate(TestQos): - volume_type = volume_fakes.FakeType.create_one_type() + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() qos_spec = volume_fakes.FakeQos.create_one_qos() def setUp(self): @@ -263,7 +263,7 @@ def test_delete_multiple_qoses_with_exception(self): class TestQosDisassociate(TestQos): - volume_type = volume_fakes.FakeType.create_one_type() + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() qos_spec = volume_fakes.FakeQos.create_one_qos() def setUp(self): diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index f1d469140b..f3707849ea 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -49,9 +49,9 @@ class TestTypeCreate(TestType): def setUp(self): super(TestTypeCreate, self).setUp() - self.new_volume_type = volume_fakes.FakeType.create_one_type( - methods={'set_keys': {'myprop': 'myvalue'}} - ) + self.new_volume_type = \ + volume_fakes.FakeVolumeType.create_one_volume_type( + methods={'set_keys': {'myprop': 'myvalue'}}) self.data = ( self.new_volume_type.description, self.new_volume_type.id, @@ -87,11 +87,12 @@ def test_type_create_with_encryption(self): 'key_size': '128', 'control_location': 'front-end', } - encryption_type = volume_fakes.FakeType.create_one_encryption_type( - attrs=encryption_info - ) - self.new_volume_type = volume_fakes.FakeType.create_one_type( - attrs={'encryption': encryption_info}) + 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}) self.types_mock.create.return_value = self.new_volume_type self.encryption_types_mock.create.return_value = encryption_type encryption_columns = ( @@ -144,12 +145,12 @@ def test_type_create_with_encryption(self): class TestTypeDelete(TestType): - volume_types = volume_fakes.FakeType.create_types(count=2) + volume_types = volume_fakes.FakeVolumeType.create_volume_types(count=2) def setUp(self): super(TestTypeDelete, self).setUp() - self.types_mock.get = volume_fakes.FakeType.get_types( + self.types_mock.get = volume_fakes.FakeVolumeType.get_volume_types( self.volume_types) self.types_mock.delete.return_value = None @@ -220,7 +221,7 @@ def test_delete_multiple_types_with_exception(self): class TestTypeList(TestType): - volume_types = volume_fakes.FakeType.create_types() + volume_types = volume_fakes.FakeVolumeType.create_volume_types() columns = [ "ID", @@ -287,8 +288,9 @@ def test_type_list_with_options(self): self.assertItemsEqual(self.data_long, list(data)) def test_type_list_with_encryption(self): - encryption_type = volume_fakes.FakeType.create_one_encryption_type( - attrs={'volume_type_id': self.volume_types[0].id}) + encryption_type = \ + volume_fakes.FakeVolumeType.create_one_encryption_volume_type( + attrs={'volume_type_id': self.volume_types[0].id}) encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, @@ -333,7 +335,7 @@ def test_type_list_with_encryption(self): class TestTypeSet(TestType): - volume_type = volume_fakes.FakeType.create_one_type( + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( methods={'set_keys': None}) def setUp(self): @@ -441,7 +443,7 @@ class TestTypeShow(TestType): def setUp(self): super(TestTypeShow, self).setUp() - self.volume_type = volume_fakes.FakeType.create_one_type() + self.volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() self.data = ( self.volume_type.description, self.volume_type.id, @@ -472,14 +474,15 @@ def test_type_show(self): self.assertItemsEqual(self.data, data) def test_type_show_with_encryption(self): - encryption_type = volume_fakes.FakeType.create_one_encryption_type() + encryption_type = \ + volume_fakes.FakeVolumeType.create_one_encryption_volume_type() encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, 'key_size': None, 'control_location': 'front-end', } - self.volume_type = volume_fakes.FakeType.create_one_type( + self.volume_type = volume_fakes.FakeVolumeType.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 @@ -518,7 +521,7 @@ def test_type_show_with_encryption(self): class TestTypeUnset(TestType): - volume_type = volume_fakes.FakeType.create_one_type( + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( methods={'unset_keys': None}) def setUp(self): @@ -596,7 +599,7 @@ def test_type_unset_encryption_type(self): class TestColumns(TestType): def test_encryption_info_column_with_info(self): - fake_volume_type = volume_fakes.FakeType.create_one_type() + fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() type_id = fake_volume_type.id encryption_info = { @@ -612,7 +615,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.FakeType.create_one_type() + fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() type_id = fake_volume_type.id col = volume_type.EncryptionInfoColumn(type_id, {}) diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 5f18990e96..86778698da 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -983,12 +983,12 @@ def get_snapshots(snapshots=None, count=2): return mock.Mock(side_effect=snapshots) -class FakeType(object): - """Fake one or more type.""" +class FakeVolumeType(object): + """Fake one or more volume type.""" @staticmethod - def create_one_type(attrs=None, methods=None): - """Create a fake type. + def create_one_volume_type(attrs=None, methods=None): + """Create a fake volume type. :param Dictionary attrs: A dictionary with all attributes @@ -1001,7 +1001,7 @@ def create_one_type(attrs=None, methods=None): methods = methods or {} # Set default attributes. - type_info = { + volume_type_info = { "id": 'type-id-' + uuid.uuid4().hex, "name": 'type-name-' + uuid.uuid4().hex, "description": 'type-description-' + uuid.uuid4().hex, @@ -1010,17 +1010,17 @@ def create_one_type(attrs=None, methods=None): } # Overwrite default attributes. - type_info.update(attrs) + volume_type_info.update(attrs) volume_type = fakes.FakeResource( - info=copy.deepcopy(type_info), + info=copy.deepcopy(volume_type_info), methods=methods, loaded=True) return volume_type @staticmethod - def create_types(attrs=None, count=2): - """Create multiple fake types. + def create_volume_types(attrs=None, count=2): + """Create multiple fake volume_types. :param Dictionary attrs: A dictionary with all attributes @@ -1031,34 +1031,34 @@ def create_types(attrs=None, count=2): """ volume_types = [] for i in range(0, count): - volume_type = FakeType.create_one_type(attrs) + volume_type = FakeVolumeType.create_one_volume_type(attrs) volume_types.append(volume_type) return volume_types @staticmethod - def get_types(types=None, count=2): - """Get an iterable MagicMock object with a list of faked types. + def get_volume_types(volume_types=None, count=2): + """Get an iterable MagicMock object with a list of faked volume types. - If types list is provided, then initialize the Mock object with the - list. Otherwise create one. + If volume_types list is provided, then initialize the Mock object with + the list. Otherwise create one. - :param List types: - A list of FakeResource objects faking types + :param List volume_types: + A list of FakeResource objects faking volume types :param Integer count: - The number of types to be faked + The number of volume types to be faked :return An iterable Mock object with side_effect set to a list of faked - types + volume types """ - if types is None: - types = FakeType.create_types(count) + if volume_types is None: + volume_types = FakeVolumeType.create_volume_types(count) - return mock.Mock(side_effect=types) + return mock.Mock(side_effect=volume_types) @staticmethod - def create_one_encryption_type(attrs=None): - """Create a fake encryption type. + def create_one_encryption_volume_type(attrs=None): + """Create a fake encryption volume type. :param Dictionary attrs: A dictionary with all attributes diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index 6bb6c02978..dcdd9bc808 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -148,7 +148,7 @@ def test_add_multiple_volumes_to_consistency_group_with_exception( class TestConsistencyGroupCreate(TestConsistencyGroup): - volume_type = volume_fakes.FakeType.create_one_type() + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() new_consistency_group = ( volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) consistency_group_snapshot = ( diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index bc4cee8b40..a29080d10b 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -39,7 +39,7 @@ def setUp(self): class TestQosAssociate(TestQos): - volume_type = volume_fakes.FakeType.create_one_type() + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() qos_spec = volume_fakes.FakeQos.create_one_qos() def setUp(self): @@ -255,7 +255,7 @@ def test_delete_multiple_qoses_with_exception(self): class TestQosDisassociate(TestQos): - volume_type = volume_fakes.FakeType.create_one_type() + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() qos_spec = volume_fakes.FakeQos.create_one_qos() def setUp(self): diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 000464c5d8..d1cbda2f3d 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -58,7 +58,8 @@ class TestTypeCreate(TestType): def setUp(self): super(TestTypeCreate, self).setUp() - self.new_volume_type = volume_fakes.FakeType.create_one_type() + self.new_volume_type = \ + volume_fakes.FakeVolumeType.create_one_volume_type() self.data = ( self.new_volume_type.description, self.new_volume_type.id, @@ -143,11 +144,12 @@ def test_type_create_with_encryption(self): 'key_size': '128', 'control_location': 'front-end', } - encryption_type = volume_fakes.FakeType.create_one_encryption_type( - attrs=encryption_info - ) - self.new_volume_type = volume_fakes.FakeType.create_one_type( - attrs={'encryption': encryption_info}) + 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}) self.types_mock.create.return_value = self.new_volume_type self.encryption_types_mock.create.return_value = encryption_type encryption_columns = ( @@ -201,12 +203,12 @@ def test_type_create_with_encryption(self): class TestTypeDelete(TestType): - volume_types = volume_fakes.FakeType.create_types(count=2) + volume_types = volume_fakes.FakeVolumeType.create_volume_types(count=2) def setUp(self): super(TestTypeDelete, self).setUp() - self.types_mock.get = volume_fakes.FakeType.get_types( + self.types_mock.get = volume_fakes.FakeVolumeType.get_volume_types( self.volume_types) self.types_mock.delete.return_value = None @@ -276,7 +278,7 @@ def test_delete_multiple_types_with_exception(self): class TestTypeList(TestType): - volume_types = volume_fakes.FakeType.create_types() + volume_types = volume_fakes.FakeVolumeType.create_volume_types() columns = [ "ID", @@ -386,8 +388,9 @@ def test_type_list_with_default_option(self): self.assertItemsEqual(self.data_with_default_type, list(data)) def test_type_list_with_encryption(self): - encryption_type = volume_fakes.FakeType.create_one_encryption_type( - attrs={'volume_type_id': self.volume_types[0].id}) + encryption_type = \ + volume_fakes.FakeVolumeType.create_one_encryption_volume_type( + attrs={'volume_type_id': self.volume_types[0].id}) encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, @@ -433,7 +436,7 @@ def test_type_list_with_encryption(self): class TestTypeSet(TestType): project = identity_fakes.FakeProject.create_one_project() - volume_type = volume_fakes.FakeType.create_one_type( + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( methods={'set_keys': None}) def setUp(self): @@ -684,7 +687,7 @@ class TestTypeShow(TestType): def setUp(self): super(TestTypeShow, self).setUp() - self.volume_type = volume_fakes.FakeType.create_one_type() + self.volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() self.data = ( None, self.volume_type.description, @@ -724,7 +727,7 @@ def test_type_show_with_access(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.FakeType.create_one_type( + 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', @@ -757,7 +760,7 @@ def test_type_show_with_list_access_exec(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.FakeType.create_one_type( + private_type = volume_fakes.FakeVolumeType.create_one_volume_type( attrs={'is_public': False}) with mock.patch.object(self.types_mock, 'get', return_value=private_type): @@ -781,14 +784,15 @@ def test_type_show_with_list_access_exec(self): self.assertItemsEqual(private_type_data, data) def test_type_show_with_encryption(self): - encryption_type = volume_fakes.FakeType.create_one_encryption_type() + encryption_type = \ + volume_fakes.FakeVolumeType.create_one_encryption_volume_type() encryption_info = { 'provider': 'LuksEncryptor', 'cipher': None, 'key_size': None, 'control_location': 'front-end', } - self.volume_type = volume_fakes.FakeType.create_one_type( + self.volume_type = volume_fakes.FakeVolumeType.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 @@ -830,7 +834,7 @@ def test_type_show_with_encryption(self): class TestTypeUnset(TestType): project = identity_fakes.FakeProject.create_one_project() - volume_type = volume_fakes.FakeType.create_one_type( + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type( methods={'unset_keys': None}) def setUp(self): @@ -932,7 +936,7 @@ def test_type_unset_encryption_type(self): class TestColumns(TestType): def test_encryption_info_column_with_info(self): - fake_volume_type = volume_fakes.FakeType.create_one_type() + fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() type_id = fake_volume_type.id encryption_info = { @@ -948,7 +952,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.FakeType.create_one_type() + fake_volume_type = volume_fakes.FakeVolumeType.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 b9fe4e834c..377f7ec49f 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -1173,7 +1173,7 @@ def test_volume_migrate_without_host(self): class TestVolumeSet(TestVolume): - volume_type = volume_fakes.FakeType.create_one_type() + volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() def setUp(self): super(TestVolumeSet, self).setUp() From 4c2e8523a98a0dd33e0203c47e94384420c14f9c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Jun 2021 10:21:20 +0100 Subject: [PATCH 0414/1120] volume: Add 'volume group *' commands These mirror the 'cinder group-*' commands, with arguments copied across essentially verbatim. The only significant departures are the replacement of "tenant" terminology with "project" and the merging of the various volume group replication action commands into the parent volume group (e.g. 'openstack volume group set --enable-replication' instead of 'cinder group enable-replication') volume group create volume group delete volume group list volume group show volume group set volume group failover Change-Id: I3b2c0cb92b8a53cc1c0cefa3313b80f59c9e5835 Signed-off-by: Stephen Finucane --- .../cli/command-objects/volume-group.rst | 23 + doc/source/cli/commands.rst | 1 + doc/source/cli/data/cinder.csv | 18 +- openstackclient/tests/unit/volume/v3/fakes.py | 111 ++++ .../tests/unit/volume/v3/test_volume_group.py | 497 +++++++++++++++++ openstackclient/volume/v3/volume_group.py | 506 ++++++++++++++++++ ...olume-group-commands-b121d6ec7da9779a.yaml | 8 + setup.cfg | 7 + 8 files changed, 1162 insertions(+), 9 deletions(-) create mode 100644 doc/source/cli/command-objects/volume-group.rst create mode 100644 openstackclient/tests/unit/volume/v3/test_volume_group.py create mode 100644 openstackclient/volume/v3/volume_group.py create mode 100644 releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml diff --git a/doc/source/cli/command-objects/volume-group.rst b/doc/source/cli/command-objects/volume-group.rst new file mode 100644 index 0000000000..50bc830f90 --- /dev/null +++ b/doc/source/cli/command-objects/volume-group.rst @@ -0,0 +1,23 @@ +============ +volume group +============ + +Block Storage v3 + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group create + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group delete + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group list + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group failover + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group set + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group show diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index cc95b3d121..b91a896f41 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -159,6 +159,7 @@ referring to both Compute and Volume quotas. * ``volume backend pool``: (**Volume**) volume backend storage pools * ``volume backup record``: (**Volume**) volume record that can be imported or exported * ``volume backend``: (**Volume**) volume backend storage +* ``volume group``: (**Volume**) group of volumes * ``volume host``: (**Volume**) the physical computer for volumes * ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 649cd8f59a..031aa43900 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -44,15 +44,15 @@ force-delete,volume delete --force,"Attempts force-delete of volume regardless o freeze-host,volume host set --disable,Freeze and disable the specified cinder-volume host. get-capabilities,volume backend capability show,Show capabilities of a volume backend. Admin only. get-pools,volume backend pool list,Show pool information for backends. Admin only. -group-create,,Creates a group. (Supported by API versions 3.13 - 3.latest) +group-create,volume group create,Creates a group. (Supported by API versions 3.13 - 3.latest) group-create-from-src,,Creates a group from a group snapshot or a source group. (Supported by API versions 3.14 - 3.latest) -group-delete,,Removes one or more groups. (Supported by API versions 3.13 - 3.latest) -group-disable-replication,,Disables replication for group. (Supported by API versions 3.38 - 3.latest) -group-enable-replication,,Enables replication for group. (Supported by API versions 3.38 - 3.latest) -group-failover-replication,,Fails over replication for group. (Supported by API versions 3.38 - 3.latest) -group-list,,Lists all groups. (Supported by API versions 3.13 - 3.latest) -group-list-replication-targets,,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest) -group-show,,Shows details of a group. (Supported by API versions 3.13 - 3.latest) +group-delete,volume group delete,Removes one or more groups. (Supported by API versions 3.13 - 3.latest) +group-disable-replication,volume group set --disable-replication,Disables replication for group. (Supported by API versions 3.38 - 3.latest) +group-enable-replication,volume group set --enable-replication,Enables replication for group. (Supported by API versions 3.38 - 3.latest) +group-failover-replication,volume group failover,Fails over replication for group. (Supported by API versions 3.38 - 3.latest) +group-list,volume group list,Lists all groups. (Supported by API versions 3.13 - 3.latest) +group-list-replication-targets,volume group list --replication-targets,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest) +group-show,volume group show,Shows details of a group. (Supported by API versions 3.13 - 3.latest) group-snapshot-create,,Creates a group snapshot. (Supported by API versions 3.14 - 3.latest) group-snapshot-delete,,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest) group-snapshot-list,,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest) @@ -65,7 +65,7 @@ group-type-key,,Sets or unsets group_spec for a group type. (Supported by API ve group-type-list,,Lists available 'group types'. (Admin only will see private types) (Supported by API versions 3.11 - 3.latest) group-type-show,,Show group type details. (Supported by API versions 3.11 - 3.latest) group-type-update,,Updates group type name description and/or is_public. (Supported by API versions 3.11 - 3.latest) -group-update,,Updates a group. (Supported by API versions 3.13 - 3.latest) +group-update,volume group set,Updates a group. (Supported by API versions 3.13 - 3.latest) image-metadata,volume set --image-property,Sets or deletes volume image metadata. image-metadata-show,volume show,Shows volume image metadata. list,volume list,Lists all volumes. diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index 45cad8c14c..b0c96290f8 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -32,10 +32,16 @@ def __init__(self, **kwargs): self.attachments = mock.Mock() self.attachments.resource_class = fakes.FakeResource(None, {}) + self.groups = mock.Mock() + self.groups.resource_class = fakes.FakeResource(None, {}) + self.group_types = mock.Mock() + self.group_types.resource_class = fakes.FakeResource(None, {}) self.messages = mock.Mock() self.messages.resource_class = fakes.FakeResource(None, {}) self.volumes = mock.Mock() self.volumes.resource_class = fakes.FakeResource(None, {}) + self.volume_types = mock.Mock() + self.volume_types.resource_class = fakes.FakeResource(None, {}) class TestVolume(utils.TestCommand): @@ -59,6 +65,111 @@ def setUp(self): # TODO(stephenfin): Check if the responses are actually the same FakeVolume = volume_v2_fakes.FakeVolume +FakeVolumeType = volume_v2_fakes.FakeVolumeType + + +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([ + '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 FakeVolumeGroupType: + """Fake one or more volume group types.""" + + @staticmethod + def create_one_volume_group_type(attrs=None): + """Create a fake group type. + + :param attrs: A dictionary with all attributes of group type + :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, + 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: diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group.py b/openstackclient/tests/unit/volume/v3/test_volume_group.py new file mode 100644 index 0000000000..13ef38d208 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_group.py @@ -0,0 +1,497 @@ +# 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 cinderclient import api_versions +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_group + + +class TestVolumeGroup(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.volume_groups_mock = self.app.client_manager.volume.groups + self.volume_groups_mock.reset_mock() + + self.volume_group_types_mock = \ + self.app.client_manager.volume.group_types + self.volume_group_types_mock.reset_mock() + + self.volume_types_mock = self.app.client_manager.volume.volume_types + self.volume_types_mock.reset_mock() + + +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( + attrs={ + 'group_type': fake_volume_group_type.id, + 'volume_types': [fake_volume_type.id], + }, + ) + + columns = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group Type', + 'Volume Types', + 'Availability Zone', + 'Created At', + 'Volumes', + 'Group Snapshot ID', + 'Source Group ID', + ) + data = ( + fake_volume_group.id, + fake_volume_group.status, + fake_volume_group.name, + fake_volume_group.description, + fake_volume_group.group_type, + fake_volume_group.volume_types, + fake_volume_group.availability_zone, + fake_volume_group.created_at, + fake_volume_group.volumes, + fake_volume_group.group_snapshot_id, + fake_volume_group.source_group_id, + ) + + def setUp(self): + super().setUp() + + self.volume_types_mock.get.return_value = self.fake_volume_type + self.volume_group_types_mock.get.return_value = \ + self.fake_volume_group_type + self.volume_groups_mock.create.return_value = self.fake_volume_group + self.volume_groups_mock.get.return_value = self.fake_volume_group + + self.cmd = volume_group.CreateVolumeGroup(self.app, None) + + def test_volume_group_create(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group_type.id, + self.fake_volume_type.id, + ] + verifylist = [ + ('volume_group_type', self.fake_volume_group_type.id), + ('volume_types', [self.fake_volume_type.id]), + ('name', None), + ('description', None), + ('availability_zone', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.get.assert_called_once_with( + self.fake_volume_group_type.id) + self.volume_types_mock.get.assert_called_once_with( + self.fake_volume_type.id) + self.volume_groups_mock.create.assert_called_once_with( + self.fake_volume_group_type.id, + self.fake_volume_type.id, + None, + None, + availability_zone=None, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_create_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group_type.id, + self.fake_volume_type.id, + '--name', 'foo', + '--description', 'hello, world', + '--availability-zone', 'bar', + ] + verifylist = [ + ('volume_group_type', self.fake_volume_group_type.id), + ('volume_types', [self.fake_volume_type.id]), + ('name', 'foo'), + ('description', 'hello, world'), + ('availability_zone', 'bar'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.get.assert_called_once_with( + self.fake_volume_group_type.id) + self.volume_types_mock.get.assert_called_once_with( + self.fake_volume_type.id) + self.volume_groups_mock.create.assert_called_once_with( + self.fake_volume_group_type.id, + self.fake_volume_type.id, + 'foo', + 'hello, world', + availability_zone='bar', + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_create_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + self.fake_volume_group_type.id, + self.fake_volume_type.id, + ] + verifylist = [ + ('volume_group_type', self.fake_volume_group_type.id), + ('volume_types', [self.fake_volume_type.id]), + ('name', None), + ('description', None), + ('availability_zone', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + +class TestVolumeGroupDelete(TestVolumeGroup): + + fake_volume_group = \ + volume_fakes.FakeVolumeGroup.create_one_volume_group() + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_groups_mock.delete.return_value = None + + self.cmd = volume_group.DeleteVolumeGroup(self.app, None) + + def test_volume_group_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group.id, + '--force', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('force', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.delete.assert_called_once_with( + self.fake_volume_group.id, delete_volumes=True, + ) + self.assertIsNone(result) + + def test_volume_group_delete_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + self.fake_volume_group.id, + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('force', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + +class TestVolumeGroupSet(TestVolumeGroup): + + fake_volume_group = \ + volume_fakes.FakeVolumeGroup.create_one_volume_group() + + columns = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group Type', + 'Volume Types', + 'Availability Zone', + 'Created At', + 'Volumes', + 'Group Snapshot ID', + 'Source Group ID', + ) + data = ( + fake_volume_group.id, + fake_volume_group.status, + fake_volume_group.name, + fake_volume_group.description, + fake_volume_group.group_type, + fake_volume_group.volume_types, + fake_volume_group.availability_zone, + fake_volume_group.created_at, + fake_volume_group.volumes, + fake_volume_group.group_snapshot_id, + fake_volume_group.source_group_id, + ) + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_groups_mock.update.return_value = self.fake_volume_group + + self.cmd = volume_group.SetVolumeGroup(self.app, None) + + def test_volume_group_set(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group.id, + '--name', 'foo', + '--description', 'hello, world', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('name', 'foo'), + ('description', 'hello, world'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.update.assert_called_once_with( + self.fake_volume_group.id, name='foo', description='hello, world', + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_with_enable_replication_option(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.38') + + arglist = [ + self.fake_volume_group.id, + '--enable-replication', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('enable_replication', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.enable_replication.assert_called_once_with( + self.fake_volume_group.id) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_set_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + self.fake_volume_group.id, + '--name', 'foo', + '--description', 'hello, world', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('name', 'foo'), + ('description', 'hello, world'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + def test_volume_group_with_enable_replication_option_pre_v338(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.37') + + arglist = [ + self.fake_volume_group.id, + '--enable-replication', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('enable_replication', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.38 or greater is required', + str(exc)) + + +class TestVolumeGroupList(TestVolumeGroup): + + fake_volume_groups = \ + volume_fakes.FakeVolumeGroup.create_volume_groups() + + columns = ( + 'ID', + 'Status', + 'Name', + ) + data = [ + ( + fake_volume_group.id, + fake_volume_group.status, + fake_volume_group.name, + ) for fake_volume_group in fake_volume_groups + ] + + def setUp(self): + super().setUp() + + self.volume_groups_mock.list.return_value = self.fake_volume_groups + + self.cmd = volume_group.ListVolumeGroup(self.app, None) + + def test_volume_group_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': True, + }, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), data) + + def test_volume_group_list_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + +class TestVolumeGroupFailover(TestVolumeGroup): + + fake_volume_group = \ + volume_fakes.FakeVolumeGroup.create_one_volume_group() + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_groups_mock.failover_replication.return_value = None + + self.cmd = volume_group.FailoverVolumeGroup(self.app, None) + + def test_volume_group_failover(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.38') + + arglist = [ + self.fake_volume_group.id, + '--allow-attached-volume', + '--secondary-backend-id', 'foo', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('allow_attached_volume', True), + ('secondary_backend_id', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.failover_replication.assert_called_once_with( + self.fake_volume_group.id, + allow_attached_volume=True, + secondary_backend_id='foo', + ) + self.assertIsNone(result) + + def test_volume_group_failover_pre_v338(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.37') + + arglist = [ + self.fake_volume_group.id, + '--allow-attached-volume', + '--secondary-backend-id', 'foo', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('allow_attached_volume', True), + ('secondary_backend_id', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.38 or greater is required', + str(exc)) diff --git a/openstackclient/volume/v3/volume_group.py b/openstackclient/volume/v3/volume_group.py new file mode 100644 index 0000000000..db4e9a94fa --- /dev/null +++ b/openstackclient/volume/v3/volume_group.py @@ -0,0 +1,506 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _format_group(group): + columns = ( + 'id', + 'status', + 'name', + 'description', + 'group_type', + 'volume_types', + 'availability_zone', + 'created_at', + 'volumes', + 'group_snapshot_id', + 'source_group_id', + ) + column_headers = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group Type', + 'Volume Types', + 'Availability Zone', + 'Created At', + 'Volumes', + 'Group Snapshot ID', + 'Source Group ID', + ) + + # TODO(stephenfin): Consider using a formatter for volume_types since it's + # a list + return ( + column_headers, + utils.get_item_properties( + group, + columns, + ), + ) + + +class CreateVolumeGroup(command.ShowOne): + """Create a volume group. + + Generic volume groups enable you to create a group of volumes and manage + them together. + + Generic volume groups are more flexible than consistency groups. Currently + volume consistency groups only support consistent group snapshot. It + cannot be extended easily to serve other purposes. A project may want to + put volumes used in the same application together in a group so that it is + easier to manage them together, and this group of volumes may or may not + support consistent group snapshot. Generic volume group solve this problem. + By decoupling the tight relationship between the group construct and the + consistency concept, generic volume groups can be extended to support other + features in the future. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'volume_group_type', + metavar='', + help=_('Name or ID of volume group type to use.'), + ) + parser.add_argument( + 'volume_types', + metavar='', + nargs='+', + default=[], + help=_('Name or ID of volume type(s) to use.'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the volume group.'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('Description of a volume group.') + ) + parser.add_argument( + '--availability-zone', + metavar='', + help=_('Availability zone for volume group.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group create' command" + ) + raise exceptions.CommandError(msg) + + volume_group_type = utils.find_resource( + volume_client.group_types, + parsed_args.volume_group_type, + ) + + volume_types = [] + for volume_type in parsed_args.volume_types: + volume_types.append( + utils.find_resource( + volume_client.volume_types, + volume_type, + ) + ) + + group = volume_client.groups.create( + volume_group_type.id, + ','.join(x.id for x in volume_types), + parsed_args.name, + parsed_args.description, + availability_zone=parsed_args.availability_zone) + + group = volume_client.groups.get(group.id) + + return _format_group(group) + + +class DeleteVolumeGroup(command.Command): + """Delete a volume group. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='', + help=_('Name or ID of volume group to delete'), + ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_( + 'Delete the volume group even if it contains volumes. ' + 'This will delete any remaining volumes in the group.', + ) + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group delete' command" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + volume_client.groups.delete( + group.id, delete_volumes=parsed_args.force) + + +class SetVolumeGroup(command.ShowOne): + """Update a volume group. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='', + help=_('Name or ID of volume group.'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('New name for group.'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New description for group.'), + ) + parser.add_argument( + '--enable-replication', + action='store_true', + dest='enable_replication', + default=None, + help=_( + 'Enable replication for group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + parser.add_argument( + '--disable-replication', + action='store_false', + dest='enable_replication', + help=_( + 'Disable replication for group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group set' command" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + if parsed_args.enable_replication is not None: + if volume_client.api_version < api_versions.APIVersion('3.38'): + msg = _( + "--os-volume-api-version 3.38 or greater is required to " + "support the '--enable-replication' or " + "'--disable-replication' options" + ) + raise exceptions.CommandError(msg) + + if parsed_args.enable_replication: + volume_client.groups.enable_replication(group.id) + else: + volume_client.groups.disable_replication(group.id) + + kwargs = {} + + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + + if parsed_args.description is not None: + kwargs['description'] = parsed_args.description + + if kwargs: + group = volume_client.groups.update(group.id, **kwargs) + + return _format_group(group) + + +class ListVolumeGroup(command.Lister): + """Lists all volume groups. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + dest='all_projects', + action='store_true', + default=utils.env('ALL_PROJECTS', default=False), + help=_('Shows details for all projects (admin only).'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group list' command" + ) + raise exceptions.CommandError(msg) + + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + groups = volume_client.groups.list( + search_opts=search_opts) + + column_headers = ( + 'ID', + 'Status', + 'Name', + ) + columns = ( + 'id', + 'status', + 'name', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in groups + ), + ) + + +class ShowVolumeGroup(command.ShowOne): + """Show detailed information for a volume group. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='', + help=_('Name or ID of volume group.'), + ) + parser.add_argument( + '--volumes', + action='store_true', + dest='show_volumes', + default=None, + help=_( + 'Show volumes included in the group. ' + '(supported by --os-volume-api-version 3.25 or above)' + ), + ) + parser.add_argument( + '--no-volumes', + action='store_false', + dest='show_volumes', + help=_( + 'Do not show volumes included in the group. ' + '(supported by --os-volume-api-version 3.25 or above)' + ), + ) + parser.add_argument( + '--replication-targets', + action='store_true', + dest='show_replication_targets', + default=None, + help=_( + 'Show replication targets for the group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + parser.add_argument( + '--no-replication-targets', + action='store_false', + dest='show_replication_targets', + help=_( + 'Do not show replication targets for the group. ' + '(supported by --os-volume-api-version 3.38 or above)' + ), + ) + + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.13'): + msg = _( + "--os-volume-api-version 3.13 or greater is required to " + "support the 'volume group show' command" + ) + raise exceptions.CommandError(msg) + + kwargs = {} + + if parsed_args.show_volumes is not None: + if volume_client.api_version < api_versions.APIVersion('3.25'): + msg = _( + "--os-volume-api-version 3.25 or greater is required to " + "support the '--(no-)volumes' option" + ) + raise exceptions.CommandError(msg) + + kwargs['list_volume'] = parsed_args.show_volumes + + if parsed_args.show_replication_targets is not None: + if volume_client.api_version < api_versions.APIVersion('3.38'): + msg = _( + "--os-volume-api-version 3.38 or greater is required to " + "support the '--(no-)replication-targets' option" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + group = volume_client.groups.show(group.id, **kwargs) + + if parsed_args.show_replication_targets: + replication_targets = \ + volume_client.groups.list_replication_targets(group.id) + + group.replication_targets = replication_targets + + # TODO(stephenfin): Show replication targets + return _format_group(group) + + +class FailoverVolumeGroup(command.Command): + """Failover replication for a volume group. + + This command requires ``--os-volume-api-version`` 3.38 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group', + metavar='', + help=_('Name or ID of volume group to failover replication for.'), + ) + parser.add_argument( + '--allow-attached-volume', + action='store_true', + dest='allow_attached_volume', + default=False, + help=_( + 'Allow group with attached volumes to be failed over.', + ) + ) + parser.add_argument( + '--disallow-attached-volume', + action='store_false', + dest='allow_attached_volume', + default=False, + help=_( + 'Disallow group with attached volumes to be failed over.', + ) + ) + parser.add_argument( + '--secondary-backend-id', + metavar='', + help=_('Secondary backend ID.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.38'): + msg = _( + "--os-volume-api-version 3.38 or greater is required to " + "support the 'volume group failover' command" + ) + raise exceptions.CommandError(msg) + + group = utils.find_resource( + volume_client.groups, + parsed_args.group, + ) + + volume_client.groups.failover_replication( + group.id, + allow_attached_volume=parsed_args.allow_attached_volume, + secondary_backend_id=parsed_args.secondary_backend_id, + ) diff --git a/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml b/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml new file mode 100644 index 0000000000..8b3fe7ecc4 --- /dev/null +++ b/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add ``volume group create``, ``volume group delete``, + ``volume group list``, ``volume group failover``, + ``volume group set/unset`` and ``volume attachment show`` + commands to create, delete, list, failover, update and show volume groups, + respectively. diff --git a/setup.cfg b/setup.cfg index d593762ac9..1d031a8fc4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -714,6 +714,13 @@ openstack.volume.v3 = volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord + volume_group_create = openstackclient.volume.v3.volume_group:CreateVolumeGroup + volume_group_delete = openstackclient.volume.v3.volume_group:DeleteVolumeGroup + volume_group_list = openstackclient.volume.v3.volume_group:ListVolumeGroup + volume_group_failover = openstackclient.volume.v3.volume_group:FailoverVolumeGroup + volume_group_set = openstackclient.volume.v3.volume_group:SetVolumeGroup + volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup + volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage From 83551d2a0c7604179c0988c13d58455a6b289cc8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Jun 2021 14:38:55 +0100 Subject: [PATCH 0415/1120] volume: Add 'volume group type *' commands These mirror the 'cinder group-type-*' commands, with arguments copied across essentially verbatim. The only significant departure is the merging of some commands, such as 'group-type-default' and 'group-type-list' into 'group type list', and 'group-type-update' and 'group-type-key' into 'group type set/unset'. volume group type create volume group type delete volume group type list volume group type show volume group type set volume group type unset Change-Id: Iee6ee2f1f276e6ef6f75a74f8f2980f14c0d5e2f Signed-off-by: Stephen Finucane --- .../cli/command-objects/volume-group-type.rst | 8 + doc/source/cli/commands.rst | 1 + doc/source/cli/data/cinder.csv | 16 +- openstackclient/tests/unit/volume/v3/fakes.py | 4 +- .../unit/volume/v3/test_volume_group_type.py | 475 ++++++++++++++++++ .../volume/v3/volume_group_type.py | 410 +++++++++++++++ ...-group-type-commands-13eabc7664a5c2bc.yaml | 7 + setup.cfg | 7 + 8 files changed, 919 insertions(+), 9 deletions(-) create mode 100644 doc/source/cli/command-objects/volume-group-type.rst create mode 100644 openstackclient/tests/unit/volume/v3/test_volume_group_type.py create mode 100644 openstackclient/volume/v3/volume_group_type.py create mode 100644 releasenotes/notes/add-volume-group-type-commands-13eabc7664a5c2bc.yaml diff --git a/doc/source/cli/command-objects/volume-group-type.rst b/doc/source/cli/command-objects/volume-group-type.rst new file mode 100644 index 0000000000..edb88dc7b0 --- /dev/null +++ b/doc/source/cli/command-objects/volume-group-type.rst @@ -0,0 +1,8 @@ +================= +volume group type +================= + +Block Storage v3 + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group type * diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index b91a896f41..ada38e3e57 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -160,6 +160,7 @@ referring to both Compute and Volume quotas. * ``volume backup record``: (**Volume**) volume record that can be imported or exported * ``volume backend``: (**Volume**) volume backend storage * ``volume group``: (**Volume**) group of volumes +* ``volume group type``: (**Volume**) deployment-specific types of volumes groups available * ``volume host``: (**Volume**) the physical computer for volumes * ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages * ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 031aa43900..5de8ea5cdd 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -57,14 +57,14 @@ group-snapshot-create,,Creates a group snapshot. (Supported by API versions 3.14 group-snapshot-delete,,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest) group-snapshot-list,,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest) group-snapshot-show,,Shows group snapshot details. (Supported by API versions 3.14 - 3.latest) -group-specs-list,,Lists current group types and specs. (Supported by API versions 3.11 - 3.latest) -group-type-create,,Creates a group type. (Supported by API versions 3.11 - 3.latest) -group-type-default,,List the default group type. (Supported by API versions 3.11 - 3.latest) -group-type-delete,,Deletes group type or types. (Supported by API versions 3.11 - 3.latest) -group-type-key,,Sets or unsets group_spec for a group type. (Supported by API versions 3.11 - 3.latest) -group-type-list,,Lists available 'group types'. (Admin only will see private types) (Supported by API versions 3.11 - 3.latest) -group-type-show,,Show group type details. (Supported by API versions 3.11 - 3.latest) -group-type-update,,Updates group type name description and/or is_public. (Supported by API versions 3.11 - 3.latest) +group-specs-list,volume group type list,Lists current group types and specs. (Supported by API versions 3.11 - 3.latest) +group-type-create,volume group type create,Creates a group type. (Supported by API versions 3.11 - 3.latest) +group-type-default,volume group type list --default,List the default group type. (Supported by API versions 3.11 - 3.latest) +group-type-delete,volume group type delete,Deletes group type or types. (Supported by API versions 3.11 - 3.latest) +group-type-key,volume group type set,Sets or unsets group_spec for a group type. (Supported by API versions 3.11 - 3.latest) +group-type-list,volume group type set,Lists available 'group types'. (Admin only will see private types) (Supported by API versions 3.11 - 3.latest) +group-type-show,volume group type show,Show group type details. (Supported by API versions 3.11 - 3.latest) +group-type-update,volume group type set,Updates group type name description and/or is_public. (Supported by API versions 3.11 - 3.latest) group-update,volume group set,Updates a group. (Supported by API versions 3.13 - 3.latest) image-metadata,volume set --image-property,Sets or deletes volume image metadata. image-metadata-show,volume show,Shows volume image metadata. diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index b0c96290f8..c300ca3855 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -129,10 +129,11 @@ class FakeVolumeGroupType: """Fake one or more volume group types.""" @staticmethod - def create_one_volume_group_type(attrs=None): + 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 {} @@ -152,6 +153,7 @@ def create_one_volume_group_type(attrs=None): group_type = fakes.FakeResource( None, group_type_info, + methods=methods, loaded=True) return group_type diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group_type.py b/openstackclient/tests/unit/volume/v3/test_volume_group_type.py new file mode 100644 index 0000000000..7e758a2c1f --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_group_type.py @@ -0,0 +1,475 @@ +# 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 unittest import mock + +from cinderclient import api_versions +from osc_lib.cli import format_columns +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_group_type + + +class TestVolumeGroupType(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.volume_group_types_mock = \ + self.app.client_manager.volume.group_types + self.volume_group_types_mock.reset_mock() + + +class TestVolumeGroupTypeCreate(TestVolumeGroupType): + + maxDiff = 2000 + + fake_volume_group_type = \ + volume_fakes.FakeVolumeGroupType.create_one_volume_group_type() + + columns = ( + 'ID', + 'Name', + 'Description', + 'Is Public', + 'Properties', + ) + data = ( + fake_volume_group_type.id, + fake_volume_group_type.name, + fake_volume_group_type.description, + fake_volume_group_type.is_public, + format_columns.DictColumn(fake_volume_group_type.group_specs), + ) + + def setUp(self): + super().setUp() + + self.volume_group_types_mock.create.return_value = \ + self.fake_volume_group_type + + self.cmd = volume_group_type.CreateVolumeGroupType(self.app, None) + + def test_volume_group_type_create(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + self.fake_volume_group_type.name, + ] + verifylist = [ + ('name', self.fake_volume_group_type.name), + ('description', None), + ('is_public', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.create.assert_called_once_with( + self.fake_volume_group_type.name, + None, + True) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_type_create_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + self.fake_volume_group_type.name, + '--description', 'foo', + '--private', + ] + verifylist = [ + ('name', self.fake_volume_group_type.name), + ('description', 'foo'), + ('is_public', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.create.assert_called_once_with( + self.fake_volume_group_type.name, + 'foo', + False) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_type_create_pre_v311(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.10') + + arglist = [ + self.fake_volume_group_type.name, + ] + verifylist = [ + ('name', self.fake_volume_group_type.name), + ('description', None), + ('is_public', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.11 or greater is required', + str(exc)) + + +class TestVolumeGroupTypeDelete(TestVolumeGroupType): + + fake_volume_group_type = \ + volume_fakes.FakeVolumeGroupType.create_one_volume_group_type() + + def setUp(self): + super().setUp() + + self.volume_group_types_mock.get.return_value = \ + self.fake_volume_group_type + self.volume_group_types_mock.delete.return_value = None + + self.cmd = volume_group_type.DeleteVolumeGroupType(self.app, None) + + def test_volume_group_type_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + self.fake_volume_group_type.id, + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.delete.assert_called_once_with( + self.fake_volume_group_type.id, + ) + self.assertIsNone(result) + + def test_volume_group_type_delete_pre_v311(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.10') + + arglist = [ + self.fake_volume_group_type.id, + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.11 or greater is required', + str(exc)) + + +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, + }) + + columns = ( + 'ID', + 'Name', + 'Description', + 'Is Public', + 'Properties', + ) + data = ( + fake_volume_group_type.id, + fake_volume_group_type.name, + fake_volume_group_type.description, + fake_volume_group_type.is_public, + format_columns.DictColumn(fake_volume_group_type.group_specs), + ) + + def setUp(self): + super().setUp() + + self.volume_group_types_mock.get.return_value = \ + self.fake_volume_group_type + self.volume_group_types_mock.update.return_value = \ + self.fake_volume_group_type + + self.cmd = volume_group_type.SetVolumeGroupType(self.app, None) + + def test_volume_group_type_set(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + self.fake_volume_group_type.set_keys.return_value = None + + arglist = [ + self.fake_volume_group_type.id, + '--name', 'foo', + '--description', 'hello, world', + '--public', + '--property', 'fizz=buzz', + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ('name', 'foo'), + ('description', 'hello, world'), + ('is_public', True), + ('no_property', False), + ('properties', {'fizz': 'buzz'}), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.update.assert_called_once_with( + self.fake_volume_group_type.id, + name='foo', + description='hello, world', + is_public=True, + ) + self.fake_volume_group_type.set_keys.assert_called_once_with( + {'fizz': 'buzz'}, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_type_with_no_property_option(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + self.fake_volume_group_type.id, + '--no-property', + '--property', 'fizz=buzz', + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ('name', None), + ('description', None), + ('is_public', None), + ('no_property', True), + ('properties', {'fizz': 'buzz'}), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.get.assert_called_once_with( + self.fake_volume_group_type.id) + self.fake_volume_group_type.get_keys.assert_called_once_with() + self.fake_volume_group_type.unset_keys.assert_called_once_with( + {'foo': 'bar'}.keys()) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_type_set_pre_v311(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.10') + + arglist = [ + self.fake_volume_group_type.id, + '--name', 'foo', + '--description', 'hello, world', + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ('name', 'foo'), + ('description', 'hello, world'), + ('is_public', None), + ('no_property', False), + ('properties', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.11 or greater is required', + str(exc)) + + +class TestVolumeGroupTypeUnset(TestVolumeGroupType): + + fake_volume_group_type = \ + volume_fakes.FakeVolumeGroupType.create_one_volume_group_type( + methods={'unset_keys': None}) + + columns = ( + 'ID', + 'Name', + 'Description', + 'Is Public', + 'Properties', + ) + data = ( + fake_volume_group_type.id, + fake_volume_group_type.name, + fake_volume_group_type.description, + fake_volume_group_type.is_public, + format_columns.DictColumn(fake_volume_group_type.group_specs), + ) + + def setUp(self): + super().setUp() + + self.volume_group_types_mock.get.return_value = \ + self.fake_volume_group_type + + self.cmd = volume_group_type.UnsetVolumeGroupType(self.app, None) + + def test_volume_group_type_unset(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + self.fake_volume_group_type.id, + '--property', 'fizz', + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ('properties', ['fizz']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.get.assert_has_calls([ + mock.call(self.fake_volume_group_type.id), + mock.call(self.fake_volume_group_type.id), + ]) + self.fake_volume_group_type.unset_keys.assert_called_once_with( + ['fizz']) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_type_unset_pre_v311(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.10') + + arglist = [ + self.fake_volume_group_type.id, + '--property', 'fizz', + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ('properties', ['fizz']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.11 or greater is required', + str(exc)) + + +class TestVolumeGroupTypeList(TestVolumeGroupType): + + fake_volume_group_types = \ + volume_fakes.FakeVolumeGroupType.create_volume_group_types() + + columns = ( + 'ID', + 'Name', + 'Is Public', + 'Properties', + ) + data = [ + ( + fake_volume_group_type.id, + fake_volume_group_type.name, + fake_volume_group_type.is_public, + fake_volume_group_type.group_specs, + ) for fake_volume_group_type in fake_volume_group_types + ] + + def setUp(self): + super().setUp() + + self.volume_group_types_mock.list.return_value = \ + self.fake_volume_group_types + self.volume_group_types_mock.default.return_value = \ + self.fake_volume_group_types[0] + + self.cmd = volume_group_type.ListVolumeGroupType(self.app, None) + + def test_volume_group_type_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + ] + verifylist = [ + ('show_default', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.list.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), data) + + def test_volume_group_type_list_with_default_option(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.11') + + arglist = [ + '--default', + ] + verifylist = [ + ('show_default', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.default.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple([self.data[0]]), data) + + def test_volume_group_type_list_pre_v311(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.10') + + arglist = [ + ] + verifylist = [ + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.11 or greater is required', + str(exc)) diff --git a/openstackclient/volume/v3/volume_group_type.py b/openstackclient/volume/v3/volume_group_type.py new file mode 100644 index 0000000000..860fa544a5 --- /dev/null +++ b/openstackclient/volume/v3/volume_group_type.py @@ -0,0 +1,410 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from cinderclient import api_versions +from osc_lib.cli import format_columns +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _format_group_type(group): + columns = ( + 'id', + 'name', + 'description', + 'is_public', + 'group_specs', + ) + column_headers = ( + 'ID', + 'Name', + 'Description', + 'Is Public', + 'Properties', + ) + + # TODO(stephenfin): Consider using a formatter for volume_types since it's + # a list + return ( + column_headers, + utils.get_item_properties( + group, + columns, + formatters={ + 'group_specs': format_columns.DictColumn, + }, + ), + ) + + +class CreateVolumeGroupType(command.ShowOne): + """Create a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of new volume group type.'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('Description of the volume group type.') + ) + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( + '--public', + dest='is_public', + action='store_true', + default=True, + help=_( + 'Volume group type is available to other projects (default)' + ), + ) + type_group.add_argument( + '--private', + dest='is_public', + action='store_false', + help=_('Volume group type is not available to other projects') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type create' command" + ) + raise exceptions.CommandError(msg) + + group_type = volume_client.group_types.create( + parsed_args.name, + parsed_args.description, + parsed_args.is_public) + + return _format_group_type(group_type) + + +class DeleteVolumeGroupType(command.Command): + """Delete a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='', + help=_('Name or ID of volume group type to delete'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type delete' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + volume_client.group_types.delete(group_type.id) + + +class SetVolumeGroupType(command.ShowOne): + """Update a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='', + help=_('Name or ID of volume group type.'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('New name for volume group type.'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New description for volume group type.'), + ) + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( + '--public', + dest='is_public', + action='store_true', + default=None, + help=_('Make volume group type available to other projects.'), + ) + type_group.add_argument( + '--private', + dest='is_public', + action='store_false', + help=_('Make volume group type unavailable to other projects.') + ) + parser.add_argument( + '--no-property', + action='store_true', + help=_( + 'Remove all properties from this volume group type ' + '(specify both --no-property and --property ' + 'to remove the current properties before setting ' + 'new properties)' + ), + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Property to add or modify for this volume group type ' + '(repeat option to set multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type set' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + kwargs = {} + errors = 0 + + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + + if parsed_args.description is not None: + kwargs['description'] = parsed_args.description + + if parsed_args.is_public is not None: + kwargs['is_public'] = parsed_args.is_public + + if kwargs: + try: + group_type = volume_client.group_types.update( + group_type.id, **kwargs) + except Exception as e: + LOG.error(_("Failed to update group type: %s"), e) + errors += 1 + + if parsed_args.no_property: + try: + keys = group_type.get_keys().keys() + group_type.unset_keys(keys) + except Exception as e: + LOG.error(_("Failed to clear group type properties: %s"), e) + errors += 1 + + if parsed_args.properties: + try: + group_type.set_keys(parsed_args.properties) + except Exception as e: + LOG.error(_("Failed to set group type properties: %s"), e) + errors += 1 + + if errors > 0: + msg = _( + "Command Failed: One or more of the operations failed" + ) + raise exceptions.CommandError() + + return _format_group_type(group_type) + + +class UnsetVolumeGroupType(command.ShowOne): + """Unset properties of a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='', + help=_('Name or ID of volume group type.'), + ) + parser.add_argument( + '--property', + metavar='', + action='append', + dest='properties', + help=_( + 'Property to remove from this volume group type ' + '(repeat option to unset multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type unset' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + group_type.unset_keys(parsed_args.properties) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group_type, + ) + + return _format_group_type(group_type) + + +class ListVolumeGroupType(command.Lister): + """Lists all volume group types. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--default', + action='store_true', + dest='show_default', + default=False, + help=_('List the default volume group type.'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type list' command" + ) + raise exceptions.CommandError(msg) + + if parsed_args.show_default: + group_types = [volume_client.group_types.default()] + else: + group_types = volume_client.group_types.list() + + column_headers = ( + 'ID', + 'Name', + 'Is Public', + 'Properties', + ) + columns = ( + 'id', + 'name', + 'is_public', + 'group_specs', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in group_types + ), + ) + + +class ShowVolumeGroupType(command.ShowOne): + """Show detailed information for a volume group type. + + This command requires ``--os-volume-api-version`` 3.11 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'group_type', + metavar='', + help=_('Name or ID of volume group type.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.11'): + msg = _( + "--os-volume-api-version 3.11 or greater is required to " + "support the 'volume group type show' command" + ) + raise exceptions.CommandError(msg) + + group_type = utils.find_resource( + volume_client.group_types, + parsed_args.group, + ) + + return _format_group_type(group_type) diff --git a/releasenotes/notes/add-volume-group-type-commands-13eabc7664a5c2bc.yaml b/releasenotes/notes/add-volume-group-type-commands-13eabc7664a5c2bc.yaml new file mode 100644 index 0000000000..02ffd38dda --- /dev/null +++ b/releasenotes/notes/add-volume-group-type-commands-13eabc7664a5c2bc.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add ``volume group type create``, ``volume group type delete``, + ``volume group type list``, ``volume group type set/unset`` and + ``volume group type show`` commands to create, delete, list, update, + and show volume group types, respectively. diff --git a/setup.cfg b/setup.cfg index 1d031a8fc4..2a01ae7b0d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -719,8 +719,15 @@ openstack.volume.v3 = volume_group_list = openstackclient.volume.v3.volume_group:ListVolumeGroup volume_group_failover = openstackclient.volume.v3.volume_group:FailoverVolumeGroup volume_group_set = openstackclient.volume.v3.volume_group:SetVolumeGroup + volume_group_unset = openstackclient.volume.v3.volume_group:UnsetVolumeGroup volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup + volume_group_type_create = openstackclient.volume.v3.volume_group_type:CreateVolumeGroupType + volume_group_type_delete = openstackclient.volume.v3.volume_group_type:DeleteVolumeGroupType + volume_group_type_list = openstackclient.volume.v3.volume_group_type:ListVolumeGroupType + volume_group_type_set = openstackclient.volume.v3.volume_group_type:SetVolumeGroupType + volume_group_type_show = openstackclient.volume.v3.volume_group_type:ShowVolumeGroupType + volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage From fa8c8d26a7696d169b0b9d5aaf6b723d8feee08a Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Wed, 7 Apr 2021 23:40:16 +0200 Subject: [PATCH 0416/1120] Add support for Neutron's L3 conntrack helper resource Neutron has got CRUD API for L3 conntrack helper since some time. This patch adds support for it in the OSC. OpenStack SDK supports that since [1] This patch also bumps minimum OpenStack SDK version to the 0.56.0 as that version introduced support for the Neutron's L3 conntrack helper. [1] https://review.opendev.org/c/openstack/openstacksdk/+/782870 Change-Id: I55604182ae50b6ad70c8bc1f7efad8859f191269 --- .zuul.yaml | 1 + .../network-l3-conntrack-helper.rst | 8 + .../network/v2/l3_conntrack_helper.py | 255 ++++++++++++++ .../network/v2/test_l3_conntrack_helper.py | 145 ++++++++ .../tests/unit/network/v2/fakes.py | 75 +++++ .../network/v2/test_l3_conntrack_helper.py | 316 ++++++++++++++++++ .../L3-conntrack-helper-bd0d9da041747e84.yaml | 8 + setup.cfg | 6 + 8 files changed, 814 insertions(+) create mode 100644 doc/source/cli/command-objects/network-l3-conntrack-helper.rst create mode 100644 openstackclient/network/v2/l3_conntrack_helper.py create mode 100644 openstackclient/tests/functional/network/v2/test_l3_conntrack_helper.py create mode 100644 openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py create mode 100644 releasenotes/notes/L3-conntrack-helper-bd0d9da041747e84.yaml diff --git a/.zuul.yaml b/.zuul.yaml index e7b01da409..e1c1f970af 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -106,6 +106,7 @@ q-metering: true q-qos: true neutron-tag-ports-during-bulk-creation: true + neutron-conntrack-helper: true devstack_localrc: Q_AGENT: openvswitch Q_ML2_TENANT_NETWORK_TYPE: vxlan diff --git a/doc/source/cli/command-objects/network-l3-conntrack-helper.rst b/doc/source/cli/command-objects/network-l3-conntrack-helper.rst new file mode 100644 index 0000000000..badbab6337 --- /dev/null +++ b/doc/source/cli/command-objects/network-l3-conntrack-helper.rst @@ -0,0 +1,8 @@ +=========================== +network l3 conntrack helper +=========================== + +Network v2 + +.. autoprogram-cliff:: openstack.network.v2 + :command: network l3 conntrack helper * diff --git a/openstackclient/network/v2/l3_conntrack_helper.py b/openstackclient/network/v2/l3_conntrack_helper.py new file mode 100644 index 0000000000..dae259273f --- /dev/null +++ b/openstackclient/network/v2/l3_conntrack_helper.py @@ -0,0 +1,255 @@ +# 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. +# + +"""L3 Conntrack Helper action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = {} + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _get_attrs(client, parsed_args): + router = client.find_router(parsed_args.router, ignore_missing=False) + attrs = {'router_id': router.id} + if parsed_args.helper: + attrs['helper'] = parsed_args.helper + if parsed_args.protocol: + attrs['protocol'] = parsed_args.protocol + if parsed_args.port: + attrs['port'] = parsed_args.port + + return attrs + + +class CreateConntrackHelper(command.ShowOne): + _description = _("Create a new L3 conntrack helper") + + def get_parser(self, prog_name): + parser = super(CreateConntrackHelper, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_('Router for which conntrack helper will be created') + ) + parser.add_argument( + '--helper', + required=True, + metavar='', + help=_('The netfilter conntrack helper module') + ) + parser.add_argument( + '--protocol', + required=True, + metavar='', + help=_('The network protocol for the netfilter conntrack target ' + 'rule') + ) + parser.add_argument( + '--port', + required=True, + metavar='', + type=int, + help=_('The network port for the netfilter conntrack target rule') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + attrs = _get_attrs(client, parsed_args) + obj = client.create_conntrack_helper(attrs.pop('router_id'), **attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +class DeleteConntrackHelper(command.Command): + _description = _("Delete L3 conntrack helper") + + def get_parser(self, prog_name): + parser = super(DeleteConntrackHelper, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_('Router that the conntrack helper belong to') + ) + parser.add_argument( + 'conntrack_helper_ids', + metavar='', + nargs='+', + help=_('The ID of the conntrack helper(s) to delete') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + router = client.find_router(parsed_args.router, ignore_missing=False) + for ct_helper in parsed_args.conntrack_helper_ids: + try: + client.delete_conntrack_helper( + ct_helper, router.id, ignore_missing=False) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete L3 conntrack helper with " + "ID '%(ct_helper)s': %(e)s"), + {'ct_helper': ct_helper, 'e': e}) + + if result > 0: + total = len(parsed_args.conntrack_helper_ids) + msg = (_("%(result)s of %(total)s L3 conntrack helpers failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListConntrackHelper(command.Lister): + _description = _("List L3 conntrack helpers") + + def get_parser(self, prog_name): + parser = super(ListConntrackHelper, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_('Router that the conntrack helper belong to') + ) + parser.add_argument( + '--helper', + metavar='', + help=_('The netfilter conntrack helper module') + ) + parser.add_argument( + '--protocol', + metavar='', + help=_('The network protocol for the netfilter conntrack target ' + 'rule') + ) + parser.add_argument( + '--port', + metavar='', + help=_('The network port for the netfilter conntrack target rule') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'id', + 'router_id', + 'helper', + 'protocol', + 'port', + ) + column_headers = ( + 'ID', + 'Router ID', + 'Helper', + 'Protocol', + 'Port', + ) + attrs = _get_attrs(client, parsed_args) + data = client.conntrack_helpers(attrs.pop('router_id'), **attrs) + + return (column_headers, + (utils.get_item_properties( + s, columns, formatters={}, + ) for s in data)) + + +class SetConntrackHelper(command.Command): + _description = _("Set L3 conntrack helper properties") + + def get_parser(self, prog_name): + parser = super(SetConntrackHelper, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_('Router that the conntrack helper belong to') + ) + parser.add_argument( + 'conntrack_helper_id', + metavar='', + help=_('The ID of the conntrack helper(s)') + ) + parser.add_argument( + '--helper', + metavar='', + help=_('The netfilter conntrack helper module') + ) + parser.add_argument( + '--protocol', + metavar='', + help=_('The network protocol for the netfilter conntrack target ' + 'rule') + ) + parser.add_argument( + '--port', + metavar='', + type=int, + help=_('The network port for the netfilter conntrack target rule') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(client, parsed_args) + if attrs: + client.update_conntrack_helper( + parsed_args.conntrack_helper_id, attrs.pop('router_id'), + **attrs) + + +class ShowConntrackHelper(command.ShowOne): + _description = _("Display L3 conntrack helper details") + + def get_parser(self, prog_name): + parser = super(ShowConntrackHelper, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='', + help=_('Router that the conntrack helper belong to') + ) + parser.add_argument( + 'conntrack_helper_id', + metavar='', + help=_('The ID of the conntrack helper') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + router = client.find_router(parsed_args.router, ignore_missing=False) + obj = client.get_conntrack_helper( + parsed_args.conntrack_helper_id, router.id) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) diff --git a/openstackclient/tests/functional/network/v2/test_l3_conntrack_helper.py b/openstackclient/tests/functional/network/v2/test_l3_conntrack_helper.py new file mode 100644 index 0000000000..bbb9a7cd97 --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_l3_conntrack_helper.py @@ -0,0 +1,145 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +import json +import uuid + +from openstackclient.tests.functional.network.v2 import common + + +class L3ConntrackHelperTests(common.NetworkTests): + + def setUp(self): + super(L3ConntrackHelperTests, self).setUp() + # Nothing in this class works with Nova Network + if not self.haz_network: + self.skipTest("No Network service present") + if not self.is_extension_enabled('l3-conntrack-helper'): + self.skipTest("No l3-conntrack-helper extension present") + if not self.is_extension_enabled('expose-l3-conntrack-helper'): + self.skipTest("No expose-l3-conntrack-helper extension present") + + def _create_router(self): + router_name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + 'router create -f json ' + router_name + )) + self.assertIsNotNone(json_output['id']) + router_id = json_output['id'] + self.addCleanup(self.openstack, 'router delete ' + router_id) + return router_id + + def _create_helpers(self, router_id, helpers): + created_helpers = [] + for helper in helpers: + output = json.loads(self.openstack( + 'network l3 conntrack helper create %(router)s ' + '--helper %(helper)s --protocol %(protocol)s --port %(port)s ' + '-f json' % {'router': router_id, + 'helper': helper['helper'], + 'protocol': helper['protocol'], + 'port': helper['port']})) + self.assertEqual(helper['helper'], output['helper']) + self.assertEqual(helper['protocol'], output['protocol']) + self.assertEqual(helper['port'], output['port']) + created_helpers.append(output) + return created_helpers + + def test_l3_conntrack_helper_create_and_delete(self): + """Test create, delete multiple""" + + helpers = [ + { + 'helper': 'tftp', + 'protocol': 'udp', + 'port': 69 + }, { + 'helper': 'ftp', + 'protocol': 'tcp', + 'port': 21 + } + ] + router_id = self._create_router() + created_helpers = self._create_helpers(router_id, helpers) + ct_ids = " ".join([ct['id'] for ct in created_helpers]) + + raw_output = self.openstack( + '--debug network l3 conntrack helper delete %(router)s ' + '%(ct_ids)s' % { + 'router': router_id, 'ct_ids': ct_ids}) + self.assertOutput('', raw_output) + + def test_l3_conntrack_helper_list(self): + helpers = [ + { + 'helper': 'tftp', + 'protocol': 'udp', + 'port': 69 + }, { + 'helper': 'ftp', + 'protocol': 'tcp', + 'port': 21 + } + ] + expected_helpers = [ + { + 'Helper': 'tftp', + 'Protocol': 'udp', + 'Port': 69 + }, { + 'Helper': 'ftp', + 'Protocol': 'tcp', + 'Port': 21 + } + ] + router_id = self._create_router() + self._create_helpers(router_id, helpers) + output = json.loads(self.openstack( + 'network l3 conntrack helper list %s -f json ' % router_id + )) + for ct in output: + self.assertEqual(router_id, ct.pop('Router ID')) + ct.pop("ID") + self.assertIn(ct, expected_helpers) + + def test_l3_conntrack_helper_set_and_show(self): + helper = { + 'helper': 'tftp', + 'protocol': 'udp', + 'port': 69} + router_id = self._create_router() + created_helper = self._create_helpers(router_id, [helper])[0] + output = json.loads(self.openstack( + 'network l3 conntrack helper show %(router_id)s %(ct_id)s ' + '-f json' % { + 'router_id': router_id, 'ct_id': created_helper['id']})) + self.assertEqual(helper['helper'], output['helper']) + self.assertEqual(helper['protocol'], output['protocol']) + self.assertEqual(helper['port'], output['port']) + + raw_output = self.openstack( + 'network l3 conntrack helper set %(router_id)s %(ct_id)s ' + '--port %(port)s ' % { + 'router_id': router_id, + 'ct_id': created_helper['id'], + 'port': helper['port'] + 1}) + self.assertOutput('', raw_output) + + output = json.loads(self.openstack( + 'network l3 conntrack helper show %(router_id)s %(ct_id)s ' + '-f json' % { + 'router_id': router_id, 'ct_id': created_helper['id']})) + self.assertEqual(helper['port'] + 1, output['port']) + self.assertEqual(helper['helper'], output['helper']) + self.assertEqual(helper['protocol'], output['protocol']) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index e5023d43c5..ab77d719c0 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -1973,3 +1973,78 @@ def get_port_forwardings(port_forwardings=None, count=2): ) return mock.Mock(side_effect=port_forwardings) + + +class FakeL3ConntrackHelper(object): + """"Fake one or more L3 conntrack helper""" + + @staticmethod + def create_one_l3_conntrack_helper(attrs=None): + """Create a fake L3 conntrack helper. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with protocol, port, etc. + """ + attrs = attrs or {} + router_id = ( + attrs.get('router_id') or 'router-id-' + uuid.uuid4().hex + ) + # Set default attributes. + ct_attrs = { + 'id': uuid.uuid4().hex, + 'router_id': router_id, + 'helper': 'tftp', + 'protocol': 'tcp', + 'port': randint(1, 65535), + } + + # Overwrite default attributes. + ct_attrs.update(attrs) + + ct = fakes.FakeResource( + info=copy.deepcopy(ct_attrs), + loaded=True + ) + return ct + + @staticmethod + def create_l3_conntrack_helpers(attrs=None, count=2): + """Create multiple fake L3 Conntrack helpers. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of L3 Conntrack helper rule to fake + :return: + A list of FakeResource objects faking the Conntrack helpers + """ + ct_helpers = [] + for i in range(0, count): + ct_helpers.append( + FakeL3ConntrackHelper.create_one_l3_conntrack_helper(attrs) + ) + return ct_helpers + + @staticmethod + def get_l3_conntrack_helpers(ct_helpers=None, count=2): + """Get a list of faked L3 Conntrack helpers. + + If ct_helpers list is provided, then initialize the Mock object + with the list. Otherwise create one. + + :param List ct_helpers: + A list of FakeResource objects faking conntrack helpers + :param int count: + The number of L3 conntrack helpers to fake + :return: + An iterable Mock object with side_effect set to a list of faked + L3 conntrack helpers + """ + if ct_helpers is None: + ct_helpers = ( + FakeL3ConntrackHelper.create_l3_conntrack_helpers(count) + ) + + return mock.Mock(side_effect=ct_helpers) diff --git a/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py b/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py new file mode 100644 index 0000000000..1676c9ffad --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py @@ -0,0 +1,316 @@ +# 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 unittest import mock + +from osc_lib import exceptions + +from openstackclient.network.v2 import l3_conntrack_helper +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestConntrackHelper(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestConntrackHelper, self).setUp() + # Get a shortcut to the network client + self.network = self.app.client_manager.network + self.router = network_fakes.FakeRouter.create_one_router() + self.network.find_router = mock.Mock(return_value=self.router) + + +class TestCreateL3ConntrackHelper(TestConntrackHelper): + + def setUp(self): + super(TestCreateL3ConntrackHelper, self).setUp() + attrs = {'router_id': self.router.id} + self.ct_helper = ( + network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper( + attrs)) + self.columns = ( + 'helper', + 'id', + 'port', + 'protocol', + 'router_id' + ) + + self.data = ( + self.ct_helper.helper, + self.ct_helper.id, + self.ct_helper.port, + self.ct_helper.protocol, + self.ct_helper.router_id + ) + self.network.create_conntrack_helper = mock.Mock( + return_value=self.ct_helper) + + # Get the command object to test + self.cmd = l3_conntrack_helper.CreateConntrackHelper(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--helper', 'tftp', + '--protocol', 'udp', + '--port', '69', + self.router.id, + ] + + verifylist = [ + ('helper', 'tftp'), + ('protocol', 'udp'), + ('port', 69), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_conntrack_helper.assert_called_once_with( + self.router.id, + **{'helper': 'tftp', 'protocol': 'udp', + 'port': 69} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--protocol', 'udp', + '--port', '69', + self.router.id, + ] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, arglist, None) + + +class TestDeleteL3ConntrackHelper(TestConntrackHelper): + + def setUp(self): + super(TestDeleteL3ConntrackHelper, self).setUp() + attrs = {'router_id': self.router.id} + self.ct_helper = ( + network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper( + attrs)) + self.network.delete_conntrack_helper = mock.Mock( + return_value=None) + + # Get the command object to test + self.cmd = l3_conntrack_helper.DeleteConntrackHelper(self.app, + self.namespace) + + def test_delete(self): + arglist = [ + self.ct_helper.router_id, + self.ct_helper.id + ] + verifylist = [ + ('conntrack_helper_ids', [self.ct_helper.id]), + ('router', self.ct_helper.router_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.delete_conntrack_helper.assert_called_once_with( + self.ct_helper.id, self.router.id, + ignore_missing=False) + self.assertIsNone(result) + + def test_delete_error(self): + arglist = [ + self.router.id, + self.ct_helper.id + ] + verifylist = [ + ('conntrack_helper_ids', [self.ct_helper.id]), + ('router', self.router.id), + ] + self.network.delete_conntrack_helper.side_effect = Exception( + 'Error message') + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + + +class TestListL3ConntrackHelper(TestConntrackHelper): + + def setUp(self): + super(TestListL3ConntrackHelper, self).setUp() + attrs = {'router_id': self.router.id} + ct_helpers = ( + network_fakes.FakeL3ConntrackHelper.create_l3_conntrack_helpers( + attrs, count=3)) + self.columns = ( + 'ID', + 'Router ID', + 'Helper', + 'Protocol', + 'Port', + ) + self.data = [] + for ct_helper in ct_helpers: + self.data.append(( + ct_helper.id, + ct_helper.router_id, + ct_helper.helper, + ct_helper.protocol, + ct_helper.port, + )) + self.network.conntrack_helpers = mock.Mock( + return_value=ct_helpers) + + # Get the command object to test + self.cmd = l3_conntrack_helper.ListConntrackHelper(self.app, + self.namespace) + + def test_conntrack_helpers_list(self): + arglist = [ + self.router.id + ] + verifylist = [ + ('router', self.router.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.conntrack_helpers.assert_called_once_with( + self.router.id) + self.assertEqual(self.columns, columns) + list_data = list(data) + self.assertEqual(len(self.data), len(list_data)) + for index in range(len(list_data)): + self.assertEqual(self.data[index], list_data[index]) + + +class TestSetL3ConntrackHelper(TestConntrackHelper): + + def setUp(self): + super(TestSetL3ConntrackHelper, self).setUp() + attrs = {'router_id': self.router.id} + self.ct_helper = ( + network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper( + attrs)) + self.network.update_conntrack_helper = mock.Mock(return_value=None) + + # Get the command object to test + self.cmd = l3_conntrack_helper.SetConntrackHelper(self.app, + self.namespace) + + def test_set_nothing(self): + arglist = [ + self.router.id, + self.ct_helper.id, + ] + verifylist = [ + ('router', self.router.id), + ('conntrack_helper_id', self.ct_helper.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = (self.cmd.take_action(parsed_args)) + + self.network.update_conntrack_helper.assert_called_once_with( + self.ct_helper.id, self.router.id + ) + self.assertIsNone(result) + + def test_set_port(self): + arglist = [ + self.router.id, + self.ct_helper.id, + '--port', '124', + ] + verifylist = [ + ('router', self.router.id), + ('conntrack_helper_id', self.ct_helper.id), + ('port', 124), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = (self.cmd.take_action(parsed_args)) + + self.network.update_conntrack_helper.assert_called_once_with( + self.ct_helper.id, self.router.id, port=124 + ) + self.assertIsNone(result) + + +class TestShowL3ConntrackHelper(TestConntrackHelper): + + def setUp(self): + super(TestShowL3ConntrackHelper, self).setUp() + attrs = {'router_id': self.router.id} + self.ct_helper = ( + network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper( + attrs)) + self.columns = ( + 'helper', + 'id', + 'port', + 'protocol', + 'router_id' + ) + + self.data = ( + self.ct_helper.helper, + self.ct_helper.id, + self.ct_helper.port, + self.ct_helper.protocol, + self.ct_helper.router_id + ) + self.network.get_conntrack_helper = mock.Mock( + return_value=self.ct_helper) + + # Get the command object to test + self.cmd = l3_conntrack_helper.ShowConntrackHelper(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_default_options(self): + arglist = [ + self.router.id, + self.ct_helper.id, + ] + verifylist = [ + ('router', self.router.id), + ('conntrack_helper_id', self.ct_helper.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.get_conntrack_helper.assert_called_once_with( + self.ct_helper.id, self.router.id + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/releasenotes/notes/L3-conntrack-helper-bd0d9da041747e84.yaml b/releasenotes/notes/L3-conntrack-helper-bd0d9da041747e84.yaml new file mode 100644 index 0000000000..de5348dc16 --- /dev/null +++ b/releasenotes/notes/L3-conntrack-helper-bd0d9da041747e84.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add new commands ``network l3 conntrack helper create``, + ``network l3 conntrack helper set``, ``network l3 conntrack helper show``, + ``network l3 conntrack helper set`` and + ``network l3 conntrack helper delete`` to support Neutron L3 conntrack + helper CRUD operations. diff --git a/setup.cfg b/setup.cfg index d6d7a3d2b0..7084c70bf3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -448,6 +448,12 @@ openstack.network.v2 = network_show = openstackclient.network.v2.network:ShowNetwork network_unset = openstackclient.network.v2.network:UnsetNetwork + network_l3_conntrack_helper_create = openstackclient.network.v2.l3_conntrack_helper:CreateConntrackHelper + network_l3_conntrack_helper_delete = openstackclient.network.v2.l3_conntrack_helper:DeleteConntrackHelper + network_l3_conntrack_helper_list = openstackclient.network.v2.l3_conntrack_helper:ListConntrackHelper + network_l3_conntrack_helper_set = openstackclient.network.v2.l3_conntrack_helper:SetConntrackHelper + network_l3_conntrack_helper_show = openstackclient.network.v2.l3_conntrack_helper:ShowConntrackHelper + network_meter_create = openstackclient.network.v2.network_meter:CreateMeter network_meter_delete = openstackclient.network.v2.network_meter:DeleteMeter network_meter_list = openstackclient.network.v2.network_meter:ListMeter From 02d6fe9be629c28da2568d1ad51a5334064709bd Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Tue, 8 Jun 2021 15:28:06 +0900 Subject: [PATCH 0417/1120] L3 conntrack helper: Use singular name consistently We use singular form for delete command argument in all places. This commit replaces conntrack-helper-ids with a singular form. The only visible change is a fix for the help message below. openstack network l3 conntrack helper delete [ ...] Change-Id: I50bbd9f6199071bb86cbb2f37c45ebda1de58433 --- openstackclient/network/v2/l3_conntrack_helper.py | 8 ++++---- .../tests/unit/network/v2/test_l3_conntrack_helper.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openstackclient/network/v2/l3_conntrack_helper.py b/openstackclient/network/v2/l3_conntrack_helper.py index dae259273f..94788823ab 100644 --- a/openstackclient/network/v2/l3_conntrack_helper.py +++ b/openstackclient/network/v2/l3_conntrack_helper.py @@ -99,8 +99,8 @@ def get_parser(self, prog_name): help=_('Router that the conntrack helper belong to') ) parser.add_argument( - 'conntrack_helper_ids', - metavar='', + 'conntrack_helper_id', + metavar='', nargs='+', help=_('The ID of the conntrack helper(s) to delete') ) @@ -112,7 +112,7 @@ def take_action(self, parsed_args): result = 0 router = client.find_router(parsed_args.router, ignore_missing=False) - for ct_helper in parsed_args.conntrack_helper_ids: + for ct_helper in parsed_args.conntrack_helper_id: try: client.delete_conntrack_helper( ct_helper, router.id, ignore_missing=False) @@ -123,7 +123,7 @@ def take_action(self, parsed_args): {'ct_helper': ct_helper, 'e': e}) if result > 0: - total = len(parsed_args.conntrack_helper_ids) + total = len(parsed_args.conntrack_helper_id) msg = (_("%(result)s of %(total)s L3 conntrack helpers failed " "to delete.") % {'result': result, 'total': total}) raise exceptions.CommandError(msg) diff --git a/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py b/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py index 1676c9ffad..b3d026a7ef 100644 --- a/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py +++ b/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py @@ -127,7 +127,7 @@ def test_delete(self): self.ct_helper.id ] verifylist = [ - ('conntrack_helper_ids', [self.ct_helper.id]), + ('conntrack_helper_id', [self.ct_helper.id]), ('router', self.ct_helper.router_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -143,7 +143,7 @@ def test_delete_error(self): self.ct_helper.id ] verifylist = [ - ('conntrack_helper_ids', [self.ct_helper.id]), + ('conntrack_helper_id', [self.ct_helper.id]), ('router', self.router.id), ] self.network.delete_conntrack_helper.side_effect = Exception( From 34de2d3352aaef5c1bb86a5441cc8781e03b5587 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Jun 2021 16:58:07 +0100 Subject: [PATCH 0418/1120] volume: Add 'volume group snapshot *' commands These mirror the 'cinder group-snapshot-*' commands, with arguments copied across essentially verbatim. The only significant departure is the replacement of "tenant" terminology with "project". volume group snapshot create volume group snapshot delete volume group snapshot list volume group snapshot show Change-Id: Ia5084749b7c1a5a936fd6d6e8d89b9b80969f68c Signed-off-by: Stephen Finucane --- .../command-objects/volume-group-snapshot.rst | 8 + doc/source/cli/commands.rst | 1 + doc/source/cli/data/cinder.csv | 8 +- openstackclient/tests/unit/volume/v3/fakes.py | 53 ++++ .../volume/v3/test_volume_group_snapshot.py | 262 ++++++++++++++++++ .../volume/v3/volume_group_snapshot.py | 234 ++++++++++++++++ ...up-snapshot-commands-27fa8920d55f6bdb.yaml | 6 + setup.cfg | 5 + 8 files changed, 573 insertions(+), 4 deletions(-) create mode 100644 doc/source/cli/command-objects/volume-group-snapshot.rst create mode 100644 openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py create mode 100644 openstackclient/volume/v3/volume_group_snapshot.py create mode 100644 releasenotes/notes/add-volume-group-snapshot-commands-27fa8920d55f6bdb.yaml diff --git a/doc/source/cli/command-objects/volume-group-snapshot.rst b/doc/source/cli/command-objects/volume-group-snapshot.rst new file mode 100644 index 0000000000..02a33c1e3c --- /dev/null +++ b/doc/source/cli/command-objects/volume-group-snapshot.rst @@ -0,0 +1,8 @@ +===================== +volume group snapshot +===================== + +Block Storage v3 + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group snapshot * diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index ada38e3e57..7e59215271 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -160,6 +160,7 @@ referring to both Compute and Volume quotas. * ``volume backup record``: (**Volume**) volume record that can be imported or exported * ``volume backend``: (**Volume**) volume backend storage * ``volume group``: (**Volume**) group of volumes +* ``volume group snapshot``: (**Volume**) a point-in-time copy of a volume group * ``volume group type``: (**Volume**) deployment-specific types of volumes groups available * ``volume host``: (**Volume**) the physical computer for volumes * ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv index 5de8ea5cdd..9d79d1ba97 100644 --- a/doc/source/cli/data/cinder.csv +++ b/doc/source/cli/data/cinder.csv @@ -53,10 +53,10 @@ group-failover-replication,volume group failover,Fails over replication for grou group-list,volume group list,Lists all groups. (Supported by API versions 3.13 - 3.latest) group-list-replication-targets,volume group list --replication-targets,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest) group-show,volume group show,Shows details of a group. (Supported by API versions 3.13 - 3.latest) -group-snapshot-create,,Creates a group snapshot. (Supported by API versions 3.14 - 3.latest) -group-snapshot-delete,,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest) -group-snapshot-list,,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest) -group-snapshot-show,,Shows group snapshot details. (Supported by API versions 3.14 - 3.latest) +group-snapshot-create,volume group snapshot create,Creates a group snapshot. (Supported by API versions 3.14 - 3.latest) +group-snapshot-delete,volume group snapshot delete,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest) +group-snapshot-list,volume group snapshot list,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest) +group-snapshot-show,volume group snapshot show,Shows group snapshot details. (Supported by API versions 3.14 - 3.latest) group-specs-list,volume group type list,Lists current group types and specs. (Supported by API versions 3.11 - 3.latest) group-type-create,volume group type create,Creates a group type. (Supported by API versions 3.11 - 3.latest) group-type-default,volume group type list --default,List the default group type. (Supported by API versions 3.11 - 3.latest) diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index c300ca3855..9040b2be08 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -34,6 +34,8 @@ def __init__(self, **kwargs): self.attachments.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() self.groups.resource_class = fakes.FakeResource(None, {}) + self.group_snapshots = mock.Mock() + self.group_snapshots.resource_class = fakes.FakeResource(None, {}) self.group_types = mock.Mock() self.group_types.resource_class = fakes.FakeResource(None, {}) self.messages = mock.Mock() @@ -125,6 +127,57 @@ def create_volume_groups(attrs=None, count=2): 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.""" diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py b/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py new file mode 100644 index 0000000000..509d9f08f2 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py @@ -0,0 +1,262 @@ +# 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 cinderclient import api_versions +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_group_snapshot + + +class TestVolumeGroupSnapshot(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.volume_groups_mock = self.app.client_manager.volume.groups + self.volume_groups_mock.reset_mock() + + self.volume_group_snapshots_mock = \ + self.app.client_manager.volume.group_snapshots + self.volume_group_snapshots_mock.reset_mock() + + +class TestVolumeGroupSnapshotCreate(TestVolumeGroupSnapshot): + + fake_volume_group = volume_fakes.FakeVolumeGroup.create_one_volume_group() + fake_volume_group_snapshot = \ + volume_fakes.FakeVolumeGroupSnapshot.create_one_volume_group_snapshot() + + columns = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group', + 'Group Type', + ) + data = ( + fake_volume_group_snapshot.id, + fake_volume_group_snapshot.status, + fake_volume_group_snapshot.name, + fake_volume_group_snapshot.description, + fake_volume_group_snapshot.group_id, + fake_volume_group_snapshot.group_type_id, + ) + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_group_snapshots_mock.create.return_value = \ + self.fake_volume_group_snapshot + self.volume_group_snapshots_mock.get.return_value = \ + self.fake_volume_group_snapshot + + self.cmd = volume_group_snapshot.CreateVolumeGroupSnapshot( + self.app, None) + + def test_volume_group_snapshot_create(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.14') + + arglist = [ + self.fake_volume_group.id, + ] + verifylist = [ + ('volume_group', self.fake_volume_group.id), + ('name', None), + ('description', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.get.assert_called_once_with( + self.fake_volume_group.id) + self.volume_group_snapshots_mock.create.assert_called_once_with( + self.fake_volume_group.id, None, None, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_snapshot_create_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.14') + + arglist = [ + self.fake_volume_group.id, + '--name', 'foo', + '--description', 'hello, world', + ] + verifylist = [ + ('volume_group', self.fake_volume_group.id), + ('name', 'foo'), + ('description', 'hello, world'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.get.assert_called_once_with( + self.fake_volume_group.id) + self.volume_group_snapshots_mock.create.assert_called_once_with( + self.fake_volume_group.id, 'foo', 'hello, world', + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_snapshot_create_pre_v314(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group.id, + ] + verifylist = [ + ('volume_group', self.fake_volume_group.id), + ('name', None), + ('description', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.14 or greater is required', + str(exc)) + + +class TestVolumeGroupSnapshotDelete(TestVolumeGroupSnapshot): + + fake_volume_group_snapshot = \ + volume_fakes.FakeVolumeGroupSnapshot.create_one_volume_group_snapshot() + + def setUp(self): + super().setUp() + + self.volume_group_snapshots_mock.get.return_value = \ + self.fake_volume_group_snapshot + self.volume_group_snapshots_mock.delete.return_value = None + + self.cmd = volume_group_snapshot.DeleteVolumeGroupSnapshot( + self.app, None) + + def test_volume_group_snapshot_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.14') + + arglist = [ + self.fake_volume_group_snapshot.id, + ] + verifylist = [ + ('snapshot', self.fake_volume_group_snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_group_snapshots_mock.delete.assert_called_once_with( + self.fake_volume_group_snapshot.id, + ) + self.assertIsNone(result) + + def test_volume_group_snapshot_delete_pre_v314(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group_snapshot.id, + ] + verifylist = [ + ('snapshot', self.fake_volume_group_snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.14 or greater is required', + str(exc)) + + +class TestVolumeGroupSnapshotList(TestVolumeGroupSnapshot): + + fake_volume_group_snapshots = \ + volume_fakes.FakeVolumeGroupSnapshot.create_volume_group_snapshots() + + columns = ( + 'ID', + 'Status', + 'Name', + ) + data = [ + ( + fake_volume_group_snapshot.id, + fake_volume_group_snapshot.status, + fake_volume_group_snapshot.name, + ) for fake_volume_group_snapshot in fake_volume_group_snapshots + ] + + def setUp(self): + super().setUp() + + self.volume_group_snapshots_mock.list.return_value = \ + self.fake_volume_group_snapshots + + self.cmd = volume_group_snapshot.ListVolumeGroupSnapshot( + self.app, None) + + def test_volume_group_snapshot_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.14') + + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_snapshots_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': True, + }, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), data) + + def test_volume_group_snapshot_list_pre_v314(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + ] + verifylist = [ + ('all_projects', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.14 or greater is required', + str(exc)) diff --git a/openstackclient/volume/v3/volume_group_snapshot.py b/openstackclient/volume/v3/volume_group_snapshot.py new file mode 100644 index 0000000000..229cbd713c --- /dev/null +++ b/openstackclient/volume/v3/volume_group_snapshot.py @@ -0,0 +1,234 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from cinderclient import api_versions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def _format_group_snapshot(snapshot): + columns = ( + 'id', + 'status', + 'name', + 'description', + 'group_id', + 'group_type_id', + ) + column_headers = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group', + 'Group Type', + ) + + return ( + column_headers, + utils.get_item_properties( + snapshot, + columns, + ), + ) + + +class CreateVolumeGroupSnapshot(command.ShowOne): + """Create a volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.13 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'volume_group', + metavar='', + help=_('Name or ID of volume group to create a snapshot of.'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the volume group snapshot.'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('Description of a volume group snapshot.') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot create' command" + ) + raise exceptions.CommandError(msg) + + volume_group = utils.find_resource( + volume_client.groups, + parsed_args.volume_group, + ) + + snapshot = volume_client.group_snapshots.create( + volume_group.id, + parsed_args.name, + parsed_args.description) + + return _format_group_snapshot(snapshot) + + +class DeleteVolumeGroupSnapshot(command.Command): + """Delete a volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.14 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='', + help=_('Name or ID of volume group snapshot to delete'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot delete' command" + ) + raise exceptions.CommandError(msg) + + snapshot = utils.find_resource( + volume_client.group_snapshots, + parsed_args.snapshot, + ) + + volume_client.group_snapshots.delete(snapshot.id) + + +class ListVolumeGroupSnapshot(command.Lister): + """Lists all volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.14 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + dest='all_projects', + action='store_true', + default=utils.env('ALL_PROJECTS', default=False), + help=_('Shows details for all projects (admin only).'), + ) + # TODO(stephenfin): Add once we have an equivalent command for + # 'cinder list-filters' + # parser.add_argument( + # '--filter', + # metavar='', + # action=parseractions.KeyValueAction, + # dest='filters', + # help=_( + # "Filter key and value pairs. Use 'foo' to " + # "check enabled filters from server. Use 'key~=value' for " + # "inexact filtering if the key supports " + # "(supported by --os-volume-api-version 3.33 or above)" + # ), + # ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot list' command" + ) + raise exceptions.CommandError(msg) + + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + groups = volume_client.group_snapshots.list( + search_opts=search_opts) + + column_headers = ( + 'ID', + 'Status', + 'Name', + ) + columns = ( + 'id', + 'status', + 'name', + ) + + return ( + column_headers, + ( + utils.get_item_properties(a, columns) + for a in groups + ), + ) + + +class ShowVolumeGroupSnapshot(command.ShowOne): + """Show detailed information for a volume group snapshot. + + This command requires ``--os-volume-api-version`` 3.14 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='', + help=_('Name or ID of volume group snapshot.'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.14'): + msg = _( + "--os-volume-api-version 3.14 or greater is required to " + "support the 'volume group snapshot show' command" + ) + raise exceptions.CommandError(msg) + + snapshot = utils.find_resource( + volume_client.group_snapshots, + parsed_args.snapshot, + ) + + # TODO(stephenfin): Do we need this? + snapshot = volume_client.groups.show(snapshot.id) + + return _format_group_snapshot(snapshot) diff --git a/releasenotes/notes/add-volume-group-snapshot-commands-27fa8920d55f6bdb.yaml b/releasenotes/notes/add-volume-group-snapshot-commands-27fa8920d55f6bdb.yaml new file mode 100644 index 0000000000..820faf7361 --- /dev/null +++ b/releasenotes/notes/add-volume-group-snapshot-commands-27fa8920d55f6bdb.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``volume group snapshot create``, ``volume group snapshot delete``, + ``volume group snapshot list`` and ``volume group snapshot show`` commands + to create, delete, list, and show volume group snapshots, respectively. diff --git a/setup.cfg b/setup.cfg index 2a01ae7b0d..761736998a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -722,6 +722,11 @@ openstack.volume.v3 = volume_group_unset = openstackclient.volume.v3.volume_group:UnsetVolumeGroup volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup + volume_group_snapshot_create = openstackclient.volume.v3.volume_group_snapshot:CreateVolumeGroupSnapshot + volume_group_snapshot_delete = openstackclient.volume.v3.volume_group_snapshot:DeleteVolumeGroupSnapshot + volume_group_snapshot_list = openstackclient.volume.v3.volume_group_snapshot:ListVolumeGroupSnapshot + volume_group_snapshot_show = openstackclient.volume.v3.volume_group_snapshot:ShowVolumeGroupSnapshot + volume_group_type_create = openstackclient.volume.v3.volume_group_type:CreateVolumeGroupType volume_group_type_delete = openstackclient.volume.v3.volume_group_type:DeleteVolumeGroupType volume_group_type_list = openstackclient.volume.v3.volume_group_type:ListVolumeGroupType From 7f66dfe0e3e8720e847211494c185d7d4983ba5b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 9 Jun 2021 11:57:32 +0100 Subject: [PATCH 0419/1120] volume: Add more missing 'volume backup *' options Add an additional '--no-property' option to the 'volume backup set' command, along with a brand spanking new 'volume backup unset' command. Change-Id: Id7ca925e0ada03e259f0ecaf3e02af11c900641e Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v2/fakes.py | 3 + .../unit/volume/v2/test_volume_backup.py | 157 +++++++++++++++++- openstackclient/volume/v2/volume_backup.py | 87 +++++++++- ...g-volume-backup-opts-b9246aded87427ce.yaml | 8 +- setup.cfg | 1 + 5 files changed, 251 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 86778698da..b5f66d4b1d 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -17,6 +17,7 @@ from unittest import mock import uuid +from cinderclient import api_versions from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes @@ -292,6 +293,8 @@ 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() diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index 7b5a965e09..5d57bf609b 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -490,7 +490,9 @@ def test_backup_restore(self): class TestBackupSet(TestBackup): - backup = volume_fakes.FakeBackup.create_one_backup() + backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'metadata': {'wow': 'cool'}}, + ) def setUp(self): super(TestBackupSet, self).setUp() @@ -627,6 +629,159 @@ def test_backup_set_state_failed(self): self.backups_mock.reset_state.assert_called_with( self.backup.id, 'error') + def test_backup_set_no_property(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.43') + + arglist = [ + '--no-property', + self.backup.id, + ] + verifylist = [ + ('no_property', True), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'metadata': {}, + } + self.backups_mock.update.assert_called_once_with( + self.backup.id, + **kwargs + ) + self.assertIsNone(result) + + def test_backup_set_no_property_pre_v343(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.42') + + arglist = [ + '--no-property', + self.backup.id, + ] + verifylist = [ + ('no_property', True), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.43 or greater", str(exc)) + + def test_backup_set_property(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.43') + + arglist = [ + '--property', 'foo=bar', + self.backup.id, + ] + verifylist = [ + ('properties', {'foo': 'bar'}), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'metadata': {'wow': 'cool', 'foo': 'bar'}, + } + self.backups_mock.update.assert_called_once_with( + self.backup.id, + **kwargs + ) + self.assertIsNone(result) + + def test_backup_set_property_pre_v343(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.42') + + arglist = [ + '--property', 'foo=bar', + self.backup.id, + ] + verifylist = [ + ('properties', {'foo': 'bar'}), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.43 or greater", str(exc)) + + +class TestBackupUnset(TestBackup): + + backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'metadata': {'foo': 'bar'}}, + ) + + def setUp(self): + super().setUp() + + self.backups_mock.get.return_value = self.backup + + # Get the command object to test + self.cmd = volume_backup.UnsetVolumeBackup(self.app, None) + + def test_backup_unset_property(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.43') + + arglist = [ + '--property', 'foo', + self.backup.id, + ] + verifylist = [ + ('properties', ['foo']), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'metadata': {}, + } + self.backups_mock.update.assert_called_once_with( + self.backup.id, + **kwargs + ) + self.assertIsNone(result) + + def test_backup_unset_property_pre_v343(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.42') + + arglist = [ + '--property', 'foo', + self.backup.id, + ] + verifylist = [ + ('properties', ['foo']), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn("--os-volume-api-version 3.43 or greater", str(exc)) + class TestBackupShow(TestBackup): diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index a171164e3a..96b22a681c 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -14,6 +14,7 @@ """Volume v2 Backup action implementations""" +import copy import functools import logging @@ -405,11 +406,21 @@ def get_parser(self, prog_name): 'exercise caution when using)' ), ) + parser.add_argument( + '--no-property', + action='store_true', + help=_( + 'Remove all properties from this backup ' + '(specify both --no-property and --property to remove the ' + 'current properties before setting new properties)' + ), + ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, dest='properties', + default={}, help=_( 'Set a property on this backup ' '(repeat option to set multiple values) ' @@ -454,6 +465,14 @@ def take_action(self, parsed_args): kwargs['description'] = parsed_args.description + if parsed_args.no_property: + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --no-property option' + ) + raise exceptions.CommandError(msg) + if parsed_args.properties: if volume_client.api_version < api_versions.APIVersion('3.43'): msg = _( @@ -462,13 +481,20 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - kwargs['metadata'] = parsed_args.properties + if volume_client.api_version >= api_versions.APIVersion('3.43'): + metadata = copy.deepcopy(backup.metadata) + + if parsed_args.no_property: + metadata = {} + + metadata.update(parsed_args.properties) + kwargs['metadata'] = metadata if kwargs: try: volume_client.backups.update(backup.id, **kwargs) except Exception as e: - LOG.error("Failed to update backup name or description: %s", e) + LOG.error("Failed to update backup: %s", e) result += 1 if result > 0: @@ -476,6 +502,63 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) +class UnsetVolumeBackup(command.Command): + """Unset volume backup properties. + + This command requires ``--os-volume-api-version`` 3.43 or greater. + """ + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument( + 'backup', + metavar='', + help=_('Backup to modify (name or ID)') + ) + parser.add_argument( + '--property', + metavar='', + action='append', + dest='properties', + help=_( + 'Property to remove from this backup ' + '(repeat option to unset multiple values) ' + ), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + if volume_client.api_version < api_versions.APIVersion('3.43'): + msg = _( + '--os-volume-api-version 3.43 or greater is required to ' + 'support the --property option' + ) + raise exceptions.CommandError(msg) + + backup = utils.find_resource( + volume_client.backups, parsed_args.backup) + metadata = copy.deepcopy(backup.metadata) + + for key in parsed_args.properties: + if key not in metadata: + # ignore invalid properties but continue + LOG.warning( + "'%s' is not a valid property for backup '%s'", + key, parsed_args.backup, + ) + continue + + del metadata[key] + + kwargs = { + 'metadata': metadata, + } + + volume_client.backups.update(backup.id, **kwargs) + + class ShowVolumeBackup(command.ShowOne): _description = _("Display volume backup details") diff --git a/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml b/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml index 883cb0d564..f3b8bbc389 100644 --- a/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml +++ b/releasenotes/notes/add-missing-volume-backup-opts-b9246aded87427ce.yaml @@ -6,8 +6,12 @@ features: non-incremental backup, set a metadata property on the created backup, and set an availability zone on the created backup, respectively. - | - Add ``--property`` option the ``volume backup set`` command to set a - metadata property on an existing backup. + Add ``--property`` and ``--no-property`` options to the + ``volume backup set`` command to set a metadata property or remove all + metadata properties from an existing backup. + - | + Add new ``volume backup unset`` command to allow unsetting of properties + from an existing volume backup. fixes: - | The ``--name`` and ``--description`` options of the ``volume backup set`` diff --git a/setup.cfg b/setup.cfg index 761736998a..a46b7edb1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -709,6 +709,7 @@ openstack.volume.v3 = volume_backup_list = openstackclient.volume.v2.volume_backup:ListVolumeBackup volume_backup_restore = openstackclient.volume.v2.volume_backup:RestoreVolumeBackup volume_backup_set = openstackclient.volume.v2.volume_backup:SetVolumeBackup + volume_backup_unset = openstackclient.volume.v2.volume_backup:UnsetVolumeBackup volume_backup_show = openstackclient.volume.v2.volume_backup:ShowVolumeBackup volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord From 280b14abcddf3a308a819771117dd6b72d802642 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Jun 2021 16:19:08 +0100 Subject: [PATCH 0420/1120] compute: Note that '--password' is deployment-specific Password injection requires either hypervisor-support or an agent running in the guest that will talk to the metadata service. It can be disabled for a deployment using the '[api] enable_instance_password' nova config option. Indicate this, albeit briefly. Change-Id: Ief94ea07fc7ab6a487af972e8759ca6704d8f085 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 468c6b1b3e..272233917a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1012,7 +1012,10 @@ def get_parser(self, prog_name): parser.add_argument( '--password', metavar='', - help=_("Set the password to this server"), + help=_( + 'Set the password to this server. ' + 'This option requires cloud support.' + ), ) parser.add_argument( '--security-group', @@ -3142,7 +3145,10 @@ def get_parser(self, prog_name): parser.add_argument( '--password', metavar='', - help=_('Set the password on the rebuilt server'), + help=_( + 'Set the password on the rebuilt server. ' + 'This option requires cloud support.' + ), ) parser.add_argument( '--property', @@ -3435,7 +3441,8 @@ def get_parser(self, prog_name): '--password', metavar='', default=None, help=_( 'Set the password on the evacuated instance. This option is ' - 'mutually exclusive with the --shared-storage option' + 'mutually exclusive with the --shared-storage option. ' + 'This option requires cloud support.' ), ) shared_storage_group.add_argument( @@ -3725,7 +3732,10 @@ def get_parser(self, prog_name): parser.add_argument( '--password', metavar='', - help=_("Set the password on the rescued instance"), + help=_( + 'Set the password on the rescued instance. ' + 'This option requires cloud support.' + ), ) return parser @@ -3992,7 +4002,10 @@ def get_parser(self, prog_name): password_group = parser.add_mutually_exclusive_group() password_group.add_argument( '--password', - help=_('Set the server password'), + help=_( + 'Set the server password. ' + 'This option requires cloud support.' + ), ) password_group.add_argument( '--no-password', From 13de3494115c1c460613b9792d1a4186234a25be Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Jun 2021 18:09:32 +0100 Subject: [PATCH 0421/1120] compute: Better help text for 'openstack server set --state' Manually changing the server state is a potentially dangerous operation that should only be done under limited circumstances. It's also an admin-only operation by default. Highlight both points. Change-Id: Ifd8aec94937764202131ba8caf6b507caa76d7e9 Signed-off-by: Stephen Finucane Story: 2008549 Task: 41672 --- openstackclient/compute/v2/server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 468c6b1b3e..ef0f714915 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -4023,7 +4023,12 @@ def get_parser(self, prog_name): '--state', metavar='', choices=['active', 'error'], - help=_('New server state (valid value: active, error)'), + help=_( + 'New server state ' + '**WARNING** This can result in instances that are no longer ' + 'usable and should be used with caution ' + '(admin only)' + ), ) parser.add_argument( '--description', From 98979cfc7f27e323e576af58b5b5d9c0594a8a70 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Thu, 17 Jun 2021 17:07:39 +0000 Subject: [PATCH 0422/1120] Correct the tox option for skipping sdist generation The tox option to skip source distribution building is skipsdist, but this seems to be often misspelled skipdist instead, which gets silently ignored and so does not take effect. Correct it everywhere, in hopes that new projects will finally stop copying this mistake around. See https://tox.readthedocs.io/en/latest/config.html#conf-skipsdist and https://github.com/tox-dev/tox/issues/1388 for details. Change-Id: I05c1cc0c2fbf77021cc1e05bc96bee03528c69f0 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ebcdc2d5d9..c43e9d75ad 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 3.2.0 envlist = py38,pep8 -skipdist = True +skipsdist = True # Automatic envs (pyXX) will only use the python version appropriate to that # env and ignore basepython inherited from [testenv] if we set # ignore_basepython_conflict. From af406f33e38a8e9a5fb3c58e34f0d55c1d4d1d74 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 22 Jun 2021 18:25:41 +0100 Subject: [PATCH 0423/1120] cinder: Remove redundant command There is no 'volume group unset' command nor any need for one right now. This was mistakenly added in I3b2c0cb92b8a53cc1c0cefa3313b80f59c9e5835. Change-Id: I9386d1350099b10659c6b0e632e4d83cae5b2bfd Signed-off-by: Stephen Finucane --- .../notes/add-volume-group-commands-b121d6ec7da9779a.yaml | 2 +- setup.cfg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml b/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml index 8b3fe7ecc4..bd99818a0f 100644 --- a/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml +++ b/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml @@ -3,6 +3,6 @@ features: - | Add ``volume group create``, ``volume group delete``, ``volume group list``, ``volume group failover``, - ``volume group set/unset`` and ``volume attachment show`` + ``volume group set`` and ``volume attachment show`` commands to create, delete, list, failover, update and show volume groups, respectively. diff --git a/setup.cfg b/setup.cfg index b39061622b..cb15203803 100644 --- a/setup.cfg +++ b/setup.cfg @@ -726,7 +726,6 @@ openstack.volume.v3 = volume_group_list = openstackclient.volume.v3.volume_group:ListVolumeGroup volume_group_failover = openstackclient.volume.v3.volume_group:FailoverVolumeGroup volume_group_set = openstackclient.volume.v3.volume_group:SetVolumeGroup - volume_group_unset = openstackclient.volume.v3.volume_group:UnsetVolumeGroup volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup volume_group_snapshot_create = openstackclient.volume.v3.volume_group_snapshot:CreateVolumeGroupSnapshot From 4891bb38208fdcd1a2ae60e47b056841e14fbdf7 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Wed, 7 Jul 2021 19:43:00 -0500 Subject: [PATCH 0424/1120] Moving IRC network reference to OFTC Change-Id: I11e00f18fa8dca02bc0f136c0c5e9a2f040eef8f --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7dfabd8456..7f31bcdbea 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ language to describe operations in OpenStack. * `Developer`_ - getting started as a developer * `Contributing`_ - contributing code * `Testing`_ - testing code -* IRC: #openstack-sdks on Freenode (irc.freenode.net) +* IRC: #openstack-sdks on OFTC (irc.oftc.net) * License: Apache 2.0 .. _PyPi: https://pypi.org/project/python-openstackclient From a821d6b7c57c7684a990ee39b6b93d5085f25a70 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 13 Jul 2021 20:29:43 +0100 Subject: [PATCH 0425/1120] volume: Add 'volume transfer request create --(no-)snapshots' option This closes a gap with cinderclient's 'transfer-create' command. Change-Id: I7386a7be15c0e3ee87abbcfc2275ba8524c10ff8 Signed-off-by: Stephen Finucane Story: 2009054 Task: 42831 --- ...est.py => test_volume_transfer_request.py} | 46 +++++++++++++++++++ .../volume/v2/volume_transfer_request.py | 36 +++++++++++++++ ...reate-snapshots-opts-1361416d37021e89.yaml | 6 +++ 3 files changed, 88 insertions(+) rename openstackclient/tests/unit/volume/v2/{test_transfer_request.py => test_volume_transfer_request.py} (89%) create mode 100644 releasenotes/notes/add-volume-transfer-request-create-snapshots-opts-1361416d37021e89.yaml diff --git a/openstackclient/tests/unit/volume/v2/test_transfer_request.py b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py similarity index 89% rename from openstackclient/tests/unit/volume/v2/test_transfer_request.py rename to openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py index c9dce3cab0..1a1f220ff6 100644 --- a/openstackclient/tests/unit/volume/v2/test_transfer_request.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py @@ -15,6 +15,7 @@ from unittest import mock from unittest.mock import call +from cinderclient import api_versions from osc_lib import exceptions from osc_lib import utils @@ -172,6 +173,51 @@ def test_transfer_create_with_name(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_transfer_create_with_no_snapshots(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.55') + + arglist = [ + '--no-snapshots', + self.volume.id, + ] + verifylist = [ + ('name', None), + ('snapshots', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.transfer_mock.create.assert_called_once_with( + self.volume.id, None, no_snapshots=True) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_create_pre_v355(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.54') + + arglist = [ + '--no-snapshots', + self.volume.id, + ] + verifylist = [ + ('name', None), + ('snapshots', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.55 or greater is required', + str(exc)) + class TestTransferDelete(TestTransfer): diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index 2a1ace1f42..8919933609 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -16,6 +16,7 @@ import logging +from cinderclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -76,6 +77,25 @@ def get_parser(self, prog_name): metavar="", help=_('New transfer request name (default to None)'), ) + parser.add_argument( + '--snapshots', + action='store_true', + dest='snapshots', + help=_( + 'Allow transfer volumes without snapshots (default) ' + '(supported by --os-volume-api-version 3.55 or later)' + ), + default=None, + ) + parser.add_argument( + '--no-snapshots', + action='store_false', + dest='snapshots', + help=_( + 'Disallow transfer volumes without snapshots ' + '(supported by --os-volume-api-version 3.55 or later)' + ), + ) parser.add_argument( 'volume', metavar="", @@ -85,6 +105,21 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): volume_client = self.app.client_manager.volume + + kwargs = {} + + if parsed_args.snapshots is not None: + if volume_client.api_version < api_versions.APIVersion('3.55'): + msg = _( + "--os-volume-api-version 3.55 or greater is required to " + "support the '--(no-)snapshots' option" + ) + raise exceptions.CommandError(msg) + + # unfortunately this option is negative so we have to reverse + # things + kwargs['no_snapshots'] = not parsed_args.snapshots + volume_id = utils.find_resource( volume_client.volumes, parsed_args.volume, @@ -92,6 +127,7 @@ def take_action(self, parsed_args): volume_transfer_request = volume_client.transfers.create( volume_id, parsed_args.name, + **kwargs, ) volume_transfer_request._info.pop("links", None) diff --git a/releasenotes/notes/add-volume-transfer-request-create-snapshots-opts-1361416d37021e89.yaml b/releasenotes/notes/add-volume-transfer-request-create-snapshots-opts-1361416d37021e89.yaml new file mode 100644 index 0000000000..f915f87c16 --- /dev/null +++ b/releasenotes/notes/add-volume-transfer-request-create-snapshots-opts-1361416d37021e89.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The ``volume transfer request create`` command now accepts the + ``--snapshots`` / ``--no-snapshots`` option to configure whether to + create a transfer request for a volume without snapshots or not. From c1209601b4f4b81690a186e51aa819c783367fae Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 23 Jul 2021 12:48:23 +0100 Subject: [PATCH 0426/1120] tests: Handle removal of block-storage v2 API Cinder recently removed their v2 API [1] which is causing the functional tests to fail. Improve our 'is_service_enabled' test helper to use the 'versions show' command, which queries the service catalog and can give us information about the service version as well as answer the more general "is this service available" question. We also resolve a long-standing TODO in the process. [1] https://review.opendev.org/c/openstack/cinder/+/792299 Change-Id: I381069357aa008344e15327adf3a863c0c2e1f04 Signed-off-by: Stephen Finucane --- openstackclient/tests/functional/base.py | 29 ++++++++++++------- .../tests/functional/volume/v1/common.py | 23 ++++----------- .../tests/functional/volume/v2/common.py | 11 ++++++- .../tests/functional/volume/v3/common.py | 11 ++++++- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 3542a82756..0ed7dff8c4 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -68,17 +68,24 @@ def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): ) @classmethod - def is_service_enabled(cls, service): - """Ask client cloud if service is available""" - cmd = ('service show -f value -c enabled {service}' - .format(service=service)) - try: - return "True" in cls.openstack(cmd) - except exceptions.CommandFailed as e: - if "No service with a type, name or ID of" in str(e): - return False - else: - raise # Unable to determine if service is enabled + def is_service_enabled(cls, service, version=None): + """Ask client cloud if service is available + + :param service: The service name or type. This should be either an + exact match to what is in the catalog or a known official value or + alias from service-types-authority + :param version: Optional version. This should be a major version, e.g. + '2.0' + :returns: True if the service is enabled and optionally provides the + specified API version, else False + """ + ret = cls.openstack( + f'versions show --service {service} -f value -c Version' + ).splitlines() + if version: + return version in ret + + return bool(ret) @classmethod def is_extension_enabled(cls, alias): diff --git a/openstackclient/tests/functional/volume/v1/common.py b/openstackclient/tests/functional/volume/v1/common.py index 04eb1f48ae..755874785d 100644 --- a/openstackclient/tests/functional/volume/v1/common.py +++ b/openstackclient/tests/functional/volume/v1/common.py @@ -20,25 +20,14 @@ class BaseVolumeTests(volume_base.BaseVolumeTests): @classmethod def setUpClass(cls): - super(BaseVolumeTests, cls).setUpClass() - # TODO(dtroyer): This needs to be updated to specifically check for - # Volume v1 rather than just 'volume', but for now - # that is enough until we get proper version negotiation - cls.haz_volume_v1 = cls.is_service_enabled('volume') + super().setUpClass() + cls.haz_volume_v1 = cls.is_service_enabled('block-storage', '1.0') def setUp(self): - super(BaseVolumeTests, self).setUp() - - # This class requires Volume v1 - # if not self.haz_volume_v1: - # self.skipTest("No Volume v1 service present") - - # TODO(dtroyer): We really want the above to work but right now - # (12Sep2017) DevStack still creates a 'volume' - # service type even though there is no service behind - # it. Until that is fixed we need to just skip the - # volume v1 functional tests in master. - self.skipTest("No Volume v1 service present") + super().setUp() + + if not self.haz_volume_v1: + self.skipTest("No Volume v1 service present") ver_fixture = fixtures.EnvironmentVariable( 'OS_VOLUME_API_VERSION', '1' diff --git a/openstackclient/tests/functional/volume/v2/common.py b/openstackclient/tests/functional/volume/v2/common.py index 3817671425..7e3a80845a 100644 --- a/openstackclient/tests/functional/volume/v2/common.py +++ b/openstackclient/tests/functional/volume/v2/common.py @@ -18,8 +18,17 @@ class BaseVolumeTests(base.BaseVolumeTests): """Base class for Volume functional tests. """ + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.haz_volume_v2 = cls.is_service_enabled('block-storage', '2.0') + def setUp(self): - super(BaseVolumeTests, self).setUp() + super().setUp() + + if not self.haz_volume_v2: + self.skipTest("No Volume v2 service present") + ver_fixture = fixtures.EnvironmentVariable( 'OS_VOLUME_API_VERSION', '2' ) diff --git a/openstackclient/tests/functional/volume/v3/common.py b/openstackclient/tests/functional/volume/v3/common.py index a710a6835c..29f769b640 100644 --- a/openstackclient/tests/functional/volume/v3/common.py +++ b/openstackclient/tests/functional/volume/v3/common.py @@ -18,8 +18,17 @@ class BaseVolumeTests(base.BaseVolumeTests): """Base class for Volume functional tests. """ + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.haz_volume_v3 = cls.is_service_enabled('block-storage', '3.0') + def setUp(self): - super(BaseVolumeTests, self).setUp() + super().setUp() + + if not self.haz_volume_v3: + self.skipTest("No Volume v3 service present") + ver_fixture = fixtures.EnvironmentVariable( 'OS_VOLUME_API_VERSION', '3' ) From 4f6fe1c0fd0ee5be3cc78961fd334aac0bacd57b Mon Sep 17 00:00:00 2001 From: melanie witt Date: Tue, 27 Jul 2021 02:10:20 +0000 Subject: [PATCH 0427/1120] Fix TestListMigrationV223 test class MIGRATION_COLUMNS Currently only the test_server_migration_list adds the 'Id' and 'Type' columns to the expected output, so if the test_server_migration_list_no_options test is run by itself, it fails as the actual response contains 'Id' and 'Type' but the reference does not. This example run fails: tox -epy38 test_server_migration_list_no_options The reason the tests pass in the gate is because test_server_migration_list (which adds the 'Id' and 'Type' columns to self.MIGRATION_COLUMNS) appears to always run before test_server_migration_list_no_options, so the latter test gets the benefit of the former test's column additions. This changes the test class to just include the 'Id' and 'Type' columns all the time as they are always returned in microversion 2.23 anyway. Story: 2009079 Task: 42891 Change-Id: I2c97e9f64790b5e978e4d04230d45b8e343b53d4 --- openstackclient/tests/unit/compute/v2/test_server.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c6dff5a8a2..42c8816bcf 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4977,9 +4977,9 @@ class TestListMigrationV223(TestListMigration): """Test fetch all migrations. """ MIGRATION_COLUMNS = [ - 'Source Node', 'Dest Node', 'Source Compute', - 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', - 'Old Flavor', 'New Flavor', 'Created At', 'Updated At' + 'Id', 'Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', + 'Dest Host', 'Status', 'Server UUID', 'Old Flavor', 'New Flavor', + 'Type', 'Created At', 'Updated At' ] def setUp(self): @@ -5006,9 +5006,6 @@ def test_server_migration_list(self): self.migrations_mock.list.assert_called_with(**kwargs) - self.MIGRATION_COLUMNS.insert(0, "Id") - self.MIGRATION_COLUMNS.insert( - len(self.MIGRATION_COLUMNS) - 2, 'Type') self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) From e0dc31f32eb6720059439e791713e2c61f81bf70 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 27 Jul 2021 11:08:35 +0100 Subject: [PATCH 0428/1120] volume: Add missing 'volume list --offset' parameter Looking at the code for the ancient v1 cinder API, we see that this supported offset-style pagination [1][2][3]. Add this parameter, simplifying a future patch to standardize pagination across OSC. [1] https://github.com/openstack/cinder/blob/juno-eol/cinder/api/v1/volumes.py#L259 [2] https://github.com/openstack/cinder/blob/juno-eol/cinder/api/v1/volumes.py#L292 [3] https://github.com/openstack/cinder/blob/juno-eol/cinder/api/common.py#L120 Change-Id: Ifec208ea9ed7afb4bebced6132abb96a3af034b5 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v1/test_volume.py | 8 ++++++-- openstackclient/volume/v1/volume.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 704a66da71..b8002d6341 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -858,9 +858,10 @@ def test_volume_list_long(self): ), ) self.assertItemsEqual(datalist, tuple(data)) - def test_volume_list_with_limit(self): + def test_volume_list_with_limit_and_offset(self): arglist = [ '--limit', '2', + '--offset', '5', ] verifylist = [ ('long', False), @@ -868,6 +869,7 @@ def test_volume_list_with_limit(self): ('name', None), ('status', None), ('limit', 2), + ('offset', 5), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -876,9 +878,11 @@ def test_volume_list_with_limit(self): self.volumes_mock.list.assert_called_once_with( limit=2, search_opts={ + 'offset': 5, 'status': None, 'display_name': None, - 'all_tenants': False, } + 'all_tenants': False, + }, ) self.assertEqual(self.columns, columns) self.assertItemsEqual(self.datalist, tuple(data)) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 460bd85a8b..dfbb0c545f 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -327,6 +327,13 @@ def get_parser(self, prog_name): default=False, help=_('List additional fields in output'), ) + parser.add_argument( + '--offset', + type=int, + action=parseractions.NonNegativeAction, + metavar='', + help=_('Index from which to start listing volumes'), + ) parser.add_argument( '--limit', type=int, @@ -395,6 +402,9 @@ def take_action(self, parsed_args): 'status': parsed_args.status, } + if parsed_args.offset: + search_opts['offset'] = parsed_args.offset + data = volume_client.volumes.list( search_opts=search_opts, limit=parsed_args.limit, From ed87f7949ef1ef580ed71b9820e16823c0466472 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 26 Jul 2021 22:13:55 +0000 Subject: [PATCH 0429/1120] Correct REST API response fields for /os-migrations API The compute APIs are unfortunately inconsistent with regard to the response parameters for migrations. * GET /servers/{server_id}/migrations returns server_uuid * GET /os-migrations returns instance_uuid Because the 'Server UUID' column is being specified for parsing the response from GET /os-migrations, it is always showing as an empty string to users. There are a few other mismatches between the column names and the REST API response fields [1]: * 'Old Flavor' vs 'old_instance_type_id' * 'New Flavor' vs 'new_instance_type_id' * 'Type' vs 'migration_type' This adds a new list containing the REST API response field names to pass to utils.get_item_properties so that the responses are correctly parsed and the client output contains the response data instead of empty strings. Story: 2009078 Task: 42890 [1] https://docs.openstack.org/api-ref/compute/?expanded=list-migrations-detail#list-migrations Change-Id: I8aab60619e0225047f6a1c31e44917ca8fcc799e --- openstackclient/compute/v2/server.py | 27 +++++++--- .../tests/unit/compute/v2/fakes.py | 8 +-- .../tests/unit/compute/v2/test_server.py | 54 ++++++++++++++++++- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 24df46d8ce..a09fd44d0a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2781,28 +2781,41 @@ def get_parser(self, prog_name): return parser def print_migrations(self, parsed_args, compute_client, migrations): - columns = [ + column_headers = [ 'Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', 'Old Flavor', 'New Flavor', 'Created At', 'Updated At', ] + # Response fields coming back from the REST API are not always exactly + # the same as the column header names. + columns = [ + 'source_node', 'dest_node', 'source_compute', 'dest_compute', + 'dest_host', 'status', 'instance_uuid', 'old_instance_type_id', + 'new_instance_type_id', 'created_at', 'updated_at', + ] + # Insert migrations UUID after ID if compute_client.api_version >= api_versions.APIVersion("2.59"): - columns.insert(0, "UUID") + column_headers.insert(0, "UUID") + columns.insert(0, "uuid") if compute_client.api_version >= api_versions.APIVersion("2.23"): - columns.insert(0, "Id") - columns.insert(len(columns) - 2, "Type") + column_headers.insert(0, "Id") + columns.insert(0, "id") + column_headers.insert(len(column_headers) - 2, "Type") + columns.insert(len(columns) - 2, "migration_type") if compute_client.api_version >= api_versions.APIVersion("2.80"): if parsed_args.project: - columns.insert(len(columns) - 2, "Project") + column_headers.insert(len(column_headers) - 2, "Project") + columns.insert(len(columns) - 2, "project_id") if parsed_args.user: - columns.insert(len(columns) - 2, "User") + column_headers.insert(len(column_headers) - 2, "User") + columns.insert(len(columns) - 2, "user_id") return ( - columns, + column_headers, (utils.get_item_properties(mig, columns) for mig in migrations), ) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 4a2a44de12..47457acbc6 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1587,20 +1587,20 @@ def create_one_migration(attrs=None, methods=None): migration_info = { "dest_host": "10.0.2.15", "status": "migrating", - "type": "migration", + "migration_type": "migration", "updated_at": "2017-01-31T08:03:25.000000", "created_at": "2017-01-31T08:03:21.000000", "dest_compute": "compute-" + uuid.uuid4().hex, "id": random.randint(1, 999), "source_node": "node-" + uuid.uuid4().hex, - "server": uuid.uuid4().hex, + "instance_uuid": uuid.uuid4().hex, "dest_node": "node-" + uuid.uuid4().hex, "source_compute": "compute-" + uuid.uuid4().hex, "uuid": uuid.uuid4().hex, "old_instance_type_id": uuid.uuid4().hex, "new_instance_type_id": uuid.uuid4().hex, - "project": uuid.uuid4().hex, - "user": uuid.uuid4().hex + "project_id": uuid.uuid4().hex, + "user_id": uuid.uuid4().hex } # Overwrite default attributes. diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 42c8816bcf..57840cb076 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4909,6 +4909,13 @@ class TestListMigration(TestServer): 'Old Flavor', 'New Flavor', 'Created At', 'Updated At' ] + # These are the fields that come back in the response from the REST API. + MIGRATION_FIELDS = [ + 'source_node', 'dest_node', 'source_compute', 'dest_compute', + 'dest_host', 'status', 'instance_uuid', 'old_instance_type_id', + 'new_instance_type_id', 'created_at', 'updated_at' + ] + def setUp(self): super(TestListMigration, self).setUp() @@ -4920,7 +4927,7 @@ def setUp(self): self.migrations_mock.list.return_value = self.migrations self.data = (common_utils.get_item_properties( - s, self.MIGRATION_COLUMNS) for s in self.migrations) + s, self.MIGRATION_FIELDS) for s in self.migrations) # Get the command object to test self.cmd = server.ListMigration(self.app, None) @@ -4982,6 +4989,13 @@ class TestListMigrationV223(TestListMigration): 'Type', 'Created At', 'Updated At' ] + # These are the fields that come back in the response from the REST API. + MIGRATION_FIELDS = [ + 'id', 'source_node', 'dest_node', 'source_compute', 'dest_compute', + 'dest_host', 'status', 'instance_uuid', 'old_instance_type_id', + 'new_instance_type_id', 'migration_type', 'created_at', 'updated_at' + ] + def setUp(self): super(TestListMigrationV223, self).setUp() @@ -5019,6 +5033,14 @@ class TestListMigrationV259(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] + # These are the fields that come back in the response from the REST API. + MIGRATION_FIELDS = [ + 'id', 'uuid', 'source_node', 'dest_node', 'source_compute', + 'dest_compute', 'dest_host', 'status', 'instance_uuid', + 'old_instance_type_id', 'new_instance_type_id', 'migration_type', + 'created_at', 'updated_at' + ] + def setUp(self): super(TestListMigrationV259, self).setUp() @@ -5125,6 +5147,14 @@ class TestListMigrationV266(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] + # These are the fields that come back in the response from the REST API. + MIGRATION_FIELDS = [ + 'id', 'uuid', 'source_node', 'dest_node', 'source_compute', + 'dest_compute', 'dest_host', 'status', 'instance_uuid', + 'old_instance_type_id', 'new_instance_type_id', 'migration_type', + 'created_at', 'updated_at' + ] + def setUp(self): super(TestListMigrationV266, self).setUp() @@ -5194,6 +5224,14 @@ class TestListMigrationV280(TestListMigration): 'Old Flavor', 'New Flavor', 'Type', 'Created At', 'Updated At' ] + # These are the fields that come back in the response from the REST API. + MIGRATION_FIELDS = [ + 'id', 'uuid', 'source_node', 'dest_node', 'source_compute', + 'dest_compute', 'dest_host', 'status', 'instance_uuid', + 'old_instance_type_id', 'new_instance_type_id', 'migration_type', + 'created_at', 'updated_at' + ] + project = identity_fakes.FakeProject.create_one_project() user = identity_fakes.FakeUser.create_one_user() @@ -5247,10 +5285,14 @@ def test_server_migration_list_with_project(self): self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "Project") + self.MIGRATION_FIELDS.insert( + len(self.MIGRATION_FIELDS) - 2, "project_id") self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) # Clean up global variables MIGRATION_COLUMNS self.MIGRATION_COLUMNS.remove('Project') + # Clean up global variables MIGRATION_FIELDS + self.MIGRATION_FIELDS.remove('project_id') def test_get_migrations_with_project_pre_v280(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -5309,10 +5351,14 @@ def test_server_migration_list_with_user(self): self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "User") + self.MIGRATION_FIELDS.insert( + len(self.MIGRATION_FIELDS) - 2, "user_id") self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) # Clean up global variables MIGRATION_COLUMNS self.MIGRATION_COLUMNS.remove('User') + # Clean up global variables MIGRATION_FIELDS + self.MIGRATION_FIELDS.remove('user_id') def test_get_migrations_with_user_pre_v280(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -5371,13 +5417,19 @@ def test_server_migration_list_with_project_and_user(self): self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "Project") + self.MIGRATION_FIELDS.insert( + len(self.MIGRATION_FIELDS) - 2, "project_id") self.MIGRATION_COLUMNS.insert( len(self.MIGRATION_COLUMNS) - 2, "User") + self.MIGRATION_FIELDS.insert( + len(self.MIGRATION_FIELDS) - 2, "user_id") self.assertEqual(self.MIGRATION_COLUMNS, columns) self.assertEqual(tuple(self.data), tuple(data)) # Clean up global variables MIGRATION_COLUMNS self.MIGRATION_COLUMNS.remove('Project') + self.MIGRATION_FIELDS.remove('project_id') self.MIGRATION_COLUMNS.remove('User') + self.MIGRATION_FIELDS.remove('user_id') def test_get_migrations_with_project_and_user_pre_v280(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( From 12c93c6d5ff420f6a4a8833d33bad6ee7222e2f7 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Thu, 12 Aug 2021 22:06:36 +0000 Subject: [PATCH 0430/1120] Show "Forced Down" compute service status with --long Currently, the unified client does not have the ability to show the "Forced Down" field of a GET /os-services response in microversion 2.11 even though the legacy client can. This adds a "Forced Down" column to the 'openstack compute service list --long' command output when microversion 2.11 is used. Story: 2009115 Task: 43011 Change-Id: I10bc2fedbf0e867a990227962b2b6e60f5681f69 --- openstackclient/compute/v2/service.py | 4 +++ .../tests/unit/compute/v2/fakes.py | 2 ++ .../tests/unit/compute/v2/test_service.py | 32 +++++++++++++++++++ ...ice-list-forced-down-2b16d1cb44f71a08.yaml | 5 +++ 4 files changed, 43 insertions(+) create mode 100644 releasenotes/notes/compute-service-list-forced-down-2b16d1cb44f71a08.yaml diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 625c0fad3a..6427e548be 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -103,6 +103,10 @@ def take_action(self, parsed_args): "Updated At", "Disabled Reason" ) + has_forced_down = ( + compute_client.api_version >= api_versions.APIVersion('2.11')) + if has_forced_down: + columns += ("Forced Down",) else: columns = ( "ID", diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 47457acbc6..05a14e16a7 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -722,6 +722,8 @@ def create_one_service(attrs=None): 'state': 'state-' + uuid.uuid4().hex, 'updated_at': 'time-' + uuid.uuid4().hex, 'disabled_reason': 'earthquake', + # Introduced in API microversion 2.11 + 'forced_down': False, } # Overwrite default attributes. diff --git a/openstackclient/tests/unit/compute/v2/test_service.py b/openstackclient/tests/unit/compute/v2/test_service.py index 87e54747f1..c547c3a687 100644 --- a/openstackclient/tests/unit/compute/v2/test_service.py +++ b/openstackclient/tests/unit/compute/v2/test_service.py @@ -190,6 +190,38 @@ def test_service_list_with_long_option(self): self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + def test_service_list_with_long_option_2_11(self): + arglist = [ + '--host', self.service.host, + '--service', self.service.binary, + '--long' + ] + verifylist = [ + ('host', self.service.host), + ('service', self.service.binary), + ('long', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.compute.api_version = api_versions.APIVersion( + '2.11') + + # 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.service_mock.list.assert_called_with( + self.service.host, + self.service.binary, + ) + + # In 2.11 there is also a forced_down column. + columns_long = self.columns_long + ('Forced Down',) + data_long = [self.data_long[0] + (self.service.forced_down,)] + + self.assertEqual(columns_long, columns) + self.assertEqual(data_long, list(data)) + class TestServiceSet(TestService): diff --git a/releasenotes/notes/compute-service-list-forced-down-2b16d1cb44f71a08.yaml b/releasenotes/notes/compute-service-list-forced-down-2b16d1cb44f71a08.yaml new file mode 100644 index 0000000000..ebdc41556f --- /dev/null +++ b/releasenotes/notes/compute-service-list-forced-down-2b16d1cb44f71a08.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add column ``Forced Down`` to the output of ``compute service list + --long``. Only available starting with ``--os-compute-api-version 2.11``. From 4aad7dd77953e09b4973df0b37d1cb23d8b0afbf Mon Sep 17 00:00:00 2001 From: LEE JAE YONG Date: Sat, 28 Aug 2021 07:17:04 +0000 Subject: [PATCH 0431/1120] Fix typo error in listing server's column name openstack server list -c "Created At" command doesn't work because the wrong variable was used here. When we receive resp data, Created At data is saved with the name "created". But in "server.py", we append columns as created_at. So it seems to print an empty table. Story: 2009149 Task: 43112 Change-Id: I06de6903d5cc427a8b0fdcd168fec47192f4365b --- openstackclient/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a09fd44d0a..6996405faa 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2341,7 +2341,7 @@ def take_action(self, parsed_args): columns.append('user_id') column_headers.append('User ID') if c in ('Created At', 'created_at'): - columns.append('created_at') + columns.append('created') column_headers.append('Created At') # convert back to tuple From 6ce7da8aeb7e5d1347940433e087036e8e43eaa6 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Mon, 30 Aug 2021 12:06:10 -0500 Subject: [PATCH 0432/1120] [community goal] Update contributor documentation This patch updates/adds the contributor documentation to follow the guidelines of the Ussuri cycle community goal[1]. [1] https://governance.openstack.org/tc/goals/selected/ussuri/project-ptl-and-contrib-docs.html Story: #2007236 Task: #38547 Change-Id: I0afa1796d488a96160f4a7fd615920d05fe1771c --- CONTRIBUTING.rst | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 69ecd79cad..f8732b7211 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,16 +1,27 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps documented at: +The source repository for this project can be found at: - https://docs.openstack.org/infra/manual/developers.html + https://opendev.org/openstack/python-openstackclient -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +Pull requests submitted through GitHub are not monitored. - https://docs.openstack.org/infra/manual/developers.html#development-workflow +To start contributing to OpenStack, follow the steps in the contribution guide +to set up and use Gerrit: -Pull requests submitted through GitHub will be ignored. + https://docs.openstack.org/contributors/code-and-documentation/quick-start.html -Bugs should be filed on Storyboard, not GitHub or Launchpad: +Bugs should be filed on StoryBoard: https://storyboard.openstack.org/#!/project/openstack/python-openstackclient + +Developers should also join the discussion on the mailing list, at: + + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss + +or join the IRC channel on + + #openstack-sdks on OFTC (irc.oftc.net) + +For more specific information about contributing to this repository, see the +openstacksdk contributor guide: + + https://docs.openstack.org/openstacksdk/latest/contributor/index.html From 8e833a3ed26467a1190ba69d8ba716a7cd1cccb3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 1 Sep 2021 13:04:51 +0100 Subject: [PATCH 0433/1120] compute: Add support for microversion 2.90 Allow configuring hostname when creating a new server or updating or rebuilding an existing server. Change-Id: Ibe603eab78bbbec43605f56de62a20493b6aa93d Signed-off-by: Stephen Finucane Depends-On: https://review.opendev.org/c/openstack/python-novaclient/+/806917 --- openstackclient/compute/v2/server.py | 76 +++++++- .../tests/unit/compute/v2/test_server.py | 171 ++++++++++++++++-- ...server-hostname-opts-3cb4fd90b5bf47ca.yaml | 8 + 3 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a09fd44d0a..4750583812 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1154,6 +1154,18 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.52 or above)' ), ) + parser.add_argument( + '--hostname', + metavar='', + help=_( + 'Hostname configured for the server in the metadata service. ' + 'If unset, a hostname will be automatically generated from ' + 'the server name. ' + 'A utility such as cloud-init is required to propagate the ' + 'hostname in the metadata service to the guest OS itself. ' + '(supported by --os-compute-api-version 2.90 or above)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -1618,6 +1630,16 @@ def _match_image(image_api, wanted_properties): boot_kwargs['hypervisor_hostname'] = ( parsed_args.hypervisor_hostname) + if parsed_args.hostname: + if compute_client.api_version < api_versions.APIVersion("2.90"): + msg = _( + '--os-compute-api-version 2.90 or greater is required to ' + 'support the --hostname option' + ) + raise exceptions.CommandError(msg) + + boot_kwargs['hostname'] = parsed_args.hostname + LOG.debug('boot_args: %s', boot_args) LOG.debug('boot_kwargs: %s', boot_kwargs) @@ -3273,6 +3295,16 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.63 or above)' ), ) + parser.add_argument( + '--hostname', + metavar='', + help=_( + 'Hostname configured for the server in the metadata service. ' + 'A separate utility running in the guest is required to ' + 'propagate changes to this value to the guest OS itself. ' + '(supported by --os-compute-api-version 2.90 or above)' + ), + ) parser.add_argument( '--wait', action='store_true', @@ -3390,6 +3422,16 @@ def _show_progress(progress): kwargs['trusted_image_certificates'] = None + if parsed_args.hostname: + if compute_client.api_version < api_versions.APIVersion('2.90'): + msg = _( + '--os-compute-api-version 2.90 or greater is required to ' + 'support the --hostname option' + ) + raise exceptions.CommandError(msg) + + kwargs['hostname'] = parsed_args.hostname + try: server = server.rebuild(image, parsed_args.password, **kwargs) finally: @@ -4076,6 +4118,16 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.26 or above)' ), ) + parser.add_argument( + '--hostname', + metavar='', + help=_( + 'Hostname configured for the server in the metadata service. ' + 'A separate utility running in the guest is required to ' + 'propagate changes to this value to the guest OS itself. ' + '(supported by --os-compute-api-version 2.90 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -4102,8 +4154,27 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) + if parsed_args.hostname: + if server.api_version < api_versions.APIVersion('2.90'): + msg = _( + '--os-compute-api-version 2.90 or greater is required to ' + 'support the --hostname option' + ) + raise exceptions.CommandError(msg) + + update_kwargs = {} + if parsed_args.name: - server.update(name=parsed_args.name) + update_kwargs['name'] = parsed_args.name + + if parsed_args.description: + update_kwargs['description'] = parsed_args.description + + if parsed_args.hostname: + update_kwargs['hostname'] = parsed_args.hostname + + if update_kwargs: + server.update(**update_kwargs) if parsed_args.properties: compute_client.servers.set_meta(server, parsed_args.properties) @@ -4124,9 +4195,6 @@ def take_action(self, parsed_args): elif parsed_args.no_password: server.clear_password() - if parsed_args.description: - server.update(description=parsed_args.description) - if parsed_args.tags: for tag in parsed_args.tags: server.add_tag(tag=tag) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 57840cb076..cab9efd0c0 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3554,6 +3554,76 @@ def test_server_create_with_host_and_hypervisor_hostname_v274(self): self.assertFalse(self.images_mock.called) self.assertFalse(self.flavors_mock.called) + def test_server_create_with_hostname_v290(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.90') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hostname', 'hostname', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('hostname', 'hostname'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + admin_pass=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + hostname='hostname', + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_hostname_pre_v290(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.89') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--hostname', 'hostname', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('hostname', 'hostname'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerDelete(TestServer): @@ -6235,6 +6305,46 @@ def test_rebuild_with_no_trusted_image_cert_pre_257(self): self.cmd.take_action, parsed_args) + def test_rebuild_with_hostname(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.90') + + arglist = [ + self.server.id, + '--hostname', 'new-hostname' + ] + verifylist = [ + ('server', self.server.id), + ('hostname', 'new-hostname') + ] + 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.get_image_mock.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with( + self.image, None, hostname='new-hostname') + + def test_rebuild_with_hostname_pre_v290(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.89') + + arglist = [ + self.server.id, + '--hostname', 'new-hostname', + ] + verifylist = [ + ('server', self.server.id), + ('hostname', 'new-hostname') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + class TestEvacuateServer(TestServer): @@ -7340,10 +7450,10 @@ def test_server_set_with_root_password(self, mock_getpass): mock.sentinel.fake_pass) self.assertIsNone(result) - def test_server_set_with_description_api_newer(self): + def test_server_set_with_description(self): # Description is supported for nova api version 2.19 or above - self.fake_servers[0].api_version = 2.19 + self.fake_servers[0].api_version = api_versions.APIVersion('2.19') arglist = [ '--description', 'foo_description', @@ -7354,18 +7464,15 @@ def test_server_set_with_description_api_newer(self): ('server', 'foo_vm'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.19): - result = self.cmd.take_action(parsed_args) - self.fake_servers[0].update.assert_called_once_with( - description='foo_description') - self.assertIsNone(result) + result = self.cmd.take_action(parsed_args) + self.fake_servers[0].update.assert_called_once_with( + description='foo_description') + self.assertIsNone(result) - def test_server_set_with_description_api_older(self): + def test_server_set_with_description_pre_v219(self): # Description is not supported for nova api version below 2.19 - self.fake_servers[0].api_version = 2.18 + self.fake_servers[0].api_version = api_versions.APIVersion('2.18') arglist = [ '--description', 'foo_description', @@ -7376,11 +7483,8 @@ def test_server_set_with_description_api_older(self): ('server', 'foo_vm'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - with mock.patch.object(api_versions, - 'APIVersion', - return_value=2.19): - self.assertRaises(exceptions.CommandError, self.cmd.take_action, - parsed_args) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) def test_server_set_with_tag(self): self.fake_servers[0].api_version = api_versions.APIVersion('2.26') @@ -7426,6 +7530,41 @@ def test_server_set_with_tag_pre_v226(self): '--os-compute-api-version 2.26 or greater is required', str(ex)) + def test_server_set_with_hostname(self): + + self.fake_servers[0].api_version = api_versions.APIVersion('2.90') + + arglist = [ + '--hostname', 'foo-hostname', + 'foo_vm', + ] + verifylist = [ + ('hostname', 'foo-hostname'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.fake_servers[0].update.assert_called_once_with( + hostname='foo-hostname') + self.assertIsNone(result) + + def test_server_set_with_hostname_pre_v290(self): + + self.fake_servers[0].api_version = api_versions.APIVersion('2.89') + + arglist = [ + '--hostname', 'foo-hostname', + 'foo_vm', + ] + verifylist = [ + ('hostname', 'foo-hostname'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerShelve(TestServer): diff --git a/releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml b/releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml new file mode 100644 index 0000000000..458d1529e8 --- /dev/null +++ b/releasenotes/notes/add-server-hostname-opts-3cb4fd90b5bf47ca.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``server create``, ``server set`` and ``server rebuild`` commands now + accept an optional ``--hostname HOSTNAME`` option. This can be used to + configure the hostname stored in the metadata service and/or config drive. + Utilities such as ``cloud-init`` can then consume this information to set + the hostname within the guest OS. From 51ee17a94dccd297101725593b223f01c4f9b906 Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Thu, 12 Aug 2021 11:27:17 +0100 Subject: [PATCH 0434/1120] compute: Add support for microversion 2.89 This microversion drops the duplicate ``id`` field while adding ``attachment_id`` and ``bdm_uuid`` to the output of the os-volume_attachments API reflected within osc by the ``openstack server volume list $server``command. Depends-On: https://review.opendev.org/c/openstack/nova/+/804275 Change-Id: I8a7002d8d65d7795e106b768df868198ab8b8143 --- openstackclient/compute/v2/server_volume.py | 18 +++++-- .../tests/unit/compute/v2/fakes.py | 3 ++ .../unit/compute/v2/test_server_volume.py | 49 +++++++++++++++++++ ...to_volume_attachment-cea605585db29e14.yaml | 11 +++++ 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add_attachment_id_to_volume_attachment-cea605585db29e14.yaml diff --git a/openstackclient/compute/v2/server_volume.py b/openstackclient/compute/v2/server_volume.py index b53c92fe5e..d53cec931d 100644 --- a/openstackclient/compute/v2/server_volume.py +++ b/openstackclient/compute/v2/server_volume.py @@ -44,18 +44,24 @@ def take_action(self, parsed_args): volumes = compute_client.volumes.get_server_volumes(server.id) - columns = ( - 'id', + columns = () + column_headers = () + + if compute_client.api_version < api_versions.APIVersion('2.89'): + columns += ('id',) + column_headers += ('ID',) + + columns += ( 'device', 'serverId', 'volumeId', ) - column_headers = ( - 'ID', + column_headers += ( 'Device', 'Server ID', 'Volume ID', ) + if compute_client.api_version >= api_versions.APIVersion('2.70'): columns += ('tag',) column_headers += ('Tag',) @@ -64,6 +70,10 @@ def take_action(self, parsed_args): columns += ('delete_on_termination',) column_headers += ('Delete On Termination?',) + if compute_client.api_version >= api_versions.APIVersion('2.89'): + columns += ('attachment_id', 'bdm_uuid') + column_headers += ('Attachment ID', 'BlockDeviceMapping UUID') + return ( column_headers, ( diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 05a14e16a7..3142a24489 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1715,6 +1715,9 @@ def create_one_volume_attachment(attrs=None, methods=None): "tag": "foo", # introduced in API microversion 2.79 "delete_on_termination": True, + # introduced in API microversion 2.89 + "attachment_id": uuid.uuid4().hex, + "bdm_uuid": uuid.uuid4().hex } # Overwrite default attributes. diff --git a/openstackclient/tests/unit/compute/v2/test_server_volume.py b/openstackclient/tests/unit/compute/v2/test_server_volume.py index 4d4916b7ec..02d378f82a 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_volume.py +++ b/openstackclient/tests/unit/compute/v2/test_server_volume.py @@ -167,6 +167,55 @@ def test_server_volume_list_with_delete_on_attachment(self): self.servers_volumes_mock.get_server_volumes.assert_called_once_with( self.server.id) + def test_server_volume_list_with_attachment_ids(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.89') + + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual( + ( + 'Device', 'Server ID', 'Volume ID', 'Tag', + 'Delete On Termination?', 'Attachment ID', + 'BlockDeviceMapping UUID', + ), + columns, + ) + self.assertEqual( + ( + ( + self.volume_attachments[0].device, + self.volume_attachments[0].serverId, + self.volume_attachments[0].volumeId, + self.volume_attachments[0].tag, + self.volume_attachments[0].delete_on_termination, + self.volume_attachments[0].attachment_id, + self.volume_attachments[0].bdm_uuid + + ), + ( + self.volume_attachments[1].device, + self.volume_attachments[1].serverId, + self.volume_attachments[1].volumeId, + self.volume_attachments[1].tag, + self.volume_attachments[1].delete_on_termination, + self.volume_attachments[1].attachment_id, + self.volume_attachments[1].bdm_uuid + ), + ), + tuple(data), + ) + self.servers_volumes_mock.get_server_volumes.assert_called_once_with( + self.server.id) + class TestServerVolumeUpdate(TestServerVolume): diff --git a/releasenotes/notes/add_attachment_id_to_volume_attachment-cea605585db29e14.yaml b/releasenotes/notes/add_attachment_id_to_volume_attachment-cea605585db29e14.yaml new file mode 100644 index 0000000000..9a43989683 --- /dev/null +++ b/releasenotes/notes/add_attachment_id_to_volume_attachment-cea605585db29e14.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for `microversion 2.89`_. This microversion removes the + ``id`` field while adding the ``attachment_id`` and ``bdm_uuid`` fields to + the responses of ``GET /servers/{server_id}/os-volume_attachments`` and + ``GET /servers/{server_id}/os-volume_attachments/{volume_id}`` with these + changes reflected in novaclient under the ``openstack server volume list`` + command. + + .. _microversion 2.89: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-89 From ed5d2a37c51dfc621b0d8ac79510e21c9ee3dd0f Mon Sep 17 00:00:00 2001 From: Alfredo Moralejo Date: Thu, 9 Sep 2021 15:49:04 +0200 Subject: [PATCH 0435/1120] Replace assertItemsEqual with assertCountEqual Follow-up of [1]. After this patch was sent, two more assertItemsEqual were added in [2]. This patch is fixing it. [1] https://review.opendev.org/c/openstack/python-openstackclient/+/789410 [2] https://review.opendev.org/c/openstack/python-openstackclient/+/781637 Change-Id: Ic2276bd0ff0f5df76505f37d8994b3384d40e9a7 --- openstackclient/tests/unit/volume/v3/test_volume_message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_message.py b/openstackclient/tests/unit/volume/v3/test_volume_message.py index 68becf4418..8cabc0c3ad 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_message.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_message.py @@ -198,7 +198,7 @@ def test_message_list(self): limit=None, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_message_list_with_options(self): self.app.client_manager.volume.api_version = \ @@ -227,7 +227,7 @@ def test_message_list_with_options(self): limit=3, ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_message_list_pre_v33(self): self.app.client_manager.volume.api_version = \ From 8ef9280af93b46e44b489592d3ae640a01c98190 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Wed, 22 Sep 2021 10:42:16 +0000 Subject: [PATCH 0436/1120] Update master for stable/xena Add file to the reno documentation build to show release notes for stable/xena. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/xena. Sem-Ver: feature Change-Id: Iedf2c908bf5a9d87effa02717eb604ee8d15ef3b --- releasenotes/source/index.rst | 1 + releasenotes/source/xena.rst | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/xena.rst diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index cdbb595ce8..0ad18a2b9f 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + xena wallaby victoria ussuri diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst new file mode 100644 index 0000000000..1be85be3eb --- /dev/null +++ b/releasenotes/source/xena.rst @@ -0,0 +1,6 @@ +========================= +Xena Series Release Notes +========================= + +.. release-notes:: + :branch: stable/xena From ff372ffdfbfe036993f84be20cd18262599b37de Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Wed, 22 Sep 2021 10:42:18 +0000 Subject: [PATCH 0437/1120] Add Python3 yoga unit tests This is an automatically generated patch to ensure unit testing is in place for all the of the tested runtimes for yoga. See also the PTI in governance [1]. [1]: https://governance.openstack.org/tc/reference/project-testing-interface.html Change-Id: I89cff43c0eb97c63deaba320e0fc63bd8ba31a2a --- .zuul.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index e1c1f970af..9ed506c676 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -240,7 +240,7 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python3-xena-jobs + - openstack-python3-yoga-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 From c0a0f0f3d86cacbff386a5ea7b8e846dd595e197 Mon Sep 17 00:00:00 2001 From: ryanKor Date: Sat, 25 Sep 2021 18:21:03 +0900 Subject: [PATCH 0438/1120] Fix that the path of functional test before change: $ tox -e functional -- --regex functional.tests.compute.v2.test_server after change: $ tox -e functional -- --regex tests.functional.compute.v2.test_server the test unit path document should be change the above line. (fixed wrong letter) Change-Id: I49674fb0d56ee65c1f6328b9d960b16876173e2d --- doc/source/contributor/developing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor/developing.rst b/doc/source/contributor/developing.rst index a70b33268a..c6573b9ba6 100644 --- a/doc/source/contributor/developing.rst +++ b/doc/source/contributor/developing.rst @@ -88,7 +88,7 @@ To run a specific functional test: .. code-block:: bash - $ tox -e functional -- --regex functional.tests.compute.v2.test_server + $ tox -e functional -- --regex tests.functional.compute.v2.test_server Running with PDB ~~~~~~~~~~~~~~~~ From 28a376bfb0a330470b028b6d5244ee4c8e1fe864 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Thu, 30 Sep 2021 17:14:19 +0300 Subject: [PATCH 0439/1120] Add --trusted-image-cert option for server create this already exists for server rebuild, but was missing for server create. This option is supported from Compute API version >= 2.63, and is only available for servers booted directly from images (not from volumes, not from snapshots, and not from images first converted to volumes). Additionally, this patch removes mentions of OS_TRUSTED_IMAGE_CERTIFICATE_IDS env var from similar option help string in server rebuild command as it is not actually implemented yet. Change-Id: I4e9faea05c499bd91034d1d284c44fdcc8e18db5 --- openstackclient/compute/v2/server.py | 32 +++- .../tests/unit/compute/v2/test_server.py | 150 ++++++++++++++++++ ...option-server-create-a660488407300f22.yaml | 7 + 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-trusted-certs-option-server-create-a660488407300f22.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 4750583812..291397769f 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -1171,6 +1171,19 @@ def get_parser(self, prog_name): action='store_true', help=_('Wait for build to complete'), ) + parser.add_argument( + '--trusted-image-cert', + metavar='', + action='append', + dest='trusted_image_certs', + help=_( + 'Trusted image certificate IDs used to validate certificates ' + 'during the image signature verification process. ' + 'May be specified multiple times to pass multiple trusted ' + 'image certificate IDs. ' + '(supported by --os-compute-api-version 2.63 or above)' + ), + ) return parser def take_action(self, parsed_args): @@ -1640,6 +1653,24 @@ def _match_image(image_api, wanted_properties): boot_kwargs['hostname'] = parsed_args.hostname + # TODO(stephenfin): Handle OS_TRUSTED_IMAGE_CERTIFICATE_IDS + if parsed_args.trusted_image_certs: + if not (image and not parsed_args.boot_from_volume): + msg = _( + '--trusted-image-cert option is only supported for ' + 'servers booted directly from images' + ) + raise exceptions.CommandError(msg) + if compute_client.api_version < api_versions.APIVersion('2.63'): + msg = _( + '--os-compute-api-version 2.63 or greater is required to ' + 'support the --trusted-image-cert option' + ) + raise exceptions.CommandError(msg) + + certs = parsed_args.trusted_image_certs + boot_kwargs['trusted_image_certificates'] = certs + LOG.debug('boot_args: %s', boot_args) LOG.debug('boot_kwargs: %s', boot_kwargs) @@ -3277,7 +3308,6 @@ def get_parser(self, prog_name): help=_( 'Trusted image certificate IDs used to validate certificates ' 'during the image signature verification process. ' - 'Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. ' 'May be specified multiple times to pass multiple trusted ' 'image certificate IDs. ' 'Cannot be specified with the --no-trusted-certs option. ' diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index cab9efd0c0..13431e005d 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -3624,6 +3624,156 @@ def test_server_create_with_hostname_pre_v290(self): exceptions.CommandError, self.cmd.take_action, parsed_args) + def test_server_create_with_trusted_image_cert(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.63') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('trusted_image_certs', ['foo', 'bar']), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + admin_pass=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + trusted_image_certificates=['foo', 'bar'], + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_trusted_image_cert_prev263(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.62') + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('trusted_image_certs', ['foo', 'bar']), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_server_create_with_trusted_image_cert_from_volume(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.63') + arglist = [ + '--volume', 'volume1', + '--flavor', 'flavor1', + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + self.new_server.name, + ] + verifylist = [ + ('volume', 'volume1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('trusted_image_certs', ['foo', 'bar']), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_server_create_with_trusted_image_cert_from_snapshot(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.63') + arglist = [ + '--snapshot', 'snapshot1', + '--flavor', 'flavor1', + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + self.new_server.name, + ] + verifylist = [ + ('snapshot', 'snapshot1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('trusted_image_certs', ['foo', 'bar']), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_server_create_with_trusted_image_cert_boot_from_volume(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.63') + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--boot-from-volume', '1', + '--trusted-image-cert', 'foo', + '--trusted-image-cert', 'bar', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('boot_from_volume', 1), + ('config_drive', False), + ('trusted_image_certs', ['foo', 'bar']), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + class TestServerDelete(TestServer): diff --git a/releasenotes/notes/add-trusted-certs-option-server-create-a660488407300f22.yaml b/releasenotes/notes/add-trusted-certs-option-server-create-a660488407300f22.yaml new file mode 100644 index 0000000000..8814a63ae6 --- /dev/null +++ b/releasenotes/notes/add-trusted-certs-option-server-create-a660488407300f22.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added ``--trusted-image-cert`` option for server create. It is available + only when directly booting server from image (not from volume, not from + snapshot and not via image converted to volume first). + This option is supported for Compute API version >=2.63 From abed9f20f5d9c1af3345456ec82d726e49db9f68 Mon Sep 17 00:00:00 2001 From: lsmman Date: Wed, 6 Oct 2021 19:21:51 +0900 Subject: [PATCH 0440/1120] Remove non-working code after method return. Delete duplicate return code. While adding return of a new Member type, the existing return code part is not deleted. Note the code in fakes.py in the below commit where these codes were added. - Project: python-openstackclient - The commit: 60e7c51df4cf061ebbb435a959ad63c7d3a296bf Change-Id: Iae44770a784732991962cd38472095f76ab2543f --- openstackclient/tests/unit/image/v2/fakes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 516d563001..0d83f98b95 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -308,8 +308,3 @@ def create_one_image_member(attrs=None): image_member_info.update(attrs) return member.Member(**image_member_info) - - image_member = fakes.FakeModel( - copy.deepcopy(image_member_info)) - - return image_member From 70fed75c852d5a0518adbde14dad41b7579b04a5 Mon Sep 17 00:00:00 2001 From: choidoa-git Date: Wed, 6 Oct 2021 15:32:29 +0000 Subject: [PATCH 0441/1120] Update the Nova CLI decoder document In this patch, Update missing command in Mapping Guide. List of updated commands (Nova CLI / OSC) - server-migration-list / server migration list - server-migration-show / server migration show - live-migration-abort / server migration abort - live-migration-force-complete / server migration force complete - migration-list / server migration list - evacuate / server evacuate - flavor-access-add / flavor set --project - flavor-access-list / flavor show - flavor-access-remove / flavor unset - server-tag-add / server set --tag - server-tag-delete / server unset --tag - server-tag-delete-all / server unset --tag - server-tag-list / server list --tag - server-tag-set / server set --tag - quota-class-show / quota show --class Change-Id: Id1b4980fbc0f6e8e58bfae6f393f9336c6a7e3b1 --- doc/source/cli/data/nova.csv | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 83911f1237..cc42fe5475 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -19,10 +19,10 @@ clear-password,server set --root-password,Clear the admin password for a server console-log,console log show,Get console log output of a server. delete,server delete,Immediately shut down and delete specified server(s). diagnostics,openstack server show --diagnostics,Retrieve server diagnostics. -evacuate,,Evacuate server from failed host. -flavor-access-add,,Add flavor access for the given tenant. -flavor-access-list,,Print access information about the given flavor. -flavor-access-remove,,Remove flavor access for the given tenant. +evacuate,server evacuate,Evacuate server from failed host. +flavor-access-add,flavor set --project,Add flavor access for the given tenant. +flavor-access-list,flavor show,Print access information about the given flavor. +flavor-access-remove,flavor unset,Remove flavor access for the given tenant. flavor-create,flavor create,Create a new flavor. flavor-delete,flavor delete,Delete a specific flavor flavor-key,flavor set / unset,Set or unset extra_spec for a flavor. @@ -59,15 +59,15 @@ keypair-show,keypair show,Show details about the given keypair. limits,limits show,Print rate and absolute limits. list,server list,List active servers. list-secgroup,security group list,List Security Group(s) of a server. -live-migration,,Migrate running server to a new machine. -live-migration-abort,,Abort an on-going live migration. -live-migration-force-comp,,Force on-going live migration to complete. +live-migration,server migration list,Migrate running server to a new machine. +live-migration-abort,server migration abort,Abort an on-going live migration. +live-migration-force-comp,server migration force complete,Force on-going live migration to complete. 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. pause,server pause,Pause a server. -quota-class-show,,List the quotas for a quota class. +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. @@ -89,13 +89,13 @@ server-group-create,server group create,Create a new server group with the speci server-group-delete,server group delete,Delete specific server group(s). server-group-get,server group show,Get a specific server group. server-group-list,server group list,Print a list of all server groups. -server-migration-list,,Get the migrations list of specified server. -server-migration-show,,Get the migration of specified server. -server-tag-add,,Add one or more tags to a server. -server-tag-delete,,Delete one or more tags from a server. -server-tag-delete-all,,Delete all tags from a server. -server-tag-list,,Get list of tags from a server. -server-tag-set,,Set list of tags to a server. +server-migration-list,server migration list,Get the migrations list of specified server. +server-migration-show,server migration show,Get the migration of specified server. +server-tag-add,server set --tag,Add one or more tags to a server. +server-tag-delete,server unset --tag,Delete one or more tags from a server. +server-tag-delete-all,server unset --tag,Delete all tags from a server. +server-tag-list,server list --tag,Get list of tags from a server. +server-tag-set,server set --tag,Set list of tags to a server. server-topology,openstack server show --topology,Retrieve server topology. (Supported by API versions '2.78' - '2.latest') [hint: use '-- os-compute-api-version' flag to show help message for proper version] service-delete,compute service delete,Delete the service. service-disable,compute service set --disable,Disable the service. From e06a4f1c20496c3acc4cdd0027d31d1cfe3485ea Mon Sep 17 00:00:00 2001 From: JIHOJU Date: Tue, 5 Oct 2021 09:13:57 +0900 Subject: [PATCH 0442/1120] Update the Nova CLI docoder document There are several update in CLI decoder document. - Change flavor set/unset to flavor set/unset --property - Update the mapping with flavor-update, interface-attach, and interface-detach Change-Id: I1db50188b3643d3fe28689dc73b3f63806defd29 --- doc/source/cli/data/nova.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index 83911f1237..5141e86ea6 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -25,10 +25,10 @@ flavor-access-list,,Print access information about the given flavor. flavor-access-remove,,Remove flavor access for the given tenant. flavor-create,flavor create,Create a new flavor. flavor-delete,flavor delete,Delete a specific flavor -flavor-key,flavor set / unset,Set or unset extra_spec for a flavor. +flavor-key,flavor set / unset --property,Set or unset extra_spec for a flavor. flavor-list,flavor list,Print a list of available 'flavors' flavor-show,flavor show,Show details about the given flavor. -flavor-update,,Update the description of an existing flavor. (Supported by API versions '2.55' - '2.latest') [hint: use '--os-compute-api- version' flag to show help message for proper version] +flavor-update,flavor set --description,Update the description of an existing flavor. (Supported by API versions '2.55' - '2.latest') [hint: use '--os-compute-api-version' flag to show help message for proper version] force-delete,server delete,Force delete a server. get-mks-console,console url show --mks,Get an MKS console to a server. (Supported by API versions '2.8' - '2.latest') [hint: use ' --os-compute-api-version' flag to show help message for proper version] get-password,WONTFIX,Get the admin password for a server. This operation calls the metadata service to query metadata information and does not read password information from the server itself. @@ -49,8 +49,8 @@ image-create,server image create,Create a new image by taking a snapshot of a ru instance-action,,Show an action. instance-action-list,,List actions on a server. instance-usage-audit-log,,List/Get server usage audits. -interface-attach,,Attach a network interface to a server. -interface-detach,,Detach a network interface from a server. +interface-attach,server add port / server add floating ip / server add fixed ip,Attach a network interface to a server. +interface-detach,server remove port,Detach a network interface from a server. interface-list,port list --server,List interfaces attached to a server. keypair-add,keypair create,Create a new key pair for use with servers. keypair-delete,keypair delete,Delete keypair given by its name. From 53debe7fe1978f661768a27430f646a288948ecc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 13 Oct 2021 10:18:53 +0100 Subject: [PATCH 0443/1120] compute: Fix filtering servers by tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nova API expects the 'tags' and 'not-tags' filters of the 'GET /servers' (list servers) API to be a CSV string [1]: tags (Optional) A list of tags to filter the server list by. Servers that match all tags in this list will be returned. Boolean expression in this case is 't1 AND t2'. Tags in query must be separated by comma. New in version 2.26 not-tags (Optional) A list of tags to filter the server list by. Servers that don’t match all tags in this list will be returned. Boolean expression in this case is 'NOT (t1 AND t2)'. Tags in query must be separated by comma. New in version 2.26 We were instead providing a Python list, which was simply being URL encoded. Correct this. [1] https://docs.openstack.org/api-ref/compute/?expanded=list-servers-detail#list-servers Change-Id: Ie0251a0dccdf3385089e5bbaedf646a5e928cc48 Signed-off-by: Stephen Finucane Closes-Bug: #1946816 --- openstackclient/compute/v2/server.py | 4 ++-- openstackclient/tests/unit/compute/v2/test_server.py | 4 ++-- releasenotes/notes/bug-1946816-7665858605453578.yaml | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/bug-1946816-7665858605453578.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 08345243e9..c11f4b5781 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2257,7 +2257,7 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - search_opts['tags'] = parsed_args.tags + search_opts['tags'] = ','.join(parsed_args.tags) if parsed_args.not_tags: if compute_client.api_version < api_versions.APIVersion('2.26'): @@ -2267,7 +2267,7 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - search_opts['not-tags'] = parsed_args.not_tags + search_opts['not-tags'] = ','.join(parsed_args.not_tags) if parsed_args.locked: if compute_client.api_version < api_versions.APIVersion('2.73'): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 13431e005d..3d8c17fdeb 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4489,7 +4489,7 @@ def test_server_list_with_tag(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.search_opts['tags'] = ['tag1', 'tag2'] + self.search_opts['tags'] = 'tag1,tag2' self.servers_mock.list.assert_called_with(**self.kwargs) @@ -4532,7 +4532,7 @@ def test_server_list_with_not_tag(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.search_opts['not-tags'] = ['tag1', 'tag2'] + self.search_opts['not-tags'] = 'tag1,tag2' self.servers_mock.list.assert_called_with(**self.kwargs) diff --git a/releasenotes/notes/bug-1946816-7665858605453578.yaml b/releasenotes/notes/bug-1946816-7665858605453578.yaml new file mode 100644 index 0000000000..f9e8406a9e --- /dev/null +++ b/releasenotes/notes/bug-1946816-7665858605453578.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Filtering servers by tags (``server list --tag``, + ``server list --not-tag``) now works correctly. + [Bug `1946816 `_] From a797c9d2a351fd87d2f8075bbf7180fc6b202d92 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 13 Oct 2021 12:14:30 +0100 Subject: [PATCH 0444/1120] tox: Ignore virtualenvs for pep8 environment Change-Id: I473d1b6c1287325566a5f5f5aadaea802c6af6f4 Signed-off-by: Stephen Finucane --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8a386267da..6eefc26ece 100644 --- a/tox.ini +++ b/tox.ini @@ -133,7 +133,7 @@ commands = show-source = True # H203: Use assertIs(Not)None to check for None enable-extensions = H203 -exclude = .git,.tox,dist,doc,*lib/python*,*egg,build,tools +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 import-order-style = pep8 From 30612bf62295720a21d39c4f589cfcefb7a86a2f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 8 Oct 2021 18:06:11 +0100 Subject: [PATCH 0445/1120] Remove 'get_osc_show_columns_for_sdk_resource' duplicates There were a number of 'get_osc_show_columns_for_sdk_resource' defined in-tree. However, osc-lib has provided this method for some time (since 2.2.0, June 2020 [1] - our minimum version is currently 2.3.0) so there's no need to provide our own copies. Remove them. [1] https://github.com/openstack/osc-lib/commit/29a0c5a5 Change-Id: I25695f4f9a379dd691b7eaa1e3247164668ae77e Signed-off-by: Stephen Finucane --- openstackclient/common/sdk_utils.py | 58 ----------------- openstackclient/image/v1/image.py | 13 ++-- openstackclient/image/v2/image.py | 7 +-- openstackclient/network/sdk_utils.py | 63 ------------------- openstackclient/network/v2/address_group.py | 4 +- openstackclient/network/v2/address_scope.py | 4 +- openstackclient/network/v2/floating_ip.py | 3 +- .../network/v2/floating_ip_port_forwarding.py | 5 +- openstackclient/network/v2/ip_availability.py | 3 +- .../network/v2/l3_conntrack_helper.py | 4 +- openstackclient/network/v2/network.py | 5 +- openstackclient/network/v2/network_agent.py | 4 +- .../v2/network_auto_allocated_topology.py | 3 +- openstackclient/network/v2/network_flavor.py | 4 +- .../network/v2/network_flavor_profile.py | 4 +- openstackclient/network/v2/network_meter.py | 3 +- .../network/v2/network_meter_rule.py | 3 +- .../network/v2/network_qos_policy.py | 4 +- .../network/v2/network_qos_rule.py | 4 +- .../network/v2/network_qos_rule_type.py | 3 +- openstackclient/network/v2/network_rbac.py | 4 +- openstackclient/network/v2/network_segment.py | 4 +- .../network/v2/network_segment_range.py | 3 +- openstackclient/network/v2/port.py | 4 +- openstackclient/network/v2/router.py | 4 +- openstackclient/network/v2/security_group.py | 3 +- .../network/v2/security_group_rule.py | 4 +- openstackclient/network/v2/subnet.py | 4 +- openstackclient/network/v2/subnet_pool.py | 3 +- .../tests/unit/network/test_sdk_utils.py | 59 ----------------- 30 files changed, 37 insertions(+), 256 deletions(-) delete mode 100644 openstackclient/common/sdk_utils.py delete mode 100644 openstackclient/network/sdk_utils.py delete mode 100644 openstackclient/tests/unit/network/test_sdk_utils.py diff --git a/openstackclient/common/sdk_utils.py b/openstackclient/common/sdk_utils.py deleted file mode 100644 index af9c74f944..0000000000 --- a/openstackclient/common/sdk_utils.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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. - - -def get_osc_show_columns_for_sdk_resource( - sdk_resource, - osc_column_map, - invisible_columns=None -): - """Get and filter the display and attribute columns for an SDK resource. - - Common utility function for preparing the output of an OSC show command. - Some of the columns may need to get renamed, others made invisible. - - :param sdk_resource: An SDK resource - :param osc_column_map: A hash of mappings for display column names - :param invisible_columns: A list of invisible column names - - :returns: Two tuples containing the names of the display and attribute - columns - """ - - if getattr(sdk_resource, 'allow_get', None) is not None: - resource_dict = sdk_resource.to_dict( - body=True, headers=False, ignore_none=False) - else: - resource_dict = sdk_resource - - # Build the OSC column names to display for the SDK resource. - attr_map = {} - display_columns = list(resource_dict.keys()) - invisible_columns = [] if invisible_columns is None else invisible_columns - for col_name in invisible_columns: - if col_name in display_columns: - display_columns.remove(col_name) - for sdk_attr, osc_attr in osc_column_map.items(): - if sdk_attr in display_columns: - attr_map[osc_attr] = sdk_attr - display_columns.remove(sdk_attr) - if osc_attr not in display_columns: - display_columns.append(osc_attr) - sorted_display_columns = sorted(display_columns) - - # Build the SDK attribute names for the OSC column names. - attr_columns = [] - for column in sorted_display_columns: - new_column = attr_map[column] if column in attr_map else column - attr_columns.append(new_column) - return tuple(sorted_display_columns), tuple(attr_columns) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 64aa3fcdab..43ccf5d212 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -28,7 +28,6 @@ from osc_lib.command import command from osc_lib import utils -from openstackclient.common import sdk_utils from openstackclient.i18n import _ if os.name == "nt": @@ -48,15 +47,17 @@ def _get_columns(item): - # Trick sdk_utils to return URI attribute column_map = { 'is_protected': 'protected', 'owner_id': 'owner' } - hidden_columns = ['location', 'checksum', - 'copy_from', 'created_at', 'status', 'updated_at'] - return sdk_utils.get_osc_show_columns_for_sdk_resource( - item.to_dict(), column_map, hidden_columns) + hidden_columns = [ + 'location', 'checksum', 'copy_from', 'created_at', 'status', + 'updated_at', + ] + return utils.get_osc_show_columns_for_sdk_resource( + item.to_dict(), column_map, hidden_columns, + ) _formatters = { diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index c1f46d2d99..becb54f456 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -31,7 +31,6 @@ from osc_lib import utils from openstackclient.common import progressbar -from openstackclient.common import sdk_utils from openstackclient.i18n import _ from openstackclient.identity import common @@ -99,13 +98,13 @@ def _format_image(image, human_readable=False): def _get_member_columns(item): - # Trick sdk_utils to return URI attribute column_map = { 'image_id': 'image_id' } hidden_columns = ['id', 'location', 'name'] - return sdk_utils.get_osc_show_columns_for_sdk_resource( - item.to_dict(), column_map, hidden_columns) + return utils.get_osc_show_columns_for_sdk_resource( + item.to_dict(), column_map, hidden_columns, + ) def get_data_file(args): diff --git a/openstackclient/network/sdk_utils.py b/openstackclient/network/sdk_utils.py deleted file mode 100644 index cff3071354..0000000000 --- a/openstackclient/network/sdk_utils.py +++ /dev/null @@ -1,63 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import munch - - -def get_osc_show_columns_for_sdk_resource( - sdk_resource, - osc_column_map, - invisible_columns=None -): - """Get and filter the display and attribute columns for an SDK resource. - - Common utility function for preparing the output of an OSC show command. - Some of the columns may need to get renamed, others made invisible. - - :param sdk_resource: An SDK resource - :param osc_column_map: A hash of mappings for display column names - :param invisible_columns: A list of invisible column names - - :returns: Two tuples containing the names of the display and attribute - columns - """ - - if getattr(sdk_resource, 'allow_get', None) is not None: - resource_dict = sdk_resource.to_dict( - body=True, headers=False, ignore_none=False) - else: - resource_dict = sdk_resource - - # Build the OSC column names to display for the SDK resource. - attr_map = {} - display_columns = list(resource_dict.keys()) - for col_name in display_columns: - if isinstance(resource_dict[col_name], munch.Munch): - display_columns.remove(col_name) - invisible_columns = [] if invisible_columns is None else invisible_columns - for col_name in invisible_columns: - if col_name in display_columns: - display_columns.remove(col_name) - for sdk_attr, osc_attr in osc_column_map.items(): - if sdk_attr in display_columns: - attr_map[osc_attr] = sdk_attr - display_columns.remove(sdk_attr) - if osc_attr not in display_columns: - display_columns.append(osc_attr) - sorted_display_columns = sorted(display_columns) - - # Build the SDK attribute names for the OSC column names. - attr_columns = [] - for column in sorted_display_columns: - new_column = attr_map[column] if column in attr_map else column - attr_columns.append(new_column) - return tuple(sorted_display_columns), tuple(attr_columns) diff --git a/openstackclient/network/v2/address_group.py b/openstackclient/network/v2/address_group.py index fc83470053..9017047fac 100644 --- a/openstackclient/network/v2/address_group.py +++ b/openstackclient/network/v2/address_group.py @@ -23,8 +23,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -33,7 +31,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _format_addresses(addresses): diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index cd27678ee9..5748793a57 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -22,8 +22,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -33,7 +31,7 @@ def _get_columns(item): 'is_shared': 'shared', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index 25b2a1baf6..0951565cb6 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -19,7 +19,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils _formatters = { @@ -31,7 +30,7 @@ def _get_network_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_columns(item): diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py index 71b0b7da63..f137174cf1 100644 --- a/openstackclient/network/v2/floating_ip_port_forwarding.py +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -12,6 +12,7 @@ # """Floating IP Port Forwarding action implementations""" + import logging from osc_lib.command import command @@ -20,8 +21,6 @@ from openstackclient.i18n import _ from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -30,7 +29,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) class CreateFloatingIPPortForwarding(command.ShowOne, diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index ddc88e557e..6a3c67e21b 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -19,7 +19,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common -from openstackclient.network import sdk_utils _formatters = { 'subnet_ip_availability': format_columns.ListDictColumn, @@ -30,7 +29,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) # TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once diff --git a/openstackclient/network/v2/l3_conntrack_helper.py b/openstackclient/network/v2/l3_conntrack_helper.py index 94788823ab..9fc33d8f15 100644 --- a/openstackclient/network/v2/l3_conntrack_helper.py +++ b/openstackclient/network/v2/l3_conntrack_helper.py @@ -20,15 +20,13 @@ from osc_lib import utils from openstackclient.i18n import _ -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) def _get_columns(item): column_map = {} - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client, parsed_args): diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index b8eb9f014b..191e4aa8e2 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -21,7 +21,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils class AdminStateColumn(cliff_columns.FormattableColumn): @@ -62,14 +61,14 @@ def _get_columns_network(item): 'tenant_id': 'project_id', 'tags': 'tags', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_columns_compute(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs_network(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index 1678485434..c995e36cb8 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -22,8 +22,6 @@ from osc_lib import utils from openstackclient.i18n import _ -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -52,7 +50,7 @@ def _get_network_columns(item): 'is_admin_state_up': 'admin_state_up', 'is_alive': 'alive', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) class AddNetworkToAgent(command.Command): diff --git a/openstackclient/network/v2/network_auto_allocated_topology.py b/openstackclient/network/v2/network_auto_allocated_topology.py index 36f392006e..7b7df4d75c 100644 --- a/openstackclient/network/v2/network_auto_allocated_topology.py +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -20,7 +20,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common -from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -29,7 +28,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _format_check_resource_columns(): diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py index 9e758ae29c..6e3a5a0432 100644 --- a/openstackclient/network/v2/network_flavor.py +++ b/openstackclient/network/v2/network_flavor.py @@ -22,8 +22,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -34,7 +32,7 @@ def _get_columns(item): 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py index 0212e0d9ba..df7cfb7430 100644 --- a/openstackclient/network/v2/network_flavor_profile.py +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -20,8 +20,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -32,7 +30,7 @@ def _get_columns(item): 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py index f8f188a806..8b63de2c7a 100644 --- a/openstackclient/network/v2/network_meter.py +++ b/openstackclient/network/v2/network_meter.py @@ -22,7 +22,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -32,7 +31,7 @@ def _get_columns(item): 'is_shared': 'shared', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index 06362fa14c..4117d04342 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -22,7 +22,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -31,7 +30,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index 7300a5c0ac..8d4312484b 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -22,8 +22,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -33,7 +31,7 @@ def _get_columns(item): 'is_shared': 'shared', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index f30a5aeb1f..4bf72d269a 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -21,8 +21,6 @@ from openstackclient.i18n import _ from openstackclient.network import common -from openstackclient.network import sdk_utils - RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' RULE_TYPE_DSCP_MARKING = 'dscp-marking' @@ -51,7 +49,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _check_type_parameters(attrs, type, is_create): diff --git a/openstackclient/network/v2/network_qos_rule_type.py b/openstackclient/network/v2/network_qos_rule_type.py index 7b92c8ad4b..036b682fae 100644 --- a/openstackclient/network/v2/network_qos_rule_type.py +++ b/openstackclient/network/v2/network_qos_rule_type.py @@ -17,7 +17,6 @@ from osc_lib import utils from openstackclient.i18n import _ -from openstackclient.network import sdk_utils def _get_columns(item): @@ -26,7 +25,7 @@ def _get_columns(item): "drivers": "drivers", } invisible_columns = ["id", "name"] - return sdk_utils.get_osc_show_columns_for_sdk_resource( + return utils.get_osc_show_columns_for_sdk_resource( item, column_map, invisible_columns) diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 692a43857d..edca872cf2 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -22,8 +22,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -33,7 +31,7 @@ def _get_columns(item): 'target_tenant': 'target_project_id', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index 14a8edabe5..e18ac47529 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -21,14 +21,12 @@ from openstackclient.i18n import _ from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) def _get_columns(item): - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) + return utils.get_osc_show_columns_for_sdk_resource(item, {}) class CreateNetworkSegment(command.ShowOne, diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index ee414407ee..e105111dd0 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -26,14 +26,13 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) def _get_columns(item): - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) + return utils.get_osc_show_columns_for_sdk_resource(item, {}) def _get_ranges(item): diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index ecb2382a85..132c384a0e 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -29,8 +29,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -67,7 +65,7 @@ def _get_columns(item): 'is_port_security_enabled': 'port_security_enabled', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) class JSONKeyValueAction(argparse.Action): diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index d15300a0b7..dde4eda99f 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -28,8 +28,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -83,7 +81,7 @@ def _get_columns(item): if item.is_distributed is None: invisible_columns.append('is_distributed') column_map.pop('is_distributed') - return sdk_utils.get_osc_show_columns_for_sdk_resource( + return utils.get_osc_show_columns_for_sdk_resource( item, column_map, invisible_columns) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 49dc14e403..37d2dc5be0 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -23,7 +23,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils from openstackclient.network import utils as network_utils @@ -90,7 +89,7 @@ def _get_columns(item): 'security_group_rules': 'rules', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) # TODO(abhiraut): Use the SDK resource mapped attribute names once the diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index e273ded3d6..252dcb05a8 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -23,10 +23,8 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils from openstackclient.network import utils as network_utils - LOG = logging.getLogger(__name__) @@ -76,7 +74,7 @@ def _get_columns(item): column_map = { 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _convert_to_lowercase(string): diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 09fd7c7c39..c07fab4170 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -27,8 +27,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils - LOG = logging.getLogger(__name__) @@ -143,7 +141,7 @@ def _get_columns(item): } # Do not show this column when displaying a subnet invisible_columns = ['use_default_subnet_pool', 'prefix_length'] - return sdk_utils.get_osc_show_columns_for_sdk_resource( + return utils.get_osc_show_columns_for_sdk_resource( item, column_map, invisible_columns=invisible_columns diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index bdf7aba805..6b88888c0e 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -25,7 +25,6 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -39,7 +38,7 @@ def _get_columns(item): 'minimum_prefix_length': 'min_prefixlen', 'tenant_id': 'project_id', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + return utils.get_osc_show_columns_for_sdk_resource(item, column_map) _formatters = { diff --git a/openstackclient/tests/unit/network/test_sdk_utils.py b/openstackclient/tests/unit/network/test_sdk_utils.py deleted file mode 100644 index d1efa7e40a..0000000000 --- a/openstackclient/tests/unit/network/test_sdk_utils.py +++ /dev/null @@ -1,59 +0,0 @@ -# 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.network import sdk_utils -from openstackclient.tests.unit import utils as tests_utils - - -class TestSDKUtils(tests_utils.TestCase): - - def setUp(self): - super(TestSDKUtils, self).setUp() - - def _test_get_osc_show_columns_for_sdk_resource( - self, sdk_resource, column_map, - expected_display_columns, expected_attr_columns): - display_columns, attr_columns = \ - sdk_utils.get_osc_show_columns_for_sdk_resource( - sdk_resource, column_map) - self.assertEqual(expected_display_columns, display_columns) - self.assertEqual(expected_attr_columns, attr_columns) - - def test_get_osc_show_columns_for_sdk_resource_empty(self): - self._test_get_osc_show_columns_for_sdk_resource( - {}, {}, tuple(), tuple()) - - def test_get_osc_show_columns_for_sdk_resource_empty_map(self): - self._test_get_osc_show_columns_for_sdk_resource( - {'foo': 'foo1'}, {}, - ('foo',), ('foo',)) - - def test_get_osc_show_columns_for_sdk_resource_empty_data(self): - self._test_get_osc_show_columns_for_sdk_resource( - {}, {'foo': 'foo_map'}, - ('foo_map',), ('foo_map',)) - - def test_get_osc_show_columns_for_sdk_resource_map(self): - self._test_get_osc_show_columns_for_sdk_resource( - {'foo': 'foo1'}, {'foo': 'foo_map'}, - ('foo_map',), ('foo',)) - - def test_get_osc_show_columns_for_sdk_resource_map_dup(self): - self._test_get_osc_show_columns_for_sdk_resource( - {'foo': 'foo1', 'foo_map': 'foo1'}, {'foo': 'foo_map'}, - ('foo_map',), ('foo',)) - - def test_get_osc_show_columns_for_sdk_resource_map_full(self): - self._test_get_osc_show_columns_for_sdk_resource( - {'foo': 'foo1', 'bar': 'bar1'}, - {'foo': 'foo_map', 'new': 'bar'}, - ('bar', 'foo_map'), ('bar', 'foo')) From 728401bbd76afc4d31b4f22e44bf98d1de40ef46 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 8 Oct 2021 18:20:31 +0100 Subject: [PATCH 0446/1120] Remove remnants of 'six' Just one entry left. Remove it. Change-Id: Ia12173ecb7f3fed4a1195a46ebf9b096d917b3b6 Signed-off-by: Stephen Finucane --- lower-constraints.txt | 1 - openstackclient/tests/unit/common/test_progressbar.py | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lower-constraints.txt b/lower-constraints.txt index 861749b738..b98b8432a9 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -81,7 +81,6 @@ requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 simplejson==3.5.1 -six==1.15.0 statsd==3.2.1 stestr==1.0.0 stevedore==2.0.1 diff --git a/openstackclient/tests/unit/common/test_progressbar.py b/openstackclient/tests/unit/common/test_progressbar.py index 7bc0b6baf4..a624fc438a 100644 --- a/openstackclient/tests/unit/common/test_progressbar.py +++ b/openstackclient/tests/unit/common/test_progressbar.py @@ -11,10 +11,9 @@ # under the License. # +import io import sys -import six - from openstackclient.common import progressbar from openstackclient.tests.unit import utils @@ -23,7 +22,7 @@ class TestProgressBarWrapper(utils.TestCase): def test_iter_file_display_progress_bar(self): size = 98304 - file_obj = six.StringIO('X' * size) + file_obj = io.StringIO('X' * size) saved_stdout = sys.stdout try: sys.stdout = output = FakeTTYStdout() @@ -41,7 +40,7 @@ def test_iter_file_display_progress_bar(self): def test_iter_file_no_tty(self): size = 98304 - file_obj = six.StringIO('X' * size) + file_obj = io.StringIO('X' * size) saved_stdout = sys.stdout try: sys.stdout = output = FakeNoTTYStdout() @@ -56,7 +55,7 @@ def test_iter_file_no_tty(self): sys.stdout = saved_stdout -class FakeTTYStdout(six.StringIO): +class FakeTTYStdout(io.StringIO): """A Fake stdout that try to emulate a TTY device as much as possible.""" def isatty(self): @@ -67,7 +66,7 @@ def write(self, data): if data.startswith('\r'): self.seek(0) data = data[1:] - return six.StringIO.write(self, data) + return io.StringIO.write(self, data) class FakeNoTTYStdout(FakeTTYStdout): From 43639e1118a757fd1a00d9ea999db43133c40763 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 26 Oct 2021 15:53:35 +0200 Subject: [PATCH 0447/1120] Fix typos Change-Id: Idd502c8df21da79ff3b9339870f38378f5337879 --- openstackclient/common/progressbar.py | 2 +- openstackclient/common/quota.py | 4 ++-- openstackclient/compute/v2/server.py | 4 ++-- openstackclient/identity/v3/endpoint_group.py | 2 +- openstackclient/tests/functional/base.py | 4 ++-- .../tests/functional/volume/v1/test_volume_type.py | 4 ++-- .../tests/functional/volume/v2/test_volume_type.py | 4 ++-- .../tests/functional/volume/v3/test_volume_type.py | 4 ++-- openstackclient/tests/unit/compute/v2/fakes.py | 4 ++-- openstackclient/volume/v1/volume_type.py | 2 +- openstackclient/volume/v2/volume_snapshot.py | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/openstackclient/common/progressbar.py b/openstackclient/common/progressbar.py index ef767a9cf8..7678aceba0 100644 --- a/openstackclient/common/progressbar.py +++ b/openstackclient/common/progressbar.py @@ -17,7 +17,7 @@ class _ProgressBarBase(object): - """A progress bar provider for a wrapped obect. + """A progress bar provider for a wrapped object. Base abstract class used by specific class wrapper to show a progress bar when the wrapped object are consumed. diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 643cb4e474..00bbc51459 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -205,7 +205,7 @@ 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 informations for requested project") + "list detailed quota information for requested project") def _get_detailed_quotas(self, parsed_args): columns = ( @@ -230,7 +230,7 @@ def _get_detailed_quotas(self, parsed_args): result = [] for resource, values in quotas.items(): # NOTE(slaweq): there is no detailed quotas info for some resources - # and it should't be displayed here + # and it shouldn't be displayed here if type(values) is dict: result.append({ 'resource': resource, diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c11f4b5781..b0de65a7c3 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2466,7 +2466,7 @@ def take_action(self, parsed_args): # and 'image' missing in the server response during # infrastructure failure situations. # For those servers with partial constructs we just skip the - # processing of the image and flavor informations. + # processing of the image and flavor information. if not hasattr(s, 'image') or not hasattr(s, 'flavor'): continue if 'id' in s.image: @@ -4292,7 +4292,7 @@ def _show_progress(progress): server_obj.shelve() - # if we don't hav to wait, either because it was requested explicitly + # if we don't have to wait, either because it was requested explicitly # or is required implicitly, then our job is done if not parsed_args.wait and not parsed_args.offload: return diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py index cbe27edb4b..9bb026a9b8 100644 --- a/openstackclient/identity/v3/endpoint_group.py +++ b/openstackclient/identity/v3/endpoint_group.py @@ -268,7 +268,7 @@ def get_parser(self, prog_name): parser.add_argument( '--name', metavar='', - help=_('New enpoint group name'), + help=_('New endpoint group name'), ) parser.add_argument( '--filters', diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 0ed7dff8c4..3a4c13944e 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -42,7 +42,7 @@ class TestCase(testtools.TestCase): def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): """Executes openstackclient command for the given action - NOTE(dtroyer): There is a subtle distinction between pasing + 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' @@ -61,7 +61,7 @@ def openstack(cls, cmd, cloud=ADMIN_CLOUD, fail_ok=False): fail_ok=fail_ok ) else: - # Execure command with an explicit cloud specified + # Execute command with an explicit cloud specified return execute( 'openstack --os-cloud=' + cloud + ' ' + cmd, fail_ok=fail_ok diff --git a/openstackclient/tests/functional/volume/v1/test_volume_type.py b/openstackclient/tests/functional/volume/v1/test_volume_type.py index fb8dabdb04..7434b5b36b 100644 --- a/openstackclient/tests/functional/volume/v1/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v1/test_volume_type.py @@ -118,10 +118,10 @@ def test_multi_delete(self): raw_output = self.openstack(cmd) self.assertOutput('', raw_output) - # NOTE: Add some basic funtional tests with the old format to + # NOTE: Add some basic functional tests with the old format to # make sure the command works properly, need to change # these to new test format when beef up all tests for - # volume tye commands. + # volume type commands. def test_encryption_type(self): encryption_type = uuid.uuid4().hex # test create new encryption type diff --git a/openstackclient/tests/functional/volume/v2/test_volume_type.py b/openstackclient/tests/functional/volume/v2/test_volume_type.py index 3f1a6ea8a5..861c393d80 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_type.py @@ -139,10 +139,10 @@ def test_multi_delete(self): raw_output = self.openstack(cmd) self.assertOutput('', raw_output) - # NOTE: Add some basic funtional tests with the old format to + # NOTE: Add some basic functional tests with the old format to # make sure the command works properly, need to change # these to new test format when beef up all tests for - # volume tye commands. + # volume type commands. def test_encryption_type(self): name = uuid.uuid4().hex encryption_type = uuid.uuid4().hex diff --git a/openstackclient/tests/functional/volume/v3/test_volume_type.py b/openstackclient/tests/functional/volume/v3/test_volume_type.py index 79d4096998..165c625c20 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_type.py @@ -139,10 +139,10 @@ def test_multi_delete(self): raw_output = self.openstack(cmd) self.assertOutput('', raw_output) - # NOTE: Add some basic funtional tests with the old format to + # NOTE: Add some basic functional tests with the old format to # make sure the command works properly, need to change # these to new test format when beef up all tests for - # volume tye commands. + # volume type commands. def test_encryption_type(self): name = uuid.uuid4().hex encryption_type = uuid.uuid4().hex diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 3142a24489..fd0a570969 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -1285,7 +1285,7 @@ def create_one_server_group(attrs=None): class FakeServerGroupV264(object): - """Fake one server group fo API >= 2.64""" + """Fake one server group for API >= 2.64""" @staticmethod def create_one_server_group(attrs=None): @@ -1400,7 +1400,7 @@ def create_one_comp_quota(attrs=None): @staticmethod def create_one_default_comp_quota(attrs=None): - """Crate one quota""" + """Create one quota""" attrs = attrs or {} diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index 4f015d13f6..c584943e10 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -411,7 +411,7 @@ def get_parser(self, prog_name): "--encryption-type", action="store_true", help=_("Remove the encryption type for this volume type " - "(admin oly)"), + "(admin only)"), ) return parser diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index 656f59d4ac..53d8d27fed 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -98,7 +98,7 @@ def get_parser(self, prog_name): "--remote-source", metavar="", action=parseractions.KeyValueAction, - help=_("The attribute(s) of the exsiting remote volume snapshot " + help=_("The attribute(s) of the existing remote volume snapshot " "(admin required) (repeat option to specify multiple " "attributes) e.g.: '--remote-source source-name=test_name " "--remote-source source-id=test_id'"), From 57aad01886fe9d98210496a92d517aa067c049a1 Mon Sep 17 00:00:00 2001 From: Diwei Zhu Date: Tue, 26 Oct 2021 14:47:46 +0000 Subject: [PATCH 0448/1120] Switch server backup to sdk. Switch this command from novaclient to SDK. As this is the first command related to server that we are migrating, we need to extend our test fakes to support fake Server resources. The extended fakes will replace the old ones once all commands related to server are switched. Change-Id: If476fb1614a64320ed071bbda35e941bf3290a2e --- openstackclient/compute/v2/server_backup.py | 9 +- .../tests/unit/compute/v2/fakes.py | 150 ++++++++++++------ .../unit/compute/v2/test_server_backup.py | 23 ++- ...server-backup-to-sdk-0f170baf38e98b40.yaml | 4 + 4 files changed, 119 insertions(+), 67 deletions(-) create mode 100644 releasenotes/notes/migrate-server-backup-to-sdk-0f170baf38e98b40.yaml diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index b1b821b24e..53891991b4 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -72,12 +72,9 @@ def _show_progress(progress): self.app.stderr.write('\rProgress: %s' % progress) self.app.stderr.flush() - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute - server = utils.find_resource( - compute_client.servers, - parsed_args.server, - ) + server = compute_client.find_server(parsed_args.server) # Set sane defaults as this API wants all mouths to be fed if parsed_args.name is None: @@ -93,7 +90,7 @@ def _show_progress(progress): else: backup_rotation = parsed_args.rotate - compute_client.servers.backup( + compute_client.backup_server( server.id, backup_name, backup_type, diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 3142a24489..23468ebc4d 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 server from openstackclient.api import compute_v2 from openstackclient.tests.unit import fakes @@ -73,7 +74,7 @@ class FakeAggregate(object): def create_one_aggregate(attrs=None): """Create a fake aggregate. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id and other attributes @@ -104,7 +105,7 @@ def create_one_aggregate(attrs=None): def create_aggregates(attrs=None, count=2): """Create multiple fake aggregates. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of aggregates to fake @@ -255,7 +256,7 @@ class FakeAgent(object): def create_one_agent(attrs=None): """Create a fake agent. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with agent_id, os, and so on @@ -285,7 +286,7 @@ def create_one_agent(attrs=None): def create_agents(attrs=None, count=2): """Create multiple fake agents. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of agents to fake @@ -306,7 +307,7 @@ class FakeExtension(object): def create_one_extension(attrs=None): """Create a fake extension. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with name, namespace, etc. @@ -342,7 +343,7 @@ class FakeHypervisor(object): def create_one_hypervisor(attrs=None): """Create a fake hypervisor. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, hypervisor_hostname, and so on @@ -390,7 +391,7 @@ def create_one_hypervisor(attrs=None): def create_hypervisors(attrs=None, count=2): """Create multiple fake hypervisors. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of hypervisors to fake @@ -411,7 +412,7 @@ class FakeHypervisorStats(object): def create_one_hypervisor_stats(attrs=None): """Create a fake hypervisor stats. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with count, current_workload, and so on @@ -450,7 +451,7 @@ def create_one_hypervisor_stats(attrs=None): def create_hypervisors_stats(attrs=None, count=2): """Create multiple fake hypervisors stats. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of hypervisors to fake @@ -472,7 +473,7 @@ class FakeSecurityGroup(object): def create_one_security_group(attrs=None): """Create a fake security group. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, name, etc. @@ -496,7 +497,7 @@ def create_one_security_group(attrs=None): def create_security_groups(attrs=None, count=2): """Create multiple fake security groups. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of security groups to fake @@ -537,7 +538,7 @@ class FakeSecurityGroupRule(object): def create_one_security_group_rule(attrs=None): """Create a fake security group rule. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, etc. @@ -564,7 +565,7 @@ def create_one_security_group_rule(attrs=None): def create_security_group_rules(attrs=None, count=2): """Create multiple fake security group rules. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of security group rules to fake @@ -586,9 +587,9 @@ class FakeServer(object): def create_one_server(attrs=None, methods=None): """Create a fake server. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :return: A FakeResource object, with id, name, metadata, and so on @@ -622,9 +623,9 @@ def create_one_server(attrs=None, methods=None): def create_servers(attrs=None, methods=None, count=2): """Create multiple fake servers. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :param int count: The number of servers to fake @@ -637,6 +638,59 @@ def create_servers(attrs=None, methods=None, count=2): return servers + @staticmethod + def create_one_sdk_server(attrs=None, methods=None): + """Create a fake server for testing migration to sdk + + :param dict attrs: + A dictionary with all attributes + :param dict methods: + A dictionary with all methods + :return: + A openstack.compute.v2.server.Server object, + with id, name, metadata, and so on + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + server_info = { + 'id': 'server-id-' + uuid.uuid4().hex, + 'name': 'server-name-' + uuid.uuid4().hex, + 'metadata': {}, + 'image': { + 'id': 'image-id-' + uuid.uuid4().hex, + }, + 'flavor': { + 'id': 'flavor-id-' + uuid.uuid4().hex, + }, + 'OS-EXT-STS:power_state': 1, + } + + # Overwrite default attributes. + server_info.update(attrs) + return server.Server(**server_info) + + @staticmethod + def create_sdk_servers(attrs=None, methods=None, count=2): + """Create multiple fake servers for testing migration to sdk + + :param dict attrs: + A dictionary with all attributes + :param dict methods: + A dictionary with all methods + :param int count: + The number of servers to fake + :return: + A list of openstack.compute.v2.server.Server objects + faking the servers + """ + servers = [] + for i in range(0, count): + servers.append(FakeServer.create_one_sdk_server(attrs, methods)) + + return servers + @staticmethod def get_servers(servers=None, count=2): """Get an iterable MagicMock object with a list of faked servers. @@ -705,7 +759,7 @@ class FakeService(object): def create_one_service(attrs=None): """Create a fake service. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, host, binary, and so on @@ -738,7 +792,7 @@ def create_one_service(attrs=None): def create_services(attrs=None, count=2): """Create multiple fake services. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of services to fake @@ -759,7 +813,7 @@ class FakeFlavor(object): def create_one_flavor(attrs=None): """Create a fake flavor. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, name, ram, vcpus, and so on @@ -793,7 +847,7 @@ def create_one_flavor(attrs=None): def create_flavors(attrs=None, count=2): """Create multiple fake flavors. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of flavors to fake @@ -833,7 +887,7 @@ class FakeFlavorAccess(object): def create_one_flavor_access(attrs=None): """Create a fake flavor access. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with flavor_id, tenat_id @@ -862,7 +916,7 @@ class FakeKeypair(object): def create_one_keypair(attrs=None, no_pri=False): """Create a fake keypair - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, name, fingerprint, and so on @@ -892,7 +946,7 @@ def create_one_keypair(attrs=None, no_pri=False): def create_keypairs(attrs=None, count=2): """Create multiple fake keypairs. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of keypairs to fake @@ -933,7 +987,7 @@ class FakeAvailabilityZone(object): def create_one_availability_zone(attrs=None): """Create a fake AZ. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with zoneName, zoneState, etc. @@ -966,7 +1020,7 @@ def create_one_availability_zone(attrs=None): def create_availability_zones(attrs=None, count=2): """Create multiple fake AZs. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of AZs to fake @@ -989,7 +1043,7 @@ class FakeFloatingIP(object): def create_one_floating_ip(attrs=None): """Create a fake floating ip. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, ip, and so on @@ -1014,7 +1068,7 @@ def create_one_floating_ip(attrs=None): def create_floating_ips(attrs=None, count=2): """Create multiple fake floating ips. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of floating ips to fake @@ -1053,7 +1107,7 @@ class FakeFloatingIPPool(object): def create_one_floating_ip_pool(attrs=None): """Create a fake floating ip pool. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with name, etc @@ -1075,7 +1129,7 @@ def create_one_floating_ip_pool(attrs=None): def create_floating_ip_pools(attrs=None, count=2): """Create multiple fake floating ip pools. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of floating ip pools to fake @@ -1097,7 +1151,7 @@ class FakeNetwork(object): def create_one_network(attrs=None): """Create a fake network. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id, label, cidr and so on @@ -1149,7 +1203,7 @@ def create_one_network(attrs=None): def create_networks(attrs=None, count=2): """Create multiple fake networks. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of networks to fake @@ -1189,7 +1243,7 @@ class FakeHost(object): def create_one_host(attrs=None): """Create a fake host. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with uuid and other attributes @@ -1243,7 +1297,7 @@ class FakeServerGroup(object): def _create_one_server_group(attrs=None): """Create a fake server group - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id and other attributes @@ -1273,7 +1327,7 @@ def _create_one_server_group(attrs=None): def create_one_server_group(attrs=None): """Create a fake server group - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id and other attributes @@ -1291,7 +1345,7 @@ class FakeServerGroupV264(object): def create_one_server_group(attrs=None): """Create a fake server group - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with id and other attributes @@ -1309,7 +1363,7 @@ class FakeUsage(object): def create_one_usage(attrs=None): """Create a fake usage. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with tenant_id and other attributes @@ -1351,7 +1405,7 @@ def create_one_usage(attrs=None): def create_usages(attrs=None, count=2): """Create multiple fake services. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of services to fake @@ -1575,9 +1629,9 @@ class FakeMigration(object): def create_one_migration(attrs=None, methods=None): """Create a fake migration. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :return: A FakeResource object, with id, type, and so on @@ -1617,9 +1671,9 @@ def create_one_migration(attrs=None, methods=None): def create_migrations(attrs=None, methods=None, count=2): """Create multiple fake migrations. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :param int count: The number of migrations to fake @@ -1642,9 +1696,9 @@ class FakeServerMigration(object): def create_one_server_migration(attrs=None, methods=None): """Create a fake server migration. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :return: A FakeResource object, with id, type, and so on @@ -1695,9 +1749,9 @@ class FakeVolumeAttachment(object): def create_one_volume_attachment(attrs=None, methods=None): """Create a fake volume attachment. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :return: A FakeResource object, with id, device, and so on @@ -1733,9 +1787,9 @@ def create_one_volume_attachment(attrs=None, methods=None): def create_volume_attachments(attrs=None, methods=None, count=2): """Create multiple fake volume attachments (BDMs). - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :param int count: The number of volume attachments to fake diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 0012d70062..1644baae00 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -28,8 +28,9 @@ def setUp(self): super(TestServerBackup, self).setUp() # Get a shortcut to the compute client ServerManager Mock - self.servers_mock = self.app.client_manager.compute.servers - self.servers_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 # Get a shortcut to the image client ImageManager Mock self.images_mock = self.app.client_manager.image @@ -42,14 +43,14 @@ def setUp(self): self.methods = {} def setup_servers_mock(self, count): - servers = compute_fakes.FakeServer.create_servers( + servers = compute_fakes.FakeServer.create_sdk_servers( attrs=self.attrs, methods=self.methods, count=count, ) - # This is the return value for utils.find_resource() - self.servers_mock.get = compute_fakes.FakeServer.get_servers( + # This is the return value for compute_client.find_server() + self.sdk_client.find_server = compute_fakes.FakeServer.get_servers( servers, 0, ) @@ -130,8 +131,7 @@ def test_server_backup_defaults(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # ServerManager.backup(server, backup_name, backup_type, rotation) - self.servers_mock.backup.assert_called_with( + self.sdk_client.backup_server.assert_called_with( servers[0].id, servers[0].name, '', @@ -164,8 +164,7 @@ def test_server_backup_create_options(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # ServerManager.backup(server, backup_name, backup_type, rotation) - self.servers_mock.backup.assert_called_with( + self.sdk_client.backup_server.assert_called_with( servers[0].id, 'image', 'daily', @@ -212,8 +211,7 @@ def test_server_backup_wait_fail(self, mock_wait_for_status): parsed_args, ) - # ServerManager.backup(server, backup_name, backup_type, rotation) - self.servers_mock.backup.assert_called_with( + self.sdk_client.backup_server.assert_called_with( servers[0].id, 'image', 'daily', @@ -254,8 +252,7 @@ def test_server_backup_wait_ok(self, mock_wait_for_status): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # ServerManager.backup(server, backup_name, backup_type, rotation) - self.servers_mock.backup.assert_called_with( + self.sdk_client.backup_server.assert_called_with( servers[0].id, 'image', 'daily', diff --git a/releasenotes/notes/migrate-server-backup-to-sdk-0f170baf38e98b40.yaml b/releasenotes/notes/migrate-server-backup-to-sdk-0f170baf38e98b40.yaml new file mode 100644 index 0000000000..4f5cef27f1 --- /dev/null +++ b/releasenotes/notes/migrate-server-backup-to-sdk-0f170baf38e98b40.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Migrate openstack server backup from novaclient to sdk. From 8cb0a28607e7f8b9eb4bb71a95b39230d43a969c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 2 Nov 2021 09:59:27 +0000 Subject: [PATCH 0449/1120] compute: Don't warn if disk overcommit params unset Due to a small logic error, we were emitting a warning about a deprecated option when the user tried to live migrate an instance using microversion 2.25 even though the user hadn't actually set that option. Correct this. Change-Id: Ib61e817bd4ced9b5533e7c7f9d8f0b45fe81c211 Signed-off-by: Stephen Finucane Story: 2009657 Task: 43836 --- openstackclient/compute/v2/server.py | 8 ++++++-- .../tests/unit/compute/v2/test_server.py | 20 +++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c11f4b5781..d0edaf0769 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2621,7 +2621,7 @@ def get_parser(self, prog_name): disk_group.add_argument( '--disk-overcommit', action='store_true', - default=False, + default=None, help=_( 'Allow disk over-commit on the destination host' '(supported with --os-compute-api-version 2.24 or below)' @@ -2631,7 +2631,6 @@ def get_parser(self, prog_name): '--no-disk-overcommit', dest='disk_overcommit', action='store_false', - default=False, help=_( 'Do not over-commit disk on the destination host (default)' '(supported with --os-compute-api-version 2.24 or below)' @@ -2693,6 +2692,11 @@ def _show_progress(progress): if compute_client.api_version < api_versions.APIVersion('2.25'): kwargs['disk_over_commit'] = parsed_args.disk_overcommit + # We can't use an argparse default value because then we can't + # distinguish between explicit 'False' and unset for the below + # case (microversion >= 2.25) + if kwargs['disk_over_commit'] is None: + kwargs['disk_over_commit'] = False elif parsed_args.disk_overcommit is not None: # TODO(stephenfin): Raise an error here in OSC 7.0 msg = _( diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 3d8c17fdeb..285b2b411e 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4812,7 +4812,7 @@ def test_server_migrate_no_options(self): verifylist = [ ('live_migration', False), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4834,7 +4834,7 @@ def test_server_migrate_with_host_2_56(self): ('live_migration', False), ('host', 'fakehost'), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4856,7 +4856,7 @@ def test_server_migrate_with_block_migration(self): verifylist = [ ('live_migration', False), ('block_migration', True), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4897,7 +4897,7 @@ def test_server_migrate_with_host_pre_2_56(self): ('live_migration', False), ('host', 'fakehost'), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4923,7 +4923,7 @@ def test_server_live_migrate(self): ('live_migration', True), ('host', None), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4947,7 +4947,7 @@ def test_server_live_migrate_with_host(self): ('live_migration', True), ('host', 'fakehost'), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4975,7 +4975,7 @@ def test_server_live_migrate_with_host_pre_v230(self): ('live_migration', True), ('host', 'fakehost'), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -5002,7 +5002,7 @@ def test_server_block_live_migrate(self): verifylist = [ ('live_migration', True), ('block_migration', True), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -5088,7 +5088,7 @@ def test_server_migrate_with_wait(self, mock_wait_for_status): verifylist = [ ('live_migration', False), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -5108,7 +5108,7 @@ def test_server_migrate_with_wait_fails(self, mock_wait_for_status): verifylist = [ ('live_migration', False), ('block_migration', None), - ('disk_overcommit', False), + ('disk_overcommit', None), ('wait', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) From 442838ed158cd8fb4168877744a60de5cfca29cc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 2 Nov 2021 09:34:32 +0000 Subject: [PATCH 0450/1120] compute: Use correct command class for 'show migration' We should be inheriting from 'ShowOne'. Failure to do so results in a tuple being dumped to the screen. Not what we intended. While we're here, we update the docstring of this command to clarify the command's intent. Nova does not provide an API to retrieve an individual migration record for a cold migration or completed live migration. As such, the 'server migration show' command only works for in-progress live-migrations. Change-Id: I2e2fe3da7d642b9e8e3d930603dcde178cd68cde Signed-off-by: Stephen Finucane Story: 2009658 Task: 43837 --- openstackclient/compute/v2/server.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index d0edaf0769..1dc56fc7e6 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2967,8 +2967,13 @@ def take_action(self, parsed_args): return self.print_migrations(parsed_args, compute_client, migrations) -class ShowMigration(command.Command): - """Show a migration for a given server.""" +class ShowMigration(command.ShowOne): + """Show an in-progress live migration for a given server. + + Note that it is not possible to show cold migrations or completed + live-migrations. Use 'openstack server migration list' to get details for + these. + """ def get_parser(self, prog_name): parser = super().get_parser(prog_name) From 163cb01e46fc3f906154a7045fdbe9342cd446c7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 3 Nov 2021 11:31:04 +0000 Subject: [PATCH 0451/1120] compute: Return details of attached volumes The API behind the 'server add volume' command returns details of the created volume attachment, however, we were dropping these results rather than displaying them to the user. Correct this. Change-Id: I3f7e121220d29422ccf4e6940de2f28bb8496c83 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 22 ++++- .../tests/unit/compute/v2/test_server.py | 91 +++++++++++++++++-- ...or-server-add-volume-f75277ad58e31024.yaml | 5 + 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/show-result-for-server-add-volume-f75277ad58e31024.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c11f4b5781..80bdc61224 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -496,7 +496,7 @@ def take_action(self, parsed_args): server.add_security_group(security_group['id']) -class AddServerVolume(command.Command): +class AddServerVolume(command.ShowOne): _description = _( "Add volume to server. " "Specify ``--os-compute-api-version 2.20`` or higher to add a volume " @@ -595,12 +595,30 @@ def take_action(self, parsed_args): kwargs['delete_on_termination'] = False - compute_client.volumes.create_server_volume( + volume_attachment = compute_client.volumes.create_server_volume( server.id, volume.id, **kwargs ) + columns = ('id', 'serverId', 'volumeId', 'device') + column_headers = ('ID', 'Server ID', 'Volume ID', 'Device') + if compute_client.api_version >= api_versions.APIVersion('2.49'): + columns += ('tag',) + column_headers += ('Tag',) + if compute_client.api_version >= api_versions.APIVersion('2.79'): + columns += ('delete_on_termination',) + column_headers += ('Delete On Termination',) + + return ( + column_headers, + utils.get_item_properties( + volume_attachment, + columns, + mixed_case_fields=('serverId', 'volumeId'), + ) + ) + # TODO(stephenfin): Replace with 'MultiKeyValueAction' when we no longer # support '--nic=auto' and '--nic=none' diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 3d8c17fdeb..585f49de84 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -670,6 +670,11 @@ def setUp(self): def test_server_add_volume(self): servers = self.setup_servers_mock(count=1) + volume_attachment = \ + compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() + self.servers_volumes_mock.create_server_volume.return_value = \ + volume_attachment + arglist = [ '--device', '/dev/sdb', servers[0].id, @@ -683,11 +688,20 @@ def test_server_add_volume(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ('ID', 'Server ID', 'Volume ID', 'Device') + expected_data = ( + volume_attachment.id, + volume_attachment.serverId, + volume_attachment.volumeId, + volume_attachment.device, + ) + + columns, data = self.cmd.take_action(parsed_args) self.servers_volumes_mock.create_server_volume.assert_called_once_with( servers[0].id, self.volume.id, device='/dev/sdb') - self.assertIsNone(result) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) def test_server_add_volume_with_tag(self): # requires API 2.49 or later @@ -695,6 +709,11 @@ def test_server_add_volume_with_tag(self): '2.49') servers = self.setup_servers_mock(count=1) + volume_attachment = \ + compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() + self.servers_volumes_mock.create_server_volume.return_value = \ + volume_attachment + arglist = [ '--device', '/dev/sdb', '--tag', 'foo', @@ -710,11 +729,21 @@ def test_server_add_volume_with_tag(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ('ID', 'Server ID', 'Volume ID', 'Device', 'Tag') + expected_data = ( + volume_attachment.id, + volume_attachment.serverId, + volume_attachment.volumeId, + volume_attachment.device, + volume_attachment.tag, + ) + + columns, data = self.cmd.take_action(parsed_args) self.servers_volumes_mock.create_server_volume.assert_called_once_with( servers[0].id, self.volume.id, device='/dev/sdb', tag='foo') - self.assertIsNone(result) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) def test_server_add_volume_with_tag_pre_v249(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -746,6 +775,11 @@ def test_server_add_volume_with_enable_delete_on_termination(self): '2.79') servers = self.setup_servers_mock(count=1) + volume_attachment = \ + compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() + self.servers_volumes_mock.create_server_volume.return_value = \ + volume_attachment + arglist = [ '--enable-delete-on-termination', '--device', '/dev/sdb', @@ -761,18 +795,41 @@ def test_server_add_volume_with_enable_delete_on_termination(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ( + 'ID', + 'Server ID', + 'Volume ID', + 'Device', + 'Tag', + 'Delete On Termination', + ) + expected_data = ( + volume_attachment.id, + volume_attachment.serverId, + volume_attachment.volumeId, + volume_attachment.device, + volume_attachment.tag, + volume_attachment.delete_on_termination, + ) + + columns, data = self.cmd.take_action(parsed_args) self.servers_volumes_mock.create_server_volume.assert_called_once_with( servers[0].id, self.volume.id, device='/dev/sdb', delete_on_termination=True) - self.assertIsNone(result) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) def test_server_add_volume_with_disable_delete_on_termination(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.79') servers = self.setup_servers_mock(count=1) + volume_attachment = \ + compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() + self.servers_volumes_mock.create_server_volume.return_value = \ + volume_attachment + arglist = [ '--disable-delete-on-termination', '--device', '/dev/sdb', @@ -788,12 +845,30 @@ def test_server_add_volume_with_disable_delete_on_termination(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ( + 'ID', + 'Server ID', + 'Volume ID', + 'Device', + 'Tag', + 'Delete On Termination', + ) + expected_data = ( + volume_attachment.id, + volume_attachment.serverId, + volume_attachment.volumeId, + volume_attachment.device, + volume_attachment.tag, + volume_attachment.delete_on_termination, + ) + + columns, data = self.cmd.take_action(parsed_args) self.servers_volumes_mock.create_server_volume.assert_called_once_with( servers[0].id, self.volume.id, device='/dev/sdb', delete_on_termination=False) - self.assertIsNone(result) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) def test_server_add_volume_with_enable_delete_on_termination_pre_v279( self, diff --git a/releasenotes/notes/show-result-for-server-add-volume-f75277ad58e31024.yaml b/releasenotes/notes/show-result-for-server-add-volume-f75277ad58e31024.yaml new file mode 100644 index 0000000000..7e062d6d5f --- /dev/null +++ b/releasenotes/notes/show-result-for-server-add-volume-f75277ad58e31024.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``server add volume`` command will now return details of the created + volume attachment upon successful attachment. From 2183a611475090347863917f6c90f0f38cd80893 Mon Sep 17 00:00:00 2001 From: Diwei Zhu Date: Thu, 28 Oct 2021 02:16:23 +0000 Subject: [PATCH 0452/1120] Switch openstack server add port/network to using sdk. The old novaclient.v2.server.Server.interface_attach() method is replaced with proxy.create_server_interface(). In swargs, 'net_id' and 'port_id' are mutual-exclusive, if one of them is given with value, the other one cannot be None, as the API would responde with 400 (None is not string). In unit test, temporary method 'setup_sdk_servers_mock' is added, because other tests are still using the old 'setup_servers_mock'. Functional tests are added. Releasenote is generated. Change-Id: I9899f0509febc5143560a1859ae6344d0a6d1427 --- openstackclient/compute/v2/server.py | 23 ++++---- .../functional/compute/v2/test_server.py | 48 +++++++++++++++ .../tests/unit/compute/v2/test_server.py | 58 +++++++++++++------ ...work-add-port-to-sdk-7d81b25f59cfbec9.yaml | 4 ++ 4 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/migrate-server-add-network-add-port-to-sdk-7d81b25f59cfbec9.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index c11f4b5781..fcd7d69cdc 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -27,6 +27,7 @@ from novaclient import api_versions from novaclient.v2 import servers from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command @@ -378,10 +379,10 @@ 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 - server = utils.find_resource( - compute_client.servers, parsed_args.server) + server = compute_client.find_server( + parsed_args.server, ignore_missing=False) if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network @@ -392,12 +393,11 @@ def take_action(self, parsed_args): kwargs = { 'port_id': port_id, - 'net_id': None, 'fixed_ip': None, } if parsed_args.tag: - if compute_client.api_version < api_versions.APIVersion("2.49"): + if not sdk_utils.supports_microversion(compute_client, '2.49'): msg = _( '--os-compute-api-version 2.49 or greater is required to ' 'support the --tag option' @@ -405,7 +405,7 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) kwargs['tag'] = parsed_args.tag - server.interface_attach(**kwargs) + compute_client.create_server_interface(server, **kwargs) class AddNetwork(command.Command): @@ -434,10 +434,10 @@ 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 - server = utils.find_resource( - compute_client.servers, parsed_args.server) + server = compute_client.find_server( + parsed_args.server, ignore_missing=False) if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network @@ -447,13 +447,12 @@ def take_action(self, parsed_args): net_id = parsed_args.network kwargs = { - 'port_id': None, 'net_id': net_id, 'fixed_ip': None, } if parsed_args.tag: - if compute_client.api_version < api_versions.APIVersion('2.49'): + if not sdk_utils.supports_microversion(compute_client, '2.49'): msg = _( '--os-compute-api-version 2.49 or greater is required to ' 'support the --tag option' @@ -462,7 +461,7 @@ def take_action(self, parsed_args): kwargs['tag'] = parsed_args.tag - server.interface_attach(**kwargs) + compute_client.create_server_interface(server, **kwargs) class AddServerSecurityGroup(command.Command): diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 9cf2fc7f0f..59b1fad5f9 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -1071,3 +1071,51 @@ def test_server_create_with_empty_network_option_latest(self): # networks and the test didn't specify a specific network. self.assertNotIn('nics are required after microversion 2.36', e.stderr) + + def test_server_add_remove_network_port(self): + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'server create -f json ' + + '--network private ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--wait ' + + name + )) + + self.assertIsNotNone(cmd_output['id']) + self.assertEqual(name, cmd_output['name']) + + self.openstack( + 'server add network ' + name + ' public') + + cmd_output = json.loads(self.openstack( + 'server show -f json ' + name + )) + + addresses = cmd_output['addresses'] + self.assertIn('public', addresses) + + port_name = 'test-port' + + cmd_output = json.loads(self.openstack( + 'port list -f json' + )) + self.assertNotIn(port_name, cmd_output) + + cmd_output = json.loads(self.openstack( + 'port create -f json ' + + '--network private ' + port_name + )) + self.assertIsNotNone(cmd_output['id']) + + self.openstack('server add port ' + name + ' ' + port_name) + + cmd_output = json.loads(self.openstack( + 'server show -f json ' + name + )) + + # TODO(diwei): test remove network/port after the commands are switched + + self.openstack('server delete ' + name) + self.openstack('port delete ' + port_name) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 3d8c17fdeb..705a96b79c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -24,6 +24,7 @@ import iso8601 from novaclient import api_versions from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils as common_utils @@ -69,6 +70,10 @@ def setUp(self): self.servers_mock = self.app.client_manager.compute.servers self.servers_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 + # Get a shortcut to the compute client ServerMigrationsManager Mock self.server_migrations_mock = \ self.app.client_manager.compute.server_migrations @@ -133,6 +138,21 @@ def setup_servers_mock(self, count): 0) return servers + def setup_sdk_servers_mock(self, count): + servers = compute_fakes.FakeServer.create_sdk_servers( + attrs=self.attrs, + methods=self.methods, + count=count, + ) + + # This is the return value for compute_client.find_server() + self.sdk_client.find_server = compute_fakes.FakeServer.get_servers( + servers, + 0, + ) + + return servers + def run_method_with_servers(self, method_name, server_count): servers = self.setup_servers_mock(server_count) @@ -570,7 +590,7 @@ def setUp(self): self.app.client_manager.network.find_port = self.find_port def _test_server_add_port(self, port_id): - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) port = 'fake-port' arglist = [ @@ -585,8 +605,8 @@ def _test_server_add_port(self, port_id): result = self.cmd.take_action(parsed_args) - servers[0].interface_attach.assert_called_once_with( - port_id=port_id, net_id=None, fixed_ip=None) + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0], port_id=port_id, fixed_ip=None) self.assertIsNone(result) def test_server_add_port(self): @@ -599,11 +619,12 @@ def test_server_add_port_no_neutron(self): self._test_server_add_port('fake-port') self.find_port.assert_not_called() - def test_server_add_port_with_tag(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_server_add_port_with_tag(self, sm_mock): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.49') - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) self.find_port.return_value.id = 'fake-port' arglist = [ servers[0].id, @@ -620,13 +641,14 @@ def test_server_add_port_with_tag(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - servers[0].interface_attach.assert_called_once_with( + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0], port_id='fake-port', - net_id=None, fixed_ip=None, tag='tag1') - def test_server_add_port_with_tag_pre_v249(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_server_add_port_with_tag_pre_v249(self, sm_mock): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.48') @@ -891,7 +913,7 @@ def setUp(self): self.app.client_manager.network.find_network = self.find_network def _test_server_add_network(self, net_id): - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) network = 'fake-network' arglist = [ @@ -906,8 +928,8 @@ def _test_server_add_network(self, net_id): result = self.cmd.take_action(parsed_args) - servers[0].interface_attach.assert_called_once_with( - port_id=None, net_id=net_id, fixed_ip=None) + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0], net_id=net_id, fixed_ip=None) self.assertIsNone(result) def test_server_add_network(self): @@ -920,11 +942,12 @@ def test_server_add_network_no_neutron(self): self._test_server_add_network('fake-network') self.find_network.assert_not_called() - def test_server_add_network_with_tag(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_server_add_network_with_tag(self, sm_mock): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.49') - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) self.find_network.return_value.id = 'fake-network' arglist = [ @@ -942,18 +965,19 @@ def test_server_add_network_with_tag(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - servers[0].interface_attach.assert_called_once_with( - port_id=None, + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0], net_id='fake-network', fixed_ip=None, tag='tag1' ) - def test_server_add_network_with_tag_pre_v249(self): + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_server_add_network_with_tag_pre_v249(self, sm_mock): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.48') - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) self.find_network.return_value.id = 'fake-network' arglist = [ diff --git a/releasenotes/notes/migrate-server-add-network-add-port-to-sdk-7d81b25f59cfbec9.yaml b/releasenotes/notes/migrate-server-add-network-add-port-to-sdk-7d81b25f59cfbec9.yaml new file mode 100644 index 0000000000..930d6bc597 --- /dev/null +++ b/releasenotes/notes/migrate-server-add-network-add-port-to-sdk-7d81b25f59cfbec9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Migrate server add network/port from novaclient to openstacksdk. From 9acbd3e1052d533c1395eb59de4274170baed67b Mon Sep 17 00:00:00 2001 From: Thrivikram Mudunuri Date: Thu, 28 Oct 2021 18:05:03 -0400 Subject: [PATCH 0453/1120] Switch server image create to SDK Switch the server image create command from novaclient to SDK. Use the SDK versions of test fakes to support fake Server resources. Also, fetch updated image *after* waiting. If a user requests that we wait (--wait) for a server image to become active before returning, then we should probably return the final image. If we don't then the image can appear to be in a non-active state when it fact it's active. Correct this by fetching the image after the wait call. Change-Id: I83a403c035add9ab041ed6d59b5b29e42267f143 --- openstackclient/compute/v2/server_image.py | 18 ++++++------- .../unit/compute/v2/test_server_image.py | 27 +++++++++---------- ...-server-image-to-sdk-e3d8077ffe05bb3d.yaml | 4 +++ 3 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/migrate-create-server-image-to-sdk-e3d8077ffe05bb3d.yaml diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index 6c0e3b22cf..2021fae7c0 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -73,25 +73,23 @@ def _show_progress(progress): self.app.stdout.write('\rProgress: %s' % progress) self.app.stdout.flush() - compute_client = self.app.client_manager.compute + compute_client = self.app.client_manager.sdk_connection.compute + image_client = self.app.client_manager.image - server = utils.find_resource( - compute_client.servers, - parsed_args.server, + server = compute_client.find_server( + parsed_args.server, ignore_missing=False, ) + if parsed_args.name: image_name = parsed_args.name else: image_name = server.name - image_id = compute_client.servers.create_image( + image_id = compute_client.create_server_image( server.id, image_name, parsed_args.properties, - ) - - image_client = self.app.client_manager.image - image = image_client.find_image(image_id) + ).id if parsed_args.wait: if utils.wait_for_status( @@ -105,6 +103,8 @@ def _show_progress(progress): _('Error creating server image: %s'), parsed_args.server) raise exceptions.CommandError + image = image_client.find_image(image_id, ignore_missing=False) + if self.app.client_manager._api_version['image'] == '1': info = {} info.update(image._info) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 9b14428a27..b73cc763cb 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -27,8 +27,9 @@ def setUp(self): super(TestServerImage, self).setUp() # Get a shortcut to the compute client ServerManager Mock - self.servers_mock = self.app.client_manager.compute.servers - self.servers_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 # Get a shortcut to the image client ImageManager Mock self.images_mock = self.app.client_manager.image @@ -41,14 +42,14 @@ def setUp(self): self.methods = {} def setup_servers_mock(self, count): - servers = compute_fakes.FakeServer.create_servers( + servers = compute_fakes.FakeServer.create_sdk_servers( attrs=self.attrs, methods=self.methods, count=count, ) - # This is the return value for utils.find_resource() - self.servers_mock.get = compute_fakes.FakeServer.get_servers( + # This is the return value for compute_client.find_server() + self.sdk_client.find_server = compute_fakes.FakeServer.get_servers( servers, 0, ) @@ -104,8 +105,8 @@ def setup_images_mock(self, count, servers=None): ) self.images_mock.find_image = mock.Mock(side_effect=images) - self.servers_mock.create_image = mock.Mock( - return_value=images[0].id, + self.sdk_client.create_server_image = mock.Mock( + return_value=images[0], ) return images @@ -126,8 +127,7 @@ def test_server_image_create_defaults(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # ServerManager.create_image(server, image_name, metadata=) - self.servers_mock.create_image.assert_called_with( + self.sdk_client.create_server_image.assert_called_with( servers[0].id, servers[0].name, None, @@ -157,8 +157,7 @@ def test_server_image_create_options(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # ServerManager.create_image(server, image_name, metadata=) - self.servers_mock.create_image.assert_called_with( + self.sdk_client.create_server_image.assert_called_with( servers[0].id, 'img-nam', {'key': 'value'}, @@ -188,8 +187,7 @@ def test_server_create_image_wait_fail(self, mock_wait_for_status): parsed_args, ) - # ServerManager.create_image(server, image_name, metadata=) - self.servers_mock.create_image.assert_called_with( + self.sdk_client.create_server_image.assert_called_with( servers[0].id, servers[0].name, None, @@ -221,8 +219,7 @@ def test_server_create_image_wait_ok(self, mock_wait_for_status): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # ServerManager.create_image(server, image_name, metadata=) - self.servers_mock.create_image.assert_called_with( + self.sdk_client.create_server_image.assert_called_with( servers[0].id, servers[0].name, None, diff --git a/releasenotes/notes/migrate-create-server-image-to-sdk-e3d8077ffe05bb3d.yaml b/releasenotes/notes/migrate-create-server-image-to-sdk-e3d8077ffe05bb3d.yaml new file mode 100644 index 0000000000..20f8d55018 --- /dev/null +++ b/releasenotes/notes/migrate-create-server-image-to-sdk-e3d8077ffe05bb3d.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Migrate openstack server image create from novaclient to sdk. From 690e9a13a232f522162adc109d32c8eee864814e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 Nov 2021 10:28:40 +0000 Subject: [PATCH 0454/1120] image: Remove dead test helper methods These haven't been used since we switched the image commands from glanceclient to openstacksdk. There's more cleanup to be done here but that can be done later. Change-Id: I3de1f24323886b122b3a30660fb3de18eb7014e9 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/image/v1/fakes.py | 31 ---- openstackclient/tests/unit/image/v2/fakes.py | 173 ------------------- 2 files changed, 204 deletions(-) diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index add3978d1d..59ae5f7a2d 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/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. -# from unittest import mock import uuid @@ -25,36 +24,6 @@ image_id = 'im1' image_name = 'graven' -image_owner = 'baal' -image_protected = False -image_public = True -image_properties = { - 'Alpha': 'a', - 'Beta': 'b', - 'Gamma': 'g', -} -image_properties_str = "Alpha='a', Beta='b', Gamma='g'" -image_data = 'line 1\nline 2\n' -image_size = 0 - -IMAGE = { - 'id': image_id, - 'name': image_name, - 'container_format': '', - 'disk_format': '', - 'owner': image_owner, - 'min_disk': 0, - 'min_ram': 0, - 'is_public': image_public, - 'protected': image_protected, - 'properties': image_properties, - 'size': image_size, -} - -IMAGE_columns = tuple(sorted(IMAGE)) -IMAGE_output = dict(IMAGE) -IMAGE_output['properties'] = image_properties_str -IMAGE_data = tuple((IMAGE_output[x] for x in sorted(IMAGE_output))) class FakeImagev1Client(object): diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 0d83f98b95..49ce400d92 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -11,16 +11,13 @@ # 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 copy import random from unittest import mock import uuid from openstack.image.v2 import image from openstack.image.v2 import member -from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -28,121 +25,6 @@ image_id = '0f41529e-7c12-4de8-be2d-181abb825b3c' image_name = 'graven' -image_owner = 'baal' -image_protected = False -image_visibility = 'public' -image_tags = [] -image_size = 0 - -IMAGE = { - 'id': image_id, - 'name': image_name, - 'owner': image_owner, - 'protected': image_protected, - 'visibility': image_visibility, - 'tags': image_tags, - 'size': image_size -} - -IMAGE_columns = tuple(sorted(IMAGE)) -IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) - -IMAGE_SHOW = copy.copy(IMAGE) -IMAGE_SHOW['tags'] = format_columns.ListColumn(IMAGE_SHOW['tags']) -IMAGE_SHOW_data = tuple((IMAGE_SHOW[x] for x in sorted(IMAGE_SHOW))) - -# Just enough v2 schema to do some testing -IMAGE_schema = { - "additionalProperties": { - "type": "string" - }, - "name": "image", - "links": [ - { - "href": "{self}", - "rel": "self" - }, - { - "href": "{file}", - "rel": "enclosure" - }, - { - "href": "{schema}", - "rel": "describedby" - } - ], - "properties": { - "id": { - "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", # noqa - "type": "string", - "description": "An identifier for the image" - }, - "name": { - "type": [ - "null", - "string" - ], - "description": "Descriptive name for the image", - "maxLength": 255 - }, - "owner": { - "type": [ - "null", - "string" - ], - "description": "Owner of the image", - "maxLength": 255 - }, - "protected": { - "type": "boolean", - "description": "If true, image will not be deletable." - }, - "self": { - "type": "string", - "description": "(READ-ONLY)" - }, - "schema": { - "type": "string", - "description": "(READ-ONLY)" - }, - "size": { - "type": [ - "null", - "integer", - "string" - ], - "description": "Size of image file in bytes (READ-ONLY)" - }, - "status": { - "enum": [ - "queued", - "saving", - "active", - "killed", - "deleted", - "pending_delete" - ], - "type": "string", - "description": "Status of the image (READ-ONLY)" - }, - "tags": { - "items": { - "type": "string", - "maxLength": 255 - }, - "type": "array", - "description": "List of strings related to the image" - }, - "visibility": { - "enum": [ - "public", - "private" - ], - "type": "string", - "description": "Scope of image accessibility" - }, - } -} class FakeImagev2Client(object): @@ -231,61 +113,6 @@ def create_images(attrs=None, count=2): return images - @staticmethod - def get_images(images=None, count=2): - """Get an iterable MagicMock object with a list of faked images. - - If images list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List images: - A list of FakeResource objects faking images - :param Integer count: - The number of images to be faked - :return: - An iterable Mock object with side_effect set to a list of faked - images - """ - if images is None: - images = FakeImage.create_images(count) - - return mock.Mock(side_effect=images) - - @staticmethod - def get_image_columns(image=None): - """Get the image columns from a faked image object. - - :param image: - A FakeResource objects faking image - :return: - A tuple which may include the following keys: - ('id', 'name', 'owner', 'protected', 'visibility', 'tags') - """ - if image is not None: - return tuple(sorted(image)) - return IMAGE_columns - - @staticmethod - def get_image_data(image=None): - """Get the image data from a faked image object. - - :param image: - A FakeResource objects faking image - :return: - A tuple which may include the following values: - ('image-123', 'image-foo', 'admin', False, 'public', 'bar, baz') - """ - data_list = [] - if image is not None: - for x in sorted(image.keys()): - if x == 'tags': - # The 'tags' should be format_list - data_list.append( - format_columns.ListColumn(getattr(image, x))) - else: - data_list.append(getattr(image, x)) - return tuple(data_list) - @staticmethod def create_one_image_member(attrs=None): """Create a fake image member. From 2135a9ea05c79a11185ca87f6bb5ade3b71501bb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 Nov 2021 10:35:51 +0000 Subject: [PATCH 0455/1120] image: Remove FakeImage test helper We're no longer creating fake versions of glanceclient's 'Resource' object but rather openstacksdk objects. As such, there's no point nesting things under a fake resource class. Change-Id: I39cd5302622f4542db9eebcccfad0cb90d077441 Signed-off-by: Stephen Finucane --- .../tests/unit/common/test_project_purge.py | 2 +- .../tests/unit/compute/v2/test_aggregate.py | 2 +- .../tests/unit/compute/v2/test_server.py | 28 ++-- .../unit/compute/v2/test_server_backup.py | 13 +- .../unit/compute/v2/test_server_image.py | 4 +- openstackclient/tests/unit/image/v1/fakes.py | 74 +++++----- .../tests/unit/image/v1/test_image.py | 11 +- openstackclient/tests/unit/image/v2/fakes.py | 133 ++++++++--------- .../tests/unit/image/v2/test_image.py | 134 +++++++++--------- .../tests/unit/volume/v1/test_volume.py | 4 +- .../tests/unit/volume/v2/test_volume.py | 4 +- 11 files changed, 187 insertions(+), 222 deletions(-) diff --git a/openstackclient/tests/unit/common/test_project_purge.py b/openstackclient/tests/unit/common/test_project_purge.py index adc48ce26f..5199093ce3 100644 --- a/openstackclient/tests/unit/common/test_project_purge.py +++ b/openstackclient/tests/unit/common/test_project_purge.py @@ -70,7 +70,7 @@ class TestProjectPurge(TestProjectPurgeInit): project = identity_fakes.FakeProject.create_one_project() server = compute_fakes.FakeServer.create_one_server() - image = image_fakes.FakeImage.create_one_image() + 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() diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index 7c4fe5cbd3..071f2a3027 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -552,7 +552,7 @@ def test_aggregate_unset_no_option(self): class TestAggregateCacheImage(TestAggregate): - images = image_fakes.FakeImage.create_images(count=2) + images = image_fakes.create_images(count=2) def setUp(self): super(TestAggregateCacheImage, self).setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9623cb0abf..f7ed4b16a4 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1168,7 +1168,7 @@ def setUp(self): self.servers_mock.create.return_value = self.new_server - self.image = image_fakes.FakeImage.create_one_image() + self.image = image_fakes.create_one_image() self.find_image_mock.return_value = self.image self.get_image_mock.return_value = self.image @@ -2918,7 +2918,7 @@ def test_server_create_image_property(self): 'hypervisor_type': 'qemu', } - _image = image_fakes.FakeImage.create_one_image(image_info) + _image = image_fakes.create_one_image(image_info) self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -2974,7 +2974,7 @@ def test_server_create_image_property_multi(self): 'hypervisor_type': 'qemu', 'hw_disk_bus': 'ide', } - _image = image_fakes.FakeImage.create_one_image(image_info) + _image = image_fakes.create_one_image(image_info) self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -3031,7 +3031,7 @@ def test_server_create_image_property_missed(self): 'hw_disk_bus': 'ide', } - _image = image_fakes.FakeImage.create_one_image(image_info) + _image = image_fakes.create_one_image(image_info) self.images_mock.return_value = [_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -3063,8 +3063,8 @@ def test_server_create_image_property_with_image_list(self): } } - target_image = image_fakes.FakeImage.create_one_image(image_info) - another_image = image_fakes.FakeImage.create_one_image({}) + target_image = image_fakes.create_one_image(image_info) + another_image = image_fakes.create_one_image({}) self.images_mock.return_value = [target_image, another_image] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -4102,7 +4102,7 @@ def setUp(self): self.servers = self.setup_servers_mock(3) self.servers_mock.list.return_value = self.servers - self.image = image_fakes.FakeImage.create_one_image() + self.image = image_fakes.create_one_image() # self.images_mock.return_value = [self.image] self.find_image_mock.return_value = self.image @@ -6021,7 +6021,7 @@ def setUp(self): super(TestServerRebuild, self).setUp() # Return value for utils.find_resource for image - self.image = image_fakes.FakeImage.create_one_image() + self.image = image_fakes.create_one_image() self.get_image_mock.return_value = self.image # Fake the rebuilt new server. @@ -6051,7 +6051,7 @@ def setUp(self): def test_rebuild_with_image_name(self): image_name = 'my-custom-image' - user_image = image_fakes.FakeImage.create_one_image( + user_image = image_fakes.create_one_image( attrs={'name': image_name}) self.find_image_mock.return_value = user_image @@ -6600,7 +6600,7 @@ class TestEvacuateServer(TestServer): def setUp(self): super(TestEvacuateServer, self).setUp() # Return value for utils.find_resource for image - self.image = image_fakes.FakeImage.create_one_image() + self.image = image_fakes.create_one_image() self.images_mock.get.return_value = self.image # Fake the rebuilt new server. @@ -6794,7 +6794,7 @@ def setUp(self): super(TestServerRescue, self).setUp() # Return value for utils.find_resource for image - self.image = image_fakes.FakeImage.create_one_image() + self.image = image_fakes.create_one_image() self.get_image_mock.return_value = self.image new_server = compute_fakes.FakeServer.create_one_server() @@ -6835,7 +6835,7 @@ def test_rescue_with_current_image(self): self.server.rescue.assert_called_with(image=None, password=None) def test_rescue_with_new_image(self): - new_image = image_fakes.FakeImage.create_one_image() + new_image = image_fakes.create_one_image() self.find_image_mock.return_value = new_image arglist = [ '--image', new_image.id, @@ -7950,7 +7950,7 @@ class TestServerShow(TestServer): def setUp(self): super(TestServerShow, self).setUp() - self.image = image_fakes.FakeImage.create_one_image() + self.image = image_fakes.create_one_image() self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.topology = { 'nodes': [{'vcpu_set': [0, 1]}, {'vcpu_set': [2, 3]}], @@ -8540,7 +8540,7 @@ def test_prep_server_detail(self, find_resource): # - The first time, return server info. # - The second time, return image info. # - The third time, return flavor info. - _image = image_fakes.FakeImage.create_one_image() + _image = image_fakes.create_one_image() _flavor = compute_fakes.FakeFlavor.create_one_flavor() server_info = { 'image': {u'id': _image.id}, diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 1644baae00..1a5e0a1225 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -91,7 +91,7 @@ def setUp(self): def setup_images_mock(self, count, servers=None): if servers: - images = image_fakes.FakeImage.create_images( + images = image_fakes.create_images( attrs={ 'name': servers[0].name, 'status': 'active', @@ -99,7 +99,7 @@ def setup_images_mock(self, count, servers=None): count=count, ) else: - images = image_fakes.FakeImage.create_images( + images = image_fakes.create_images( attrs={ 'status': 'active', }, @@ -178,15 +178,6 @@ def test_server_backup_create_options(self): def test_server_backup_wait_fail(self, mock_wait_for_status): servers = self.setup_servers_mock(count=1) images = self.setup_images_mock(count=1, servers=servers) -# images = image_fakes.FakeImage.create_images( -# attrs={ -# 'name': servers[0].name, -# 'status': 'active', -# }, -# count=1, -# ) -# -# self.images_mock.find_image.return_value = images[0] self.images_mock.get_image = mock.Mock( side_effect=images[0], ) diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 9b14428a27..e17401691a 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -88,7 +88,7 @@ def setUp(self): def setup_images_mock(self, count, servers=None): if servers: - images = image_fakes.FakeImage.create_images( + images = image_fakes.create_images( attrs={ 'name': servers[0].name, 'status': 'active', @@ -96,7 +96,7 @@ def setup_images_mock(self, count, servers=None): count=count, ) else: - images = image_fakes.FakeImage.create_images( + images = image_fakes.create_images( attrs={ 'status': 'active', }, diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index 59ae5f7a2d..3097a42f59 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -22,10 +22,6 @@ from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes -image_id = 'im1' -image_name = 'graven' - - class FakeImagev1Client(object): def __init__(self, **kwargs): @@ -51,40 +47,36 @@ def setUp(self): ) -class FakeImage(object): - """Fake one or more images.""" - - @staticmethod - def create_one_image(attrs=None): - """Create a fake image. - - :param Dictionary attrs: - A dictionary with all attrbutes of image - :return: - A FakeResource object with id, name, owner, protected, - visibility and tags attrs - """ - attrs = attrs or {} - - # Set default attribute - image_info = { - 'id': str(uuid.uuid4()), - 'name': 'image-name' + uuid.uuid4().hex, - 'owner': 'image-owner' + uuid.uuid4().hex, - 'container_format': '', - 'disk_format': '', - 'min_disk': 0, - 'min_ram': 0, - 'is_public': True, - 'protected': False, - 'properties': { - 'Alpha': 'a', - 'Beta': 'b', - 'Gamma': 'g'}, - 'status': 'status' + uuid.uuid4().hex - } - - # Overwrite default attributes if there are some attributes set - image_info.update(attrs) - - return image.Image(**image_info) +def create_one_image(attrs=None): + """Create a fake image. + + :param Dictionary attrs: + A dictionary with all attrbutes of image + :return: + A FakeResource object with id, name, owner, protected, + visibility and tags attrs + """ + attrs = attrs or {} + + # Set default attribute + image_info = { + 'id': str(uuid.uuid4()), + 'name': 'image-name' + uuid.uuid4().hex, + 'owner': 'image-owner' + uuid.uuid4().hex, + 'container_format': '', + 'disk_format': '', + 'min_disk': 0, + 'min_ram': 0, + 'is_public': True, + 'protected': False, + 'properties': { + 'Alpha': 'a', + 'Beta': 'b', + 'Gamma': 'g'}, + 'status': 'status' + uuid.uuid4().hex + } + + # Overwrite default attributes if there are some attributes set + image_info.update(attrs) + + return image.Image(**image_info) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 5c69bf0f87..06519800ce 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -34,7 +34,7 @@ def setUp(self): class TestImageCreate(TestImage): - new_image = image_fakes.FakeImage.create_one_image() + new_image = image_fakes.create_one_image() columns = ( 'container_format', 'disk_format', @@ -210,7 +210,7 @@ def test_image_create_file(self, mock_open): class TestImageDelete(TestImage): - _image = image_fakes.FakeImage.create_one_image() + _image = image_fakes.create_one_image() def setUp(self): super(TestImageDelete, self).setUp() @@ -239,7 +239,7 @@ def test_image_delete_no_options(self): class TestImageList(TestImage): - _image = image_fakes.FakeImage.create_one_image() + _image = image_fakes.create_one_image() columns = ( 'ID', @@ -443,7 +443,7 @@ def test_image_list_sort_option(self, si_mock): class TestImageSet(TestImage): - _image = image_fakes.FakeImage.create_one_image() + _image = image_fakes.create_one_image() def setUp(self): super(TestImageSet, self).setUp() @@ -682,8 +682,7 @@ def test_image_set_numeric_options_to_zero(self): class TestImageShow(TestImage): - _image = image_fakes.FakeImage.create_one_image( - attrs={'size': 2000}) + _image = image_fakes.create_one_image(attrs={'size': 2000}) columns = ( 'container_format', 'disk_format', diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 49ce400d92..910bd726ff 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -23,9 +23,6 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils -image_id = '0f41529e-7c12-4de8-be2d-181abb825b3c' -image_name = 'graven' - class FakeImagev2Client(object): @@ -63,75 +60,67 @@ def setUp(self): ) -class FakeImage(object): - """Fake one or more images. +def create_one_image(attrs=None): + """Create a fake image. + + :param attrs: A dictionary with all attributes of image + :type attrs: dict + :return: A fake Image object. + :rtype: `openstack.image.v2.image.Image` + """ + attrs = attrs or {} + + # Set default attribute + image_info = { + 'id': str(uuid.uuid4()), + 'name': 'image-name' + uuid.uuid4().hex, + 'owner_id': 'image-owner' + uuid.uuid4().hex, + 'is_protected': bool(random.choice([0, 1])), + 'visibility': random.choice(['public', 'private']), + 'tags': [uuid.uuid4().hex for r in range(2)], + } + + # Overwrite default attributes if there are some attributes set + image_info.update(attrs) + + return image.Image(**image_info) + + +def create_images(attrs=None, count=2): + """Create multiple fake images. - TODO(xiexs): Currently, only image API v2 is supported by this class. + :param attrs: A dictionary with all attributes of image + :type attrs: dict + :param count: The number of images to be faked + :type count: int + :return: A list of fake Image objects + :rtype: list """ + images = [] + for n in range(0, count): + images.append(create_one_image(attrs)) + + return images + + +def create_one_image_member(attrs=None): + """Create a fake image member. + + :param attrs: A dictionary with all attributes of image member + :type attrs: dict + :return: A fake Member object. + :rtype: `openstack.image.v2.member.Member` + """ + attrs = attrs or {} + + # Set default attribute + image_member_info = { + 'member_id': 'member-id-' + uuid.uuid4().hex, + 'image_id': 'image-id-' + uuid.uuid4().hex, + 'status': 'pending', + } + + # Overwrite default attributes if there are some attributes set + image_member_info.update(attrs) - @staticmethod - def create_one_image(attrs=None): - """Create a fake image. - - :param Dictionary attrs: - A dictionary with all attrbutes of image - :return: - A FakeResource object with id, name, owner, protected, - visibility, tags and size attrs - """ - attrs = attrs or {} - - # Set default attribute - image_info = { - 'id': str(uuid.uuid4()), - 'name': 'image-name' + uuid.uuid4().hex, - 'owner_id': 'image-owner' + uuid.uuid4().hex, - 'is_protected': bool(random.choice([0, 1])), - 'visibility': random.choice(['public', 'private']), - 'tags': [uuid.uuid4().hex for r in range(2)], - } - - # Overwrite default attributes if there are some attributes set - image_info.update(attrs) - - return image.Image(**image_info) - - @staticmethod - def create_images(attrs=None, count=2): - """Create multiple fake images. - - :param Dictionary attrs: - A dictionary with all attributes of image - :param Integer count: - The number of images to be faked - :return: - A list of FakeResource objects - """ - images = [] - for n in range(0, count): - images.append(FakeImage.create_one_image(attrs)) - - return images - - @staticmethod - def create_one_image_member(attrs=None): - """Create a fake image member. - - :param Dictionary attrs: - A dictionary with all attributes of image member - :return: - A FakeResource object with member_id, image_id and so on - """ - attrs = attrs or {} - - # Set default attribute - image_member_info = { - 'member_id': 'member-id-' + uuid.uuid4().hex, - 'image_id': 'image-id-' + uuid.uuid4().hex, - 'status': 'pending', - } - - # Overwrite default attributes if there are some attributes set - image_member_info.update(attrs) - - return member.Member(**image_member_info) + return member.Member(**image_member_info) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 35af6799f6..f15463738a 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.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 copy import io @@ -52,7 +51,7 @@ def setUp(self): self.domain_mock.reset_mock() def setup_images_mock(self, count): - images = image_fakes.FakeImage.create_images(count=count) + images = image_fakes.create_images(count=count) return images @@ -65,7 +64,7 @@ class TestImageCreate(TestImage): def setUp(self): super(TestImageCreate, self).setUp() - self.new_image = image_fakes.FakeImage.create_one_image() + self.new_image = image_fakes.create_one_image() self.client.create_image.return_value = self.new_image self.project_mock.get.return_value = self.project @@ -182,7 +181,7 @@ def test_image_create_with_unexist_project(self): '--protected', '--private', '--project', 'unexist_owner', - image_fakes.image_name, + 'graven', ] verifylist = [ ('container_format', 'ovf'), @@ -194,7 +193,7 @@ def test_image_create_with_unexist_project(self): ('public', False), ('private', True), ('project', 'unexist_owner'), - ('name', image_fakes.image_name), + ('name', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -302,8 +301,8 @@ class TestAddProjectToImage(TestImage): project = identity_fakes.FakeProject.create_one_project() domain = identity_fakes.FakeDomain.create_one_domain() - _image = image_fakes.FakeImage.create_one_image() - new_member = image_fakes.FakeImage.create_one_image_member( + _image = image_fakes.create_one_image() + new_member = image_fakes.create_one_image_member( attrs={'image_id': _image.id, 'member_id': project.id} ) @@ -435,7 +434,7 @@ def test_image_delete_multi_images(self): def test_image_delete_multi_images_exception(self): - images = image_fakes.FakeImage.create_images(count=2) + images = image_fakes.create_images(count=2) arglist = [ images[0].id, images[1].id, @@ -467,7 +466,7 @@ def test_image_delete_multi_images_exception(self): class TestImageList(TestImage): - _image = image_fakes.FakeImage.create_one_image() + _image = image_fakes.create_one_image() columns = ( 'ID', @@ -786,18 +785,13 @@ def test_image_list_project_option(self): @mock.patch('osc_lib.utils.find_resource') def test_image_list_marker_option(self, fr_mock): - # tangchen: Since image_fakes.IMAGE is a dict, it cannot offer a .id - # operation. Will fix this by using FakeImage class instead - # of IMAGE dict. self.client.find_image = mock.Mock(return_value=self._image) -# fr_mock.return_value = mock.Mock() -# fr_mock.return_value.id = image_fakes.image_id arglist = [ - '--marker', image_fakes.image_name, + '--marker', 'graven', ] verifylist = [ - ('marker', image_fakes.image_name), + ('marker', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -806,7 +800,7 @@ def test_image_list_marker_option(self, fr_mock): marker=self._image.id, ) - self.client.find_image.assert_called_with(image_fakes.image_name) + self.client.find_image.assert_called_with('graven') def test_image_list_name_option(self): arglist = [ @@ -869,8 +863,8 @@ def test_image_list_tag_option(self): class TestListImageProjects(TestImage): project = identity_fakes.FakeProject.create_one_project() - _image = image_fakes.FakeImage.create_one_image() - member = image_fakes.FakeImage.create_one_image_member( + _image = image_fakes.create_one_image() + member = image_fakes.create_one_image_member( attrs={'image_id': _image.id, 'member_id': project.id} ) @@ -920,7 +914,7 @@ class TestRemoveProjectImage(TestImage): def setUp(self): super(TestRemoveProjectImage, self).setUp() - self._image = image_fakes.FakeImage.create_one_image() + self._image = image_fakes.create_one_image() # This is the return value for utils.find_resource() self.client.find_image.return_value = self._image @@ -979,7 +973,7 @@ class TestImageSet(TestImage): project = identity_fakes.FakeProject.create_one_project() domain = identity_fakes.FakeDomain.create_one_domain() - _image = image_fakes.FakeImage.create_one_image({'tags': []}) + _image = image_fakes.create_one_image({'tags': []}) def setUp(self): super(TestImageSet, self).setUp() @@ -999,10 +993,10 @@ def setUp(self): def test_image_set_no_options(self): arglist = [ - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ - ('image', image_fakes.image_id) + ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1013,8 +1007,8 @@ def test_image_set_no_options(self): self.image_members_mock.update.assert_not_called() def test_image_set_membership_option_accept(self): - membership = image_fakes.FakeImage.create_one_image_member( - attrs={'image_id': image_fakes.image_id, + membership = image_fakes.create_one_image_member( + attrs={'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', 'member_id': self.project.id} ) self.client.update_member.return_value = membership @@ -1044,21 +1038,21 @@ def test_image_set_membership_option_accept(self): self.client.update_image.assert_called_with(self._image.id) def test_image_set_membership_option_reject(self): - membership = image_fakes.FakeImage.create_one_image_member( - attrs={'image_id': image_fakes.image_id, + membership = image_fakes.create_one_image_member( + attrs={'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', 'member_id': self.project.id} ) self.client.update_member.return_value = membership arglist = [ '--reject', - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ ('accept', False), ('reject', True), ('pending', False), - ('image', image_fakes.image_id) + ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1075,21 +1069,21 @@ def test_image_set_membership_option_reject(self): self.client.update_image.assert_called_with(self._image.id) def test_image_set_membership_option_pending(self): - membership = image_fakes.FakeImage.create_one_image_member( - attrs={'image_id': image_fakes.image_id, + membership = image_fakes.create_one_image_member( + attrs={'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', 'member_id': self.project.id} ) self.client.update_member.return_value = membership arglist = [ '--pending', - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ ('accept', False), ('reject', False), ('pending', True), - ('image', image_fakes.image_id) + ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1149,11 +1143,11 @@ def test_image_set_with_unexist_project(self): arglist = [ '--project', 'unexist_owner', - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ ('project', 'unexist_owner'), - ('image', image_fakes.image_id), + ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1165,14 +1159,14 @@ def test_image_set_bools1(self): arglist = [ '--protected', '--private', - image_fakes.image_name, + 'graven', ] verifylist = [ ('protected', True), ('unprotected', False), ('public', False), ('private', True), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1193,14 +1187,14 @@ def test_image_set_bools2(self): arglist = [ '--unprotected', '--public', - image_fakes.image_name, + 'graven', ] verifylist = [ ('protected', False), ('unprotected', True), ('public', True), ('private', False), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1221,11 +1215,11 @@ def test_image_set_properties(self): arglist = [ '--property', 'Alpha=1', '--property', 'Beta=2', - image_fakes.image_name, + 'graven', ] verifylist = [ ('properties', {'Alpha': '1', 'Beta': '2'}), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1250,7 +1244,7 @@ def test_image_set_fake_properties(self): '--os-distro', 'cpm', '--os-version', '2.2H', '--ramdisk-id', 'xyzpdq', - image_fakes.image_name, + 'graven', ] verifylist = [ ('architecture', 'z80'), @@ -1259,7 +1253,7 @@ def test_image_set_fake_properties(self): ('os_distro', 'cpm'), ('os_version', '2.2H'), ('ramdisk_id', 'xyzpdq'), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1283,11 +1277,11 @@ def test_image_set_fake_properties(self): def test_image_set_tag(self): arglist = [ '--tag', 'test-tag', - image_fakes.image_name, + 'graven', ] verifylist = [ ('tags', ['test-tag']), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1307,11 +1301,11 @@ def test_image_set_activate(self): arglist = [ '--tag', 'test-tag', '--activate', - image_fakes.image_name, + 'graven', ] verifylist = [ ('tags', ['test-tag']), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1335,11 +1329,11 @@ def test_image_set_deactivate(self): arglist = [ '--tag', 'test-tag', '--deactivate', - image_fakes.image_name, + 'graven', ] verifylist = [ ('tags', ['test-tag']), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1365,11 +1359,11 @@ def test_image_set_tag_merge(self): self.client.find_image.return_value = old_image arglist = [ '--tag', 'test-tag', - image_fakes.image_name, + 'graven', ] verifylist = [ ('tags', ['test-tag']), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1391,11 +1385,11 @@ def test_image_set_tag_merge_dupe(self): self.client.find_image.return_value = old_image arglist = [ '--tag', 'old1', - image_fakes.image_name, + 'graven', ] verifylist = [ ('tags', ['old1']), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1415,11 +1409,11 @@ def test_image_set_dead_options(self): arglist = [ '--visibility', '1-mile', - image_fakes.image_name, + 'graven', ] verifylist = [ ('visibility', '1-mile'), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1431,12 +1425,12 @@ def test_image_set_numeric_options_to_zero(self): arglist = [ '--min-disk', '0', '--min-ram', '0', - image_fakes.image_name, + 'graven', ] verifylist = [ ('min_disk', 0), ('min_ram', 0), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1457,13 +1451,13 @@ def test_image_set_hidden(self): arglist = [ '--hidden', '--public', - image_fakes.image_name, + 'graven', ] verifylist = [ ('hidden', True), ('public', True), ('private', False), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1484,13 +1478,13 @@ def test_image_set_unhidden(self): arglist = [ '--unhidden', '--public', - image_fakes.image_name, + 'graven', ] verifylist = [ ('hidden', False), ('public', True), ('private', False), - ('image', image_fakes.image_name), + ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1510,10 +1504,10 @@ def test_image_set_unhidden(self): class TestImageShow(TestImage): - new_image = image_fakes.FakeImage.create_one_image( + new_image = image_fakes.create_one_image( attrs={'size': 1000}) - _data = image_fakes.FakeImage.create_one_image() + _data = image_fakes.create_one_image() columns = ( 'id', 'name', 'owner', 'protected', 'tags', 'visibility' @@ -1538,10 +1532,10 @@ def setUp(self): def test_image_show(self): arglist = [ - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ - ('image', image_fakes.image_id), + ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1550,7 +1544,7 @@ def test_image_show(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.client.find_image.assert_called_with( - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ignore_missing=False ) @@ -1592,7 +1586,7 @@ def setUp(self): attrs['hw_rng_model'] = 'virtio' attrs['prop'] = 'test' attrs['prop2'] = 'fake' - self.image = image_fakes.FakeImage.create_one_image(attrs) + self.image = image_fakes.create_one_image(attrs) self.client.find_image.return_value = self.image self.client.remove_tag.return_value = self.image @@ -1603,10 +1597,10 @@ def setUp(self): def test_image_unset_no_options(self): arglist = [ - image_fakes.image_id, + '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ - ('image', image_fakes.image_id) + ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1681,7 +1675,7 @@ def test_image_unset_mixed_option(self): class TestImageSave(TestImage): - image = image_fakes.FakeImage.create_one_image({}) + image = image_fakes.create_one_image({}) def setUp(self): super(TestImageSave, self).setUp() diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 702f79ed83..584eca2a7b 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -317,7 +317,7 @@ def test_volume_create_properties(self): self.assertCountEqual(self.datalist, data) def test_volume_create_image_id(self): - image = image_fakes.FakeImage.create_one_image() + image = image_fakes.create_one_image() self.images_mock.get.return_value = image arglist = [ @@ -360,7 +360,7 @@ def test_volume_create_image_id(self): self.assertCountEqual(self.datalist, data) def test_volume_create_image_name(self): - image = image_fakes.FakeImage.create_one_image() + image = image_fakes.create_one_image() self.images_mock.get.return_value = image arglist = [ diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 4aa6f906cc..ec82e6746b 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -221,7 +221,7 @@ def test_volume_create_properties(self): self.assertCountEqual(self.datalist, data) def test_volume_create_image_id(self): - image = image_fakes.FakeImage.create_one_image() + image = image_fakes.create_one_image() self.find_image_mock.return_value = image arglist = [ @@ -259,7 +259,7 @@ def test_volume_create_image_id(self): self.assertCountEqual(self.datalist, data) def test_volume_create_image_name(self): - image = image_fakes.FakeImage.create_one_image() + image = image_fakes.create_one_image() self.find_image_mock.return_value = image arglist = [ From 1feb676469f7ccd6a022027bf2e1ecee9cf6d548 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 Nov 2021 10:55:33 +0000 Subject: [PATCH 0456/1120] tests: Update fake image client in tests These clients are intended to fake out the old glanceclient client which we no longer use. They were only "working" because we weren't actually using any of the glancelclient-based stuff and were instead overriding everything within the tests. Move these overrides back to the main fake client and remove the crud. Change-Id: I92ee74a1df72a6dd23f9d2dc04342aab0cbd3210 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/image/v1/fakes.py | 8 +++--- .../tests/unit/image/v1/test_image.py | 6 +---- openstackclient/tests/unit/image/v2/fakes.py | 27 +++++++++++-------- .../tests/unit/image/v2/test_image.py | 23 +++++----------- 4 files changed, 28 insertions(+), 36 deletions(-) diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index 3097a42f59..164050c00e 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -22,11 +22,11 @@ from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes -class FakeImagev1Client(object): +class FakeImagev1Client: def __init__(self, **kwargs): self.images = mock.Mock() - self.images.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.version = 1.0 @@ -35,7 +35,7 @@ def __init__(self, **kwargs): class TestImagev1(utils.TestCommand): def setUp(self): - super(TestImagev1, self).setUp() + super().setUp() self.app.client_manager.image = FakeImagev1Client( endpoint=fakes.AUTH_URL, @@ -46,6 +46,8 @@ def setUp(self): token=fakes.AUTH_TOKEN, ) + self.client = self.app.client_manager.image + def create_one_image(attrs=None): """Create a fake image. diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 06519800ce..6c65f9a395 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -25,11 +25,7 @@ class TestImage(image_fakes.TestImagev1): - def setUp(self): - super(TestImage, self).setUp() - - self.app.client_manager.image = mock.Mock() - self.client = self.app.client_manager.image + pass class TestImageCreate(TestImage): diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 910bd726ff..a0eda6d23a 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -24,21 +24,26 @@ from openstackclient.tests.unit import utils -class FakeImagev2Client(object): +class FakeImagev2Client: def __init__(self, **kwargs): self.images = mock.Mock() - self.images.resource_class = fakes.FakeResource(None, {}) - self.image_members = mock.Mock() - self.image_members.resource_class = fakes.FakeResource(None, {}) - self.image_tags = mock.Mock() - self.image_tags.resource_class = fakes.FakeResource(None, {}) - + self.create_image = mock.Mock() + self.delete_image = mock.Mock() + self.update_image = mock.Mock() self.find_image = mock.Mock() - self.find_image.resource_class = fakes.FakeResource(None, {}) - self.get_image = mock.Mock() - self.get_image.resource_class = fakes.FakeResource(None, {}) + self.download_image = mock.Mock() + self.reactivate_image = mock.Mock() + self.deactivate_image = mock.Mock() + + self.members = mock.Mock() + self.add_member = mock.Mock() + self.remove_member = mock.Mock() + self.update_member = mock.Mock() + + self.remove_tag = mock.Mock() + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.version = 2.0 @@ -47,7 +52,7 @@ def __init__(self, **kwargs): class TestImagev2(utils.TestCommand): def setUp(self): - super(TestImagev2, self).setUp() + super().setUp() self.app.client_manager.image = FakeImagev2Client( endpoint=fakes.AUTH_URL, diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index f15463738a..510976f7e8 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -32,18 +32,9 @@ class TestImage(image_fakes.TestImagev2): def setUp(self): super(TestImage, self).setUp() - # Get shortcuts to the Mocks in image client - # SDK proxy mock - self.app.client_manager.image = mock.Mock() + # Get shortcuts to mocked image client self.client = self.app.client_manager.image - self.client.remove_member = mock.Mock() - - self.client.create_image = mock.Mock() - self.client.update_image = mock.Mock() - self.image_members_mock = self.app.client_manager.image.image_members - self.image_tags_mock = self.app.client_manager.image.image_tags - # Get shortcut to the Mocks in identity client self.project_mock = self.app.client_manager.identity.projects self.project_mock.reset_mock() @@ -483,11 +474,7 @@ class TestImageList(TestImage): def setUp(self): super(TestImageList, self).setUp() - self.api_mock = mock.Mock() - self.api_mock.side_effect = [ - [self._image], [], - ] - self.client.images = self.api_mock + self.client.images.side_effect = [[self._image], []] # Get the command object to test self.cmd = image.ListImage(self.app, None) @@ -1003,8 +990,10 @@ def test_image_set_no_options(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - - self.image_members_mock.update.assert_not_called() + # we'll have called this but not set anything + self.app.client_manager.image.update_image.called_once_with( + self._image.id, + ) def test_image_set_membership_option_accept(self): membership = image_fakes.create_one_image_member( From 61fac5b79e2e4a4120046b2f830e4745bb383fb3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 17 Nov 2021 11:31:55 +0000 Subject: [PATCH 0457/1120] image: Sanity check the 'SetImage' command This was a very difficult command to grok, due to the layering on of additional features over the years. Make this a little easier to follow by grouping related logic and making use of argparse features. Change-Id: I4e1a0aed09ea5d6a8c26ec3e888c9c7b6cefc25a Signed-off-by: Stephen Finucane --- openstackclient/image/v2/image.py | 90 ++++++++++--------- .../tests/unit/image/v2/test_image.py | 12 +-- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index becb54f456..407c129294 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -1012,17 +1012,26 @@ def get_parser(self, prog_name): membership_group = parser.add_mutually_exclusive_group() membership_group.add_argument( "--accept", - action="store_true", + action="store_const", + const="accepted", + dest="membership", + default=None, help=_("Accept the image membership"), ) membership_group.add_argument( "--reject", - action="store_true", + action="store_const", + const="rejected", + dest="membership", + default=None, help=_("Reject the image membership"), ) membership_group.add_argument( "--pending", - action="store_true", + action="store_const", + const="pending", + dest="membership", + default=None, help=_("Reset the image membership to 'pending'"), ) @@ -1053,6 +1062,43 @@ def take_action(self, parsed_args): _("ERROR: --%s was given, which is an Image v1 option" " that is no longer supported in Image v2") % deadopt) + image = image_client.find_image( + parsed_args.image, ignore_missing=False, + ) + project_id = None + if parsed_args.project: + project_id = common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + + # handle activation status changes + + activation_status = None + if parsed_args.deactivate or parsed_args.activate: + if parsed_args.deactivate: + image_client.deactivate_image(image.id) + activation_status = "deactivated" + if parsed_args.activate: + image_client.reactivate_image(image.id) + activation_status = "activated" + + # handle membership changes + + if parsed_args.membership: + # If a specific project is not passed, assume we want to update + # our own membership + if not project_id: + project_id = self.app.client_manager.auth_ref.project_id + image_client.update_member( + image=image.id, + member=project_id, + status=parsed_args.membership, + ) + + # handle everything else + kwargs = {} copy_attrs = ('architecture', 'container_format', 'disk_format', 'file', 'instance_id', 'kernel_id', 'locations', @@ -1089,48 +1135,12 @@ def take_action(self, parsed_args): kwargs['visibility'] = 'community' if parsed_args.shared: kwargs['visibility'] = 'shared' - project_id = None if parsed_args.project: - project_id = common.find_project( - identity_client, - parsed_args.project, - parsed_args.project_domain, - ).id + # We already did the project lookup above kwargs['owner_id'] = project_id - - image = image_client.find_image(parsed_args.image, - ignore_missing=False) - - # image = utils.find_resource( - # image_client.images, parsed_args.image) - - activation_status = None - if parsed_args.deactivate: - image_client.deactivate_image(image.id) - activation_status = "deactivated" - if parsed_args.activate: - image_client.reactivate_image(image.id) - activation_status = "activated" - - membership_group_args = ('accept', 'reject', 'pending') - membership_status = [status for status in membership_group_args - if getattr(parsed_args, status)] - if membership_status: - # If a specific project is not passed, assume we want to update - # our own membership - if not project_id: - project_id = self.app.client_manager.auth_ref.project_id - # The mutually exclusive group of the arg parser ensure we have at - # most one item in the membership_status list. - if membership_status[0] != 'pending': - membership_status[0] += 'ed' # Glance expects the past form - image_client.update_member( - image=image.id, member=project_id, status=membership_status[0]) - if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) - if parsed_args.hidden is not None: kwargs['is_hidden'] = parsed_args.hidden diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 510976f7e8..7ccc9f0fb4 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1007,9 +1007,7 @@ def test_image_set_membership_option_accept(self): self._image.id, ] verifylist = [ - ('accept', True), - ('reject', False), - ('pending', False), + ('membership', 'accepted'), ('image', self._image.id) ] @@ -1038,9 +1036,7 @@ def test_image_set_membership_option_reject(self): '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ - ('accept', False), - ('reject', True), - ('pending', False), + ('membership', 'rejected'), ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c') ] @@ -1069,9 +1065,7 @@ def test_image_set_membership_option_pending(self): '0f41529e-7c12-4de8-be2d-181abb825b3c', ] verifylist = [ - ('accept', False), - ('reject', False), - ('pending', True), + ('membership', 'pending'), ('image', '0f41529e-7c12-4de8-be2d-181abb825b3c') ] From b3d09ffc37f6b577ce479a5203c6c882062fee74 Mon Sep 17 00:00:00 2001 From: JieonLee Date: Sun, 21 Nov 2021 05:05:25 +0000 Subject: [PATCH 0458/1120] Add missing command mapping in nova nova command: instance-action openstack command: server event show Change-Id: I8e5dad90cfd28b1f0d65be688651918869f679e4 --- doc/source/cli/data/nova.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/cli/data/nova.csv b/doc/source/cli/data/nova.csv index dd8992f68c..0ab2b2fee4 100644 --- a/doc/source/cli/data/nova.csv +++ b/doc/source/cli/data/nova.csv @@ -46,7 +46,7 @@ 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. image-create,server image create,Create a new image by taking a snapshot of a running server. -instance-action,,Show an action. +instance-action,server event show,Show an action. instance-action-list,,List actions on a server. instance-usage-audit-log,,List/Get server usage audits. interface-attach,server add port / server add floating ip / server add fixed ip,Attach a network interface to a server. From 3078a0a121743c387d83d7f27ce3d8fd8fbb4ccf Mon Sep 17 00:00:00 2001 From: Diwei Zhu Date: Thu, 28 Oct 2021 23:25:52 +0000 Subject: [PATCH 0459/1120] Switch command server add volume to sdk. File tests.unit.volume.v2.fakes is modified to provide sdk volume fakes. File tests.unit.compute.v2.fakes is modified to provide sdk volume attachment fakes. For test, setup_sdk_volumes_mock() method is created so that volumes are created in similar way as servers are created. Change-Id: I290ba83b6ba27a1377ab73fd0ae06ecced25efd1 --- openstackclient/compute/v2/server.py | 38 ++- .../tests/unit/compute/v2/fakes.py | 56 ++++ .../tests/unit/compute/v2/test_server.py | 258 +++++++++--------- openstackclient/tests/unit/volume/v2/fakes.py | 110 ++++++-- ...er-add-volume-to-sdk-685e036a88839651.yaml | 4 + 5 files changed, 296 insertions(+), 170 deletions(-) create mode 100644 releasenotes/notes/migrate-server-add-volume-to-sdk-685e036a88839651.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 08627f9bc5..18c1197cc9 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -548,24 +548,25 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.sdk_connection.compute + volume_client = self.app.client_manager.sdk_connection.volume - server = utils.find_resource( - compute_client.servers, + server = compute_client.find_server( parsed_args.server, + ignore_missing=False, ) - volume = utils.find_resource( - volume_client.volumes, + volume = volume_client.find_volume( parsed_args.volume, + ignore_missing=False, ) kwargs = { + "volumeId": volume.id, "device": parsed_args.device } if parsed_args.tag: - if compute_client.api_version < api_versions.APIVersion('2.49'): + if not sdk_utils.supports_microversion(compute_client, '2.49'): msg = _( '--os-compute-api-version 2.49 or greater is required to ' 'support the --tag option' @@ -575,7 +576,7 @@ def take_action(self, parsed_args): kwargs['tag'] = parsed_args.tag if parsed_args.enable_delete_on_termination: - if compute_client.api_version < api_versions.APIVersion('2.79'): + if not sdk_utils.supports_microversion(compute_client, '2.79'): msg = _( '--os-compute-api-version 2.79 or greater is required to ' 'support the --enable-delete-on-termination option.' @@ -585,7 +586,7 @@ def take_action(self, parsed_args): kwargs['delete_on_termination'] = True if parsed_args.disable_delete_on_termination: - if compute_client.api_version < api_versions.APIVersion('2.79'): + if not sdk_utils.supports_microversion(compute_client, '2.79'): msg = _( '--os-compute-api-version 2.79 or greater is required to ' 'support the --disable-delete-on-termination option.' @@ -594,28 +595,23 @@ def take_action(self, parsed_args): kwargs['delete_on_termination'] = False - volume_attachment = compute_client.volumes.create_server_volume( - server.id, - volume.id, - **kwargs + volume_attachment = compute_client.create_volume_attachment( + server, + **kwargs, ) - columns = ('id', 'serverId', 'volumeId', 'device') + columns = ('id', 'server id', 'volume id', 'device') column_headers = ('ID', 'Server ID', 'Volume ID', 'Device') - if compute_client.api_version >= api_versions.APIVersion('2.49'): + if sdk_utils.supports_microversion(compute_client, '2.49'): columns += ('tag',) column_headers += ('Tag',) - if compute_client.api_version >= api_versions.APIVersion('2.79'): + if sdk_utils.supports_microversion(compute_client, '2.79'): columns += ('delete_on_termination',) column_headers += ('Delete On Termination',) return ( column_headers, - utils.get_item_properties( - volume_attachment, - columns, - mixed_case_fields=('serverId', 'volumeId'), - ) + utils.get_item_properties(volume_attachment, columns,) ) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 23468ebc4d..7618c229c5 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -21,6 +21,7 @@ from novaclient import api_versions from openstack.compute.v2 import flavor as _flavor from openstack.compute.v2 import server +from openstack.compute.v2 import volume_attachment from openstackclient.api import compute_v2 from openstackclient.tests.unit import fakes @@ -1803,3 +1804,58 @@ def create_volume_attachments(attrs=None, methods=None, count=2): attrs, methods)) return volume_attachments + + @staticmethod + def create_one_sdk_volume_attachment(attrs=None, methods=None): + """Create a fake sdk VolumeAttachment. + + :param dict attrs: + A dictionary with all attributes + :param dict methods: + A dictionary with all methods + :return: + A fake VolumeAttachment object, with id, device, and so on + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + volume_attachment_info = { + "id": uuid.uuid4().hex, + "device": "/dev/sdb", + "server_id": uuid.uuid4().hex, + "volume_id": uuid.uuid4().hex, + # introduced in API microversion 2.70 + "tag": "foo", + # introduced in API microversion 2.79 + "delete_on_termination": True, + # introduced in API microversion 2.89 + "attachment_id": uuid.uuid4().hex, + "bdm_uuid": uuid.uuid4().hex + } + + # Overwrite default attributes. + volume_attachment_info.update(attrs) + + return volume_attachment.VolumeAttachment(**volume_attachment_info) + + @staticmethod + def create_sdk_volume_attachments(attrs=None, methods=None, count=2): + """Create multiple fake VolumeAttachment objects (BDMs). + + :param dict attrs: + A dictionary with all attributes + :param dict methods: + A dictionary with all methods + :param int count: + The number of volume attachments to fake + :return: + A list of VolumeAttachment objects faking the volume attachments. + """ + volume_attachments = [] + for i in range(0, count): + volume_attachments.append( + FakeVolumeAttachment.create_one_sdk_volume_attachment( + attrs, methods)) + + return volume_attachments diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 9623cb0abf..203e47ebb3 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -105,6 +105,9 @@ def setUp(self): self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() + self.app.client_manager.sdk_connection.volume = mock.Mock() + self.sdk_volume_client = self.app.client_manager.sdk_connection.volume + # Get a shortcut to the volume client VolumeManager Mock self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock.reset_mock() @@ -146,13 +149,18 @@ def setup_sdk_servers_mock(self, count): ) # This is the return value for compute_client.find_server() - self.sdk_client.find_server = compute_fakes.FakeServer.get_servers( - servers, - 0, - ) + self.sdk_client.find_server.side_effect = servers return servers + def setup_sdk_volumes_mock(self, count): + volumes = volume_fakes.FakeVolume.create_sdk_volumes(count=count) + + # This is the return value for volume_client.find_volume() + self.sdk_volume_client.find_volume.side_effect = volumes + + return volumes + def run_method_with_servers(self, method_name, server_count): servers = self.setup_servers_mock(server_count) @@ -680,31 +688,38 @@ class TestServerVolume(TestServer): def setUp(self): super(TestServerVolume, self).setUp() - self.volume = volume_fakes.FakeVolume.create_one_volume() - self.volumes_mock.get.return_value = self.volume - self.methods = { - 'create_server_volume': None, + 'create_volume_attachment': None, } # Get the command object to test self.cmd = server.AddServerVolume(self.app, None) - def test_server_add_volume(self): - servers = self.setup_servers_mock(count=1) - volume_attachment = \ - compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() - self.servers_volumes_mock.create_server_volume.return_value = \ - volume_attachment + self.servers = self.setup_sdk_servers_mock(count=1) + self.volumes = self.setup_sdk_volumes_mock(count=1) + + attrs = { + 'server_id': self.servers[0].id, + 'volume_id': self.volumes[0].id, + } + self.volume_attachment = \ + compute_fakes.FakeVolumeAttachment.\ + create_one_sdk_volume_attachment(attrs=attrs) + + self.sdk_client.create_volume_attachment.return_value = \ + self.volume_attachment + + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_server_add_volume(self, sm_mock): arglist = [ '--device', '/dev/sdb', - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('device', '/dev/sdb'), ] @@ -712,39 +727,36 @@ def test_server_add_volume(self): expected_columns = ('ID', 'Server ID', 'Volume ID', 'Device') expected_data = ( - volume_attachment.id, - volume_attachment.serverId, - volume_attachment.volumeId, - volume_attachment.device, + self.volume_attachment.id, + self.volume_attachment.server_id, + self.volume_attachment.volume_id, + '/dev/sdb', ) columns, data = self.cmd.take_action(parsed_args) - self.servers_volumes_mock.create_server_volume.assert_called_once_with( - servers[0].id, self.volume.id, device='/dev/sdb') self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) + self.sdk_client.create_volume_attachment.assert_called_once_with( + self.servers[0], volumeId=self.volumes[0].id, device='/dev/sdb') - def test_server_add_volume_with_tag(self): - # requires API 2.49 or later - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.49') - - servers = self.setup_servers_mock(count=1) - volume_attachment = \ - compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() - self.servers_volumes_mock.create_server_volume.return_value = \ - volume_attachment + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_volume_with_tag(self, sm_mock): + def side_effect(compute_client, version): + if version == '2.49': + return True + return False + sm_mock.side_effect = side_effect arglist = [ '--device', '/dev/sdb', '--tag', 'foo', - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('device', '/dev/sdb'), ('tag', 'foo'), ] @@ -753,33 +765,33 @@ def test_server_add_volume_with_tag(self): expected_columns = ('ID', 'Server ID', 'Volume ID', 'Device', 'Tag') expected_data = ( - volume_attachment.id, - volume_attachment.serverId, - volume_attachment.volumeId, - volume_attachment.device, - volume_attachment.tag, + self.volume_attachment.id, + self.volume_attachment.server_id, + self.volume_attachment.volume_id, + self.volume_attachment.device, + self.volume_attachment.tag, ) columns, data = self.cmd.take_action(parsed_args) - self.servers_volumes_mock.create_server_volume.assert_called_once_with( - servers[0].id, self.volume.id, device='/dev/sdb', tag='foo') self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) + self.sdk_client.create_volume_attachment.assert_called_once_with( + self.servers[0], + volumeId=self.volumes[0].id, + device='/dev/sdb', + tag='foo') - def test_server_add_volume_with_tag_pre_v249(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.48') - - servers = self.setup_servers_mock(count=1) + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) + def test_server_add_volume_with_tag_pre_v249(self, sm_mock): arglist = [ - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, '--tag', 'foo', ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('tag', 'foo'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -792,26 +804,22 @@ def test_server_add_volume_with_tag_pre_v249(self): '--os-compute-api-version 2.49 or greater is required', str(ex)) - def test_server_add_volume_with_enable_delete_on_termination(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.79') - - servers = self.setup_servers_mock(count=1) - volume_attachment = \ - compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() - self.servers_volumes_mock.create_server_volume.return_value = \ - volume_attachment - + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_server_add_volume_with_enable_delete_on_termination( + self, + sm_mock, + ): + self.volume_attachment.delete_on_termination = True arglist = [ '--enable-delete-on-termination', '--device', '/dev/sdb', - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('device', '/dev/sdb'), ('enable_delete_on_termination', True), ] @@ -826,42 +834,40 @@ def test_server_add_volume_with_enable_delete_on_termination(self): 'Delete On Termination', ) expected_data = ( - volume_attachment.id, - volume_attachment.serverId, - volume_attachment.volumeId, - volume_attachment.device, - volume_attachment.tag, - volume_attachment.delete_on_termination, + self.volume_attachment.id, + self.volume_attachment.server_id, + self.volume_attachment.volume_id, + self.volume_attachment.device, + self.volume_attachment.tag, + self.volume_attachment.delete_on_termination, ) columns, data = self.cmd.take_action(parsed_args) - - self.servers_volumes_mock.create_server_volume.assert_called_once_with( - servers[0].id, self.volume.id, - device='/dev/sdb', delete_on_termination=True) self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) + self.sdk_client.create_volume_attachment.assert_called_once_with( + self.servers[0], + volumeId=self.volumes[0].id, + device='/dev/sdb', + delete_on_termination=True) - def test_server_add_volume_with_disable_delete_on_termination(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.79') - - servers = self.setup_servers_mock(count=1) - volume_attachment = \ - compute_fakes.FakeVolumeAttachment.create_one_volume_attachment() - self.servers_volumes_mock.create_server_volume.return_value = \ - volume_attachment + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) + def test_server_add_volume_with_disable_delete_on_termination( + self, + sm_mock, + ): + self.volume_attachment.delete_on_termination = False arglist = [ '--disable-delete-on-termination', '--device', '/dev/sdb', - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('device', '/dev/sdb'), ('disable_delete_on_termination', True), ] @@ -876,37 +882,43 @@ def test_server_add_volume_with_disable_delete_on_termination(self): 'Delete On Termination', ) expected_data = ( - volume_attachment.id, - volume_attachment.serverId, - volume_attachment.volumeId, - volume_attachment.device, - volume_attachment.tag, - volume_attachment.delete_on_termination, + self.volume_attachment.id, + self.volume_attachment.server_id, + self.volume_attachment.volume_id, + self.volume_attachment.device, + self.volume_attachment.tag, + self.volume_attachment.delete_on_termination, ) columns, data = self.cmd.take_action(parsed_args) - self.servers_volumes_mock.create_server_volume.assert_called_once_with( - servers[0].id, self.volume.id, - device='/dev/sdb', delete_on_termination=False) self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) + self.sdk_client.create_volume_attachment.assert_called_once_with( + self.servers[0], + volumeId=self.volumes[0].id, + device='/dev/sdb', + delete_on_termination=False) + @mock.patch.object(sdk_utils, 'supports_microversion') def test_server_add_volume_with_enable_delete_on_termination_pre_v279( self, + sm_mock, ): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.78') + def side_effect(compute_client, version): + if version == '2.79': + return False + return True + sm_mock.side_effect = side_effect - servers = self.setup_servers_mock(count=1) arglist = [ - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, '--enable-delete-on-termination', ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('enable_delete_on_termination', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -917,21 +929,25 @@ def test_server_add_volume_with_enable_delete_on_termination_pre_v279( self.assertIn('--os-compute-api-version 2.79 or greater is required', str(ex)) + @mock.patch.object(sdk_utils, 'supports_microversion') def test_server_add_volume_with_disable_delete_on_termination_pre_v279( self, + sm_mock, ): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.78') + def side_effect(compute_client, version): + if version == '2.79': + return False + return True + sm_mock.side_effect = side_effect - servers = self.setup_servers_mock(count=1) arglist = [ - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, '--disable-delete-on-termination', ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('disable_delete_on_termination', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -942,24 +958,22 @@ def test_server_add_volume_with_disable_delete_on_termination_pre_v279( self.assertIn('--os-compute-api-version 2.79 or greater is required', str(ex)) + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=True) def test_server_add_volume_with_disable_and_enable_delete_on_termination( self, + sm_mock, ): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.79') - - servers = self.setup_servers_mock(count=1) arglist = [ '--enable-delete-on-termination', '--disable-delete-on-termination', '--device', '/dev/sdb', - servers[0].id, - self.volume.id, + self.servers[0].id, + self.volumes[0].id, ] verifylist = [ - ('server', servers[0].id), - ('volume', self.volume.id), + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), ('device', '/dev/sdb'), ('enable_delete_on_termination', True), ('disable_delete_on_termination', True), diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index b5f66d4b1d..96e381d3cc 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -18,6 +18,7 @@ import uuid from cinderclient import api_versions +from openstack.block_storage.v3 import volume from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes @@ -46,7 +47,7 @@ class FakeTransfer(object): def create_one_transfer(attrs=None): """Create a fake transfer. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of Transfer Request :return: A FakeResource object with volume_id, name, id. @@ -75,7 +76,7 @@ def create_one_transfer(attrs=None): def create_transfers(attrs=None, count=2): """Create multiple fake transfers. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of transfer :param Integer count: The number of transfers to be faked @@ -116,7 +117,7 @@ class FakeTypeAccess(object): def create_one_type_access(attrs=None): """Create a fake volume type access for project. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object, with Volume_type_ID and Project_ID. @@ -148,7 +149,7 @@ class FakeService(object): def create_one_service(attrs=None): """Create a fake service. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of service :return: A FakeResource object with host, status, etc. @@ -180,7 +181,7 @@ def create_one_service(attrs=None): def create_services(attrs=None, count=2): """Create multiple fake services. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of service :param Integer count: The number of services to be faked @@ -201,7 +202,7 @@ class FakeCapability(object): def create_one_capability(attrs=None): """Create a fake volume backend capability. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of the Capabilities. :return: A FakeResource object with capability name and attrs. @@ -260,7 +261,7 @@ class FakePool(object): def create_one_pool(attrs=None): """Create a fake pool. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of the pool :return: A FakeResource object with pool name and attrs. @@ -362,7 +363,7 @@ class FakeVolume(object): def create_one_volume(attrs=None): """Create a fake volume. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of volume :return: A FakeResource object with id, name, status, etc. @@ -405,7 +406,7 @@ def create_one_volume(attrs=None): def create_volumes(attrs=None, count=2): """Create multiple fake volumes. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes of volume :param Integer count: The number of volumes to be faked @@ -418,6 +419,61 @@ def create_volumes(attrs=None, count=2): 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 + 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. @@ -484,7 +540,7 @@ class FakeAvailabilityZone(object): def create_one_availability_zone(attrs=None): """Create a fake AZ. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with zoneName, zoneState, etc. @@ -509,7 +565,7 @@ def create_one_availability_zone(attrs=None): def create_availability_zones(attrs=None, count=2): """Create multiple fake AZs. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of AZs to fake @@ -532,7 +588,7 @@ class FakeBackup(object): def create_one_backup(attrs=None): """Create a fake backup. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with id, name, volume_id, etc. @@ -565,7 +621,7 @@ def create_one_backup(attrs=None): def create_backups(attrs=None, count=2): """Create multiple fake backups. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of backups to fake @@ -636,7 +692,7 @@ class FakeConsistencyGroup(object): def create_one_consistency_group(attrs=None): """Create a fake consistency group. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with id, name, description, etc. @@ -666,7 +722,7 @@ def create_one_consistency_group(attrs=None): def create_consistency_groups(attrs=None, count=2): """Create multiple fake consistency groups. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of consistency groups to fake @@ -713,7 +769,7 @@ class FakeConsistencyGroupSnapshot(object): def create_one_consistency_group_snapshot(attrs=None): """Create a fake consistency group snapshot. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with id, name, description, etc. @@ -742,7 +798,7 @@ def create_one_consistency_group_snapshot(attrs=None): def create_consistency_group_snapshots(attrs=None, count=2): """Create multiple fake consistency group snapshots. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of consistency group snapshots to fake @@ -789,7 +845,7 @@ class FakeExtension(object): def create_one_extension(attrs=None): """Create a fake extension. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with name, namespace, etc. @@ -825,7 +881,7 @@ class FakeQos(object): def create_one_qos(attrs=None): """Create a fake Qos specification. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with id, name, consumer, etc. @@ -852,7 +908,7 @@ def create_one_qos(attrs=None): def create_one_qos_association(attrs=None): """Create a fake Qos specification association. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with id, name, association_type, etc. @@ -878,7 +934,7 @@ def create_one_qos_association(attrs=None): def create_qoses(attrs=None, count=2): """Create multiple fake Qos specifications. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of Qos specifications to fake @@ -920,7 +976,7 @@ class FakeSnapshot(object): def create_one_snapshot(attrs=None): """Create a fake snapshot. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with id, name, description, etc. @@ -951,7 +1007,7 @@ def create_one_snapshot(attrs=None): def create_snapshots(attrs=None, count=2): """Create multiple fake snapshots. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of snapshots to fake @@ -993,9 +1049,9 @@ class FakeVolumeType(object): def create_one_volume_type(attrs=None, methods=None): """Create a fake volume type. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes - :param Dictionary methods: + :param dict methods: A dictionary with all methods :return: A FakeResource object with id, name, description, etc. @@ -1025,7 +1081,7 @@ def create_one_volume_type(attrs=None, methods=None): def create_volume_types(attrs=None, count=2): """Create multiple fake volume_types. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :param int count: The number of types to fake @@ -1063,7 +1119,7 @@ def get_volume_types(volume_types=None, count=2): def create_one_encryption_volume_type(attrs=None): """Create a fake encryption volume type. - :param Dictionary attrs: + :param dict attrs: A dictionary with all attributes :return: A FakeResource object with volume_type_id etc. diff --git a/releasenotes/notes/migrate-server-add-volume-to-sdk-685e036a88839651.yaml b/releasenotes/notes/migrate-server-add-volume-to-sdk-685e036a88839651.yaml new file mode 100644 index 0000000000..54abdacbbc --- /dev/null +++ b/releasenotes/notes/migrate-server-add-volume-to-sdk-685e036a88839651.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Migrate openstack server add volume to using sdk. From 860d6360474b2f215097d1aa4018a57070e44924 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Wed, 24 Nov 2021 06:46:22 +0100 Subject: [PATCH 0460/1120] Temporarily drop aodhclient from doc build Building plugin documentation is failing for aodhclient when running with latest pyparsing. Drop the plugin from docs for now until the issue can be fixed. Needed-By: https://review.opendev.org/c/openstack/requirements/+/818614/ Signed-off-by: Dr. Jens Harbott Change-Id: I1cc1efd9ff2004dd711ed9da0b1d9e8be31175f4 --- doc/source/cli/plugin-commands/aodh.rst | 4 ---- doc/source/cli/plugin-commands/index.rst | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 doc/source/cli/plugin-commands/aodh.rst diff --git a/doc/source/cli/plugin-commands/aodh.rst b/doc/source/cli/plugin-commands/aodh.rst deleted file mode 100644 index 5d8b4332cf..0000000000 --- a/doc/source/cli/plugin-commands/aodh.rst +++ /dev/null @@ -1,4 +0,0 @@ -aodh ----- - -.. autoprogram-cliff:: openstack.alarming.v2 diff --git a/doc/source/cli/plugin-commands/index.rst b/doc/source/cli/plugin-commands/index.rst index 4e1ce54b13..638dcbe560 100644 --- a/doc/source/cli/plugin-commands/index.rst +++ b/doc/source/cli/plugin-commands/index.rst @@ -7,7 +7,6 @@ Plugin Commands .. toctree:: :maxdepth: 1 - aodh barbican designate gnocchi @@ -29,6 +28,10 @@ Plugin Commands .. TODO(efried): Make pages for the following once they're fixed. +.. aodh +.. # aodhclient docs build is failing with recent pyparsing +.. # autoprogram-cliff:: openstack.alarming.v2 + .. cue .. # cueclient is not in global-requirements .. # list-plugins:: openstack.mb.v1 From 28cd5763de8706fb997bdd53deaf94aca0de5c52 Mon Sep 17 00:00:00 2001 From: Diwei Zhu Date: Fri, 26 Nov 2021 14:11:02 +0000 Subject: [PATCH 0461/1120] Add functional test for server add/remove volume. Change-Id: I86a76f32790cafcff1d94364fb72f8890a8cb025 --- .../functional/compute/v2/test_server.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 59b1fad5f9..0d07950e97 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -1119,3 +1119,62 @@ def test_server_add_remove_network_port(self): self.openstack('server delete ' + name) self.openstack('port delete ' + port_name) + + def test_server_add_remove_volume(self): + volume_wait_for = volume_common.BaseVolumeTests.wait_for_status + + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'server create -f json ' + + '--network private ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--wait ' + + name + )) + + self.assertIsNotNone(cmd_output['id']) + self.assertEqual(name, cmd_output['name']) + self.addCleanup(self.openstack, 'server delete --wait ' + name) + server_id = cmd_output['id'] + + volume_name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + + self.assertIsNotNone(cmd_output['id']) + self.assertEqual(volume_name, cmd_output['name']) + volume_wait_for('volume', volume_name, 'available') + self.addCleanup(self.openstack, 'volume delete ' + volume_name) + volume_id = cmd_output['id'] + + cmd_output = json.loads(self.openstack( + 'server add volume -f json ' + + name + ' ' + + volume_name + ' ' + + '--tag bar' + )) + + self.assertIsNotNone(cmd_output['ID']) + self.assertEqual(server_id, cmd_output['Server ID']) + self.assertEqual(volume_id, cmd_output['Volume ID']) + volume_attachment_id = cmd_output['ID'] + + cmd_output = json.loads(self.openstack( + 'server volume list -f json ' + + name + )) + + self.assertEqual(volume_attachment_id, cmd_output[0]['ID']) + self.assertEqual(server_id, cmd_output[0]['Server ID']) + self.assertEqual(volume_id, cmd_output[0]['Volume ID']) + + volume_wait_for('volume', volume_name, 'in-use') + self.openstack('server remove volume ' + name + ' ' + volume_name) + volume_wait_for('volume', volume_name, 'available') + + raw_output = self.openstack('server volume list ' + name) + self.assertEqual('\n', raw_output) From fae293dd5218cf4ea03d0a4c44d17b97987dea12 Mon Sep 17 00:00:00 2001 From: Diwei Zhu Date: Tue, 16 Nov 2021 19:08:58 +0000 Subject: [PATCH 0462/1120] Switch command server remove volume to sdk Change-Id: If6f6cf93b55a67e767c54de8ce21f25252cf99ca --- openstackclient/compute/v2/server.py | 27 ++++++----- .../tests/unit/compute/v2/test_server.py | 45 +++++++++++++++++-- ...remove-volume-to-sdk-47e9befd2672dcdf.yaml | 4 ++ 3 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/switch-server-remove-volume-to-sdk-47e9befd2672dcdf.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 18c1197cc9..121a7b82e8 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3793,22 +3793,29 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.sdk_connection.compute + volume_client = self.app.client_manager.sdk_connection.volume - server = utils.find_resource( - compute_client.servers, + server = compute_client.find_server( parsed_args.server, + ignore_missing=False, ) - volume = utils.find_resource( - volume_client.volumes, + volume = volume_client.find_volume( parsed_args.volume, + ignore_missing=False, ) - compute_client.volumes.delete_server_volume( - server.id, - volume.id, - ) + volume_attachments = compute_client.volume_attachments(server) + for volume_attachment in volume_attachments: + if volume_attachment.volume_id == volume.id: + compute_client.delete_volume_attachment( + volume_attachment, + server, + ) + break + else: + msg = _('Target volume attachment not found.') + raise exceptions.CommandError(msg) class RescueServer(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 203e47ebb3..10ea07adb3 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -692,9 +692,6 @@ def setUp(self): 'create_volume_attachment': None, } - # Get the command object to test - self.cmd = server.AddServerVolume(self.app, None) - self.servers = self.setup_sdk_servers_mock(count=1) self.volumes = self.setup_sdk_volumes_mock(count=1) @@ -709,6 +706,15 @@ def setUp(self): self.sdk_client.create_volume_attachment.return_value = \ self.volume_attachment + +class TestServerAddVolume(TestServerVolume): + + def setUp(self): + super(TestServerAddVolume, self).setUp() + + # Get the command object to test + self.cmd = server.AddServerVolume(self.app, None) + @mock.patch.object(sdk_utils, 'supports_microversion', return_value=False) def test_server_add_volume(self, sm_mock): @@ -985,6 +991,39 @@ def test_server_add_volume_with_disable_and_enable_delete_on_termination( 'with argument --enable-delete-on-termination', str(ex)) +class TestServerRemoveVolume(TestServerVolume): + + def setUp(self): + super(TestServerRemoveVolume, self).setUp() + + # Get the command object to test + self.cmd = server.RemoveServerVolume(self.app, None) + + def test_server_remove_volume(self): + self.sdk_client.volume_attachments.return_value = [ + self.volume_attachment + ] + + arglist = [ + self.servers[0].id, + self.volumes[0].id, + ] + + verifylist = [ + ('server', self.servers[0].id), + ('volume', self.volumes[0].id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.sdk_client.delete_volume_attachment.assert_called_once_with( + self.volume_attachment, + self.servers[0]) + + class TestServerAddNetwork(TestServer): def setUp(self): diff --git a/releasenotes/notes/switch-server-remove-volume-to-sdk-47e9befd2672dcdf.yaml b/releasenotes/notes/switch-server-remove-volume-to-sdk-47e9befd2672dcdf.yaml new file mode 100644 index 0000000000..3e0397d789 --- /dev/null +++ b/releasenotes/notes/switch-server-remove-volume-to-sdk-47e9befd2672dcdf.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Switch command server remove volume to using sdk. From f4629331134c40599f9baf908a391460b74d2767 Mon Sep 17 00:00:00 2001 From: Slawek Kaplonski Date: Tue, 23 Nov 2021 21:56:56 +0100 Subject: [PATCH 0463/1120] Allow unset port's host_id It is supported by Neutron and needs to be done like that when e.g. admin wants to unbound port from the host. Task: #44043 Story: #2009705 Change-Id: I08f1bb40f4dc72cfa7c62feeb5f513455de0ca45 --- openstackclient/network/v2/port.py | 8 ++++++++ openstackclient/tests/unit/network/v2/test_port.py | 5 ++++- .../add-option-to-unset-port-host-c76de9b1d2addf9a.yaml | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-option-to-unset-port-host-c76de9b1d2addf9a.yaml diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 132c384a0e..8f79b80b0b 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -969,6 +969,12 @@ def get_parser(self, prog_name): action='store_true', help=_("Clear existing NUMA affinity policy") ) + parser.add_argument( + '--host', + action='store_true', + default=False, + help=_("Clear host binding for the port.") + ) _tag.add_tag_option_to_parser_for_unset(parser, _('port')) @@ -1026,6 +1032,8 @@ def take_action(self, parsed_args): attrs['data_plane_status'] = None if parsed_args.numa_policy: attrs['numa_affinity_policy'] = None + if parsed_args.host: + attrs['binding:host_id'] = None attrs.update( self._parse_extra_properties(parsed_args.extra_properties)) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 5f2a12836a..c8540ba057 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -1923,6 +1923,7 @@ def test_unset_port_parameters(self): 'subnet=042eb10a-3a18-4658-ab-cf47c8d03152,ip-address=1.0.0.0', '--binding-profile', 'Superman', '--qos-policy', + '--host', self._testport.name, ] verifylist = [ @@ -1931,6 +1932,7 @@ def test_unset_port_parameters(self): 'ip-address': '1.0.0.0'}]), ('binding_profile', ['Superman']), ('qos_policy', True), + ('host', True) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1941,7 +1943,8 @@ def test_unset_port_parameters(self): 'subnet_id': '042eb10a-3a18-4658-ab-cf47c8d03152', 'ip_address': '0.0.0.1'}], 'binding:profile': {'batman': 'Joker'}, - 'qos_policy_id': None + 'qos_policy_id': None, + 'binding:host_id': None } self.network.update_port.assert_called_once_with( self._testport, **attrs) diff --git a/releasenotes/notes/add-option-to-unset-port-host-c76de9b1d2addf9a.yaml b/releasenotes/notes/add-option-to-unset-port-host-c76de9b1d2addf9a.yaml new file mode 100644 index 0000000000..0aa0476050 --- /dev/null +++ b/releasenotes/notes/add-option-to-unset-port-host-c76de9b1d2addf9a.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add possibility to unbind Neutron's port from the host by unsetting its + host_id. From f82afc7f379daebd1994d9133eff801f790c0d32 Mon Sep 17 00:00:00 2001 From: Diwei Zhu Date: Sun, 14 Nov 2021 22:52:14 +0000 Subject: [PATCH 0464/1120] Switch openstack server remove port/network to using sdk Change-Id: I1540c1f52e9a107dba20eeea9dc323c5510fe2b1 --- openstackclient/compute/v2/server.py | 25 +++-- .../functional/compute/v2/test_server.py | 94 ++++++++++++++++--- .../tests/unit/compute/v2/test_server.py | 19 ++-- ...-network-port-to-sdk-829ba711e0e198d5.yaml | 4 + 4 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 releasenotes/notes/switch-server-remove-network-port-to-sdk-829ba711e0e198d5.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 18c1197cc9..dfb4dba8fe 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3690,10 +3690,10 @@ 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 - server = utils.find_resource( - compute_client.servers, parsed_args.server) + server = compute_client.find_server( + parsed_args.server, ignore_missing=False) if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network @@ -3702,7 +3702,11 @@ def take_action(self, parsed_args): else: port_id = parsed_args.port - server.interface_detach(port_id) + compute_client.delete_server_interface( + port_id, + server=server, + ignore_missing=False, + ) class RemoveNetwork(command.Command): @@ -3723,10 +3727,10 @@ 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 - server = utils.find_resource( - compute_client.servers, parsed_args.server) + server = compute_client.find_server( + parsed_args.server, ignore_missing=False) if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network @@ -3735,9 +3739,12 @@ def take_action(self, parsed_args): else: net_id = parsed_args.network - for inf in server.interface_list(): + for inf in compute_client.server_interfaces(server): if inf.net_id == net_id: - server.interface_detach(inf.port_id) + compute_client.delete_server_interface( + inf.port_id, + server=server, + ) class RemoveServerSecurityGroup(command.Command): diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 0d07950e97..cf4bcbc2a9 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -1072,7 +1072,7 @@ def test_server_create_with_empty_network_option_latest(self): self.assertNotIn('nics are required after microversion 2.36', e.stderr) - def test_server_add_remove_network_port(self): + def test_server_add_remove_network(self): name = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'server create -f json ' + @@ -1085,18 +1085,63 @@ def test_server_add_remove_network_port(self): self.assertIsNotNone(cmd_output['id']) self.assertEqual(name, cmd_output['name']) + self.addCleanup(self.openstack, 'server delete --wait ' + name) + # add network and check 'public' is in server show self.openstack( 'server add network ' + name + ' public') + wait_time = 0 + while wait_time < 60: + cmd_output = json.loads(self.openstack( + 'server show -f json ' + name + )) + if 'public' not in cmd_output['addresses']: + # Hang out for a bit and try again + print('retrying add network check') + wait_time += 10 + time.sleep(10) + else: + break + addresses = cmd_output['addresses'] + self.assertIn('public', addresses) + + # remove network and check 'public' is not in server show + self.openstack('server remove network ' + name + ' public') + + wait_time = 0 + while wait_time < 60: + cmd_output = json.loads(self.openstack( + 'server show -f json ' + name + )) + if 'public' in cmd_output['addresses']: + # Hang out for a bit and try again + print('retrying remove network check') + wait_time += 10 + time.sleep(10) + else: + break + + addresses = cmd_output['addresses'] + self.assertNotIn('public', addresses) + + def test_server_add_remove_port(self): + name = uuid.uuid4().hex cmd_output = json.loads(self.openstack( - 'server show -f json ' + name + 'server create -f json ' + + '--network private ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--wait ' + + name )) - addresses = cmd_output['addresses'] - self.assertIn('public', addresses) + self.assertIsNotNone(cmd_output['id']) + self.assertEqual(name, cmd_output['name']) + self.addCleanup(self.openstack, 'server delete --wait ' + name) - port_name = 'test-port' + # create port, record one of its ip address + port_name = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'port list -f json' @@ -1108,17 +1153,44 @@ def test_server_add_remove_network_port(self): '--network private ' + port_name )) self.assertIsNotNone(cmd_output['id']) + ip_address = cmd_output['fixed_ips'][0]['ip_address'] + self.addCleanup(self.openstack, 'port delete ' + port_name) + # add port to server, assert the ip address of the port appears self.openstack('server add port ' + name + ' ' + port_name) - cmd_output = json.loads(self.openstack( - 'server show -f json ' + name - )) + wait_time = 0 + while wait_time < 60: + cmd_output = json.loads(self.openstack( + 'server show -f json ' + name + )) + if ip_address not in cmd_output['addresses']['private']: + # Hang out for a bit and try again + print('retrying add port check') + wait_time += 10 + time.sleep(10) + else: + break + addresses = cmd_output['addresses']['private'] + self.assertIn(ip_address, addresses) - # TODO(diwei): test remove network/port after the commands are switched + # remove port, assert the ip address of the port doesn't appear + self.openstack('server remove port ' + name + ' ' + port_name) - self.openstack('server delete ' + name) - self.openstack('port delete ' + port_name) + wait_time = 0 + while wait_time < 60: + cmd_output = json.loads(self.openstack( + 'server show -f json ' + name + )) + if ip_address in cmd_output['addresses']['private']: + # Hang out for a bit and try again + print('retrying add port check') + wait_time += 10 + time.sleep(10) + else: + break + addresses = cmd_output['addresses']['private'] + self.assertNotIn(ip_address, addresses) def test_server_add_remove_volume(self): volume_wait_for = volume_common.BaseVolumeTests.wait_for_status diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 203e47ebb3..bb0a2c6768 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -6973,14 +6973,14 @@ def setUp(self): # Set method to be tested. self.methods = { - 'interface_detach': None, + 'delete_server_interface': None, } self.find_port = mock.Mock() self.app.client_manager.network.find_port = self.find_port def _test_server_remove_port(self, port_id): - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) port = 'fake-port' arglist = [ @@ -6995,7 +6995,8 @@ def _test_server_remove_port(self, port_id): result = self.cmd.take_action(parsed_args) - servers[0].interface_detach.assert_called_once_with(port_id) + self.sdk_client.delete_server_interface.assert_called_with( + port_id, server=servers[0], ignore_missing=False) self.assertIsNone(result) def test_server_remove_port(self): @@ -7020,17 +7021,18 @@ def setUp(self): # Set method to be tested. self.fake_inf = mock.Mock() self.methods = { - 'interface_list': [self.fake_inf], - 'interface_detach': None, + 'server_interfaces': [self.fake_inf], + 'delete_server_interface': None, } self.find_network = mock.Mock() self.app.client_manager.network.find_network = self.find_network + self.sdk_client.server_interfaces.return_value = [self.fake_inf] def _test_server_remove_network(self, network_id): self.fake_inf.net_id = network_id self.fake_inf.port_id = 'fake-port' - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) network = 'fake-network' arglist = [ @@ -7045,8 +7047,9 @@ def _test_server_remove_network(self, network_id): result = self.cmd.take_action(parsed_args) - servers[0].interface_list.assert_called_once_with() - servers[0].interface_detach.assert_called_once_with('fake-port') + self.sdk_client.server_interfaces.assert_called_once_with(servers[0]) + self.sdk_client.delete_server_interface.assert_called_once_with( + 'fake-port', server=servers[0]) self.assertIsNone(result) def test_server_remove_network(self): diff --git a/releasenotes/notes/switch-server-remove-network-port-to-sdk-829ba711e0e198d5.yaml b/releasenotes/notes/switch-server-remove-network-port-to-sdk-829ba711e0e198d5.yaml new file mode 100644 index 0000000000..6b47b1b35c --- /dev/null +++ b/releasenotes/notes/switch-server-remove-network-port-to-sdk-829ba711e0e198d5.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Switch server remove volume/port to using sdk. From b515fe61b27408e78639da8abb3acaa485ebca4e Mon Sep 17 00:00:00 2001 From: Thrivikram Mudunuri Date: Sat, 13 Nov 2021 02:37:44 -0500 Subject: [PATCH 0465/1120] Switch server pause and server unpause to SDK Switch the server pause and server unpause commands from novaclient to SDK. Use the SDK versions of test fakes to support fake Server resources. Change-Id: Id626f06f3d7edd44b306b7fc7b9b00d04af09621 --- openstackclient/compute/v2/server.py | 22 ++++++++--------- .../tests/unit/compute/v2/test_server.py | 24 +++++++++++++++---- ...pause-unpause-to-sdk-d74ec8536b764af6.yaml | 5 ++++ 3 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/migrate-server-pause-unpause-to-sdk-d74ec8536b764af6.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 121a7b82e8..09954c49d2 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3130,12 +3130,13 @@ 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 for server in parsed_args.server: - utils.find_resource( - compute_client.servers, - server - ).pause() + server_id = compute_client.find_server( + server, + ignore_missing=False, + ).id + compute_client.pause_server(server_id) class RebootServer(command.Command): @@ -4674,7 +4675,6 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute for server in parsed_args.server: utils.find_resource( @@ -4697,13 +4697,13 @@ 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 for server in parsed_args.server: - utils.find_resource( - compute_client.servers, + server_id = compute_client.find_server( server, - ).unpause() + ignore_missing=False, + ).id + compute_client.unpause_server(server_id) class UnrescueServer(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 10ea07adb3..435ddb4778 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -192,6 +192,22 @@ def run_method_with_servers(self, method_name, server_count): method.assert_called_with() self.assertIsNone(result) + def run_method_with_sdk_servers(self, method_name, server_count): + servers = self.setup_sdk_servers_mock(count=server_count) + + arglist = [s.id for s in servers] + verifylist = [ + ('server', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [call(s.id) for s in servers] + method = getattr(self.sdk_client, method_name) + method.assert_has_calls(calls) + self.assertIsNone(result) + class TestServerAddFixedIP(TestServer): @@ -6062,10 +6078,10 @@ def setUp(self): } def test_server_pause_one_server(self): - self.run_method_with_servers('pause', 1) + self.run_method_with_sdk_servers('pause_server', 1) def test_server_pause_multi_servers(self): - self.run_method_with_servers('pause', 3) + self.run_method_with_sdk_servers('pause_server', 3) class TestServerRebuild(TestServer): @@ -8308,10 +8324,10 @@ def setUp(self): } def test_server_unpause_one_server(self): - self.run_method_with_servers('unpause', 1) + self.run_method_with_sdk_servers('unpause_server', 1) def test_server_unpause_multi_servers(self): - self.run_method_with_servers('unpause', 3) + self.run_method_with_sdk_servers('unpause_server', 3) class TestServerUnset(TestServer): diff --git a/releasenotes/notes/migrate-server-pause-unpause-to-sdk-d74ec8536b764af6.yaml b/releasenotes/notes/migrate-server-pause-unpause-to-sdk-d74ec8536b764af6.yaml new file mode 100644 index 0000000000..e2d4003489 --- /dev/null +++ b/releasenotes/notes/migrate-server-pause-unpause-to-sdk-d74ec8536b764af6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Migrate ``server pause`` and ``server unpause`` commands from novaclient + to sdk. From ff96fea0120ab43968a10230ce7899a3c6504e75 Mon Sep 17 00:00:00 2001 From: Thrivikram Mudunuri Date: Sat, 13 Nov 2021 17:39:41 -0500 Subject: [PATCH 0466/1120] Switch server suspend and server resume to SDK Switch the server suspend and server resume commands from novaclient to SDK. Use the SDK versions of test fakes to support fake Server resources. Change-Id: Idd0b4f13fab0f238e42844a7d759538bbda24f68 --- openstackclient/compute/v2/server.py | 20 +++++++++---------- .../tests/unit/compute/v2/test_server.py | 8 ++++---- ...uspend-resume-to-sdk-fd1709336607b496.yaml | 5 +++++ 3 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/migrate-server-suspend-resume-to-sdk-fd1709336607b496.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 09954c49d2..a7479bb41a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -4081,13 +4081,13 @@ 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 for server in parsed_args.server: - utils.find_resource( - compute_client.servers, + server_id = compute_client.find_server( server, - ).resume() + ignore_missing=False, + ).id + compute_client.resume_server(server_id) class SetServer(command.Command): @@ -4652,13 +4652,13 @@ 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 for server in parsed_args.server: - utils.find_resource( - compute_client.servers, + server_id = compute_client.find_server( server, - ).suspend() + ignore_missing=False, + ).id + compute_client.suspend_server(server_id) class UnlockServer(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 435ddb4778..27ea12637c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -7617,10 +7617,10 @@ def setUp(self): } def test_server_resume_one_server(self): - self.run_method_with_servers('resume', 1) + self.run_method_with_sdk_servers('resume_server', 1) def test_server_resume_multi_servers(self): - self.run_method_with_servers('resume', 3) + self.run_method_with_sdk_servers('resume_server', 3) class TestServerSet(TestServer): @@ -8284,10 +8284,10 @@ def setUp(self): } def test_server_suspend_one_server(self): - self.run_method_with_servers('suspend', 1) + self.run_method_with_sdk_servers('suspend_server', 1) def test_server_suspend_multi_servers(self): - self.run_method_with_servers('suspend', 3) + self.run_method_with_sdk_servers('suspend_server', 3) class TestServerUnlock(TestServer): diff --git a/releasenotes/notes/migrate-server-suspend-resume-to-sdk-fd1709336607b496.yaml b/releasenotes/notes/migrate-server-suspend-resume-to-sdk-fd1709336607b496.yaml new file mode 100644 index 0000000000..7d3781bbf4 --- /dev/null +++ b/releasenotes/notes/migrate-server-suspend-resume-to-sdk-fd1709336607b496.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Migrate ``server suspend`` and ``server resume`` commands from novaclient + to sdk. From 4c3de28e83babb0672950320a20492dc61803b4a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 26 Jan 2021 16:55:05 +0000 Subject: [PATCH 0467/1120] compute: Reorder building of columns for 'server list' This has no impact on the end result, but it should make fixing issues introduced by API microversion 2.69 a little easier. Change-Id: I7d70eac8aa1a6197ed05a49f071e6899ec219c03 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 192 +++++++++++++++------------ 1 file changed, 105 insertions(+), 87 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 121a7b82e8..be14074b7d 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2204,15 +2204,19 @@ def take_action(self, parsed_args): # flavor name is given, map it to ID. flavor_id = None if parsed_args.flavor: - flavor_id = utils.find_resource(compute_client.flavors, - parsed_args.flavor).id + flavor_id = utils.find_resource( + compute_client.flavors, + parsed_args.flavor, + ).id # Nova only supports list servers searching by image ID. So if a # image name is given, map it to ID. image_id = None if parsed_args.image: - image_id = image_client.find_image(parsed_args.image, - ignore_missing=False).id + image_id = image_client.find_image( + parsed_args.image, + ignore_missing=False, + ).id search_opts = { 'reservation_id': parsed_args.reservation_id, @@ -2320,95 +2324,93 @@ def take_action(self, parsed_args): try: iso8601.parse_date(search_opts['changes-since']) except (TypeError, iso8601.ParseError): + msg = _('Invalid changes-since value: %s') raise exceptions.CommandError( - _('Invalid changes-since value: %s') % - search_opts['changes-since'] + msg % search_opts['changes-since'] ) + columns = ( + 'id', + 'name', + 'status', + ) + column_headers = ( + 'ID', + 'Name', + 'Status', + ) + if parsed_args.long: - columns = ( - 'ID', - 'Name', - 'Status', + columns += ( 'OS-EXT-STS:task_state', 'OS-EXT-STS:power_state', - 'Networks', - 'Image Name', - 'Image ID', - 'Flavor Name', - 'Flavor ID', - 'OS-EXT-AZ:availability_zone', - 'OS-EXT-SRV-ATTR:host', - 'Metadata', ) - column_headers = ( - 'ID', - 'Name', - 'Status', + column_headers += ( 'Task State', 'Power State', - 'Networks', + ) + + columns += ('networks',) + column_headers += ('Networks',) + + if parsed_args.long: + columns += ( + 'image_name', + 'image_id', + ) + column_headers += ( 'Image Name', 'Image ID', + ) + else: + if parsed_args.no_name_lookup: + columns += ('image_id',) + else: + columns += ('image_name',) + column_headers += ('Image',) + + if parsed_args.long: + columns += ( + 'flavor_name', + 'flavor_id', + ) + column_headers += ( 'Flavor Name', 'Flavor ID', - 'Availability Zone', - 'Host', - 'Properties', ) - mixed_case_fields = [ - 'OS-EXT-STS:task_state', - 'OS-EXT-STS:power_state', - 'OS-EXT-AZ:availability_zone', - 'OS-EXT-SRV-ATTR:host', - ] else: if parsed_args.no_name_lookup: - columns = ( - 'ID', - 'Name', - 'Status', - 'Networks', - 'Image ID', - 'Flavor ID', - ) + columns += ('flavor_id',) else: - columns = ( - 'ID', - 'Name', - 'Status', - 'Networks', - 'Image Name', - 'Flavor Name', - ) - column_headers = ( - 'ID', - 'Name', - 'Status', - 'Networks', - 'Image', - 'Flavor', + columns += ('flavor_name',) + column_headers += ('Flavor',) + + if parsed_args.long: + columns += ( + 'OS-EXT-AZ:availability_zone', + 'OS-EXT-SRV-ATTR:host', + 'metadata', + ) + column_headers += ( + 'Availability Zone', + 'Host', + 'Properties', ) - mixed_case_fields = [] marker_id = None # support for additional columns if parsed_args.columns: - # convert tuple to list to edit them - column_headers = list(column_headers) - columns = list(columns) - for c in parsed_args.columns: if c in ('Project ID', 'project_id'): - columns.append('tenant_id') - column_headers.append('Project ID') + columns += ('tenant_id',) + column_headers += ('Project ID',) if c in ('User ID', 'user_id'): - columns.append('user_id') - column_headers.append('User ID') + columns += ('user_id',) + column_headers += ('User ID',) if c in ('Created At', 'created_at'): - columns.append('created') - column_headers.append('Created At') + columns += ('created',) + column_headers += ('Created At',) # convert back to tuple column_headers = tuple(column_headers) @@ -2422,25 +2424,29 @@ def take_action(self, parsed_args): if parsed_args.deleted: marker_id = parsed_args.marker else: - marker_id = utils.find_resource(compute_client.servers, - parsed_args.marker).id + marker_id = utils.find_resource( + compute_client.servers, + parsed_args.marker, + ).id - data = compute_client.servers.list(search_opts=search_opts, - marker=marker_id, - limit=parsed_args.limit) + data = compute_client.servers.list( + search_opts=search_opts, + marker=marker_id, + limit=parsed_args.limit) images = {} flavors = {} if data and not parsed_args.no_name_lookup: - # Create a dict that maps image_id to image object. - # Needed so that we can display the "Image Name" column. - # "Image Name" is not crucial, so we swallow any exceptions. - # The 'image' attribute can be an empty string if the server was - # booted from a volume. + # 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(filter(lambda x: x is not None, - (s.image.get('id') for s in data - if s.image))): + for i_id in set( + s.image['id'] for s in data + if s.image and s.image.get('id') + ): + # "Image Name" is not crucial, so we swallow any exceptions try: images[i_id] = image_client.get_image(i_id) except Exception: @@ -2453,12 +2459,17 @@ def take_action(self, parsed_args): except Exception: pass - # Create a dict that maps flavor_id to flavor object. - # Needed so that we can display the "Flavor Name" column. - # "Flavor Name" is not crucial, so we swallow any exceptions. + # create a dict that maps flavor_id to flavor object, which is used + # to display the "Flavor Name" column. Note that 'flavor.id' is not + # present on microversion 2.47 or later and 'flavor' won't be + # present if there are infra failures if parsed_args.name_lookup_one_by_one or flavor_id: - for f_id in set(filter(lambda x: x is not None, - (s.flavor.get('id') for s in data))): + for f_id in set( + s.flavor['id'] for s in data + if s.flavor and s.flavor.get('id') + ): + # "Flavor Name" is not crucial, so we swallow any + # exceptions try: flavors[f_id] = compute_client.flavors.get(f_id) except Exception: @@ -2482,6 +2493,7 @@ def take_action(self, parsed_args): # processing of the image and flavor informations. if not hasattr(s, 'image') or not hasattr(s, 'flavor'): continue + if 'id' in s.image: image = images.get(s.image['id']) if image: @@ -2494,6 +2506,7 @@ def take_action(self, parsed_args): # able to grep for boot-from-volume servers when using the CLI. s.image_name = IMAGE_STRING_FOR_BFV s.image_id = IMAGE_STRING_FOR_BFV + if 'id' in s.flavor: flavor = flavors.get(s.flavor['id']) if flavor: @@ -2512,11 +2525,16 @@ def take_action(self, parsed_args): ( utils.get_item_properties( s, columns, - mixed_case_fields=mixed_case_fields, + mixed_case_fields=( + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:power_state', + 'OS-EXT-AZ:availability_zone', + 'OS-EXT-SRV-ATTR:host', + ), formatters={ 'OS-EXT-STS:power_state': PowerStateColumn, - 'Networks': format_columns.DictListColumn, - 'Metadata': format_columns.DictColumn, + 'networks': format_columns.DictListColumn, + 'metadata': format_columns.DictColumn, }, ) for s in data ), From 8e362402dee07744668bcf7f6774af4fbe9a07e3 Mon Sep 17 00:00:00 2001 From: Khomesh Thakre Date: Fri, 6 Nov 2020 22:45:03 +0530 Subject: [PATCH 0468/1120] compute: Show flavor in 'server list' with API >= 2.47 Fix the issue where the flavor name was empty in server list output. This requires somewhat invasive unit test changes to reflect the changed API response from the server, but this has the upside of meaning we don't need new tests since what we have validates things. Also drop the flavor ID column as it is removed from the compute API. Change-Id: Ica3320242a38901c1180b2b29109c9474366fde0 Signed-off-by: Khomesh Thakre Story: 2008257 Task: 41113 --- openstackclient/compute/v2/server.py | 42 +- .../tests/unit/compute/v2/test_server.py | 558 ++++++++++-------- ...st-microversion-2.47-af200e9bb4747e2d.yaml | 8 + 3 files changed, 348 insertions(+), 260 deletions(-) create mode 100644 releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index be14074b7d..a0b27242d4 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2369,21 +2369,28 @@ def take_action(self, parsed_args): columns += ('image_name',) column_headers += ('Image',) - if parsed_args.long: - columns += ( - 'flavor_name', - 'flavor_id', - ) - column_headers += ( - 'Flavor Name', - 'Flavor ID', - ) + # microversion 2.47 puts the embedded flavor into the server response + # body but omits the id, so if not present we just expose the original + # flavor name in the output + if compute_client.api_version >= api_versions.APIVersion('2.47'): + columns += ('flavor_name',) + column_headers += ('Flavor',) else: - if parsed_args.no_name_lookup: - columns += ('flavor_id',) + if parsed_args.long: + columns += ( + 'flavor_name', + 'flavor_id', + ) + column_headers += ( + 'Flavor Name', + 'Flavor ID', + ) else: - columns += ('flavor_name',) - column_headers += ('Flavor',) + if parsed_args.no_name_lookup: + columns += ('flavor_id',) + else: + columns += ('flavor_name',) + column_headers += ('Flavor',) if parsed_args.long: columns += ( @@ -2507,18 +2514,13 @@ def take_action(self, parsed_args): s.image_name = IMAGE_STRING_FOR_BFV s.image_id = IMAGE_STRING_FOR_BFV - if 'id' in s.flavor: + if compute_client.api_version < api_versions.APIVersion('2.47'): flavor = flavors.get(s.flavor['id']) if flavor: s.flavor_name = flavor.name s.flavor_id = s.flavor['id'] else: - # TODO(mriedem): Fix this for microversion >= 2.47 where the - # flavor is embedded in the server response without the id. - # We likely need to drop the Flavor ID column in that case if - # --long is specified. - s.flavor_name = '' - s.flavor_id = '' + s.flavor_name = s.flavor['original_name'] table = ( column_headers, diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 10ea07adb3..17762d72bc 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4081,7 +4081,7 @@ def test_server_dump_multi_servers(self): self.run_method_with_servers('trigger_crash_dump', 3) -class TestServerList(TestServer): +class _TestServerList(TestServer): # Columns to be listed up. columns = ( @@ -4109,7 +4109,7 @@ class TestServerList(TestServer): ) def setUp(self): - super(TestServerList, self).setUp() + super(_TestServerList, self).setUp() self.search_opts = { 'reservation_id': None, @@ -4148,7 +4148,7 @@ def setUp(self): }, 'OS-EXT-AZ:availability_zone': 'availability-zone-xxx', 'OS-EXT-SRV-ATTR:host': 'host-name-xxx', - 'Metadata': '', + 'Metadata': format_columns.DictColumn({}), } # The servers to be listed. @@ -4167,10 +4167,11 @@ def setUp(self): # Get the command object to test self.cmd = server.ListServer(self.app, None) - # Prepare data returned by fake Nova API. - self.data = [] - self.data_long = [] - self.data_no_name_lookup = [] + +class TestServerList(_TestServerList): + + def setUp(self): + super(TestServerList, self).setUp() Image = collections.namedtuple('Image', 'id name') self.images_mock.return_value = [ @@ -4185,8 +4186,8 @@ def setUp(self): for s in self.servers ] - for s in self.servers: - self.data.append(( + self.data = tuple( + ( s.id, s.name, s.status, @@ -4194,34 +4195,8 @@ def setUp(self): # Image will be an empty string if boot-from-volume self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, self.flavor.name, - )) - self.data_long.append(( - s.id, - s.name, - s.status, - getattr(s, 'OS-EXT-STS:task_state'), - server.PowerStateColumn( - getattr(s, 'OS-EXT-STS:power_state') - ), - format_columns.DictListColumn(s.networks), - # Image will be an empty string if boot-from-volume - self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, - s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, - self.flavor.name, - s.flavor['id'], - getattr(s, 'OS-EXT-AZ:availability_zone'), - getattr(s, 'OS-EXT-SRV-ATTR:host'), - format_columns.DictColumn({}), - )) - self.data_no_name_lookup.append(( - s.id, - s.name, - s.status, - format_columns.DictListColumn(s.networks), - # Image will be an empty string if boot-from-volume - s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, - s.flavor['id'] - )) + ) for s in self.servers + ) def test_server_list_no_option(self): arglist = [] @@ -4242,7 +4217,7 @@ def test_server_list_no_option(self): self.assertFalse(self.flavors_mock.get.call_count) self.assertFalse(self.get_image_mock.call_count) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_no_servers(self): arglist = [] @@ -4261,9 +4236,28 @@ def test_server_list_no_servers(self): self.assertEqual(0, self.images_mock.list.call_count) self.assertEqual(0, self.flavors_mock.list.call_count) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_long_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + getattr(s, 'OS-EXT-STS:task_state'), + server.PowerStateColumn( + getattr(s, 'OS-EXT-STS:power_state') + ), + format_columns.DictListColumn(s.networks), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + s.flavor['id'], + getattr(s, 'OS-EXT-AZ:availability_zone'), + getattr(s, 'OS-EXT-SRV-ATTR:host'), + s.Metadata, + ) for s in self.servers) arglist = [ '--long', ] @@ -4274,10 +4268,9 @@ def test_server_list_long_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns_long, columns) - self.assertCountEqual(tuple(self.data_long), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_column_option(self): arglist = [ @@ -4299,6 +4292,18 @@ def test_server_list_column_option(self): self.assertIn('Created At', columns) def test_server_list_no_name_lookup_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + format_columns.DictListColumn(s.networks), + # Image will be an empty string if boot-from-volume + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, + s.flavor['id'] + ) for s in self.servers + ) + arglist = [ '--no-name-lookup', ] @@ -4312,9 +4317,21 @@ def test_server_list_no_name_lookup_option(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data_no_name_lookup), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_n_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + format_columns.DictListColumn(s.networks), + # Image will be an empty string if boot-from-volume + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, + s.flavor['id'] + ) for s in self.servers + ) + arglist = [ '-n', ] @@ -4328,7 +4345,7 @@ def test_server_list_n_option(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data_no_name_lookup), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_name_lookup_one_by_one(self): arglist = [ @@ -4350,7 +4367,7 @@ def test_server_list_name_lookup_one_by_one(self): self.flavors_mock.get.assert_called() self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_with_image(self): @@ -4371,81 +4388,7 @@ def test_server_list_with_image(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) - - def test_server_list_with_locked_pre_v273(self): - - arglist = [ - '--locked' - ] - verifylist = [ - ('locked', True) - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - ex = self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) - self.assertIn( - '--os-compute-api-version 2.73 or greater is required', str(ex)) - - def test_server_list_with_locked_v273(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.73') - arglist = [ - '--locked' - ] - verifylist = [ - ('locked', True) - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.search_opts['locked'] = True - self.servers_mock.list.assert_called_with(**self.kwargs) - - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) - - def test_server_list_with_unlocked_v273(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.73') - arglist = [ - '--unlocked' - ] - verifylist = [ - ('unlocked', True) - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.search_opts['locked'] = False - self.servers_mock.list.assert_called_with(**self.kwargs) - - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) - - def test_server_list_with_locked_and_unlocked_v273(self): - - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.73') - arglist = [ - '--locked', - '--unlocked' - ] - verifylist = [ - ('locked', True), - ('unlocked', True) - ] - - ex = self.assertRaises( - utils.ParserException, - self.check_parser, self.cmd, arglist, verifylist) - self.assertIn('Argument parse failed', str(ex)) + self.assertEqual(self.data, tuple(data)) def test_server_list_with_flavor(self): @@ -4465,7 +4408,7 @@ def test_server_list_with_flavor(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_with_changes_since(self): @@ -4486,7 +4429,7 @@ def test_server_list_with_changes_since(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): @@ -4509,123 +4452,6 @@ def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): 'Invalid time value' ) - def test_server_list_v266_with_changes_before(self): - self.app.client_manager.compute.api_version = ( - api_versions.APIVersion('2.66')) - arglist = [ - '--changes-before', '2016-03-05T06:27:59Z', - '--deleted' - ] - verifylist = [ - ('changes_before', '2016-03-05T06:27:59Z'), - ('deleted', True), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' - self.search_opts['deleted'] = True - - self.servers_mock.list.assert_called_with(**self.kwargs) - - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) - - @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) - def test_server_list_v266_with_invalid_changes_before( - self, mock_parse_isotime): - self.app.client_manager.compute.api_version = ( - api_versions.APIVersion('2.66')) - - arglist = [ - '--changes-before', 'Invalid time value', - ] - verifylist = [ - ('changes_before', 'Invalid time value'), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('Invalid changes-before value: Invalid time ' - 'value', str(e)) - mock_parse_isotime.assert_called_once_with( - 'Invalid time value' - ) - - def test_server_with_changes_before_pre_v266(self): - self.app.client_manager.compute.api_version = ( - api_versions.APIVersion('2.65')) - - arglist = [ - '--changes-before', '2016-03-05T06:27:59Z', - '--deleted' - ] - verifylist = [ - ('changes_before', '2016-03-05T06:27:59Z'), - ('deleted', True), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) - - def test_server_list_v269_with_partial_constructs(self): - self.app.client_manager.compute.api_version = \ - api_versions.APIVersion('2.69') - - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # include "partial results" from non-responsive part of - # infrastructure. - server_dict = { - "id": "server-id-95a56bfc4xxxxxx28d7e418bfd97813a", - "status": "UNKNOWN", - "tenant_id": "6f70656e737461636b20342065766572", - "created": "2018-12-03T21:06:18Z", - "links": [ - { - "href": "http://fake/v2.1/", - "rel": "self" - }, - { - "href": "http://fake", - "rel": "bookmark" - } - ], - # We need to pass networks as {} because its defined as a property - # of the novaclient Server class which gives {} by default. If not - # it will fail at formatting the networks info later on. - "networks": {} - } - _server = compute_fakes.fakes.FakeResource( - info=server_dict, - ) - self.servers.append(_server) - columns, data = self.cmd.take_action(parsed_args) - # get the first three servers out since our interest is in the partial - # server. - next(data) - next(data) - next(data) - partial_server = next(data) - expected_row = ( - 'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', - '', - 'UNKNOWN', - format_columns.DictListColumn({}), - '', - '', - ) - self.assertEqual(expected_row, partial_server) - def test_server_list_with_tag(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( '2.26') @@ -4646,7 +4472,7 @@ def test_server_list_with_tag(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_with_tag_pre_v225(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -4689,7 +4515,7 @@ def test_server_list_with_not_tag(self): self.servers_mock.list.assert_called_with(**self.kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertEqual(self.data, tuple(data)) def test_server_list_with_not_tag_pre_v226(self): self.app.client_manager.compute.api_version = api_versions.APIVersion( @@ -4850,6 +4676,258 @@ def test_server_list_with_power_state(self): self.assertEqual(tuple(self.data), tuple(data)) +class TestServerListV273(_TestServerList): + + # Columns to be listed up. + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + 'Image', + 'Flavor', + ) + columns_long = ( + 'ID', + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks', + 'Image Name', + 'Image ID', + 'Flavor', + 'Availability Zone', + 'Host', + 'Properties', + ) + + def setUp(self): + super(TestServerListV273, self).setUp() + + # The fake servers' attributes. Use the original attributes names in + # nova, not the ones printed by "server list" command. + self.attrs['flavor'] = { + 'vcpus': self.flavor.vcpus, + 'ram': self.flavor.ram, + 'disk': self.flavor.disk, + 'ephemeral': self.flavor.ephemeral, + 'swap': self.flavor.swap, + 'original_name': self.flavor.name, + 'extra_specs': self.flavor.extra_specs, + } + + # The servers to be listed. + self.servers = self.setup_servers_mock(3) + self.servers_mock.list.return_value = self.servers + + Image = collections.namedtuple('Image', 'id name') + self.images_mock.return_value = [ + Image(id=s.image['id'], name=self.image.name) + # Image will be an empty string if boot-from-volume + for s in self.servers if s.image + ] + + # The flavor information is embedded, so now reason for this to be + # called + self.flavors_mock.list = mock.NonCallableMock() + + self.data = tuple( + ( + s.id, + s.name, + s.status, + format_columns.DictListColumn(s.networks), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + ) for s in self.servers) + + def test_server_list_with_locked_pre_v273(self): + + arglist = [ + '--locked' + ] + verifylist = [ + ('locked', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.73 or greater is required', str(ex)) + + def test_server_list_with_locked(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + arglist = [ + '--locked' + ] + verifylist = [ + ('locked', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + 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)) + + def test_server_list_with_unlocked_v273(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + arglist = [ + '--unlocked' + ] + verifylist = [ + ('unlocked', True) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + 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)) + + def test_server_list_with_locked_and_unlocked(self): + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.73') + arglist = [ + '--locked', + '--unlocked' + ] + verifylist = [ + ('locked', True), + ('unlocked', True) + ] + + ex = self.assertRaises( + utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + self.assertIn('Argument parse failed', str(ex)) + + def test_server_list_with_changes_before(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.66')) + arglist = [ + '--changes-before', '2016-03-05T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_before', '2016-03-05T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' + self.search_opts['deleted'] = True + + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertItemsEqual(self.columns, columns) + self.assertItemsEqual(self.data, tuple(data)) + + @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError) + def test_server_list_with_invalid_changes_before( + self, mock_parse_isotime): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.66')) + + arglist = [ + '--changes-before', 'Invalid time value', + ] + verifylist = [ + ('changes_before', 'Invalid time value'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('Invalid changes-before value: Invalid time ' + 'value', str(e)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) + + def test_server_with_changes_before_pre_v266(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.65')) + + arglist = [ + '--changes-before', '2016-03-05T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_before', '2016-03-05T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_server_list_v269_with_partial_constructs(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.69') + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # include "partial results" from non-responsive part of + # infrastructure. + server_dict = { + "id": "server-id-95a56bfc4xxxxxx28d7e418bfd97813a", + "status": "UNKNOWN", + "tenant_id": "6f70656e737461636b20342065766572", + "created": "2018-12-03T21:06:18Z", + "links": [ + { + "href": "http://fake/v2.1/", + "rel": "self" + }, + { + "href": "http://fake", + "rel": "bookmark" + } + ], + # We need to pass networks as {} because its defined as a property + # of the novaclient Server class which gives {} by default. If not + # it will fail at formatting the networks info later on. + "networks": {} + } + server = compute_fakes.fakes.FakeResource( + info=server_dict, + ) + self.servers.append(server) + columns, data = self.cmd.take_action(parsed_args) + # get the first three servers out since our interest is in the partial + # server. + next(data) + next(data) + next(data) + partial_server = next(data) + expected_row = ( + 'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', '', + 'UNKNOWN', format_columns.DictListColumn({}), '', '') + self.assertEqual(expected_row, partial_server) + + class TestServerLock(TestServer): def setUp(self): diff --git a/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml b/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml new file mode 100644 index 0000000000..fdb37bbbc9 --- /dev/null +++ b/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add support for compute API microversion 2.47, which changes how flavor + details are included in server detail responses. In 2.46 and below, + only the flavor ID was shown in the server detail response. Starting in + 2.47, flavor information is embedded in the server response. The newer + behavior is now supported. From c8c4f76498de3380c7cbf80c5dc800a588bed649 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 26 Oct 2021 13:03:22 +0000 Subject: [PATCH 0469/1120] Add --security-group to port list The neutron API supports filtering ports by security group. Closes-Bug: #1405057 Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/804979 Change-Id: I0f626882716c21ac200c1b929ea04664d21874d8 --- openstackclient/network/v2/port.py | 9 +++++++++ .../tests/unit/network/v2/test_port.py | 20 +++++++++++++++++++ ...-list-security-group-4af5d2e789174ff9.yaml | 5 +++++ 3 files changed, 34 insertions(+) create mode 100644 releasenotes/notes/port-list-security-group-4af5d2e789174ff9.yaml diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 132c384a0e..887c531808 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -610,6 +610,13 @@ def get_parser(self, prog_name): metavar='', help=_("List ports according to their name") ) + parser.add_argument( + '--security-group', + action='append', + dest='security_groups', + metavar='', + help=_("List only ports associated with this security group") + ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--fixed-ip', @@ -682,6 +689,8 @@ def take_action(self, parsed_args): if parsed_args.fixed_ip: filters['fixed_ips'] = _prepare_filter_fixed_ips( self.app.client_manager, parsed_args) + if parsed_args.security_groups: + filters['security_groups'] = parsed_args.security_groups _tag.get_tag_filtering_args(parsed_args, filters) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 5f2a12836a..96edb74df8 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -1296,6 +1296,26 @@ def test_list_with_tag_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_port_list_security_group(self): + arglist = [ + '--security-group', 'sg-id1', + '--security-group', 'sg-id2', + ] + verifylist = [ + ('security_groups', ['sg-id1', 'sg-id2']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = { + 'security_groups': ['sg-id1', 'sg-id2'], + 'fields': LIST_FIELDS_TO_RETRIEVE, + } + + self.network.ports.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, list(data)) + class TestSetPort(TestPort): diff --git a/releasenotes/notes/port-list-security-group-4af5d2e789174ff9.yaml b/releasenotes/notes/port-list-security-group-4af5d2e789174ff9.yaml new file mode 100644 index 0000000000..c68eeafbce --- /dev/null +++ b/releasenotes/notes/port-list-security-group-4af5d2e789174ff9.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added ``--security-group`` option to the ``os port list`` command. This + option is appendable and multiple security group IDs can be provided. From bef70397a3e1240cc593b3fb34049f2ff6601e68 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 27 Jul 2021 16:45:24 +0000 Subject: [PATCH 0470/1120] Add network update quota "limit_check" parameter This new parameter commands the Neutron server to first check the resource usage before setting the new quota limit. If the resource usage is below the new limit, the Neutron server will raise an exception. Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/806254 Depends-On: https://review.opendev.org/c/openstack/neutron/+/801470 Partial-Bug: #1936408 Change-Id: Idc1b99492d609eb699d0a6bef6cd760458a774f6 --- openstackclient/common/quota.py | 9 ++++ .../tests/functional/common/test_quota.py | 25 +++++++++++ .../tests/unit/common/test_quota.py | 43 +++++++++++++++++++ .../check-limit-quota-cc7f291dd1b537c1.yaml | 5 +++ 4 files changed, 82 insertions(+) create mode 100644 releasenotes/notes/check-limit-quota-cc7f291dd1b537c1.yaml diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 643cb4e474..677cba038e 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -535,6 +535,12 @@ def get_parser(self, prog_name): action='store_true', help=_('Force quota update (only supported by compute)') ) + parser.add_argument( + '--check-limit', + action='store_true', + help=_('Check quota limit when updating (only supported by ' + 'network)') + ) return parser def take_action(self, parsed_args): @@ -561,6 +567,9 @@ def take_action(self, parsed_args): volume_kwargs[k] = value network_kwargs = {} + if parsed_args.check_limit: + network_kwargs['check_limit'] = True + if self.app.client_manager.is_network_endpoint_enabled(): for k, v in NETWORK_QUOTAS.items(): value = getattr(parsed_args, k, None) diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 9c05746077..bf67101a32 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -11,6 +11,9 @@ # under the License. import json +import uuid + +from tempest.lib import exceptions from openstackclient.tests.functional import base @@ -165,3 +168,25 @@ def test_quota_set_class(self): # returned attributes self.assertTrue(cmd_output["key-pairs"] >= 0) self.assertTrue(cmd_output["snapshots"] >= 0) + + def test_quota_network_set_with_check_limit(self): + if not self.haz_network: + self.skipTest('No Network service present') + if not self.is_extension_enabled('quota-check-limit'): + self.skipTest('No "quota-check-limit" extension present') + + self.openstack('quota set --networks 40 ' + self.PROJECT_NAME) + cmd_output = json.loads(self.openstack( + 'quota list -f json --network' + )) + self.assertIsNotNone(cmd_output) + self.assertEqual(40, cmd_output[0]['Networks']) + + # That will ensure we have at least two networks in the system. + for _ in range(2): + self.openstack('network create --project %s %s' % + (self.PROJECT_NAME, uuid.uuid4().hex)) + + self.assertRaises(exceptions.CommandFailed, self.openstack, + 'quota set --networks 1 --check-limit ' + + self.PROJECT_NAME) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 8771359cb5..896a63a7bc 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -950,6 +950,49 @@ def test_quota_set_with_force(self): ) self.assertIsNone(result) + def test_quota_set_with_check_limit(self): + arglist = [ + '--subnets', str(network_fakes.QUOTA['subnet']), + '--volumes', str(volume_fakes.QUOTA['volumes']), + '--cores', str(compute_fakes.core_num), + '--check-limit', + self.projects[0].name, + ] + verifylist = [ + ('subnet', network_fakes.QUOTA['subnet']), + ('volumes', volume_fakes.QUOTA['volumes']), + ('cores', compute_fakes.core_num), + ('check_limit', True), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs_compute = { + 'cores': compute_fakes.core_num, + } + kwargs_volume = { + 'volumes': volume_fakes.QUOTA['volumes'], + } + kwargs_network = { + 'subnet': network_fakes.QUOTA['subnet'], + 'check_limit': True, + } + self.compute_quotas_mock.update.assert_called_once_with( + self.projects[0].id, + **kwargs_compute + ) + self.volume_quotas_mock.update.assert_called_once_with( + self.projects[0].id, + **kwargs_volume + ) + self.network_mock.update_quota.assert_called_once_with( + self.projects[0].id, + **kwargs_network + ) + self.assertIsNone(result) + class TestQuotaShow(TestQuota): diff --git a/releasenotes/notes/check-limit-quota-cc7f291dd1b537c1.yaml b/releasenotes/notes/check-limit-quota-cc7f291dd1b537c1.yaml new file mode 100644 index 0000000000..171b4a5ae0 --- /dev/null +++ b/releasenotes/notes/check-limit-quota-cc7f291dd1b537c1.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add ``--check-limit`` option to the ``openstack quota set`` command (only + for network commands). The network quota engine will check the resource + usage before setting the new quota limit. From 32e18253faa742aae5a4c9708a8a505c85ebb317 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Tue, 7 Dec 2021 18:33:54 +0100 Subject: [PATCH 0471/1120] Fix RemoveServerVolume The nova API we're using to delete a server volume attachment needs to be handed a volume, not a volume attachment. Also make sure that we create an error if the volume isn't actually attached to the server. Signed-off-by: Dr. Jens Harbott Co-authored-by: Stephen Finucane Change-Id: I12abd3787ea47acb4da282d00fdc1989405a0564 --- openstackclient/compute/v2/server.py | 16 +++++----------- .../tests/unit/compute/v2/test_server.py | 10 ++++------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index fb13d30572..a18ce81012 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3833,17 +3833,11 @@ def take_action(self, parsed_args): ignore_missing=False, ) - volume_attachments = compute_client.volume_attachments(server) - for volume_attachment in volume_attachments: - if volume_attachment.volume_id == volume.id: - compute_client.delete_volume_attachment( - volume_attachment, - server, - ) - break - else: - msg = _('Target volume attachment not found.') - raise exceptions.CommandError(msg) + compute_client.delete_volume_attachment( + volume, + server, + ignore_missing=False, + ) class RescueServer(command.Command): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 00733e4a76..15be07fc6c 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1016,10 +1016,6 @@ def setUp(self): self.cmd = server.RemoveServerVolume(self.app, None) def test_server_remove_volume(self): - self.sdk_client.volume_attachments.return_value = [ - self.volume_attachment - ] - arglist = [ self.servers[0].id, self.volumes[0].id, @@ -1036,8 +1032,10 @@ def test_server_remove_volume(self): self.assertIsNone(result) self.sdk_client.delete_volume_attachment.assert_called_once_with( - self.volume_attachment, - self.servers[0]) + self.volumes[0], + self.servers[0], + ignore_missing=False, + ) class TestServerAddNetwork(TestServer): From 4e9b9298429f5db505987853f98d2388b6745b13 Mon Sep 17 00:00:00 2001 From: "Dr. Jens Harbott" Date: Tue, 30 Nov 2021 09:21:56 +0100 Subject: [PATCH 0472/1120] Allow setting gateway when creating a router These options are not only valid when modifying a router, but also when one is created initially. Signed-off-by: Dr. Jens Harbott Change-Id: I3e12901f37cbd1639ac9dc9cc49b04114b80474c --- openstackclient/network/v2/router.py | 79 +++++++++++++------ .../tests/unit/network/v2/test_router.py | 37 +++++++++ ...ptions-create-router-97910a882b604652.yaml | 8 ++ 3 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/options-create-router-97910a882b604652.yaml diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index dde4eda99f..aeeec93175 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -111,6 +111,30 @@ def _get_attrs(client_manager, parsed_args): parsed_args.project_domain, ).id attrs['tenant_id'] = project_id + if parsed_args.external_gateway: + gateway_info = {} + n_client = client_manager.network + network = n_client.find_network( + parsed_args.external_gateway, ignore_missing=False) + gateway_info['network_id'] = network.id + if parsed_args.disable_snat: + gateway_info['enable_snat'] = False + if parsed_args.enable_snat: + gateway_info['enable_snat'] = True + if parsed_args.fixed_ip: + ips = [] + for ip_spec in parsed_args.fixed_ip: + if ip_spec.get('subnet', False): + subnet_name_id = ip_spec.pop('subnet') + if subnet_name_id: + subnet = n_client.find_subnet(subnet_name_id, + ignore_missing=False) + ip_spec['subnet_id'] = subnet.id + if ip_spec.get('ip-address', False): + ip_spec['ip_address'] = ip_spec.pop('ip-address') + ips.append(ip_spec) + gateway_info['external_fixed_ips'] = ips + attrs['external_gateway_info'] = gateway_info return attrs @@ -320,6 +344,32 @@ def get_parser(self, prog_name): "repeat option to set multiple availability zones)") ) _tag.add_tag_option_to_parser_for_create(parser, _('router')) + parser.add_argument( + '--external-gateway', + metavar="", + help=_("External Network used as router's gateway (name or ID)") + ) + parser.add_argument( + '--fixed-ip', + metavar='subnet=,ip-address=', + action=parseractions.MultiKeyValueAction, + optional_keys=['subnet', 'ip-address'], + help=_("Desired IP and/or subnet (name or ID) " + "on external gateway: " + "subnet=,ip-address= " + "(repeat option to set multiple fixed IP addresses)") + ) + snat_group = parser.add_mutually_exclusive_group() + snat_group.add_argument( + '--enable-snat', + action='store_true', + help=_("Enable Source NAT on external gateway") + ) + snat_group.add_argument( + '--disable-snat', + action='store_true', + help=_("Disable Source NAT on external gateway") + ) return parser @@ -338,6 +388,12 @@ def take_action(self, parsed_args): # tags cannot be set when created, so tags need to be set later. _tag.update_tags_for_set(client, obj, parsed_args) + if (parsed_args.disable_snat or parsed_args.enable_snat or + parsed_args.fixed_ip) and not parsed_args.external_gateway: + msg = (_("You must specify '--external-gateway' in order " + "to specify SNAT or fixed-ip values")) + raise exceptions.CommandError(msg) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) @@ -725,29 +781,6 @@ def take_action(self, parsed_args): msg = (_("You must specify '--external-gateway' in order " "to update the SNAT or fixed-ip values")) raise exceptions.CommandError(msg) - if parsed_args.external_gateway: - gateway_info = {} - network = client.find_network( - parsed_args.external_gateway, ignore_missing=False) - gateway_info['network_id'] = network.id - if parsed_args.disable_snat: - gateway_info['enable_snat'] = False - if parsed_args.enable_snat: - gateway_info['enable_snat'] = True - if parsed_args.fixed_ip: - ips = [] - for ip_spec in parsed_args.fixed_ip: - if ip_spec.get('subnet', False): - subnet_name_id = ip_spec.pop('subnet') - if subnet_name_id: - subnet = client.find_subnet(subnet_name_id, - ignore_missing=False) - ip_spec['subnet_id'] = subnet.id - if ip_spec.get('ip-address', False): - ip_spec['ip_address'] = ip_spec.pop('ip-address') - ips.append(ip_spec) - gateway_info['external_fixed_ips'] = ips - attrs['external_gateway_info'] = gateway_info if ((parsed_args.qos_policy or parsed_args.no_qos_policy) and not parsed_args.external_gateway): diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 0324674817..04d9fe4836 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -186,6 +186,43 @@ def test_create_default_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) + def test_create_with_gateway(self): + _network = network_fakes.FakeNetwork.create_one_network() + _subnet = network_fakes.FakeSubnet.create_one_subnet() + self.network.find_network = mock.Mock(return_value=_network) + self.network.find_subnet = mock.Mock(return_value=_subnet) + arglist = [ + self.new_router.name, + '--external-gateway', _network.name, + '--enable-snat', + '--fixed-ip', 'ip-address=2001:db8::1' + ] + verifylist = [ + ('name', self.new_router.name), + ('enable', True), + ('distributed', False), + ('ha', False), + ('external_gateway', _network.name), + ('enable_snat', True), + ('fixed_ip', [{'ip-address': '2001:db8::1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_router.assert_called_once_with(**{ + 'admin_state_up': True, + 'name': self.new_router.name, + 'external_gateway_info': { + 'network_id': _network.id, + 'enable_snat': True, + 'external_fixed_ips': [{'ip_address': '2001:db8::1'}], + }, + }) + self.assertFalse(self.network.set_tags.called) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + def _test_create_with_ha_options(self, option, ha): arglist = [ option, diff --git a/releasenotes/notes/options-create-router-97910a882b604652.yaml b/releasenotes/notes/options-create-router-97910a882b604652.yaml new file mode 100644 index 0000000000..f7d90b75c2 --- /dev/null +++ b/releasenotes/notes/options-create-router-97910a882b604652.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + It is now possible to add an external gateway to a router + immediately on creation. Previously it could only be done by + modifying the router after it had been created. This includes + the options to en- or disable SNAT and to specify a fixed-ip + on the external network. From b3cb85f1123b15c1ec4fafac9dcedc9381072a8b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 6 Dec 2021 10:24:15 +0000 Subject: [PATCH 0473/1120] tests: Improve logging for executed commands We're seeing failures in a recently added tests, 'ServerTests.test_server_add_remove_volume' from 'openstackclient/tests/functional/compute/v2/test_server.py'. These failures are likely the result of slow CI nodes, but we don't have enough information in the CI logs to debug them. Starting logging the various commands executed in tests so that we can see these logs if and when tests fail. Change-Id: I4584dc5e6343fe8c8544431a527d8c3c7e7b3c5b Signed-off-by: Stephen Finucane --- openstackclient/tests/functional/base.py | 21 ++++++++---- .../functional/compute/v2/test_server.py | 33 ++++++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 0ed7dff8c4..e89c5b9737 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 logging import os import shlex import subprocess @@ -18,22 +19,30 @@ from tempest.lib import exceptions import testtools - ADMIN_CLOUD = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin') +LOG = logging.getLogger(__name__) def execute(cmd, fail_ok=False, merge_stderr=False): """Executes specified command for the given action.""" + LOG.debug('Executing: %s', cmd) cmdlist = shlex.split(cmd) stdout = subprocess.PIPE stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE + proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr) - result, result_err = proc.communicate() - result = result.decode('utf-8') + + result_out, result_err = proc.communicate() + result_out = result_out.decode('utf-8') + LOG.debug('stdout: %s', result_out) + LOG.debug('stderr: %s', result_err) + if not fail_ok and proc.returncode != 0: - raise exceptions.CommandFailed(proc.returncode, cmd, result, - result_err) - return result + raise exceptions.CommandFailed( + proc.returncode, cmd, result_out, result_err, + ) + + return result_out class TestCase(testtools.TestCase): diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index cf4bcbc2a9..0558ef6213 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -1195,19 +1195,19 @@ def test_server_add_remove_port(self): def test_server_add_remove_volume(self): volume_wait_for = volume_common.BaseVolumeTests.wait_for_status - name = uuid.uuid4().hex + server_name = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'server create -f json ' + '--network private ' + '--flavor ' + self.flavor_name + ' ' + '--image ' + self.image_name + ' ' + '--wait ' + - name + server_name )) self.assertIsNotNone(cmd_output['id']) - self.assertEqual(name, cmd_output['name']) - self.addCleanup(self.openstack, 'server delete --wait ' + name) + self.assertEqual(server_name, cmd_output['name']) + self.addCleanup(self.openstack, 'server delete --wait ' + server_name) server_id = cmd_output['id'] volume_name = uuid.uuid4().hex @@ -1225,7 +1225,7 @@ def test_server_add_remove_volume(self): cmd_output = json.loads(self.openstack( 'server add volume -f json ' + - name + ' ' + + server_name + ' ' + volume_name + ' ' + '--tag bar' )) @@ -1237,7 +1237,7 @@ def test_server_add_remove_volume(self): cmd_output = json.loads(self.openstack( 'server volume list -f json ' + - name + server_name )) self.assertEqual(volume_attachment_id, cmd_output[0]['ID']) @@ -1245,8 +1245,25 @@ def test_server_add_remove_volume(self): self.assertEqual(volume_id, cmd_output[0]['Volume ID']) volume_wait_for('volume', volume_name, 'in-use') - self.openstack('server remove volume ' + name + ' ' + volume_name) + + cmd_output = json.loads(self.openstack( + 'server event list -f json ' + + server_name + )) + self.assertEqual(2, len(cmd_output)) + self.assertIn('attach_volume', {x['Action'] for x in cmd_output}) + + self.openstack( + 'server remove volume ' + server_name + ' ' + volume_name + ) volume_wait_for('volume', volume_name, 'available') - raw_output = self.openstack('server volume list ' + name) + cmd_output = json.loads(self.openstack( + 'server event list -f json ' + + server_name + )) + self.assertEqual(3, len(cmd_output)) + self.assertIn('detach_volume', {x['Action'] for x in cmd_output}) + + raw_output = self.openstack('server volume list ' + server_name) self.assertEqual('\n', raw_output) From 9971d7253e9c3abd2e3940bf549ef8532ef929f9 Mon Sep 17 00:00:00 2001 From: Ritvik Vinodkumar Date: Wed, 1 Dec 2021 16:54:53 +0000 Subject: [PATCH 0474/1120] Switch add fixed IP to SDK Switch the add fixed IP command from novaclient to SDK. Change-Id: I4752ea7b4bfc17e04b8f46dbe9a68d938501a89e --- openstackclient/compute/v2/server.py | 44 ++-- .../tests/unit/compute/v2/test_server.py | 224 ++++++++++++++---- ...-add-fixed-ip-to-sdk-3d932d77633bc765.yaml | 3 + 3 files changed, 211 insertions(+), 60 deletions(-) create mode 100644 releasenotes/notes/migrate-add-fixed-ip-to-sdk-3d932d77633bc765.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a18ce81012..0931a6ca01 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -237,30 +237,44 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - - server = utils.find_resource( - compute_client.servers, parsed_args.server) - - network = compute_client.api.network_find(parsed_args.network) - - kwargs = { - 'port_id': None, - 'net_id': network['id'], - 'fixed_ip': parsed_args.fixed_ip_address, - } + compute_client = self.app.client_manager.sdk_connection.compute + server = compute_client.find_server( + parsed_args.server, + ignore_missing=False + ) if parsed_args.tag: - if compute_client.api_version < api_versions.APIVersion('2.49'): + if not sdk_utils.supports_microversion(compute_client, '2.49'): msg = _( '--os-compute-api-version 2.49 or greater is required to ' 'support the --tag option' ) raise exceptions.CommandError(msg) - kwargs['tag'] = parsed_args.tag + if self.app.client_manager.is_network_endpoint_enabled(): + network_client = self.app.client_manager.network + net_id = network_client.find_network( + parsed_args.network, + ignore_missing=False + ).id + else: + net_id = parsed_args.network + + if not sdk_utils.supports_microversion(compute_client, '2.44'): + compute_client.add_fixed_ip_to_server( + server.id, + net_id + ) + return + + kwargs = { + 'net_id': net_id, + 'fixed_ip': parsed_args.fixed_ip_address, + } - server.interface_attach(**kwargs) + if parsed_args.tag: + kwargs['tag'] = parsed_args.tag + compute_client.create_server_interface(server.id, **kwargs) class AddFloatingIP(network_common.NetworkAndComputeCommand): diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 15be07fc6c..5d47b68784 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -217,104 +217,104 @@ def setUp(self): # Get the command object to test self.cmd = server.AddFixedIP(self.app, None) + # Mock network methods + self.find_network = mock.Mock() + self.app.client_manager.network.find_network = self.find_network + # Set add_fixed_ip method to be tested. self.methods = { 'interface_attach': None, } - def _test_server_add_fixed_ip(self, extralist, fixed_ip_address): - servers = self.setup_servers_mock(count=1) + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_pre_2_44(self, sm_mock): + sm_mock.return_value = False + + servers = self.setup_sdk_servers_mock(count=1) network = compute_fakes.FakeNetwork.create_one_network() - with mock.patch( - 'openstackclient.api.compute_v2.APIv2.network_find' - ) as net_mock: - net_mock.return_value = network + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): arglist = [ servers[0].id, network['id'], - ] + extralist + ] verifylist = [ ('server', servers[0].id), ('network', network['id']), - ('fixed_ip_address', fixed_ip_address), + ('fixed_ip_address', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - servers[0].interface_attach.assert_called_once_with( - port_id=None, - net_id=network['id'], - fixed_ip=fixed_ip_address, + self.sdk_client.add_fixed_ip_to_server.assert_called_once_with( + servers[0].id, + network['id'] ) self.assertIsNone(result) - def test_server_add_fixed_ip(self): - self._test_server_add_fixed_ip([], None) - - def test_server_add_specific_fixed_ip(self): - extralist = ['--fixed-ip-address', '5.6.7.8'] - self._test_server_add_fixed_ip(extralist, '5.6.7.8') - - def test_server_add_fixed_ip_with_tag(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.49') + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_pre_2_44_with_fixed_ip(self, sm_mock): + sm_mock.return_value = False - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) network = compute_fakes.FakeNetwork.create_one_network() - with mock.patch( - 'openstackclient.api.compute_v2.APIv2.network_find' - ) as net_mock: - net_mock.return_value = network + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): arglist = [ servers[0].id, network['id'], - '--fixed-ip-address', '5.6.7.8', - '--tag', 'tag1', + '--fixed-ip-address', '5.6.7.8' ] verifylist = [ ('server', servers[0].id), ('network', network['id']), ('fixed_ip_address', '5.6.7.8'), - ('tag', 'tag1'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - servers[0].interface_attach.assert_called_once_with( - port_id=None, - net_id=network['id'], - fixed_ip='5.6.7.8', - tag='tag1' + self.sdk_client.add_fixed_ip_to_server.assert_called_once_with( + servers[0].id, + network['id'] ) self.assertIsNone(result) - def test_server_add_fixed_ip_with_tag_pre_v249(self): - self.app.client_manager.compute.api_version = api_versions.APIVersion( - '2.48') + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_pre_2_44_with_tag(self, sm_mock): + sm_mock.return_value = False - servers = self.setup_servers_mock(count=1) + servers = self.setup_sdk_servers_mock(count=1) network = compute_fakes.FakeNetwork.create_one_network() - with mock.patch( - 'openstackclient.api.compute_v2.APIv2.network_find' - ) as net_mock: - net_mock.return_value = network + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): arglist = [ servers[0].id, network['id'], '--fixed-ip-address', '5.6.7.8', - '--tag', 'tag1', + '--tag', 'tag1' ] verifylist = [ ('server', servers[0].id), ('network', network['id']), ('fixed_ip_address', '5.6.7.8'), - ('tag', 'tag1'), + ('tag', 'tag1') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + ex = self.assertRaises( exceptions.CommandError, self.cmd.take_action, @@ -323,6 +323,140 @@ def test_server_add_fixed_ip_with_tag_pre_v249(self): '--os-compute-api-version 2.49 or greater is required', str(ex)) + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_pre_2_49_with_tag(self, sm_mock): + sm_mock.side_effect = [False, True] + + servers = self.setup_sdk_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8', + '--tag', 'tag1' + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8'), + ('tag', 'tag1') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-compute-api-version 2.49 or greater is required', + str(ex)) + + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_post_2_49(self, sm_mock): + sm_mock.side_effect = [True, True] + + servers = self.setup_sdk_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): + arglist = [ + servers[0].id, + network['id'] + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0].id, + net_id=network['id'], + fixed_ip=None + ) + self.assertIsNone(result) + + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_post_2_49_with_fixedip(self, sm_mock): + sm_mock.side_effect = [True, True] + + servers = self.setup_sdk_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8' + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0].id, + net_id=network['id'], + fixed_ip='5.6.7.8' + ) + self.assertIsNone(result) + + @mock.patch.object(sdk_utils, 'supports_microversion') + def test_server_add_fixed_ip_post_2_49_with_tag(self, sm_mock): + sm_mock.side_effect = [True, True] + + servers = self.setup_sdk_servers_mock(count=1) + network = compute_fakes.FakeNetwork.create_one_network() + + with mock.patch.object( + self.app.client_manager, + 'is_network_endpoint_enabled', + return_value=False + ): + arglist = [ + servers[0].id, + network['id'], + '--fixed-ip-address', '5.6.7.8', + '--tag', 'tag1' + ] + verifylist = [ + ('server', servers[0].id), + ('network', network['id']), + ('fixed_ip_address', '5.6.7.8'), + ('tag', 'tag1') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.sdk_client.create_server_interface.assert_called_once_with( + servers[0].id, + net_id=network['id'], + fixed_ip='5.6.7.8', + tag='tag1' + ) + self.assertIsNone(result) + @mock.patch( 'openstackclient.api.compute_v2.APIv2.floating_ip_add' diff --git a/releasenotes/notes/migrate-add-fixed-ip-to-sdk-3d932d77633bc765.yaml b/releasenotes/notes/migrate-add-fixed-ip-to-sdk-3d932d77633bc765.yaml new file mode 100644 index 0000000000..79899b3e92 --- /dev/null +++ b/releasenotes/notes/migrate-add-fixed-ip-to-sdk-3d932d77633bc765.yaml @@ -0,0 +1,3 @@ +features: + - | + Switch the add fixed IP command from novaclient to SDK. From 0cde82dcd86205f9e190b1a4f02136e1d83797b9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 14 Dec 2021 15:14:49 +0000 Subject: [PATCH 0475/1120] compute: Return information about fixed IP The compute API provides this information to us. We might as well use it. Change-Id: I5608fa80745975ce49712718452cfe296c0f64d2 Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 33 ++++- .../tests/unit/compute/v2/fakes.py | 28 +++++ .../tests/unit/compute/v2/test_server.py | 117 +++++++++++++----- 3 files changed, 145 insertions(+), 33 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 0931a6ca01..2c1e42cc79 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -204,7 +204,7 @@ def boolenv(*vars, default=False): return default -class AddFixedIP(command.Command): +class AddFixedIP(command.ShowOne): _description = _("Add fixed IP address to server") def get_parser(self, prog_name): @@ -231,7 +231,7 @@ def get_parser(self, prog_name): metavar='', help=_( 'Tag for the attached interface. ' - '(supported by --os-compute-api-version 2.52 or above)' + '(supported by --os-compute-api-version 2.49 or above)' ) ) return parser @@ -265,16 +265,39 @@ def take_action(self, parsed_args): server.id, net_id ) - return + return ((), ()) kwargs = { 'net_id': net_id, 'fixed_ip': parsed_args.fixed_ip_address, } - if parsed_args.tag: kwargs['tag'] = parsed_args.tag - compute_client.create_server_interface(server.id, **kwargs) + + interface = compute_client.create_server_interface(server.id, **kwargs) + + columns = ( + 'port_id', 'server_id', 'net_id', 'mac_addr', 'port_state', + 'fixed_ips', + ) + column_headers = ( + 'Port ID', 'Server ID', 'Network ID', 'MAC Address', 'Port State', + 'Fixed IPs', + ) + if sdk_utils.supports_microversion(compute_client, '2.49'): + columns += ('tag',) + column_headers += ('Tag',) + + return ( + column_headers, + utils.get_item_properties( + interface, + columns, + formatters={ + 'fixed_ips': format_columns.ListDictColumn, + }, + ), + ) class AddFloatingIP(network_common.NetworkAndComputeCommand): diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 7618c229c5..2a53164735 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -21,6 +21,7 @@ from novaclient import api_versions from openstack.compute.v2 import flavor as _flavor from openstack.compute.v2 import server +from openstack.compute.v2 import server_interface as _server_interface from openstack.compute.v2 import volume_attachment from openstackclient.api import compute_v2 @@ -1859,3 +1860,30 @@ def create_sdk_volume_attachments(attrs=None, methods=None, count=2): attrs, methods)) return volume_attachments + + +def create_one_server_interface(attrs=None): + """Create a fake SDK ServerInterface. + + :param dict attrs: A dictionary with all attributes + :param dict methods: A dictionary with all methods + :return: A fake ServerInterface object with various attributes set + """ + attrs = attrs or {} + + # Set default attributes. + server_interface_info = { + "fixed_ips": uuid.uuid4().hex, + "mac_addr": "aa:aa:aa:aa:aa:aa", + "net_id": uuid.uuid4().hex, + "port_id": uuid.uuid4().hex, + "port_state": "ACTIVE", + "server_id": uuid.uuid4().hex, + # introduced in API microversion 2.70 + "tag": "foo", + } + + # Overwrite default attributes. + server_interface_info.update(attrs) + + return _server_interface.ServerInterface(**server_interface_info) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 5d47b68784..6553f90465 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -212,7 +212,7 @@ def run_method_with_sdk_servers(self, method_name, server_count): class TestServerAddFixedIP(TestServer): def setUp(self): - super(TestServerAddFixedIP, self).setUp() + super().setUp() # Get the command object to test self.cmd = server.AddFixedIP(self.app, None) @@ -221,13 +221,8 @@ def setUp(self): self.find_network = mock.Mock() self.app.client_manager.network.find_network = self.find_network - # Set add_fixed_ip method to be tested. - self.methods = { - 'interface_attach': None, - } - @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_pre_2_44(self, sm_mock): + def test_server_add_fixed_ip_pre_v244(self, sm_mock): sm_mock.return_value = False servers = self.setup_sdk_servers_mock(count=1) @@ -255,10 +250,11 @@ def test_server_add_fixed_ip_pre_2_44(self, sm_mock): servers[0].id, network['id'] ) - self.assertIsNone(result) + # the legacy API operates asynchronously + self.assertEqual(((), ()), result) @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_pre_2_44_with_fixed_ip(self, sm_mock): + def test_server_add_fixed_ip_pre_v244_with_fixed_ip(self, sm_mock): sm_mock.return_value = False servers = self.setup_sdk_servers_mock(count=1) @@ -287,10 +283,11 @@ def test_server_add_fixed_ip_pre_2_44_with_fixed_ip(self, sm_mock): servers[0].id, network['id'] ) - self.assertIsNone(result) + # the legacy API operates asynchronously + self.assertEqual(((), ()), result) @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_pre_2_44_with_tag(self, sm_mock): + def test_server_add_fixed_ip_pre_v244_with_tag(self, sm_mock): sm_mock.return_value = False servers = self.setup_sdk_servers_mock(count=1) @@ -324,7 +321,7 @@ def test_server_add_fixed_ip_pre_2_44_with_tag(self, sm_mock): str(ex)) @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_pre_2_49_with_tag(self, sm_mock): + def test_server_add_fixed_ip_pre_v249_with_tag(self, sm_mock): sm_mock.side_effect = [False, True] servers = self.setup_sdk_servers_mock(count=1) @@ -358,11 +355,13 @@ def test_server_add_fixed_ip_pre_2_49_with_tag(self, sm_mock): str(ex)) @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_post_2_49(self, sm_mock): - sm_mock.side_effect = [True, True] + def test_server_add_fixed_ip(self, sm_mock): + sm_mock.side_effect = [True, False] servers = self.setup_sdk_servers_mock(count=1) network = compute_fakes.FakeNetwork.create_one_network() + interface = compute_fakes.create_one_server_interface() + self.sdk_client.create_server_interface.return_value = interface with mock.patch.object( self.app.client_manager, @@ -379,21 +378,41 @@ def test_server_add_fixed_ip_post_2_49(self, sm_mock): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ( + 'Port ID', + 'Server ID', + 'Network ID', + 'MAC Address', + 'Port State', + 'Fixed IPs', + ) + expected_data = ( + interface.port_id, + interface.server_id, + interface.net_id, + interface.mac_addr, + interface.port_state, + format_columns.ListDictColumn(interface.fixed_ips), + ) + + columns, data = self.cmd.take_action(parsed_args) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, tuple(data)) self.sdk_client.create_server_interface.assert_called_once_with( servers[0].id, net_id=network['id'], fixed_ip=None ) - self.assertIsNone(result) @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_post_2_49_with_fixedip(self, sm_mock): + def test_server_add_fixed_ip_with_fixed_ip(self, sm_mock): sm_mock.side_effect = [True, True] servers = self.setup_sdk_servers_mock(count=1) network = compute_fakes.FakeNetwork.create_one_network() + interface = compute_fakes.create_one_server_interface() + self.sdk_client.create_server_interface.return_value = interface with mock.patch.object( self.app.client_manager, @@ -412,21 +431,43 @@ def test_server_add_fixed_ip_post_2_49_with_fixedip(self, sm_mock): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ( + 'Port ID', + 'Server ID', + 'Network ID', + 'MAC Address', + 'Port State', + 'Fixed IPs', + 'Tag', + ) + expected_data = ( + interface.port_id, + interface.server_id, + interface.net_id, + interface.mac_addr, + interface.port_state, + format_columns.ListDictColumn(interface.fixed_ips), + interface.tag, + ) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, tuple(data)) self.sdk_client.create_server_interface.assert_called_once_with( servers[0].id, net_id=network['id'], fixed_ip='5.6.7.8' ) - self.assertIsNone(result) @mock.patch.object(sdk_utils, 'supports_microversion') - def test_server_add_fixed_ip_post_2_49_with_tag(self, sm_mock): - sm_mock.side_effect = [True, True] + def test_server_add_fixed_ip_with_tag(self, sm_mock): + sm_mock.side_effect = [True, True, True] servers = self.setup_sdk_servers_mock(count=1) network = compute_fakes.FakeNetwork.create_one_network() + interface = compute_fakes.create_one_server_interface() + self.sdk_client.create_server_interface.return_value = interface with mock.patch.object( self.app.client_manager, @@ -447,15 +488,35 @@ def test_server_add_fixed_ip_post_2_49_with_tag(self, sm_mock): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) + expected_columns = ( + 'Port ID', + 'Server ID', + 'Network ID', + 'MAC Address', + 'Port State', + 'Fixed IPs', + 'Tag', + ) + expected_data = ( + interface.port_id, + interface.server_id, + interface.net_id, + interface.mac_addr, + interface.port_state, + format_columns.ListDictColumn(interface.fixed_ips), + interface.tag, + ) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, tuple(data)) self.sdk_client.create_server_interface.assert_called_once_with( servers[0].id, net_id=network['id'], fixed_ip='5.6.7.8', - tag='tag1' + tag='tag1', ) - self.assertIsNone(result) @mock.patch( @@ -5265,7 +5326,7 @@ def test_server_migrate_with_disk_overcommit(self): self.assertNotCalled(self.servers_mock.live_migrate) self.assertNotCalled(self.servers_mock.migrate) - def test_server_migrate_with_host_pre_2_56(self): + def test_server_migrate_with_host_pre_v256(self): # Tests that --host is not allowed for a cold migration # before microversion 2.56 (the test defaults to 2.1). arglist = [ @@ -6682,7 +6743,7 @@ def test_rebuild_with_user_data(self, mock_open): self.image, None, userdata=mock_file,) - def test_rebuild_with_user_data_pre_257(self): + def test_rebuild_with_user_data_pre_v257(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.56') @@ -6722,7 +6783,7 @@ def test_rebuild_with_no_user_data(self): self.server.rebuild.assert_called_with( self.image, None, userdata=None) - def test_rebuild_with_no_user_data_pre_254(self): + def test_rebuild_with_no_user_data_pre_v254(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.53') @@ -6814,7 +6875,7 @@ def test_rebuild_with_no_trusted_image_cert(self): self.server.rebuild.assert_called_with( self.image, None, trusted_image_certificates=None) - def test_rebuild_with_no_trusted_image_cert_pre_257(self): + def test_rebuild_with_no_trusted_image_cert_pre_v263(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.62') From ba69870d86b5840dec06c6c30c8ddf50398bdb44 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 15 Dec 2021 17:32:10 +0000 Subject: [PATCH 0476/1120] compute: Fix weird option definition for 'server ssh' argparse allows you to specify multiple options for a given argument when declaring the argument. For some reason, we weren't doing this for the 'server ssh' command. There's no apparent reason for doing things this way and it's been that way since the beginning (2013) so let's not do that. We also add unit tests since they were missing and should exist. Change-Id: I67a9e6516d7057266210cd4083e9ddeb1cfaa5de Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/server.py | 33 +------- .../tests/unit/compute/v2/test_server.py | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a18ce81012..5c603d04da 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -4462,51 +4462,26 @@ def get_parser(self, prog_name): help=_('Server (name or ID)'), ) parser.add_argument( - '--login', + '--login', '-l', metavar='', help=_('Login name (ssh -l option)'), ) parser.add_argument( - '-l', - dest='login', - metavar='', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--port', + '--port', '-p', metavar='', type=int, help=_('Destination port (ssh -p option)'), ) parser.add_argument( - '-p', - metavar='', - dest='port', - type=int, - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--identity', + '--identity', '-i', metavar='', help=_('Private key file (ssh -i option)'), ) parser.add_argument( - '-i', - metavar='', - dest='identity', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--option', + '--option', '-o', metavar='', help=_('Options in ssh_config(5) format (ssh -o option)'), ) - parser.add_argument( - '-o', - metavar='