From 35f332539dc6a9b61c60b4314347ff0b2e7177a2 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 18 Mar 2016 13:32:41 +0200 Subject: [PATCH] Use keystoneauth for Ironic and Swift clients This patch does not change the options in config file yet to showcase backward compatibility with old config options. Change-Id: I1da93b59b2f4813c42008277bd6479dc6673e7f1 --- example.conf | 258 +++++++++++++++--- ironic_inspector/common/ironic.py | 104 ++++--- ironic_inspector/common/keystone.py | 129 +++++++++ ironic_inspector/common/swift.py | 122 ++++++--- ironic_inspector/main.py | 1 - ironic_inspector/test/test_common_ironic.py | 49 ++-- ironic_inspector/test/test_keystone.py | 115 ++++++++ ironic_inspector/test/test_swift.py | 111 +++----- ...keystoneauth-plugins-aab6cbe1d0e884bf.yaml | 17 ++ requirements.txt | 2 +- 10 files changed, 687 insertions(+), 221 deletions(-) create mode 100644 ironic_inspector/common/keystone.py create mode 100644 ironic_inspector/test/test_keystone.py create mode 100644 releasenotes/notes/keystoneauth-plugins-aab6cbe1d0e884bf.yaml diff --git a/example.conf b/example.conf index c1291b93b..7905ff42f 100644 --- a/example.conf +++ b/example.conf @@ -387,59 +387,149 @@ # From ironic_inspector.common.ironic # -# Keystone authentication endpoint for accessing Ironic API. Use -# [keystone_authtoken]/auth_uri for keystone authentication. (string -# value) -# Deprecated group/name - [discoverd]/os_auth_url -#os_auth_url = +# Authentication URL (unknown value) +#auth_url = -# User name for accessing Ironic API. Use -# [keystone_authtoken]/admin_user for keystone authentication. (string -# value) -# Deprecated group/name - [discoverd]/os_username -#os_username = +# Method to use for authentication: noauth or keystone. (string value) +# Allowed values: keystone, noauth +#auth_strategy = keystone -# Password for accessing Ironic API. Use -# [keystone_authtoken]/admin_password for keystone authentication. -# (string value) -# Deprecated group/name - [discoverd]/os_password -#os_password = +# Authentication type to load (unknown value) +# Deprecated group/name - [DEFAULT]/auth_plugin +#auth_type = -# Tenant name for accessing Ironic API. Use -# [keystone_authtoken]/admin_tenant_name for keystone authentication. -# (string value) -# Deprecated group/name - [discoverd]/os_tenant_name -#os_tenant_name = +# PEM encoded Certificate Authority to use when verifying HTTPs +# connections. (string value) +#cafile = -# Keystone admin endpoint. DEPRECATED: use -# [keystone_authtoken]/identity_uri. (string value) +# PEM encoded client certificate cert file (string value) +#certfile = + +# Optional domain ID to use with v3 and v2 parameters. It will be used +# for both the user and project domain in v3 and ignored in v2 +# authentication. (unknown value) +#default_domain_id = + +# Optional domain name to use with v3 API and v2 parameters. It will +# be used for both the user and project domain in v3 and ignored in v2 +# authentication. (unknown value) +#default_domain_name = + +# Domain ID to scope to (unknown value) +#domain_id = + +# Domain name to scope to (unknown value) +#domain_name = + +# Keystone admin endpoint. DEPRECATED: Use [keystone_authtoken] +# section for keystone token validation. (string value) # Deprecated group/name - [discoverd]/identity_uri # This option is deprecated for removal. # Its value may be silently ignored in the future. #identity_uri = -# Method to use for authentication: noauth or keystone. (string value) -# Allowed values: keystone, noauth -#auth_strategy = keystone +# Verify HTTPS connections. (boolean value) +#insecure = false # Ironic API URL, used to set Ironic API URL when auth_strategy option # is noauth to work with standalone Ironic without keystone. (string # value) #ironic_url = http://localhost:6385/ -# Ironic service type. (string value) -#os_service_type = baremetal +# PEM encoded client certificate key file (string value) +#keyfile = + +# Maximum number of retries in case of conflict error (HTTP 409). +# (integer value) +#max_retries = 30 + +# Keystone authentication endpoint for accessing Ironic API. Use +# [keystone_authtoken] section for keystone token validation. (string +# value) +# Deprecated group/name - [discoverd]/os_auth_url +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Use options presented by configured keystone auth plugin. +#os_auth_url = # Ironic endpoint type. (string value) #os_endpoint_type = internalURL +# Password for accessing Ironic API. Use [keystone_authtoken] section +# for keystone token validation. (string value) +# Deprecated group/name - [discoverd]/os_password +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Use options presented by configured keystone auth plugin. +#os_password = + +# Keystone region used to get Ironic endpoints. (string value) +#os_region = + +# Ironic service type. (string value) +#os_service_type = baremetal + +# Tenant name for accessing Ironic API. Use [keystone_authtoken] +# section for keystone token validation. (string value) +# Deprecated group/name - [discoverd]/os_tenant_name +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Use options presented by configured keystone auth plugin. +#os_tenant_name = + +# User name for accessing Ironic API. Use [keystone_authtoken] section +# for keystone token validation. (string value) +# Deprecated group/name - [discoverd]/os_username +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Use options presented by configured keystone auth plugin. +#os_username = + +# User's password (unknown value) +#password = + +# Domain ID containing project (unknown value) +#project_domain_id = + +# Domain name containing project (unknown value) +#project_domain_name = + +# Project ID to scope to (unknown value) +# Deprecated group/name - [DEFAULT]/tenant-id +#project_id = + +# Project name to scope to (unknown value) +# Deprecated group/name - [DEFAULT]/tenant-name +#project_name = + # Interval between retries in case of conflict error (HTTP 409). # (integer value) #retry_interval = 2 -# Maximum number of retries in case of conflict error (HTTP 409). -# (integer value) -#max_retries = 30 +# Tenant ID (unknown value) +#tenant_id = + +# Tenant Name (unknown value) +#tenant_name = + +# Timeout value for http requests (integer value) +#timeout = + +# Trust ID (unknown value) +#trust_id = + +# User's domain id (unknown value) +#user_domain_id = + +# User's domain name (unknown value) +#user_domain_name = + +# User id (unknown value) +#user_id = + +# Username (unknown value) +# Deprecated group/name - [DEFAULT]/username +#username = [keystone_authtoken] @@ -676,34 +766,112 @@ # From ironic_inspector.common.swift # -# Maximum number of times to retry a Swift request, before failing. -# (integer value) -#max_retries = 2 +# Authentication URL (unknown value) +#auth_url = + +# Authentication type to load (unknown value) +# Deprecated group/name - [DEFAULT]/auth_plugin +#auth_type = + +# PEM encoded Certificate Authority to use when verifying HTTPs +# connections. (string value) +#cafile = + +# PEM encoded client certificate cert file (string value) +#certfile = + +# Default Swift container to use when creating objects. (string value) +#container = ironic-inspector + +# Optional domain ID to use with v3 and v2 parameters. It will be used +# for both the user and project domain in v3 and ignored in v2 +# authentication. (unknown value) +#default_domain_id = + +# Optional domain name to use with v3 API and v2 parameters. It will +# be used for both the user and project domain in v3 and ignored in v2 +# authentication. (unknown value) +#default_domain_name = # Number of seconds that the Swift object will last before being # deleted. (set to 0 to never delete the object). (integer value) #delete_after = 0 -# Default Swift container to use when creating objects. (string value) -#container = ironic-inspector +# Domain ID to scope to (unknown value) +#domain_id = -# User name for accessing Swift API. (string value) -#username = +# Domain name to scope to (unknown value) +#domain_name = -# Password for accessing Swift API. (string value) -#password = +# Verify HTTPS connections. (boolean value) +#insecure = false -# Tenant name for accessing Swift API. (string value) -#tenant_name = +# PEM encoded client certificate key file (string value) +#keyfile = -# Keystone authentication API version (string value) -#os_auth_version = 2 +# Maximum number of times to retry a Swift request, before failing. +# (integer value) +#max_retries = 2 # Keystone authentication URL (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Use options presented by configured keystone auth plugin. #os_auth_url = +# Keystone authentication API version (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +# Reason: Use options presented by configured keystone auth plugin. +#os_auth_version = 2 + +# Swift endpoint type. (string value) +#os_endpoint_type = internalURL + +# Keystone region to get endpoint for. (string value) +#os_region = + # Swift service type. (string value) #os_service_type = object-store -# Swift endpoint type. (string value) -#os_endpoint_type = internalURL +# User's password (unknown value) +#password = + +# Domain ID containing project (unknown value) +#project_domain_id = + +# Domain name containing project (unknown value) +#project_domain_name = + +# Project ID to scope to (unknown value) +# Deprecated group/name - [DEFAULT]/tenant-id +#project_id = + +# Project name to scope to (unknown value) +# Deprecated group/name - [DEFAULT]/tenant-name +#project_name = + +# Tenant ID (unknown value) +#tenant_id = + +# Tenant Name (unknown value) +#tenant_name = + +# Timeout value for http requests (integer value) +#timeout = + +# Trust ID (unknown value) +#trust_id = + +# User's domain id (unknown value) +#user_domain_id = + +# User's domain name (unknown value) +#user_domain_name = + +# User id (unknown value) +#user_id = + +# Username (unknown value) +# Deprecated group/name - [DEFAULT]/username +#username = diff --git a/ironic_inspector/common/ironic.py b/ironic_inspector/common/ironic.py index 4734c4030..131be0010 100644 --- a/ironic_inspector/common/ironic.py +++ b/ironic_inspector/common/ironic.py @@ -14,10 +14,10 @@ import socket from ironicclient import client -from keystoneclient import client as keystone_client from oslo_config import cfg from ironic_inspector.common.i18n import _ +from ironic_inspector.common import keystone from ironic_inspector import utils CONF = cfg.CONF @@ -32,35 +32,50 @@ DEFAULT_IRONIC_API_VERSION = '1.11' IRONIC_GROUP = 'ironic' IRONIC_OPTS = [ + cfg.StrOpt('os_region', + help='Keystone region used to get Ironic endpoints.'), cfg.StrOpt('os_auth_url', default='', help='Keystone authentication endpoint for accessing Ironic ' - 'API. Use [keystone_authtoken]/auth_uri for keystone ' - 'authentication.', - deprecated_group='discoverd'), + 'API. Use [keystone_authtoken] section for keystone ' + 'token validation.', + deprecated_group='discoverd', + deprecated_for_removal=True, + deprecated_reason='Use options presented by configured ' + 'keystone auth plugin.'), cfg.StrOpt('os_username', default='', help='User name for accessing Ironic API. ' - 'Use [keystone_authtoken]/admin_user for keystone ' - 'authentication.', - deprecated_group='discoverd'), + 'Use [keystone_authtoken] section for keystone ' + 'token validation.', + deprecated_group='discoverd', + deprecated_for_removal=True, + deprecated_reason='Use options presented by configured ' + 'keystone auth plugin.'), cfg.StrOpt('os_password', default='', help='Password for accessing Ironic API. ' - 'Use [keystone_authtoken]/admin_password for keystone ' - 'authentication.', + 'Use [keystone_authtoken] section for keystone ' + 'token validation.', secret=True, - deprecated_group='discoverd'), + deprecated_group='discoverd', + deprecated_for_removal=True, + deprecated_reason='Use options presented by configured ' + 'keystone auth plugin.'), cfg.StrOpt('os_tenant_name', default='', help='Tenant name for accessing Ironic API. ' - 'Use [keystone_authtoken]/admin_tenant_name for keystone ' - 'authentication.', - deprecated_group='discoverd'), + 'Use [keystone_authtoken] section for keystone ' + 'token validation.', + deprecated_group='discoverd', + deprecated_for_removal=True, + deprecated_reason='Use options presented by configured ' + 'keystone auth plugin.'), cfg.StrOpt('identity_uri', default='', help='Keystone admin endpoint. ' - 'DEPRECATED: use [keystone_authtoken]/identity_uri.', + 'DEPRECATED: Use [keystone_authtoken] section for ' + 'keystone token validation.', deprecated_group='discoverd', deprecated_for_removal=True), cfg.StrOpt('auth_strategy', @@ -90,6 +105,24 @@ IRONIC_OPTS = [ CONF.register_opts(IRONIC_OPTS, group=IRONIC_GROUP) +keystone.register_auth_opts(IRONIC_GROUP) + +IRONIC_SESSION = None +LEGACY_MAP = { + 'auth_url': 'os_auth_url', + 'username': 'os_username', + 'password': 'os_password', + 'tenant_name': 'os_tenant_name' +} + + +def reset_ironic_session(): + """Reset the global session variable. + + Mostly useful for unit tests. + """ + global IRONIC_SESSION + IRONIC_SESSION = None def get_ipmi_address(node): @@ -114,33 +147,28 @@ def get_client(token=None, """Get Ironic client instance.""" # NOTE: To support standalone ironic without keystone if CONF.ironic.auth_strategy == 'noauth': - args = {'os_auth_token': 'noauth', - 'ironic_url': CONF.ironic.ironic_url} - elif token is None: - args = {'os_password': CONF.ironic.os_password, - 'os_username': CONF.ironic.os_username, - 'os_auth_url': CONF.ironic.os_auth_url, - 'os_tenant_name': CONF.ironic.os_tenant_name, - 'os_service_type': CONF.ironic.os_service_type, - 'os_endpoint_type': CONF.ironic.os_endpoint_type} + args = {'token': 'noauth', + 'endpoint': CONF.ironic.ironic_url} else: - keystone_creds = {'password': CONF.ironic.os_password, - 'username': CONF.ironic.os_username, - 'auth_url': CONF.ironic.os_auth_url, - 'tenant_name': CONF.ironic.os_tenant_name} - keystone = keystone_client.Client(**keystone_creds) - # FIXME(sambetts): Work around for Bug 1539839 as client.authenticate - # is not called. - keystone.authenticate() - ironic_url = keystone.service_catalog.url_for( - service_type=CONF.ironic.os_service_type, - endpoint_type=CONF.ironic.os_endpoint_type) - args = {'os_auth_token': token, - 'ironic_url': ironic_url} + global IRONIC_SESSION + if not IRONIC_SESSION: + IRONIC_SESSION = keystone.get_session( + IRONIC_GROUP, legacy_mapping=LEGACY_MAP) + if token is None: + args = {'session': IRONIC_SESSION, + 'region_name': CONF.ironic.os_region} + else: + ironic_url = IRONIC_SESSION.get_endpoint( + service_type=CONF.ironic.os_service_type, + endpoint_type=CONF.ironic.os_endpoint_type, + region_name=CONF.ironic.os_region + ) + args = {'token': token, + 'endpoint': ironic_url} args['os_ironic_api_version'] = api_version args['max_retries'] = CONF.ironic.max_retries args['retry_interval'] = CONF.ironic.retry_interval - return client.get_client(1, **args) + return client.Client(1, **args) def check_provision_state(node, with_credentials=False): @@ -173,4 +201,4 @@ def dict_to_capabilities(caps_dict): def list_opts(): - return [(IRONIC_GROUP, IRONIC_OPTS)] + return keystone.add_auth_options(IRONIC_OPTS, IRONIC_GROUP) diff --git a/ironic_inspector/common/keystone.py b/ironic_inspector/common/keystone.py new file mode 100644 index 000000000..4965cec63 --- /dev/null +++ b/ironic_inspector/common/keystone.py @@ -0,0 +1,129 @@ +# 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. + +import copy + +from keystoneauth1 import exceptions +from keystoneauth1 import loading +from oslo_config import cfg +from oslo_log import log +from six.moves.urllib import parse # for legacy options loading only + +from ironic_inspector.common.i18n import _LW + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +def register_auth_opts(group): + loading.register_session_conf_options(CONF, group) + loading.register_auth_conf_options(CONF, group) + CONF.set_default('auth_type', default='password', group=group) + + +def get_session(group, legacy_mapping=None, legacy_auth_opts=None): + auth = _get_auth(group, legacy_mapping, legacy_auth_opts) + session = loading.load_session_from_conf_options( + CONF, group, auth=auth) + return session + + +def _get_auth(group, legacy_mapping=None, legacy_opts=None): + try: + auth = loading.load_auth_from_conf_options(CONF, group) + except exceptions.MissingRequiredOptions: + auth = _get_legacy_auth(group, legacy_mapping, legacy_opts) + else: + if auth is None: + auth = _get_legacy_auth(group, legacy_mapping, legacy_opts) + return auth + + +def _get_legacy_auth(group, legacy_mapping, legacy_opts): + """Load auth plugin from legacy options. + + If legacy_opts is not empty, these options will be registered first. + + legacy_mapping is a dict that maps the following keys to legacy option + names: + auth_url + username + password + tenant_name + """ + LOG.warning(_LW("Group [%s]: Using legacy auth loader is deprecated. " + "Consider specifying appropriate keystone auth plugin as " + "'auth_type' and corresponding plugin options."), group) + if legacy_opts: + for opt in legacy_opts: + try: + CONF.register_opt(opt, group=group) + except cfg.DuplicateOptError: + pass + + conf = getattr(CONF, group) + auth_params = {a: getattr(conf, legacy_mapping[a]) + for a in legacy_mapping} + legacy_loader = loading.get_plugin_loader('password') + # NOTE(pas-ha) only Swift had this option, take it into account + try: + auth_version = conf.get('os_auth_version') + except cfg.NoSuchOptError: + auth_version = None + # NOTE(pas-ha) mimic defaults of keystoneclient + if _is_apiv3(auth_params['auth_url'], auth_version): + auth_params.update({ + 'project_domain_id': 'default', + 'user_domain_id': 'default'}) + return legacy_loader.load_from_options(**auth_params) + + +# NOTE(pas-ha): for backward compat with legacy options loading only +def _is_apiv3(auth_url, auth_version): + """Check if V3 version of API is being used or not. + + This method inspects auth_url and auth_version, and checks whether V3 + version of the API is being used or not. + When no auth_version is specified and auth_url is not a versioned + endpoint, v2.0 is assumed. + :param auth_url: a http or https url to be inspected (like + 'http://127.0.0.1:9898/'). + :param auth_version: a string containing the version (like 'v2', 'v3.0') + or None + :returns: True if V3 of the API is being used. + """ + return (auth_version in ('v3.0', '3') or + '/v3' in parse.urlparse(auth_url).path) + + +def add_auth_options(options, group): + + def add_options(opts, opts_to_add): + for new_opt in opts_to_add: + for opt in opts: + if opt.name == new_opt.name: + break + else: + opts.append(new_opt) + + opts = copy.deepcopy(options) + opts.insert(0, loading.get_auth_common_conf_options()[0]) + # NOTE(dims): There are a lot of auth plugins, we just generate + # the config options for a few common ones + plugins = ['password', 'v2password', 'v3password'] + for name in plugins: + plugin = loading.get_plugin_loader(name) + add_options(opts, loading.get_auth_plugin_conf_options(plugin)) + add_options(opts, loading.get_session_conf_options()) + opts.sort(key=lambda x: x.name) + return [(group, opts)] diff --git a/ironic_inspector/common/swift.py b/ironic_inspector/common/swift.py index c89e6cbb8..152a78227 100644 --- a/ironic_inspector/common/swift.py +++ b/ironic_inspector/common/swift.py @@ -17,10 +17,12 @@ import json from oslo_config import cfg from oslo_log import log +import six from swiftclient import client as swift_client from swiftclient import exceptions as swift_exceptions from ironic_inspector.common.i18n import _ +from ironic_inspector.common import keystone from ironic_inspector import utils CONF = cfg.CONF @@ -28,7 +30,7 @@ CONF = cfg.CONF LOG = log.getLogger('ironic_inspector.common.swift') - +SWIFT_GROUP = 'swift' SWIFT_OPTS = [ cfg.IntOpt('max_retries', default=2, @@ -41,6 +43,32 @@ SWIFT_OPTS = [ cfg.StrOpt('container', default='ironic-inspector', help='Default Swift container to use when creating objects.'), + cfg.StrOpt('os_auth_version', + default='2', + help='Keystone authentication API version', + deprecated_for_removal=True, + deprecated_reason='Use options presented by configured ' + 'keystone auth plugin.'), + cfg.StrOpt('os_auth_url', + default='', + help='Keystone authentication URL', + deprecated_for_removal=True, + deprecated_reason='Use options presented by configured ' + 'keystone auth plugin.'), + cfg.StrOpt('os_service_type', + default='object-store', + help='Swift service type.'), + cfg.StrOpt('os_endpoint_type', + default='internalURL', + help='Swift endpoint type.'), + cfg.StrOpt('os_region', + help='Keystone region to get endpoint for.'), +] + +# NOTE(pas-ha) these old options conflict with options exported by +# most used keystone auth plugins. Need to register them manually +# for the backward-compat case. +LEGACY_OPTS = [ cfg.StrOpt('username', default='', help='User name for accessing Swift API.'), @@ -51,59 +79,67 @@ SWIFT_OPTS = [ cfg.StrOpt('tenant_name', default='', help='Tenant name for accessing Swift API.'), - cfg.StrOpt('os_auth_version', - default='2', - help='Keystone authentication API version'), - cfg.StrOpt('os_auth_url', - default='', - help='Keystone authentication URL'), - cfg.StrOpt('os_service_type', - default='object-store', - help='Swift service type.'), - cfg.StrOpt('os_endpoint_type', - default='internalURL', - help='Swift endpoint type.'), ] - -def list_opts(): - return [ - ('swift', SWIFT_OPTS) - ] - -CONF.register_opts(SWIFT_OPTS, group='swift') +CONF.register_opts(SWIFT_OPTS, group=SWIFT_GROUP) +keystone.register_auth_opts(SWIFT_GROUP) OBJECT_NAME_PREFIX = 'inspector_data' +SWIFT_SESSION = None +LEGACY_MAP = { + 'auth_url': 'os_auth_url', + 'username': 'username', + 'password': 'password', + 'tenant_name': 'tenant_name', +} + + +def reset_swift_session(): + """Reset the global session variable. + + Mostly useful for unit tests. + """ + global SWIFT_SESSION + SWIFT_SESSION = None class SwiftAPI(object): """API for communicating with Swift.""" - def __init__(self, user=None, tenant_name=None, key=None, - auth_url=None, auth_version=None, - service_type=None, endpoint_type=None): + def __init__(self): """Constructor for creating a SwiftAPI object. - :param user: the name of the user for Swift account - :param tenant_name: the name of the tenant for Swift account - :param key: the 'password' or key to authenticate with - :param auth_url: the url for authentication - :param auth_version: the version of api to use for authentication - :param service_type: service type in the service catalog - :param endpoint_type: service endpoint type + Authentification is loaded from config file. """ - self.connection = swift_client.Connection( - retries=CONF.swift.max_retries, - user=user or CONF.swift.username, - tenant_name=tenant_name or CONF.swift.tenant_name, - key=key or CONF.swift.password, - authurl=auth_url or CONF.swift.os_auth_url, - auth_version=auth_version or CONF.swift.os_auth_version, - os_options={ - 'service_type': service_type or CONF.swift.os_service_type, - 'endpoint_type': endpoint_type or CONF.swift.os_endpoint_type - } + global SWIFT_SESSION + if not SWIFT_SESSION: + SWIFT_SESSION = keystone.get_session( + SWIFT_GROUP, legacy_mapping=LEGACY_MAP, + legacy_auth_opts=LEGACY_OPTS) + # TODO(pas-ha): swiftclient does not support keystone sessions ATM. + # Must be reworked when LP bug #1518938 is fixed. + swift_url = SWIFT_SESSION.get_endpoint( + service_type=CONF.swift.os_service_type, + endpoint_type=CONF.swift.os_endpoint_type, + region_name=CONF.swift.os_region ) + token = SWIFT_SESSION.get_token() + params = dict(retries=CONF.swift.max_retries, + preauthurl=swift_url, + preauthtoken=token) + # NOTE(pas-ha):session.verify is for HTTPS urls and can be + # - False (do not verify) + # - True (verify but try to locate system CA certificates) + # - Path (verify using specific CA certificate) + # This is normally handled inside the Session instance, + # but swiftclient still does not support sessions, + # so we need to reconstruct these options from Session here. + verify = SWIFT_SESSION.verify + params['insecure'] = not verify + if verify and isinstance(verify, six.string_types): + params['cacert'] = verify + + self.connection = swift_client.Connection(**params) def create_object(self, object, data, container=CONF.swift.container, headers=None): @@ -182,3 +218,7 @@ def get_introspection_data(uuid): swift_api = SwiftAPI() swift_object_name = '%s-%s' % (OBJECT_NAME_PREFIX, uuid) return swift_api.get_object(swift_object_name) + + +def list_opts(): + return keystone.add_auth_options(SWIFT_OPTS, SWIFT_GROUP) diff --git a/ironic_inspector/main.py b/ironic_inspector/main.py index 1ac67bbd7..0d8e1e4d6 100644 --- a/ironic_inspector/main.py +++ b/ironic_inspector/main.py @@ -351,7 +351,6 @@ class Service(object): log.set_defaults(default_log_levels=[ 'sqlalchemy=WARNING', - 'keystoneclient=INFO', 'iso8601=WARNING', 'requests=WARNING', 'urllib3.connectionpool=WARNING', diff --git a/ironic_inspector/test/test_common_ironic.py b/ironic_inspector/test/test_common_ironic.py index b45e31ea8..846c78376 100644 --- a/ironic_inspector/test/test_common_ironic.py +++ b/ironic_inspector/test/test_common_ironic.py @@ -16,10 +16,10 @@ import socket import unittest from ironicclient import client -from keystoneclient import client as keystone_client from oslo_config import cfg from ironic_inspector.common import ironic as ir_utils +from ironic_inspector.common import keystone from ironic_inspector.test import base from ironic_inspector import utils @@ -27,37 +27,44 @@ from ironic_inspector import utils CONF = cfg.CONF +@mock.patch.object(keystone, 'register_auth_opts') +@mock.patch.object(keystone, 'get_session') +@mock.patch.object(client, 'Client') class TestGetClient(base.BaseTest): def setUp(self): super(TestGetClient, self).setUp() - CONF.set_override('auth_strategy', 'keystone') + ir_utils.reset_ironic_session() + self.cfg.config(auth_strategy='keystone') + self.cfg.config(os_region='somewhere', group='ironic') + self.addCleanup(ir_utils.reset_ironic_session) - @mock.patch.object(client, 'get_client') - @mock.patch.object(keystone_client, 'Client') - def test_get_client_with_auth_token(self, mock_keystone_client, - mock_client): + def test_get_client_with_auth_token(self, mock_client, mock_load, + mock_opts): fake_token = 'token' fake_ironic_url = 'http://127.0.0.1:6385' - mock_keystone_client().service_catalog.url_for.return_value = ( - fake_ironic_url) + mock_sess = mock.Mock() + mock_sess.get_endpoint.return_value = fake_ironic_url + mock_load.return_value = mock_sess ir_utils.get_client(fake_token) - args = {'os_auth_token': fake_token, - 'ironic_url': fake_ironic_url, - 'os_ironic_api_version': '1.11', + mock_sess.get_endpoint.assert_called_once_with( + endpoint_type=CONF.ironic.os_endpoint_type, + service_type=CONF.ironic.os_service_type, + region_name=CONF.ironic.os_region) + args = {'token': fake_token, + 'endpoint': fake_ironic_url, + 'os_ironic_api_version': ir_utils.DEFAULT_IRONIC_API_VERSION, 'max_retries': CONF.ironic.max_retries, 'retry_interval': CONF.ironic.retry_interval} mock_client.assert_called_once_with(1, **args) - @mock.patch.object(client, 'get_client') - def test_get_client_without_auth_token(self, mock_client): + def test_get_client_without_auth_token(self, mock_client, mock_load, + mock_opts): + mock_sess = mock.Mock() + mock_load.return_value = mock_sess ir_utils.get_client(None) - args = {'os_password': CONF.ironic.os_password, - 'os_username': CONF.ironic.os_username, - 'os_auth_url': CONF.ironic.os_auth_url, - 'os_tenant_name': CONF.ironic.os_tenant_name, - 'os_endpoint_type': CONF.ironic.os_endpoint_type, - 'os_service_type': CONF.ironic.os_service_type, - 'os_ironic_api_version': '1.11', + args = {'session': mock_sess, + 'region_name': 'somewhere', + 'os_ironic_api_version': ir_utils.DEFAULT_IRONIC_API_VERSION, 'max_retries': CONF.ironic.max_retries, 'retry_interval': CONF.ironic.retry_interval} mock_client.assert_called_once_with(1, **args) @@ -92,7 +99,7 @@ class TestGetIpmiAddress(base.BaseTest): driver_info={'foo': '192.168.1.1'}) self.assertIsNone(ir_utils.get_ipmi_address(node)) - CONF.set_override('ipmi_address_fields', ['foo', 'bar', 'baz']) + self.cfg.config(ipmi_address_fields=['foo', 'bar', 'baz']) ip = ir_utils.get_ipmi_address(node) self.assertEqual(ip, '192.168.1.1') diff --git a/ironic_inspector/test/test_keystone.py b/ironic_inspector/test/test_keystone.py new file mode 100644 index 000000000..014555663 --- /dev/null +++ b/ironic_inspector/test/test_keystone.py @@ -0,0 +1,115 @@ +# 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. + +import mock + +from keystoneauth1 import exceptions as kaexc +from keystoneauth1 import loading as kaloading +from oslo_config import cfg + +from ironic_inspector.common import keystone +from ironic_inspector.test import base + + +CONF = cfg.CONF +TESTGROUP = 'keystone_test' + + +class KeystoneTest(base.BaseTest): + + def setUp(self): + super(KeystoneTest, self).setUp() + self.cfg.conf.register_group(cfg.OptGroup(TESTGROUP)) + + def test_register_auth_opts(self): + keystone.register_auth_opts(TESTGROUP) + auth_opts = ['auth_type', 'auth_section'] + sess_opts = ['certfile', 'keyfile', 'insecure', 'timeout', 'cafile'] + for o in auth_opts + sess_opts: + self.assertIn(o, self.cfg.conf[TESTGROUP]) + self.assertEqual('password', self.cfg.conf[TESTGROUP]['auth_type']) + + @mock.patch.object(keystone, '_get_auth') + def test_get_session(self, auth_mock): + keystone.register_auth_opts(TESTGROUP) + self.cfg.config(group=TESTGROUP, + cafile='/path/to/ca/file') + auth1 = mock.Mock() + auth_mock.return_value = auth1 + sess = keystone.get_session(TESTGROUP) + self.assertEqual('/path/to/ca/file', sess.verify) + self.assertEqual(auth1, sess.auth) + + @mock.patch('keystoneauth1.loading.load_auth_from_conf_options') + @mock.patch.object(keystone, '_get_legacy_auth') + def test__get_auth(self, legacy_mock, load_mock): + auth1 = mock.Mock() + load_mock.side_effect = [ + auth1, + None, + kaexc.MissingRequiredOptions([kaloading.Opt('spam')])] + auth2 = mock.Mock() + legacy_mock.return_value = auth2 + self.assertEqual(auth1, keystone._get_auth(TESTGROUP)) + self.assertEqual(auth2, keystone._get_auth(TESTGROUP)) + self.assertEqual(auth2, keystone._get_auth(TESTGROUP)) + + @mock.patch('keystoneauth1.loading._plugins.identity.generic.Password.' + 'load_from_options') + def test__get_legacy_auth(self, load_mock): + self.cfg.register_opts( + [cfg.StrOpt('identity_url'), + cfg.StrOpt('old_user'), + cfg.StrOpt('old_password')], + group=TESTGROUP) + self.cfg.config(group=TESTGROUP, + identity_url='http://fake:5000/v3', + old_password='ham', + old_user='spam') + options = [cfg.StrOpt('old_tenant_name', default='fake'), + cfg.StrOpt('old_user')] + mapping = {'username': 'old_user', + 'password': 'old_password', + 'auth_url': 'identity_url', + 'tenant_name': 'old_tenant_name'} + + keystone._get_legacy_auth(TESTGROUP, mapping, options) + load_mock.assert_called_once_with(username='spam', + password='ham', + tenant_name='fake', + user_domain_id='default', + project_domain_id='default', + auth_url='http://fake:5000/v3') + + def test__is_api_v3(self): + cases = ((False, 'http://fake:5000', None), + (False, 'http://fake:5000/v2.0', None), + (True, 'http://fake:5000/v3', None), + (True, 'http://fake:5000', '3'), + (True, 'http://fake:5000', 'v3.0')) + for case in cases: + result, url, version = case + self.assertEqual(result, keystone._is_apiv3(url, version)) + + def test_add_auth_options(self): + group, opts = keystone.add_auth_options([], TESTGROUP)[0] + self.assertEqual(TESTGROUP, group) + # check that there is no duplicates + names = {o.dest for o in opts} + self.assertEqual(len(names), len(opts)) + # NOTE(pas-ha) checking for most standard auth and session ones only + expected = {'timeout', 'insecure', 'cafile', 'certfile', 'keyfile', + 'auth_type', 'auth_url', 'username', 'password', + 'tenant_name', 'project_name', 'trust_id', + 'domain_id', 'user_domain_id', 'project_domain_id'} + self.assertTrue(expected.issubset(names)) diff --git a/ironic_inspector/test/test_swift.py b/ironic_inspector/test/test_swift.py index 567a3b9dd..c8c0668fb 100644 --- a/ironic_inspector/test/test_swift.py +++ b/ironic_inspector/test/test_swift.py @@ -14,23 +14,18 @@ # Mostly copied from ironic/tests/test_swift.py -import sys - try: from unittest import mock except ImportError: import mock -from oslo_config import cfg -from six.moves import reload_module from swiftclient import client as swift_client from swiftclient import exceptions as swift_exception +from ironic_inspector.common import keystone from ironic_inspector.common import swift from ironic_inspector.test import base as test_base from ironic_inspector import utils -CONF = cfg.CONF - class BaseTest(test_base.NodeTest): def setUp(self): @@ -52,61 +47,43 @@ class BaseTest(test_base.NodeTest): } +@mock.patch.object(keystone, 'register_auth_opts') +@mock.patch.object(keystone, 'get_session') @mock.patch.object(swift_client, 'Connection', autospec=True) class SwiftTestCase(BaseTest): def setUp(self): super(SwiftTestCase, self).setUp() + swift.reset_swift_session() self.swift_exception = swift_exception.ClientException('', '') + self.cfg.config(group='swift', + os_service_type='object-store', + os_endpoint_type='internalURL', + os_region='somewhere', + max_retries=2) + self.addCleanup(swift.reset_swift_session) - CONF.set_override('username', 'swift', 'swift') - CONF.set_override('tenant_name', 'tenant', 'swift') - CONF.set_override('password', 'password', 'swift') - CONF.set_override('os_auth_url', 'http://authurl/v2.0', 'swift') - CONF.set_override('os_auth_version', '2', 'swift') - CONF.set_override('max_retries', 2, 'swift') - CONF.set_override('os_service_type', 'object-store', 'swift') - CONF.set_override('os_endpoint_type', 'internalURL', 'swift') - - # The constructor of SwiftAPI accepts arguments whose - # default values are values of some config options above. So reload - # the module to make sure the required values are set. - reload_module(sys.modules['ironic_inspector.common.swift']) - - def test___init__(self, connection_mock): - swift.SwiftAPI(user=CONF.swift.username, - tenant_name=CONF.swift.tenant_name, - key=CONF.swift.password, - auth_url=CONF.swift.os_auth_url, - auth_version=CONF.swift.os_auth_version) - params = {'retries': 2, - 'user': 'swift', - 'tenant_name': 'tenant', - 'key': 'password', - 'authurl': 'http://authurl/v2.0', - 'auth_version': '2', - 'os_options': {'service_type': 'object-store', - 'endpoint_type': 'internalURL'}} - connection_mock.assert_called_once_with(**params) - - def test___init__defaults(self, connection_mock): + def test___init__(self, connection_mock, load_mock, opts_mock): + swift_url = 'http://swiftapi' + token = 'secret_token' + mock_sess = mock.Mock() + mock_sess.get_token.return_value = token + mock_sess.get_endpoint.return_value = swift_url + mock_sess.verify = False + load_mock.return_value = mock_sess swift.SwiftAPI() params = {'retries': 2, - 'user': 'swift', - 'tenant_name': 'tenant', - 'key': 'password', - 'authurl': 'http://authurl/v2.0', - 'auth_version': '2', - 'os_options': {'service_type': 'object-store', - 'endpoint_type': 'internalURL'}} + 'preauthurl': swift_url, + 'preauthtoken': token, + 'insecure': True} connection_mock.assert_called_once_with(**params) + mock_sess.get_endpoint.assert_called_once_with( + service_type='object-store', + endpoint_type='internalURL', + region_name='somewhere') - def test_create_object(self, connection_mock): - swiftapi = swift.SwiftAPI(user=CONF.swift.username, - tenant_name=CONF.swift.tenant_name, - key=CONF.swift.password, - auth_url=CONF.swift.os_auth_url, - auth_version=CONF.swift.os_auth_version) + def test_create_object(self, connection_mock, load_mock, opts_mock): + swiftapi = swift.SwiftAPI() connection_obj_mock = connection_mock.return_value connection_obj_mock.put_object.return_value = 'object-uuid' @@ -119,12 +96,9 @@ class SwiftTestCase(BaseTest): 'ironic-inspector', 'object', 'some-string-data', headers=None) self.assertEqual('object-uuid', object_uuid) - def test_create_object_create_container_fails(self, connection_mock): - swiftapi = swift.SwiftAPI(user=CONF.swift.username, - tenant_name=CONF.swift.tenant_name, - key=CONF.swift.password, - auth_url=CONF.swift.os_auth_url, - auth_version=CONF.swift.os_auth_version) + def test_create_object_create_container_fails(self, connection_mock, + load_mock, opts_mock): + swiftapi = swift.SwiftAPI() connection_obj_mock = connection_mock.return_value connection_obj_mock.put_container.side_effect = self.swift_exception self.assertRaises(utils.Error, swiftapi.create_object, 'object', @@ -133,12 +107,9 @@ class SwiftTestCase(BaseTest): 'inspector') self.assertFalse(connection_obj_mock.put_object.called) - def test_create_object_put_object_fails(self, connection_mock): - swiftapi = swift.SwiftAPI(user=CONF.swift.username, - tenant_name=CONF.swift.tenant_name, - key=CONF.swift.password, - auth_url=CONF.swift.os_auth_url, - auth_version=CONF.swift.os_auth_version) + def test_create_object_put_object_fails(self, connection_mock, load_mock, + opts_mock): + swiftapi = swift.SwiftAPI() connection_obj_mock = connection_mock.return_value connection_obj_mock.put_object.side_effect = self.swift_exception self.assertRaises(utils.Error, swiftapi.create_object, 'object', @@ -148,12 +119,8 @@ class SwiftTestCase(BaseTest): connection_obj_mock.put_object.assert_called_once_with( 'ironic-inspector', 'object', 'some-string-data', headers=None) - def test_get_object(self, connection_mock): - swiftapi = swift.SwiftAPI(user=CONF.swift.username, - tenant_name=CONF.swift.tenant_name, - key=CONF.swift.password, - auth_url=CONF.swift.os_auth_url, - auth_version=CONF.swift.os_auth_version) + def test_get_object(self, connection_mock, load_mock, opts_mock): + swiftapi = swift.SwiftAPI() connection_obj_mock = connection_mock.return_value expected_obj = self.data @@ -165,12 +132,8 @@ class SwiftTestCase(BaseTest): 'ironic-inspector', 'object') self.assertEqual(expected_obj, swift_obj) - def test_get_object_fails(self, connection_mock): - swiftapi = swift.SwiftAPI(user=CONF.swift.username, - tenant_name=CONF.swift.tenant_name, - key=CONF.swift.password, - auth_url=CONF.swift.os_auth_url, - auth_version=CONF.swift.os_auth_version) + def test_get_object_fails(self, connection_mock, load_mock, opts_mock): + swiftapi = swift.SwiftAPI() connection_obj_mock = connection_mock.return_value connection_obj_mock.get_object.side_effect = self.swift_exception self.assertRaises(utils.Error, swiftapi.get_object, diff --git a/releasenotes/notes/keystoneauth-plugins-aab6cbe1d0e884bf.yaml b/releasenotes/notes/keystoneauth-plugins-aab6cbe1d0e884bf.yaml new file mode 100644 index 000000000..f0d0db554 --- /dev/null +++ b/releasenotes/notes/keystoneauth-plugins-aab6cbe1d0e884bf.yaml @@ -0,0 +1,17 @@ +--- +features: + - Ironic-Inspector is now using keystoneauth and proper auth_plugins + instead of keystoneclient for communicating with Ironic and Swift. + It allows to finely tune authentification for each service independently. + For each service, the keystone session is created and reused, minimizing + the number of authentification requests to Keystone. +upgrade: + - Operators are advised to specify a proper keystoneauth plugin + and its appropriate settings in [ironic] and [swift] config sections. + Backward compatibility with previous authentification options is included. + Using authentification informaiton for Ironic and Swift from + [keystone_authtoken] config section is no longer supported. +deprecations: + - Most of current authentification options for either Ironic or Swift are + deprecated and will be removed in a future release. Please configure + the keystoneauth auth plugin authentification instead. diff --git a/requirements.txt b/requirements.txt index ae3600211..0830b8fea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,11 +8,11 @@ Flask<1.0,>=0.10 # BSD futurist>=0.11.0 # Apache-2.0 jsonpath-rw<2.0,>=1.2.0 # Apache-2.0 jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT +keystoneauth1>=2.1.0 # Apache-2.0 keystonemiddleware!=4.1.0,>=4.0.0 # Apache-2.0 netaddr!=0.7.16,>=0.7.12 # BSD pbr>=1.6 # Apache-2.0 python-ironicclient>=1.1.0 # Apache-2.0 -python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 python-swiftclient>=2.2.0 # Apache-2.0 oslo.concurrency>=3.5.0 # Apache-2.0 oslo.config>=3.7.0 # Apache-2.0