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
This commit is contained in:
Satoshi S. 2025-04-14 12:45:02 +00:00
parent 854f059b82
commit a39f11cece
4 changed files with 55 additions and 2 deletions

View File

@ -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 Images from Glance used by Ironic must be flagged as ``public``, which
requires administrative privileges with the Glance image service to set. 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: - For *whole disk images* just upload the image:
.. code-block:: console .. code-block:: console

View File

@ -22,6 +22,7 @@ from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
from ironic.common import exception from ironic.common import exception
from ironic.common import image_service as service
from ironic.common import keystone from ironic.common import keystone
from ironic.conf import CONF from ironic.conf import CONF
@ -140,10 +141,13 @@ def is_image_available(context, image):
image_visibility = getattr(image, 'visibility', None) image_visibility = getattr(image, 'visibility', None)
image_owner = getattr(image, 'owner', None) image_owner = getattr(image, 'owner', None)
image_id = getattr(image, 'id', 'unknown') image_id = getattr(image, 'id', 'unknown')
image_shared_member_list = get_image_member_list(image_id, context)
is_admin = 'admin' in getattr(context, 'roles', []) is_admin = 'admin' in getattr(context, 'roles', [])
project = getattr(context, 'project', 'unknown') project = getattr(context, 'project', 'unknown')
# If an auth token is present and the config allows access via auth token, # If an auth token is present and the config allows access via auth token,
# allow image access. # 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: if CONF.allow_image_access_via_auth_token and auth_token:
# We return true here since we want the *user* request context to # We return true here since we want the *user* request context to
# be able to be used. # be able to be used.
@ -159,7 +163,11 @@ def is_image_available(context, image):
# allow access. # allow access.
if image_visibility == 'private' and image_owner == conductor_project_id: if image_visibility == 'private' and image_owner == conductor_project_id:
return True 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( LOG.info(
'Access to %s owned by %s denied to requester %s', 'Access to %s owned by %s denied to requester %s',
image_id, image_owner, project image_id, image_owner, project
@ -198,3 +206,17 @@ def get_conductor_project_id():
except Exception as e: except Exception as e:
LOG.debug("Error getting conductor project ID: %s", str(e)) LOG.debug("Error getting conductor project ID: %s", str(e))
return None 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 []

View File

@ -1041,8 +1041,24 @@ class TestIsImageAvailable(base.TestCase):
self.assertTrue(service_utils.is_image_available( self.assertTrue(service_utils.is_image_available(
self.context, self.image)) 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): 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.config(ignore_project_check_for_admin_tasks=False)
self.image.visibility = 'private' self.image.visibility = 'private'

View File

@ -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.