From 0a8f019be0f75005929724df3238b0a1ed49c88d Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Wed, 19 Apr 2017 17:03:17 -0500 Subject: [PATCH] nova.utils.get_ksa_adapter() Provide a new method: nova.utils.get_ksa_adapter(service_type, ks_auth=None, ks_session=None, min_version=None, max_version=None)) ...to configure a keystoneauth1 Adapter for a service. The Adapter, and its component keystoneauth1 artifacts not passed into the method, are loaded based on options in the conf group corresponding to the specified service_type. The ultimate goal is to replace the various disparate mechanisms used by different services to do endpoint URL and version discovery. In Queens, the original mechanisms will still take precedence, but (other than [glance]api_servers - see the spec) will be deprecated. In Rocky, the deprecated options will be removed. This change incorporates the above utility into endpoint discovery for glance and ironic. Future change sets will do the same for other services (cinder, neutron, placement). Change-Id: If625411f40be0ba642baeb02950f568f43673655 Partial-Implements: bp use-ksa-adapter-for-endpoints Closes-Bug: #1707860 --- nova/conf/glance.py | 48 ++++++----- nova/conf/ironic.py | 33 +++++-- nova/conf/utils.py | 85 +++++++++++++++++++ nova/exception.py | 5 ++ nova/image/glance.py | 38 ++++++--- nova/tests/unit/image/test_glance.py | 21 ++++- nova/tests/unit/test_utils.py | 78 +++++++++++++++++ .../unit/virt/ironic/test_client_wrapper.py | 58 +++++++++++++ nova/utils.py | 58 +++++++++++++ nova/virt/ironic/client_wrapper.py | 26 +++++- .../glance-via-ksa-5646eb3d5db51c54.yaml | 10 +++ .../ironic-via-ksa-deffd3dac48ff4eb.yaml | 11 +++ requirements.txt | 1 + 13 files changed, 427 insertions(+), 45 deletions(-) create mode 100644 nova/conf/utils.py create mode 100644 releasenotes/notes/glance-via-ksa-5646eb3d5db51c54.yaml create mode 100644 releasenotes/notes/ironic-via-ksa-deffd3dac48ff4eb.yaml diff --git a/nova/conf/glance.py b/nova/conf/glance.py index 61693b5a5b7e..43a31d60811d 100644 --- a/nova/conf/glance.py +++ b/nova/conf/glance.py @@ -15,22 +15,29 @@ from keystoneauth1 import loading as ks_loading from oslo_config import cfg +from nova.conf import utils as confutils + + +DEFAULT_SERVICE_TYPE = 'image' + glance_group = cfg.OptGroup( 'glance', title='Glance Options', help='Configuration options for the Image service') glance_opts = [ - # NOTE(sdague): there is intentionally no default here. This - # requires configuration. Eventually this will come from the - # service catalog, however we don't have a good path there atm. - # TODO(raj_singh): Add "required=True" flag to this option. + # NOTE(sdague/efried): there is intentionally no default here. This + # requires configuration if ksa adapter config is not used. cfg.ListOpt('api_servers', help=""" List of glance api servers endpoints available to nova. https is used for ssl-based glance api servers. +NOTE: The preferred mechanism for endpoint discovery is via keystoneauth1 +loading options. Only use api_servers if you need multiple endpoints and are +unable to use a load balancer for some reason. + Possible values: * A list of any fully qualified url of the form "scheme://hostname:port[/path]" @@ -130,28 +137,29 @@ Related options: help='Enable or disable debug logging with glanceclient.') ] +deprecated_ksa_opts = { + 'insecure': [cfg.DeprecatedOpt('api_insecure', group=glance_group.name)], + 'cafile': [cfg.DeprecatedOpt('ca_file', group="ssl")], + 'certfile': [cfg.DeprecatedOpt('cert_file', group="ssl")], + 'keyfile': [cfg.DeprecatedOpt('key_file', group="ssl")], +} + def register_opts(conf): conf.register_group(glance_group) conf.register_opts(glance_opts, group=glance_group) - deprecated = { - 'insecure': [cfg.DeprecatedOpt('api_insecure', - group=glance_group.name)], - 'cafile': [cfg.DeprecatedOpt('ca_file', - group="ssl")], - 'certfile': [cfg.DeprecatedOpt('cert_file', - group="ssl")], - 'keyfile': [cfg.DeprecatedOpt('key_file', - group="ssl")], - } - ks_loading.register_session_conf_options(conf, glance_group.name, - deprecated) + confutils.register_ksa_opts(conf, glance_group, DEFAULT_SERVICE_TYPE, + deprecated_opts=deprecated_ksa_opts) def list_opts(): - return { - glance_group: ( - glance_opts + - ks_loading.get_session_conf_options()) + return {glance_group: ( + glance_opts + + ks_loading.get_session_conf_options() + + ks_loading.get_auth_plugin_conf_options('password') + + ks_loading.get_auth_plugin_conf_options('v2password') + + ks_loading.get_auth_plugin_conf_options('v3password') + + confutils.get_ksa_adapter_opts(DEFAULT_SERVICE_TYPE, + deprecated_opts=deprecated_ksa_opts)) } diff --git a/nova/conf/ironic.py b/nova/conf/ironic.py index 756e544ea2a7..f2ac59aa626f 100644 --- a/nova/conf/ironic.py +++ b/nova/conf/ironic.py @@ -16,6 +16,11 @@ from keystoneauth1 import loading as ks_loading from oslo_config import cfg +from nova.conf import utils as confutils + + +DEFAULT_SERVICE_TYPE = 'baremetal' + ironic_group = cfg.OptGroup( 'ironic', title='Ironic Options', @@ -35,6 +40,13 @@ ironic_options = [ cfg.URIOpt( 'api_endpoint', schemes=['http', 'https'], + deprecated_for_removal=True, + deprecated_reason='Endpoint lookup uses the service catalog via ' + 'common keystoneauth1 Adapter configuration ' + 'options. In the current release, api_endpoint will ' + 'override this behavior, but will be ignored and/or ' + 'removed in a future release. To achieve the same ' + 'result, use the endpoint_override option instead.', sample_default='http://ironic.example.org:6385/', help='URL override for the Ironic API endpoint.'), cfg.IntOpt( @@ -69,17 +81,24 @@ Related options: 'changed. Set to 0 to disable timeout.'), ] +deprecated_opts = { + 'endpoint_override': [cfg.DeprecatedOpt('api_endpoint', + group=ironic_group.name)]} + def register_opts(conf): conf.register_group(ironic_group) conf.register_opts(ironic_options, group=ironic_group) - ks_loading.register_auth_conf_options(conf, group=ironic_group.name) - ks_loading.register_session_conf_options(conf, group=ironic_group.name) + confutils.register_ksa_opts(conf, ironic_group, DEFAULT_SERVICE_TYPE, + deprecated_opts=deprecated_opts) def list_opts(): - return {ironic_group: ironic_options + - ks_loading.get_session_conf_options() + - ks_loading.get_auth_common_conf_options() + - ks_loading.get_auth_plugin_conf_options('v3password') - } + return {ironic_group: ( + ironic_options + + ks_loading.get_session_conf_options() + + ks_loading.get_auth_common_conf_options() + + ks_loading.get_auth_plugin_conf_options('v3password') + + confutils.get_ksa_adapter_opts(DEFAULT_SERVICE_TYPE, + deprecated_opts=deprecated_opts)) + } diff --git a/nova/conf/utils.py b/nova/conf/utils.py new file mode 100644 index 000000000000..f24ef44baa75 --- /dev/null +++ b/nova/conf/utils.py @@ -0,0 +1,85 @@ +# Copyright 2017 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Common utilities for conf providers. + +This module does not provide any actual conf options. +""" +from keystoneauth1 import loading as ks_loading +from oslo_config import cfg + + +_ADAPTER_VERSION_OPTS = ('version', 'min_version', 'max_version') + + +def get_ksa_adapter_opts(default_service_type, deprecated_opts=None): + """Get auth, Session, and Adapter conf options from keystonauth1.loading. + + :param default_service_type: Default for the service_type conf option on + the Adapter. + :param deprecated_opts: dict of deprecated opts to register with the ksa + Adapter opts. Works the same as the + deprecated_opts kwarg to: + keystoneauth1.loading.session.Session.register_conf_options + :return: List of cfg.Opts. + """ + opts = ks_loading.get_adapter_conf_options(include_deprecated=False, + deprecated_opts=deprecated_opts) + + for opt in opts[:]: + # Remove version-related opts. Required/supported versions are + # something the code knows about, not the operator. + if opt.dest in _ADAPTER_VERSION_OPTS: + opts.remove(opt) + + # Override defaults that make sense for nova + cfg.set_defaults(opts, + valid_interfaces=['internal', 'public'], + service_type=default_service_type) + return opts + + +def _dummy_opt(name): + # A config option that can't be set by the user, so it behaves as if it's + # ignored; but consuming code may expect it to be present in a conf group. + return cfg.Opt(name, type=lambda x: None) + + +def register_ksa_opts(conf, group, default_service_type, deprecated_opts=None): + """Register keystoneauth auth, Session, and Adapter opts. + + :param conf: oslo_config.cfg.CONF in which to register the options + :param group: Conf group, or string name thereof, in which to register the + options. + :param default_service_type: Default for the service_type conf option on + the Adapter. + :param deprecated_opts: dict of deprecated opts to register with the ksa + Session or Adapter opts. See docstring for + the deprecated_opts param of: + keystoneauth1.loading.session.Session.register_conf_options + """ + # ksa register methods need the group name as a string. oslo doesn't care. + group = getattr(group, 'name', group) + ks_loading.register_session_conf_options( + conf, group, deprecated_opts=deprecated_opts) + ks_loading.register_auth_conf_options(conf, group) + conf.register_opts(get_ksa_adapter_opts( + default_service_type, deprecated_opts=deprecated_opts), group=group) + # Have to register dummies for the version-related opts we removed + for name in _ADAPTER_VERSION_OPTS: + conf.register_opt(_dummy_opt(name), group=group) + + +# NOTE(efried): Required for docs build. +def list_opts(): + return {} diff --git a/nova/exception.py b/nova/exception.py index 972c148590f6..68e5e5f4590d 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -968,6 +968,11 @@ class ServiceNotFound(NotFound): msg_fmt = _("Service %(service_id)s could not be found.") +class ConfGroupForServiceTypeNotFound(ServiceNotFound): + msg_fmt = _("No conf group name could be found for service type " + "%(stype)s. Please report this bug.") + + class ServiceBinaryExists(NovaException): msg_fmt = _("Service with host %(host)s binary %(binary)s exists.") diff --git a/nova/image/glance.py b/nova/image/glance.py index 456a8f8591ce..eb4e96772da4 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -47,6 +47,7 @@ import nova.image.download as image_xfers from nova import objects from nova.objects import fields from nova import service_auth +from nova import utils LOG = logging.getLogger(__name__) @@ -106,22 +107,33 @@ def generate_identity_headers(context, status='Confirmed'): def get_api_servers(): - """Shuffle a list of CONF.glance.api_servers and return an iterator - that will cycle through the list, looping around to the beginning - if necessary. + """Shuffle a list of service endpoints and return an iterator that will + cycle through the list, looping around to the beginning if necessary. """ + # NOTE(efried): utils.get_ksa_adapter().get_endpoint() is the preferred + # mechanism for endpoint discovery. Only use `api_servers` if you really + # need to shuffle multiple endpoints. api_servers = [] + if CONF.glance.api_servers: + for api_server in CONF.glance.api_servers: + if '//' not in api_server: + api_server = 'http://' + api_server + # NOTE(sdague): remove in O. + LOG.warning("No protocol specified in for api_server '%s', " + "please update [glance] api_servers with fully " + "qualified url including scheme (http / https)", + api_server) + api_servers.append(api_server) + random.shuffle(api_servers) + else: + # TODO(efried): Plumb in a reasonable auth from callers' contexts + ksa_adap = utils.get_ksa_adapter( + nova.conf.glance.DEFAULT_SERVICE_TYPE, + min_version='2.0', max_version='2.latest') + # TODO(efried): Use ksa_adap.get_endpoint() when bug #1707995 is fixed. + api_servers.append(ksa_adap.endpoint_override or + ksa_adap.get_endpoint_data().catalog_url) - for api_server in CONF.glance.api_servers: - if '//' not in api_server: - api_server = 'http://' + api_server - # NOTE(sdague): remove in O. - LOG.warning("No protocol specified in for api_server '%s', " - "please update [glance] api_servers with fully " - "qualified url including scheme (http / https)", - api_server) - api_servers.append(api_server) - random.shuffle(api_servers) return itertools.cycle(api_servers) diff --git a/nova/tests/unit/image/test_glance.py b/nova/tests/unit/image/test_glance.py index 879ce27ee057..347314a7028d 100644 --- a/nova/tests/unit/image/test_glance.py +++ b/nova/tests/unit/image/test_glance.py @@ -1573,7 +1573,8 @@ class TestDelete(test.NoDBTestCase): class TestGlanceApiServers(test.NoDBTestCase): - def test_get_api_servers(self): + def test_get_api_servers_multiple(self): + """Test get_api_servers via `api_servers` conf option.""" glance_servers = ['10.0.1.1:9292', 'https://10.0.0.1:9293', 'http://10.0.2.2:9294'] @@ -1589,6 +1590,24 @@ class TestGlanceApiServers(test.NoDBTestCase): if i > 2: break + @mock.patch('keystoneauth1.adapter.Adapter.get_endpoint_data') + def test_get_api_servers_get_ksa_adapter(self, mock_epd): + """Test get_api_servers via nova.utils.get_ksa_adapter().""" + self.flags(api_servers=None, group='glance') + api_servers = glance.get_api_servers() + self.assertEqual(mock_epd.return_value.catalog_url, next(api_servers)) + # Still get itertools.cycle behavior + self.assertEqual(mock_epd.return_value.catalog_url, next(api_servers)) + mock_epd.assert_called_once_with() + + # Now test with endpoint_override - get_endpoint_data is not called. + mock_epd.reset_mock() + self.flags(endpoint_override='foo', group='glance') + api_servers = glance.get_api_servers() + self.assertEqual('foo', next(api_servers)) + self.assertEqual('foo', next(api_servers)) + mock_epd.assert_not_called() + class TestUpdateGlanceImage(test.NoDBTestCase): @mock.patch('nova.image.glance.GlanceImageServiceV2') diff --git a/nova/tests/unit/test_utils.py b/nova/tests/unit/test_utils.py index a1815ba41b65..f2a7d0f626a0 100644 --- a/nova/tests/unit/test_utils.py +++ b/nova/tests/unit/test_utils.py @@ -20,6 +20,8 @@ import os.path import tempfile import eventlet +from keystoneauth1.identity import base as ks_identity +from keystoneauth1 import session as ks_session import mock import netaddr from oslo_concurrency import processutils @@ -1307,3 +1309,79 @@ class TestObjectCallHelpers(test.NoDBTestCase): test_utils.obj_called_once_with( tester.foo, 3, test_objects.MyObj(foo=3, bar='baz'))) + + +class GetKSAAdapterTestCase(test.NoDBTestCase): + """Tests for nova.utils.get_endpoint_data().""" + def setUp(self): + super(GetKSAAdapterTestCase, self).setUp() + self.sess = mock.create_autospec(ks_session.Session, instance=True) + self.auth = mock.create_autospec(ks_identity.BaseIdentityPlugin, + instance=True) + + load_sess_p = mock.patch( + 'keystoneauth1.loading.load_session_from_conf_options') + self.addCleanup(load_sess_p.stop) + self.load_sess = load_sess_p.start() + self.load_sess.return_value = self.sess + + load_adap_p = mock.patch( + 'keystoneauth1.loading.load_adapter_from_conf_options') + self.addCleanup(load_adap_p.stop) + self.load_adap = load_adap_p.start() + + load_auth_p = mock.patch( + 'keystoneauth1.loading.load_auth_from_conf_options') + self.addCleanup(load_auth_p.stop) + self.load_auth = load_auth_p.start() + self.load_auth.return_value = self.auth + + def test_bogus_service_type(self): + self.assertRaises(exception.ConfGroupForServiceTypeNotFound, + utils.get_ksa_adapter, 'bogus') + self.load_auth.assert_not_called() + self.load_sess.assert_not_called() + self.load_adap.assert_not_called() + + def test_all_params(self): + ret = utils.get_ksa_adapter( + 'image', ksa_auth='auth', ksa_session='sess', + min_version='min', max_version='max') + # Returned the result of load_adapter_from_conf_options + self.assertEqual(self.load_adap.return_value, ret) + # Because we supplied ksa_auth, load_auth* not called + self.load_auth.assert_not_called() + # Ditto ksa_session/load_session* + self.load_sess.assert_not_called() + # load_adapter* called with what we passed in (and the right group) + self.load_adap.assert_called_once_with( + utils.CONF, 'glance', session='sess', auth='auth', + min_version='min', max_version='max') + + def test_auth_from_session(self): + self.sess.auth = 'auth' + ret = utils.get_ksa_adapter('baremetal', ksa_session=self.sess) + # Returned the result of load_adapter_from_conf_options + self.assertEqual(self.load_adap.return_value, ret) + # Because ksa_auth found in ksa_session, load_auth* not called + self.load_auth.assert_not_called() + # Because we supplied ksa_session, load_session* not called + self.load_sess.assert_not_called() + # load_adapter* called with the auth from the session + self.load_adap.assert_called_once_with( + utils.CONF, 'ironic', session=self.sess, auth='auth', + min_version=None, max_version=None) + + def test_load_auth_and_session(self): + ret = utils.get_ksa_adapter('volumev2') + # Returned the result of load_adapter_from_conf_options + self.assertEqual(self.load_adap.return_value, ret) + # Had to load the auth + self.load_auth.assert_called_once_with(utils.CONF, 'cinder') + # Had to load the session, passed in the loaded auth + self.load_sess.assert_called_once_with(utils.CONF, 'cinder', + auth=self.auth) + # load_adapter* called with the loaded auth & session + self.load_adap.assert_called_once_with( + utils.CONF, 'cinder', session=self.sess, auth=self.auth, + min_version=None, max_version=None) diff --git a/nova/tests/unit/virt/ironic/test_client_wrapper.py b/nova/tests/unit/virt/ironic/test_client_wrapper.py index c320b134e588..a4ec9d6d01ff 100644 --- a/nova/tests/unit/virt/ironic/test_client_wrapper.py +++ b/nova/tests/unit/virt/ironic/test_client_wrapper.py @@ -15,6 +15,7 @@ from ironicclient import client as ironic_client from ironicclient import exc as ironic_exception +from keystoneauth1 import discover as ksa_disc import keystoneauth1.session import mock from oslo_config import cfg @@ -41,6 +42,13 @@ class IronicClientWrapperTestCase(test.NoDBTestCase): self.ironicclient = client_wrapper.IronicClientWrapper() # Do not waste time sleeping cfg.CONF.set_override('api_retry_interval', 0, 'ironic') + get_ksa_adapter_p = mock.patch('nova.utils.get_ksa_adapter') + self.addCleanup(get_ksa_adapter_p.stop) + self.get_ksa_adapter = get_ksa_adapter_p.start() + get_auth_plugin_p = mock.patch('nova.virt.ironic.client_wrapper.' + 'IronicClientWrapper._get_auth_plugin') + self.addCleanup(get_auth_plugin_p.stop) + self.get_auth_plugin = get_auth_plugin_p.start() @mock.patch.object(client_wrapper.IronicClientWrapper, '_multi_getattr') @mock.patch.object(client_wrapper.IronicClientWrapper, '_get_client') @@ -69,6 +77,38 @@ class IronicClientWrapperTestCase(test.NoDBTestCase): ironicclient = client_wrapper.IronicClientWrapper() # dummy call to have _get_client() called ironicclient.call("node.list") + # With no api_endpoint in the conf, ironic_url is retrieved from + # nova.utils.get_ksa_adapter().get_endpoint() + self.get_ksa_adapter.assert_called_once_with( + 'baremetal', ksa_auth=self.get_auth_plugin.return_value, + ksa_session='session', min_version=(1, 32), + max_version=(1, ksa_disc.LATEST)) + expected = {'session': 'session', + 'max_retries': CONF.ironic.api_max_retries, + 'retry_interval': CONF.ironic.api_retry_interval, + 'os_ironic_api_version': '1.32', + 'ironic_url': + self.get_ksa_adapter.return_value.get_endpoint.return_value} + mock_ir_cli.assert_called_once_with(1, **expected) + + @mock.patch.object(keystoneauth1.session, 'Session') + @mock.patch.object(ironic_client, 'get_client') + def test__get_client_session_service_not_found(self, mock_ir_cli, + mock_session): + """Validate behavior when get_endpoint_data raises.""" + mock_session.return_value = 'session' + self.get_ksa_adapter.side_effect = ( + exception.ConfGroupForServiceTypeNotFound(stype='baremetal')) + ironicclient = client_wrapper.IronicClientWrapper() + # dummy call to have _get_client() called + ironicclient.call("node.list") + # With no api_endpoint in the conf, ironic_url is retrieved from + # nova.utils.get_endpoint_data + self.get_ksa_adapter.assert_called_once_with( + 'baremetal', ksa_auth=self.get_auth_plugin.return_value, + ksa_session='session', min_version=(1, 32), + max_version=(1, ksa_disc.LATEST)) + # When get_endpoint_data raises any ServiceNotFound, None is returned. expected = {'session': 'session', 'max_retries': CONF.ironic.api_max_retries, 'retry_interval': CONF.ironic.api_retry_interval, @@ -76,6 +116,24 @@ class IronicClientWrapperTestCase(test.NoDBTestCase): 'ironic_url': None} mock_ir_cli.assert_called_once_with(1, **expected) + @mock.patch.object(keystoneauth1.session, 'Session') + @mock.patch.object(ironic_client, 'get_client') + def test__get_client_session_legacy(self, mock_ir_cli, mock_session): + """Endpoint discovery via legacy api_endpoint conf option.""" + mock_session.return_value = 'session' + endpoint = 'https://baremetal.example.com/endpoint' + self.flags(api_endpoint=endpoint, group='ironic') + ironicclient = client_wrapper.IronicClientWrapper() + # dummy call to have _get_client() called + ironicclient.call("node.list") + self.get_ksa_adapter.assert_not_called() + expected = {'session': 'session', + 'max_retries': CONF.ironic.api_max_retries, + 'retry_interval': CONF.ironic.api_retry_interval, + 'os_ironic_api_version': '1.32', + 'ironic_url': endpoint} + mock_ir_cli.assert_called_once_with(1, **expected) + @mock.patch.object(client_wrapper.IronicClientWrapper, '_multi_getattr') @mock.patch.object(client_wrapper.IronicClientWrapper, '_get_client') def test_call_fail_exception(self, mock_get_client, mock_multi_getattr): diff --git a/nova/utils.py b/nova/utils.py index 7a3209b19355..7653a5fc79c5 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -33,7 +33,9 @@ import tempfile import time import eventlet +from keystoneauth1 import loading as ks_loading import netaddr +from os_service_types import service_types from oslo_concurrency import lockutils from oslo_concurrency import processutils from oslo_context import context as common_context @@ -87,6 +89,8 @@ VIM_IMAGE_ATTRIBUTES = ( _FILE_CACHE = {} +_SERVICE_TYPES = service_types.ServiceTypes() + def get_root_helper(): if CONF.workarounds.disable_rootwrap: @@ -1262,3 +1266,57 @@ def isotime(at=None): def strtime(at): return at.strftime("%Y-%m-%dT%H:%M:%S.%f") + + +def get_ksa_adapter(service_type, ksa_auth=None, ksa_session=None, + min_version=None, max_version=None): + """Construct a keystoneauth1 Adapter for a given service type. + + We expect to find a conf group whose name corresponds to the service_type's + project according to the service-types-authority. That conf group must + provide at least ksa adapter options. Depending how the result is to be + used, ksa auth and/or session options may also be required, or the relevant + parameter supplied. + + :param service_type: String name of the service type for which the Adapter + is to be constructed. + :param ksa_auth: A keystoneauth1 auth plugin. If not specified, we attempt + to find one in ksa_session. Failing that, we attempt to + load one from the conf. + :param ksa_session: A keystoneauth1 Session. If not specified, we attempt + to load one from the conf. + :param min_version: The minimum major version of the adapter's endpoint, + intended to be used as the lower bound of a range with + max_version. + If min_version is given with no max_version it is as + if max version is 'latest'. + :param max_version: The maximum major version of the adapter's endpoint, + intended to be used as the upper bound of a range with + min_version. + :return: A keystoneauth1 Adapter object for the specified service_type. + :raise: ConfGroupForServiceTypeNotFound If no conf group name could be + found for the specified service_type. This should be considered a + bug. + """ + # Get the conf group corresponding to the service type. + confgrp = _SERVICE_TYPES.get_project_name(service_type) + if not confgrp: + raise exception.ConfGroupForServiceTypeNotFound(stype=service_type) + + # Ensure we have an auth. + # NOTE(efried): This could be None, and that could be okay - e.g. if the + # result is being used for get_endpoint() and the conf only contains + # endpoint_override. + if not ksa_auth: + if ksa_session and ksa_session.auth: + ksa_auth = ksa_session.auth + else: + ksa_auth = ks_loading.load_auth_from_conf_options(CONF, confgrp) + + if not ksa_session: + ksa_session = ks_loading.load_session_from_conf_options( + CONF, confgrp, auth=ksa_auth) + + return ks_loading.load_adapter_from_conf_options( + CONF, confgrp, session=ksa_session, auth=ksa_auth, + min_version=min_version, max_version=max_version) diff --git a/nova/virt/ironic/client_wrapper.py b/nova/virt/ironic/client_wrapper.py index 9453adfcf27d..184ca84d732b 100644 --- a/nova/virt/ironic/client_wrapper.py +++ b/nova/virt/ironic/client_wrapper.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneauth1 import discover as ks_disc from keystoneauth1 import loading as ks_loading from oslo_log import log as logging from oslo_utils import importutils @@ -20,6 +21,7 @@ from oslo_utils import importutils import nova.conf from nova import exception from nova.i18n import _ +from nova import utils LOG = logging.getLogger(__name__) @@ -89,10 +91,26 @@ class IronicClientWrapper(object): kwargs['retry_interval'] = retry_interval kwargs['os_ironic_api_version'] = '%d.%d' % IRONIC_API_VERSION - # NOTE(clenimar): by default, the endpoint is taken from the service - # catalog. Use `api_endpoint` if you want to override it. - ironic_url = (CONF.ironic.api_endpoint - if CONF.ironic.api_endpoint else None) + # NOTE(clenimar/efried): by default, the endpoint is taken from the + # service catalog. Use `endpoint_override` if you want to override it. + if CONF.ironic.api_endpoint: + # NOTE(efried): `api_endpoint` still overrides service catalog and + # `endpoint_override` conf options. This will be removed in a + # future release. + ironic_url = CONF.ironic.api_endpoint + else: + try: + ksa_adap = utils.get_ksa_adapter( + nova.conf.ironic.DEFAULT_SERVICE_TYPE, + ksa_auth=auth_plugin, ksa_session=sess, + min_version=IRONIC_API_VERSION, + max_version=(IRONIC_API_VERSION[0], ks_disc.LATEST)) + ironic_url = ksa_adap.get_endpoint() + except exception.ServiceNotFound: + # NOTE(efried): No reason to believe service catalog lookup + # won't also fail in ironic client init, but this way will + # yield the expected exception/behavior. + ironic_url = None try: cli = ironic.client.get_client(IRONIC_API_VERSION[0], diff --git a/releasenotes/notes/glance-via-ksa-5646eb3d5db51c54.yaml b/releasenotes/notes/glance-via-ksa-5646eb3d5db51c54.yaml new file mode 100644 index 000000000000..1039b6d7199b --- /dev/null +++ b/releasenotes/notes/glance-via-ksa-5646eb3d5db51c54.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + Nova now uses keystoneauth1 configuration to set up communication with the + image service. Use keystoneauth1 loading parameters for auth, Session, and + Adapter setup in the ``[glance]`` conf section. This includes using + ``endpoint_override`` in favor of ``api_servers``. The + ``[glance]api_servers`` conf option is still supported, but should only be + used if you need multiple endpoints and are unable to use a load balancer + for some reason. diff --git a/releasenotes/notes/ironic-via-ksa-deffd3dac48ff4eb.yaml b/releasenotes/notes/ironic-via-ksa-deffd3dac48ff4eb.yaml new file mode 100644 index 000000000000..35138fff9258 --- /dev/null +++ b/releasenotes/notes/ironic-via-ksa-deffd3dac48ff4eb.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - | + Nova now uses keystoneauth1 configuration to set up communication with the + baremetal service. Use keystoneauth1 loading parameters for auth, Session, + and Adapter setup in the ``[ironic]`` conf section. This includes using + ``endpoint_override`` in favor of ``api_endpoint``. +deprecations: + - | + Configuration option ``[ironic]api_endpoint`` is deprecated in favor of + ``[ironic]endpoint_override``. diff --git a/requirements.txt b/requirements.txt index 69beefa1b769..699d078e9715 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,3 +62,4 @@ os-xenapi>=0.3.1 # Apache-2.0 tooz>=1.58.0 # Apache-2.0 cursive>=0.1.2 # Apache-2.0 pypowervm>=1.1.7 # Apache-2.0 +os-service-types>=1.1.0 # Apache-2.0