diff --git a/.gitreview b/.gitreview index 4eee726db..c92c5e404 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/python-openstackclient.git +defaultbranch=stable/2025.2 diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index f706a5974..41c57e63d 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -746,21 +746,32 @@ def take_action(self, parsed_args): # values if the project or class does not exist. This is expected # behavior. However, we have already checked for the presence of the # project above so it shouldn't be an issue. - if parsed_args.service in {'all', 'compute'}: + if parsed_args.service == 'compute' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_compute_endpoint_enabled() + ): compute_quota_info = get_compute_quotas( self.app, project, detail=parsed_args.usage, default=parsed_args.default, ) - if parsed_args.service in {'all', 'volume'}: + + if parsed_args.service == 'volume' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_volume_endpoint_enabled() + ): volume_quota_info = get_volume_quotas( self.app, project, detail=parsed_args.usage, default=parsed_args.default, ) - if parsed_args.service in {'all', 'network'}: + + if parsed_args.service == 'network' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_network_endpoint_enabled() + ): network_quota_info = get_network_quotas( self.app, project, @@ -906,12 +917,18 @@ def take_action(self, parsed_args): ) # compute quotas - if parsed_args.service in {'all', 'compute'}: + if parsed_args.service == 'compute' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_compute_endpoint_enabled() + ): compute_client = self.app.client_manager.compute compute_client.revert_quota_set(project.id) # volume quotas - if parsed_args.service in {'all', 'volume'}: + if parsed_args.service == 'volume' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_volume_endpoint_enabled() + ): volume_client = self.app.client_manager.sdk_connection.volume volume_client.revert_quota_set(project.id) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index e14392a53..f7f35a478 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -184,8 +184,13 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): 'user_data': 'OS-EXT-SRV-ATTR:user_data', 'vm_state': 'OS-EXT-STS:vm_state', 'pinned_availability_zone': 'pinned_availability_zone', - 'scheduler_hints': 'scheduler_hints', } + + # NOTE(ratailor): microversion 2.100 introduces + # scheduler_hints support + if sdk_utils.supports_microversion(compute_client, '2.100'): + column_map['scheduler_hints'] = 'scheduler_hints' + # Some columns returned by openstacksdk should not be shown because they're # either irrelevant or duplicates ignored_columns = { @@ -326,10 +331,11 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): info['OS-EXT-STS:power_state'] ) - if 'scheduler_hints' in info: - info['scheduler_hints'] = format_columns.DictListColumn( - info.pop('scheduler_hints', {}), - ) + if sdk_utils.supports_microversion(compute_client, '2.100'): + if 'scheduler_hints' in info: + info['scheduler_hints'] = format_columns.DictListColumn( + info.pop('scheduler_hints', {}), + ) return info @@ -2841,16 +2847,18 @@ def take_action(self, parsed_args): 'pinned_availability_zone', 'hypervisor_hostname', 'metadata', - 'scheduler_hints', ) column_headers += ( 'Availability Zone', 'Pinned Availability Zone', 'Host', 'Properties', - 'Scheduler Hints', ) + if sdk_utils.supports_microversion(compute_client, '2.100'): + columns += ('scheduler_hints',) + column_headers += ('Scheduler Hints',) + if parsed_args.all_projects: columns += ('project_id',) column_headers += ('Project ID',) @@ -2901,8 +2909,11 @@ def take_action(self, parsed_args): 'scheduler_hints', "Scheduler Hints", ): - columns += ('scheduler_hints',) - column_headers += ('Scheduler Hints',) + if sdk_utils.supports_microversion( + compute_client, '2.100' + ): + columns += ('scheduler_hints',) + column_headers += ('Scheduler Hints',) # remove duplicates column_headers = tuple(dict.fromkeys(column_headers)) diff --git a/openstackclient/identity/v3/application_credential.py b/openstackclient/identity/v3/application_credential.py index 4c4cd3f5a..93367f8a3 100644 --- a/openstackclient/identity/v3/application_credential.py +++ b/openstackclient/identity/v3/application_credential.py @@ -352,7 +352,7 @@ def take_action(self, parsed_args): user_id = conn.config.get_auth().get_user_id(conn.identity) application_credential = identity_client.find_application_credential( - user_id, parsed_args.application_credential + user_id, parsed_args.application_credential, ignore_missing=False ) return _format_application_credential(application_credential) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index fee7dbe33..05d37f134 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -298,6 +298,9 @@ def take_action(self, parsed_args): "when a user does not have a password." ) ) + else: + kwargs['password'] = password + options = _get_options_for_user(identity_client, parsed_args) if options: kwargs['options'] = options @@ -306,7 +309,6 @@ def take_action(self, parsed_args): user = identity_client.create_user( is_enabled=is_enabled, name=parsed_args.name, - password=password, **kwargs, ) except sdk_exc.ConflictException: @@ -467,15 +469,13 @@ def take_action(self, parsed_args): ignore_missing=False, ).id - assignments = identity_client.role_assignments_filter( - project=project - ) - # NOTE(stevemar): If a user has more than one role on a project # then they will have two entries in the returned data. Since we # are looking for any role, let's just track unique user IDs. user_ids = set() - for assignment in assignments: + for assignment in identity_client.role_assignments( + scope_project_id=project + ): if assignment.user: user_ids.add(assignment.user['id']) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index f35c57231..938e1a2d9 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -1393,7 +1393,10 @@ def take_action(self, parsed_args): if parsed_args.visibility is not None: kwargs['visibility'] = parsed_args.visibility - if parsed_args.project: + # Only set owner_id if --project is used WITHOUT membership flags + # When --project is used with --accept/--reject/--pending, it should + # only identify which member's status to update, not change ownership + if parsed_args.project and not parsed_args.membership: # We already did the project lookup above kwargs['owner_id'] = project_id diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 53df4da84..a2418d01b 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -1041,6 +1041,26 @@ def test_quota_show(self): ) self.assertNotCalled(self.network_client.get_quota_default) + def test_quota_show__missing_services(self): + self.app.client_manager.compute_endpoint_enabled = False + self.app.client_manager.volume_endpoint_enabled = False + self.app.client_manager.network_endpoint_enabled = False + + arglist = [ + self.projects[0].name, + ] + verifylist = [ + ('service', 'all'), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.compute_client.get_quota_set.assert_not_called() + self.volume_sdk_client.get_quota_set.assert_not_called() + self.network_client.get_quota.assert_not_called() + def test_quota_show__with_compute(self): arglist = [ '--compute', diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 86951360b..a230e7cbd 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -13,7 +13,6 @@ # under the License. import base64 -import collections import getpass import json import tempfile @@ -21,9 +20,11 @@ import uuid import iso8601 +from openstack.compute.v2 import flavor as _flavor from openstack.compute.v2 import server as _server from openstack.compute.v2 import server_group as _server_group from openstack import exceptions as sdk_exceptions +from openstack.image.v2 import image as _image from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions @@ -4586,7 +4587,6 @@ class _TestServerList(TestServer): 'Pinned Availability Zone', 'Host', 'Properties', - 'Scheduler Hints', ) columns_all_projects = ( 'ID', @@ -4653,17 +4653,19 @@ class TestServerList(_TestServerList): def setUp(self): super().setUp() - Image = collections.namedtuple('Image', 'id name') self.image_client.images.return_value = [ - Image(id=s.image['id'], name=self.image.name) + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) # Image will be an empty string if boot-from-volume for s in self.servers if s.image ] - Flavor = collections.namedtuple('Flavor', 'id name') self.compute_client.flavors.return_value = [ - Flavor(id=s.flavor['id'], name=self.flavor.name) + sdk_fakes.generate_fake_resource( + _flavor.Flavor, id=s.flavor['id'], name=self.flavor.name + ) for s in self.servers ] @@ -4735,7 +4737,6 @@ def test_server_list_long_option(self): getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), - format_columns.DictListColumn(None), ) for s in self.servers ) @@ -4814,8 +4815,6 @@ def test_server_list_column_option(self): 'Host', '-c', 'Properties', - '-c', - 'Scheduler Hints', '--long', ] verifylist = [ @@ -4838,7 +4837,6 @@ def test_server_list_column_option(self): self.assertIn('Pinned Availability Zone', columns) self.assertIn('Host', columns) self.assertIn('Properties', columns) - self.assertIn('Scheduler Hints', columns) self.assertCountEqual(columns, set(columns)) def test_server_list_no_name_lookup_option(self): @@ -5252,7 +5250,6 @@ def test_server_list_long_with_host_status_v216(self): getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), - format_columns.DictListColumn(s.scheduler_hints), ) for s in self.servers ) @@ -5281,9 +5278,10 @@ def test_server_list_long_with_host_status_v216(self): self.compute_client.servers.return_value = servers # Make sure the returned image and flavor IDs match the servers. - Image = collections.namedtuple('Image', 'id name') self.image_client.images.return_value = [ - Image(id=s.image['id'], name=self.image.name) + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) # Image will be an empty string if boot-from-volume for s in servers if s.image @@ -5308,7 +5306,6 @@ def test_server_list_long_with_host_status_v216(self): getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), - format_columns.DictListColumn(s.scheduler_hints), s.host_status, ) for s in servers @@ -5368,9 +5365,10 @@ def setUp(self): self.servers = self.setup_sdk_servers_mock(3) self.compute_client.servers.return_value = self.servers - Image = collections.namedtuple('Image', 'id name') self.image_client.images.return_value = [ - Image(id=s.image['id'], name=self.image.name) + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) # Image will be an empty string if boot-from-volume for s in self.servers if s.image @@ -5541,6 +5539,159 @@ def test_server_list_v269_with_partial_constructs(self): self.assertEqual(expected_row, partial_server) +class TestServerListV2100(_TestServerList): + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + 'Image', + 'Flavor', + ) + columns_long = ( + 'ID', + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks', + 'Image Name', + 'Image ID', + 'Flavor', + 'Availability Zone', + 'Pinned Availability Zone', + 'Host', + 'Properties', + 'Scheduler Hints', + ) + + def setUp(self): + super().setUp() + self.set_compute_api_version('2.100') + + self.image_client.images.return_value = [ + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) + # Image will be an empty string if boot-from-volume + for s in self.servers + if s.image + ] + + self.compute_client.flavors.return_value = [ + sdk_fakes.generate_fake_resource( + _flavor.Flavor, id=s.flavor['id'], name=self.flavor.name + ) + for s in self.servers + ] + + self.data = tuple( + ( + s.id, + s.name, + s.status, + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + ) + for s in self.servers + ) + + def test_server_list_long_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + getattr(s, 'task_state'), + server.PowerStateColumn(getattr(s, 'power_state')), + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + getattr(s, 'availability_zone'), + getattr(s, 'pinned_availability_zone', ''), + server.HostColumn(getattr(s, 'hypervisor_hostname')), + format_columns.DictColumn(s.metadata), + format_columns.DictListColumn(None), + ) + for s in self.servers + ) + arglist = [ + '--long', + ] + verifylist = [ + ('all_projects', False), + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.compute_client.servers.assert_called_with(**self.kwargs) + image_ids = {s.image['id'] for s in self.servers if s.image} + self.image_client.images.assert_called_once_with( + id=f'in:{",".join(image_ids)}', + ) + self.compute_client.flavors.assert_called_once_with(is_public=None) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data, tuple(data)) + + def test_server_list_column_option(self): + arglist = [ + '-c', + 'Project ID', + '-c', + 'User ID', + '-c', + 'Created At', + '-c', + 'Security Groups', + '-c', + 'Task State', + '-c', + 'Power State', + '-c', + 'Image ID', + '-c', + 'Flavor ID', + '-c', + 'Availability Zone', + '-c', + 'Pinned Availability Zone', + '-c', + 'Host', + '-c', + 'Properties', + '-c', + 'Scheduler Hints', + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute_client.servers.assert_called_with(**self.kwargs) + self.assertIn('Project ID', columns) + self.assertIn('User ID', columns) + self.assertIn('Created At', columns) + self.assertIn('Security Groups', columns) + self.assertIn('Task State', columns) + self.assertIn('Power State', columns) + self.assertIn('Image ID', columns) + self.assertIn('Flavor ID', columns) + self.assertIn('Availability Zone', columns) + self.assertIn('Pinned Availability Zone', columns) + self.assertIn('Host', columns) + self.assertIn('Properties', columns) + self.assertIn('Scheduler Hints', columns) + self.assertCountEqual(columns, set(columns)) + + class TestServerAction(compute_fakes.TestComputev2): def run_method_with_sdk_servers(self, method_name, server_count): servers = compute_fakes.create_servers(count=server_count) diff --git a/openstackclient/tests/unit/identity/v3/test_application_credential.py b/openstackclient/tests/unit/identity/v3/test_application_credential.py index a5fda7c4f..3bc2d0c29 100644 --- a/openstackclient/tests/unit/identity/v3/test_application_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_application_credential.py @@ -457,7 +457,7 @@ def test_application_credential_show(self): columns, data = self.cmd.take_action(parsed_args) self.identity_sdk_client.find_application_credential.assert_called_with( - user_id, self.application_credential.id + user_id, self.application_credential.id, ignore_missing=False ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 1236b86e2..ab904e545 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -94,7 +94,6 @@ def test_user_create_no_options(self): kwargs = { 'name': self.user.name, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -138,7 +137,6 @@ def test_user_create_password_prompt(self): self.user.name, ] verifylist = [ - ('password', None), ('password_prompt', True), ('enable', False), ('disable', False), @@ -171,7 +169,6 @@ def test_user_create_password_prompt_no_warning(self): self.user.name, ] verifylist = [ - ('password', None), ('password_prompt', True), ('enable', False), ('disable', False), @@ -236,7 +233,6 @@ def test_user_create_email(self): 'name': self.user.name, 'email': 'barney@example.com', 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -267,7 +263,6 @@ def test_user_create_project(self): 'name': self.user.name, 'default_project_id': self.project.id, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -312,7 +307,6 @@ def test_user_create_project_domain(self): 'name': self.user.name, 'default_project_id': self.project.id, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_once_with(**kwargs) self.identity_sdk_client.find_domain.assert_called_once_with( @@ -357,7 +351,6 @@ def test_user_create_domain(self): 'name': self.user.name, 'domain_id': self.domain.id, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -385,7 +378,6 @@ def test_user_create_enable(self): kwargs = { 'name': self.user.name, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -413,7 +405,6 @@ def test_user_create_disable(self): kwargs = { 'name': self.user.name, 'is_enabled': False, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -443,7 +434,6 @@ def test_user_create_ignore_lockout_failure_attempts(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_lockout_failure_attempts': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -473,7 +463,6 @@ def test_user_create_no_ignore_lockout_failure_attempts(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_lockout_failure_attempts': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -503,7 +492,6 @@ def test_user_create_ignore_password_expiry(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_password_expiry': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -533,7 +521,6 @@ def test_user_create_no_ignore_password_expiry(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_password_expiry': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -563,7 +550,6 @@ def test_user_create_ignore_change_password_upon_first_use(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_change_password_upon_first_use': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -593,7 +579,6 @@ def test_user_create_no_ignore_change_password_upon_first_use(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_change_password_upon_first_use': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -623,7 +608,6 @@ def test_user_create_enables_lock_password(self): 'name': self.user.name, 'is_enabled': True, 'options': {'lock_password': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -653,7 +637,6 @@ def test_user_create_disables_lock_password(self): 'name': self.user.name, 'is_enabled': True, 'options': {'lock_password': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -683,7 +666,6 @@ def test_user_create_enable_multi_factor_auth(self): 'name': self.user.name, 'is_enabled': True, 'options': {'multi_factor_auth_enabled': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -713,7 +695,6 @@ def test_user_create_disable_multi_factor_auth(self): 'name': self.user.name, 'is_enabled': True, 'options': {'multi_factor_auth_enabled': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -751,7 +732,6 @@ def test_user_create_option_with_multi_factor_auth_rule(self): 'options': { 'multi_factor_auth_rules': [["password", "totp"], ["password"]] }, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -790,7 +770,6 @@ def test_user_create_with_multiple_options(self): 'multi_factor_auth_enabled': False, 'multi_factor_auth_rules': [["password", "totp"]], }, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -891,7 +870,7 @@ def setUp(self): self.identity_sdk_client.find_domain.return_value = self.domain self.identity_sdk_client.find_group.return_value = self.group self.identity_sdk_client.find_project.return_value = self.project - self.identity_sdk_client.role_assignments_filter.return_value = [ + self.identity_sdk_client.role_assignments.return_value = [ self.role_assignment ] @@ -1029,12 +1008,10 @@ def test_user_list_project(self): columns, data = self.cmd.take_action(parsed_args) kwargs = { - 'project': self.project.id, + 'scope_project_id': self.project.id, } - self.identity_sdk_client.role_assignments_filter.assert_called_with( - **kwargs - ) + self.identity_sdk_client.role_assignments.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -1086,7 +1063,6 @@ def test_user_set_no_options(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', None), ('enable', False), @@ -1107,7 +1083,6 @@ def test_user_set_name(self): ] verifylist = [ ('name', 'qwerty'), - ('password', None), ('email', None), ('project', None), ('enable', False), @@ -1138,7 +1113,6 @@ def test_user_set_specify_domain(self): ] verifylist = [ ('name', 'qwerty'), - ('password', None), ('domain', self.domain.id), ('email', None), ('project', None), @@ -1194,7 +1168,6 @@ def test_user_set_password_prompt(self): ] verifylist = [ ('name', None), - ('password', None), ('password_prompt', True), ('email', None), ('project', None), @@ -1227,7 +1200,6 @@ def test_user_set_email(self): ] verifylist = [ ('name', None), - ('password', None), ('email', 'barney@example.com'), ('project', None), ('enable', False), @@ -1256,7 +1228,6 @@ def test_user_set_project(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', self.project.id), ('enable', False), @@ -1298,7 +1269,6 @@ def test_user_set_project_domain(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', self.project.id), ('project_domain', self.project.domain_id), @@ -1332,7 +1302,6 @@ def test_user_set_enable(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', None), ('enable', True), @@ -1359,7 +1328,6 @@ def test_user_set_disable(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', None), ('enable', False), @@ -1386,7 +1354,6 @@ def test_user_set_ignore_lockout_failure_attempts(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_lockout_failure_attempts', True), ('project', None), @@ -1414,7 +1381,6 @@ def test_user_set_no_ignore_lockout_failure_attempts(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('no_ignore_lockout_failure_attempts', True), ('project', None), @@ -1442,7 +1408,6 @@ def test_user_set_ignore_password_expiry(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_password_expiry', True), ('project', None), @@ -1470,7 +1435,6 @@ def test_user_set_no_ignore_password_expiry(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('no_ignore_password_expiry', True), ('project', None), @@ -1498,7 +1462,6 @@ def test_user_set_ignore_change_password_upon_first_use(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_change_password_upon_first_use', True), ('project', None), @@ -1526,7 +1489,6 @@ def test_user_set_no_ignore_change_password_upon_first_use(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('no_ignore_change_password_upon_first_use', True), ('project', None), @@ -1554,7 +1516,6 @@ def test_user_set_enable_lock_password(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('enable_lock_password', True), ('project', None), @@ -1582,7 +1543,6 @@ def test_user_set_disable_lock_password(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('disable_lock_password', True), ('project', None), @@ -1610,7 +1570,6 @@ def test_user_set_enable_multi_factor_auth(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('enable_multi_factor_auth', True), ('project', None), @@ -1638,7 +1597,6 @@ def test_user_set_disable_multi_factor_auth(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('disable_multi_factor_auth', True), ('project', None), @@ -1667,7 +1625,6 @@ def test_user_set_option_multi_factor_auth_rule(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), ('project', None), @@ -1699,7 +1656,6 @@ def test_user_set_with_multiple_options(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_password_expiry', True), ('enable_multi_factor_auth', True), diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 7eb7331f3..e6de9f2eb 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -1295,6 +1295,117 @@ def test_image_set_membership_option_pending(self): # the 'update membership' route. self.image_client.update_image.assert_called_with(self._image.id) + def test_image_set_membership_accept_with_project_no_owner_change(self): + """Test that --project with --accept doesn't change image owner.""" + membership = image_fakes.create_one_image_member( + attrs={ + 'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', + 'member_id': self.project.id, + } + ) + self.image_client.update_member.return_value = membership + + arglist = [ + '--project', + self.project.name, + '--accept', + self._image.id, + ] + verifylist = [ + ('project', self.project.name), + ('membership', 'accepted'), + ('image', self._image.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_client.update_member.assert_called_once_with( + image=self._image.id, + member=self.project.id, + status='accepted', + ) + + self.image_client.update_image.assert_called() + call_args = self.image_client.update_image.call_args + if call_args: + args, kwargs = call_args + self.assertNotIn('owner_id', kwargs) + + def test_image_set_membership_reject_with_project_no_owner_change(self): + """Test that --project with --reject doesn't change image owner.""" + membership = image_fakes.create_one_image_member( + attrs={ + 'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', + 'member_id': self.project.id, + } + ) + self.image_client.update_member.return_value = membership + + arglist = [ + '--project', + self.project.name, + '--reject', + self._image.id, + ] + verifylist = [ + ('project', self.project.name), + ('membership', 'rejected'), + ('image', self._image.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_client.update_member.assert_called_once_with( + image=self._image.id, + member=self.project.id, + status='rejected', + ) + + self.image_client.update_image.assert_called() + call_args = self.image_client.update_image.call_args + if call_args: + args, kwargs = call_args + self.assertNotIn('owner_id', kwargs) + + def test_image_set_membership_pending_with_project_no_owner_change(self): + """Test that --project with --pending doesn't change image owner.""" + membership = image_fakes.create_one_image_member( + attrs={ + 'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', + 'member_id': self.project.id, + } + ) + self.image_client.update_member.return_value = membership + + arglist = [ + '--project', + self.project.name, + '--pending', + self._image.id, + ] + verifylist = [ + ('project', self.project.name), + ('membership', 'pending'), + ('image', self._image.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_client.update_member.assert_called_once_with( + image=self._image.id, + member=self.project.id, + status='pending', + ) + + self.image_client.update_image.assert_called() + call_args = self.image_client.update_image.call_args + if call_args: + args, kwargs = call_args + self.assertNotIn('owner_id', kwargs) + def test_image_set_options(self): arglist = [ '--name', diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 5fb35a067..33dcfe5a4 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -37,6 +37,7 @@ class TestVolumeCreate(volume_fakes.TestVolume): columns = ( 'attachments', 'availability_zone', + 'backup_id', 'bootable', 'cluster_name', 'consistencygroup_id', @@ -78,6 +79,7 @@ def setUp(self): self.datalist = ( self.volume.attachments, self.volume.availability_zone, + self.volume.backup_id, self.volume.is_bootable, self.volume.cluster_name, self.volume.consistency_group_id, @@ -2011,6 +2013,7 @@ def setUp(self): self.columns = ( 'attachments', 'availability_zone', + 'backup_id', 'bootable', 'cluster_name', 'consistencygroup_id', @@ -2045,6 +2048,7 @@ def setUp(self): self.data = ( self.volume.attachments, self.volume.availability_zone, + self.volume.backup_id, self.volume.is_bootable, self.volume.cluster_name, self.volume.consistency_group_id, diff --git a/releasenotes/notes/bug-2126565-a119ac242d9ac795.yaml b/releasenotes/notes/bug-2126565-a119ac242d9ac795.yaml new file mode 100644 index 000000000..87fe689b1 --- /dev/null +++ b/releasenotes/notes/bug-2126565-a119ac242d9ac795.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Running ``openstack application credential show`` on + a non-existent application credential does not + raise an exception. + + [Bug `2126565 `_] diff --git a/releasenotes/notes/fix-image-set-project-accept-owner-bug-2136795.yaml b/releasenotes/notes/fix-image-set-project-accept-owner-bug-2136795.yaml new file mode 100644 index 000000000..4cd755b3f --- /dev/null +++ b/releasenotes/notes/fix-image-set-project-accept-owner-bug-2136795.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fix a bug where using ``openstack image set --project + --accept `` incorrectly changed the image owner to the specified + project instead of only updating the member status. The ``--project`` + parameter when used with ``--accept``, ``--reject``, or ``--pending`` + should only identify which member's status to update, not change the + image ownership. + [Bug `2136795 `_] diff --git a/releasenotes/notes/keystone-create-user-no-password-619bcddcd046dda8.yaml b/releasenotes/notes/keystone-create-user-no-password-619bcddcd046dda8.yaml new file mode 100644 index 000000000..7cd6acba0 --- /dev/null +++ b/releasenotes/notes/keystone-create-user-no-password-619bcddcd046dda8.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + [Bug `2136148 `_] Keystone allows + users to be created with no password but no value should be submitted for + the password instead of a ``null`` value. diff --git a/tox.ini b/tox.ini index 7b3d951d2..24c21dd21 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ setenv = OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = @@ -63,7 +63,7 @@ commands = description = Run specified command in a virtual environment with all dependencies installed. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = @@ -93,7 +93,7 @@ commands = description = Build documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html @@ -105,7 +105,7 @@ commands = description = Build release note documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html