From 47ba155a3c345752101a4bb61e094d4e5a5c6af7 Mon Sep 17 00:00:00 2001 From: 0weng Date: Tue, 25 Feb 2025 13:30:07 -0800 Subject: [PATCH 01/31] Identity: Migrate 'registered limit' commands to SDK Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/942649 Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/942818 Change-Id: I8e464d64bd7962bd0fecc90698f67ebc93ae4357 Signed-off-by: 0weng --- .../identity/v3/registered_limit.py | 181 +++--- .../unit/identity/v3/test_registered_limit.py | 575 +++++++++++------- ...istered-limit-to-sdk-36b6451e3a799a43.yaml | 10 + 3 files changed, 447 insertions(+), 319 deletions(-) create mode 100644 releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml diff --git a/openstackclient/identity/v3/registered_limit.py b/openstackclient/identity/v3/registered_limit.py index 41b8cdac0..25d226959 100644 --- a/openstackclient/identity/v3/registered_limit.py +++ b/openstackclient/identity/v3/registered_limit.py @@ -25,6 +25,29 @@ LOG = logging.getLogger(__name__) +def _format_registered_limit(registered_limit): + columns = ( + 'default_limit', + 'description', + 'id', + 'region_id', + 'resource_name', + 'service_id', + ) + column_headers = ( + 'default_limit', + 'description', + 'id', + 'region_id', + 'resource_name', + 'service_id', + ) + return ( + column_headers, + utils.get_item_properties(registered_limit, columns), + ) + + class CreateRegisteredLimit(command.ShowOne): _description = _("Create a registered limit") @@ -64,43 +87,28 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity + + kwargs = {} + + if parsed_args.description: + kwargs["description"] = parsed_args.description + + kwargs["service_id"] = common_utils.find_service_sdk( + identity_client, parsed_args.service + ).id - service = utils.find_resource( - identity_client.services, parsed_args.service - ) - region = None if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - registered_limit = identity_client.registered_limits.create( - service, - parsed_args.resource_name, - parsed_args.default_limit, - description=parsed_args.description, - region=region, - ) + kwargs["resource_name"] = parsed_args.resource_name + kwargs["default_limit"] = parsed_args.default_limit + + registered_limit = identity_client.create_registered_limit(**kwargs) - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + return _format_registered_limit(registered_limit) class DeleteRegisteredLimit(command.Command): @@ -112,17 +120,22 @@ def get_parser(self, prog_name): 'registered_limits', metavar='', nargs="+", - help=_('Registered limit(s) to delete (ID)'), + help=_( + 'Registered limit(s) to delete (ID) ' + '(repeat option to remove multiple registered limits)' + ), ) return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity errors = 0 for registered_limit_id in parsed_args.registered_limits: try: - identity_client.registered_limits.delete(registered_limit_id) + identity_client.delete_registered_limit( + registered_limit_id, ignore_missing=False + ) except Exception as e: errors += 1 from pprint import pprint @@ -170,40 +183,22 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - service = None + kwargs = {} if parsed_args.service: - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) - region = None + ).id if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - registered_limits = identity_client.registered_limits.list( - service=service, - resource_name=parsed_args.resource_name, - region=region, - ) + if parsed_args.resource_name: + kwargs["resource_name"] = parsed_args.resource_name + + registered_limits = identity_client.registered_limits(**kwargs) columns = ( 'ID', @@ -273,44 +268,33 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - service = None + kwargs = {} if parsed_args.service: - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) + ).id + + if parsed_args.resource_name: + kwargs["resource_name"] = parsed_args.resource_name + + if parsed_args.default_limit: + kwargs["default_limit"] = parsed_args.default_limit + + if parsed_args.description: + kwargs["description"] = parsed_args.description - region = None if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _("Passing 'None' to indicate no region is deprecated.") - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - registered_limit = identity_client.registered_limits.update( - parsed_args.registered_limit_id, - service=service, - resource_name=parsed_args.resource_name, - default_limit=parsed_args.default_limit, - description=parsed_args.description, - region=region, + registered_limit = identity_client.update_registered_limit( + parsed_args.registered_limit_id, **kwargs ) - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + return _format_registered_limit(registered_limit) class ShowRegisteredLimit(command.ShowOne): @@ -326,9 +310,8 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - registered_limit = identity_client.registered_limits.get( + identity_client = self.app.client_manager.sdk_connection.identity + registered_limit = identity_client.get_registered_limit( parsed_args.registered_limit_id ) - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + return _format_registered_limit(registered_limit) diff --git a/openstackclient/tests/unit/identity/v3/test_registered_limit.py b/openstackclient/tests/unit/identity/v3/test_registered_limit.py index a120714ec..1864914fd 100644 --- a/openstackclient/tests/unit/identity/v3/test_registered_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_registered_limit.py @@ -10,72 +10,79 @@ # License for the specific language governing permissions and limitations # under the License. -import copy - -from keystoneauth1.exceptions import http as ksa_exceptions +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import region as _region +from openstack.identity.v3 import registered_limit as _registered_limit +from openstack.identity.v3 import service as _service +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.identity.v3 import registered_limit -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestRegisteredLimit(identity_fakes.TestIdentityv3): +class TestRegisteredLimitCreate(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - self.registered_limit_mock = self.identity_client.registered_limits - - self.services_mock = self.identity_client.services - self.services_mock.reset_mock() - - self.regions_mock = self.identity_client.regions - self.regions_mock.reset_mock() + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.description = 'default limit of foobars' + self.default_limit = 10 + self.resource_name = 'foobars' -class TestRegisteredLimitCreate(TestRegisteredLimit): - def setUp(self): - super().setUp() + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region - self.service = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.SERVICE), loaded=True + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) - self.services_mock.get.return_value = self.service - - self.region = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGION), loaded=True + self.registered_limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=self.description, + region_id=self.region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) - self.regions_mock.get.return_value = self.region self.cmd = registered_limit.CreateRegisteredLimit(self.app, None) def test_registered_limit_create_without_options(self): - self.registered_limit_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGISTERED_LIMIT), loaded=True + self.identity_sdk_client.create_registered_limit.return_value = ( + self.registered_limit ) - resource_name = identity_fakes.registered_limit_resource_name - default_limit = identity_fakes.registered_limit_default_limit arglist = [ '--service', - identity_fakes.service_id, + self.service.id, '--default-limit', - '10', - resource_name, + str(self.default_limit), + self.resource_name, ] verifylist = [ - ('service', identity_fakes.service_id), - ('default_limit', default_limit), - ('resource_name', resource_name), + ('service', self.service.id), + ('default_limit', self.default_limit), + ('resource_name', self.resource_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': None, 'region': None} - self.registered_limit_mock.create.assert_called_with( - self.service, resource_name, default_limit, **kwargs + kwargs = { + 'service_id': self.service.id, + 'default_limit': self.default_limit, + 'resource_name': self.resource_name, + } + self.identity_sdk_client.create_registered_limit.assert_called_with( + **kwargs ) collist = ( @@ -89,51 +96,52 @@ def test_registered_limit_create_without_options(self): self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_create_with_options(self): - self.registered_limit_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.REGISTERED_LIMIT_OPTIONS), - loaded=True, + self.identity_sdk_client.create_registered_limit.return_value = ( + self.registered_limit_with_options ) - resource_name = identity_fakes.registered_limit_resource_name - default_limit = identity_fakes.registered_limit_default_limit - description = identity_fakes.registered_limit_description arglist = [ '--region', - identity_fakes.region_id, + self.region.id, '--description', - description, + self.description, '--service', - identity_fakes.service_id, + self.service.id, '--default-limit', - '10', - resource_name, + str(self.default_limit), + self.resource_name, ] verifylist = [ - ('region', identity_fakes.region_id), - ('description', description), - ('service', identity_fakes.service_id), - ('default_limit', default_limit), - ('resource_name', resource_name), + ('region', self.region.id), + ('description', self.description), + ('service', self.service.id), + ('default_limit', self.default_limit), + ('resource_name', self.resource_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': description, 'region': self.region} - self.registered_limit_mock.create.assert_called_with( - self.service, resource_name, default_limit, **kwargs + kwargs = { + 'description': self.description, + 'region_id': self.region.id, + 'service_id': self.service.id, + 'default_limit': self.default_limit, + 'resource_name': self.resource_name, + } + self.identity_sdk_client.create_registered_limit.assert_called_with( + **kwargs ) collist = ( @@ -147,41 +155,44 @@ def test_registered_limit_create_with_options(self): self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, - description, - identity_fakes.registered_limit_id, - identity_fakes.region_id, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.default_limit, + self.description, + self.registered_limit_with_options.id, + self.region.id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestRegisteredLimitDelete(TestRegisteredLimit): +class TestRegisteredLimitDelete(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() self.cmd = registered_limit.DeleteRegisteredLimit(self.app, None) def test_registered_limit_delete(self): - self.registered_limit_mock.delete.return_value = None + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + ) + self.identity_sdk_client.delete_registered_limit.return_value = None - arglist = [identity_fakes.registered_limit_id] - verifylist = [ - ('registered_limits', [identity_fakes.registered_limit_id]) - ] + arglist = [self.registered_limit.id] + verifylist = [('registered_limits', [self.registered_limit.id])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.registered_limit_mock.delete.assert_called_with( - identity_fakes.registered_limit_id + self.identity_sdk_client.delete_registered_limit.assert_called_with( + self.registered_limit.id, + ignore_missing=False, ) self.assertIsNone(result) def test_registered_limit_delete_with_exception(self): - return_value = ksa_exceptions.NotFound() - self.registered_limit_mock.delete.side_effect = return_value + self.identity_sdk_client.delete_registered_limit.side_effect = ( + sdk_exc.ResourceNotFound + ) arglist = ['fake-registered-limit-id'] verifylist = [('registered_limits', ['fake-registered-limit-id'])] @@ -196,27 +207,52 @@ def test_registered_limit_delete_with_exception(self): ) -class TestRegisteredLimitShow(TestRegisteredLimit): +class TestRegisteredLimitShow(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - self.registered_limit_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGISTERED_LIMIT), loaded=True + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + + self.description = 'default limit of foobars' + self.default_limit = 10 + self.resource_name = 'foobars' + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.registered_limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=self.description, + region_id=self.region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) self.cmd = registered_limit.ShowRegisteredLimit(self.app, None) def test_registered_limit_show(self): - arglist = [identity_fakes.registered_limit_id] - verifylist = [ - ('registered_limit_id', identity_fakes.registered_limit_id) - ] + self.identity_sdk_client.get_registered_limit.return_value = ( + self.registered_limit + ) + + arglist = [self.registered_limit.id] + verifylist = [('registered_limit_id', self.registered_limit.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.get.assert_called_with( - identity_fakes.registered_limit_id + self.identity_sdk_client.get_registered_limit.assert_called_with( + self.registered_limit.id ) collist = ( @@ -229,50 +265,107 @@ def test_registered_limit_show(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, + ) + self.assertEqual(datalist, data) + + def test_registered_limit_show_with_options(self): + self.identity_sdk_client.get_registered_limit.return_value = ( + self.registered_limit_with_options + ) + + arglist = [self.registered_limit_with_options.id] + verifylist = [ + ('registered_limit_id', self.registered_limit_with_options.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.get_registered_limit.assert_called_with( + self.registered_limit_with_options.id + ) + + collist = ( + 'default_limit', + 'description', + 'id', + 'region_id', + 'resource_name', + 'service_id', + ) + self.assertEqual(collist, columns) + datalist = ( + self.default_limit, + self.description, + self.registered_limit_with_options.id, + self.region.id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestRegisteredLimitSet(TestRegisteredLimit): +class TestRegisteredLimitSet(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() + + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + + self.default_limit = 10 + self.resource_name = 'foobars' + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.cmd = registered_limit.SetRegisteredLimit(self.app, None) def test_registered_limit_set_description(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - registered_limit['description'] = ( - identity_fakes.registered_limit_description + updated_description = 'default limit of foobars' + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=updated_description, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) arglist = [ '--description', - identity_fakes.registered_limit_description, - identity_fakes.registered_limit_id, + updated_description, + self.registered_limit.id, ] verifylist = [ - ('description', identity_fakes.registered_limit_description), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('description', updated_description), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=None, - default_limit=None, - description=identity_fakes.registered_limit_description, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + description=updated_description, ) collist = ( @@ -285,43 +378,46 @@ def test_registered_limit_set_description(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, - identity_fakes.registered_limit_description, - identity_fakes.registered_limit_id, + self.default_limit, + updated_description, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_default_limit(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - default_limit = 20 - registered_limit['default_limit'] = default_limit - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_default_limit = 20 + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=updated_default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) arglist = [ '--default-limit', - str(default_limit), - identity_fakes.registered_limit_id, + str(updated_default_limit), + self.registered_limit.id, ] verifylist = [ - ('default_limit', default_limit), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('default_limit', updated_default_limit), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=None, - default_limit=default_limit, - description=None, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + default_limit=updated_default_limit, ) collist = ( @@ -334,43 +430,46 @@ def test_registered_limit_set_default_limit(self): ) self.assertEqual(collist, columns) datalist = ( - default_limit, + updated_default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_resource_name(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - resource_name = 'volumes' - registered_limit['resource_name'] = resource_name - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_resource_name = 'volumes' + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=updated_resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) arglist = [ '--resource-name', - resource_name, - identity_fakes.registered_limit_id, + updated_resource_name, + self.registered_limit.id, ] verifylist = [ - ('resource_name', resource_name), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('resource_name', updated_resource_name), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=resource_name, - default_limit=None, - description=None, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + resource_name=updated_resource_name, ) collist = ( @@ -383,40 +482,43 @@ def test_registered_limit_set_resource_name(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - resource_name, - identity_fakes.service_id, + updated_resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_service(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - service = identity_fakes.FakeService.create_one_service() - registered_limit['service_id'] = service.id - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_service = sdk_fakes.generate_fake_resource(_service.Service) + self.identity_sdk_client.find_service.return_value = updated_service + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=None, + service_id=updated_service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) - self.services_mock.get.return_value = service - arglist = ['--service', service.id, identity_fakes.registered_limit_id] + arglist = ['--service', updated_service.id, self.registered_limit.id] verifylist = [ - ('service', service.id), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('service', updated_service.id), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=service, - resource_name=None, - default_limit=None, - description=None, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + service_id=updated_service.id, ) collist = ( @@ -429,42 +531,43 @@ def test_registered_limit_set_service(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - service.id, + self.resource_name, + updated_service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_region(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - region = identity_fakes.REGION - region['id'] = 'RegionTwo' - region = fakes.FakeResource(None, copy.deepcopy(region), loaded=True) - registered_limit['region_id'] = region.id - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_region = sdk_fakes.generate_fake_resource(_region.Region) + self.identity_sdk_client.get_region.return_value = updated_region + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=updated_region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) - self.regions_mock.get.return_value = region - arglist = ['--region', region.id, identity_fakes.registered_limit_id] + arglist = ['--region', updated_region.id, self.registered_limit.id] verifylist = [ - ('region', region.id), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('region', updated_region.id), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=None, - default_limit=None, - description=None, - region=region, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + region_id=updated_region.id, ) collist = ( @@ -477,54 +580,86 @@ def test_registered_limit_set_region(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, - region.id, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.registered_limit.id, + updated_region.id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestRegisteredLimitList(TestRegisteredLimit): +class TestRegisteredLimitList(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - self.registered_limit_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGISTERED_LIMIT), loaded=True - ) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) - self.cmd = registered_limit.ShowRegisteredLimit(self.app, None) + self.description = 'default limit of foobars' + self.default_limit = 10 + self.resource_name = 'foobars' - def test_limit_show(self): - arglist = [identity_fakes.registered_limit_id] - verifylist = [ - ('registered_limit_id', identity_fakes.registered_limit_id) + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.registered_limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=self.description, + region_id=self.region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.registered_limits.return_value = [ + self.registered_limit, + self.registered_limit_with_options, ] + + self.cmd = registered_limit.ListRegisteredLimit(self.app, None) + + def test_registered_limit_list(self): + arglist = [] + verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.get.assert_called_with( - identity_fakes.registered_limit_id - ) - + self.identity_sdk_client.registered_limits.assert_called_with() collist = ( - 'default_limit', - 'description', - 'id', - 'region_id', - 'resource_name', - 'service_id', + "ID", + "Service ID", + "Resource Name", + "Default Limit", + "Description", + "Region ID", ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, - None, - identity_fakes.registered_limit_id, - None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, - ) - self.assertEqual(datalist, data) + ( + self.registered_limit.id, + self.service.id, + self.resource_name, + self.default_limit, + None, + None, + ), + ( + self.registered_limit_with_options.id, + self.service.id, + self.resource_name, + self.default_limit, + self.description, + self.region.id, + ), + ) + self.assertEqual(datalist, tuple(data)) diff --git a/releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml b/releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml new file mode 100644 index 000000000..77f404a9f --- /dev/null +++ b/releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Migrate ``registered limit`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``registered limit`` commands is now case sensitive. + - | + Specifying ``--region None`` is no longer supported for ``registered limit`` + commands. From 5b5287355efda03173dea279bc5d08e4753849aa Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Dec 2025 13:26:08 +0000 Subject: [PATCH 02/31] network: Move TaaS commands to a separate entrypoint group Resolve a TODO from the review round. Change-Id: I819f6b8545081037a500bebefcbde898e62fdcc5 Signed-off-by: Stephen Finucane --- openstackclient/network/client.py | 1 + openstackclient/shell.py | 33 ++++++++++++++++++++++++++++--- pyproject.toml | 7 +------ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 5165e1ecd..82a47fb25 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -23,6 +23,7 @@ API_VERSION_OPTION = 'os_network_api_version' API_NAME = 'network' API_VERSIONS = ('2.0', '2') +API_EXTENSIONS = ('taas',) def make_client(instance): diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6bbbb5f7b..56b315878 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -118,15 +118,42 @@ def _load_plugins(self): }, ) - # Command groups deal only with major versions - version = '.v' + version_opt.replace('.', '_').split('_')[0] - cmd_group = 'openstack.' + api.replace('-', '_') + version + # Build our command group which we expect to look like: + # + # openstack..vN + # + # Note that command groups deal only with major versions + cmd_group = '.'.join( + [ + 'openstack', + api.replace('-', '_'), + 'v' + version_opt.replace('.', '_').split('_')[0], + ] + ) self.command_manager.add_command_group(cmd_group) self.log.debug( '%(name)s API version %(version)s, cmd group %(group)s', {'name': api, 'version': version_opt, 'group': cmd_group}, ) + mod_extensions = getattr(mod, 'API_EXTENSIONS', None) + if not mod_extensions: + continue + + for extension in mod_extensions: + extension_cmd_group = '.'.join([cmd_group, extension]) + self.command_manager.add_command_group(extension_cmd_group) + self.log.debug( + '%(name)s API version %(version)s ' + '(%(extension)s extension), cmd group %(group)s', + { + 'name': api, + 'version': version_opt, + 'extension': extension, + 'group': cmd_group, + }, + ) + def _load_commands(self): """Load commands via cliff/stevedore diff --git a/pyproject.toml b/pyproject.toml index dc2cbf386..95b6de098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ authors = [ readme = {file = "README.rst", content-type = "text/x-rst"} license = {text = "Apache-2.0"} dynamic = ["version", "dependencies"] -# dependencies = [ ] requires-python = ">=3.10" classifiers = [ "Environment :: OpenStack", @@ -27,10 +26,6 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] -# [project.optional-dependencies] -# test = [ -# ] - [project.urls] Homepage = "https://docs.openstack.org/python-openstackclient/" Repository = "https://opendev.org/openstack/python-openstackclient/" @@ -540,7 +535,7 @@ subnet_pool_set = "openstackclient.network.v2.subnet_pool:SetSubnetPool" subnet_pool_show = "openstackclient.network.v2.subnet_pool:ShowSubnetPool" subnet_pool_unset = "openstackclient.network.v2.subnet_pool:UnsetSubnetPool" -# Tap-as-a-Service +[project.entry-points."openstack.network.v2.taas"] tap_flow_create = "openstackclient.network.v2.taas.tap_flow:CreateTapFlow" tap_flow_delete = "openstackclient.network.v2.taas.tap_flow:DeleteTapFlow" tap_flow_list = "openstackclient.network.v2.taas.tap_flow:ListTapFlow" From ea0a2c126ca0720e21fdd203d173c15b23160691 Mon Sep 17 00:00:00 2001 From: Jan Ueberacker Date: Tue, 20 Jan 2026 10:50:48 +0100 Subject: [PATCH 03/31] Fix: Make server resize options required Change-Id: I7e8f4e750822a2e8c85826f49c56e10f7605fedb Signed-off-by: Jan Ueberacker --- openstackclient/compute/v2/server.py | 2 +- .../tests/unit/compute/v2/test_server.py | 14 ++++++-------- ...size-server-args-required-2e9013bcbf207f6a.yaml | 8 ++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1eee828d9..b4488c3f1 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -4267,7 +4267,7 @@ def get_parser(self, prog_name): metavar='', help=_('Server (name or ID)'), ) - phase_group = parser.add_mutually_exclusive_group() + phase_group = parser.add_mutually_exclusive_group(required=True) phase_group.add_argument( '--flavor', metavar='', diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 059731251..8da351676 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -7896,15 +7896,13 @@ def test_server_resize_no_options(self): ('server', self.server.id), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - self.compute_client.find_server.assert_called_once_with( - self.server.id, ignore_missing=False + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, ) - self.compute_client.find_flavor.assert_not_called() - self.compute_client.resize_server.assert_not_called() - self.assertIsNone(result) def test_server_resize(self): arglist = [ diff --git a/releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml b/releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml new file mode 100644 index 000000000..e1faf7e60 --- /dev/null +++ b/releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The ``openstack server resize`` command now requires the ``--flavor`` + option or one of the deprecated ``--confirm`` or ``--revert`` options + to be provided. + Previously, the command would silently exit successfully without + performing any action if no option was provided. From 911e643f2c6ff15072ce558d4f8b547af8505155 Mon Sep 17 00:00:00 2001 From: 0weng Date: Tue, 26 Nov 2024 08:37:38 -0800 Subject: [PATCH 04/31] Identity: Migrate 'limit' commands to SDK Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/937397 Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/942818 Change-Id: I3b9833d5cbb1f7275ceee56cb2599b76e878e98e Signed-off-by: 0weng --- openstackclient/identity/v3/limit.py | 161 ++++----- .../tests/unit/identity/v3/test_limit.py | 334 ++++++++++-------- ...migrate-limit-to-sdk-378037ec2b79e302.yaml | 9 + 3 files changed, 275 insertions(+), 229 deletions(-) create mode 100644 releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py index 15da04369..4671c5acb 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -25,6 +25,28 @@ LOG = logging.getLogger(__name__) +def _format_limit(limit): + columns = ( + "description", + "id", + "project_id", + "region_id", + "resource_limit", + "resource_name", + "service_id", + ) + column_headers = ( + "description", + "id", + "project_id", + "region_id", + "resource_limit", + "resource_name", + "service_id", + ) + return (column_headers, utils.get_item_properties(limit, columns)) + + class CreateLimit(command.ShowOne): _description = _("Create a limit") @@ -67,47 +89,33 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - project = common_utils.find_project( - identity_client, parsed_args.project + identity_client = self.app.client_manager.sdk_connection.identity + + kwargs = { + "resource_name": parsed_args.resource_name, + "resource_limit": parsed_args.resource_limit, + } + if parsed_args.description: + kwargs["description"] = parsed_args.description + + # TODO(0weng): Add --project-domain option + # to support filtering project domain + kwargs["project_id"] = common_utils._find_sdk_id( + identity_client.find_project, + name_or_id=parsed_args.project, ) - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) - region = None + ).id + if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - limit = identity_client.limits.create( - project, - service, - parsed_args.resource_name, - parsed_args.resource_limit, - description=parsed_args.description, - region=region, - ) + limit = identity_client.create_limit(**kwargs) - limit._info.pop('links', None) - return zip(*sorted(limit._info.items())) + return _format_limit(limit) class ListLimit(command.Lister): @@ -139,47 +147,31 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - service = None + kwargs = {} if parsed_args.service: - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service ) - region = None + if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - project = None + # TODO(0weng): Add --project-domain option + # to support filtering project domain if parsed_args.project: - project = utils.find_resource( - identity_client.projects, parsed_args.project + kwargs["project_id"] = common_utils._find_sdk_id( + identity_client.find_project, + name_or_id=parsed_args.project, ) - limits = identity_client.limits.list( - service=service, - resource_name=parsed_args.resource_name, - region=region, - project=project, - ) + if parsed_args.resource_name: + kwargs["resource_name"] = parsed_args.resource_name + + limits = identity_client.limits(**kwargs) columns = ( 'ID', @@ -209,10 +201,9 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - limit = identity_client.limits.get(parsed_args.limit_id) - limit._info.pop('links', None) - return zip(*sorted(limit._info.items())) + identity_client = self.app.client_manager.sdk_connection.identity + limit = identity_client.get_limit(parsed_args.limit_id) + return _format_limit(limit) class SetLimit(command.ShowOne): @@ -240,17 +231,16 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - limit = identity_client.limits.update( - parsed_args.limit_id, - description=parsed_args.description, - resource_limit=parsed_args.resource_limit, - ) + identity_client = self.app.client_manager.sdk_connection.identity - limit._info.pop('links', None) + kwargs = {} + if parsed_args.description: + kwargs["description"] = parsed_args.description + if parsed_args.resource_limit: + kwargs["resource_limit"] = parsed_args.resource_limit + limit = identity_client.update_limit(parsed_args.limit_id, **kwargs) - return zip(*sorted(limit._info.items())) + return _format_limit(limit) class DeleteLimit(command.Command): @@ -262,17 +252,20 @@ def get_parser(self, prog_name): 'limit_id', metavar='', nargs="+", - help=_('Limit to delete (ID)'), + help=_( + 'Limit to delete (ID) ' + '(repeat option to remove multiple limits)' + ), ) return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity errors = 0 for limit_id in parsed_args.limit_id: try: - identity_client.limits.delete(limit_id) + identity_client.delete_limit(limit_id) except Exception as e: errors += 1 LOG.error( diff --git a/openstackclient/tests/unit/identity/v3/test_limit.py b/openstackclient/tests/unit/identity/v3/test_limit.py index a1d180b4b..a0c045e66 100644 --- a/openstackclient/tests/unit/identity/v3/test_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_limit.py @@ -10,87 +10,80 @@ # License for the specific language governing permissions and limitations # under the License. -import copy - -from keystoneauth1.exceptions import http as ksa_exceptions +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import limit as _limit +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import region as _region +from openstack.identity.v3 import service as _service +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.identity.v3 import limit -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestLimit(identity_fakes.TestIdentityv3): +class TestLimitCreate(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - identity_manager = self.identity_client - - self.limit_mock = identity_manager.limits - - self.services_mock = identity_manager.services - self.services_mock.reset_mock() - - self.projects_mock = identity_manager.projects - self.projects_mock.reset_mock() - - self.regions_mock = identity_manager.regions - self.regions_mock.reset_mock() + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.resource_limit = 15 -class TestLimitCreate(TestLimit): - def setUp(self): - super().setUp() + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + self.identity_sdk_client.find_project.return_value = self.project - self.service = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.SERVICE), loaded=True - ) - self.services_mock.get.return_value = self.service - - self.project = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.PROJECT), loaded=True + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=self.resource_limit, + region_id=None, ) - self.projects_mock.get.return_value = self.project - - self.region = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGION), loaded=True + self.limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_limit=self.resource_limit, + resource_name='foobars', + description='test description', + region_id=self.region.id, ) - self.regions_mock.get.return_value = self.region self.cmd = limit.CreateLimit(self.app, None) def test_limit_create_without_options(self): - self.limit_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT), loaded=True - ) + self.identity_sdk_client.create_limit.return_value = self.limit - resource_limit = 15 arglist = [ '--project', - identity_fakes.project_id, + self.project.id, '--service', - identity_fakes.service_id, + self.service.id, '--resource-limit', - str(resource_limit), - identity_fakes.limit_resource_name, + str(self.resource_limit), + self.limit.resource_name, ] verifylist = [ - ('project', identity_fakes.project_id), - ('service', identity_fakes.service_id), - ('resource_name', identity_fakes.limit_resource_name), - ('resource_limit', resource_limit), + ('project', self.project.id), + ('service', self.service.id), + ('resource_name', self.limit.resource_name), + ('resource_limit', self.resource_limit), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': None, 'region': None} - self.limit_mock.create.assert_called_with( - self.project, - self.service, - identity_fakes.limit_resource_name, - resource_limit, - **kwargs, + self.identity_sdk_client.create_limit.assert_called_with( + project_id=self.project.id, + service_id=self.service.id, + resource_name=self.limit.resource_name, + resource_limit=self.resource_limit, ) collist = ( @@ -105,55 +98,55 @@ def test_limit_create_without_options(self): self.assertEqual(collist, columns) datalist = ( None, - identity_fakes.limit_id, - identity_fakes.project_id, + self.limit.id, + self.project.id, None, - resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + self.resource_limit, + self.limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_limit_create_with_options(self): - self.limit_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT_OPTIONS), loaded=True + self.identity_sdk_client.create_limit.return_value = ( + self.limit_with_options ) resource_limit = 15 arglist = [ '--project', - identity_fakes.project_id, + self.project.id, '--service', - identity_fakes.service_id, + self.service.id, '--resource-limit', str(resource_limit), '--region', - identity_fakes.region_id, + self.region.id, '--description', - identity_fakes.limit_description, - identity_fakes.limit_resource_name, + self.limit_with_options.description, + self.limit_with_options.resource_name, ] verifylist = [ - ('project', identity_fakes.project_id), - ('service', identity_fakes.service_id), - ('resource_name', identity_fakes.limit_resource_name), + ('project', self.project.id), + ('service', self.service.id), + ('resource_name', self.limit_with_options.resource_name), ('resource_limit', resource_limit), - ('region', identity_fakes.region_id), - ('description', identity_fakes.limit_description), + ('region', self.region.id), + ('description', self.limit_with_options.description), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) kwargs = { - 'description': identity_fakes.limit_description, - 'region': self.region, + 'project_id': self.project.id, + 'service_id': self.service.id, + 'region_id': self.region.id, + 'resource_name': self.limit_with_options.resource_name, + 'resource_limit': resource_limit, + 'description': self.limit_with_options.description, } - self.limit_mock.create.assert_called_with( - self.project, - self.service, - identity_fakes.limit_resource_name, - resource_limit, + self.identity_sdk_client.create_limit.assert_called_with( **kwargs, ) @@ -168,37 +161,41 @@ def test_limit_create_with_options(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.limit_description, - identity_fakes.limit_id, - identity_fakes.project_id, - identity_fakes.region_id, + self.limit_with_options.description, + self.limit_with_options.id, + self.project.id, + self.region.id, resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + self.limit_with_options.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestLimitDelete(TestLimit): +class TestLimitDelete(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() self.cmd = limit.DeleteLimit(self.app, None) def test_limit_delete(self): - self.limit_mock.delete.return_value = None + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit + ) + self.identity_sdk_client.delete_limit.return_value = None - arglist = [identity_fakes.limit_id] - verifylist = [('limit_id', [identity_fakes.limit_id])] + arglist = [self.limit.id] + verifylist = [('limit_id', [self.limit.id])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.limit_mock.delete.assert_called_with(identity_fakes.limit_id) + self.identity_sdk_client.delete_limit.assert_called_with(self.limit.id) self.assertIsNone(result) def test_limit_delete_with_exception(self): - return_value = ksa_exceptions.NotFound() - self.limit_mock.delete.side_effect = return_value + self.identity_sdk_client.delete_limit.side_effect = ( + sdk_exc.ResourceNotFound + ) arglist = ['fake-limit-id'] verifylist = [('limit_id', ['fake-limit-id'])] @@ -211,24 +208,42 @@ def test_limit_delete_with_exception(self): self.assertEqual('1 of 1 limits failed to delete.', str(e)) -class TestLimitShow(TestLimit): +class TestLimitShow(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - self.limit_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT), loaded=True + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + + self.resource_limit = 15 + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + self.identity_sdk_client.find_project.return_value = self.project + + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=self.resource_limit, + region_id=None, ) + self.identity_sdk_client.get_limit.return_value = self.limit + self.cmd = limit.ShowLimit(self.app, None) def test_limit_show(self): - arglist = [identity_fakes.limit_id] - verifylist = [('limit_id', identity_fakes.limit_id)] + arglist = [self.limit.id] + verifylist = [('limit_id', self.limit.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.get.assert_called_with(identity_fakes.limit_id) + self.identity_sdk_client.get_limit.assert_called_with(self.limit.id) collist = ( 'description', @@ -242,45 +257,61 @@ def test_limit_show(self): self.assertEqual(collist, columns) datalist = ( None, - identity_fakes.limit_id, - identity_fakes.project_id, + self.limit.id, + self.project.id, None, - identity_fakes.limit_resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + self.resource_limit, + self.limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestLimitSet(TestLimit): +class TestLimitSet(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + + self.resource_limit = 15 + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + self.identity_sdk_client.find_project.return_value = self.project + self.cmd = limit.SetLimit(self.app, None) def test_limit_set_description(self): - limit = copy.deepcopy(identity_fakes.LIMIT) - limit['description'] = identity_fakes.limit_description - self.limit_mock.update.return_value = fakes.FakeResource( - None, limit, loaded=True + description = 'limit of foobars' + limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=description, + resource_limit=self.resource_limit, + region_id=None, ) + self.identity_sdk_client.update_limit.return_value = limit arglist = [ '--description', - identity_fakes.limit_description, - identity_fakes.limit_id, + description, + limit.id, ] verifylist = [ - ('description', identity_fakes.limit_description), - ('limit_id', identity_fakes.limit_id), + ('description', description), + ('limit_id', limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.update.assert_called_with( - identity_fakes.limit_id, - description=identity_fakes.limit_description, - resource_limit=None, + self.identity_sdk_client.update_limit.assert_called_with( + limit.id, + description=description, ) collist = ( @@ -294,40 +325,44 @@ def test_limit_set_description(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.limit_description, - identity_fakes.limit_id, - identity_fakes.project_id, + description, + limit.id, + self.project.id, None, - identity_fakes.limit_resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + limit.resource_limit, + limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_limit_set_resource_limit(self): resource_limit = 20 - limit = copy.deepcopy(identity_fakes.LIMIT) - limit['resource_limit'] = resource_limit - self.limit_mock.update.return_value = fakes.FakeResource( - None, limit, loaded=True + limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=resource_limit, + region_id=None, ) + self.identity_sdk_client.update_limit.return_value = limit arglist = [ '--resource-limit', str(resource_limit), - identity_fakes.limit_id, + limit.id, ] verifylist = [ ('resource_limit', resource_limit), - ('limit_id', identity_fakes.limit_id), + ('limit_id', limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.update.assert_called_with( - identity_fakes.limit_id, - description=None, + self.identity_sdk_client.update_limit.assert_called_with( + limit.id, resource_limit=resource_limit, ) @@ -343,25 +378,36 @@ def test_limit_set_resource_limit(self): self.assertEqual(collist, columns) datalist = ( None, - identity_fakes.limit_id, - identity_fakes.project_id, + limit.id, + self.project.id, None, resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestLimitList(TestLimit): +class TestLimitList(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - self.limit_mock.list.return_value = [ - fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT), loaded=True - ) - ] + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + + self.resource_limit = 15 + + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=self.resource_limit, + region_id=None, + ) + self.identity_sdk_client.limits.return_value = [self.limit] self.cmd = limit.ListLimit(self.app, None) @@ -372,9 +418,7 @@ def test_limit_list(self): columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.list.assert_called_with( - service=None, resource_name=None, region=None, project=None - ) + self.identity_sdk_client.limits.assert_called_with() collist = ( 'ID', @@ -388,11 +432,11 @@ def test_limit_list(self): self.assertEqual(collist, columns) datalist = ( ( - identity_fakes.limit_id, - identity_fakes.project_id, - identity_fakes.service_id, - identity_fakes.limit_resource_name, - identity_fakes.limit_resource_limit, + self.limit.id, + self.project.id, + self.service.id, + self.limit.resource_name, + self.limit.resource_limit, None, None, ), diff --git a/releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml b/releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml new file mode 100644 index 000000000..d4a57329e --- /dev/null +++ b/releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Migrate ``limit`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``limit`` commands is now case sensitive. + - | + Specifying ``--region None`` is no longer supported for ``limit`` commands. From 85e731df4715536da91a8afedbe32da7de65e53a Mon Sep 17 00:00:00 2001 From: 0weng Date: Wed, 29 Oct 2025 10:50:45 -0700 Subject: [PATCH 05/31] Identity: Migrate 'project' commands to SDK Change-Id: I0f673658bc02423c18af82fe52ed9f0587763882 Signed-off-by: 0weng --- openstackclient/identity/common.py | 31 + openstackclient/identity/v3/project.py | 288 ++-- openstackclient/identity/v3/tag.py | 11 - .../tests/unit/identity/v3/test_project.py | 1343 ++++++++++------- ...grate-project-to-sdk-9201efd2804371de.yaml | 7 + 5 files changed, 1046 insertions(+), 634 deletions(-) create mode 100644 releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 068476470..f33c37033 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -256,6 +256,37 @@ def find_project(identity_client, name_or_id, domain_name_or_id=None): ) +def find_project_id_sdk( + identity_client, + name_or_id, + domain_name_or_id=None, + *, + validate_actor_existence=True, + validate_domain_actor_existence=None, +): + if domain_name_or_id is None: + return _find_sdk_id( + identity_client.find_project, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + ) + + if validate_domain_actor_existence is None: + validate_domain_actor_existence = validate_actor_existence + + domain_id = find_domain_id_sdk( + identity_client, + name_or_id=domain_name_or_id, + validate_actor_existence=validate_domain_actor_existence, + ) + return _find_sdk_id( + identity_client.find_project, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + domain_id=domain_id, + ) + + def find_user(identity_client, name_or_id, domain_name_or_id=None): if domain_name_or_id is None: return _find_identity_resource( diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index e70a8a501..7c27bb718 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -17,7 +17,7 @@ import logging -from keystoneauth1 import exceptions as ks_exc +from openstack import exceptions as sdk_exc from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils @@ -30,6 +30,21 @@ LOG = logging.getLogger(__name__) +def _format_project(project): + # NOTE(0weng): Projects allow unknown attributes in the body, so extract + # the column names separately. + (column_headers, columns) = utils.get_osc_show_columns_for_sdk_resource( + project, + {'is_enabled': 'enabled'}, + ['links', 'location', 'parents_as_ids', 'subtree_as_ids'], + ) + + return ( + column_headers, + utils.get_item_properties(project, columns), + ) + + class CreateProject(command.ShowOne): _description = _("Create new project") @@ -90,22 +105,13 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - domain = None - if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain).id - - parent = None - if parsed_args.parent: - parent = utils.find_resource( - identity_client.projects, - parsed_args.parent, - ).id + identity_client = self.app.client_manager.sdk_connection.identity kwargs = {} + if parsed_args.properties: kwargs = parsed_args.properties.copy() + if 'is_domain' in kwargs.keys(): if kwargs['is_domain'].lower() == "true": kwargs['is_domain'] = True @@ -114,35 +120,54 @@ def take_action(self, parsed_args): elif kwargs['is_domain'].lower() == "none": kwargs['is_domain'] = None - kwargs['tags'] = list(set(parsed_args.tags)) + if parsed_args.description: + kwargs['description'] = parsed_args.description + + if parsed_args.name: + kwargs['name'] = parsed_args.name + + domain = None + if parsed_args.domain: + domain = common.find_domain_id_sdk( + identity_client, parsed_args.domain + ) + kwargs['domain_id'] = domain + + if parsed_args.parent: + kwargs['parent_id'] = common.find_project_id_sdk( + identity_client, + parsed_args.parent, + ) + + kwargs['is_enabled'] = parsed_args.enabled + + if parsed_args.tags: + kwargs['tags'] = list(set(parsed_args.tags)) - options = {} if parsed_args.immutable is not None: - options['immutable'] = parsed_args.immutable + kwargs['options'] = {'immutable': parsed_args.immutable} try: - project = identity_client.projects.create( - name=parsed_args.name, - domain=domain, - parent=parent, - description=parsed_args.description, - enabled=parsed_args.enabled, - options=options, + project = identity_client.create_project( **kwargs, ) - except ks_exc.Conflict: + except sdk_exc.ConflictException: if parsed_args.or_show: - project = utils.find_resource( - identity_client.projects, - parsed_args.name, - domain_id=domain, - ) + if parsed_args.domain: + project = identity_client.find_project( + parsed_args.name, + domain_id=domain, + ignore_missing=False, + ) + else: + project = identity_client.find_project( + parsed_args.name, ignore_missing=False + ) LOG.info(_('Returning existing project %s'), project.name) else: raise - project._info.pop('links') - return zip(*sorted(project._info.items())) + return _format_project(project) class DeleteProject(command.Command): @@ -171,23 +196,19 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - domain = None - if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) errors = 0 for project in parsed_args.projects: try: - if domain is not None: - project_obj = utils.find_resource( - identity_client.projects, project, domain_id=domain.id - ) - else: - project_obj = utils.find_resource( - identity_client.projects, project - ) - identity_client.projects.delete(project_obj.id) + project = common.find_project_id_sdk( + identity_client, + project, + domain_name_or_id=parsed_args.domain, + validate_actor_existence=True, + validate_domain_actor_existence=False, + ) + identity_client.delete_project(project) except Exception as e: errors += 1 LOG.error( @@ -268,38 +289,44 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - columns: tuple[str, ...] = ('ID', 'Name') + identity_client = self.app.client_manager.sdk_connection.identity + + column_headers: tuple[str, ...] = ('ID', 'Name') if parsed_args.long: - columns += ('Domain ID', 'Description', 'Enabled') + column_headers += ('Domain ID', 'Description', 'Enabled') + + columns: tuple[str, ...] = ('id', 'name') + if parsed_args.long: + columns += ('domain_id', 'description', 'is_enabled') + kwargs = {} domain_id = None if parsed_args.domain: - domain_id = common.find_domain( + domain_id = common.find_domain_id_sdk( identity_client, parsed_args.domain - ).id - kwargs['domain'] = domain_id + ) + kwargs['domain_id'] = domain_id if parsed_args.parent: - parent_id = common.find_project( + parent_id = common.find_project_id_sdk( identity_client, parsed_args.parent - ).id - kwargs['parent'] = parent_id + ) + kwargs['parent_id'] = parent_id + user = None if parsed_args.user: if parsed_args.domain: - user_id = utils.find_resource( - identity_client.users, + user = common.find_user_id_sdk( + identity_client, parsed_args.user, - domain_id=domain_id, - ).id + domain_name_or_id=domain_id, + ) else: - user_id = utils.find_resource( - identity_client.users, parsed_args.user - ).id - - kwargs['user'] = user_id + user = common.find_user_id_sdk( + identity_client, + parsed_args.user, + ) if parsed_args.is_enabled is not None: kwargs['is_enabled'] = parsed_args.is_enabled @@ -308,32 +335,29 @@ def take_action(self, parsed_args): if parsed_args.my_projects: # NOTE(adriant): my-projects supersedes all the other filters. - kwargs = {'user': self.app.client_manager.auth_ref.user_id} + kwargs = {} + user = self.app.client_manager.auth_ref.user_id - try: - data = identity_client.projects.list(**kwargs) - except ks_exc.Forbidden: - # NOTE(adriant): if no filters, assume a forbidden is non-admin - # wanting their own project list. - if not kwargs: - user = self.app.client_manager.auth_ref.user_id - data = identity_client.projects.list(user=user) - else: - raise + if user: + data = identity_client.user_projects(user, **kwargs) + else: + try: + data = identity_client.projects(**kwargs) + except sdk_exc.ForbiddenException: + # NOTE(adriant): if no filters, assume a forbidden is non-admin + # wanting their own project list. + if not kwargs: + user = self.app.client_manager.auth_ref.user_id + data = identity_client.user_projects(user) + else: + raise if parsed_args.sort: data = utils.sort_items(data, parsed_args.sort) return ( - columns, - ( - utils.get_item_properties( - s, - columns, - formatters={}, - ) - for s in data - ), + column_headers, + (utils.get_item_properties(s, columns) for s in data), ) @@ -392,11 +416,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - project = common.find_project( - identity_client, parsed_args.project, parsed_args.domain - ) + identity_client = self.app.client_manager.sdk_connection.identity kwargs = {} if parsed_args.name: @@ -409,9 +429,50 @@ def take_action(self, parsed_args): kwargs['options'] = {'immutable': parsed_args.immutable} if parsed_args.properties: kwargs.update(parsed_args.properties) - tag.update_tags_in_args(parsed_args, project, kwargs) - identity_client.projects.update(project.id, **kwargs) + if parsed_args.domain: + domain = common.find_domain_id_sdk( + identity_client, + parsed_args.domain, + validate_actor_existence=False, + ) + project = identity_client.find_project( + parsed_args.project, + domain_id=domain, + ignore_missing=True, + ) + else: + project = identity_client.find_project( + parsed_args.project, + ignore_missing=True, + ) + + if ( + parsed_args.tags + or parsed_args.remove_tags + or parsed_args.clear_tags + ): + existing_tags = [] + if project: + existing_tags = project.tags + + if parsed_args.clear_tags: + kwargs['tags'] = [] + else: + existing_tags_set = set(existing_tags) + if parsed_args.remove_tags: + tags = sorted( + existing_tags_set - set(parsed_args.remove_tags) + ) + if parsed_args.tags: + tags = sorted( + existing_tags_set.union(set(parsed_args.tags)) + ) + kwargs['tags'] = tags + + project_id = project.id if project else parsed_args.project + + identity_client.update_project(project_id, **kwargs) class ShowProject(command.ShowOne): @@ -444,31 +505,36 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - project_str = common._get_token_resource( - identity_client, 'project', parsed_args.project, parsed_args.domain - ) + kwargs = {} + domain = None if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) - project = utils.find_resource( - identity_client.projects, project_str, domain_id=domain.id - ) - else: - project = utils.find_resource( - identity_client.projects, project_str + domain = common.find_domain_id_sdk( + identity_client, parsed_args.domain ) - if parsed_args.parents or parsed_args.children: - # NOTE(RuiChen): utils.find_resource() can't pass kwargs, - # if id query hit the result at first, so call - # identity manager.get() with kwargs directly. - project = identity_client.projects.get( - project.id, - parents_as_ids=parsed_args.parents, - subtree_as_ids=parsed_args.children, - ) + kwargs['domain_id'] = domain + + # Get project id first; otherwise, find_project() can't find + # parents/children if only project name was given + project = common.find_project_id_sdk( + identity_client, + parsed_args.project, + domain_name_or_id=domain, + validate_actor_existence=False, + validate_domain_actor_existence=False, + ) + + # Include these options as query parameters if they are provided + if parsed_args.parents: + kwargs['parents_as_ids'] = True + if parsed_args.children: + kwargs['subtree_as_ids'] = True + + project = identity_client.find_project( + project, **kwargs, ignore_missing=False + ) - project._info.pop('links') - return zip(*sorted(project._info.items())) + return _format_project(project) diff --git a/openstackclient/identity/v3/tag.py b/openstackclient/identity/v3/tag.py index 41493c993..f49a1ca98 100644 --- a/openstackclient/identity/v3/tag.py +++ b/openstackclient/identity/v3/tag.py @@ -123,14 +123,3 @@ def add_tag_option_to_parser_for_set(parser, resource_name): ) % resource_name, ) - - -def update_tags_in_args(parsed_args, obj, args): - if parsed_args.clear_tags: - args['tags'] = [] - obj.tags = [] - if parsed_args.remove_tags: - args['tags'] = sorted(set(obj.tags) - set(parsed_args.remove_tags)) - return - if parsed_args.tags: - args['tags'] = sorted(set(obj.tags).union(set(parsed_args.tags))) diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 065a65cb1..dae1aebc8 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -13,31 +13,20 @@ # under the License. from unittest import mock -from unittest.mock import call +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions -from osc_lib import utils -from openstackclient.identity import common from openstackclient.identity.v3 import project from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestProject(identity_fakes.TestIdentityv3): - def setUp(self): - super().setUp() - - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - self.domains_mock.reset_mock() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestProjectCreate(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestProjectCreate(identity_fakes.TestIdentityv3): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( 'description', @@ -46,39 +35,41 @@ class TestProjectCreate(TestProject): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'tags', ) + project_kwargs_no_options = { + 'description': None, + 'domain_id': None, + 'enabled': True, + 'is_domain': False, + 'parent_id': None, + 'tags': [], + } + def setUp(self): super().setUp() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id} - ) - self.domains_mock.get.return_value = self.domain - self.projects_mock.create.return_value = self.project - self.datalist = ( - self.project.description, - self.project.domain_id, - True, - self.project.id, - False, - self.project.name, - self.project.parent_id, - self.project.tags, - ) + self.identity_sdk_client.find_domain.return_value = self.domain + # Get the command object to test self.cmd = project.CreateProject(self.app, None) def test_project_create_no_options(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ - self.project.name, + project.name, ] verifylist = [ ('parent', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -88,53 +79,43 @@ def test_project_create_no_options(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) - collist = ( - 'description', - 'domain_id', - 'enabled', - 'id', - 'is_domain', - 'name', - 'parent_id', - 'tags', - ) - self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, + None, + None, True, - self.project.id, + project.id, False, - self.project.name, - self.project.parent_id, - self.project.tags, + project.name, + {}, + None, + [], ) self.assertEqual(datalist, data) def test_project_create_description(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, description='new desc'), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--description', 'new desc', - self.project.name, + project.name, ] verifylist = [ ('description', 'new desc'), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -145,33 +126,43 @@ def test_project_create_description(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, + 'name': project.name, 'description': 'new desc', - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + 'new desc', + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_domain(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, domain_id=self.domain.id), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--domain', - self.project.domain_id, - self.project.name, + project.domain_id, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), + ('domain', project.domain_id), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -184,63 +175,90 @@ def test_project_create_domain(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'domain_id': project.domain_id, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_domain_no_perms(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, domain_id=self.domain.id), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--domain', - self.project.domain_id, - self.project.name, + project.domain_id, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), + ('domain', project.domain_id), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - mocker = mock.Mock() - mocker.return_value = None - with mock.patch("osc_lib.utils.find_resource", mocker): - columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_domain.side_effect = ( + sdk_exc.ForbiddenException + ) + self.identity_sdk_client.find_domain.return_value = None + + columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'domain_id': project.domain_id, + 'is_enabled': True, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_enable(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, enabled=True), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--enable', - self.project.name, + project.name, ] verifylist = [ ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -253,29 +271,39 @@ def test_project_create_enable(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_disable(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, enabled=False), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--disable', - self.project.name, + project.name, ] verifylist = [ ('enabled', False), - ('name', self.project.name), + ('name', project.name), ('parent', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -287,32 +315,42 @@ def test_project_create_disable(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': False, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'is_enabled': False, } - # ProjectManager.create(name=, domain=, - # description=, enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + False, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, fee='fi', fo='fum'), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'fee=fi', '--property', 'fo=fum', - self.project.name, + project.name, ] verifylist = [ + ('name', project.name), ('properties', {'fee': 'fi', 'fo': 'fum'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -323,36 +361,62 @@ def test_project_create_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'fee': 'fi', 'fo': 'fum', - 'tags': [], - 'options': {}, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual( + ( + 'description', + 'domain_id', + 'enabled', + 'fee', + 'fo', + 'id', + 'is_domain', + 'name', + 'options', + 'parent_id', + 'tags', + ), + columns, + ) + datalist = ( + None, + None, + True, + 'fi', + 'fum', + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_is_domain_false_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, is_domain=False), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'is_domain=false', - self.project.name, + project.name, ] verifylist = [ ('parent', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ('properties', {'is_domain': 'false'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -364,33 +428,44 @@ def test_project_create_is_domain_false_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'is_domain': False, - 'tags': [], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_is_domain_true_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, is_domain=True), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'is_domain=true', - self.project.name, + project.name, ] verifylist = [ ('parent', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ('properties', {'is_domain': 'true'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -402,33 +477,44 @@ def test_project_create_is_domain_true_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'is_domain': True, - 'tags': [], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + True, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_is_domain_none_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, is_domain=None), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'is_domain=none', - self.project.name, + project.name, ] verifylist = [ ('parent', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ('properties', {'is_domain': 'none'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -440,40 +526,51 @@ def test_project_create_is_domain_none_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'is_domain': None, - 'tags': [], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + None, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_parent(self): - self.parent = identity_fakes.FakeProject.create_one_project() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id, 'parent_id': self.parent.id} + parent = sdk_fakes.generate_fake_resource(_project.Project) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + domain_id=self.domain.id, + parent_id=parent.id, + ), ) - self.projects_mock.get.return_value = self.parent - self.projects_mock.create.return_value = self.project + self.identity_sdk_client.find_project.return_value = parent + self.identity_sdk_client.create_project.return_value = project arglist = [ '--domain', - self.project.domain_id, + project.domain_id, '--parent', - self.parent.name, - self.project.name, + parent.name, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), - ('parent', self.parent.name), + ('domain', project.domain_id), + ('parent', parent.name), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -481,61 +578,52 @@ def test_project_create_parent(self): columns, data = self.cmd.take_action(parsed_args) kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'parent': self.parent.id, - 'description': None, - 'enabled': True, - 'tags': [], - 'options': {}, + 'name': project.name, + 'domain_id': project.domain_id, + 'parent_id': parent.id, + 'is_enabled': True, } + self.identity_sdk_client.create_project.assert_called_with(**kwargs) - self.projects_mock.create.assert_called_with(**kwargs) - - collist = ( - 'description', - 'domain_id', - 'enabled', - 'id', - 'is_domain', - 'name', - 'parent_id', - 'tags', - ) - self.assertEqual(columns, collist) + self.assertEqual(self.columns, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.parent.id, - self.project.tags, + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + parent.id, + [], ) self.assertEqual(data, datalist) def test_project_create_invalid_parent(self): - self.projects_mock.resource_class.__name__ = 'Project' - self.projects_mock.get.side_effect = exceptions.NotFound( - 'Invalid parent' + self.identity_sdk_client.find_project.side_effect = ( + sdk_exc.ResourceNotFound ) - self.projects_mock.find.side_effect = exceptions.NotFound( - 'Invalid parent' + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + domain_id=self.domain.id, + parent_id='invalid', + ), ) arglist = [ '--domain', - self.project.domain_id, + project.domain_id, '--parent', 'invalid', - self.project.name, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), + ('domain', project.domain_id), ('parent', 'invalid'), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -546,17 +634,27 @@ def test_project_create_invalid_parent(self): ) def test_project_create_with_tags(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + domain_id=self.domain.id, + tags=['foo'], + ), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--domain', - self.project.domain_id, + project.domain_id, '--tag', 'foo', - self.project.name, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), + ('domain', project.domain_id), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', ['foo']), ] @@ -569,29 +667,45 @@ def test_project_create_with_tags(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'domain_id': project.domain_id, + 'is_enabled': True, 'tags': ['foo'], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + None, + ['foo'], + ) + self.assertEqual(datalist, data) def test_project_create_with_immutable_option(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, options={'immutable': True} + ), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--immutable', - self.project.name, + project.name, ] verifylist = [ ('immutable', True), ('description', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -604,31 +718,44 @@ def test_project_create_with_immutable_option(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], + 'name': project.name, + 'is_enabled': True, 'options': {'immutable': True}, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {'immutable': True}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_with_no_immutable_option(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, options={'immutable': False} + ), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--no-immutable', - self.project.name, + project.name, ] verifylist = [ ('immutable', False), ('description', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -641,36 +768,121 @@ def test_project_create_with_no_immutable_option(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], + 'name': project.name, + 'is_enabled': True, 'options': {'immutable': False}, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {'immutable': False}, + None, + [], + ) + self.assertEqual(datalist, data) + def test_project_create_conflict_with_or_show(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) + self.identity_sdk_client.create_project.side_effect = ( + sdk_exc.ConflictException + ) + self.identity_sdk_client.find_project.return_value = project + + arglist = [ + '--or-show', + project.name, + ] + verifylist = [ + ('or_show', True), + ('description', None), + ('enabled', True), + ('name', project.name), + ('parent', None), + ('tags', []), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'name': project.name, + 'is_enabled': True, + } + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) + + def test_project_create_conflict_without_or_show(self): + self.identity_sdk_client.create_project.side_effect = ( + sdk_exc.ConflictException + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) -class TestProjectDelete(TestProject): - project = identity_fakes.FakeProject.create_one_project() + arglist = [ + project.name, + ] + verifylist = [ + ('or_show', False), + ('description', None), + ('enabled', True), + ('name', project.name), + ('parent', None), + ('tags', []), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + sdk_exc.ConflictException, + self.cmd.take_action, + parsed_args, + ) + + +class TestProjectDelete(identity_fakes.TestIdentityv3): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.projects_mock.get.return_value = self.project - self.projects_mock.delete.return_value = None + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.project_with_domain = sdk_fakes.generate_fake_resource( + _project.Project, + name=self.project.name, + domain_id=self.domain.id, + ) + self.identity_sdk_client.delete_project.return_value = None # Get the command object to test self.cmd = project.DeleteProject(self.app, None) def test_project_delete_no_options(self): + self.identity_sdk_client.find_project.return_value = self.project + arglist = [ self.project.id, ] @@ -681,16 +893,72 @@ def test_project_delete_no_options(self): result = self.cmd.take_action(parsed_args) - self.projects_mock.delete.assert_called_with( + self.identity_sdk_client.delete_project.assert_called_with( self.project.id, ) self.assertIsNone(result) - @mock.patch.object(utils, 'find_resource') - def test_delete_multi_projects_with_exception(self, find_mock): - find_mock.side_effect = [self.project, exceptions.CommandError] + def test_project_multi_delete(self): + self.identity_sdk_client.find_project.side_effect = [ + self.project, + self.project_with_domain, + ] + arglist = [self.project.id, self.project_with_domain.id] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.delete_project.assert_has_calls( + [ + mock.call(self.project.id), + mock.call(self.project_with_domain.id), + ] + ) + self.assertIsNone(result) + + def test_project_delete_with_forbidden_domain(self): + self.identity_sdk_client.find_domain.side_effect = [ + sdk_exc.ForbiddenException + ] + self.identity_sdk_client.find_project.return_value = ( + self.project_with_domain + ) + + arglist = [ + '--domain', + self.project_with_domain.domain_id, + self.project_with_domain.name, + ] + verifylist = [ + ('domain', self.domain.id), + ('projects', [self.project_with_domain.name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_project.assert_called_with( + name_or_id=self.project_with_domain.name, + ignore_missing=False, + domain_id=self.domain.id, + ) + self.identity_sdk_client.delete_project.assert_called_once_with( + self.project_with_domain.id + ) + self.assertIsNone(result) + + def test_delete_multi_projects_with_exception(self): + self.identity_sdk_client.find_project.side_effect = [ + self.project, + self.project_with_domain, + sdk_exc.NotFoundException, + ] + arglist = [ self.project.id, + self.project_with_domain.id, 'unexist_project', ] verifylist = [ @@ -702,21 +970,36 @@ def test_delete_multi_projects_with_exception(self, find_mock): self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: - self.assertEqual('1 of 2 projects failed to delete.', str(e)) + self.assertEqual('1 of 3 projects failed to delete.', str(e)) - find_mock.assert_any_call(self.projects_mock, self.project.id) - find_mock.assert_any_call(self.projects_mock, 'unexist_project') + self.identity_sdk_client.find_project.assert_has_calls( + [ + mock.call(name_or_id=self.project.id, ignore_missing=False), + mock.call( + name_or_id=self.project_with_domain.id, + ignore_missing=False, + ), + mock.call(name_or_id='unexist_project', ignore_missing=False), + ] + ) - self.assertEqual(2, find_mock.call_count) - self.projects_mock.delete.assert_called_once_with(self.project.id) + self.assertEqual(3, self.identity_sdk_client.find_project.call_count) + self.identity_sdk_client.delete_project.assert_has_calls( + [ + mock.call(self.project.id), + mock.call(self.project_with_domain.id), + ] + ) -class TestProjectList(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() - project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': domain.id} +class TestProjectList(identity_fakes.TestIdentityv3): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) + project = sdk_fakes.generate_fake_resource( + _project.Project, domain_id=domain.id + ) + projects = list( + sdk_fakes.generate_fake_resources(_project.Project, count=2) ) - projects = identity_fakes.FakeProject.create_projects() columns = ( 'ID', @@ -746,12 +1029,12 @@ class TestProjectList(TestProject): def setUp(self): super().setUp() - self.projects_mock.list.return_value = [self.project] - # Get the command object to test self.cmd = project.ListProject(self.app, None) def test_project_list_no_options(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -760,12 +1043,14 @@ def test_project_list_no_options(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_with() self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_long(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [ '--long', ] @@ -778,7 +1063,7 @@ def test_project_list_long(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_with() collist = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') self.assertEqual(collist, columns) @@ -794,6 +1079,8 @@ def test_project_list_long(self): self.assertEqual(datalist, tuple(data)) def test_project_list_domain(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [ '--domain', self.project.domain_id, @@ -802,7 +1089,7 @@ def test_project_list_domain(self): ('domain', self.project.domain_id), ] - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_domain.return_value = self.domain parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -810,14 +1097,16 @@ def test_project_list_domain(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with( - domain=self.project.domain_id + self.identity_sdk_client.projects.assert_called_with( + domain_id=self.project.domain_id ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_domain_no_perms(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [ '--domain', self.project.domain_id, @@ -826,23 +1115,30 @@ def test_project_list_domain_no_perms(self): ('domain', self.project.domain_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - mocker = mock.Mock() - mocker.return_value = None - with mock.patch("osc_lib.utils.find_resource", mocker): - columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_project.side_effect = ( + sdk_exc.ResourceNotFound + ) + self.identity_sdk_client.find_domain.return_value = self.domain + + columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with( - domain=self.project.domain_id + self.identity_sdk_client.projects.assert_called_with( + domain_id=self.project.domain_id ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_parent(self): - self.parent = identity_fakes.FakeProject.create_one_project() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id, 'parent_id': self.parent.id} + self.parent = sdk_fakes.generate_fake_resource(_project.Project) + self.project = sdk_fakes.generate_fake_resource( + _project.Project, + id=self.project.id, + name=self.project.name, + domain_id=self.domain.id, + parent_id=self.parent.id, ) + self.identity_sdk_client.projects.return_value = [self.project] arglist = [ '--parent', @@ -852,18 +1148,48 @@ def test_project_list_parent(self): ('parent', self.parent.id), ] - self.projects_mock.get.return_value = self.parent + self.identity_sdk_client.find_project.return_value = self.parent + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.projects.assert_called_with( + parent_id=self.parent.id + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_project_list_user(self): + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.project = sdk_fakes.generate_fake_resource( + _project.UserProject, + id=self.project.id, + name=self.project.name, + user_id=self.user.id, + ) + self.identity_sdk_client.user_projects.return_value = [self.project] + + arglist = [ + '--user', + self.user.id, + ] + verifylist = [ + ('user', self.user.id), + ] + + self.identity_sdk_client.find_user.return_value = self.user parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with(parent=self.parent.id) + self.identity_sdk_client.user_projects.assert_called_with(self.user.id) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_sort(self): - self.projects_mock.list.return_value = self.projects + self.identity_sdk_client.projects.return_value = self.projects arglist = [ '--sort', @@ -877,7 +1203,7 @@ def test_project_list_sort(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. (columns, data) = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_with() collist = ('ID', 'Name') self.assertEqual(collist, columns) @@ -896,6 +1222,8 @@ def test_project_list_sort(self): self.assertEqual(datalists, tuple(data)) def test_project_list_my_projects(self): + self.identity_sdk_client.user_projects.return_value = [self.project] + auth_ref = identity_fakes.fake_auth_ref( identity_fakes.TOKEN_WITH_PROJECT_ID, ) @@ -913,8 +1241,8 @@ def test_project_list_my_projects(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with( - user=self.app.client_manager.auth_ref.user_id + self.identity_sdk_client.user_projects.assert_called_with( + self.app.client_manager.auth_ref.user_id ) collist = ('ID', 'Name') @@ -928,6 +1256,8 @@ def test_project_list_my_projects(self): self.assertEqual(datalist, tuple(data)) def test_project_list_with_option_enabled(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = ['--enabled'] verifylist = [('is_enabled', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -938,25 +1268,28 @@ def test_project_list_with_option_enabled(self): columns, data = self.cmd.take_action(parsed_args) kwargs = {'is_enabled': True} - self.projects_mock.list.assert_called_with(**kwargs) + self.identity_sdk_client.projects.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) -class TestProjectSet(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() - project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': domain.id, 'tags': ['tag1', 'tag2', 'tag3']} +class TestProjectSet(identity_fakes.TestIdentityv3): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) + + project_kwargs_no_options = { + 'domain_id': domain.id, + 'tags': ['tag1', 'tag2', 'tag3'], + } + project = sdk_fakes.generate_fake_resource( + _project.Project, **project_kwargs_no_options ) def setUp(self): super().setUp() - self.domains_mock.get.return_value = self.domain - - self.projects_mock.get.return_value = self.project - self.projects_mock.update.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project # Get the command object to test self.cmd = project.SetProject(self.app, None) @@ -997,9 +1330,10 @@ def test_project_set_name(self): kwargs = { 'name': 'qwerty', } - # ProjectManager.update(project, name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_description(self): @@ -1024,7 +1358,9 @@ def test_project_set_description(self): kwargs = { 'description': 'new desc', } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_enable(self): @@ -1047,7 +1383,9 @@ def test_project_set_enable(self): kwargs = { 'enabled': True, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_disable(self): @@ -1070,7 +1408,9 @@ def test_project_set_disable(self): kwargs = { 'enabled': False, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_property(self): @@ -1097,7 +1437,9 @@ def test_project_set_property(self): 'fee': 'fi', 'fo': 'fum', } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_tags(self): @@ -1112,7 +1454,7 @@ def test_project_set_tags(self): ] verifylist = [ ('name', 'qwerty'), - ('domain', self.project.domain_id), + ('domain', self.domain.id), ('enabled', None), ('project', self.project.name), ('tags', ['foo']), @@ -1126,9 +1468,9 @@ def test_project_set_tags(self): 'name': 'qwerty', 'tags': sorted({'tag1', 'tag2', 'tag3', 'foo'}), } - # ProjectManager.update(project, name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_remove_tags(self): @@ -1149,7 +1491,9 @@ def test_project_remove_tags(self): result = self.cmd.take_action(parsed_args) kwargs = {'tags': list({'tag3'})} - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_with_immutable_option(self): @@ -1173,7 +1517,9 @@ def test_project_set_with_immutable_option(self): kwargs = { 'options': {'immutable': True}, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_with_no_immutable_option(self): @@ -1197,114 +1543,108 @@ def test_project_set_with_no_immutable_option(self): kwargs = { 'options': {'immutable': False}, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) -class TestProjectShow(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestProjectShow(identity_fakes.TestIdentityv3): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) + + columns = ( + 'description', + 'domain_id', + 'enabled', + 'id', + 'is_domain', + 'name', + 'options', + 'parent_id', + 'tags', + ) + + project_kwargs_no_options = { + 'description': None, + 'domain_id': None, + 'enabled': True, + 'is_domain': False, + 'parent_id': None, + 'tags': [], + } def setUp(self): super().setUp() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id} - ) - # Get the command object to test self.cmd = project.ShowProject(self.app, None) def test_project_show(self): - self.projects_mock.get.return_value = self.project + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } - # In base command class ShowOne in cliff, abstract method take_action() # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_once_with(self.project.id) - - collist = ( - 'description', - 'domain_id', - 'enabled', - 'id', - 'is_domain', - 'name', - 'parent_id', - 'tags', + self.identity_sdk_client.find_project.assert_called_with( + project.id, ignore_missing=False ) - self.assertEqual(collist, columns) + + self.assertEqual(self.columns, columns) datalist = ( - self.project.description, - self.project.domain_id, + None, + None, True, - self.project.id, + project.id, False, - self.project.name, - self.project.parent_id, - self.project.tags, + project.name, + {}, + None, + [], ) self.assertEqual(datalist, data) def test_project_show_parents(self): - self.project = identity_fakes.FakeProject.create_one_project( - attrs={ - 'parent_id': self.project.parent_id, - 'parents': [{'project': {'id': self.project.parent_id}}], - } + parent = sdk_fakes.generate_fake_resource( + _project.Project, parent_id='default' + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + parent_id=parent.id, + parents={parent.id: {parent.parent_id: None}}, + ), ) - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, '--parents', ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ('parents', True), ('children', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_has_calls( - [ - call(self.project.id), - call( - self.project.id, - parents_as_ids=True, - subtree_as_ids=False, - ), - ] + self.identity_sdk_client.find_project.assert_called_with( + project.id, parents_as_ids=True, ignore_missing=False ) collist = ( @@ -1314,63 +1654,51 @@ def test_project_show_parents(self): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'parents', 'tags', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.project.parent_id, - [{'project': {'id': self.project.parent_id}}], - self.project.tags, + None, + None, + True, + project.id, + False, + project.name, + {}, + parent.id, + {parent.id: {'default': None}}, + [], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_project_show_subtree(self): - self.project = identity_fakes.FakeProject.create_one_project( - attrs={ - 'parent_id': self.project.parent_id, - 'subtree': [{'project': {'id': 'children-id'}}], - } + child = sdk_fakes.generate_fake_resource( + _project.Project, subtree=None + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, subtree={child.id: None}), ) - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, '--children', ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ('parents', False), ('children', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_has_calls( - [ - call(self.project.id), - call( - self.project.id, - parents_as_ids=False, - subtree_as_ids=True, - ), - ] + + self.identity_sdk_client.find_project.assert_called_with( + project.id, subtree_as_ids=True, ignore_missing=False ) collist = ( @@ -1380,65 +1708,63 @@ def test_project_show_subtree(self): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'subtree', 'tags', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.project.parent_id, - [{'project': {'id': 'children-id'}}], - self.project.tags, + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + {child.id: None}, + [], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_project_show_parents_and_children(self): - self.project = identity_fakes.FakeProject.create_one_project( - attrs={ - 'parent_id': self.project.parent_id, - 'parents': [{'project': {'id': self.project.parent_id}}], - 'subtree': [{'project': {'id': 'children-id'}}], - } + parent = sdk_fakes.generate_fake_resource( + _project.Project, parent_id='default' + ) + child = sdk_fakes.generate_fake_resource( + _project.Project, subtree=None ) - self.projects_mock.get.return_value = self.project + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + parent_id=parent.id, + parents={parent.id: {parent.parent_id: None}}, + subtree={child.id: None}, + ), + ) + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, '--parents', '--children', ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ('parents', True), ('children', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_has_calls( - [ - call(self.project.id), - call( - self.project.id, - parents_as_ids=True, - subtree_as_ids=True, - ), - ] + + self.identity_sdk_client.find_project.assert_called_with( + project.id, + parents_as_ids=True, + subtree_as_ids=True, + ignore_missing=False, ) collist = ( @@ -1448,42 +1774,36 @@ def test_project_show_parents_and_children(self): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'parents', 'subtree', 'tags', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.project.parent_id, - [{'project': {'id': self.project.parent_id}}], - [{'project': {'id': 'children-id'}}], - self.project.tags, + None, + None, + True, + project.id, + False, + project.name, + {}, + parent.id, + {parent.id: {'default': None}}, + {child.id: None}, + [], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_project_show_with_domain(self): - project = identity_fakes.FakeProject.create_one_project( - {"name": self.project.name} - ) - - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {"id": self.project.domain_id}, - 'name': self.project.name, - 'id': self.project.id, - } - } - } + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, domain_id=self.domain.id), + ) + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.find_project.return_value = project - identity_client = self.identity_client arglist = [ "--domain", self.domain.id, @@ -1495,23 +1815,22 @@ def test_project_show_with_domain(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - project_str = common._get_token_resource( - identity_client, 'project', parsed_args.project, parsed_args.domain + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.find_project.assert_called_with( + project.id, domain_id=self.domain.id, ignore_missing=False ) - self.assertEqual(self.project.id, project_str) - arglist = [ - "--domain", - project.domain_id, + self.assertEqual(self.columns, columns) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, project.name, - ] - verifylist = [ - ('domain', project.domain_id), - ('project', project.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - project_str = common._get_token_resource( - identity_client, 'project', parsed_args.project, parsed_args.domain + {}, + None, + [], ) - self.assertEqual(project.name, project_str) + self.assertEqual(datalist, data) diff --git a/releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml b/releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml new file mode 100644 index 000000000..90c603174 --- /dev/null +++ b/releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``project`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``project`` commands is now case sensitive. From 8668f2632a3de46d1d8f69c2749d1e498bb5c7e3 Mon Sep 17 00:00:00 2001 From: 0weng Date: Fri, 23 Jan 2026 14:58:46 -0800 Subject: [PATCH 06/31] Identity: Use project's domain for its parent When creating or listing projects, search for parent project in the same domain as the child project. Change-Id: I1912f06df353a64eb0573f080ac9ab067cb90632 Signed-off-by: 0weng --- openstackclient/identity/v3/project.py | 5 ++++- .../use-project-domain-for-parent-cb29ee3f5adeb647.yaml | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 7c27bb718..99242391f 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -137,6 +137,7 @@ def take_action(self, parsed_args): kwargs['parent_id'] = common.find_project_id_sdk( identity_client, parsed_args.parent, + domain_name_or_id=domain, ) kwargs['is_enabled'] = parsed_args.enabled @@ -310,7 +311,9 @@ def take_action(self, parsed_args): if parsed_args.parent: parent_id = common.find_project_id_sdk( - identity_client, parsed_args.parent + identity_client, + parsed_args.parent, + domain_name_or_id=domain_id, ) kwargs['parent_id'] = parent_id diff --git a/releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml b/releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml new file mode 100644 index 000000000..acbf2ba6c --- /dev/null +++ b/releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + When creating or listing projects, parent project will now be searched from + the same domain as the child project. From ea1f8134c2f08749756044014299bf9ba12cc1f8 Mon Sep 17 00:00:00 2001 From: 0weng Date: Tue, 13 Jan 2026 15:28:42 -0800 Subject: [PATCH 07/31] Identity: Add --project-domain option for limits Change-Id: Ia0930c8dbd3325e0eeadf91716e7dfaabbfd1978 Signed-off-by: 0weng --- openstackclient/identity/v3/limit.py | 22 ++++++--- .../functional/identity/v3/test_limit.py | 47 +++++++++++++++++++ .../tests/unit/identity/v3/test_limit.py | 10 +++- ...roject-domain-option-84bfbb0e30e21b73.yaml | 5 ++ 4 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py index 4671c5acb..94f6bcaf0 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -68,6 +68,7 @@ def get_parser(self, prog_name): required=True, help=_('Project to associate the resource limit to'), ) + common_utils.add_project_domain_option_to_parser(parser) parser.add_argument( '--service', metavar='', @@ -98,12 +99,12 @@ def take_action(self, parsed_args): if parsed_args.description: kwargs["description"] = parsed_args.description - # TODO(0weng): Add --project-domain option - # to support filtering project domain - kwargs["project_id"] = common_utils._find_sdk_id( - identity_client.find_project, - name_or_id=parsed_args.project, + kwargs["project_id"] = common_utils.find_project_id_sdk( + identity_client, + parsed_args.project, + domain_name_or_id=parsed_args.project_domain, ) + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service ).id @@ -144,6 +145,8 @@ def get_parser(self, prog_name): metavar='', help=_('List resource limits associated with project'), ) + common_utils.add_project_domain_option_to_parser(parser) + return parser def take_action(self, parsed_args): @@ -160,12 +163,17 @@ def take_action(self, parsed_args): parsed_args.region ).id - # TODO(0weng): Add --project-domain option - # to support filtering project domain if parsed_args.project: + project_domain_id = None + if parsed_args.project_domain: + project_domain_id = common_utils.find_domain_id_sdk( + identity_client, parsed_args.project_domain + ) + kwargs["project_id"] = common_utils._find_sdk_id( identity_client.find_project, name_or_id=parsed_args.project, + domain_id=project_domain_id, ) if parsed_args.resource_name: diff --git a/openstackclient/tests/functional/identity/v3/test_limit.py b/openstackclient/tests/functional/identity/v3/test_limit.py index 8c0bbcd6a..497d33a9c 100644 --- a/openstackclient/tests/functional/identity/v3/test_limit.py +++ b/openstackclient/tests/functional/identity/v3/test_limit.py @@ -100,6 +100,53 @@ def test_limit_create_with_project_name(self): self.assert_show_fields(items, self.LIMIT_FIELDS) registered_limit_id = self._create_dummy_registered_limit() + def test_limit_create_with_project_domain(self): + registered_limit_id = self._create_dummy_registered_limit() + raw_output = self.openstack( + f'registered limit show {registered_limit_id}', + cloud=SYSTEM_CLOUD, + ) + items = self.parse_show(raw_output) + service_id = self._extract_value_from_items('service_id', items) + resource_name = self._extract_value_from_items('resource_name', items) + + raw_output = self.openstack(f'service show {service_id}') + items = self.parse_show(raw_output) + service_name = self._extract_value_from_items('name', items) + + project_name = self._create_dummy_project() + raw_output = self.openstack( + f'project show {project_name}', + cloud=SYSTEM_CLOUD, + ) + items = self.parse_show(raw_output) + domain_id = self._extract_value_from_items('domain_id', items) + + params = { + 'project_name': project_name, + 'project_domain': domain_id, + 'service_name': service_name, + 'resource_name': resource_name, + 'resource_limit': 15, + } + raw_output = self.openstack( + 'limit create' + ' --project {project_name}' + ' --project-domain {project_domain}' + ' --service {service_name}' + ' --resource-limit {resource_limit}' + ' {resource_name}'.format(**params), + cloud=SYSTEM_CLOUD, + ) + items = self.parse_show(raw_output) + limit_id = self._extract_value_from_items('id', items) + self.addCleanup( + self.openstack, f'limit delete {limit_id}', cloud=SYSTEM_CLOUD + ) + + self.assert_show_fields(items, self.LIMIT_FIELDS) + registered_limit_id = self._create_dummy_registered_limit() + def test_limit_create_with_service_id(self): self._create_dummy_limit() diff --git a/openstackclient/tests/unit/identity/v3/test_limit.py b/openstackclient/tests/unit/identity/v3/test_limit.py index a0c045e66..4c2184c2a 100644 --- a/openstackclient/tests/unit/identity/v3/test_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_limit.py @@ -11,6 +11,7 @@ # under the License. from openstack import exceptions as sdk_exc +from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import limit as _limit from openstack.identity.v3 import project as _project from openstack.identity.v3 import region as _region @@ -26,7 +27,10 @@ class TestLimitCreate(identity_fakes.TestIdentityv3): def setUp(self): super().setUp() - self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.project = sdk_fakes.generate_fake_resource( + _project.Project, domain_id=self.domain.id + ) self.region = sdk_fakes.generate_fake_resource(_region.Region) self.service = sdk_fakes.generate_fake_resource(_service.Service) @@ -35,6 +39,7 @@ def setUp(self): self.identity_sdk_client.find_service.return_value = self.service self.identity_sdk_client.get_region.return_value = self.region self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain self.limit = sdk_fakes.generate_fake_resource( resource_type=_limit.Limit, @@ -116,6 +121,8 @@ def test_limit_create_with_options(self): arglist = [ '--project', self.project.id, + '--project-domain', + self.domain.name, '--service', self.service.id, '--resource-limit', @@ -128,6 +135,7 @@ def test_limit_create_with_options(self): ] verifylist = [ ('project', self.project.id), + ('project_domain', self.domain.name), ('service', self.service.id), ('resource_name', self.limit_with_options.resource_name), ('resource_limit', resource_limit), diff --git a/releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml b/releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml new file mode 100644 index 000000000..ab9468022 --- /dev/null +++ b/releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--project-domain`` option for the ``limit list`` and ``limit create`` + commands. From d1359947728868adf4f4f6e0a385f9f347c12537 Mon Sep 17 00:00:00 2001 From: 0weng Date: Fri, 6 Feb 2026 13:56:46 -0800 Subject: [PATCH 08/31] Identity: Migrate 'federation protocol' commands to SDK Change-Id: I4f1303b7b6c7ea2e67e5770745db060f08ba7761 Signed-off-by: 0weng --- .../identity/v3/federation_protocol.py | 79 ++++---- .../tests/unit/identity/v3/test_protocol.py | 185 +++++++++--------- 2 files changed, 136 insertions(+), 128 deletions(-) diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 850ec0ac7..3e1dea14d 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -26,6 +26,15 @@ LOG = logging.getLogger(__name__) +def _format_protocol(protocol): + columns = ('name', 'idp_id', 'mapping_id') + column_headers = ('id', 'identity_provider', 'mapping') + return ( + column_headers, + utils.get_item_properties(protocol, columns), + ) + + class CreateProtocol(command.ShowOne): _description = _("Create new federation protocol") @@ -58,21 +67,15 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - protocol = identity_client.federation.protocols.create( - protocol_id=parsed_args.federation_protocol, - identity_provider=parsed_args.identity_provider, - mapping=parsed_args.mapping, + identity_client = self.app.client_manager.sdk_connection.identity + + protocol = identity_client.create_federation_protocol( + name=parsed_args.federation_protocol, + idp_id=parsed_args.identity_provider, + mapping_id=parsed_args.mapping, ) - info = dict(protocol._info) - # NOTE(marek-denis): Identity provider is not included in a response - # from Keystone, however it should be listed to the user. Add it - # manually to the output list, simply reusing value provided by the - # user. - info['identity_provider'] = parsed_args.identity_provider - info['mapping'] = info.pop('mapping_id') - info.pop('links', None) - return zip(*sorted(info.items())) + + return _format_protocol(protocol) class DeleteProtocol(command.Command): @@ -99,12 +102,15 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity + result = 0 for i in parsed_args.federation_protocol: try: - identity_client.federation.protocols.delete( - parsed_args.identity_provider, i + identity_client.delete_federation_protocol( + idp_id=parsed_args.identity_provider, + protocol=i, + ignore_missing=False, ) except Exception as e: result += 1 @@ -140,9 +146,9 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - protocols = identity_client.federation.protocols.list( + protocols = identity_client.federation_protocols( parsed_args.identity_provider ) columns = ('id', 'mapping') @@ -181,21 +187,16 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - protocol = identity_client.federation.protocols.update( - parsed_args.identity_provider, - parsed_args.federation_protocol, - parsed_args.mapping, - ) - info = dict(protocol._info) - # NOTE(marek-denis): Identity provider is not included in a response - # from Keystone, however it should be listed to the user. Add it - # manually to the output list, simply reusing value provided by the - # user. - info['identity_provider'] = parsed_args.identity_provider - info['mapping'] = info.pop('mapping_id') - return zip(*sorted(info.items())) + kwargs = {'idp_id': parsed_args.identity_provider} + if parsed_args.federation_protocol: + kwargs['name'] = parsed_args.federation_protocol + if parsed_args.mapping: + kwargs['mapping_id'] = parsed_args.mapping + + protocol = identity_client.update_federation_protocol(**kwargs) + return _format_protocol(protocol) class ShowProtocol(command.ShowOne): @@ -220,12 +221,10 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - protocol = identity_client.federation.protocols.get( - parsed_args.identity_provider, parsed_args.federation_protocol + protocol = identity_client.get_federation_protocol( + idp_id=parsed_args.identity_provider, + protocol=parsed_args.federation_protocol, ) - info = dict(protocol._info) - info['mapping'] = info.pop('mapping_id') - info.pop('links', None) - return zip(*sorted(info.items())) + return _format_protocol(protocol) diff --git a/openstackclient/tests/unit/identity/v3/test_protocol.py b/openstackclient/tests/unit/identity/v3/test_protocol.py index c85699685..4ac77d6b2 100644 --- a/openstackclient/tests/unit/identity/v3/test_protocol.py +++ b/openstackclient/tests/unit/identity/v3/test_protocol.py @@ -12,202 +12,211 @@ # License for the specific language governing permissions and limitations # under the License. -import copy - +from openstack.identity.v3 import federation_protocol as _federation_protocol +from openstack.identity.v3 import mapping as _mapping +from openstack.test import fakes as sdk_fakes from openstackclient.identity.v3 import federation_protocol -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestProtocol(identity_fakes.TestFederatedIdentity): - def setUp(self): - super().setUp() - - federation_lib = self.identity_client.federation - self.protocols_mock = federation_lib.protocols - self.protocols_mock.reset_mock() - - -class TestProtocolCreate(TestProtocol): +class TestProtocolCreate(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - proto = copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT) - resource = fakes.FakeResource(None, proto, loaded=True) - self.protocols_mock.create.return_value = resource + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol + ) + self.identity_sdk_client.create_federation_protocol.return_value = ( + self.proto + ) self.cmd = federation_protocol.CreateProtocol(self.app, None) def test_create_protocol(self): argslist = [ - identity_fakes.protocol_id, + self.proto.name, '--identity-provider', - identity_fakes.idp_id, + self.proto.idp_id, '--mapping', - identity_fakes.mapping_id, + self.proto.mapping_id, ] verifylist = [ - ('federation_protocol', identity_fakes.protocol_id), - ('identity_provider', identity_fakes.idp_id), - ('mapping', identity_fakes.mapping_id), + ('federation_protocol', self.proto.name), + ('identity_provider', self.proto.idp_id), + ('mapping', self.proto.mapping_id), ] parsed_args = self.check_parser(self.cmd, argslist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.create.assert_called_with( - protocol_id=identity_fakes.protocol_id, - identity_provider=identity_fakes.idp_id, - mapping=identity_fakes.mapping_id, + self.identity_sdk_client.create_federation_protocol.assert_called_with( + name=self.proto.id, + idp_id=self.proto.idp_id, + mapping_id=self.proto.mapping_id, ) collist = ('id', 'identity_provider', 'mapping') self.assertEqual(collist, columns) datalist = ( - identity_fakes.protocol_id, - identity_fakes.idp_id, - identity_fakes.mapping_id, + self.proto.id, + self.proto.idp_id, + self.proto.mapping_id, ) self.assertEqual(datalist, data) -class TestProtocolDelete(TestProtocol): +class TestProtocolDelete(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT), - loaded=True, + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol ) - - self.protocols_mock.delete.return_value = None + self.identity_sdk_client.delete_federation_protocol.return_value = None self.cmd = federation_protocol.DeleteProtocol(self.app, None) - def test_delete_identity_provider(self): + def test_delete_protocol(self): arglist = [ '--identity-provider', - identity_fakes.idp_id, - identity_fakes.protocol_id, + self.proto.idp_id, + self.proto.name, ] verifylist = [ - ('federation_protocol', [identity_fakes.protocol_id]), - ('identity_provider', identity_fakes.idp_id), + ('federation_protocol', [self.proto.id]), + ('identity_provider', self.proto.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.protocols_mock.delete.assert_called_with( - identity_fakes.idp_id, identity_fakes.protocol_id + self.identity_sdk_client.delete_federation_protocol.assert_called_with( + idp_id=self.proto.idp_id, + protocol=self.proto.id, + ignore_missing=False, ) self.assertIsNone(result) -class TestProtocolList(TestProtocol): +class TestProtocolList(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True + self.proto1 = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol ) - - self.protocols_mock.list.return_value = [ - fakes.FakeResource( - None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True - ) + self.proto2 = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol, idp_id=self.proto1 + ) + self.identity_sdk_client.federation_protocols.return_value = [ + self.proto1, + self.proto2, ] - self.cmd = federation_protocol.ListProtocols(self.app, None) def test_list_protocols(self): - arglist = ['--identity-provider', identity_fakes.idp_id] - verifylist = [('identity_provider', identity_fakes.idp_id)] + arglist = ['--identity-provider', self.proto1.idp_id] + verifylist = [('identity_provider', self.proto1.idp_id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.list.assert_called_with(identity_fakes.idp_id) + self.identity_sdk_client.federation_protocols.assert_called_with( + self.proto1.idp_id + ) + self.assertEqual(columns, ('id', 'mapping')) + datalist = ( + ( + self.proto1.name, + self.proto1.mapping_id, + ), + ( + self.proto2.name, + self.proto2.mapping_id, + ), + ) + self.assertEqual(datalist, tuple(data)) -class TestProtocolSet(TestProtocol): + +class TestProtocolSet(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_OUTPUT, loaded=True + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol ) - self.protocols_mock.update.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_OUTPUT_UPDATED, loaded=True + self.mapping = sdk_fakes.generate_fake_resource(_mapping.Mapping) + self.identity_sdk_client.update_federation_protocol.return_value = ( + self.proto ) - self.cmd = federation_protocol.SetProtocol(self.app, None) def test_set_new_mapping(self): arglist = [ - identity_fakes.protocol_id, + self.proto.name, '--identity-provider', - identity_fakes.idp_id, + self.proto.idp_id, '--mapping', - identity_fakes.mapping_id, + self.mapping.name, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), - ('federation_protocol', identity_fakes.protocol_id), - ('mapping', identity_fakes.mapping_id), + ('identity_provider', self.proto.idp_id), + ('federation_protocol', self.proto.name), + ('mapping', self.mapping.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.update.assert_called_with( - identity_fakes.idp_id, - identity_fakes.protocol_id, - identity_fakes.mapping_id, + self.identity_sdk_client.update_federation_protocol.assert_called_with( + idp_id=self.proto.idp_id, + name=self.proto.name, + mapping_id=self.mapping.id, ) collist = ('id', 'identity_provider', 'mapping') self.assertEqual(collist, columns) datalist = ( - identity_fakes.protocol_id, - identity_fakes.idp_id, - identity_fakes.mapping_id_updated, + self.proto.name, + self.proto.idp_id, + self.proto.mapping_id, ) self.assertEqual(datalist, data) -class TestProtocolShow(TestProtocol): +class TestProtocolShow(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_OUTPUT, loaded=False + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol + ) + self.identity_sdk_client.get_federation_protocol.return_value = ( + self.proto ) - self.cmd = federation_protocol.ShowProtocol(self.app, None) def test_show_protocol(self): arglist = [ - identity_fakes.protocol_id, + self.proto.name, '--identity-provider', - identity_fakes.idp_id, + self.proto.idp_id, ] verifylist = [ - ('federation_protocol', identity_fakes.protocol_id), - ('identity_provider', identity_fakes.idp_id), + ('federation_protocol', self.proto.name), + ('identity_provider', self.proto.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.get.assert_called_with( - identity_fakes.idp_id, identity_fakes.protocol_id + self.identity_sdk_client.get_federation_protocol.assert_called_with( + idp_id=self.proto.idp_id, protocol=self.proto.name ) collist = ('id', 'identity_provider', 'mapping') self.assertEqual(collist, columns) datalist = ( - identity_fakes.protocol_id, - identity_fakes.idp_id, - identity_fakes.mapping_id, + self.proto.name, + self.proto.idp_id, + self.proto.mapping_id, ) self.assertEqual(datalist, data) From 0803fd2112a6ebb4de7e5f95a00b9bef3e1d86f2 Mon Sep 17 00:00:00 2001 From: Piotr Sipika Date: Thu, 22 Jan 2026 16:24:18 -0500 Subject: [PATCH 09/31] Make --all-stores behave the same as in glanceclient. In order for python-openstackclient to support image imports to mutliple stores at the same time an update is needed to the --all-stores argument used by the client whereby the argument is explicitly set to contain a boolean value. This change makes the argument do what it's supposed to and do it in a way consistent with the API contract exposed by Glance. Amend tests to support the change in type of the --all-stores option. Change-Id: If5a72ca3ca68656555b5eb478e104d43f419c77e Closes-Bug: 2138903 Signed-off-by: Piotr Sipika Co-authored-by: Stephen Finucane --- openstackclient/image/v2/image.py | 1 + openstackclient/tests/unit/image/v2/test_image.py | 10 +++++----- releasenotes/notes/bug-2138903-f75c7348f22db195.yaml | 5 +++++ 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/bug-2138903-f75c7348f22db195.yaml diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index cbb6d874d..e8d040e14 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -1719,6 +1719,7 @@ def get_parser(self, prog_name): ) stores_group.add_argument( '--all-stores', + action='store_true', help=_( "Make image available to all stores " "(either '--store' or '--all-stores' required with the " diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index e6de9f2eb..c9a273ba9 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -2085,7 +2085,7 @@ def test_import_image__glance_direct(self): remote_image_id=None, remote_service_interface=None, stores=None, - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) @@ -2115,7 +2115,7 @@ def test_import_image__web_download(self): remote_image_id=None, remote_service_interface=None, stores=None, - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) @@ -2253,7 +2253,7 @@ def test_import_image__copy_image(self): remote_image_id=None, remote_service_interface=None, stores=['fast'], - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) @@ -2285,7 +2285,7 @@ def test_import_image__copy_image_disallow_failure(self): remote_image_id=None, remote_service_interface=None, stores=['fast'], - all_stores=None, + all_stores=False, all_stores_must_succeed=True, ) @@ -2320,7 +2320,7 @@ def test_import_image__glance_download(self): remote_image_id='remote-image-id', remote_service_interface='private', stores=None, - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) diff --git a/releasenotes/notes/bug-2138903-f75c7348f22db195.yaml b/releasenotes/notes/bug-2138903-f75c7348f22db195.yaml new file mode 100644 index 000000000..249242146 --- /dev/null +++ b/releasenotes/notes/bug-2138903-f75c7348f22db195.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The ``--all-stores`` of the ``image import`` command is now correctly + treated as a boolean flag. From cebf4d78d6ed251727dd1ebef3d00dd5a2c90f85 Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Wed, 4 Dec 2024 18:51:17 +0000 Subject: [PATCH 10/31] Rename openstack volume delete --purge -> --cascade This flag is called "cascade" in the Cinder API. The flag "purge" doesn't really communicate an obvious meaning in the context of volume deletion. It also has the danger of implying some kind of behavior about volume wiping that does not exist. Rename this flag to "--cascade" and preserve "--purge" as a hidden flag for compatibility. Change-Id: I8de27811222c17155697073fb9c512746d009266 Signed-off-by: Eric Harney Co-authored-by: Stephen Finucane --- .../tests/unit/volume/v2/test_volume.py | 10 ++--- .../tests/unit/volume/v3/test_volume.py | 43 ++++++++++++++----- openstackclient/volume/v2/volume.py | 11 ++++- openstackclient/volume/v3/volume.py | 15 +++++-- ...olume-delete-cascade-384003efc8896096.yaml | 6 +++ 5 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index b68020fa9..1d4be1c39 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -655,7 +655,7 @@ def test_volume_delete_one_volume(self): arglist = [self.volumes[0].id] verifylist = [ ("force", False), - ("purge", False), + ("cascade", False), ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -674,7 +674,7 @@ def test_volume_delete_multi_volumes(self): arglist = [v.id for v in self.volumes] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -701,7 +701,7 @@ def test_volume_delete_multi_volumes_with_exception(self): ] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -732,7 +732,7 @@ def test_volume_delete_with_purge(self): ] verifylist = [ ('force', False), - ('purge', True), + ('cascade', True), ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -754,7 +754,7 @@ def test_volume_delete_with_force(self): ] verifylist = [ ('force', True), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 33dcfe5a4..1669aaf8a 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -912,7 +912,7 @@ def test_volume_delete_one_volume(self): arglist = [self.volumes[0].id] verifylist = [ ("force", False), - ("purge", False), + ("cascade", False), ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -931,7 +931,7 @@ def test_volume_delete_multi_volumes(self): arglist = [v.id for v in self.volumes] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -958,7 +958,7 @@ def test_volume_delete_multi_volumes_with_exception(self): ] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -989,7 +989,29 @@ def test_volume_delete_with_purge(self): ] verifylist = [ ('force', False), - ('purge', True), + ('cascade', True), + ('volumes', [self.volumes[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.volume_sdk_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_sdk_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=True, force=False + ) + + def test_volume_delete_with_cascade(self): + arglist = [ + '--cascade', + self.volumes[0].id, + ] + verifylist = [ + ('force', False), + ('cascade', True), ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1011,7 +1033,7 @@ def test_volume_delete_with_force(self): ] verifylist = [ ('force', True), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1031,7 +1053,7 @@ def test_volume_delete_remote(self): verifylist = [ ("remote", True), ("force", False), - ("purge", False), + ("cascade", False), ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1052,7 +1074,7 @@ def test_volume_delete_multi_volumes_remote(self): verifylist = [ ('remote', True), ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist[1:]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1077,7 +1099,6 @@ def test_volume_delete_remote_with_purge(self): verifylist = [ ('remote', True), ('force', False), - ('purge', True), ('volumes', [self.volumes[0].id]), ] @@ -1086,7 +1107,7 @@ def test_volume_delete_remote_with_purge(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) self.assertIn( - "The --force and --purge options are not supported with the " + "The --force and --cascade options are not supported with the " "--remote parameter.", str(exc), ) @@ -1104,7 +1125,7 @@ def test_volume_delete_remote_with_force(self): verifylist = [ ('remote', True), ('force', True), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id]), ] @@ -1113,7 +1134,7 @@ def test_volume_delete_remote_with_force(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) self.assertIn( - "The --force and --purge options are not supported with the " + "The --force and --cascade options are not supported with the " "--remote parameter.", str(exc), ) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 61cce04f7..4dd1434f3 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -390,12 +390,19 @@ def get_parser(self, prog_name): ), ) group.add_argument( - "--purge", + "--cascade", action="store_true", help=_( "Remove any snapshots along with volume(s) (defaults to False)" ), ) + group.add_argument( + # now called "cascade", accept old arg for compatibility + "--purge", + action="store_true", + help=argparse.SUPPRESS, + dest='cascade', + ) return parser def take_action(self, parsed_args): @@ -410,7 +417,7 @@ def take_action(self, parsed_args): volume_client.delete_volume( volume_obj.id, force=parsed_args.force, - cascade=parsed_args.purge, + cascade=parsed_args.cascade, ) except Exception as e: result += 1 diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 50ea77fb5..77dab293b 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -515,12 +515,19 @@ def get_parser(self, prog_name): ), ) group.add_argument( - "--purge", + "--cascade", action="store_true", help=_( "Remove any snapshots along with volume(s) (defaults to False)" ), ) + group.add_argument( + # now called "cascade", accept old arg for compatibility + "--purge", + action="store_true", + help=argparse.SUPPRESS, + dest='cascade', + ) parser.add_argument( '--remote', action='store_true', @@ -532,9 +539,9 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.sdk_connection.volume result = 0 - if parsed_args.remote and (parsed_args.force or parsed_args.purge): + if parsed_args.remote and (parsed_args.force or parsed_args.cascade): msg = _( - "The --force and --purge options are not " + "The --force and --cascade options are not " "supported with the --remote parameter." ) raise exceptions.CommandError(msg) @@ -550,7 +557,7 @@ def take_action(self, parsed_args): volume_client.delete_volume( volume_obj.id, force=parsed_args.force, - cascade=parsed_args.purge, + cascade=parsed_args.cascade, ) except Exception as e: result += 1 diff --git a/releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml b/releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml new file mode 100644 index 000000000..679964f1b --- /dev/null +++ b/releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``--purge`` argument to the ``volume delete`` command has been renamed + to ``--cascade`` to better match the Cinder API and the meaning of what + this argument does. An alias is provided for backwards compatibility. From 1bc0d2b30690913ac60f7d19192e14af177c657e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Feb 2026 15:09:33 +0000 Subject: [PATCH 11/31] trivial: Align tox indentation with other SDK projects This makes sharing snippets between the two easier. Change-Id: I8cf32d5ffd25d1b951f654c631ef06b18a5bade5 Signed-off-by: Stephen Finucane --- tox.ini | 110 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/tox.ini b/tox.ini index 1988ec823..63692fe4a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,111 +4,111 @@ envlist = py3,pep8 [testenv] description = - Run unit tests. + Run unit tests. usedevelop = true setenv = - OS_STDOUT_CAPTURE=1 - OS_STDERR_CAPTURE=1 - OS_TEST_TIMEOUT=60 + OS_STDOUT_CAPTURE=1 + OS_STDERR_CAPTURE=1 + OS_TEST_TIMEOUT=60 deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = - stestr run {posargs} + stestr run {posargs} [testenv:pep8] description = - Run style checks. + Run style checks. skip_install = true deps = - pre-commit + pre-commit commands = - pre-commit run --all-files --show-diff-on-failure + pre-commit run --all-files --show-diff-on-failure [testenv:bandit] description = - Run bandit security checks. + Run bandit security checks. skip_install = true deps = - pre-commit + pre-commit commands = - pre-commit run --all-files --show-diff-on-failure bandit + pre-commit run --all-files --show-diff-on-failure bandit [testenv:unit-tips] commands = - python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff - python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth - python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib - python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk - python -m pip freeze - stestr run {posargs} + python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff + python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth + python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib + python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk + python -m pip freeze + stestr run {posargs} [testenv:functional{,-tips,-py310,-py311,-py312,-py313,-py314}] description = - Run functional tests. + Run functional tests. setenv = - OS_TEST_PATH=./openstackclient/tests/functional + OS_TEST_PATH=./openstackclient/tests/functional passenv = - OS_* + OS_* commands = - tips: python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff - tips: python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth1 - tips: python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib - tips: python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk - tips: python -m pip freeze - {[testenv]commands} + tips: python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff + tips: python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth1 + tips: python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib + tips: python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk + tips: python -m pip freeze + {[testenv]commands} [testenv:venv] description = - Run specified command in a virtual environment with all dependencies installed. + Run specified command in a virtual environment with all dependencies installed. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = - {posargs} + {posargs} [testenv:cover] description = - Run unit tests and generate coverage report. + Run unit tests and generate coverage report. setenv = - {[testenv]setenv} - PYTHON=coverage run --source openstackclient --parallel-mode + {[testenv]setenv} + PYTHON=coverage run --source openstackclient --parallel-mode commands = - stestr run {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml [testenv:debug] description = - Run specified tests through oslo_debug_helper, which allows use of pdb. + Run specified tests through oslo_debug_helper, which allows use of pdb. passenv = - OS_* + OS_* commands = - oslo_debug_helper -t openstackclient/tests {posargs} + oslo_debug_helper -t openstackclient/tests {posargs} [testenv:docs] description = - Build documentation in HTML format. + Build documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html - sphinx-build -a -E -W -d doc/build/doctrees -b man doc/source doc/build/man - # Validate redirects (must be done after the docs build - whereto doc/build/html/.htaccess doc/test/redirect-tests.txt + sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html + sphinx-build -a -E -W -d doc/build/doctrees -b man doc/source doc/build/man + # Validate redirects (must be done after the docs build + whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:releasenotes] description = - Build release note documentation in HTML format. + Build release note documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] show-source = true From 164e5d0f5f65460bc095611ff677295ed7f07081 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 11 Dec 2025 13:33:40 +0000 Subject: [PATCH 12/31] Run mypy from tox This avoids the need to duplicate our dependency list in multiple places and allows us to take advantage of tox's dependency management infrastructure, to ensure we always get the latest and greatest version of a package allowed by upper-constraints. While here, we also update the versions of the remaining pre-commit hooks and change the indentation of the tox file to align with the two indent spacing used for other SDK projects. This makes copy-pasting easier. Change-Id: Ibde8ecda673b2346c82aab68d4f4b49be08414ae Signed-off-by: Stephen Finucane --- .pre-commit-config.yaml | 18 ++---------------- openstackclient/common/module.py | 2 +- openstackclient/identity/v2_0/service.py | 4 +++- openstackclient/volume/v2/volume_type.py | 2 +- openstackclient/volume/v3/volume_type.py | 2 +- pyproject.toml | 10 +--------- tox.ini | 14 ++++++++------ 7 files changed, 17 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b277bb059..718c80f0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,28 +15,14 @@ repos: files: .*\.(yaml|yml)$ args: ['--unsafe'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.15.1 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes'] - id: ruff-format - repo: https://opendev.org/openstack/hacking - rev: 7.0.0 + rev: 8.0.0 hooks: - id: hacking additional_dependencies: [] exclude: '^(doc|releasenotes)/.*$' - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 - hooks: - - id: mypy - additional_dependencies: - - types-requests - # keep this in-sync with '[tool.mypy] exclude' in 'pyproject.toml' - exclude: | - (?x)( - doc/.* - | examples/.* - | hacking/.* - | releasenotes/.* - ) diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 6ca5dc231..098510751 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -61,7 +61,7 @@ def take_action(self, parsed_args): # TODO(bapalm): Fix this when cliff properly supports # handling the detection rather than using the hard-code below. if parsed_args.formatter == 'table': - command_names = utils.format_list(command_names, "\n") + command_names = utils.format_list(command_names, "\n") # type: ignore commands.append((group, command_names)) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 5e8dca735..a93a95326 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -160,7 +160,9 @@ def take_action(self, parsed_args): for service, service_endpoints in endpoints.items(): if service_endpoints: info = {"type": service} - info.update(service_endpoints[0]) + # FIXME(stephenfin): The return type for this in ksa is + # wrong + info.update(service_endpoints[0]) # type: ignore return zip(*sorted(info.items())) msg = _( diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index e7b90af95..27c7a35aa 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -466,7 +466,7 @@ def take_action(self, parsed_args): _EncryptionInfoColumn = functools.partial( EncryptionInfoColumn, encryption_data=encryption ) - formatters['id'] = _EncryptionInfoColumn + formatters['id'] = _EncryptionInfoColumn # type: ignore return ( column_headers, diff --git a/openstackclient/volume/v3/volume_type.py b/openstackclient/volume/v3/volume_type.py index fbce2f2c9..196b356c0 100644 --- a/openstackclient/volume/v3/volume_type.py +++ b/openstackclient/volume/v3/volume_type.py @@ -549,7 +549,7 @@ def take_action(self, parsed_args): _EncryptionInfoColumn = functools.partial( EncryptionInfoColumn, encryption_data=encryption ) - formatters['id'] = _EncryptionInfoColumn + formatters['id'] = _EncryptionInfoColumn # type: ignore return ( column_headers, diff --git a/pyproject.toml b/pyproject.toml index 37fb8d0c2..4d72625b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -744,15 +744,7 @@ follow_imports = "normal" incremental = true check_untyped_defs = true warn_unused_ignores = true -# keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' -exclude = ''' -(?x)( - doc - | examples - | hacking - | releasenotes - ) -''' +exclude = '(?x)(doc | examples | hacking | releasenotes)' [[tool.mypy.overrides]] module = ["openstackclient.tests.unit.*"] diff --git a/tox.ini b/tox.ini index 63692fe4a..6ce9e96c6 100644 --- a/tox.ini +++ b/tox.ini @@ -20,20 +20,22 @@ commands = [testenv:pep8] description = Run style checks. -skip_install = true deps = pre-commit + {[testenv:mypy]deps} commands = pre-commit run --all-files --show-diff-on-failure + {[testenv:mypy]commands} -[testenv:bandit] +[testenv:mypy] description = - Run bandit security checks. -skip_install = true + Run type checks. deps = - pre-commit + {[testenv]deps} + mypy + types-requests commands = - pre-commit run --all-files --show-diff-on-failure bandit + mypy --cache-dir="{envdir}/mypy_cache" {posargs:openstackclient} [testenv:unit-tips] commands = From 55e862b09eb2400bce56ff076d02202e0f9317f6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Feb 2026 15:12:38 +0000 Subject: [PATCH 13/31] trivial: Enable flake8-logging-format (G) rules Change-Id: Iabc94a0fd40903dc2a81bf62aea8460f20a7e0e4 Signed-off-by: Stephen Finucane --- examples/common.py | 2 +- openstackclient/common/quota.py | 15 ++++++++++++--- openstackclient/compute/v2/server.py | 2 +- openstackclient/image/v2/image.py | 2 +- .../unit/volume/v2/test_consistency_group.py | 10 ++++++++-- .../tests/unit/volume/v2/test_volume.py | 3 ++- .../tests/unit/volume/v3/test_volume.py | 3 ++- openstackclient/volume/v2/consistency_group.py | 16 ++++++++-------- .../volume/v2/consistency_group_snapshot.py | 4 ++-- openstackclient/volume/v2/qos_specs.py | 4 ++-- openstackclient/volume/v2/volume.py | 6 +++--- openstackclient/volume/v2/volume_backup.py | 4 ++-- openstackclient/volume/v2/volume_snapshot.py | 4 ++-- .../volume/v2/volume_transfer_request.py | 4 ++-- openstackclient/volume/v2/volume_type.py | 8 ++++---- openstackclient/volume/v3/volume.py | 6 +++--- openstackclient/volume/v3/volume_backup.py | 4 ++-- openstackclient/volume/v3/volume_snapshot.py | 4 ++-- .../volume/v3/volume_transfer_request.py | 4 ++-- openstackclient/volume/v3/volume_type.py | 8 ++++---- pyproject.toml | 2 +- 21 files changed, 66 insertions(+), 49 deletions(-) diff --git a/examples/common.py b/examples/common.py index 650139ec2..8213f4b53 100755 --- a/examples/common.py +++ b/examples/common.py @@ -264,7 +264,7 @@ def main(opts, run): if dump_stack_trace: _logger.error(traceback.format_exc(e)) else: - _logger.error('Exception raised: ' + str(e)) + _logger.error('Exception raised: %s', e) return 1 diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 6d0025a75..0e7edf48f 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -272,7 +272,10 @@ def _list_quota_compute(self, parsed_args, project_ids): sdk_exceptions.NotFoundException, ) as exc: # Project not found, move on to next one - LOG.warning(f"Project {project_id} not found: {exc}") + LOG.warning( + 'Project %(project_id)s not found: %(exc)s', + {'project_id': project_id, 'exc': exc}, + ) continue project_result = _xform_get_quota( @@ -334,7 +337,10 @@ def _list_quota_volume(self, parsed_args, project_ids): sdk_exceptions.NotFoundException, ) as exc: # Project not found, move on to next one - LOG.warning(f"Project {project_id} not found: {exc}") + LOG.warning( + 'Project %(project_id)s not found: %(exc)s', + {'project_id': project_id, 'exc': exc}, + ) continue project_result = _xform_get_quota( @@ -389,7 +395,10 @@ def _list_quota_network(self, parsed_args, project_ids): sdk_exceptions.ForbiddenException, ) as exc: # Project not found, move on to next one - LOG.warning(f"Project {project_id} not found: {exc}") + LOG.warning( + 'Project %(project_id)s not found: %(exc)s', + {'project_id': project_id, 'exc': exc}, + ) continue project_result = _xform_get_quota( diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index b4488c3f1..a52657c96 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -5018,7 +5018,7 @@ def take_action(self, parsed_args): ) cmd = ' '.join(['ssh', ip_address] + args) - LOG.debug(f"ssh command: {cmd}") + LOG.debug('ssh command: %s', cmd) # we intentionally pass through user-provided arguments and run this in # the user's shell os.system(cmd) # noqa: S605 diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index e8d040e14..a7db86c11 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -622,7 +622,7 @@ def _take_action_volume(self, parsed_args): ) # TODO(stephenfin): These should be an error in a future # version - LOG.warning(msg % opt_name) + LOG.warning(msg, opt_name) source_volume = volume_client.find_volume( parsed_args.volume, ignore_missing=False diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index dc62e5e42..68021c889 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -129,7 +129,10 @@ def test_add_multiple_volumes_to_consistency_group_with_exception( utils, 'find_resource', side_effect=find_mock_result ) as find_mock: result = self.cmd.take_action(parsed_args) - mock_error.assert_called_with("1 of 2 volumes failed to add.") + mock_error.assert_called_with( + '%(result)s of %(total)s volumes failed to add.', + {'result': 1, 'total': 2}, + ) self.assertIsNone(result) find_mock.assert_any_call( self.consistencygroups_mock, self._consistency_group.id @@ -602,7 +605,10 @@ def test_remove_multiple_volumes_from_consistency_group_with_exception( utils, 'find_resource', side_effect=find_mock_result ) as find_mock: result = self.cmd.take_action(parsed_args) - mock_error.assert_called_with("1 of 2 volumes failed to remove.") + mock_error.assert_called_with( + '%(result)s of %(total)s volumes failed to remove.', + {'result': 1, 'total': 2}, + ) self.assertIsNone(result) find_mock.assert_any_call( self.consistencygroups_mock, self._consistency_group.id diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index b68020fa9..65d83ab82 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -1618,7 +1618,8 @@ def test_volume_set_with_only_retype_policy(self, mock_warning): result = self.cmd.take_action(parsed_args) self.volumes_mock.retype.assert_not_called() mock_warning.assert_called_with( - "'--retype-policy' option will not work without '--type' option" + "'%s' option will not work without '--type' option", + '--retype-policy', ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 33dcfe5a4..2cae958a5 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -1998,7 +1998,8 @@ def test_volume_set_with_only_retype_policy(self, mock_warning): result = self.cmd.take_action(parsed_args) self.volumes_mock.retype.assert_not_called() mock_warning.assert_called_with( - "'--retype-policy' option will not work without '--type' option" + "'%s' option will not work without '--type' option", + '--retype-policy', ) self.assertIsNone(result) diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 4910bb129..31959c70b 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -38,8 +38,8 @@ def _find_volumes(parsed_args_volumes, volume_client): except Exception as e: result += 1 LOG.error( - _("Failed to find volume with name or ID '%(volume)s':%(e)s") - % {'volume': volume, 'e': e} + _("Failed to find volume with name or ID '%(volume)s':%(e)s"), + {'volume': volume, 'e': e}, ) return result, uuid @@ -73,8 +73,8 @@ def take_action(self, parsed_args): if result > 0: total = len(parsed_args.volumes) LOG.error( - _("%(result)s of %(total)s volumes failed to add.") - % {'result': result, 'total': total} + _("%(result)s of %(total)s volumes failed to add."), + {'result': result, 'total': total}, ) if add_uuid: @@ -226,8 +226,8 @@ def take_action(self, parsed_args): _( "Failed to delete consistency group with " "name or ID '%(consistency_group)s':%(e)s" - ) - % {'consistency_group': i, 'e': e} + ), + {'consistency_group': i, 'e': e}, ) if result > 0: @@ -317,8 +317,8 @@ def take_action(self, parsed_args): if result > 0: total = len(parsed_args.volumes) LOG.error( - _("%(result)s of %(total)s volumes failed to remove.") - % {'result': result, 'total': total} + _("%(result)s of %(total)s volumes failed to remove."), + {'result': result, 'total': total}, ) if remove_uuid: diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py index 23c3f1034..4adf4a25c 100644 --- a/openstackclient/volume/v2/consistency_group_snapshot.py +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -101,8 +101,8 @@ def take_action(self, parsed_args): _( "Failed to delete consistency group snapshot " "with name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': snapshot, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index 39aa99eb4..6dcc23f62 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -146,8 +146,8 @@ def take_action(self, parsed_args): _( "Failed to delete QoS specification with " "name or ID '%(qos)s': %(e)s" - ) - % {'qos': i, 'e': e} + ), + {'qos': i, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 61cce04f7..30f418d84 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -910,12 +910,12 @@ def take_action(self, parsed_args): elif policy: # If the "--migration-policy" is specified without "--type" LOG.warning( - _("'%s' option will not work without '--type' option") - % ( + _("'%s' option will not work without '--type' option"), + ( '--migration-policy' if parsed_args.migration_policy else '--retype-policy' - ) + ), ) kwargs = {} diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index 7dbe92c96..7f3665276 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -175,8 +175,8 @@ def take_action(self, parsed_args): _( "Failed to delete backup with " "name or ID '%(backup)s': %(e)s" - ) - % {'backup': backup, 'e': e} + ), + {'backup': backup, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index 3b1dbbabf..9533a1a69 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -228,8 +228,8 @@ def take_action(self, parsed_args): _( "Failed to delete snapshot with " "name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': snapshot, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index dcdc52762..2f1bee859 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -128,8 +128,8 @@ def take_action(self, parsed_args): _( "Failed to delete volume transfer request " "with name or ID '%(transfer)s': %(e)s" - ) - % {'transfer': t, 'e': e} + ), + {'transfer': t, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 27c7a35aa..9c804615a 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -277,7 +277,7 @@ def take_action(self, parsed_args): msg = _( "Failed to add project %(project)s access to type: %(e)s" ) - LOG.error(msg % {'project': parsed_args.project, 'e': e}) + LOG.error(msg, {'project': parsed_args.project, 'e': e}) properties = {} if parsed_args.properties: @@ -358,8 +358,8 @@ def take_action(self, parsed_args): _( "Failed to delete volume type with " "name or ID '%(volume_type)s': %(e)s" - ) - % {'volume_type': volume_type, 'e': e} + ), + {'volume_type': volume_type, 'e': e}, ) if result > 0: @@ -763,7 +763,7 @@ def take_action(self, parsed_args): 'Failed to get access project list for volume type ' '%(type)s: %(e)s' ) - LOG.error(msg % {'type': volume_type.id, 'e': e}) + LOG.error(msg, {'type': volume_type.id, 'e': e}) volume_type._info.update({'access_project_ids': access_project_ids}) if parsed_args.encryption_type: # show encryption type information for this volume type diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 50ea77fb5..8d47f6516 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -1071,12 +1071,12 @@ def take_action(self, parsed_args): elif policy: # If the "--migration-policy" is specified without "--type" LOG.warning( - _("'%s' option will not work without '--type' option") - % ( + _("'%s' option will not work without '--type' option"), + ( '--migration-policy' if parsed_args.migration_policy else '--retype-policy' - ) + ), ) kwargs = {} diff --git a/openstackclient/volume/v3/volume_backup.py b/openstackclient/volume/v3/volume_backup.py index df9a17eb0..2ab24f3e1 100644 --- a/openstackclient/volume/v3/volume_backup.py +++ b/openstackclient/volume/v3/volume_backup.py @@ -218,8 +218,8 @@ def take_action(self, parsed_args): _( "Failed to delete backup with " "name or ID '%(backup)s': %(e)s" - ) - % {'backup': backup, 'e': e} + ), + {'backup': backup, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v3/volume_snapshot.py b/openstackclient/volume/v3/volume_snapshot.py index f89174c3d..6eddb5a4d 100644 --- a/openstackclient/volume/v3/volume_snapshot.py +++ b/openstackclient/volume/v3/volume_snapshot.py @@ -246,8 +246,8 @@ def take_action(self, parsed_args): _( "Failed to delete snapshot with " "name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': snapshot, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v3/volume_transfer_request.py b/openstackclient/volume/v3/volume_transfer_request.py index afd462603..42fd37c1b 100644 --- a/openstackclient/volume/v3/volume_transfer_request.py +++ b/openstackclient/volume/v3/volume_transfer_request.py @@ -163,8 +163,8 @@ def take_action(self, parsed_args): _( "Failed to delete volume transfer request " "with name or ID '%(transfer)s': %(e)s" - ) - % {'transfer': t, 'e': e} + ), + {'transfer': t, 'e': e}, ) if result > 0: diff --git a/openstackclient/volume/v3/volume_type.py b/openstackclient/volume/v3/volume_type.py index 196b356c0..ba692525b 100644 --- a/openstackclient/volume/v3/volume_type.py +++ b/openstackclient/volume/v3/volume_type.py @@ -277,7 +277,7 @@ def take_action(self, parsed_args): msg = _( "Failed to add project %(project)s access to type: %(e)s" ) - LOG.error(msg % {'project': parsed_args.project, 'e': e}) + LOG.error(msg, {'project': parsed_args.project, 'e': e}) properties = {} if parsed_args.properties: @@ -358,8 +358,8 @@ def take_action(self, parsed_args): _( "Failed to delete volume type with " "name or ID '%(volume_type)s': %(e)s" - ) - % {'volume_type': volume_type, 'e': e} + ), + {'volume_type': volume_type, 'e': e}, ) if result > 0: @@ -846,7 +846,7 @@ def take_action(self, parsed_args): 'Failed to get access project list for volume type ' '%(type)s: %(e)s' ) - LOG.error(msg % {'type': volume_type.id, 'e': e}) + LOG.error(msg, {'type': volume_type.id, 'e': e}) volume_type._info.update({'access_project_ids': access_project_ids}) if parsed_args.encryption_type: # show encryption type information for this volume type diff --git a/pyproject.toml b/pyproject.toml index 4d72625b0..b23877ed5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -758,7 +758,7 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E5", "E7", "E9", "F", "S", "UP"] +select = ["E4", "E5", "E7", "E9", "F", "G", "S", "UP"] [tool.ruff.lint.per-file-ignores] "openstackclient/tests/*" = ["E501", "S"] From 0d0e58c5bf6ebac317214fb64a3c965cbd7f90a7 Mon Sep 17 00:00:00 2001 From: 0weng Date: Fri, 13 Feb 2026 10:39:59 -0800 Subject: [PATCH 14/31] Add release notes for federation protocol commands Release notes to go with change I4f1303b7b6c7ea2e67e5770745db060f08ba7761 that migrates the federation protocol commands to SDK. Change-Id: Ia2dbdefd505a44527c97c7549f33edcbdd7831c0 Signed-off-by: 0weng --- ...igrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml diff --git a/releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml b/releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml new file mode 100644 index 000000000..ea3808a7b --- /dev/null +++ b/releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``federation protocol`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``federation protocol`` commands is now case sensitive. From 87df8961738d78fb25639e9ff46087d485513c2e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Feb 2026 15:23:39 +0000 Subject: [PATCH 15/31] trivial: Enable ruff-specific (RUF) rules Change-Id: I60cc0c22c692414f758df431c049e51b1baecfc7 Signed-off-by: Stephen Finucane --- openstackclient/api/api.py | 2 +- openstackclient/common/progressbar.py | 2 +- openstackclient/common/quota.py | 6 ++--- openstackclient/compute/v2/server.py | 8 +++--- openstackclient/identity/common.py | 2 +- .../identity/v2_0/role_assignment.py | 2 +- .../identity/v3/identity_provider.py | 4 +-- openstackclient/identity/v3/role.py | 6 +++-- openstackclient/image/v1/image.py | 4 +-- openstackclient/network/utils.py | 16 +++++++----- .../network/v2/network_segment_range.py | 23 ++++++++++------ openstackclient/tests/functional/base.py | 2 +- .../functional/identity/v3/test_project.py | 2 +- .../tests/functional/network/v2/common.py | 2 +- .../functional/volume/v2/test_volume_type.py | 4 +-- .../functional/volume/v3/test_volume_type.py | 4 +-- .../tests/unit/common/test_quota.py | 6 ++--- .../tests/unit/compute/v2/test_flavor.py | 3 ++- .../tests/unit/compute/v2/test_server.py | 12 ++++----- .../tests/unit/identity/v3/test_group.py | 9 +++---- .../tests/unit/identity/v3/test_user.py | 2 +- .../tests/unit/image/v2/test_image.py | 22 ++++++++-------- .../unit/network/v2/test_address_group.py | 4 +-- .../network/v2/test_floating_ip_network.py | 3 ++- .../v2/test_floating_ip_port_forwarding.py | 26 +++++++++---------- .../unit/network/v2/test_network_agent.py | 4 +-- .../test_network_auto_allocated_topology.py | 6 ++--- .../unit/network/v2/test_network_flavor.py | 4 +-- .../unit/network/v2/test_network_qos_rule.py | 4 +-- .../unit/network/v2/test_network_rbac.py | 2 +- .../unit/network/v2/test_network_segment.py | 2 +- .../network/v2/test_network_segment_range.py | 17 +++++------- .../unit/network/v2/test_network_trunk.py | 2 +- .../tests/unit/network/v2/test_router.py | 13 ++++------ .../v2/test_security_group_rule_compute.py | 6 +++-- .../tests/unit/network/v2/test_subnet.py | 3 ++- .../tests/unit/network/v2/test_subnet_pool.py | 3 ++- .../unit/volume/v2/test_volume_backup.py | 6 +---- .../unit/volume/v2/test_volume_snapshot.py | 3 ++- .../tests/unit/volume/v2/test_volume_type.py | 6 ++--- .../unit/volume/v3/test_volume_backup.py | 6 +---- .../unit/volume/v3/test_volume_snapshot.py | 3 ++- .../tests/unit/volume/v3/test_volume_type.py | 6 ++--- openstackclient/volume/v2/volume.py | 2 +- openstackclient/volume/v3/volume.py | 2 +- pyproject.toml | 9 ++++++- 46 files changed, 145 insertions(+), 140 deletions(-) diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 5f78b0ee5..eae73eea6 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -254,7 +254,7 @@ def find_bulk(self, path, **kwargs): items = self.list(path) if isinstance(items, dict): # strip off the enclosing dict - key = list(items.keys())[0] + key = next(iter(items.keys())) items = items[key] ret = [] diff --git a/openstackclient/common/progressbar.py b/openstackclient/common/progressbar.py index 2852bb250..49eb6d6df 100644 --- a/openstackclient/common/progressbar.py +++ b/openstackclient/common/progressbar.py @@ -40,7 +40,7 @@ def _display_progress_bar(self, size_read): # Output something like this: [==========> ] 49% sys.stdout.write( '\r[{:<30}] {:.0%}'.format( - '=' * int(round(self._percent * 29)) + '>', self._percent + '=' * round(self._percent * 29) + '>', self._percent ) ) sys.stdout.flush() diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 0e7edf48f..c0e98a836 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -827,13 +827,11 @@ def _normalize_names(section: dict) -> None: _normalize_names(info["usage"]) # Remove the 'id' field since it's not very useful - if 'id' in info: - del info['id'] + info.pop('id', None) # Remove the sdk-derived fields for field in ('location', 'name', 'force'): - if field in info: - del info[field] + info.pop(field, None) if not parsed_args.usage: result = [{'resource': k, 'limit': v} for k, v in info.items()] diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a52657c96..67a6a22c5 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -940,9 +940,9 @@ def __call__(self, parser, namespace, values, option_string=None): } for kv_str in values.split(','): - k, sep, v = kv_str.partition('=') + k, _sep, v = kv_str.partition('=') - if k not in list(info) + ['tag'] or not v: + if k not in [*list(info), 'tag'] or not v: msg = _( "Invalid argument %s; argument must be of form " "'net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr," @@ -975,7 +975,7 @@ def __call__(self, parser, namespace, values, option_string=None): if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) - dev_name, sep, dev_map = values.partition('=') + 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 = _( @@ -5017,7 +5017,7 @@ def take_action(self, parsed_args): ip_address_family, ) - cmd = ' '.join(['ssh', ip_address] + args) + cmd = ' '.join(['ssh', ip_address, *args]) LOG.debug('ssh command: %s', cmd) # we intentionally pass through user-provided arguments and run this in # the user's shell diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index f33c37033..9de1c4cbb 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -180,7 +180,7 @@ def _get_token_resource(client, resource, parsed_name, parsed_domain=None): return parsed_name return obj['id'] if obj['name'] == parsed_name else parsed_name # diaper defense in case parsing the token fails - except Exception: # noqa + except Exception: return parsed_name diff --git a/openstackclient/identity/v2_0/role_assignment.py b/openstackclient/identity/v2_0/role_assignment.py index 0aa800ef8..eab7522a5 100644 --- a/openstackclient/identity/v2_0/role_assignment.py +++ b/openstackclient/identity/v2_0/role_assignment.py @@ -17,7 +17,7 @@ from osc_lib import utils from openstackclient import command -from openstackclient.i18n import _ # noqa +from openstackclient.i18n import _ class ListRoleAssignment(command.Lister): diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index f1af03f05..c085afea1 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -188,13 +188,13 @@ def get_parser(self, prog_name): parser.add_argument( '--id', metavar='', - help=_('The Identity Providers’ ID attribute'), + help=_('Filter identity providers by ID'), ) parser.add_argument( '--enabled', dest='enabled', action='store_true', - help=_('The Identity Providers that are enabled will be returned'), + help=_('List only enabled identity providers'), ) return parser diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 3c580d6f8..d8f66e1eb 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -416,8 +416,10 @@ def take_action(self, parsed_args): return ( ('ID', 'Name', 'Domain'), ( - utils.get_item_properties(s, ('id', 'name')) - + (domain.name,) + ( + *utils.get_item_properties(s, ('id', 'name')), + domain.name, + ) for s in data ), ) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 0ea7eca71..cf0f68919 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -299,7 +299,7 @@ def take_action(self, parsed_args): volume_client.volumes, parsed_args.volume, ) - response, body = volume_client.volumes.upload_to_image( + _response, body = volume_client.volumes.upload_to_image( source_volume.id, parsed_args.force, parsed_args.name, @@ -498,7 +498,7 @@ def take_action(self, parsed_args): if parsed_args.property: # NOTE(dtroyer): coerce to a list to subscript it in py3 - attr, value = list(parsed_args.property.items())[0] + attr, value = next(iter(parsed_args.property.items())) api_utils.simple_filter( images, attr=attr, diff --git a/openstackclient/network/utils.py b/openstackclient/network/utils.py index 30c0da064..43ffea679 100644 --- a/openstackclient/network/utils.py +++ b/openstackclient/network/utils.py @@ -78,7 +78,7 @@ def str2dict(strdict: str) -> dict[str, str]: else: kvlist[i - 1] = f"{kvlist[i - 1]};{kv}" for kv in kvlist: - key, sep, value = kv.partition(':') + key, value = kv.split(':', 1) result[key] = value return result @@ -173,11 +173,15 @@ def is_ipv6_protocol(protocol): # However, while the OSC CLI doesn't document the protocol, # the code must still handle it. In addition, handle both # protocol names and numbers. - if ( - protocol is not None - and protocol.startswith('ipv6-') - or protocol in ['icmpv6', '41', '43', '44', '58', '59', '60'] - ): + if (protocol is not None and protocol.startswith('ipv6-')) or protocol in [ + 'icmpv6', + '41', + '43', + '44', + '58', + '59', + '60', + ]: return True else: return False diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index c8b9a0e4c..f0172a685 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -406,14 +406,21 @@ def take_action(self, parsed_args): for s in data: props = utils.get_item_properties(s, columns) if ( - parsed_args.available - and _is_prop_empty(columns, props, 'available') - or parsed_args.unavailable - and not _is_prop_empty(columns, props, 'available') - or parsed_args.used - and _is_prop_empty(columns, props, 'used') - or parsed_args.unused - and not _is_prop_empty(columns, props, 'used') + ( + parsed_args.available + and _is_prop_empty(columns, props, 'available') + ) + or ( + parsed_args.unavailable + and not _is_prop_empty(columns, props, 'available') + ) + or ( + parsed_args.used and _is_prop_empty(columns, props, 'used') + ) + or ( + parsed_args.unused + and not _is_prop_empty(columns, props, 'used') + ) ): continue if parsed_args.long: diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 96c9accf6..ee3006261 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -92,7 +92,7 @@ def openstack( format_args.append('-f json') output = execute( - ' '.join(['openstack'] + auth_args + [cmd] + format_args), + ' '.join(['openstack', *auth_args, cmd, *format_args]), fail_ok=fail_ok, ) diff --git a/openstackclient/tests/functional/identity/v3/test_project.py b/openstackclient/tests/functional/identity/v3/test_project.py index 7a66c1851..1804fd495 100644 --- a/openstackclient/tests/functional/identity/v3/test_project.py +++ b/openstackclient/tests/functional/identity/v3/test_project.py @@ -105,6 +105,6 @@ def test_project_show_with_parents_children(self): f'{self.project_name}', parse_output=True, ) - for attr_name in self.PROJECT_FIELDS + ['parents', 'subtree']: + for attr_name in [*self.PROJECT_FIELDS, 'parents', 'subtree']: self.assertIn(attr_name, output) self.assertEqual(self.project_name, output.get('name')) diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py index 248758a84..245297bd9 100644 --- a/openstackclient/tests/functional/network/v2/common.py +++ b/openstackclient/tests/functional/network/v2/common.py @@ -84,7 +84,7 @@ def _list_tag_check(self, project_id, expected): parse_output=True, ) for name, tags in expected: - net = [n for n in cmd_output if n['Name'] == name][0] + net = next(n for n in cmd_output if n['Name'] == name) self.assertEqual(set(tags), set(net['Tags'])) def _create_resource_for_tag_test(self, name, args): diff --git a/openstackclient/tests/functional/volume/v2/test_volume_type.py b/openstackclient/tests/functional/volume/v2/test_volume_type.py index 80bf85a5b..c80e6c154 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_type.py @@ -173,9 +173,9 @@ def test_encryption_type(self): 'volume type list --encryption-type', parse_output=True, ) - encryption_output = [ + encryption_output = next( t['Encryption'] for t in cmd_output if t['Name'] == encryption_type - ][0] + ) expected = { 'provider': 'LuksEncryptor', 'cipher': 'aes-xts-plain64', diff --git a/openstackclient/tests/functional/volume/v3/test_volume_type.py b/openstackclient/tests/functional/volume/v3/test_volume_type.py index 421b3224f..cda56b004 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_type.py @@ -173,9 +173,9 @@ def test_encryption_type(self): 'volume type list --encryption-type', parse_output=True, ) - encryption_output = [ + encryption_output = next( t['Encryption'] for t in cmd_output if t['Name'] == encryption_type - ][0] + ) expected = { 'provider': 'LuksEncryptor', 'cipher': 'aes-xts-plain64', diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 7bfb2e7f2..833d71fe7 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -458,7 +458,7 @@ def test_quota_set(self): 'floating_ips': floating_ip_num, 'fixed_ips': fix_ip_num, 'injected_files': injected_file_num, - 'injected_file_content_bytes': injected_file_size_num, # noqa: E501 + 'injected_file_content_bytes': injected_file_size_num, 'injected_file_path_bytes': injected_path_size_num, 'key_pairs': key_pair_num, 'cores': core_num, @@ -729,7 +729,7 @@ def test_quota_set_with_class(self): kwargs_compute = { 'injected_files': injected_file_num, - 'injected_file_content_bytes': injected_file_size_num, # noqa: E501 + 'injected_file_content_bytes': injected_file_size_num, 'injected_file_path_bytes': injected_path_size_num, 'key_pairs': key_pair_num, 'cores': core_num, @@ -827,7 +827,7 @@ def test_quota_set_default(self): kwargs_compute = { 'injected_files': injected_file_num, - 'injected_file_content_bytes': injected_file_size_num, # noqa: E501 + 'injected_file_content_bytes': injected_file_size_num, 'injected_file_path_bytes': injected_path_size_num, 'key_pairs': key_pair_num, 'cores': core_num, diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 25bc8eaa7..6c9104bbb 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -494,7 +494,8 @@ def setUp(self): 'VCPUs', 'Is Public', ) - self.columns_long = self.columns + ( + self.columns_long = ( + *self.columns, 'Swap', 'RXTX Factor', 'Properties', diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 8da351676..b5f77aff7 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4865,7 +4865,7 @@ def test_server_list_column_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.compute_client.servers.assert_called_with(**self.kwargs) self.assertIn('Project ID', columns) @@ -5329,7 +5329,7 @@ def test_server_list_long_with_host_status_v216(self): ] # Add the expected host_status column and data. - columns_long = self.columns_long + ('Host Status',) + columns_long = (*self.columns_long, 'Host Status') self.data2 = tuple( ( s.id, @@ -5560,7 +5560,7 @@ def test_server_list_v269_with_partial_constructs(self): } fake_server = _server.Server(**server_dict) self.servers.append(fake_server) - columns, data = self.cmd.take_action(parsed_args) + _columns, data = self.cmd.take_action(parsed_args) # get the first three servers out since our interest is in the partial # server. next(data) @@ -5708,7 +5708,7 @@ def test_server_list_column_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.compute_client.servers.assert_called_with(**self.kwargs) self.assertIn('Project ID', columns) @@ -5860,7 +5860,7 @@ def test_server_list_column_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.compute_client.servers.assert_called_with(**self.kwargs) self.assertIn('Project ID', columns) @@ -8896,7 +8896,7 @@ def setUp(self): None, # OS-EXT-SRV-ATTR:user_data server.PowerStateColumn( self.server.power_state - ), # OS-EXT-STS:power_state # noqa: E501 + ), # OS-EXT-STS:power_state None, # OS-EXT-STS:task_state None, # OS-EXT-STS:vm_state None, # OS-SRV-USG:launched_at diff --git a/openstackclient/tests/unit/identity/v3/test_group.py b/openstackclient/tests/unit/identity/v3/test_group.py index 598402e07..b3ebfb75c 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -103,7 +103,7 @@ def test_group_add_user_with_error(self, mock_error): except exceptions.CommandError as e: msg = f"1 of 2 users not added to group {self._group.name}." self.assertEqual(msg, str(e)) - msg = f"{self.users[0].name} not added to group {self._group.name}: {str(sdk_exc.ResourceNotFound())}" + msg = f"{self.users[0].name} not added to group {self._group.name}: {sdk_exc.ResourceNotFound()!s}" mock_error.assert_called_once_with(msg) @@ -587,10 +587,7 @@ def test_group_list_long(self): self.identity_sdk_client.groups.assert_called_with() - long_columns = self.columns + ( - 'Domain ID', - 'Description', - ) + long_columns = (*self.columns, 'Domain ID', 'Description') datalist = ( ( self.group.id, @@ -687,7 +684,7 @@ def test_group_remove_user_with_error(self, mock_error): except exceptions.CommandError as e: msg = f"1 of 2 users not removed from group {self._group.id}." self.assertEqual(msg, str(e)) - msg = f"{self.users[0].id} not removed from group {self._group.id}: {str(sdk_exc.ResourceNotFound())}" + msg = f"{self.users[0].id} not removed from group {self._group.id}: {sdk_exc.ResourceNotFound()!s}" mock_error.assert_called_once_with(msg) diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 134ba5a0b..00b310e13 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -1773,7 +1773,7 @@ def setUp(self): # Get the command object to test self.cmd = user.ShowUser(self.app, None) - self.identity_client.auth.client.get_user_id.return_value = ( # noqa: E501 + self.identity_client.auth.client.get_user_id.return_value = ( self.user.id ) self.identity_client.tokens.get_token_data.return_value = { diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index c9a273ba9..98a18cf91 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -305,7 +305,7 @@ def test_image_create_import(self, raw_input): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.create_image.assert_called_with( name=self.new_image.name, @@ -336,7 +336,7 @@ def test_image_create_from_volume(self, mock_get_data_f): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.volume_sdk_client.upload_volume_to_image.assert_called_once_with( volume.id, @@ -397,7 +397,7 @@ def test_image_create_from_volume_v31(self, mock_get_data_f): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.volume_sdk_client.upload_volume_to_image.assert_called_once_with( volume.id, @@ -946,7 +946,7 @@ def test_image_list_marker_option(self, fr_mock): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with( marker=self._image.id, ) @@ -966,7 +966,7 @@ def test_image_list_name_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with( name='abc', # marker=self._image.id @@ -982,7 +982,7 @@ def test_image_list_status_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with(status='active') def test_image_list_hidden_option(self): @@ -994,7 +994,7 @@ def test_image_list_hidden_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with(is_hidden=True) def test_image_list_tag_option(self): @@ -1004,7 +1004,7 @@ def test_image_list_tag_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with(tag=['abc', 'cba']) @@ -1329,7 +1329,7 @@ def test_image_set_membership_accept_with_project_no_owner_change(self): self.image_client.update_image.assert_called() call_args = self.image_client.update_image.call_args if call_args: - args, kwargs = call_args + _args, kwargs = call_args self.assertNotIn('owner_id', kwargs) def test_image_set_membership_reject_with_project_no_owner_change(self): @@ -1366,7 +1366,7 @@ def test_image_set_membership_reject_with_project_no_owner_change(self): self.image_client.update_image.assert_called() call_args = self.image_client.update_image.call_args if call_args: - args, kwargs = call_args + _args, kwargs = call_args self.assertNotIn('owner_id', kwargs) def test_image_set_membership_pending_with_project_no_owner_change(self): @@ -1403,7 +1403,7 @@ def test_image_set_membership_pending_with_project_no_owner_change(self): self.image_client.update_image.assert_called() call_args = self.image_client.update_image.call_args if call_args: - args, kwargs = call_args + _args, kwargs = call_args self.assertNotIn('owner_id', kwargs) def test_image_set_options(self): diff --git a/openstackclient/tests/unit/network/v2/test_address_group.py b/openstackclient/tests/unit/network/v2/test_address_group.py index 48f706631..97218ab0a 100644 --- a/openstackclient/tests/unit/network/v2/test_address_group.py +++ b/openstackclient/tests/unit/network/v2/test_address_group.py @@ -519,7 +519,7 @@ def test_unset_one_address(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network_client.remove_addresses_from_address_group.assert_called_once_with( # noqa: E501 + self.network_client.remove_addresses_from_address_group.assert_called_once_with( self._address_group, ['10.0.0.2/32'] ) self.assertIsNone(result) @@ -539,7 +539,7 @@ def test_unset_multiple_addresses(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network_client.remove_addresses_from_address_group.assert_called_once_with( # noqa: E501 + self.network_client.remove_addresses_from_address_group.assert_called_once_with( self._address_group, ['10.0.0.2/32', '2001::/16'] ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py index ab0ec176a..685402a76 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_network.py @@ -425,7 +425,8 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): 'Floating Network', 'Project', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Router', 'Status', 'Description', diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py index 33b9011c6..354036d34 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py @@ -39,14 +39,14 @@ def setUp(self): class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): def setUp(self): super().setUp() - self.new_port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self.new_port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( attrs={ 'internal_port_id': self.port.id, 'floatingip_id': self.floating_ip.id, } ) - self.new_port_forwarding_with_ranges = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self.new_port_forwarding_with_ranges = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( use_range=True, attrs={ 'internal_port_id': self.port.id, @@ -144,15 +144,15 @@ def test_create_all_options_with_range(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( # noqa: E501 + self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( self.new_port_forwarding.floatingip_id, **{ - 'external_port_range': self.new_port_forwarding_with_ranges.external_port_range, # noqa: E501 - 'internal_ip_address': self.new_port_forwarding_with_ranges.internal_ip_address, # noqa: E501 - 'internal_port_range': self.new_port_forwarding_with_ranges.internal_port_range, # noqa: E501 - 'internal_port_id': self.new_port_forwarding_with_ranges.internal_port_id, # noqa: E501 + 'external_port_range': self.new_port_forwarding_with_ranges.external_port_range, + 'internal_ip_address': self.new_port_forwarding_with_ranges.internal_ip_address, + 'internal_port_range': self.new_port_forwarding_with_ranges.internal_port_range, + 'internal_port_id': self.new_port_forwarding_with_ranges.internal_port_id, 'protocol': self.new_port_forwarding_with_ranges.protocol, - 'description': self.new_port_forwarding_with_ranges.description, # noqa: E501 + 'description': self.new_port_forwarding_with_ranges.description, }, ) self.assertEqual(self.columns, columns) @@ -325,11 +325,11 @@ def test_create_all_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( # noqa: E501 + self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( self.new_port_forwarding.floatingip_id, **{ 'external_port': self.new_port_forwarding.external_port, - 'internal_ip_address': self.new_port_forwarding.internal_ip_address, # noqa: E501 + 'internal_ip_address': self.new_port_forwarding.internal_ip_address, 'internal_port': self.new_port_forwarding.internal_port, 'internal_port_id': self.new_port_forwarding.internal_port_id, 'protocol': self.new_port_forwarding.protocol, @@ -375,7 +375,7 @@ def test_port_forwarding_delete(self): result = self.cmd.take_action(parsed_args) - self.network_client.delete_floating_ip_port_forwarding.assert_called_once_with( # noqa: E501 + self.network_client.delete_floating_ip_port_forwarding.assert_called_once_with( self.floating_ip.id, self._port_forwarding[0].id, ignore_missing=False, @@ -553,7 +553,7 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding): # The Port Forwarding to set. def setUp(self): super().setUp() - self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( attrs={ 'floatingip_id': self.floating_ip.id, } @@ -675,7 +675,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): def setUp(self): super().setUp() - self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( attrs={ 'floatingip_id': self.floating_ip.id, } diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 48b394d7a..1198e1a6b 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -340,8 +340,8 @@ def test_network_agents_list_routers_with_long_option(self): ) # Add a column 'HA State' and corresponding data. - router_agent_columns = self.columns + ('HA State',) - router_agent_data = [d + ('',) for d in self.data] + router_agent_columns = (*self.columns, 'HA State') + router_agent_data = [(*d, '') for d in self.data] self.assertEqual(router_agent_columns, columns) self.assertEqual(len(router_agent_data), len(list(data))) diff --git a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py index d13bd8cf9..502b881da 100644 --- a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py +++ b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py @@ -167,7 +167,7 @@ def test_show_dry_run_no_project(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.validate_auto_allocated_topology.assert_called_with( None @@ -185,7 +185,7 @@ def test_show_dry_run_project_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.validate_auto_allocated_topology.assert_called_with( self.project.id @@ -206,7 +206,7 @@ def test_show_dry_run_project_domain_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.validate_auto_allocated_topology.assert_called_with( self.project.id diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor.py b/openstackclient/tests/unit/network/v2/test_network_flavor.py index 10038e393..4044312a1 100644 --- a/openstackclient/tests/unit/network/v2/test_network_flavor.py +++ b/openstackclient/tests/unit/network/v2/test_network_flavor.py @@ -71,7 +71,7 @@ def test_add_flavor_to_service_profile(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.network_client.associate_flavor_with_service_profile.assert_called_once_with( # noqa: E501 + self.network_client.associate_flavor_with_service_profile.assert_called_once_with( self.network_flavor, self.service_profile ) @@ -377,7 +377,7 @@ def test_remove_flavor_from_service_profile(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.network_client.disassociate_flavor_from_service_profile.assert_called_once_with( # noqa: E501 + self.network_client.disassociate_flavor_from_service_profile.assert_called_once_with( self.network_flavor, self.service_profile ) diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py index 430030402..3448589fa 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py @@ -226,7 +226,7 @@ def test_create_default_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.create_qos_minimum_packet_rate_rule.assert_called_once_with( # noqa: E501 + self.network_client.create_qos_minimum_packet_rate_rule.assert_called_once_with( self.qos_policy.id, **{ 'min_kpps': self.new_rule.min_kpps, @@ -613,7 +613,7 @@ def test_qos_policy_delete(self): self.network_client.find_qos_policy.assert_called_once_with( self.qos_policy.id, ignore_missing=False ) - self.network_client.delete_qos_minimum_packet_rate_rule.assert_called_once_with( # noqa: E501 + self.network_client.delete_qos_minimum_packet_rate_rule.assert_called_once_with( self.new_rule.id, self.qos_policy.id ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index d3a719245..656f2624e 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -224,7 +224,7 @@ def test_network_rbac_create_with_target_all_projects(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.create_rbac_policy.assert_called_with( **{ diff --git a/openstackclient/tests/unit/network/v2/test_network_segment.py b/openstackclient/tests/unit/network/v2/test_network_segment.py index ab71c3254..5bec65069 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment.py @@ -250,7 +250,7 @@ class TestListNetworkSegment(TestNetworkSegment): 'Network Type', 'Segment', ) - columns_long = columns + ('Physical Network',) + columns_long = (*columns, 'Physical Network') data = [] for _network_segment in _network_segments: diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index 9c9c900e3..db48fb248 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -333,7 +333,7 @@ def test_create_all_options(self): 'shared': self._network_segment_range.shared, 'project_id': mock.ANY, 'network_type': self._network_segment_range.network_type, - 'physical_network': self._network_segment_range.physical_network, # noqa: E501 + 'physical_network': self._network_segment_range.physical_network, 'minimum': self._network_segment_range.minimum, 'maximum': self._network_segment_range.maximum, 'name': self._network_segment_range.name, @@ -450,10 +450,7 @@ class TestListNetworkSegmentRange(TestNetworkSegmentRange): 'Minimum ID', 'Maximum ID', ) - columns_long = columns + ( - 'Used', - 'Available', - ) + columns_long = (*columns, 'Used', 'Available') data = [] for _network_segment_range in _network_segment_ranges: @@ -544,11 +541,11 @@ class TestSetNetworkSegmentRange(TestNetworkSegmentRange): # The network segment range updated. minimum_updated = _network_segment_range.minimum - 5 maximum_updated = _network_segment_range.maximum + 5 - available_updated = ( - list(range(minimum_updated, 104)) - + [105] - + list(range(107, maximum_updated + 1)) - ) + available_updated = [ + *list(range(minimum_updated, 104)), + 105, + *list(range(107, maximum_updated + 1)), + ] _network_segment_range_updated = ( network_fakes.create_one_network_segment_range( attrs={ diff --git a/openstackclient/tests/unit/network/v2/test_network_trunk.py b/openstackclient/tests/unit/network/v2/test_network_trunk.py index 1056c21c3..452870c88 100644 --- a/openstackclient/tests/unit/network/v2/test_network_trunk.py +++ b/openstackclient/tests/unit/network/v2/test_network_trunk.py @@ -468,7 +468,7 @@ class TestListNetworkTrunk(TestNetworkTrunk): ) columns = ('ID', 'Name', 'Parent Port', 'Description') - columns_long = columns + ('Status', 'State', 'Created At', 'Updated At') + columns_long = (*columns, 'Status', 'State', 'Created At', 'Updated At') data = [] for t in new_trunks: data.append((t['id'], t['name'], t['port_id'], t['description'])) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 6ebb7809e..56faa79a6 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -711,17 +711,14 @@ class TestListRouter(TestRouter): 'Distributed', 'HA', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Routes', 'External gateway info', 'Availability zones', 'Tags', ) - columns_long_no_az = columns + ( - 'Routes', - 'External gateway info', - 'Tags', - ) + columns_long_no_az = (*columns, 'Routes', 'External gateway info', 'Tags') data = [] for r in routers: @@ -824,7 +821,7 @@ def test_router_list_no_ha_no_distributed(self): with mock.patch.object( self.network_client, "routers", return_value=_routers ): - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.assertNotIn("is_distributed", columns) self.assertNotIn("is_ha", columns) @@ -1900,7 +1897,7 @@ def test_show_no_ha_no_distributed(self): with mock.patch.object( self.network_client, "find_router", return_value=_router ): - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.assertNotIn("is_distributed", columns) self.assertNotIn("is_ha", columns) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py index 9cab52e39..3c869aabf 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py @@ -411,7 +411,8 @@ class TestListSecurityGroupRuleCompute(compute_fakes.TestComputev2): 'Direction', 'Remote Security Group', ) - expected_columns_no_group = expected_columns_with_group + ( + expected_columns_no_group = ( + *expected_columns_with_group, 'Security Group', ) @@ -429,7 +430,8 @@ class TestListSecurityGroupRuleCompute(compute_fakes.TestComputev2): rule['port_range'], rule['remote_security_group'], ) - expected_rule_no_group = expected_rule_with_group + ( + expected_rule_no_group = ( + *expected_rule_with_group, _security_group_rule['parent_group_id'], ) expected_data_with_group.append(expected_rule_with_group) diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index e59168e51..e4b434215 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -798,7 +798,8 @@ class TestListSubnet(TestSubnet): 'Network', 'Subnet', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Project', 'DHCP', 'Name Servers', diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 013550ec1..eed947bfc 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -471,7 +471,8 @@ class TestListSubnetPool(TestSubnetPool): 'Name', 'Prefixes', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Default Prefix Length', 'Address Scope', 'Default Subnet Pool', diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index e7bbb6999..8d7984e80 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -220,11 +220,7 @@ class TestBackupList(volume_fakes.TestVolume): 'Incremental', 'Created At', ) - columns_long = columns + ( - 'Availability Zone', - 'Volume', - 'Container', - ) + columns_long = (*columns, 'Availability Zone', 'Volume', 'Container') def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py index 0df379bb2..c1d88c453 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py @@ -297,7 +297,8 @@ def setUp(self): self.project_mock.get.return_value = self.project self.columns = ("ID", "Name", "Description", "Status", "Size") - self.columns_long = self.columns + ( + self.columns_long = ( + *self.columns, "Created At", "Volume", "Properties", diff --git a/openstackclient/tests/unit/volume/v2/test_volume_type.py b/openstackclient/tests/unit/volume/v2/test_volume_type.py index 6f50ff2ef..b4df3e63c 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_type.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_type.py @@ -332,7 +332,7 @@ class TestTypeList(TestType): "Name", "Is Public", ] - columns_long = columns + ["Description"] + columns_long = [*columns, "Description"] data_with_default_type = [(volume_types[0].id, volume_types[0].name, True)] data = [] for t in volume_types: @@ -436,9 +436,7 @@ def test_type_list_with_encryption(self): 'key_size': None, 'control_location': 'front-end', } - encryption_columns = self.columns + [ - "Encryption", - ] + encryption_columns = [*self.columns, "Encryption"] encryption_data = [] encryption_data.append( ( diff --git a/openstackclient/tests/unit/volume/v3/test_volume_backup.py b/openstackclient/tests/unit/volume/v3/test_volume_backup.py index 86bde785f..480912796 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_backup.py @@ -319,11 +319,7 @@ class TestBackupList(volume_fakes.TestVolume): 'Incremental', 'Created At', ) - columns_long = columns + ( - 'Availability Zone', - 'Volume', - 'Container', - ) + columns_long = (*columns, 'Availability Zone', 'Volume', 'Container') def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py index 85613603d..59fc42baa 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py @@ -340,7 +340,8 @@ def setUp(self): self.project_mock.get.return_value = self.project self.columns = ("ID", "Name", "Description", "Status", "Size") - self.columns_long = self.columns + ( + self.columns_long = ( + *self.columns, "Created At", "Volume", "Properties", diff --git a/openstackclient/tests/unit/volume/v3/test_volume_type.py b/openstackclient/tests/unit/volume/v3/test_volume_type.py index 828f8b090..eedd07e5b 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_type.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_type.py @@ -331,7 +331,7 @@ class TestTypeList(TestType): "Name", "Is Public", ] - columns_long = columns + ["Description", "Properties"] + columns_long = [*columns, "Description", "Properties"] data_with_default_type = [(volume_types[0].id, volume_types[0].name, True)] data = [] for t in volume_types: @@ -509,9 +509,7 @@ def test_type_list_with_encryption(self): 'key_size': None, 'control_location': 'front-end', } - encryption_columns = self.columns + [ - "Encryption", - ] + encryption_columns = [*self.columns, "Encryption"] encryption_data = [] encryption_data.append( ( diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index ef4ff05ff..095a90223 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -555,7 +555,7 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute for s in compute_client.servers(): server_cache[s.id] = s - except sdk_exceptions.SDKException: # noqa: S110 + except sdk_exceptions.SDKException: # Just forget it if there's any trouble pass AttachmentsColumnWithCache = functools.partial( diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 428460006..1c5b03e46 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -706,7 +706,7 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute for s in compute_client.servers(): server_cache[s.id] = s - except sdk_exceptions.SDKException: # noqa: S110 + except sdk_exceptions.SDKException: # Just forget it if there's any trouble pass AttachmentsColumnWithCache = functools.partial( diff --git a/pyproject.toml b/pyproject.toml index b23877ed5..a938144be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -758,7 +758,14 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E5", "E7", "E9", "F", "G", "S", "UP"] +select = ["E4", "E5", "E7", "E9", "F", "G", "RUF", "S", "UP"] +ignore = [ + # the following are ignored because they don't provide enough value for the + # changes required + "RUF012", # Mutable default value for class attribute +] +# don't remove hacking (H) or openstackclient (O) checks +external = ["H", "O"] [tool.ruff.lint.per-file-ignores] "openstackclient/tests/*" = ["E501", "S"] From 2e45f8d6aa8e06fd9606d38ea68da24e6e3e19a1 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 3 Mar 2026 09:03:51 +0000 Subject: [PATCH 16/31] Update master for stable/2026.1 Add file to the reno documentation build to show release notes for stable/2026.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2026.1. Sem-Ver: feature Change-Id: I81e3760add22809c94018921f6d9bc47f1710330 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2026.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2026.1.rst diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 2b28cfb92..2b9b35f7b 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + 2026.1 2025.2 2025.1 2024.2 From aeb0c6828b764e47a90f9f719a33a61bcb438ee7 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 3 Mar 2026 09:38:25 +0000 Subject: [PATCH 17/31] typing: Add class variables to testcases testtools was recently bumped to a version that is typed, which means we now see type hints for that library. As a result, we now see issues with variables defined in tests via setUpClass. Note that the issue lies with mypy and not testtools, however: mypy doesn't adding class variables via assignment [1]. [1] https://github.com/python/mypy/issues/8723 Change-Id: I8b09846ff8fc6ee51ba03531f5b41039282ee1c0 Signed-off-by: Stephen Finucane --- .../tests/functional/common/test_extension.py | 4 ++ .../tests/functional/common/test_quota.py | 4 +- .../functional/compute/v2/test_flavor.py | 4 +- .../functional/compute/v2/test_server.py | 3 ++ .../tests/functional/identity/v2/common.py | 4 ++ .../tests/functional/identity/v3/common.py | 6 +++ .../tests/functional/image/base.py | 6 +++ .../tests/functional/network/v2/common.py | 5 ++- .../functional/network/v2/test_floating_ip.py | 6 +++ .../network/v2/test_ip_availability.py | 4 ++ .../network/v2/test_network_meter_rule.py | 5 ++- .../network/v2/test_network_segment.py | 5 +++ .../functional/network/v2/test_subnet.py | 22 ++++++----- .../tests/functional/object/v1/common.py | 4 ++ .../tests/functional/volume/v2/common.py | 4 ++ .../volume/v2/test_volume_snapshot.py | 38 +++++++++++++------ .../tests/functional/volume/v3/common.py | 4 ++ .../volume/v3/test_volume_snapshot.py | 18 +++++---- 18 files changed, 114 insertions(+), 32 deletions(-) diff --git a/openstackclient/tests/functional/common/test_extension.py b/openstackclient/tests/functional/common/test_extension.py index c65f52db5..1a8af9bc8 100644 --- a/openstackclient/tests/functional/common/test_extension.py +++ b/openstackclient/tests/functional/common/test_extension.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + from tempest.lib import exceptions as tempest_exc from openstackclient.tests.functional import base @@ -21,6 +23,8 @@ class ExtensionTests(base.TestCase): """Functional tests for extension""" + haz_network: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 373b178c1..a9bcaec74 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from tempest.lib.common.utils import data_utils @@ -25,7 +26,8 @@ class QuotaTests(base.TestCase): test runs as these may run in parallel and otherwise step on each other. """ - PROJECT_NAME: str + haz_network: ClassVar[bool] + PROJECT_NAME: ClassVar[str] @classmethod def setUpClass(cls): diff --git a/openstackclient/tests/functional/compute/v2/test_flavor.py b/openstackclient/tests/functional/compute/v2/test_flavor.py index 4a0ff4883..7f4cc43e7 100644 --- a/openstackclient/tests/functional/compute/v2/test_flavor.py +++ b/openstackclient/tests/functional/compute/v2/test_flavor.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional import base @@ -19,6 +20,7 @@ class FlavorTests(base.TestCase): """Functional tests for flavor.""" PROJECT_NAME = uuid.uuid4().hex + PROJECT_ID: ClassVar[str] @classmethod def setUpClass(cls): @@ -28,7 +30,7 @@ def setUpClass(cls): "project create --enable " + cls.PROJECT_NAME, parse_output=True, ) - cls.project_id = cmd_output["id"] + cls.PROJECT_ID = cmd_output["id"] @classmethod def tearDownClass(cls): diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 6afa2c7c0..c00d9dc55 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -13,6 +13,7 @@ import itertools import json import time +from typing import ClassVar import uuid from tempest.lib import exceptions @@ -25,6 +26,8 @@ class ServerTests(common.ComputeTestCase): """Functional tests for openstack server commands""" + haz_network: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/identity/v2/common.py b/openstackclient/tests/functional/identity/v2/common.py index dd2e27193..34962e6fe 100644 --- a/openstackclient/tests/functional/identity/v2/common.py +++ b/openstackclient/tests/functional/identity/v2/common.py @@ -11,6 +11,7 @@ # under the License. import os +from typing import ClassVar import unittest import fixtures @@ -57,6 +58,9 @@ class IdentityTests(base.TestCase): CATALOG_LIST_HEADERS = ['Name', 'Type', 'Endpoints'] ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type'] + project_name: ClassVar[str] + project_description: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 9f21374ff..8089e1d04 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -11,6 +11,7 @@ # under the License. import os +from typing import ClassVar import fixtures from tempest.lib.common.utils import data_utils @@ -147,6 +148,11 @@ class IdentityTests(base.TestCase): 'Region ID', ] + domain_name: ClassVar[str] + domain_description: ClassVar[str] + project_name: ClassVar[str] + project_description: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/image/base.py b/openstackclient/tests/functional/image/base.py index d948f8155..e11093f38 100644 --- a/openstackclient/tests/functional/image/base.py +++ b/openstackclient/tests/functional/image/base.py @@ -10,12 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + from openstackclient.tests.functional import base class BaseImageTests(base.TestCase): """Functional tests for Image commands""" + # TODO(stephenfin): Nothing sets this to true any more. We should remove it + # along with any dependent tests. + haz_v1_api: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py index 245297bd9..00c0b57e3 100644 --- a/openstackclient/tests/functional/network/v2/common.py +++ b/openstackclient/tests/functional/network/v2/common.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional import base @@ -18,6 +19,8 @@ class NetworkTests(base.TestCase): """Functional tests for Network commands""" + haz_network: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() @@ -33,7 +36,7 @@ def setUp(self): class NetworkTagTests(NetworkTests): """Functional tests with tag operation""" - base_command: str + base_command: ClassVar[str] def test_tag_operation(self): # Get project IDs diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index a1b11a44a..e34fe552c 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -11,6 +11,7 @@ # under the License. import random +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -19,6 +20,11 @@ class FloatingIpTests(common.NetworkTests): """Functional tests for floating ip""" + EXTERNAL_NETWORK_NAME: ClassVar[str] + PRIVATE_NETWORK_NAME: ClassVar[str] + external_network_id: ClassVar[str] + private_network_id: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/network/v2/test_ip_availability.py b/openstackclient/tests/functional/network/v2/test_ip_availability.py index 1cdbd487a..be1df9108 100644 --- a/openstackclient/tests/functional/network/v2/test_ip_availability.py +++ b/openstackclient/tests/functional/network/v2/test_ip_availability.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -18,6 +19,9 @@ class IPAvailabilityTests(common.NetworkTests): """Functional tests for IP availability""" + NAME: ClassVar[str] + NETWORK_NAME: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/network/v2/test_network_meter_rule.py b/openstackclient/tests/functional/network/v2/test_network_meter_rule.py index c80643e31..268794298 100644 --- a/openstackclient/tests/functional/network/v2/test_network_meter_rule.py +++ b/openstackclient/tests/functional/network/v2/test_network_meter_rule.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import unittest import uuid @@ -22,8 +23,8 @@ class TestMeterRule(common.NetworkTests): """Functional tests for meter rule""" - METER_ID: str - METER_RULE_ID: str + METER_ID: ClassVar[str] + METER_NAME: ClassVar[str] @classmethod def setUpClass(cls): diff --git a/openstackclient/tests/functional/network/v2/test_network_segment.py b/openstackclient/tests/functional/network/v2/test_network_segment.py index 03f5daf74..df26bcc8d 100644 --- a/openstackclient/tests/functional/network/v2/test_network_segment.py +++ b/openstackclient/tests/functional/network/v2/test_network_segment.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -18,6 +19,10 @@ class NetworkSegmentTests(common.NetworkTests): """Functional tests for network segment""" + NETWORK_NAME: ClassVar[str] + NETWORK_ID: ClassVar[str] + PHYSICAL_NETWORK_NAME: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/network/v2/test_subnet.py b/openstackclient/tests/functional/network/v2/test_subnet.py index 2ec987e9b..6af7c9454 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet.py +++ b/openstackclient/tests/functional/network/v2/test_subnet.py @@ -11,6 +11,7 @@ # under the License. import random +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -21,19 +22,22 @@ class SubnetTests(common.NetworkTagTests): base_command = 'subnet' + NETWORK_NAME: ClassVar[str] + NETWORK_ID: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() - if cls.haz_network: - cls.NETWORK_NAME = uuid.uuid4().hex - # Create a network for the all subnet tests - cmd_output = cls.openstack( - 'network create ' + cls.NETWORK_NAME, - parse_output=True, - ) - # Get network_id for assertEqual - cls.NETWORK_ID = cmd_output["id"] + cls.NETWORK_NAME = uuid.uuid4().hex + + # Create a network for the all subnet tests + cmd_output = cls.openstack( + 'network create ' + cls.NETWORK_NAME, + parse_output=True, + ) + # Get network_id for assertEqual + cls.NETWORK_ID = cmd_output["id"] @classmethod def tearDownClass(cls): diff --git a/openstackclient/tests/functional/object/v1/common.py b/openstackclient/tests/functional/object/v1/common.py index 036731da5..f3cc9aea9 100644 --- a/openstackclient/tests/functional/object/v1/common.py +++ b/openstackclient/tests/functional/object/v1/common.py @@ -10,12 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + from openstackclient.tests.functional import base class ObjectStoreTests(base.TestCase): """Functional tests for Object Store commands""" + haz_object_store: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/volume/v2/common.py b/openstackclient/tests/functional/volume/v2/common.py index f15d4d961..ed55030d2 100644 --- a/openstackclient/tests/functional/volume/v2/common.py +++ b/openstackclient/tests/functional/volume/v2/common.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + import fixtures from openstackclient.tests.functional.volume import base @@ -18,6 +20,8 @@ class BaseVolumeTests(base.BaseVolumeTests): """Base class for Volume functional tests.""" + haz_volume_v2: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py index e5daded1b..a5302033b 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.volume.v2 import common @@ -18,24 +19,27 @@ class VolumeSnapshotTests(common.BaseVolumeTests): """Functional tests for volume snapshot.""" - VOLLY = uuid.uuid4().hex + VOLUME_NAME = uuid.uuid4().hex + VOLUME_ID: ClassVar[str] @classmethod def setUpClass(cls): super().setUpClass() # create a volume for all tests to create snapshot cmd_output = cls.openstack( - 'volume create ' + '--size 1 ' + cls.VOLLY, + 'volume create ' + '--size 1 ' + cls.VOLUME_NAME, parse_output=True, ) - cls.wait_for_status('volume', cls.VOLLY, 'available') + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') cls.VOLUME_ID = cmd_output['id'] @classmethod def tearDownClass(cls): try: - cls.wait_for_status('volume', cls.VOLLY, 'available') - raw_output = cls.openstack('volume delete --force ' + cls.VOLLY) + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') + raw_output = cls.openstack( + 'volume delete --force ' + cls.VOLUME_NAME + ) cls.assertOutput('', raw_output) finally: super().tearDownClass() @@ -44,7 +48,10 @@ def test_volume_snapshot_delete(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name1 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name1 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.assertEqual( @@ -54,7 +61,10 @@ def test_volume_snapshot_delete(self): name2 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name2 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name2 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.assertEqual( @@ -76,7 +86,10 @@ def test_volume_snapshot_list(self): """Test create, list filter""" name1 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name1 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name1 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.addCleanup(self.wait_for_delete, 'volume snapshot', name1) @@ -97,7 +110,10 @@ def test_volume_snapshot_list(self): name2 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name2 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name2 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.addCleanup(self.wait_for_delete, 'volume snapshot', name2) @@ -146,7 +162,7 @@ def test_volume_snapshot_list(self): # Test list --volume cmd_output = self.openstack( - 'volume snapshot list ' + '--volume ' + self.VOLLY, + 'volume snapshot list ' + '--volume ' + self.VOLUME_NAME, parse_output=True, ) names = [x["Name"] for x in cmd_output] @@ -169,7 +185,7 @@ def test_volume_snapshot_set(self): cmd_output = self.openstack( 'volume snapshot create ' + '--volume ' - + self.VOLLY + + self.VOLUME_NAME + ' --description aaaa ' + '--property Alpha=a ' + name, diff --git a/openstackclient/tests/functional/volume/v3/common.py b/openstackclient/tests/functional/volume/v3/common.py index cbab39275..038991313 100644 --- a/openstackclient/tests/functional/volume/v3/common.py +++ b/openstackclient/tests/functional/volume/v3/common.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + import fixtures from openstackclient.tests.functional.volume import base @@ -18,6 +20,8 @@ class BaseVolumeTests(base.BaseVolumeTests): """Base class for Volume functional tests.""" + haz_volume_v3: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py index b84bb0368..6921c997d 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.volume.v3 import common @@ -18,24 +19,27 @@ class VolumeSnapshotTests(common.BaseVolumeTests): """Functional tests for volume snapshot.""" - VOLLY = uuid.uuid4().hex + VOLUME_NAME = uuid.uuid4().hex + VOLUME_ID: ClassVar[str] @classmethod def setUpClass(cls): super().setUpClass() # create a test volume used by all snapshot tests cmd_output = cls.openstack( - 'volume create ' + '--size 1 ' + cls.VOLLY, + 'volume create ' + '--size 1 ' + cls.VOLUME_NAME, parse_output=True, ) - cls.wait_for_status('volume', cls.VOLLY, 'available') + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') cls.VOLUME_ID = cmd_output['id'] @classmethod def tearDownClass(cls): try: - cls.wait_for_status('volume', cls.VOLLY, 'available') - raw_output = cls.openstack('volume delete --force ' + cls.VOLLY) + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') + raw_output = cls.openstack( + 'volume delete --force ' + cls.VOLUME_NAME + ) cls.assertOutput('', raw_output) finally: super().tearDownClass() @@ -47,7 +51,7 @@ def test_volume_snapshot(self): cmd_output = self.openstack( 'volume snapshot create ' + '--volume ' - + self.VOLLY + + self.VOLUME_NAME + ' --description aaaa ' + '--property Alpha=a ' + name, @@ -83,7 +87,7 @@ def test_volume_snapshot(self): # list volume snapshot --volume cmd_output = self.openstack( - 'volume snapshot list ' + '--volume ' + self.VOLLY, + 'volume snapshot list ' + '--volume ' + self.VOLUME_NAME, parse_output=True, ) names = [x["Name"] for x in cmd_output] From 2c01f526d135d160b26e9f764bba18ff9927b9d0 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 3 Mar 2026 10:18:12 +0000 Subject: [PATCH 18/31] tests: Trivial capitalization fixes Change-Id: Ie6d518b44bfc27ae957816ce00a59a1cdf7b247b Signed-off-by: Stephen Finucane --- .../tests/functional/identity/v2/common.py | 16 +++---- .../tests/functional/identity/v2/test_role.py | 8 ++-- .../tests/functional/identity/v3/common.py | 46 +++++++++---------- .../functional/identity/v3/test_group.py | 40 ++++++++-------- .../functional/identity/v3/test_project.py | 18 ++++---- .../tests/functional/identity/v3/test_role.py | 36 +++++++-------- .../identity/v3/test_role_assignment.py | 24 +++++----- .../tests/functional/identity/v3/test_user.py | 16 +++---- .../functional/network/v2/test_floating_ip.py | 12 ++--- 9 files changed, 108 insertions(+), 108 deletions(-) diff --git a/openstackclient/tests/functional/identity/v2/common.py b/openstackclient/tests/functional/identity/v2/common.py index 34962e6fe..c17c713eb 100644 --- a/openstackclient/tests/functional/identity/v2/common.py +++ b/openstackclient/tests/functional/identity/v2/common.py @@ -58,22 +58,22 @@ class IdentityTests(base.TestCase): CATALOG_LIST_HEADERS = ['Name', 'Type', 'Endpoints'] ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type'] - project_name: ClassVar[str] - project_description: ClassVar[str] + PROJECT_NAME: ClassVar[str] + PROJECT_DESCRIPTION: ClassVar[str] @classmethod def setUpClass(cls): super().setUpClass() # create dummy project - cls.project_name = data_utils.rand_name('TestProject') - cls.project_description = data_utils.rand_name('description') + cls.PROJECT_NAME = data_utils.rand_name('TestProject') + cls.PROJECT_DESCRIPTION = data_utils.rand_name('description') try: cls.openstack( '--os-identity-api-version 2 ' 'project create ' - f'--description {cls.project_description} ' + f'--description {cls.PROJECT_DESCRIPTION} ' '--enable ' - f'{cls.project_name}' + f'{cls.PROJECT_NAME}' ) except tempest_exceptions.CommandFailed: # Good chance this is due to Identity v2 admin not being enabled @@ -87,7 +87,7 @@ def tearDownClass(cls): try: cls.openstack( '--os-identity-api-version 2 ' - f'project delete {cls.project_name}' + f'project delete {cls.PROJECT_NAME}' ) finally: super().tearDownClass() @@ -129,7 +129,7 @@ def _create_dummy_user(self, add_clean_up=True): email = data_utils.rand_name() + '@example.com' raw_output = self.openstack( 'user create ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--password {password} ' f'--email {email} ' '--enable ' diff --git a/openstackclient/tests/functional/identity/v2/test_role.py b/openstackclient/tests/functional/identity/v2/test_role.py index ec6134012..58a04f5e2 100644 --- a/openstackclient/tests/functional/identity/v2/test_role.py +++ b/openstackclient/tests/functional/identity/v2/test_role.py @@ -39,14 +39,14 @@ def test_role_add(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}', ) @@ -58,13 +58,13 @@ def test_role_remove(self): username = self._create_dummy_user() add_raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) del_raw_output = self.openstack( 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 8089e1d04..53733b965 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -148,35 +148,35 @@ class IdentityTests(base.TestCase): 'Region ID', ] - domain_name: ClassVar[str] - domain_description: ClassVar[str] - project_name: ClassVar[str] - project_description: ClassVar[str] + DOMAIN_NAME: ClassVar[str] + DOMAIN_DESCRIPTION: ClassVar[str] + PROJECT_NAME: ClassVar[str] + PROJECT_DESCRIPTION: ClassVar[str] @classmethod def setUpClass(cls): super().setUpClass() # create dummy domain - cls.domain_name = data_utils.rand_name('TestDomain') - cls.domain_description = data_utils.rand_name('description') + cls.DOMAIN_NAME = data_utils.rand_name('TestDomain') + cls.DOMAIN_DESCRIPTION = data_utils.rand_name('description') cls.openstack( '--os-identity-api-version 3 ' 'domain create ' - f'--description {cls.domain_description} ' + f'--description {cls.DOMAIN_DESCRIPTION} ' '--enable ' - f'{cls.domain_name}' + f'{cls.DOMAIN_NAME}' ) # create dummy project - cls.project_name = data_utils.rand_name('TestProject') - cls.project_description = data_utils.rand_name('description') + cls.PROJECT_NAME = data_utils.rand_name('TestProject') + cls.PROJECT_DESCRIPTION = data_utils.rand_name('description') cls.openstack( '--os-identity-api-version 3 ' 'project create ' - f'--domain {cls.domain_name} ' - f'--description {cls.project_description} ' + f'--domain {cls.DOMAIN_NAME} ' + f'--description {cls.PROJECT_DESCRIPTION} ' '--enable ' - f'{cls.project_name}' + f'{cls.PROJECT_NAME}' ) @classmethod @@ -185,15 +185,15 @@ def tearDownClass(cls): # delete dummy project cls.openstack( '--os-identity-api-version 3 ' - f'project delete {cls.project_name}' + f'project delete {cls.PROJECT_NAME}' ) # disable and delete dummy domain cls.openstack( '--os-identity-api-version 3 ' - f'domain set --disable {cls.domain_name}' + f'domain set --disable {cls.DOMAIN_NAME}' ) cls.openstack( - f'--os-identity-api-version 3 domain delete {cls.domain_name}' + f'--os-identity-api-version 3 domain delete {cls.DOMAIN_NAME}' ) finally: super().tearDownClass() @@ -219,9 +219,9 @@ def _create_dummy_user(self, add_clean_up=True): description = data_utils.rand_name('description') raw_output = self.openstack( 'user create ' - f'--domain {self.domain_name} ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--password {password} ' f'--email {email} ' f'--description {description} ' @@ -268,14 +268,14 @@ def _create_dummy_group(self, add_clean_up=True): description = data_utils.rand_name('description') raw_output = self.openstack( 'group create ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--description {description} ' f'{group_name}' ) if add_clean_up: self.addCleanup( self.openstack, - f'group delete --domain {self.domain_name} {group_name}', + f'group delete --domain {self.DOMAIN_NAME} {group_name}', ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.GROUP_FIELDS) @@ -301,14 +301,14 @@ def _create_dummy_project(self, add_clean_up=True): project_description = data_utils.rand_name('description') self.openstack( 'project create ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--description {project_description} ' f'--enable {project_name}' ) if add_clean_up: self.addCleanup( self.openstack, - f'project delete --domain {self.domain_name} {project_name}', + f'project delete --domain {self.DOMAIN_NAME} {project_name}', ) return project_name diff --git a/openstackclient/tests/functional/identity/v3/test_group.py b/openstackclient/tests/functional/identity/v3/test_group.py index a2e41d813..0292d866b 100644 --- a/openstackclient/tests/functional/identity/v3/test_group.py +++ b/openstackclient/tests/functional/identity/v3/test_group.py @@ -28,7 +28,7 @@ def test_group_list(self): def test_group_list_with_domain(self): group_name = self._create_dummy_group() - raw_output = self.openstack(f'group list --domain {self.domain_name}') + raw_output = self.openstack(f'group list --domain {self.DOMAIN_NAME}') items = self.parse_listing(raw_output) self.assert_table_structure(items, common.BASIC_LIST_HEADERS) self.assertIn(group_name, raw_output) @@ -36,14 +36,14 @@ def test_group_list_with_domain(self): def test_group_delete(self): group_name = self._create_dummy_group(add_clean_up=False) raw_output = self.openstack( - f'group delete --domain {self.domain_name} {group_name}' + f'group delete --domain {self.DOMAIN_NAME} {group_name}' ) self.assertEqual(0, len(raw_output)) def test_group_show(self): group_name = self._create_dummy_group() raw_output = self.openstack( - f'group show --domain {self.domain_name} {group_name}' + f'group show --domain {self.DOMAIN_NAME} {group_name}' ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.GROUP_FIELDS) @@ -53,20 +53,20 @@ def test_group_set(self): new_group_name = data_utils.rand_name('NewTestGroup') raw_output = self.openstack( 'group set ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--name {new_group_name} ' f'{group_name}' ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'group show --domain {self.domain_name} {new_group_name}' + f'group show --domain {self.DOMAIN_NAME} {new_group_name}' ) group = self.parse_show_as_object(raw_output) self.assertEqual(new_group_name, group['name']) # reset group name to make sure it will be cleaned up raw_output = self.openstack( 'group set ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--name {group_name} ' f'{new_group_name}' ) @@ -77,15 +77,15 @@ def test_group_add_user(self): username = self._create_dummy_user() raw_output = self.openstack( 'group add user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.addCleanup( self.openstack, 'group remove user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}', ) self.assertOutput('', raw_output) @@ -95,22 +95,22 @@ def test_group_contains_user(self): username = self._create_dummy_user() raw_output = self.openstack( 'group add user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.addCleanup( self.openstack, 'group remove user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}', ) self.assertOutput('', raw_output) raw_output = self.openstack( 'group contains user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.assertEqual( @@ -123,14 +123,14 @@ def test_group_remove_user(self): username = self._create_dummy_user() add_raw_output = self.openstack( 'group add user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) remove_raw_output = self.openstack( 'group remove user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.assertOutput('', add_raw_output) diff --git a/openstackclient/tests/functional/identity/v3/test_project.py b/openstackclient/tests/functional/identity/v3/test_project.py index 1804fd495..8394377e4 100644 --- a/openstackclient/tests/functional/identity/v3/test_project.py +++ b/openstackclient/tests/functional/identity/v3/test_project.py @@ -21,7 +21,7 @@ def test_project_create(self): description = data_utils.rand_name('description') raw_output = self.openstack( 'project create ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--description {description} ' '--enable ' '--property k1=v1 ' @@ -30,7 +30,7 @@ def test_project_create(self): ) self.addCleanup( self.openstack, - f'project delete --domain {self.domain_name} {project_name}', + f'project delete --domain {self.DOMAIN_NAME} {project_name}', ) items = self.parse_show(raw_output) show_fields = list(self.PROJECT_FIELDS) @@ -43,7 +43,7 @@ def test_project_create(self): def test_project_delete(self): project_name = self._create_dummy_project(add_clean_up=False) raw_output = self.openstack( - f'project delete --domain {self.domain_name} {project_name}' + f'project delete --domain {self.DOMAIN_NAME} {project_name}' ) self.assertEqual(0, len(raw_output)) @@ -55,7 +55,7 @@ def test_project_list(self): def test_project_list_with_domain(self): project_name = self._create_dummy_project() raw_output = self.openstack( - f'project list --domain {self.domain_name}' + f'project list --domain {self.DOMAIN_NAME}' ) items = self.parse_listing(raw_output) self.assert_table_structure(items, common.BASIC_LIST_HEADERS) @@ -75,7 +75,7 @@ def test_project_set(self): self.assertEqual(0, len(raw_output)) # check project details raw_output = self.openstack( - f'project show --domain {self.domain_name} {new_project_name}' + f'project show --domain {self.DOMAIN_NAME} {new_project_name}' ) items = self.parse_show(raw_output) fields = list(self.PROJECT_FIELDS) @@ -92,7 +92,7 @@ def test_project_set(self): def test_project_show(self): raw_output = self.openstack( - f'project show --domain {self.domain_name} {self.project_name}' + f'project show --domain {self.DOMAIN_NAME} {self.PROJECT_NAME}' ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.PROJECT_FIELDS) @@ -101,10 +101,10 @@ def test_project_show_with_parents_children(self): output = self.openstack( 'project show ' '--parents --children ' - f'--domain {self.domain_name} ' - f'{self.project_name}', + f'--domain {self.DOMAIN_NAME} ' + f'{self.PROJECT_NAME}', parse_output=True, ) for attr_name in [*self.PROJECT_FIELDS, 'parents', 'subtree']: self.assertIn(attr_name, output) - self.assertEqual(self.project_name, output.get('name')) + self.assertEqual(self.PROJECT_NAME, output.get('name')) diff --git a/openstackclient/tests/functional/identity/v3/test_role.py b/openstackclient/tests/functional/identity/v3/test_role.py index 3237c0bfb..f4189aeef 100644 --- a/openstackclient/tests/functional/identity/v3/test_role.py +++ b/openstackclient/tests/functional/identity/v3/test_role.py @@ -76,19 +76,19 @@ def test_role_add(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}', ) self.assertEqual(0, len(raw_output)) @@ -98,20 +98,20 @@ def test_role_add_inherited(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' '--inherited ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' '--inherited ' f'{role_name}', ) @@ -122,18 +122,18 @@ def test_role_remove(self): username = self._create_dummy_user() add_raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}' ) remove_raw_output = self.openstack( 'role remove ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}' ) self.assertEqual(0, len(add_raw_output)) diff --git a/openstackclient/tests/functional/identity/v3/test_role_assignment.py b/openstackclient/tests/functional/identity/v3/test_role_assignment.py index 1255841af..373f7a8ef 100644 --- a/openstackclient/tests/functional/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/functional/identity/v3/test_role_assignment.py @@ -79,14 +79,14 @@ def test_role_assignment_list_group_domain(self): ) raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--group {group_name} --group-domain {domain_name_A} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--group {group_name} --group-domain {domain_name_A} ' f'{role_name}', ) @@ -108,20 +108,20 @@ def test_role_assignment_list_domain(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--user {username} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--user {username} ' f'{role_name}', ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'role assignment list --domain {self.domain_name} ' + f'role assignment list --domain {self.DOMAIN_NAME} ' ) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) @@ -141,14 +141,14 @@ def test_role_assignment_list_user_domain(self): ) raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} --user-domain {domain_name_A} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} --user-domain {domain_name_A} ' f'{role_name}', ) @@ -214,20 +214,20 @@ def test_role_assignment_list_project(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}', ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'role assignment list --project {self.project_name} ' + f'role assignment list --project {self.PROJECT_NAME} ' ) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) @@ -302,7 +302,7 @@ def test_role_assignment_list_inherited(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' '--inherited ' f'{role_name}' @@ -310,7 +310,7 @@ def test_role_assignment_list_inherited(self): self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' '--inherited ' f'{role_name}', diff --git a/openstackclient/tests/functional/identity/v3/test_user.py b/openstackclient/tests/functional/identity/v3/test_user.py index dd56293e6..a5e7f1adb 100644 --- a/openstackclient/tests/functional/identity/v3/test_user.py +++ b/openstackclient/tests/functional/identity/v3/test_user.py @@ -22,7 +22,7 @@ def test_user_create(self): def test_user_delete(self): username = self._create_dummy_user(add_clean_up=False) raw_output = self.openstack( - f'user delete --domain {self.domain_name} {username}' + f'user delete --domain {self.DOMAIN_NAME} {username}' ) self.assertEqual(0, len(raw_output)) @@ -34,7 +34,7 @@ def test_user_list(self): def test_user_set(self): username = self._create_dummy_user() raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) user = self.parse_show_as_object(raw_output) new_username = data_utils.rand_name('NewTestUser') @@ -46,7 +46,7 @@ def test_user_set(self): ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'user show --domain {self.domain_name} {new_username}' + f'user show --domain {self.DOMAIN_NAME} {new_username}' ) updated_user = self.parse_show_as_object(raw_output) self.assertEqual(user['id'], updated_user['id']) @@ -57,7 +57,7 @@ def test_user_set_default_project_id(self): project_name = self._create_dummy_project() # get original user details raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) user = self.parse_show_as_object(raw_output) # update user @@ -67,19 +67,19 @@ def test_user_set_default_project_id(self): '--project-domain {project_domain} ' '{id}'.format( project=project_name, - project_domain=self.domain_name, + project_domain=self.DOMAIN_NAME, id=user['id'], ) ) self.assertEqual(0, len(raw_output)) # get updated user details raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) updated_user = self.parse_show_as_object(raw_output) # get project details raw_output = self.openstack( - f'project show --domain {self.domain_name} {project_name}' + f'project show --domain {self.DOMAIN_NAME} {project_name}' ) project = self.parse_show_as_object(raw_output) # check updated user details @@ -89,7 +89,7 @@ def test_user_set_default_project_id(self): def test_user_show(self): username = self._create_dummy_user() raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.USER_FIELDS) diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index e34fe552c..c2ce00d48 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -21,9 +21,9 @@ class FloatingIpTests(common.NetworkTests): """Functional tests for floating ip""" EXTERNAL_NETWORK_NAME: ClassVar[str] + EXTERNAL_NETWORK_ID: ClassVar[str] PRIVATE_NETWORK_NAME: ClassVar[str] - external_network_id: ClassVar[str] - private_network_id: ClassVar[str] + PRIVATE_NETWORK_ID: ClassVar[str] @classmethod def setUpClass(cls): @@ -38,14 +38,14 @@ def setUpClass(cls): 'network create ' + '--external ' + cls.EXTERNAL_NETWORK_NAME, parse_output=True, ) - cls.external_network_id = json_output["id"] + cls.EXTERNAL_NETWORK_ID = json_output["id"] # Create a private network for the port json_output = cls.openstack( 'network create ' + cls.PRIVATE_NETWORK_NAME, parse_output=True, ) - cls.private_network_id = json_output["id"] + cls.PRIVATE_NETWORK_ID = json_output["id"] @classmethod def tearDownClass(cls): @@ -65,8 +65,8 @@ def setUp(self): super().setUp() # Verify setup - self.assertIsNotNone(self.external_network_id) - self.assertIsNotNone(self.private_network_id) + self.assertIsNotNone(self.EXTERNAL_NETWORK_ID) + self.assertIsNotNone(self.PRIVATE_NETWORK_ID) def _create_subnet(self, network_name, subnet_name): subnet_id = None From 18ba16b668b6f9a5168a20517c4463354646f876 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 3 Mar 2026 10:20:13 +0000 Subject: [PATCH 19/31] tests: Remove image v1 functional tests Nothing was setting haz_v1_api which means it was not possible for these to run. Even if it had been, getting a cloud with the v1 image API up and running is no easy feat nowadays. Change-Id: Ic2ba82aedae4dbcae6313150335ee1c7412ce7b3 Signed-off-by: Stephen Finucane --- .../tests/functional/image/base.py | 13 +-- .../tests/functional/image/v1/__init__.py | 0 .../tests/functional/image/v1/test_image.py | 97 ------------------- 3 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 openstackclient/tests/functional/image/v1/__init__.py delete mode 100644 openstackclient/tests/functional/image/v1/test_image.py diff --git a/openstackclient/tests/functional/image/base.py b/openstackclient/tests/functional/image/base.py index e11093f38..174c47c63 100644 --- a/openstackclient/tests/functional/image/base.py +++ b/openstackclient/tests/functional/image/base.py @@ -10,21 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from typing import ClassVar - from openstackclient.tests.functional import base class BaseImageTests(base.TestCase): """Functional tests for Image commands""" - # TODO(stephenfin): Nothing sets this to true any more. We should remove it - # along with any dependent tests. - haz_v1_api: ClassVar[bool] - - @classmethod - def setUpClass(cls): - super().setUpClass() - # TODO(dtroyer): maybe do image API discovery here to determine - # what is available, it isn't in the service catalog - cls.haz_v1_api = False + ... diff --git a/openstackclient/tests/functional/image/v1/__init__.py b/openstackclient/tests/functional/image/v1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openstackclient/tests/functional/image/v1/test_image.py b/openstackclient/tests/functional/image/v1/test_image.py deleted file mode 100644 index c4118babb..000000000 --- a/openstackclient/tests/functional/image/v1/test_image.py +++ /dev/null @@ -1,97 +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 uuid - -import fixtures - -from openstackclient.tests.functional.image import base - - -class ImageTests(base.BaseImageTests): - """Functional tests for Image commands""" - - def setUp(self): - super().setUp() - - if not self.haz_v1_api: - self.skipTest('No Image v1 API present') - - ver_fixture = fixtures.EnvironmentVariable('OS_IMAGE_API_VERSION', '1') - self.useFixture(ver_fixture) - - self.name = uuid.uuid4().hex - output = self.openstack( - 'image create ' + self.name, - parse_output=True, - ) - self.image_id = output["id"] - self.assertOutput(self.name, output['name']) - - def tearDown(self): - try: - self.openstack('image delete ' + self.image_id) - finally: - super().tearDown() - - def test_image_list(self): - output = self.openstack('image list') - self.assertIn(self.name, [img['Name'] for img in output]) - - def test_image_attributes(self): - """Test set, unset, show on attributes, tags and properties""" - - # Test explicit attributes - self.openstack( - 'image set ' - + '--min-disk 4 ' - + '--min-ram 5 ' - + '--disk-format qcow2 ' - + '--public ' - + self.name - ) - output = self.openstack( - 'image show ' + self.name, - parse_output=True, - ) - self.assertEqual( - 4, - output["min_disk"], - ) - self.assertEqual( - 5, - output["min_ram"], - ) - self.assertEqual( - 'qcow2', - output['disk_format'], - ) - self.assertTrue( - output["is_public"], - ) - - # Test properties - self.openstack( - 'image set ' - + '--property a=b ' - + '--property c=d ' - + '--public ' - + self.name - ) - output = self.openstack( - 'image show ' + self.name, - parse_output=True, - ) - self.assertEqual( - {'a': 'b', 'c': 'd'}, - output["properties"], - ) From a6b9c358915bb5783c0e0f9b3d87d59fda3bcb19 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Mar 2026 12:16:16 +0000 Subject: [PATCH 20/31] identity: Fix project list SDK returns generators, not lists, and we do not see errors until they are iterated. Force early iteration by wrapping then in a list to ensure we actually see the HTTP 403 errors we were expecting. Change-Id: I0ab72e587bf4e16ae877db7a81023a226124e4d5 Signed-off-by: Stephen Finucane --- openstackclient/identity/v3/project.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 99242391f..f3d7fea23 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -342,21 +342,21 @@ def take_action(self, parsed_args): user = self.app.client_manager.auth_ref.user_id if user: - data = identity_client.user_projects(user, **kwargs) + data = list(identity_client.user_projects(user, **kwargs)) else: try: - data = identity_client.projects(**kwargs) + data = list(identity_client.projects(**kwargs)) except sdk_exc.ForbiddenException: # NOTE(adriant): if no filters, assume a forbidden is non-admin # wanting their own project list. if not kwargs: user = self.app.client_manager.auth_ref.user_id - data = identity_client.user_projects(user) + data = list(identity_client.user_projects(user)) else: raise if parsed_args.sort: - data = utils.sort_items(data, parsed_args.sort) + data = list(utils.sort_items(data, parsed_args.sort)) return ( column_headers, From 78e0bf023b533698663c46be54d938233a368c2c Mon Sep 17 00:00:00 2001 From: hongp Date: Wed, 14 Jan 2026 13:47:46 +0900 Subject: [PATCH 21/31] Add --cluster option to volume migration This patch adds the '--cluster' optional argument to the 'volume migration' command. This allows users to migrate volumes to a destination cluster instead of a specific host, which is particularly useful in Active-Active configurations. The '--cluster' option requires Cinder API microversion 3.16 or higher. The '--host' and '--cluster' options are mutually exclusive; one of them must be provided for the migration to start. Change-Id: Ibd45ac35e39a2a9f88a39f8041c06c4469727098 Signed-off-by: hongp --- .../tests/unit/volume/v3/test_volume.py | 63 ++++++++++++++++++- openstackclient/volume/v3/volume.py | 24 ++++++- ...-to-volume-migration-9fe0cc84e9c80a4c.yaml | 7 +++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 33dcfe5a4..a004dc122 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -1702,9 +1702,10 @@ def test_volume_migrate(self): host="host@backend-name#pool", force_host_copy=False, lock_volume=False, + cluster=None, ) - def test_volume_migrate_with_option(self): + def test_volume_migrate_with_host(self): arglist = [ "--force-host-copy", "--lock-volume", @@ -1731,9 +1732,66 @@ def test_volume_migrate_with_option(self): host="host@backend-name#pool", force_host_copy=True, lock_volume=True, + cluster=None, ) - def test_volume_migrate_without_host(self): + def test_volume_migrate_with_cluster(self): + self.set_volume_api_version('3.16') + arglist = [ + "--cluster", + "cluster@backend-name#pool", + self.volume.id, + ] + verifylist = [ + ( + "cluster", + "cluster@backend-name#pool", + ), + ("volume", self.volume.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.volume_sdk_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_sdk_client.migrate_volume.assert_called_once_with( + self.volume.id, + host=None, + force_host_copy=False, + lock_volume=False, + cluster="cluster@backend-name#pool", + ) + + def test_volume_migrate_with_cluster_pre_v316(self): + self.set_volume_api_version('3.15') + arglist = [ + "--cluster", + "cluster@backend-name#pool", + self.volume.id, + ] + verifylist = [ + ( + "cluster", + "cluster@backend-name#pool", + ), + ("volume", self.volume.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.volume_sdk_client.migrate_volume.assert_not_called() + + def test_volume_migrate_without_host_and_cluster(self): arglist = [ self.volume.id, ] @@ -1750,7 +1808,6 @@ def test_volume_migrate_without_host(self): arglist, verifylist, ) - self.volume_sdk_client.find_volume.assert_not_called() self.volume_sdk_client.migrate_volume.assert_not_called() diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 50ea77fb5..d1c1e43b7 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -736,14 +736,22 @@ def get_parser(self, prog_name): metavar="", help=_("Volume to migrate (name or ID)"), ) - parser.add_argument( + destination_group = parser.add_mutually_exclusive_group(required=True) + destination_group.add_argument( '--host', metavar="", - required=True, help=_( "Destination host (takes the form: host@backend-name#pool)" ), ) + destination_group.add_argument( + '--cluster', + metavar="", + help=_( + "Destination cluster to migrate the volume to " + "(requires --os-volume-api-version 3.16 or higher)" + ), + ) parser.add_argument( '--force-host-copy', action="store_true", @@ -761,7 +769,6 @@ def get_parser(self, prog_name): "(possibly by another operation)" ), ) - # TODO(stephenfin): Add --cluster argument return parser def take_action(self, parsed_args): @@ -769,11 +776,22 @@ def take_action(self, parsed_args): volume = volume_client.find_volume( parsed_args.volume, ignore_missing=False ) + + if parsed_args.cluster and not sdk_utils.supports_microversion( + volume_client, '3.16' + ): + msg = _( + "--os-volume-api-version 3.16 or greater is required to " + "support the volume migration with cluster" + ) + raise exceptions.CommandError(msg) + volume_client.migrate_volume( volume.id, host=parsed_args.host, force_host_copy=parsed_args.force_host_copy, lock_volume=parsed_args.lock_volume, + cluster=parsed_args.cluster, ) diff --git a/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml b/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml new file mode 100644 index 000000000..5b9e87805 --- /dev/null +++ b/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``volume migration`` command now supports the ``--cluster`` + optional argument, allowing volumes to be migrated to a destination + cluster. This feature requires Cinder API microversion 3.16 or + higher and is mutually exclusive with the ``--host`` option. \ No newline at end of file From de81f473059c3fb5014fb10081da409b3664e14b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 10 Mar 2026 13:17:08 +0000 Subject: [PATCH 22/31] hacking: Ensure use of openstackclient.command These have the necessary type hints for the clientmanager and should always be used. Change-Id: I8420212be63dbeaff02c97823e1b80441cbc62ca Signed-off-by: Stephen Finucane --- hacking/checks.py | 17 +++++++++++++++++ tox.ini | 1 + 2 files changed, 18 insertions(+) diff --git a/hacking/checks.py b/hacking/checks.py index 0eb485e7e..0b0855d19 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -177,3 +177,20 @@ def assert_find_ignore_missing_kwargs(logical_line, filename): 'O403: Calls to find_* proxy methods must explicitly set ' 'ignore_missing', ) + + +@core.flake8ext +def assert_use_of_osc_command(logical_line, filename): + """Ensure we use openstackclient.command instead of osc_lib.command. + + O404 + """ + if filename == 'openstackclient/command.py': + return + + if re.match(r'^from osc_lib\.command import command$', logical_line): + yield ( + 0, + 'O404: Import Command classes from openstackclient.command, not ' + 'osc_lib.command.command', + ) diff --git a/tox.ini b/tox.ini index 6ce9e96c6..36ec6a426 100644 --- a/tox.ini +++ b/tox.ini @@ -128,4 +128,5 @@ extension = O401 = checks:assert_no_duplicated_setup O402 = checks:assert_use_of_client_aliases O403 = checks:assert_find_ignore_missing_kwargs + O404 = checks:assert_use_of_osc_command paths = ./hacking From c20a0537d736a0c3d4d3e051ba038514b5fff3c6 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 11 Mar 2026 13:46:34 +0000 Subject: [PATCH 23/31] Expand help of server migrate command mention server migrate is 'offline', and mention live migration and how it is different. Change-Id: Ia9af8e4a21989c39deaf2f6f7e1039b3fc4a0eef Signed-off-by: Pavlo Shchelokovskyy --- openstackclient/compute/v2/server.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 67a6a22c5..e89a48aee 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -3189,13 +3189,22 @@ class MigrateServer(command.Command): _description = _( """Migrate server to different host. -A migrate operation is implemented as a resize operation using the same flavor -as the old server. This means that, like resize, migrate works by creating a -new server using the same flavor and copying the contents of the original disk -into a new one. As with resize, the migrate operation is a two-step process for -the user: the first step is to perform the migrate, and the second step is to -either confirm (verify) success and release the old server, or to declare a -revert to release the new server and restart the old one.""" +There are two types of migration operation: a cold migration and a live +migration. + +A cold migration operation is implemented as a resize operation +using the same flavor as the old server. This means that, like resize, migrate +works by shutting down the original server, creating a new server using the +same flavor and copying the contents of the original disk into a new one. +As with resize, the migrate operation is a two-step process for the user: +the first step is to perform the migrate, and the second step is to either +confirm (verify) success and release the old server, or to declare a revert +to release the new server and restart the old one. + +By comparison, a live migration operation does not involve shutting the server +down, and is a one-step process that does not require a confirmation or revert +to finish. +""" ) def get_parser(self, prog_name): From a8d7f64817c432c476ce21595f45a04668319404 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 24 Mar 2026 23:25:26 +0100 Subject: [PATCH 24/31] tests: Add object store v1 FakeClientMixin As done elsewhere in e.g. Ic203964c7dede7dd80ae2d93b8fa1b7e6634a758 Change-Id: I8183761513fb2d498d40bb6491e9ee1e61ae5fd1 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/object/v1/fakes.py | 15 +- .../tests/unit/object/v1/test_container.py | 166 +++++++----------- .../unit/object/v1/test_container_all.py | 9 +- .../tests/unit/object/v1/test_object.py | 128 +++++--------- .../tests/unit/object/v1/test_object_all.py | 9 +- 5 files changed, 133 insertions(+), 194 deletions(-) diff --git a/openstackclient/tests/unit/object/v1/fakes.py b/openstackclient/tests/unit/object/v1/fakes.py index eedccdc0e..d43e5065f 100644 --- a/openstackclient/tests/unit/object/v1/fakes.py +++ b/openstackclient/tests/unit/object/v1/fakes.py @@ -11,9 +11,8 @@ # 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 keystoneauth1 import session +from unittest import mock from openstackclient.api import object_store_v1 as object_store from openstackclient.tests.unit import utils @@ -80,12 +79,14 @@ object_upload_name = 'test-object-name' -class TestObjectv1(utils.TestCommand): +class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.session = session.Session() - self.app.client_manager.object_store = object_store.APIv1( - session=self.app.client_manager.session, - endpoint=ENDPOINT, + self.app.client_manager.object_store = mock.Mock( + spec=object_store.APIv1 ) + self.object_store_client = self.app.client_manager.object_store + + +class TestObjectV1(FakeClientMixin, utils.TestCommand): ... diff --git a/openstackclient/tests/unit/object/v1/test_container.py b/openstackclient/tests/unit/object/v1/test_container.py index 9143df9c9..0ba9118bf 100644 --- a/openstackclient/tests/unit/object/v1/test_container.py +++ b/openstackclient/tests/unit/object/v1/test_container.py @@ -14,48 +14,21 @@ # import copy -from unittest import mock -from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container from openstackclient.tests.unit.object.v1 import fakes as object_fakes -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class FakeClient: - def __init__(self, endpoint=None, **kwargs): - self.endpoint = AUTH_URL - self.token = AUTH_TOKEN - - -class TestContainer(object_fakes.TestObjectv1): - columns = ('Name',) - +class TestContainerDelete(object_fakes.TestObjectV1): def setUp(self): super().setUp() - self.app.client_manager.object_store = object_store.APIv1( - session=mock.Mock(), - service_type="object-store", - ) - self.api = self.app.client_manager.object_store - -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_delete') -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_list') -@mock.patch('openstackclient.api.object_store_v1.APIv1.container_delete') -class TestContainerDelete(TestContainer): - def setUp(self): - super().setUp() + self.object_store_client.container_delete.return_value = None # Get the command object to test self.cmd = container.DeleteContainer(self.app, None) - def test_container_delete(self, c_mock, o_list_mock, o_delete_mock): - c_mock.return_value = None - + def test_container_delete(self): arglist = [ object_fakes.container_name, ] @@ -68,16 +41,17 @@ def test_container_delete(self, c_mock, o_list_mock, o_delete_mock): self.assertIsNone(self.cmd.take_action(parsed_args)) kwargs = {} - c_mock.assert_called_with( + self.object_store_client.container_delete.assert_called_with( container=object_fakes.container_name, **kwargs ) - self.assertFalse(o_list_mock.called) - self.assertFalse(o_delete_mock.called) + self.object_store_client.object_list.assert_not_called() + self.object_store_client.object_delete.assert_not_called() - def test_recursive_delete(self, c_mock, o_list_mock, o_delete_mock): - c_mock.return_value = None - o_list_mock.return_value = [object_fakes.OBJECT] - o_delete_mock.return_value = None + def test_recursive_delete(self): + self.object_store_client.object_delete.return_value = None + self.object_store_client.object_list.return_value = [ + object_fakes.OBJECT + ] arglist = [ '--recursive', @@ -91,20 +65,22 @@ def test_recursive_delete(self, c_mock, o_list_mock, o_delete_mock): self.assertIsNone(self.cmd.take_action(parsed_args)) - kwargs = {} - c_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.container_delete.assert_called_with( + container=object_fakes.container_name ) - o_list_mock.assert_called_with(container=object_fakes.container_name) - o_delete_mock.assert_called_with( + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name + ) + self.object_store_client.object_delete.assert_called_with( container=object_fakes.container_name, object=object_fakes.OBJECT['name'], ) - def test_r_delete(self, c_mock, o_list_mock, o_delete_mock): - c_mock.return_value = None - o_list_mock.return_value = [object_fakes.OBJECT] - o_delete_mock.return_value = None + def test_r_delete(self): + self.object_store_client.object_delete.return_value = None + self.object_store_client.object_list.return_value = [ + object_fakes.OBJECT + ] arglist = [ '-r', @@ -118,27 +94,29 @@ def test_r_delete(self, c_mock, o_list_mock, o_delete_mock): self.assertIsNone(self.cmd.take_action(parsed_args)) - kwargs = {} - c_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.container_delete.assert_called_with( + container=object_fakes.container_name ) - o_list_mock.assert_called_with(container=object_fakes.container_name) - o_delete_mock.assert_called_with( + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name + ) + self.object_store_client.object_delete.assert_called_with( container=object_fakes.container_name, object=object_fakes.OBJECT['name'], ) -@mock.patch('openstackclient.api.object_store_v1.APIv1.container_list') -class TestContainerList(TestContainer): +class TestContainerList(object_fakes.TestObjectV1): + columns = ('Name',) + def setUp(self): super().setUp() # Get the command object to test self.cmd = container.ListContainer(self.app, None) - def test_object_list_containers_no_options(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_no_options(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), copy.deepcopy(object_fakes.CONTAINER_2), @@ -153,9 +131,7 @@ def test_object_list_containers_no_options(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with() self.assertEqual(self.columns, columns) datalist = ( @@ -165,8 +141,8 @@ def test_object_list_containers_no_options(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_prefix(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_prefix(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -185,11 +161,9 @@ def test_object_list_containers_prefix(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'prefix': 'bit', - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + prefix='bit', + ) self.assertEqual(self.columns, columns) datalist = ( @@ -198,8 +172,8 @@ def test_object_list_containers_prefix(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_marker(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_marker(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -221,12 +195,10 @@ def test_object_list_containers_marker(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'marker': object_fakes.container_name, - 'end_marker': object_fakes.container_name_3, - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + marker=object_fakes.container_name, + end_marker=object_fakes.container_name_3, + ) self.assertEqual(self.columns, columns) datalist = ( @@ -235,8 +207,8 @@ def test_object_list_containers_marker(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_limit(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_limit(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -255,11 +227,9 @@ def test_object_list_containers_limit(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'limit': 2, - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + limit=2, + ) self.assertEqual(self.columns, columns) datalist = ( @@ -268,8 +238,8 @@ def test_object_list_containers_limit(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_long(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_long(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -287,9 +257,7 @@ def test_object_list_containers_long(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with() collist = ('Name', 'Bytes', 'Count') self.assertEqual(collist, columns) @@ -307,8 +275,8 @@ def test_object_list_containers_long(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_all(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_all(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_2), copy.deepcopy(object_fakes.CONTAINER_3), @@ -327,11 +295,9 @@ def test_object_list_containers_all(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'full_listing': True, - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + full_listing=True, + ) self.assertEqual(self.columns, columns) datalist = ( @@ -342,16 +308,17 @@ def test_object_list_containers_all(self, c_mock): self.assertEqual(datalist, tuple(data)) -@mock.patch('openstackclient.api.object_store_v1.APIv1.container_show') -class TestContainerShow(TestContainer): +class TestContainerShow(object_fakes.TestObjectV1): def setUp(self): super().setUp() # Get the command object to test self.cmd = container.ShowContainer(self.app, None) - def test_container_show(self, c_mock): - c_mock.return_value = copy.deepcopy(object_fakes.CONTAINER) + def test_container_show(self): + self.object_store_client.container_show.return_value = copy.deepcopy( + object_fakes.CONTAINER + ) arglist = [ object_fakes.container_name, @@ -366,11 +333,8 @@ def test_container_show(self, c_mock): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - # lib.container.show_container(api, url, container) - c_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.container_show.assert_called_with( + container=object_fakes.container_name, ) collist = ('bytes', 'count', 'name') diff --git a/openstackclient/tests/unit/object/v1/test_container_all.py b/openstackclient/tests/unit/object/v1/test_container_all.py index 0a795dd86..7de013788 100644 --- a/openstackclient/tests/unit/object/v1/test_container_all.py +++ b/openstackclient/tests/unit/object/v1/test_container_all.py @@ -13,17 +13,24 @@ import copy +from keystoneauth1 import session from requests_mock.contrib import fixture +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container as container_cmds from openstackclient.tests.unit.object.v1 import fakes as object_fakes -class TestContainerAll(object_fakes.TestObjectv1): +class TestContainerAll(object_fakes.TestObjectV1): def setUp(self): super().setUp() + # these tests require a "real" client since we mock requests self.requests_mock = self.useFixture(fixture.Fixture()) + self.app.client_manager.object_store = object_store.APIv1( + session=session.Session(), + endpoint=object_fakes.ENDPOINT, + ) class TestContainerCreate(TestContainerAll): diff --git a/openstackclient/tests/unit/object/v1/test_object.py b/openstackclient/tests/unit/object/v1/test_object.py index f1777f963..d544cd00e 100644 --- a/openstackclient/tests/unit/object/v1/test_object.py +++ b/openstackclient/tests/unit/object/v1/test_object.py @@ -14,29 +14,12 @@ # import copy -from unittest import mock -from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as obj from openstackclient.tests.unit.object.v1 import fakes as object_fakes -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class TestObject(object_fakes.TestObjectv1): - def setUp(self): - super().setUp() - self.app.client_manager.object_store = object_store.APIv1( - session=mock.Mock(), - service_type="object-store", - ) - self.api = self.app.client_manager.object_store - - -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_list') -class TestObjectList(TestObject): +class TestObjectList(object_fakes.TestObjectV1): columns = ('Name',) datalist = ((object_fakes.object_name_2,),) @@ -46,8 +29,8 @@ def setUp(self): # Get the command object to test self.cmd = obj.ListObject(self.app, None) - def test_object_list_objects_no_options(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_no_options(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT), copy.deepcopy(object_fakes.OBJECT_2), ] @@ -65,7 +48,7 @@ def test_object_list_objects_no_options(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - o_mock.assert_called_with( + self.object_store_client.object_list.assert_called_with( container=object_fakes.container_name, ) @@ -76,8 +59,8 @@ def test_object_list_objects_no_options(self, o_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_objects_prefix(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_prefix(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -97,19 +80,16 @@ def test_object_list_objects_prefix(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'prefix': 'floppy', - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + prefix='floppy', ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_delimiter(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_delimiter(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -129,19 +109,16 @@ def test_object_list_objects_delimiter(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'delimiter': '=', - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + delimiter='=', ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_marker(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_marker(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -161,19 +138,16 @@ def test_object_list_objects_marker(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'marker': object_fakes.object_name_2, - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + marker=object_fakes.object_name_2, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_end_marker(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_end_marker(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -193,19 +167,16 @@ def test_object_list_objects_end_marker(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'end_marker': object_fakes.object_name_2, - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + end_marker=object_fakes.object_name_2, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_limit(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_limit(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -225,19 +196,16 @@ def test_object_list_objects_limit(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'limit': 2, - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + limit=2, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_long(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_long(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT), copy.deepcopy(object_fakes.OBJECT_2), ] @@ -257,10 +225,8 @@ def test_object_list_objects_long(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - o_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name, ) collist = ('Name', 'Bytes', 'Hash', 'Content Type', 'Last Modified') @@ -283,8 +249,8 @@ def test_object_list_objects_long(self, o_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_objects_all(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_all(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT), copy.deepcopy(object_fakes.OBJECT_2), ] @@ -304,12 +270,9 @@ def test_object_list_objects_all(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'full_listing': True, - } - o_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name, + full_listing=True, ) self.assertEqual(self.columns, columns) @@ -320,16 +283,17 @@ def test_object_list_objects_all(self, o_mock): self.assertEqual(datalist, tuple(data)) -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_show') -class TestObjectShow(TestObject): +class TestObjectShow(object_fakes.TestObjectV1): def setUp(self): super().setUp() # Get the command object to test self.cmd = obj.ShowObject(self.app, None) - def test_object_show(self, c_mock): - c_mock.return_value = copy.deepcopy(object_fakes.OBJECT) + def test_object_show(self): + self.object_store_client.object_show.return_value = copy.deepcopy( + object_fakes.OBJECT + ) arglist = [ object_fakes.container_name, @@ -346,13 +310,9 @@ def test_object_show(self, c_mock): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - # lib.container.show_container(api, url, container) - c_mock.assert_called_with( + self.object_store_client.object_show.assert_called_with( container=object_fakes.container_name, object=object_fakes.object_name_1, - **kwargs, ) collist = ('bytes', 'content_type', 'hash', 'last_modified', 'name') diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py index 968667b68..66dfabf48 100644 --- a/openstackclient/tests/unit/object/v1/test_object_all.py +++ b/openstackclient/tests/unit/object/v1/test_object_all.py @@ -15,18 +15,25 @@ import io from unittest import mock +from keystoneauth1 import session from osc_lib import exceptions from requests_mock.contrib import fixture +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as object_cmds from openstackclient.tests.unit.object.v1 import fakes as object_fakes -class TestObjectAll(object_fakes.TestObjectv1): +class TestObjectAll(object_fakes.TestObjectV1): def setUp(self): super().setUp() + # these tests require a "real" client since we mock requests self.requests_mock = self.useFixture(fixture.Fixture()) + self.app.client_manager.object_store = object_store.APIv1( + session=session.Session(), + endpoint=object_fakes.ENDPOINT, + ) class TestObjectCreate(TestObjectAll): From afc32da26463a5c440ed350b38c1a497a6e63dc1 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 25 Mar 2026 00:12:12 +0100 Subject: [PATCH 25/31] hacking: Fix typos 0 != O. Change-Id: Iea443da7e802607081150c52d38be2f28293a969 Signed-off-by: Stephen Finucane --- hacking/checks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hacking/checks.py b/hacking/checks.py index 0b0855d19..9d3b0d551 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -32,8 +32,10 @@ def assert_no_oslo(logical_line): O400 """ - if re.match(r'(from|import) oslo_.*', logical_line): - yield (0, "0400: oslo libraries should not be used in SDK projects") + if match := re.match(r'(from|import) (oslo_.*)', logical_line): + if match.group(2) == 'oslo_i18n': + return + yield (0, "O400: oslo libraries should not be used in SDK projects") @core.flake8ext @@ -86,7 +88,7 @@ def assert_use_of_client_aliases(logical_line): logical_line, ): service = match.group(1) - yield (0, f"0402: prefer {service}_client to sdk_connection.{service}") + yield (0, f"O402: prefer {service}_client to sdk_connection.{service}") if match := re.match( r'(self\.app\.client_manager\.(compute|network|image)+\.[a-z_]+) = mock.Mock', # noqa: E501 From 793739ad01e37ed8a25e13697a726e761f0a32d9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 24 Mar 2026 21:26:23 +0100 Subject: [PATCH 26/31] hacking: Make check for duplicate clients less specific Change-Id: Ie08a09c209d24d54b062a44e5b535c8a2926285c Signed-off-by: Stephen Finucane --- hacking/checks.py | 2 +- .../tests/unit/network/v2/test_network_qos_policy.py | 2 +- openstackclient/tests/unit/volume/v3/test_volume_attachment.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hacking/checks.py b/hacking/checks.py index 9d3b0d551..a94891ebf 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -55,7 +55,7 @@ def assert_no_duplicated_setup(logical_line, filename): if os.path.basename(filename) != 'fakes.py': if re.match( - r'self.[a-z_]+_client = self.app.client_manager.*', logical_line + r'self.[a-zA-Z0-9_]+ = self.app.client_manager.*', logical_line ): yield ( 0, diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py index 17f40ef6b..29f775434 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py @@ -28,7 +28,7 @@ class TestQosPolicy(network_fakes.TestNetworkV2): def setUp(self): super().setUp() # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock = self.identity_client.projects class TestCreateNetworkQosPolicy(TestQosPolicy): diff --git a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py index b7838e034..d8e6dcacb 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py @@ -23,7 +23,7 @@ class TestVolumeAttachment(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock = self.identity_client.projects class TestVolumeAttachmentCreate(TestVolumeAttachment): From 1cc484f2b50b6839716743b4cb51784ab5e4b08e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 25 Mar 2026 00:02:42 +0100 Subject: [PATCH 27/31] tests: Make use of fake client aliases Change-Id: Iae56252c955540360b50c77594a1dff9d82b764f Signed-off-by: Stephen Finucane --- .../tests/unit/common/test_extension.py | 4 +- .../tests/unit/compute/v2/test_server.py | 6 +-- ...{test_osc_tap_flow.py => test_tap_flow.py} | 43 +++++++---------- ...t_osc_tap_mirror.py => test_tap_mirror.py} | 48 ++++++++----------- ...osc_tap_service.py => test_tap_service.py} | 48 ++++++++----------- 5 files changed, 61 insertions(+), 88 deletions(-) rename openstackclient/tests/unit/network/v2/taas/{test_osc_tap_flow.py => test_tap_flow.py} (85%) rename openstackclient/tests/unit/network/v2/taas/{test_osc_tap_mirror.py => test_tap_mirror.py} (83%) rename openstackclient/tests/unit/network/v2/taas/{test_osc_tap_service.py => test_tap_service.py} (82%) diff --git a/openstackclient/tests/unit/common/test_extension.py b/openstackclient/tests/unit/common/test_extension.py index dd684312c..1141a81a3 100644 --- a/openstackclient/tests/unit/common/test_extension.py +++ b/openstackclient/tests/unit/common/test_extension.py @@ -294,7 +294,7 @@ def setUp(self): self.cmd = extension.ShowExtension(self.app, None) - self.app.client_manager.network.find_extension.return_value = ( + self.network_client.find_extension.return_value = ( self.extension_details ) @@ -322,7 +322,7 @@ def test_show_all_options(self): columns, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.find_extension.assert_called_with( + self.network_client.find_extension.assert_called_with( self.extension_details.alias, ignore_missing=False ) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index b5f77aff7..e88b07a4e 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1608,8 +1608,8 @@ def find_port(name_or_id, ignore_missing): port_port2.id: port_port2, }[name_or_id] - self.app.client_manager.network.find_network.side_effect = find_network - self.app.client_manager.network.find_port.side_effect = find_port + self.network_client.find_network.side_effect = find_network + self.network_client.find_port.side_effect = find_port arglist = [ '--image', @@ -1728,7 +1728,7 @@ def test_server_create_with_network_tag(self): self.set_compute_api_version('2.43') network = network_fakes.create_one_network() - self.app.client_manager.network.find_network.return_value = network + self.network_client.find_network.return_value = network arglist = [ '--image', diff --git a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_flow.py b/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py similarity index 85% rename from openstackclient/tests/unit/network/v2/taas/test_osc_tap_flow.py rename to openstackclient/tests/unit/network/v2/taas/test_tap_flow.py index 8e4f185c8..8cd1c3f15 100644 --- a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_flow.py +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py @@ -77,13 +77,9 @@ def test_create_tap_flow(self): 'direction': 'BOTH', }, ) - self.app.client_manager.network.create_tap_flow.return_value = ( - fake_tap_flow - ) - self.app.client_manager.network.find_port.return_value = fake_port - self.app.client_manager.network.find_tap_service.return_value = ( - fake_tap_service - ) + self.network_client.create_tap_flow.return_value = fake_tap_flow + self.network_client.find_port.return_value = fake_port + self.network_client.find_tap_service.return_value = fake_tap_service arg_list = [ '--name', fake_tap_flow['name'], @@ -103,8 +99,7 @@ def test_create_tap_flow(self): parsed_args = self.check_parser(self.cmd, arg_list, verify_list) columns, data = self.cmd.take_action(parsed_args) - mock_create_t_f = self.app.client_manager.network.create_tap_flow - mock_create_t_f.assert_called_once_with( + self.network_client.create_tap_flow.assert_called_once_with( **{ 'name': fake_tap_flow['name'], 'source_port': fake_tap_flow['source_port'], @@ -129,7 +124,7 @@ def test_list_tap_flows(self): fake_tap_flows = list( sdk_fakes.generate_fake_resources(_tap_flow.TapFlow, count=2) ) - self.app.client_manager.network.tap_flows.return_value = fake_tap_flows + self.network_client.tap_flows.return_value = fake_tap_flows arg_list = [] verify_list = [] @@ -137,7 +132,7 @@ def test_list_tap_flows(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.tap_flows.assert_called_once() + self.network_client.tap_flows.assert_called_once() self.assertEqual(headers, list(headers_long)) self.assertCountEqual( list(data), @@ -151,7 +146,7 @@ def test_list_tap_flows(self): class TestDeleteTapFlow(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_flow.side_effect = ( + self.network_client.find_tap_flow.side_effect = ( lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) ) self.cmd = osc_tap_flow.DeleteTapFlow(self.app, None) @@ -171,8 +166,9 @@ def test_delete_tap_flow(self): result = self.cmd.take_action(parsed_args) - mock_delete_tap_flow = self.app.client_manager.network.delete_tap_flow - mock_delete_tap_flow.assert_called_once_with(fake_tap_flow['id']) + self.network_client.delete_tap_flow.assert_called_once_with( + fake_tap_flow['id'] + ) self.assertIsNone(result) @@ -190,7 +186,7 @@ class TestShowTapFlow(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_flow.side_effect = ( + self.network_client.find_tap_flow.side_effect = ( lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) ) self.cmd = osc_tap_flow.ShowTapFlow(self.app, None) @@ -198,9 +194,7 @@ def setUp(self): def test_show_tap_flow(self): """Test Show tap flow.""" fake_tap_flow = sdk_fakes.generate_fake_resource(_tap_flow.TapFlow) - self.app.client_manager.network.get_tap_flow.return_value = ( - fake_tap_flow - ) + self.network_client.get_tap_flow.return_value = fake_tap_flow arg_list = [ fake_tap_flow['id'], ] @@ -212,7 +206,7 @@ def test_show_tap_flow(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.get_tap_flow.assert_called_once_with( + self.network_client.get_tap_flow.assert_called_once_with( fake_tap_flow['id'] ) self.assertEqual(self.columns, headers) @@ -245,7 +239,7 @@ class TestUpdateTapFlow(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.cmd = osc_tap_flow.UpdateTapFlow(self.app, None) - self.app.client_manager.network.find_tap_flow.side_effect = ( + self.network_client.find_tap_flow.side_effect = ( lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) ) @@ -255,9 +249,7 @@ def test_update_tap_flow(self): new_tap_flow = copy.deepcopy(fake_tap_flow) new_tap_flow['name'] = self._new_name - self.app.client_manager.network.update_tap_flow.return_value = ( - new_tap_flow - ) + self.network_client.update_tap_flow.return_value = new_tap_flow arg_list = [ fake_tap_flow['id'], @@ -270,7 +262,8 @@ def test_update_tap_flow(self): columns, data = self.cmd.take_action(parsed_args) attrs = {'name': self._new_name} - mock_update_t_f = self.app.client_manager.network.update_tap_flow - mock_update_t_f.assert_called_once_with(new_tap_flow['id'], **attrs) + self.network_client.update_tap_flow.assert_called_once_with( + new_tap_flow['id'], **attrs + ) self.assertEqual(self.columns, columns) self.assertEqual(_get_data(new_tap_flow, self.columns), data) diff --git a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_mirror.py b/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py similarity index 83% rename from openstackclient/tests/unit/network/v2/taas/test_osc_tap_mirror.py rename to openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py index 10f3251c3..1d153b51e 100644 --- a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_mirror.py +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py @@ -64,11 +64,9 @@ def test_create_tap_mirror(self): fake_tap_mirror = sdk_fakes.generate_fake_resource( tap_mirror.TapMirror, **{'port_id': port_id, 'directions': 'IN=99'} ) - self.app.client_manager.network.create_tap_mirror.return_value = ( - fake_tap_mirror - ) - self.app.client_manager.network.find_port.return_value = fake_port - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.create_tap_mirror.return_value = fake_tap_mirror + self.network_client.find_port.return_value = fake_port + self.network_client.find_tap_mirror.side_effect = ( lambda _, name_or_id: {'id': name_or_id} ) arg_list = [ @@ -96,13 +94,10 @@ def test_create_tap_mirror(self): ] parsed_args = self.check_parser(self.cmd, arg_list, verify_list) - self.app.client_manager.network.find_tap_mirror.return_value = ( - fake_tap_mirror - ) + self.network_client.find_tap_mirror.return_value = fake_tap_mirror columns, data = self.cmd.take_action(parsed_args) - create_tap_m_mock = self.app.client_manager.network.create_tap_mirror - create_tap_m_mock.assert_called_once_with( + self.network_client.create_tap_mirror.assert_called_once_with( **{ 'name': fake_tap_mirror['name'], 'port_id': fake_tap_mirror['port_id'], @@ -128,9 +123,7 @@ def test_list_tap_mirror(self): fake_tap_mirrors = list( sdk_fakes.generate_fake_resources(tap_mirror.TapMirror, count=4) ) - self.app.client_manager.network.tap_mirrors.return_value = ( - fake_tap_mirrors - ) + self.network_client.tap_mirrors.return_value = fake_tap_mirrors arg_list = [] verify_list = [] @@ -139,7 +132,7 @@ def test_list_tap_mirror(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.tap_mirrors.assert_called_once() + self.network_client.tap_mirrors.assert_called_once() self.assertEqual(headers, list(headers_long)) self.assertCountEqual( list(data), @@ -153,7 +146,7 @@ def test_list_tap_mirror(self): class TestDeleteTapMirror(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.find_tap_mirror.side_effect = ( lambda name_or_id, ignore_missing: tap_mirror.TapMirror( id=name_or_id ) @@ -177,8 +170,9 @@ def test_delete_tap_mirror(self): parsed_args = self.check_parser(self.cmd, arg_list, verify_list) result = self.cmd.take_action(parsed_args) - mock_delete_tap_m = self.app.client_manager.network.delete_tap_mirror - mock_delete_tap_m.assert_called_once_with(fake_tap_mirror['id']) + self.network_client.delete_tap_mirror.assert_called_once_with( + fake_tap_mirror['id'] + ) self.assertIsNone(result) @@ -196,7 +190,7 @@ class TestShowTapMirror(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.find_tap_mirror.side_effect = ( lambda name_or_id, ignore_missing: tap_mirror.TapMirror( id=name_or_id ) @@ -209,9 +203,7 @@ def test_show_tap_mirror(self): fake_tap_mirror = sdk_fakes.generate_fake_resource( tap_mirror.TapMirror ) - self.app.client_manager.network.get_tap_mirror.return_value = ( - fake_tap_mirror - ) + self.network_client.get_tap_mirror.return_value = fake_tap_mirror arg_list = [ fake_tap_mirror['id'], ] @@ -223,8 +215,9 @@ def test_show_tap_mirror(self): headers, data = self.cmd.take_action(parsed_args) - mock_get_tap_m = self.app.client_manager.network.get_tap_mirror - mock_get_tap_m.assert_called_once_with(fake_tap_mirror['id']) + self.network_client.get_tap_mirror.assert_called_once_with( + fake_tap_mirror['id'] + ) self.assertEqual(self.columns, headers) fake_data = _get_data( fake_tap_mirror, osc_tap_mirror._get_columns(fake_tap_mirror)[1] @@ -248,7 +241,7 @@ class TestUpdateTapMirror(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.cmd = osc_tap_mirror.UpdateTapMirror(self.app, None) - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.find_tap_mirror.side_effect = ( lambda name_or_id, ignore_missing: tap_mirror.TapMirror( id=name_or_id ) @@ -262,9 +255,7 @@ def test_update_tap_mirror(self): new_tap_mirror = copy.deepcopy(fake_tap_mirror) new_tap_mirror['name'] = self._new_name - self.app.client_manager.network.update_tap_mirror.return_value = ( - new_tap_mirror - ) + self.network_client.update_tap_mirror.return_value = new_tap_mirror arg_list = [ fake_tap_mirror['id'], @@ -277,8 +268,7 @@ def test_update_tap_mirror(self): columns, data = self.cmd.take_action(parsed_args) attrs = {'name': self._new_name} - mock_update_tap_m = self.app.client_manager.network.update_tap_mirror - mock_update_tap_m.assert_called_once_with( + self.network_client.update_tap_mirror.assert_called_once_with( fake_tap_mirror['id'], **attrs ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_service.py b/openstackclient/tests/unit/network/v2/taas/test_tap_service.py similarity index 82% rename from openstackclient/tests/unit/network/v2/taas/test_osc_tap_service.py rename to openstackclient/tests/unit/network/v2/taas/test_tap_service.py index fa766891e..dac094f90 100644 --- a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_service.py +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_service.py @@ -65,11 +65,9 @@ def test_create_tap_service(self): fake_tap_service = sdk_fakes.generate_fake_resource( tap_service.TapService, **{'port_id': port_id} ) - self.app.client_manager.network.create_tap_service.return_value = ( - fake_tap_service - ) - self.app.client_manager.network.find_port.return_value = fake_port - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.create_tap_service.return_value = fake_tap_service + self.network_client.find_port.return_value = fake_port + self.network_client.find_tap_service.side_effect = ( lambda _, name_or_id: {'id': name_or_id} ) arg_list = [ @@ -85,13 +83,10 @@ def test_create_tap_service(self): ] parsed_args = self.check_parser(self.cmd, arg_list, verify_list) - self.app.client_manager.network.find_tap_service.return_value = ( - fake_tap_service - ) + self.network_client.find_tap_service.return_value = fake_tap_service columns, data = self.cmd.take_action(parsed_args) - create_tap_s_mock = self.app.client_manager.network.create_tap_service - create_tap_s_mock.assert_called_once_with( + self.network_client.create_tap_service.assert_called_once_with( **{ 'name': fake_tap_service['name'], 'port_id': fake_tap_service['port_id'], @@ -114,9 +109,7 @@ def test_list_tap_service(self): fake_tap_services = list( sdk_fakes.generate_fake_resources(tap_service.TapService, count=4) ) - self.app.client_manager.network.tap_services.return_value = ( - fake_tap_services - ) + self.network_client.tap_services.return_value = fake_tap_services arg_list = [] verify_list = [] @@ -125,7 +118,7 @@ def test_list_tap_service(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.tap_services.assert_called_once() + self.network_client.tap_services.assert_called_once() self.assertEqual(headers, list(headers_long)) self.assertCountEqual( list(data), @@ -139,7 +132,7 @@ def test_list_tap_service(self): class TestDeleteTapService(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.find_tap_service.side_effect = ( lambda name_or_id, ignore_missing: tap_service.TapService( id=name_or_id ) @@ -163,8 +156,9 @@ def test_delete_tap_service(self): parsed_args = self.check_parser(self.cmd, arg_list, verify_list) result = self.cmd.take_action(parsed_args) - mock_delete_tap_s = self.app.client_manager.network.delete_tap_service - mock_delete_tap_s.assert_called_once_with(fake_tap_service['id']) + self.network_client.delete_tap_service.assert_called_once_with( + fake_tap_service['id'] + ) self.assertIsNone(result) @@ -180,7 +174,7 @@ class TestShowTapService(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.find_tap_service.side_effect = ( lambda name_or_id, ignore_missing: tap_service.TapService( id=name_or_id ) @@ -193,9 +187,7 @@ def test_show_tap_service(self): fake_tap_service = sdk_fakes.generate_fake_resource( tap_service.TapService ) - self.app.client_manager.network.get_tap_service.return_value = ( - fake_tap_service - ) + self.network_client.get_tap_service.return_value = fake_tap_service arg_list = [ fake_tap_service['id'], ] @@ -207,8 +199,9 @@ def test_show_tap_service(self): headers, data = self.cmd.take_action(parsed_args) - mock_get_tap_s = self.app.client_manager.network.get_tap_service - mock_get_tap_s.assert_called_once_with(fake_tap_service['id']) + self.network_client.get_tap_service.assert_called_once_with( + fake_tap_service['id'] + ) self.assertEqual(self.columns, headers) fake_data = _get_data( fake_tap_service, osc_tap_service._get_columns(fake_tap_service)[1] @@ -231,7 +224,7 @@ class TestUpdateTapService(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.cmd = osc_tap_service.UpdateTapService(self.app, None) - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.find_tap_service.side_effect = ( lambda name_or_id, ignore_missing: tap_service.TapService( id=name_or_id ) @@ -245,9 +238,7 @@ def test_update_tap_service(self): new_tap_service = copy.deepcopy(fake_tap_service) new_tap_service['name'] = self._new_name - self.app.client_manager.network.update_tap_service.return_value = ( - new_tap_service - ) + self.network_client.update_tap_service.return_value = new_tap_service arg_list = [ fake_tap_service['id'], @@ -260,8 +251,7 @@ def test_update_tap_service(self): columns, data = self.cmd.take_action(parsed_args) attrs = {'name': self._new_name} - mock_update_tap_s = self.app.client_manager.network.update_tap_service - mock_update_tap_s.assert_called_once_with( + self.network_client.update_tap_service.assert_called_once_with( fake_tap_service['id'], **attrs ) self.assertEqual(self.columns, columns) From 39842bcf0bf635148ba4358b8480cc70a4556191 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 24 Mar 2026 23:58:39 +0100 Subject: [PATCH 28/31] hacking: Check for overly complicated client access Change-Id: I531c0879c413cf500b099826047d03dd90dc0f5d Signed-off-by: Stephen Finucane --- hacking/checks.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/hacking/checks.py b/hacking/checks.py index a94891ebf..6558e59a6 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -77,7 +77,7 @@ def assert_no_duplicated_setup(logical_line, filename): @core.flake8ext -def assert_use_of_client_aliases(logical_line): +def assert_use_of_client_aliases(logical_line, filename): """Ensure we use $service_client instead of $sdk_connection.service. O402 @@ -90,6 +90,21 @@ def assert_use_of_client_aliases(logical_line): service = match.group(1) yield (0, f"O402: prefer {service}_client to sdk_connection.{service}") + # everything from here down only affects unit tests + if os.path.join('openstackclient', 'tests', 'unit') not in filename: + return + + if match := re.match( + r'(self\.app\.client_manager\.(compute|network|image)+\.[a-z_]+)\.(return_value|side_effect) = ', # noqa: E501 + logical_line, + ): + service = match.group(1) + yield ( + 0, + f"O402: prefer {service}_client to " + f"self.app.client_manager.{service}", + ) + if match := re.match( r'(self\.app\.client_manager\.(compute|network|image)+\.[a-z_]+) = mock.Mock', # noqa: E501 logical_line, From 47ac6e2780b0995a080f6931b90e76b113a49a65 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 25 Mar 2026 00:22:22 +0100 Subject: [PATCH 29/31] hacking: Fix check for missing ignore_missing calls Change-Id: I54f7bf2026507b357fdf921993c69913e7c32db2 Signed-off-by: Stephen Finucane --- hacking/checks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hacking/checks.py b/hacking/checks.py index 6558e59a6..2c2c09944 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -154,6 +154,11 @@ def visit_Call(self, node): isinstance(node.func.value, ast.Name) and node.func.value.id.endswith('client') ) + or ( + # handle calls like 'self.compute_client.find_server' + isinstance(node.func.value, ast.Attribute) + and node.func.value.attr.endswith('_client') + ) or ( # handle calls like 'self.app.client_manager.image.find_image' isinstance(node.func.value, ast.Attribute) From 73c66035c4ec8519d3bc9ad628526ca0df3caeef Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 25 Mar 2026 00:11:25 +0100 Subject: [PATCH 30/31] hacking: Add doctests Nothing actually tests these yet, but it is the desired behavior. Change-Id: I3c89234a7a06e06f2740aec0bc7dc19da9cafeb5 Signed-off-by: Stephen Finucane --- hacking/checks.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/hacking/checks.py b/hacking/checks.py index 2c2c09944..80807df87 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -30,7 +30,10 @@ def assert_no_oslo(logical_line): """Check for use of oslo libraries. - O400 + Okay: import os + Okay: from os import path + O400: import oslo_messaging + O400: from oslo_log import log """ if match := re.match(r'(from|import) (oslo_.*)', logical_line): if match.group(2) == 'oslo_i18n': @@ -42,7 +45,13 @@ def assert_no_oslo(logical_line): def assert_no_duplicated_setup(logical_line, filename): """Check for use of various unnecessary test duplications. - O401 + This check only applies to files under openstackclient/tests/unit/. + + Okay: self.app = fakes.FakeShell() + Okay: self.app.client_manager.auth_ref = mock.Mock() + O401: self.app = Namespace(self.app, self.namespace) + O401: self.network_client = self.app.client_manager.network + O401: self.app.client_manager.network = mock.Mock() """ if os.path.join('openstackclient', 'tests', 'unit') not in filename: return @@ -80,7 +89,14 @@ def assert_no_duplicated_setup(logical_line, filename): def assert_use_of_client_aliases(logical_line, filename): """Ensure we use $service_client instead of $sdk_connection.service. - O402 + Okay: self.compute_client.find_server(foo) + O402: self.app.client_manager.sdk_connnection.compute.find_server(foo) + + The following checks only apply to files under openstackclient/tests/unit/: + + O402: self.app.client_manager.compute.find_server.return_value = server + O402: self.app.client_manager.compute.find_server = mock.Mock() + O402: self.compute_client.find_server = mock.Mock() """ # we should expand the list of services as we drop legacy clients if match := re.match( @@ -205,7 +221,9 @@ def assert_find_ignore_missing_kwargs(logical_line, filename): def assert_use_of_osc_command(logical_line, filename): """Ensure we use openstackclient.command instead of osc_lib.command. - O404 + Okay: from openstackclient.command import command + Okay: import openstackclient.command + O404: from osc_lib.command import command """ if filename == 'openstackclient/command.py': return From b031b7d9ab8204ce337032832efaa57c8d0c4a84 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 25 Mar 2026 00:23:24 +0100 Subject: [PATCH 31/31] hacking: Add tests Change-Id: Ic973aaa0ea623575399868c47e8aac7b7d6d0561 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/test_hacking.py | 108 +++++++++++++++++++++ test-requirements.txt | 1 + 2 files changed, 109 insertions(+) create mode 100644 openstackclient/tests/unit/test_hacking.py diff --git a/openstackclient/tests/unit/test_hacking.py b/openstackclient/tests/unit/test_hacking.py new file mode 100644 index 000000000..ff22ffb2c --- /dev/null +++ b/openstackclient/tests/unit/test_hacking.py @@ -0,0 +1,108 @@ +# 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 importlib.util +import os +import re +import subprocess +import sys +import unittest + +import fixtures + +ROOT_DIR = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..', '..') +) +SELFTEST_REGEX = re.compile(r'\b(Okay|[HEW]\d{3}|O\d{3}):\s(.*)') + +# Checks that filter on 'openstackclient/tests/unit' in the filename need the +# temp file written into that path structure so the check is not skipped. +_UNIT_TEST_SUBDIRS = { + 'O401': os.path.join('openstackclient', 'tests', 'unit'), + 'O402': os.path.join('openstackclient', 'tests', 'unit'), +} + + +def _load_checks(): + spec = importlib.util.spec_from_file_location( + '_osc_hacking_checks', + os.path.join(ROOT_DIR, 'hacking', 'checks.py'), + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _get_examples(check): + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match: + yield match.group(1), match.group(2) + + +class HackingTestCase(unittest.TestCase): + def _test_check(self, code, source): + lines = [ + part.replace(r'\t', '\t') + '\n' for part in source.split(r'\n') + ] + subdir = { + 'O401': os.path.join('openstackclient', 'tests', 'unit'), + 'O402': os.path.join('openstackclient', 'tests', 'unit'), + }.get(code, '') + + with fixtures.TempDir() as tmp: + dirpath = os.path.join(tmp.path, subdir) if subdir else tmp.path + if subdir: + os.makedirs(dirpath) + + fpath = os.path.join(dirpath, 'test_tmp.py') + with open(fpath, 'w') as f: + f.write(''.join(lines)) + + cmd = [ + sys.executable, + '-mflake8', + '--config', + os.path.join(ROOT_DIR, 'tox.ini'), + f'--select={code}', + '--format=%(code)s\t%(path)s\t%(row)d', + fpath, + ] + out, _ = subprocess.Popen( + cmd, stdout=subprocess.PIPE, cwd=ROOT_DIR + ).communicate() + out = out.decode('utf-8') + + if code == 'Okay': + self.assertEqual('', out) + else: + self.assertNotEqual('', out, f"Failed to trigger rule {code}") + self.assertEqual(code, out.split('\t')[0].rstrip(':'), out) + + def test_checks(self): + checks_module = _load_checks() + + for name in sorted(dir(checks_module)): + check = getattr(checks_module, name) + if not callable(check): + continue + + if getattr(check, 'skip_on_py3', None) is not False: + continue + + if not check.__doc__: + continue + + for code, source in _get_examples(check): + with self.subTest(check=name, example=source): + self._test_check(code, source) diff --git a/test-requirements.txt b/test-requirements.txt index c9c1b28cc..d6a0db5b4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,6 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD +hacking>=8.0.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT