From 782ab2b1349c39dbdc75e604e9b47e9c083b3364 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 18 Mar 2025 09:01:51 +0000 Subject: [PATCH 01/10] Update .gitreview for stable/2025.1 Change-Id: I98ee6b449f487bda11c4cbf5560693902040177f --- .gitreview | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitreview b/.gitreview index 4eee726db6..a5f028781b 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/python-openstackclient.git +defaultbranch=stable/2025.1 From c5dc92b17bb0cc0ea50d811b2ffcde5d2034a3b5 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Tue, 18 Mar 2025 09:01:52 +0000 Subject: [PATCH 02/10] Update TOX_CONSTRAINTS_FILE for stable/2025.1 Update the URL to the upper-constraints file to point to the redirect rule on releases.openstack.org so that anyone working on this branch will switch to the correct upper-constraints list automatically when the requirements repository branches. Until the requirements repository has as stable/2025.1 branch, tests will continue to use the upper-constraints list on master. Change-Id: Iafeb7966d23d4672d45f700d13dc8e117fc0852a --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 7b3d951d20..380f64fdc4 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.1} -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.1} -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.1} -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.1} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html From 9e623da845670006d3000a9ad735d2998e565b1e Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Mon, 17 Mar 2025 09:23:09 -0700 Subject: [PATCH 03/10] Add libpcre3-dev in bindep.txt for pcre.h Doc job is going to run on Ubuntu Noble[1] and we need libpcre3-dev package for pcre.h [1] https://review.opendev.org/c/openstack/openstack-zuul-jobs/+/935459 Change-Id: I0fe73c02b093805d8eb1b15303f92633fad809cb (cherry picked from commit 702a37c7ca74e88c80a6450d97eac9584ff4fd3c) --- bindep.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/bindep.txt b/bindep.txt index 4c90a026fe..83ebf2d43f 100644 --- a/bindep.txt +++ b/bindep.txt @@ -8,3 +8,4 @@ libffi-dev [compile test platform:dpkg] libssl-dev [compile test platform:dpkg] python3-dev [compile test platform:dpkg] python3-devel [compile test platform:rpm] +libpcre3-dev [test platform:dpkg] From d455044154546695d7c52e0655bdf657ea4ef5a3 Mon Sep 17 00:00:00 2001 From: Vladimir Kozhukalov Date: Mon, 10 Mar 2025 11:33:52 -0500 Subject: [PATCH 04/10] identity: Fix 'trust' commands to work with SDK Closes-Bug: #2102039 Change-Id: I632937e06683cc76e78390a4e6f3de4e3c4f1f87 (cherry picked from commit 1458330d3b9cae0961c8cbce95a365f62fda759f) --- openstackclient/identity/v3/trust.py | 97 ++++++++++++++----- .../tests/unit/identity/v3/test_trust.py | 7 +- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index 447a57f2b8..41d01fa7b8 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -123,37 +123,67 @@ def take_action(self, parsed_args): # pointless, and trusts are immutable, so let's enforce it at the # client level. try: - trustor_id = identity_client.find_user( - parsed_args.trustor, parsed_args.trustor_domain - ).id - kwargs['trustor_id'] = trustor_id + if parsed_args.trustor_domain: + trustor_domain_id = identity_client.find_domain( + parsed_args.trustor_domain, ignore_missing=False + ).id + trustor_id = identity_client.find_user( + parsed_args.trustor, + ignore_missing=False, + domain_id=trustor_domain_id, + ).id + else: + trustor_id = identity_client.find_user( + parsed_args.trustor, ignore_missing=False + ).id + kwargs['trustor_user_id'] = trustor_id except sdk_exceptions.ForbiddenException: - kwargs['trustor_id'] = parsed_args.trustor + kwargs['trustor_user_id'] = parsed_args.trustor try: - trustee_id = identity_client.find_user( - parsed_args.trustee, parsed_args.trustee_domain - ).id - kwargs['trustee_id'] = trustee_id + if parsed_args.trustee_domain: + trustee_domain_id = identity_client.find_domain( + parsed_args.trustee_domain, ignore_missing=False + ).id + trustee_id = identity_client.find_user( + parsed_args.trustee, + ignore_missing=False, + domain_id=trustee_domain_id, + ).id + else: + trustee_id = identity_client.find_user( + parsed_args.trustee, ignore_missing=False + ).id + kwargs['trustee_user_id'] = trustee_id except sdk_exceptions.ForbiddenException: - kwargs['trustee_id'] = parsed_args.trustee + kwargs['trustee_user_id'] = parsed_args.trustee try: - project_id = identity_client.find_project( - parsed_args.project, parsed_args.project_domain - ).id + if parsed_args.project_domain: + project_domain_id = identity_client.find_domain( + parsed_args.project_domain, ignore_missing=False + ).id + project_id = identity_client.find_project( + parsed_args.project, + ignore_missing=False, + domain_id=project_domain_id, + ).id + else: + project_id = identity_client.find_project( + parsed_args.project, ignore_missing=False + ).id kwargs['project_id'] = project_id except sdk_exceptions.ForbiddenException: kwargs['project_id'] = parsed_args.project - role_ids = [] + roles = [] for role in parsed_args.roles: try: role_id = identity_client.find_role(role).id except sdk_exceptions.ForbiddenException: role_id = role - role_ids.append(role_id) - kwargs['roles'] = role_ids + roles.append({"id": role_id}) + kwargs['roles'] = roles if parsed_args.expiration: expires_at = datetime.datetime.strptime( @@ -161,8 +191,7 @@ def take_action(self, parsed_args): ) kwargs['expires_at'] = expires_at - if parsed_args.is_impersonation: - kwargs['is_impersonation'] = parsed_args.is_impersonation + kwargs['impersonation'] = bool(parsed_args.is_impersonation) trust = identity_client.create_trust(**kwargs) @@ -289,9 +318,19 @@ def take_action(self, parsed_args): trustor = None if parsed_args.trustor: try: - trustor_id = identity_client.find_user( - parsed_args.trustor, parsed_args.trustor_domain - ).id + if parsed_args.trustor_domain: + trustor_domain_id = identity_client.find_domain( + parsed_args.trustor_domain, ignore_missing=False + ).id + trustor_id = identity_client.find_user( + parsed_args.trustor, + ignore_missing=False, + domain_id=trustor_domain_id, + ).id + else: + trustor_id = identity_client.find_user( + parsed_args.trustor, ignore_missing=False + ).id trustor = trustor_id except sdk_exceptions.ForbiddenException: trustor = parsed_args.trustor @@ -299,9 +338,19 @@ def take_action(self, parsed_args): trustee = None if parsed_args.trustee: try: - trustee_id = identity_client.find_user( - parsed_args.trustee, parsed_args.trustee_domain - ).id + if parsed_args.trustee_domain: + trustee_domain_id = identity_client.find_domain( + parsed_args.trustee_domain, ignore_missing=False + ).id + trustee_id = identity_client.find_user( + parsed_args.trustee, + ignore_missing=False, + domain_id=trustee_domain_id, + ).id + else: + trustee_id = identity_client.find_user( + parsed_args.trustee, ignore_missing=False + ).id trustee = trustee_id except sdk_exceptions.ForbiddenException: trustee = parsed_args.trustee diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index 07776fa45b..5c14b7ad98 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -70,12 +70,15 @@ def test_trust_create_basic(self): # Set expected values kwargs = { 'project_id': self.project.id, - 'roles': [self.role.id], + 'roles': [{'id': self.role.id}], + 'impersonation': False, } # TrustManager.create(trustee_id, trustor_id, impersonation=, # project=, role_names=, expires_at=) self.identity_sdk_client.create_trust.assert_called_with( - trustor_id=self.user.id, trustee_id=self.user.id, **kwargs + trustor_user_id=self.user.id, + trustee_user_id=self.user.id, + **kwargs, ) collist = ( From 94fe341fa5b6eefdb60cdab56a14cb9cbd751d46 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Mar 2025 15:03:41 +0000 Subject: [PATCH 05/10] zuul: Make image job non-voting We may need to remove this soon enough, given the new Docker rate limits that we keep bumping into. Change-Id: Id4a9d8df770d107986b20e4a98835ee4e0b6117d Signed-off-by: Stephen Finucane (cherry picked from commit 7ef588d6952cf4c90f39759e88fdba38007e5975) --- .zuul.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.zuul.yaml b/.zuul.yaml index e688bd26a2..2654db30ac 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -217,7 +217,8 @@ - release-notes-jobs-python3 check: jobs: - - osc-build-image + - osc-build-image: + voting: false - osc-functional-devstack - osc-functional-devstack-tips: # The functional-tips job only tests the latest and shouldn't be run From 80a4b582813fdb6ef786c63a091fa0bd67b239b2 Mon Sep 17 00:00:00 2001 From: Dmitriy Chubinidze Date: Fri, 21 Mar 2025 08:56:32 +0000 Subject: [PATCH 06/10] Specifying project-domain for project The fix ensures that if a user wants to set a default project, they must also provide the project domain. If it's missing, an explicit error message is shown, making it clear that the project domain is required. Also adding some unit tests by modifying respective calls. Change-Id: Ia6e921a53da55ab1bce85a42c8160872a9d47d64 Closes-Bug: #2102146 (cherry picked from commit 2883f3fb957ca79b984f8b9e312ee500f2194aab) --- openstackclient/identity/v3/user.py | 10 ++++++---- .../tests/unit/identity/v3/test_user.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 545b1cda77..4c2aa1db09 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -612,10 +612,12 @@ def take_action(self, parsed_args): if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.project: - project_domain_id = identity_client.find_domain( - name_or_id=parsed_args.project_domain, - ignore_missing=False, - ).id + project_domain_id = None + if parsed_args.project_domain: + project_domain_id = identity_client.find_domain( + name_or_id=parsed_args.project_domain, + ignore_missing=False, + ).id project_id = identity_client.find_project( name_or_id=parsed_args.project, ignore_missing=False, diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 7f4c2497ec..91e8b45c3e 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -1206,6 +1206,17 @@ def test_user_set_project(self): self.identity_sdk_client.update_user.assert_called_with( user=self.user, **kwargs ) + self.identity_sdk_client.find_domain.assert_not_called() + + # Set expected values + kwargs = { + 'ignore_missing': False, + 'domain_id': None, + } + self.identity_sdk_client.find_project.assert_called_once_with( + name_or_id=self.project.id, **kwargs + ) + self.assertIsNone(result) def test_user_set_project_domain(self): @@ -1238,6 +1249,11 @@ def test_user_set_project_domain(self): self.identity_sdk_client.update_user.assert_called_with( user=self.user, **kwargs ) + + self.identity_sdk_client.find_domain.assert_called_once_with( + name_or_id=self.project.domain_id, ignore_missing=False + ) + self.assertIsNone(result) def test_user_set_enable(self): From 798832536cb9fd974b2e10950dba0e020a3c45cd Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 29 Sep 2025 14:59:03 +0200 Subject: [PATCH 07/10] Fix openstack quota show without cinder Per this Debian bug [1], 'openstack quota show --default' fails when cinder is NOT installed. This is also true of other services. Conflicts: openstackclient/common/quota.py NOTE(stephenfin): Conflicts are due to the absence of change I43d9ede39d36cc29301f94fa462b9b9d9441807c which repurposed the compute client attribute for the SDK client. We also need to update the new test to reflect this old naming scheme. [1] https://bugs.debian.org/1109288 Change-Id: I361da44b9f1d09ba3a454632d41e2110a3815395 Signed-off-by: Svein-Erik Skjelbred Signed-off-by: Thomas Goirand Signed-off-by: Stephen Finucane (cherry picked from commit de88853de29d30ef6d1cc1966c93befd3e100cf3) (cherry picked from commit 63f78be1094a6423258c16cf17e004058228c7ea) --- openstackclient/common/quota.py | 27 +++++++++++++++---- .../tests/unit/common/test_quota.py | 20 ++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 638d2e1cea..329dc5f964 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -733,21 +733,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, @@ -880,12 +891,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.sdk_connection.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/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index e18161d4e4..d05aa7697a 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -1018,6 +1018,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_sdk_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', From bb9c7ad36bffbb923d46039f19226930cd43dd48 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 16 Apr 2025 10:59:36 +0100 Subject: [PATCH 08/10] zuul: Remove osc-upload-image, osc-promote-image jobs We are no longer going to publish these images to Dockerhub, given the recent changes to quotas there coupled with the fact that no one appears to be using them [1]. The osc-build-image job is retained to ensure our Dockerfile keeps working. [1] https://lists.openstack.org/archives/list/openstack-discuss@lists.openstack.org/thread/BE7PPQL4DGNDZ2SIMUVSK67I5NF3TFCX/ Change-Id: I9d2ca8f90b8244a09832da673491312095520968 Signed-off-by: Stephen Finucane (cherry picked from commit e4d621d24fec9629e4764494200d37e552959be3) --- .zuul.yaml | 52 ++-------------------------------------------------- 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 2654db30ac..a320f6d029 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -137,22 +137,6 @@ tox_envlist: functional tox_install_siblings: true -- secret: - name: osc-dockerhub - data: - username: osclientzuul - password: !encrypted/pkcs1-oaep - - LbIZjJiVstRVXMpoLQ3+/JcNB6lKVUWJXXo5+Outf+PKAaO7mNnv8XLiFMKnJ6ftopLyu - hWbX9rA+NddvplLQkf1xxkh7QBBU8PToLr58quI2SENUclt4tpjxbZfZu451kFSNJvNvR - E58cHHpfJZpyRnS2htXmN/Qy24gbV2w7CQxSZD2YhlcrerD8uQ8rWEnlY1wcJEaEGomtS - ZTGxsdK2TsZC2cd4b7TG7+xbl2i+hjADzwSQAgUzlLlwuG71667+IWk4SOZ7OycJTv9NN - ZTak8+CGfiMKdmsxZ1Z8uD7DC+RIklDjMWyly6zuhWzfhOmsmU0CesR50moodRUvbK79p - NZM8u0hBex5cl2EpUEwJL/FSPJXUhDMPoMoTZT/SAuXf25R9eZ9JGrKsIAlmVhpl8ifoE - 8TpPyvIHGS3YelTQjhqOX0wGb9T4ZauQCcI5Ajzy9NuCTyD9xxme9OX1zz7gMACRnVHvz - q7U7Ue90MnmGH6E2SgKjIZhyzy9Efwb7JUvH1Zb3hlrjCjEhwi9MV5FnABTEeXyYwE10s - 3o/KZg2zvdWkVG6x0dEkjpoQaNuaB7T2Na7Sm421n/z3LCzhiQGuTUjENnL6cMEtuA6Pp - BfI5+Qlg7HMwkBXNB73EPfWHzbCR3VNrzGYTy9FvhGud0/cXsuBXgps4WH63ic= - - job: name: osc-build-image parent: opendev-build-docker-image @@ -162,38 +146,10 @@ - python-builder-3.11-bookworm-container-image - python-base-3.11-bookworm-container-image provides: osc-container-image - vars: &osc_image_vars + vars: docker_images: - context: . - repository: osclient/python-openstackclient - -- job: - name: osc-upload-image - parent: opendev-upload-docker-image - description: Build Docker images and upload to Docker Hub. - allowed-projects: openstack/python-openstackclient - requires: - - python-builder-3.11-bookworm-container-image - - python-base-3.11-bookworm-container-image - provides: osc-container-image - secrets: - - name: docker_credentials - secret: osc-dockerhub - pass-to-parent: true - vars: *osc_image_vars - -- job: - name: osc-promote-image - parent: opendev-promote-docker-image - allowed-projects: openstack/python-openstackclient - description: Promote previously uploaded Docker images. - secrets: - - name: docker_credentials - secret: osc-dockerhub - pass-to-parent: true - nodeset: - nodes: [] - vars: *osc_image_vars + tags: [] - project-template: name: osc-tox-unit-tips @@ -226,8 +182,4 @@ branches: ^master$ gate: jobs: - - osc-upload-image - osc-functional-devstack - promote: - jobs: - - osc-promote-image From da911738818ee11902e30f14eb0c059c3669d24c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 1 Oct 2025 15:26:25 +0100 Subject: [PATCH 09/10] identity: Fix 'user list --project' option The 'role_assignments_filter' identity proxy method requires either a user or group, which defeats the entire purpose of the command when used with this option. Use 'role_assignments' instead. Change-Id: I8fb705c55fb4e81fa82d4a7dbe4c5bf7e1edd98a Signed-off-by: Stephen Finucane Closes-bug: #1616104 (cherry picked from commit 0ed122094ae480a4b3a02948e374aefe0eb3390a) (cherry picked from commit 33673279484abd6e886b388ff8f566fe8624c5eb) --- openstackclient/identity/v3/user.py | 8 +++----- openstackclient/tests/unit/identity/v3/test_user.py | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 4c2aa1db09..1a15bfa483 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -443,15 +443,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/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 91e8b45c3e..d0e596a33b 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -840,7 +840,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 ] @@ -978,12 +978,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)) From 2d5f60b878f7e500fbfec8fbdd35540a36a07c40 Mon Sep 17 00:00:00 2001 From: Rajesh Tailor Date: Mon, 23 Jun 2025 13:17:30 +0530 Subject: [PATCH 10/10] Fix microversion 2.96 This change fixes missing conditional logic for microversion 2.96 which adds `pinned_availability_zone` field to `openstack server list` output. Conflicts: Resolved the conflict because of missing change Ia5a4e0047b5123f2fb063cfc9ab1f58b2844308f Change-Id: I1e398bb3379fa6443b0a44db76baaf6241a945e7 Signed-off-by: Rajesh Tailor (cherry picked from commit dbddbf976008cd8aba31a787cc1f043952e2ef94) (cherry picked from commit 9b137bfcea42ad3714f9af7d13afd435d68c7419) --- openstackclient/compute/v2/server.py | 23 ++- .../tests/unit/compute/v2/test_server.py | 160 ++++++++++++++++-- 2 files changed, 163 insertions(+), 20 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index dcadca1b65..a26628d3a3 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -183,8 +183,12 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): 'updated_at': 'updated', 'user_data': 'OS-EXT-SRV-ATTR:user_data', 'vm_state': 'OS-EXT-STS:vm_state', - 'pinned_availability_zone': 'pinned_availability_zone', } + # NOTE(ratailor): microversion 2.96 introduces + # pinned_availability_zone support + if sdk_utils.supports_microversion(compute_client, '2.96'): + column_map['pinned_availability_zone'] = 'pinned_availability_zone' + # Some columns returned by openstacksdk should not be shown because they're # either irrelevant or duplicates ignored_columns = { @@ -235,6 +239,11 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): info = data + # NOTE(ratailor): microversion 2.96 introduces + # pinned_availability_zone support + if not sdk_utils.supports_microversion(compute_client, '2.96'): + info.pop('pinned_availability_zone', None) + # Convert the image blob to a name image_info = info.get('image', {}) if image_info and any(image_info.values()): @@ -2870,16 +2879,17 @@ def take_action(self, parsed_args): if parsed_args.long: columns += ( 'availability_zone', - 'pinned_availability_zone', 'hypervisor_hostname', 'metadata', ) column_headers += ( 'Availability Zone', - 'Pinned Availability Zone', 'Host', 'Properties', ) + if sdk_utils.supports_microversion(compute_client, '2.96'): + columns += ('pinned_availability_zone',) + column_headers += ('Pinned Availability Zone',) # support for additional columns if parsed_args.columns: @@ -2913,10 +2923,11 @@ def take_action(self, parsed_args): column_headers += ('Availability Zone',) if c in ( 'pinned_availability_zone', - "Pinned Availability Zone", + 'Pinned Availability Zone', ): - columns += ('Pinned Availability Zone',) - column_headers += ('Pinned Availability Zone',) + if sdk_utils.supports_microversion(compute_client, '2.96'): + columns += ('pinned_availability_zone',) + column_headers += ('Pinned Availability Zone',) if c in ('Host', "host"): columns += ('hypervisor_hostname',) column_headers += ('Host',) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 789daadf47..d149ec4f40 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1227,7 +1227,6 @@ class TestServerCreate(TestServer): 'locked', 'locked_reason', 'name', - 'pinned_availability_zone', 'progress', 'project_id', 'properties', @@ -1276,7 +1275,6 @@ def datalist(self): None, # locked None, # locked_reason self.server.name, - None, # pinned_availability_zone None, # progress None, # project_id format_columns.DictColumn({}), # properties @@ -4600,7 +4598,6 @@ class _TestServerList(TestServer): 'Flavor Name', 'Flavor ID', 'Availability Zone', - 'Pinned Availability Zone', 'Host', 'Properties', ) @@ -4739,7 +4736,6 @@ def test_server_list_long_option(self): self.flavor.name, s.flavor['id'], getattr(s, 'availability_zone'), - getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), ) @@ -4785,8 +4781,6 @@ def test_server_list_column_option(self): '-c', 'Availability Zone', '-c', - 'Pinned Availability Zone', - '-c', 'Host', '-c', 'Properties', @@ -4809,7 +4803,6 @@ def test_server_list_column_option(self): 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.assertCountEqual(columns, set(columns)) @@ -5222,7 +5215,6 @@ def test_server_list_long_with_host_status_v216(self): self.flavor.name, s.flavor['id'], getattr(s, 'availability_zone'), - getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), ) @@ -5277,7 +5269,6 @@ def test_server_list_long_with_host_status_v216(self): self.flavor.name, s.flavor['id'], getattr(s, 'availability_zone'), - getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), s.host_status, @@ -5314,7 +5305,6 @@ class TestServerListV273(_TestServerList): 'Image ID', 'Flavor', 'Availability Zone', - 'Pinned Availability Zone', 'Host', 'Properties', ) @@ -5513,6 +5503,152 @@ def test_server_list_v269_with_partial_constructs(self): self.assertEqual(expected_row, partial_server) +class TestServerListV296(_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', + 'Host', + 'Properties', + 'Pinned Availability Zone', + ) + + def setUp(self): + super().setUp() + self.set_compute_api_version('2.96') + + Image = collections.namedtuple('Image', 'id name') + self.image_client.images.return_value = [ + Image(id=s.image['id'], name=self.image.name) + # Image will be an empty string if boot-from-volume + for s in self.servers + if s.image + ] + + Flavor = collections.namedtuple('Flavor', 'id name') + self.compute_sdk_client.flavors.return_value = [ + 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'), + server.HostColumn(getattr(s, 'hypervisor_hostname')), + format_columns.DictColumn(s.metadata), + getattr(s, 'pinned_availability_zone', ''), + ) + 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_sdk_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_sdk_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', + 'Host', + '-c', + 'Properties', + '-c', + 'Pinned Availability Zone', + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute_sdk_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.assertCountEqual(columns, set(columns)) + + class TestServerAction(compute_fakes.TestComputev2): def run_method_with_sdk_servers(self, method_name, server_count): servers = compute_fakes.create_sdk_servers(count=server_count) @@ -8443,7 +8579,6 @@ def setUp(self): 'locked', 'locked_reason', 'name', - 'pinned_availability_zone', 'progress', 'project_id', 'properties', @@ -8493,7 +8628,6 @@ def setUp(self): None, # locked None, # locked_reason self.server.name, - None, # pinned_availability_zone None, # progress 'tenant-id-xxx', # project_id format_columns.DictColumn({}), # properties @@ -9444,7 +9578,6 @@ def test_prep_server_detail(self): 'locked': None, 'locked_reason': None, 'name': _server.name, - 'pinned_availability_zone': None, 'progress': None, 'project_id': 'tenant-id-xxx', 'properties': format_columns.DictColumn({}), @@ -9531,7 +9664,6 @@ def test_prep_server_detail_v247(self): 'locked': None, 'locked_reason': None, 'name': _server.name, - 'pinned_availability_zone': None, 'progress': None, 'project_id': 'tenant-id-xxx', 'properties': format_columns.DictColumn({}),