Fix 'openstack keypair list --project <project>'

The --project option of 'openstack keypair list' is supposed to filter
keypairs by a project but has not been working and instead returns
keypairs from all projects.

The reason appears to be because it uses a request for a user list
filtered by project but tenant_id/project_id is not a valid filter for
GET /users.

This fixes the issue by requesting role assignments for the specified
project and then requesting keypairs for users with a role in the
project.

This change depends on a recent openstacksdk bug fix change
Ic552dee83d56278d2b866de0cb365a0c394fe26a which fixed the user_id query
parameter for the compute /os-keypairs APIs. The bug fix was released in
openstacksdk 4.4.0.

Closes-Bug: #2096947

Change-Id: Ibb5757766e3040e58d64388b95678fab9b2b6f23
This commit is contained in:
melanie witt 2025-02-01 00:44:03 +00:00
parent 616d6f3a29
commit d123be0819
5 changed files with 66 additions and 16 deletions

View File

@ -300,6 +300,7 @@ class ListKeypair(command.Lister):
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity
identity_sdk_client = self.app.client_manager.sdk_connection.identity
kwargs = {}
@ -345,11 +346,17 @@ class ListKeypair(command.Lister):
parsed_args.project,
parsed_args.project_domain,
).id
users = identity_client.users.list(tenant_id=project)
assignments = identity_sdk_client.role_assignments(
scope_project_id=project
)
user_ids = set()
for assignment in assignments:
if assignment.user:
user_ids.add(assignment.user['id'])
data = []
for user in users:
kwargs['user_id'] = user.id
for user_id in user_ids:
kwargs['user_id'] = user_id
data.extend(compute_client.keypairs(**kwargs))
elif parsed_args.user:
if not sdk_utils.supports_microversion(compute_client, '2.10'):

View File

@ -21,12 +21,18 @@ from openstackclient.tests.functional import base
class KeypairBase(base.TestCase):
"""Methods for functional tests."""
def keypair_create(self, name=data_utils.rand_uuid()):
def keypair_create(self, name=data_utils.rand_uuid(), user=None):
"""Create keypair and add cleanup."""
raw_output = self.openstack('keypair create ' + name)
self.addCleanup(self.keypair_delete, name, True)
cmd = 'keypair create ' + name
if user is not None:
cmd += ' --user ' + user
raw_output = self.openstack(cmd)
self.addCleanup(
self.keypair_delete, name, ignore_exceptions=True, user=user
)
if not raw_output:
self.fail('Keypair has not been created!')
return name
def keypair_list(self, params=''):
"""Return dictionary with list of keypairs."""
@ -34,10 +40,13 @@ class KeypairBase(base.TestCase):
keypairs = self.parse_show_as_object(raw_output)
return keypairs
def keypair_delete(self, name, ignore_exceptions=False):
def keypair_delete(self, name, ignore_exceptions=False, user=None):
"""Try to delete keypair by name."""
try:
self.openstack('keypair delete ' + name)
cmd = 'keypair delete ' + name
if user is not None:
cmd += ' --user ' + user
self.openstack(cmd)
except exceptions.CommandFailed:
if not ignore_exceptions:
raise
@ -200,3 +209,30 @@ class KeypairTests(KeypairBase):
items = self.parse_listing(raw_output)
self.assert_table_structure(items, HEADERS)
self.assertInOutput(self.KPName, raw_output)
def test_keypair_list_by_project(self):
"""Test keypair list by project.
Test steps:
1) Create keypair for admin project in setUp
2) Create a new project
3) Create a new user
4) Associate the new user with the new project
5) Create keypair for the new user
6) List keypairs by the new project
7) Check that only the keypair from step 5 is returned
"""
project_name = data_utils.rand_name('TestProject')
self.openstack(f'project create {project_name}')
self.addCleanup(self.openstack, f'project delete {project_name}')
user_name = data_utils.rand_name('TestUser')
self.openstack(f'user create {user_name}')
self.addCleanup(self.openstack, f'user delete {user_name}')
self.openstack(
f'role add --user {user_name} --project {project_name} member'
)
keypair_name = self.keypair_create(user=user_name)
raw_output = self.openstack(f'keypair list --project {project_name}')
items = self.parse_listing(raw_output)
self.assertEqual(1, len(items))
self.assertEqual(keypair_name, items[0]['Name'])

View File

@ -33,7 +33,7 @@ from openstack.compute.v2 import server_migration as _server_migration
from openstack.compute.v2 import volume_attachment as _volume_attachment
from openstackclient.tests.unit import fakes
from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
from openstackclient.tests.unit.image.v2 import fakes as image_fakes
from openstackclient.tests.unit.network.v2 import fakes as network_fakes
from openstackclient.tests.unit import utils
@ -121,10 +121,10 @@ class FakeClientMixin:
class TestComputev2(
identity_fakes.FakeClientMixin,
network_fakes.FakeClientMixin,
image_fakes.FakeClientMixin,
volume_fakes.FakeClientMixin,
identity_fakes.FakeClientMixin,
FakeClientMixin,
utils.TestCommand,
): ...

View File

@ -18,6 +18,7 @@ import uuid
from openstack.compute.v2 import keypair as _keypair
from openstack.identity.v3 import project as _project
from openstack.identity.v3 import role_assignment as _role_assignment
from openstack.identity.v3 import user as _user
from openstack.test import fakes as sdk_fakes
from osc_lib import exceptions
@ -529,13 +530,17 @@ class TestKeypairList(TestKeypair):
def test_keypair_list_with_project(self):
self.set_compute_api_version('2.35')
projects_mock = self.identity_client.tenants
projects_mock = self.identity_client.projects
projects_mock.reset_mock()
projects_mock.get.return_value = self._project
users_mock = self.identity_client.users
users_mock.reset_mock()
users_mock.list.return_value = [self._user]
role_assignments_mock = self.identity_sdk_client.role_assignments
role_assignments_mock.reset_mock()
assignment = sdk_fakes.generate_fake_resource(
_role_assignment.RoleAssignment
)
assignment.user = self._user
role_assignments_mock.return_value = [assignment]
arglist = ['--project', self._project.name]
verifylist = [('project', self._project.name)]
@ -544,7 +549,9 @@ class TestKeypairList(TestKeypair):
columns, data = self.cmd.take_action(parsed_args)
projects_mock.get.assert_called_with(self._project.name)
users_mock.list.assert_called_with(tenant_id=self._project.id)
role_assignments_mock.assert_called_with(
scope_project_id=self._project.id
)
self.compute_client.keypairs.assert_called_with(
user_id=self._user.id,
)

View File

@ -7,7 +7,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
cryptography>=2.7 # BSD/Apache-2.0
cliff>=3.5.0 # Apache-2.0
iso8601>=0.1.11 # MIT
openstacksdk>=3.3.0 # Apache-2.0
openstacksdk>=4.4.0 # Apache-2.0
osc-lib>=2.3.0 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
python-keystoneclient>=3.22.0 # Apache-2.0