From a39f11cecee1e0ae7d2ff452da4459cd0f4238d4 Mon Sep 17 00:00:00 2001 From: "Satoshi S." Date: Mon, 14 Apr 2025 12:45:02 +0000 Subject: [PATCH] Add shared image support - If image availability is shared and conductor_project_id is in the image shared member list, allows access Closes-Bug: #2099276 Change-Id: I6b8a10fe82b41aa37b4f14bca9d3c0c498882bd1 --- .../install/configure-glance-images.rst | 7 ++++++ ironic/common/glance_service/service_utils.py | 24 ++++++++++++++++++- .../tests/unit/common/test_glance_service.py | 18 +++++++++++++- ...upport-shared-images-8279f7ecd66b7218.yaml | 8 +++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/support-shared-images-8279f7ecd66b7218.yaml diff --git a/doc/source/install/configure-glance-images.rst b/doc/source/install/configure-glance-images.rst index 7268d02c8b..f3198ecd8c 100644 --- a/doc/source/install/configure-glance-images.rst +++ b/doc/source/install/configure-glance-images.rst @@ -33,6 +33,13 @@ the Image service for each one as it is generated. Images from Glance used by Ironic must be flagged as ``public``, which requires administrative privileges with the Glance image service to set. +.. note:: + Starting with the 2025.2 release, the Ironic conductor can access images that + are shared with its project, in addition to those it owns. + + To use this feature, ensure the required images are shared with the project + associated with the conductor's credentials. + - For *whole disk images* just upload the image: .. code-block:: console diff --git a/ironic/common/glance_service/service_utils.py b/ironic/common/glance_service/service_utils.py index 82c801307c..9e3ff9ae75 100644 --- a/ironic/common/glance_service/service_utils.py +++ b/ironic/common/glance_service/service_utils.py @@ -22,6 +22,7 @@ from oslo_utils import timeutils from oslo_utils import uuidutils from ironic.common import exception +from ironic.common import image_service as service from ironic.common import keystone from ironic.conf import CONF @@ -140,10 +141,13 @@ def is_image_available(context, image): image_visibility = getattr(image, 'visibility', None) image_owner = getattr(image, 'owner', None) image_id = getattr(image, 'id', 'unknown') + image_shared_member_list = get_image_member_list(image_id, context) is_admin = 'admin' in getattr(context, 'roles', []) project = getattr(context, 'project', 'unknown') + # If an auth token is present and the config allows access via auth token, # allow image access. + # NOTE(satoshi): This config should be removed in the H (2026.2) cycle if CONF.allow_image_access_via_auth_token and auth_token: # We return true here since we want the *user* request context to # be able to be used. @@ -159,7 +163,11 @@ def is_image_available(context, image): # allow access. if image_visibility == 'private' and image_owner == conductor_project_id: return True - + # If the image is shared and the conductor_project_id is in the shared + # member list, allow access + if image_visibility == 'shared'\ + and conductor_project_id in image_shared_member_list: + return True LOG.info( 'Access to %s owned by %s denied to requester %s', image_id, image_owner, project @@ -198,3 +206,17 @@ def get_conductor_project_id(): except Exception as e: LOG.debug("Error getting conductor project ID: %s", str(e)) return None + + +def get_image_member_list(image_id, context): + try: + glance_service = service.GlanceImageService(context=context) + members = glance_service.client.image.members(image_id) + return [ + member['member_id'] + for member in members + ] + except Exception as e: + LOG.error("Unable to retrieve image members for image %s: %s", + image_id, e) + return [] diff --git a/ironic/tests/unit/common/test_glance_service.py b/ironic/tests/unit/common/test_glance_service.py index 9d9344faf9..b294c25771 100644 --- a/ironic/tests/unit/common/test_glance_service.py +++ b/ironic/tests/unit/common/test_glance_service.py @@ -1041,8 +1041,24 @@ class TestIsImageAvailable(base.TestCase): self.assertTrue(service_utils.is_image_available( self.context, self.image)) + @mock.patch.object(service_utils, 'get_image_member_list', autospec=True) + def test_allow_shared_image_if_member(self, mock_get_members): + self.image.visibility = 'shared' + self.image.id = 'shared-image-id' + self.image.owner = 'some-other-project' + + self.context.project = 'test-project' + + # Mock the conductor project ID and the shared member list + conductor_id = service_utils.get_conductor_project_id() + mock_get_members.return_value = [conductor_id] + + self.assertTrue(service_utils.is_image_available( + self.context, self.image)) + mock_get_members.assert_called_once_with('shared-image-id', + self.context) + def test_deny_private_image_different_owner(self): - self.config(allow_image_access_via_auth_token=False) self.config(ignore_project_check_for_admin_tasks=False) self.image.visibility = 'private' diff --git a/releasenotes/notes/support-shared-images-8279f7ecd66b7218.yaml b/releasenotes/notes/support-shared-images-8279f7ecd66b7218.yaml new file mode 100644 index 0000000000..6632c2b10e --- /dev/null +++ b/releasenotes/notes/support-shared-images-8279f7ecd66b7218.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The Ironic conductor can now access images that are + shared with its project, in addition to those it owns. + + To use the feature, ensure the images are shared with + the project associated with the conductor's credentials.