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:

committed by
Rajat Dhasmana

parent
2f095cfee3
commit
9dfb500d5b
@@ -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:
|
||||||
|
@@ -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,
|
||||||
|
@@ -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')
|
||||||
|
@@ -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.
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user