diff --git a/cinder/image/glance.py b/cinder/image/glance.py index bdcc91d4b4b..5060603a15c 100644 --- a/cinder/image/glance.py +++ b/cinder/image/glance.py @@ -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: diff --git a/cinder/opts.py b/cinder/opts.py index bb90c0cf552..d78f64e19d0 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -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, diff --git a/cinder/tests/unit/image/test_glance.py b/cinder/tests/unit/image/test_glance.py index 9f4d09ac9cc..c9528e812b4 100644 --- a/cinder/tests/unit/image/test_glance.py +++ b/cinder/tests/unit/image/test_glance.py @@ -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') diff --git a/releasenotes/notes/add-glance-service-section-3e73daee0e995442.yaml b/releasenotes/notes/add-glance-service-section-3e73daee0e995442.yaml new file mode 100644 index 00000000000..b5aa42300a2 --- /dev/null +++ b/releasenotes/notes/add-glance-service-section-3e73daee0e995442.yaml @@ -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. diff --git a/tools/config/generate_cinder_opts.py b/tools/config/generate_cinder_opts.py index d307e83121e..08a87983d91 100644 --- a/tools/config/generate_cinder_opts.py +++ b/tools/config/generate_cinder_opts.py @@ -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)