Modernize the nova client in cinder

The nova client (used by the InstanceLocalityFilter for example)
seems to have obscure config options (compared to other projects),
and seems it is buggy too. Fix this by introducing a [nova] section,
where the usual auth parameters can be put (auth_type, auth_url,
username, password, etc...), and deprecate the old options.
Also doesn't play with the Service Catalog, the authentication plugins
can handle it, use a Token authentication plugin when using
the user context, and removed the separate usage of the admin endpoint.

Change-Id: I55613793c8f525a36ac74636f47d7ab76f5c7e39
Closes-bug: #1686616
DocImpact: to use a Nova connection (e.g. for InstanceLocalityFilter),
one has to configure the [nova] section.
This commit is contained in:
Gyorgy Szombathelyi 2017-04-27 17:25:49 +02:00
parent 0193bd6c30
commit da93e11794
6 changed files with 192 additions and 215 deletions

View File

@ -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)

View File

@ -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: '
'<service_type>:<service_name>:<endpoint_type>'),
'<service_type>:<service_name>:<endpoint_type>',
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(

View File

@ -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,
)),
]

View File

@ -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).
"""

View File

@ -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',

View File

@ -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)