From 4d3a3bb28f61a75c14a06e98dc0dd265308d658f Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 1 Oct 2020 21:52:45 +0100 Subject: [PATCH 001/770] 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 30d5f14a700dd53d80e0fbedefd9b1ad337390f9 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 Dec 2020 15:40:24 +0100 Subject: [PATCH 002/770] 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 bb15b29190dfe77ea0218a01edc1f4886993b177 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Mon, 11 Jan 2021 14:24:48 +0000 Subject: [PATCH 003/770] 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 e01e59caeb56cb7bf4f38403b82d0a265b163098 Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Wed, 14 Oct 2020 11:35:22 -0500 Subject: [PATCH 004/770] 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 0cc878e5b053765a0d3c13f5588bc160b05a388b Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 17 Dec 2020 17:08:22 +0000 Subject: [PATCH 005/770] 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 006/770] 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 007/770] 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 008/770] 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 009/770] 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 010/770] 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 011/770] 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 012/770] 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 013/770] 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 014/770] 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 015/770] 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 016/770] 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 017/770] 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 018/770] 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 019/770] 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 020/770] 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 021/770] 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 022/770] 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 023/770] 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 024/770] 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 025/770] 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 026/770] 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 027/770] 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 028/770] 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 029/770] 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 030/770] 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 031/770] 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 032/770] 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 033/770] 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 034/770] 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 035/770] 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 036/770] 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 037/770] 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 038/770] 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 039/770] 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 040/770] 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 041/770] 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 042/770] 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 043/770] 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 044/770] 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 045/770] 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 046/770] 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 047/770] 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 048/770] 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 049/770] 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 050/770] 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 051/770] 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 052/770] 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 053/770] 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 054/770] 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 055/770] 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 056/770] 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 057/770] 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 058/770] 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 059/770] 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 060/770] 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 061/770] 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 062/770] 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 063/770] 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 064/770] 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 065/770] 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 066/770] 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 067/770] 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 068/770] 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 069/770] 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 070/770] 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 071/770] 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 072/770] 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 073/770] 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 074/770] 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 075/770] 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 076/770] 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 077/770] 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 078/770] 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 079/770] 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 080/770] 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 081/770] 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 082/770] [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 083/770] 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 084/770] 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 085/770] 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 086/770] 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 087/770] 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 088/770] 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 089/770] 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 090/770] 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 091/770] 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 092/770] 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 093/770] 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 094/770] 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 095/770] 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 096/770] 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 097/770] 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 098/770] 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 099/770] 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 100/770] 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 101/770] 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 102/770] 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 103/770] 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 104/770] 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 105/770] 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 106/770] 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 107/770] 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 108/770] 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 109/770] 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 110/770] 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 111/770] 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 112/770] 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 113/770] 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 114/770] 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 115/770] 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 116/770] 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 117/770] 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 118/770] 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 119/770] 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 120/770] 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 121/770] 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 122/770] 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 123/770] 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 124/770] 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 125/770] 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 126/770] 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='