diff --git a/cinder/common/config.py b/cinder/common/config.py index ff5074f0c6f..334fd93e00d 100644 --- a/cinder/common/config.py +++ b/cinder/common/config.py @@ -186,17 +186,33 @@ global_opts = [ cfg.StrOpt('os_privileged_user_name', help='OpenStack privileged account username. Used for requests ' 'to other services (such as Nova) that require an account ' - 'with special rights.'), + 'with special rights.', + deprecated_for_removal=True, + deprecated_since="11.0.0", + deprecated_reason='Use the [nova] section for configuring ' + 'Keystone authentication for a privileged user.'), cfg.StrOpt('os_privileged_user_password', help='Password associated with the OpenStack privileged ' 'account.', + deprecated_for_removal=True, + deprecated_since="11.0.0", + deprecated_reason='Use the [nova] section to configure ' + 'Keystone authentication for a privileged user.', secret=True), cfg.StrOpt('os_privileged_user_tenant', help='Tenant name associated with the OpenStack privileged ' - 'account.'), + 'account.', + deprecated_for_removal=True, + deprecated_since="11.0.0", + deprecated_reason='Use the [nova] section to configure ' + 'Keystone authentication for a privileged user.'), cfg.URIOpt('os_privileged_user_auth_url', help='Auth URL associated with the OpenStack privileged ' - 'account.'), + 'account.', + deprecated_for_removal=True, + deprecated_since="11.0.0", + deprecated_reason='Use the [nova] section to configure ' + 'Keystone authentication for a privileged user.') ] CONF.register_opts(core_opts) diff --git a/cinder/compute/nova.py b/cinder/compute/nova.py index 5290d5ca2e3..7edecfd0dc4 100644 --- a/cinder/compute/nova.py +++ b/cinder/compute/nova.py @@ -17,7 +17,7 @@ Handles all requests to Nova. """ from keystoneauth1 import identity -from keystoneauth1 import session as ka_session +from keystoneauth1 import loading as ks_loading from novaclient import api_versions from novaclient import client as nova_client from novaclient import exceptions as nova_exceptions @@ -25,37 +25,61 @@ from oslo_config import cfg from oslo_log import log as logging from requests import exceptions as request_exceptions -from cinder import context as ctx from cinder.db import base from cinder import exception -nova_opts = [ +old_opts = [ cfg.StrOpt('nova_catalog_info', default='compute:Compute Service:publicURL', help='Match this value when searching for nova in the ' 'service catalog. Format is: separated values of ' 'the form: ' - '::'), + '::', + deprecated_for_removal=True), cfg.StrOpt('nova_catalog_admin_info', default='compute:Compute Service:publicURL', - help='Same as nova_catalog_info, but for admin endpoint.'), + help='Same as nova_catalog_info, but for admin endpoint.', + deprecated_for_removal=True), cfg.StrOpt('nova_endpoint_template', help='Override service catalog lookup with template for nova ' - 'endpoint e.g. http://localhost:8774/v2/%(project_id)s'), + 'endpoint e.g. http://localhost:8774/v2/%(project_id)s', + deprecated_for_removal=True), cfg.StrOpt('nova_endpoint_admin_template', - help='Same as nova_endpoint_template, but for admin endpoint.'), - cfg.StrOpt('os_region_name', - help='Region name of this node'), - cfg.StrOpt('nova_ca_certificates_file', - help='Location of ca certificates file to use for nova client ' - 'requests.'), - cfg.BoolOpt('nova_api_insecure', - default=False, - help='Allow to perform insecure SSL requests to nova'), + help='Same as nova_endpoint_template, but for admin endpoint.', + deprecated_for_removal=True), ] +nova_opts = [ + cfg.StrOpt('region_name', + help='Name of nova region to use. Useful if keystone manages ' + 'more than one region.', + deprecated_name="os_region_name", + deprecated_group="DEFAULT"), + cfg.StrOpt('interface', + default='public', + choices=['public', 'admin', 'internal'], + help='Type of the nova endpoint to use. This endpoint will ' + 'be looked up in the keystone catalog and should be ' + 'one of public, internal or admin.'), + cfg.StrOpt('token_auth_url', + help='The authentication URL for the nova connection when ' + 'using the current user''s token'), +] + + +NOVA_GROUP = 'nova' CONF = cfg.CONF -CONF.register_opts(nova_opts) + +deprecations = {'cafile': [cfg.DeprecatedOpt('nova_ca_certificates_file')], + 'insecure': [cfg.DeprecatedOpt('nova_api_insecure')]} +nova_session_opts = ks_loading.get_session_conf_options( + deprecated_opts=deprecations) +nova_auth_opts = ks_loading.get_auth_common_conf_options() + +CONF.register_opts(old_opts) +CONF.register_opts(nova_opts, group=NOVA_GROUP) +CONF.register_opts(nova_session_opts, group=NOVA_GROUP) +CONF.register_opts(nova_auth_opts, group=NOVA_GROUP) LOG = logging.getLogger(__name__) @@ -67,167 +91,54 @@ nova_extensions = [ext for ext in "list_extensions")] -# TODO(dmllr): This is a copy of the ServiceCatalog class in python-novaclient -# that got removed in 7.0.0 release. This needs to be cleaned up once we depend -# on newer novaclient. -class _NovaClientServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" - - def __init__(self, resource_dict): - self.catalog = resource_dict - - def url_for(self, attr=None, filter_value=None, - service_type=None, endpoint_type='publicURL', - service_name=None, volume_service_name=None): - """Fetch public URL for a particular endpoint. - - If none given, return the first. - See tests for sample service catalog. - """ - matching_endpoints = [] - if 'endpoints' in self.catalog: - # We have a bastardized service catalog. Treat it special. :/ - for endpoint in self.catalog['endpoints']: - if not filter_value or endpoint[attr] == filter_value: - # Ignore 1.0 compute endpoints - if endpoint.get("type") == 'compute' and \ - endpoint.get('versionId') in (None, '1.1', '2'): - matching_endpoints.append(endpoint) - if not matching_endpoints: - raise nova_exceptions.EndpointNotFound() - - # We don't always get a service catalog back ... - if 'serviceCatalog' not in self.catalog['access']: - return None - - # Full catalog ... - catalog = self.catalog['access']['serviceCatalog'] - - for service in catalog: - if service.get("type") != service_type: - continue - - if (service_name and service_type == 'compute' and - service.get('name') != service_name): - continue - - if (volume_service_name and service_type == 'volume' and - service.get('name') != volume_service_name): - continue - - endpoints = service['endpoints'] - for endpoint in endpoints: - # Ignore 1.0 compute endpoints - if (service.get("type") == 'compute' and - endpoint.get('versionId', '2') not in ('1.1', '2')): - continue - if (not filter_value or - endpoint.get(attr).lower() == filter_value.lower()): - endpoint["serviceName"] = service.get("name") - matching_endpoints.append(endpoint) - - if not matching_endpoints: - raise nova_exceptions.EndpointNotFound() - elif len(matching_endpoints) > 1: - raise nova_exceptions.AmbiguousEndpoints( - endpoints=matching_endpoints) - else: - return matching_endpoints[0][endpoint_type] - - -def novaclient(context, admin_endpoint=False, privileged_user=False, - timeout=None): +def novaclient(context, privileged_user=False, timeout=None): """Returns a Nova client - @param admin_endpoint: If True, use the admin endpoint template from - configuration ('nova_endpoint_admin_template' and 'nova_catalog_info') @param privileged_user: If True, use the account from configuration - (requires 'os_privileged_user_name', 'os_privileged_user_password' and - 'os_privileged_user_tenant' to be set) + (requires 'auth_type' and the other usual Keystone authentication + options to be set in the [nova] section) @param timeout: Number of seconds to wait for an answer before raising a Timeout exception (None to disable) """ - # FIXME: the novaclient ServiceCatalog object is mis-named. - # It actually contains the entire access blob. - # Only needed parts of the service catalog are passed in, see - # nova/context.py. - compat_catalog = { - 'access': {'serviceCatalog': context.service_catalog or []} - } - sc = _NovaClientServiceCatalog(compat_catalog) - nova_endpoint_template = CONF.nova_endpoint_template - nova_catalog_info = CONF.nova_catalog_info - - if admin_endpoint: - nova_endpoint_template = CONF.nova_endpoint_admin_template - nova_catalog_info = CONF.nova_catalog_admin_info - service_type, service_name, endpoint_type = nova_catalog_info.split(':') - - # Extract the region if set in configuration - if CONF.os_region_name: - region_filter = {'attr': 'region', 'filter_value': CONF.os_region_name} + if privileged_user and CONF[NOVA_GROUP].auth_type: + n_auth = ks_loading.load_auth_from_conf_options(CONF, NOVA_GROUP) else: - region_filter = {} - - if privileged_user and CONF.os_privileged_user_name: - context = ctx.RequestContext( - CONF.os_privileged_user_name, None, - auth_token=CONF.os_privileged_user_password, - project_name=CONF.os_privileged_user_tenant, - service_catalog=context.service_catalog) - - # When privileged_user is used, it needs to authenticate to Keystone - # before querying Nova, so we set auth_url to the identity service - # endpoint. - if CONF.os_privileged_user_auth_url: - url = CONF.os_privileged_user_auth_url + if CONF[NOVA_GROUP].token_auth_url: + url = CONF[NOVA_GROUP].token_auth_url else: - # We then pass region_name, endpoint_type, etc. to the - # Client() constructor so that the final endpoint is - # chosen correctly. - url = sc.url_for(service_type='identity', - endpoint_type=endpoint_type, - **region_filter) - - LOG.debug('Creating a Nova client using "%s" user', - CONF.os_privileged_user_name) - else: - if nova_endpoint_template: - url = nova_endpoint_template % context.to_dict() - else: - url = sc.url_for(service_type=service_type, - service_name=service_name, - endpoint_type=endpoint_type, - **region_filter) - - LOG.debug('Nova client connection created using URL: %s', url) - - # Now that we have the correct auth_url, username, password, project_name - # and domain information, i.e. project_domain_id and user_domain_id (if - # using Identity v3 API) let's build a Keystone session. - auth = identity.Password(auth_url=url, - username=context.user_id, - password=context.auth_token, - project_name=context.project_name, - project_domain_id=context.project_domain, - user_domain_id=context.user_domain) - keystone_session = ka_session.Session(auth=auth) + # Search for the identity endpoint in the service catalog + # if nova.token_auth_url is not configured + matching_endpoints = [] + for service in context.service_catalog: + if service.get('type') != 'identity': + continue + for endpoint in service['endpoints']: + if (not CONF[NOVA_GROUP].region_name or + endpoint.get('region') == + CONF[NOVA_GROUP].region_name): + matching_endpoints.append(endpoint) + if not matching_endpoints: + raise nova_exceptions.EndpointNotFound() + url = matching_endpoints[0].get(CONF[NOVA_GROUP].interface + 'URL') + n_auth = identity.Token(auth_url=url, + token=context.auth_token, + project_name=context.project_name, + project_domain_id=context.project_domain) + keystone_session = ks_loading.load_session_from_conf_options( + CONF, + NOVA_GROUP, + auth=n_auth) c = nova_client.Client(api_versions.APIVersion(NOVA_API_VERSION), session=keystone_session, - insecure=CONF.nova_api_insecure, + insecure=CONF[NOVA_GROUP].insecure, timeout=timeout, - region_name=CONF.os_region_name, - endpoint_type=endpoint_type, - cacert=CONF.nova_ca_certificates_file, + region_name=CONF[NOVA_GROUP].region_name, + endpoint_type=CONF[NOVA_GROUP].interface, + cacert=CONF[NOVA_GROUP].cafile, extensions=nova_extensions) - if not privileged_user: - # noauth extracts user_id:project_id from auth_token - c.client.auth_token = (context.auth_token or '%s:%s' - % (context.user_id, context.project_id)) - c.client.management_url = url return c @@ -243,13 +154,13 @@ class API(base.Base): def update_server_volume(self, context, server_id, attachment_id, new_volume_id): - nova = novaclient(context, admin_endpoint=True, privileged_user=True) + nova = novaclient(context, privileged_user=True) nova.volumes.update_server_volume(server_id, attachment_id, new_volume_id) def create_volume_snapshot(self, context, volume_id, create_info): - nova = novaclient(context, admin_endpoint=True, privileged_user=True) + nova = novaclient(context, privileged_user=True) # pylint: disable=E1101 nova.assisted_volume_snapshots.create( @@ -257,7 +168,7 @@ class API(base.Base): create_info=create_info) def delete_volume_snapshot(self, context, snapshot_id, delete_info): - nova = novaclient(context, admin_endpoint=True, privileged_user=True) + nova = novaclient(context, privileged_user=True) # pylint: disable=E1101 nova.assisted_volume_snapshots.delete( diff --git a/cinder/opts.py b/cinder/opts.py index 8e9dd58b9db..011bdbb9091 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -243,7 +243,7 @@ def list_opts(): cinder_common_config.core_opts, cinder_common_config.global_opts, cinder.compute.compute_opts, - cinder_compute_nova.nova_opts, + cinder_compute_nova.old_opts, cinder_context.context_opts, cinder_db_api.db_opts, [cinder_db_base.db_driver_opt], @@ -388,4 +388,10 @@ def list_opts(): itertools.chain( cinder_keymgr_confkeymgr.key_mgr_opts, )), + ('NOVA_GROUP', + itertools.chain( + cinder_compute_nova.nova_opts, + cinder_compute_nova.nova_session_opts, + cinder_compute_nova.nova_auth_opts, + )), ] diff --git a/cinder/scheduler/filters/instance_locality_filter.py b/cinder/scheduler/filters/instance_locality_filter.py index 652bd763c85..83ecafcec82 100644 --- a/cinder/scheduler/filters/instance_locality_filter.py +++ b/cinder/scheduler/filters/instance_locality_filter.py @@ -43,9 +43,9 @@ class InstanceLocalityFilter(filters.BaseBackendFilter): is by default), so that the 'OS-EXT-SRV-ATTR:host' property is returned when requesting instance info. - Either an account with privileged rights for Nova must be configured in - Cinder configuration (see 'os_privileged_user_name'), or the user making - the call needs to have sufficient rights (see - 'extended_server_attributes' in Nova policy). + Cinder configuration (configure a keystone authentication plugin in the + [nova] section), or the user making the call needs to have sufficient + rights (see 'extended_server_attributes' in Nova policy). """ diff --git a/cinder/tests/unit/compute/test_nova.py b/cinder/tests/unit/compute/test_nova.py index f9206abbb62..5ed1f8c2bc7 100644 --- a/cinder/tests/unit/compute/test_nova.py +++ b/cinder/tests/unit/compute/test_nova.py @@ -17,64 +17,86 @@ import mock from cinder.compute import nova from cinder import context from cinder import test +from keystoneauth1 import loading as ks_loading from novaclient import exceptions as nova_exceptions +from oslo_config import cfg + +CONF = cfg.CONF class NovaClientTestCase(test.TestCase): def setUp(self): super(NovaClientTestCase, self).setUp() + # Register the Password auth plugin options, + # so we can use CONF.set_override + # reset() first, otherwise already registered CLI options will + # prevent unregister in tearDown() + # Use CONF.set_override(), because we'll unregister the opts, + # no need (and not possible) to cleanup. + CONF.reset() + self.password_opts = \ + ks_loading.get_auth_plugin_conf_options('password') + CONF.register_opts(self.password_opts, group='nova') + CONF.set_override('auth_url', + 'http://keystonehost:5000', + group='nova') + CONF.set_override('username', 'adminuser', group='nova') + CONF.set_override('password', 'strongpassword', group='nova') self.ctx = context.RequestContext('regularuser', 'e3f0833dc08b4cea', auth_token='token', is_admin=False) self.ctx.service_catalog = \ [{'type': 'compute', 'name': 'nova', 'endpoints': [{'publicURL': 'http://novahost:8774/v2/e3f0833dc08b4cea'}]}, {'type': 'identity', 'name': 'keystone', 'endpoints': - [{'publicURL': 'http://keystonehost:5000/v2.0'}]}] + [{'publicURL': 'http://keystonehostfromsc:5000/v3'}]}] - self.override_config('nova_endpoint_template', - 'http://novahost:8774/v2/%(project_id)s') - self.override_config('nova_endpoint_admin_template', - 'http://novaadmhost:4778/v2/%(project_id)s') - self.override_config('nova_catalog_admin_info', - 'compute:Compute Service:adminURL') - self.override_config('os_privileged_user_name', 'adminuser') - self.override_config('os_privileged_user_password', 'strongpassword') + self.override_config('auth_type', 'password', group='nova') + self.override_config('cafile', 'my.ca', group='nova') + + def tearDown(self): + super(NovaClientTestCase, self).tearDown() + + CONF.unregister_opts(self.password_opts, group='nova') @mock.patch('novaclient.api_versions.APIVersion') @mock.patch('novaclient.client.Client') - @mock.patch('keystoneauth1.identity.Password') + @mock.patch('keystoneauth1.identity.Token') @mock.patch('keystoneauth1.session.Session') - def test_nova_client_regular(self, p_session, p_password_plugin, p_client, + def test_nova_client_regular(self, p_session, p_token_plugin, p_client, p_api_version): + + self.override_config('token_auth_url', + 'http://keystonehost:5000', + group='nova') nova.novaclient(self.ctx) - p_password_plugin.assert_called_once_with( - auth_url='http://novahost:8774/v2/e3f0833dc08b4cea', - password='token', project_name=None, username='regularuser', - project_domain_id=None, user_domain_id=None + p_token_plugin.assert_called_once_with( + auth_url='http://keystonehost:5000', + token='token', project_name=None, project_domain_id=None ) p_client.assert_called_once_with( p_api_version(nova.NOVA_API_VERSION), session=p_session.return_value, region_name=None, - insecure=False, endpoint_type='publicURL', cacert=None, + insecure=False, endpoint_type='public', cacert='my.ca', timeout=None, extensions=nova.nova_extensions) @mock.patch('novaclient.api_versions.APIVersion') @mock.patch('novaclient.client.Client') - @mock.patch('keystoneauth1.identity.Password') + @mock.patch('keystoneauth1.identity.Token') @mock.patch('keystoneauth1.session.Session') - def test_nova_client_admin_endpoint(self, p_session, p_password_plugin, - p_client, p_api_version): - nova.novaclient(self.ctx, admin_endpoint=True) - p_password_plugin.assert_called_once_with( - auth_url='http://novaadmhost:4778/v2/e3f0833dc08b4cea', - password='token', project_name=None, username='regularuser', - project_domain_id=None, user_domain_id=None + def test_nova_client_regular_service_catalog(self, p_session, + p_token_plugin, p_client, + p_api_version): + + nova.novaclient(self.ctx) + p_token_plugin.assert_called_once_with( + auth_url='http://keystonehostfromsc:5000/v3', + token='token', project_name=None, project_domain_id=None ) p_client.assert_called_once_with( p_api_version(nova.NOVA_API_VERSION), session=p_session.return_value, region_name=None, - insecure=False, endpoint_type='adminURL', cacert=None, + insecure=False, endpoint_type='public', cacert='my.ca', timeout=None, extensions=nova.nova_extensions) @mock.patch('novaclient.api_versions.APIVersion') @@ -83,16 +105,20 @@ class NovaClientTestCase(test.TestCase): @mock.patch('keystoneauth1.session.Session') def test_nova_client_privileged_user(self, p_session, p_password_plugin, p_client, p_api_version): + nova.novaclient(self.ctx, privileged_user=True) p_password_plugin.assert_called_once_with( - auth_url='http://keystonehost:5000/v2.0', - password='strongpassword', project_name=None, username='adminuser', - project_domain_id=None, user_domain_id=None + auth_url='http://keystonehost:5000', default_domain_id=None, + default_domain_name=None, domain_id=None, domain_name=None, + password='strongpassword', project_domain_id=None, + project_domain_name=None, project_id=None, project_name=None, + trust_id=None, user_domain_id=None, user_domain_name=None, + user_id=None, username='adminuser' ) p_client.assert_called_once_with( p_api_version(nova.NOVA_API_VERSION), session=p_session.return_value, region_name=None, - insecure=False, endpoint_type='publicURL', cacert=None, + insecure=False, endpoint_type='public', cacert='my.ca', timeout=None, extensions=nova.nova_extensions) @mock.patch('novaclient.api_versions.APIVersion') @@ -103,18 +129,23 @@ class NovaClientTestCase(test.TestCase): p_password_plugin, p_client, p_api_version): - self.override_config('os_privileged_user_auth_url', - 'http://privatekeystonehost:5000/v2.0') + + CONF.set_override('auth_url', + 'http://privatekeystonehost:5000', + group='nova') nova.novaclient(self.ctx, privileged_user=True) p_password_plugin.assert_called_once_with( - auth_url='http://privatekeystonehost:5000/v2.0', - password='strongpassword', project_name=None, username='adminuser', - project_domain_id=None, user_domain_id=None + auth_url='http://privatekeystonehost:5000', default_domain_id=None, + default_domain_name=None, domain_id=None, domain_name=None, + password='strongpassword', project_domain_id=None, + project_domain_name=None, project_id=None, project_name=None, + trust_id=None, user_domain_id=None, user_domain_name=None, + user_id=None, username='adminuser' ) p_client.assert_called_once_with( p_api_version(nova.NOVA_API_VERSION), session=p_session.return_value, region_name=None, - insecure=False, endpoint_type='publicURL', cacert=None, + insecure=False, endpoint_type='public', cacert='my.ca', timeout=None, extensions=nova.nova_extensions) @mock.patch('novaclient.api_versions.APIVersion') @@ -123,17 +154,21 @@ class NovaClientTestCase(test.TestCase): @mock.patch('keystoneauth1.session.Session') def test_nova_client_custom_region(self, p_session, p_password_plugin, p_client, p_api_version): - self.override_config('os_region_name', 'farfaraway') - nova.novaclient(self.ctx) + + CONF.set_override('region_name', 'farfaraway', group='nova') + nova.novaclient(self.ctx, privileged_user=True) p_password_plugin.assert_called_once_with( - auth_url='http://novahost:8774/v2/e3f0833dc08b4cea', - password='token', project_name=None, username='regularuser', - project_domain_id=None, user_domain_id=None + auth_url='http://keystonehost:5000', default_domain_id=None, + default_domain_name=None, domain_id=None, domain_name=None, + password='strongpassword', project_domain_id=None, + project_domain_name=None, project_id=None, project_name=None, + trust_id=None, user_domain_id=None, user_domain_name=None, + user_id=None, username='adminuser' ) p_client.assert_called_once_with( p_api_version(nova.NOVA_API_VERSION), session=p_session.return_value, region_name='farfaraway', - insecure=False, endpoint_type='publicURL', cacert=None, + insecure=False, endpoint_type='public', cacert='my.ca', timeout=None, extensions=nova.nova_extensions) def test_novaclient_exceptions(self): @@ -141,7 +176,6 @@ class NovaClientTestCase(test.TestCase): # removed from novaclient since the service catalog # code does not have thorough tests. self.assertTrue(hasattr(nova_exceptions, 'EndpointNotFound')) - self.assertTrue(hasattr(nova_exceptions, 'AmbiguousEndpoints')) class FakeNovaClient(object): @@ -178,7 +212,6 @@ class NovaApiTestCase(test.TestCase): 'attach_id', 'new_volume_id') mock_novaclient.assert_called_once_with(self.ctx, - admin_endpoint=True, privileged_user=True) mock_update_server_volume.assert_called_once_with( 'server_id', diff --git a/releasenotes/notes/new-nova-config-section-2a7a51a0572e7064.yaml b/releasenotes/notes/new-nova-config-section-2a7a51a0572e7064.yaml new file mode 100644 index 00000000000..dc91f1ae498 --- /dev/null +++ b/releasenotes/notes/new-nova-config-section-2a7a51a0572e7064.yaml @@ -0,0 +1,11 @@ +--- +features: + - a [nova] section is added to configure the connection + to the compute service, which is needed to the + InstanceLocalityFilter, for example. +deprecations: + - The os_privileged_xxx and nova_xxx in the [default] + section are deprecated in favor of the settings in + the [nova] section. +fixes: + - Fixed using of the user's token in the nova client (bug #1686616)