Merge "Register glance user in keystoneauth plugin"

This commit is contained in:
Zuul
2025-09-09 19:55:56 +00:00
committed by Gerrit Code Review
5 changed files with 101 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ import urllib.parse
import glanceclient
import glanceclient.exc
from keystoneauth1 import loading as ks_loading
from keystoneauth1.loading import session as ks_session
from oslo_config import cfg
from oslo_log import log as logging
@@ -87,6 +88,14 @@ CONF = cfg.CONF
CONF.register_opts(image_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
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)
def _create_glance_client(context: context.RequestContext,
netloc: str,
use_ssl: bool) -> glanceclient.Client:
def _create_glance_client(
context: context.RequestContext,
netloc: str,
use_ssl: bool,
privileged_user: bool = False) -> glanceclient.Client:
"""Instantiate a new glanceclient.Client object."""
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':
params = {'insecure': CONF.glance_api_insecure,
'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)
auth = service_auth.get_auth_plugin(context)
auth = service_auth.get_auth_plugin(context, auth=g_auth)
params['auth'] = auth
params['session'] = _SESSION
@@ -186,44 +200,51 @@ class GlanceClientWrapper(object):
def __init__(self,
context: Optional[context.RequestContext] = None,
netloc: Optional[str] = None,
use_ssl: bool = False):
use_ssl: bool = False,
privileged_user: bool = False):
self.client: Optional[glanceclient.Client]
if netloc is not None:
assert context is not None
self.client = self._create_static_client(context,
netloc,
use_ssl)
use_ssl,
privileged_user)
else:
self.client = None
self.api_servers: Optional[Iterable] = None
def _create_static_client(self,
context: context.RequestContext,
netloc: str,
use_ssl: bool) -> glanceclient.Client:
def _create_static_client(
self,
context: context.RequestContext,
netloc: str,
use_ssl: bool,
privileged_user: bool = False) -> glanceclient.Client:
"""Create a client that we'll use for every call."""
self.netloc = netloc
self.use_ssl = use_ssl
return _create_glance_client(context,
self.netloc,
self.use_ssl)
self.use_ssl,
privileged_user)
def _create_onetime_client(
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."""
if self.api_servers is None:
self.api_servers = get_api_servers(context)
self.netloc, self.use_ssl = next(self.api_servers) # type: ignore
return _create_glance_client(context,
self.netloc,
self.use_ssl)
self.use_ssl,
privileged_user)
def call(self,
context: context.RequestContext,
method: str,
*args: Any,
**kwargs: str) -> Any:
**kwargs: Any) -> Any:
"""Call a glance client method.
If we get a connection error,
@@ -237,9 +258,11 @@ class GlanceClientWrapper(object):
glance_controller = kwargs.pop('controller', 'images')
store_id = kwargs.pop('store_id', 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):
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',)
values = (store_id, base_image_ref,)
@@ -387,8 +410,16 @@ class GlanceImageService(object):
try_methods = ('add_image_location', 'add_location')
for method in try_methods:
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,
image_id, url, metadata)
image_id, url, metadata,
privileged_user=privileged_user)
except glanceclient.exc.HTTPNotImplemented:
LOG.debug('Glance method %s not available', method)
except Exception:

View File

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

View File

@@ -21,6 +21,7 @@ from unittest import mock
import ddt
import glanceclient.exc
from keystoneauth1 import loading as ksloading
from keystoneauth1.loading import session as ks_session
from keystoneauth1 import session
from oslo_config import cfg
@@ -112,7 +113,8 @@ class TestGlanceImageService(test.TestCase):
self.mock_object(glance.time, 'sleep', return_value=None)
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
self.mock_object(glance, '_create_glance_client',
@@ -782,7 +784,8 @@ class TestGlanceImageService(test.TestCase):
service.add_location(self.context, image_id, url, metadata)
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')
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)
calls = [
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(
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)
def test_download_with_retries(self):
@@ -1278,7 +1283,7 @@ class TestGlanceImageServiceClient(test.TestCase):
client = glance._create_glance_client(self.context, 'fake_host:9292',
False)
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.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',
True)
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)
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',
False)
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'):
group_name = nova.NOVA_GROUP
if (group_name == 'GLANCE_GROUP'):
group_name = 'glance'
if group_name in registered_opts_dict:
line = key + "." + formatted_opt
registered_opts_dict[group_name].append(line)