Register glance user in keystoneauth plugin

The OpenStack services communicate with each other by
passing the user token and service token wrapped in
keystoneauth's ServiceTokenAuthWrapper. The purpose
of passing the service token is for long-running
operations and in case the user token gets expired.

For RBAC, services need to check if a user token has the
'service'
role or not. For that calling service needs to load the
configured user auth plugin (where the user should have
the 'service' role) from keystoneauth and pass that to
the other services and called service (glance in this case)
will use that user role to verify the policy permission.

Cinder register and load user auth plugin from keystonauth
for nova communication case
- 644b6362a6/cinder/compute/nova.py (L100)

But it is missing for glance case which is fixed in this change.

Closes-Bug: #2121622
Needed-By: https://review.opendev.org/c/openstack/glance/+/958715

Change-Id: Ia3fe15517cdbeb8295725b99b526dd70ce290562
Signed-off-by: Ghanshyam Maan <gmaan@ghanshyammann.com>
This commit is contained in:
Ghanshyam Maan
2025-08-28 03:53:10 +00:00
committed by Rajat Dhasmana
parent 2f095cfee3
commit 9dfb500d5b
5 changed files with 101 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ import urllib.parse
import glanceclient import glanceclient
import glanceclient.exc import glanceclient.exc
from keystoneauth1 import loading as ks_loading
from keystoneauth1.loading import session as ks_session from keystoneauth1.loading import session as ks_session
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@@ -87,6 +88,14 @@ CONF = cfg.CONF
CONF.register_opts(image_opts) CONF.register_opts(image_opts)
CONF.register_opts(glance_core_properties_opts) CONF.register_opts(glance_core_properties_opts)
# Register keystoneauth options to create service user
# to talk to glance.
GLANCE_GROUP = 'glance'
glance_session_opts = ks_loading.get_session_conf_options()
glance_auth_opts = ks_loading.get_auth_common_conf_options()
CONF.register_opts(glance_session_opts, group=GLANCE_GROUP)
CONF.register_opts(glance_auth_opts, group=GLANCE_GROUP)
_SESSION = None _SESSION = None
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -107,12 +116,17 @@ def _parse_image_ref(image_href: str) -> tuple[str, str, bool]:
return (image_id, netloc, use_ssl) return (image_id, netloc, use_ssl)
def _create_glance_client(context: context.RequestContext, def _create_glance_client(
netloc: str, context: context.RequestContext,
use_ssl: bool) -> glanceclient.Client: netloc: str,
use_ssl: bool,
privileged_user: bool = False) -> glanceclient.Client:
"""Instantiate a new glanceclient.Client object.""" """Instantiate a new glanceclient.Client object."""
params = {'global_request_id': context.global_id} params = {'global_request_id': context.global_id}
g_auth = None
if privileged_user and CONF[GLANCE_GROUP].auth_type:
LOG.debug('Creating Keystone auth plugin from conf')
g_auth = ks_loading.load_auth_from_conf_options(CONF, GLANCE_GROUP)
if use_ssl and CONF.auth_strategy == 'noauth': if use_ssl and CONF.auth_strategy == 'noauth':
params = {'insecure': CONF.glance_api_insecure, params = {'insecure': CONF.glance_api_insecure,
'cacert': CONF.glance_ca_certificates_file, 'cacert': CONF.glance_ca_certificates_file,
@@ -131,7 +145,7 @@ def _create_glance_client(context: context.RequestContext,
} }
_SESSION = ks_session.Session().load_from_options(**config_options) _SESSION = ks_session.Session().load_from_options(**config_options)
auth = service_auth.get_auth_plugin(context) auth = service_auth.get_auth_plugin(context, auth=g_auth)
params['auth'] = auth params['auth'] = auth
params['session'] = _SESSION params['session'] = _SESSION
@@ -186,44 +200,51 @@ class GlanceClientWrapper(object):
def __init__(self, def __init__(self,
context: Optional[context.RequestContext] = None, context: Optional[context.RequestContext] = None,
netloc: Optional[str] = None, netloc: Optional[str] = None,
use_ssl: bool = False): use_ssl: bool = False,
privileged_user: bool = False):
self.client: Optional[glanceclient.Client] self.client: Optional[glanceclient.Client]
if netloc is not None: if netloc is not None:
assert context is not None assert context is not None
self.client = self._create_static_client(context, self.client = self._create_static_client(context,
netloc, netloc,
use_ssl) use_ssl,
privileged_user)
else: else:
self.client = None self.client = None
self.api_servers: Optional[Iterable] = None self.api_servers: Optional[Iterable] = None
def _create_static_client(self, def _create_static_client(
context: context.RequestContext, self,
netloc: str, context: context.RequestContext,
use_ssl: bool) -> glanceclient.Client: netloc: str,
use_ssl: bool,
privileged_user: bool = False) -> glanceclient.Client:
"""Create a client that we'll use for every call.""" """Create a client that we'll use for every call."""
self.netloc = netloc self.netloc = netloc
self.use_ssl = use_ssl self.use_ssl = use_ssl
return _create_glance_client(context, return _create_glance_client(context,
self.netloc, self.netloc,
self.use_ssl) self.use_ssl,
privileged_user)
def _create_onetime_client( def _create_onetime_client(
self, self,
context: context.RequestContext) -> glanceclient.Client: context: context.RequestContext,
privileged_user: bool = False) -> glanceclient.Client:
"""Create a client that will be used for one call.""" """Create a client that will be used for one call."""
if self.api_servers is None: if self.api_servers is None:
self.api_servers = get_api_servers(context) self.api_servers = get_api_servers(context)
self.netloc, self.use_ssl = next(self.api_servers) # type: ignore self.netloc, self.use_ssl = next(self.api_servers) # type: ignore
return _create_glance_client(context, return _create_glance_client(context,
self.netloc, self.netloc,
self.use_ssl) self.use_ssl,
privileged_user)
def call(self, def call(self,
context: context.RequestContext, context: context.RequestContext,
method: str, method: str,
*args: Any, *args: Any,
**kwargs: str) -> Any: **kwargs: Any) -> Any:
"""Call a glance client method. """Call a glance client method.
If we get a connection error, If we get a connection error,
@@ -237,9 +258,11 @@ class GlanceClientWrapper(object):
glance_controller = kwargs.pop('controller', 'images') glance_controller = kwargs.pop('controller', 'images')
store_id = kwargs.pop('store_id', None) store_id = kwargs.pop('store_id', None)
base_image_ref = kwargs.pop('base_image_ref', None) base_image_ref = kwargs.pop('base_image_ref', None)
privileged_user = kwargs.pop('privileged_user', False)
for attempt in range(1, num_attempts + 1): for attempt in range(1, num_attempts + 1):
client = self.client or self._create_onetime_client(context) client = self.client or self._create_onetime_client(
context, privileged_user)
keys = ('x-image-meta-store', 'x-openstack-base-image-ref',) keys = ('x-image-meta-store', 'x-openstack-base-image-ref',)
values = (store_id, base_image_ref,) values = (store_id, base_image_ref,)
@@ -387,8 +410,16 @@ class GlanceImageService(object):
try_methods = ('add_image_location', 'add_location') try_methods = ('add_image_location', 'add_location')
for method in try_methods: for method in try_methods:
try: try:
# NOTE(gmaan): Glance add_image_location API policy rule is
# default to 'service' role so cinder needs to load the auth
# plugin from the keystoneauth which has the 'service' role.
if method == 'add_image_location':
privileged_user = True
else:
privileged_user = False
return client.call(context, method, return client.call(context, method,
image_id, url, metadata) image_id, url, metadata,
privileged_user=privileged_user)
except glanceclient.exc.HTTPNotImplemented: except glanceclient.exc.HTTPNotImplemented:
LOG.debug('Glance method %s not available', method) LOG.debug('Glance method %s not available', method)
except Exception: except Exception:

View File

@@ -453,6 +453,11 @@ def list_opts():
cinder_volume_manager.volume_backend_opts, cinder_volume_manager.volume_backend_opts,
cinder_volume_targets_spdknvmf.spdk_opts, cinder_volume_targets_spdknvmf.spdk_opts,
)), )),
('glance',
itertools.chain(
cinder_image_glance.glance_session_opts,
cinder_image_glance.glance_auth_opts,
)),
('nova', ('nova',
itertools.chain( itertools.chain(
cinder_compute_nova.nova_opts, cinder_compute_nova.nova_opts,

View File

@@ -21,6 +21,7 @@ from unittest import mock
import ddt import ddt
import glanceclient.exc import glanceclient.exc
from keystoneauth1 import loading as ksloading
from keystoneauth1.loading import session as ks_session from keystoneauth1.loading import session as ks_session
from keystoneauth1 import session from keystoneauth1 import session
from oslo_config import cfg from oslo_config import cfg
@@ -112,7 +113,8 @@ class TestGlanceImageService(test.TestCase):
self.mock_object(glance.time, 'sleep', return_value=None) self.mock_object(glance.time, 'sleep', return_value=None)
def _create_image_service(self, client): def _create_image_service(self, client):
def _fake_create_glance_client(context, netloc, use_ssl): def _fake_create_glance_client(
context, netloc, use_ssl, privileged_user=False):
return client return client
self.mock_object(glance, '_create_glance_client', self.mock_object(glance, '_create_glance_client',
@@ -782,7 +784,8 @@ class TestGlanceImageService(test.TestCase):
service.add_location(self.context, image_id, url, metadata) service.add_location(self.context, image_id, url, metadata)
mock_call.assert_called_once_with( mock_call.assert_called_once_with(
self.context, 'add_image_location', image_id, url, metadata) self.context, 'add_image_location',
image_id, url, metadata, privileged_user=True)
@mock.patch.object(glance.GlanceClientWrapper, 'call') @mock.patch.object(glance.GlanceClientWrapper, 'call')
def test_add_location_old(self, mock_call): def test_add_location_old(self, mock_call):
@@ -795,9 +798,11 @@ class TestGlanceImageService(test.TestCase):
service.add_location(self.context, image_id, url, metadata) service.add_location(self.context, image_id, url, metadata)
calls = [ calls = [
mock.call.call( mock.call.call(
self.context, 'add_image_location', image_id, url, metadata), self.context, 'add_image_location',
image_id, url, metadata, privileged_user=True),
mock.call.call( mock.call.call(
self.context, 'add_location', image_id, url, metadata)] self.context, 'add_location',
image_id, url, metadata, privileged_user=False)]
mock_call.assert_has_calls(calls) mock_call.assert_has_calls(calls)
def test_download_with_retries(self): def test_download_with_retries(self):
@@ -1278,7 +1283,7 @@ class TestGlanceImageServiceClient(test.TestCase):
client = glance._create_glance_client(self.context, 'fake_host:9292', client = glance._create_glance_client(self.context, 'fake_host:9292',
False) False)
self.assertIsInstance(client, MyGlanceStubClient) self.assertIsInstance(client, MyGlanceStubClient)
mock_get_auth_plugin.assert_called_once_with(self.context) mock_get_auth_plugin.assert_called_once_with(self.context, auth=None)
mock_load.assert_called_once_with(**config_options) mock_load.assert_called_once_with(**config_options)
@mock.patch('cinder.service_auth.get_auth_plugin') @mock.patch('cinder.service_auth.get_auth_plugin')
@@ -1314,7 +1319,7 @@ class TestGlanceImageServiceClient(test.TestCase):
client = glance._create_glance_client(self.context, 'fake_host:9292', client = glance._create_glance_client(self.context, 'fake_host:9292',
True) True)
self.assertIsInstance(client, MyGlanceStubClient) self.assertIsInstance(client, MyGlanceStubClient)
mock_get_auth_plugin.assert_called_once_with(self.context) mock_get_auth_plugin.assert_called_once_with(self.context, auth=None)
mock_load.assert_called_once_with(**config_options) mock_load.assert_called_once_with(**config_options)
def test_create_glance_client_auth_strategy_noauth_with_protocol_https( def test_create_glance_client_auth_strategy_noauth_with_protocol_https(
@@ -1358,3 +1363,15 @@ class TestGlanceImageServiceClient(test.TestCase):
client = glance._create_glance_client(self.context, 'fake_host:9292', client = glance._create_glance_client(self.context, 'fake_host:9292',
False) False)
self.assertIsInstance(client, MyGlanceStubClient) self.assertIsInstance(client, MyGlanceStubClient)
@mock.patch('cinder.service_auth.get_auth_plugin')
@mock.patch.object(ksloading, 'load_auth_from_conf_options')
def test_create_glance_client_with_privileged_user(
self, mock_load, mock_get_auth_plugin):
self.flags(auth_strategy='keystone')
self.flags(auth_type='password', group='glance')
mock_load.return_value = 'fake_auth_plugin'
glance.GlanceClientWrapper(self.context, 'fake_host:9292', True, True)
mock_load.assert_called_once()
mock_get_auth_plugin.assert_called_once_with(
self.context, auth='fake_auth_plugin')

View File

@@ -0,0 +1,22 @@
---
upgrade:
- |
With the adoption of New Location APIs, we need a mechanism
to perform service-to-service communication to access the
``add_image_location`` and ``get_image_locations`` APIs.
To achieve the desired functionality, we will need to perform
two additional changes during the deployment:
1. Assign the ``admin`` and ``service`` role to the ``glance`` user
2. Configure a ``[glance]`` section in cinder configuration file
with the credentials of ``glance`` user and ``service`` project.
Refer to the ``[nova]`` or ``[service_user]`` section for reference.
features:
- |
Added support for service-to-service communication between Cinder and
Glance.
Currently the service-to-service communication is leveraged by the new
location APIs for which we will need to configure a dedicated ``[glance]``
section in cinder configuration file with the credentials of ``glance``
user and ``service`` project.
Refer to the ``[nova]`` or ``[service_user]`` section for reference.

View File

@@ -228,6 +228,9 @@ if __name__ == "__main__":
if (group_name == 'NOVA_GROUP'): if (group_name == 'NOVA_GROUP'):
group_name = nova.NOVA_GROUP group_name = nova.NOVA_GROUP
if (group_name == 'GLANCE_GROUP'):
group_name = 'glance'
if group_name in registered_opts_dict: if group_name in registered_opts_dict:
line = key + "." + formatted_opt line = key + "." + formatted_opt
registered_opts_dict[group_name].append(line) registered_opts_dict[group_name].append(line)