Merge "Rework keystone auth for glance"

This commit is contained in:
Zuul 2017-11-20 17:38:10 +00:00 committed by Gerrit Code Review
commit b9ebc7f121
18 changed files with 483 additions and 316 deletions

View File

@ -1113,7 +1113,7 @@ function configure_ironic_conductor {
# TODO(pas-ha) this block is for transition period only, # TODO(pas-ha) this block is for transition period only,
# after all clients are moved to use keystoneauth adapters, # after all clients are moved to use keystoneauth adapters,
# it will be deleted # it will be deleted
local sections_with_adapter="service_catalog" local sections_with_adapter="service_catalog glance"
for conf_section in $sections_with_adapter; do for conf_section in $sections_with_adapter; do
configure_adapter_for $conf_section configure_adapter_for $conf_section
done done

View File

@ -79,13 +79,13 @@ To enable secure HTTPS communication between Bare Metal service and Image servic
served by Image service. served by Image service.
#. If not using the keystone service catalog for the Image service API endpoint #. If not using the keystone service catalog for the Image service API endpoint
discovery, also edit the ``glance_api_servers`` option to point to HTTPS URL discovery, also edit the ``endpoint_override`` option to point to HTTPS URL
of image service (replace ``<GLANCE_API_ADDRESS>`` with hostname[:port][path] of image service (replace ``<GLANCE_API_ADDRESS>`` with hostname[:port][path]
of the Image service endpoint):: of the Image service endpoint)::
[glance] [glance]
... ...
glance_api_servers = https://<GLANCE_API_ADDRESS> endpoint_override = https://<GLANCE_API_ADDRESS>
#. Restart ironic-conductor service:: #. Restart ironic-conductor service::

View File

@ -131,7 +131,7 @@ Configuring ironic-conductor service
.. code-block:: ini .. code-block:: ini
[glance] [glance]
glance_api_servers = <GLANCE_SERVICE_URL> endpoint_override = <GLANCE_SERVICE_URL>
#. Notes for configuring the Network service access #. Notes for configuring the Network service access

View File

@ -1507,9 +1507,14 @@
# Authentication URL (string value) # Authentication URL (string value)
#auth_url = <None> #auth_url = <None>
# Authentication strategy to use when connecting to glance. # DEPRECATED: Authentication strategy to use when connecting
# (string value) # to glance. (string value)
# Allowed values: keystone, noauth # Allowed values: keystone, noauth
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
# Reason: To configure glance in noauth mode, set
# [glance]/auth_type=none and
# [glance]/endpoint_override=<GLANCE_API_ADDRESS> instead.
#auth_strategy = keystone #auth_strategy = keystone
# Authentication type to load (string value) # Authentication type to load (string value)
@ -1539,15 +1544,24 @@
# Domain name to scope to (string value) # Domain name to scope to (string value)
#domain_name = <None> #domain_name = <None>
# Allow to perform insecure SSL (https) requests to glance. # Always use this endpoint URL for requests for this client.
# (boolean value) # (string value)
#endpoint_override = <None>
# DEPRECATED: Allow to perform insecure SSL (https) requests
# to glance. (boolean value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
# Reason: Use [glance]/insecure option instead.
#glance_api_insecure = false #glance_api_insecure = false
# A list of the glance api servers available to ironic. Prefix # DEPRECATED: A list of the glance api servers available to
# with https:// for SSL-based glance API servers. Format is # ironic. Prefix with https:// for SSL-based glance API
# [hostname|IP]:port. If this option is not set, the service # servers. Format is [hostname|IP]:port. (list value)
# catalog is used. It is recommended to rely on the service # This option is deprecated for removal.
# catalog, if possible. (list value) # Its value may be silently ignored in the future.
# Reason: Use [glance]/endpoint_override option to set the
# full load-balanced glance API URL instead.
#glance_api_servers = <None> #glance_api_servers = <None>
# DEPRECATED: Glance API version (1 or 2) to use. (integer # DEPRECATED: Glance API version (1 or 2) to use. (integer
@ -1560,9 +1574,13 @@
# in the Queens release. # in the Queens release.
#glance_api_version = 2 #glance_api_version = 2
# Optional path to a CA certificate bundle to be used to # DEPRECATED: Optional path to a CA certificate bundle to be
# validate the SSL certificate served by glance. It is used # used to validate the SSL certificate served by glance. It is
# when glance_api_insecure is set to False. (string value) # used when glance_api_insecure is set to False. (string
# value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
# Reason: Use [glance]/cafile option instead.
#glance_cafile = <None> #glance_cafile = <None>
# Number of retries when downloading an image from glance. # Number of retries when downloading an image from glance.
@ -1575,6 +1593,18 @@
# PEM encoded client certificate key file (string value) # PEM encoded client certificate key file (string value)
#keyfile = <None> #keyfile = <None>
# The maximum major version of a given API, intended to be
# used as the upper bound of a range with min_version.
# Mutually exclusive with version. (string value)
#max_version = <None>
# The minimum major version of a given API, intended to be
# used as the lower bound of a range with max_version.
# Mutually exclusive with version. If min_version is given
# with no max_version it is as if max version is "latest".
# (string value)
#min_version = <None>
# User's password (string value) # User's password (string value)
#password = <None> #password = <None>
@ -1592,6 +1622,18 @@
# Deprecated group/name - [glance]/tenant_name # Deprecated group/name - [glance]/tenant_name
#project_name = <None> #project_name = <None>
# The default region_name for endpoint URL discovery. (string
# value)
#region_name = <None>
# The default service_name for endpoint URL discovery. (string
# value)
#service_name = <None>
# The default service_type for endpoint URL discovery. (string
# value)
#service_type = image
# The account that Glance uses to communicate with Swift. The # The account that Glance uses to communicate with Swift. The
# format is "AUTH_uuid". "uuid" is the UUID for the account # format is "AUTH_uuid". "uuid" is the UUID for the account
# configured in the glance-api.conf. Required for temporary # configured in the glance-api.conf. Required for temporary
@ -1689,6 +1731,15 @@
# Deprecated group/name - [glance]/user_name # Deprecated group/name - [glance]/user_name
#username = <None> #username = <None>
# List of interfaces, in order of preference, for endpoint
# URL. (list value)
#valid_interfaces = internal,public
# Minimum Major API version within a given Major API version
# for endpoint URL discovery. Mutually exclusive with
# min_version and max_version (string value)
#version = <None>
[ilo] [ilo]

View File

@ -487,7 +487,7 @@ class UnsupportedDriverExtension(Invalid):
class GlanceConnectionFailed(IronicException): class GlanceConnectionFailed(IronicException):
_msg_fmt = _("Connection to glance host %(endpoint)s failed: " _msg_fmt = _("Connection to glance endpoint %(endpoint)s failed: "
"%(reason)s") "%(reason)s")

View File

@ -21,7 +21,6 @@ import time
from glanceclient import client from glanceclient import client
from glanceclient import exc as glance_exc from glanceclient import exc as glance_exc
from oslo_config import cfg
from oslo_log import log from oslo_log import log
import sendfile import sendfile
import six import six
@ -29,10 +28,20 @@ import six.moves.urllib.parse as urlparse
from ironic.common import exception from ironic.common import exception
from ironic.common.glance_service import service_utils from ironic.common.glance_service import service_utils
from ironic.common import keystone
from ironic.conf import CONF
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF
_GLANCE_SESSION = None
def _get_glance_session(**session_kwargs):
global _GLANCE_SESSION
if not _GLANCE_SESSION:
_GLANCE_SESSION = keystone.get_session('glance', **session_kwargs)
return _GLANCE_SESSION
def _translate_image_exception(image_id, exc_value): def _translate_image_exception(image_id, exc_value):
@ -46,6 +55,10 @@ def _translate_image_exception(image_id, exc_value):
return exc_value return exc_value
# NOTE(pas-ha) while looking very ugly currently, this will be simplified
# in Rocky after all deprecated [glance] options are removed and
# keystone catalog is always used with 'keystone' auth strategy
# together with session always loaded from config options
def check_image_service(func): def check_image_service(func):
"""Creates a glance client if doesn't exists and calls the function.""" """Creates a glance client if doesn't exists and calls the function."""
@six.wraps(func) @six.wraps(func)
@ -58,19 +71,54 @@ def check_image_service(func):
if self.client: if self.client:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
image_href = kwargs.get('image_href') # TODO(pas-ha) remove in Rocky
_id, self.endpoint, use_ssl = service_utils.parse_image_ref(image_href) session_params = {}
if CONF.glance.glance_api_insecure and not CONF.glance.insecure:
session_params['insecure'] = CONF.glance.glance_api_insecure
if CONF.glance.glance_cafile and not CONF.glance.cafile:
session_params['cacert'] = CONF.glance.glance_cafile
# NOTE(pas-ha) glanceclient uses Adapter-based SessionClient,
# so we can pass session and auth separately, makes things easier
session = _get_glance_session(**session_params)
params = {} # TODO(pas-ha) remove in Rocky
params['insecure'] = CONF.glance.glance_api_insecure # NOTE(pas-ha) new option must win if configured
if (not params['insecure'] and CONF.glance.glance_cafile if (CONF.glance.glance_api_servers and
and use_ssl): not CONF.glance.endpoint_override):
params['cacert'] = CONF.glance.glance_cafile # NOTE(pas-ha) all the 2 methods have image_href as the first
if CONF.glance.auth_strategy == 'keystone': # positional arg, but check in kwargs too
params['token'] = self.context.auth_token image_href = args[0] if args else kwargs.get('image_href')
self.client = client.Client(self.version, url = service_utils.get_glance_api_server(image_href)
self.endpoint, **params) CONF.set_override('endpoint_override', url, group='glance')
# TODO(pas-ha) remove in Rocky
if CONF.glance.auth_strategy == 'noauth':
CONF.set_override('auth_type', 'none', group='glance')
service_auth = keystone.get_auth('glance')
# TODO(pas-ha) remove in Rocky
adapter_params = {}
if CONF.keystone.region_name and not CONF.glance.region_name:
adapter_params['region_name'] = CONF.keystone.region_name
adapter = keystone.get_adapter('glance', session=session,
auth=service_auth, **adapter_params)
self.endpoint = adapter.get_endpoint()
user_auth = None
# NOTE(pas-ha) our ContextHook removes context.auth_token in noauth
# case, so when ironic is in noauth but glance is not, we will not
# enter the next if-block and use auth from [glance] config section
if self.context.auth_token:
user_auth = keystone.get_service_auth(self.context, self.endpoint,
service_auth)
self.client = client.Client(self.version, session=session,
auth=user_auth or service_auth,
endpoint_override=self.endpoint,
global_request_id=self.context.global_id)
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return wrapper return wrapper
@ -110,16 +158,16 @@ class BaseImageService(object):
try: try:
return getattr(self.client.images, method)(*args, **kwargs) return getattr(self.client.images, method)(*args, **kwargs)
except retry_excs as e: except retry_excs as e:
error_msg = ("Error contacting glance server " error_msg = ("Error contacting glance endpoint "
"'%(endpoint)s' for '%(method)s', attempt" "%(endpoint)s for '%(method)s', attempt "
" %(attempt)s of %(num_attempts)s failed.") "%(attempt)s of %(num_attempts)s failed.")
LOG.exception(error_msg, {'endpoint': self.endpoint, LOG.exception(error_msg, {'endpoint': self.endpoint,
'num_attempts': num_attempts, 'num_attempts': num_attempts,
'attempt': attempt, 'attempt': attempt,
'method': method}) 'method': method})
if attempt == num_attempts: if attempt == num_attempts:
raise exception.GlanceConnectionFailed( raise exception.GlanceConnectionFailed(
endpoint=self.endpoint, reason=str(e)) endpoint=self.endpoint, reason=e)
time.sleep(1) time.sleep(1)
except image_excs as e: except image_excs as e:
exc_type, exc_value, exc_trace = sys.exc_info() exc_type, exc_value, exc_trace = sys.exc_info()
@ -131,14 +179,14 @@ class BaseImageService(object):
def _show(self, image_href, method='get'): def _show(self, image_href, method='get'):
"""Returns a dict with image data for the given opaque image id. """Returns a dict with image data for the given opaque image id.
:param image_id: The opaque image identifier. :param image_href: The opaque image identifier.
:returns: A dict containing image metadata. :returns: A dict containing image metadata.
:raises: ImageNotFound :raises: ImageNotFound
""" """
LOG.debug("Getting image metadata from glance. Image: %s", LOG.debug("Getting image metadata from glance. Image: %s",
image_href) image_href)
image_id = service_utils.parse_image_ref(image_href)[0] image_id = service_utils.parse_image_id(image_href)
image = self.call(method, image_id) image = self.call(method, image_id)
@ -152,10 +200,10 @@ class BaseImageService(object):
def _download(self, image_href, data=None, method='data'): def _download(self, image_href, data=None, method='data'):
"""Calls out to Glance for data and writes data. """Calls out to Glance for data and writes data.
:param image_id: The opaque image identifier. :param image_href: The opaque image identifier.
:param data: (Optional) File object to write data to. :param data: (Optional) File object to write data to.
""" """
image_id = service_utils.parse_image_ref(image_href)[0] image_id = service_utils.parse_image_id(image_href)
if (self.version == 2 and if (self.version == 2 and
'file' in CONF.glance.allowed_direct_url_schemes): 'file' in CONF.glance.allowed_direct_url_schemes):

View File

@ -18,7 +18,7 @@ import copy
import itertools import itertools
import random import random
from oslo_config import cfg from oslo_log import log
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
@ -27,15 +27,17 @@ import six.moves.urllib.parse as urlparse
from ironic.common import exception from ironic.common import exception
from ironic.common import image_service from ironic.common import image_service
from ironic.common import keystone from ironic.conf import CONF
CONF = cfg.CONF
LOG = log.getLogger(__name__)
_GLANCE_API_SERVER = None _GLANCE_API_SERVER = None
""" iterator that cycles (indefinitely) over glance API servers. """ """ iterator that cycles (indefinitely) over glance API servers. """
def _extract_attributes(image): def _extract_attributes(image):
# TODO(pas-ha) in Queens unify these once GlanceV1 is no longer supported
IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner', IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner',
'container_format', 'checksum', 'id', 'container_format', 'checksum', 'id',
'name', 'created_at', 'updated_at', 'name', 'created_at', 'updated_at',
@ -90,73 +92,47 @@ def _convert(metadata):
return metadata return metadata
def _get_api_server_iterator(): def parse_image_id(image_href):
"""Return iterator over shuffled API servers. """Parse an image id from image href.
Shuffle a list of CONF.glance.glance_api_servers and return an iterator
that will cycle through the list, looping around to the beginning if
necessary.
If CONF.glance.glance_api_servers isn't set, fetch the endpoint from the
service catalog.
:returns: iterator that cycles (indefinitely) over shuffled glance API
servers.
"""
api_servers = []
if not CONF.glance.glance_api_servers:
session = keystone.get_session('glance',
auth=keystone.get_auth('glance'))
api_servers = [keystone.get_service_url(session, service_type='image',
endpoint_type='public')]
else:
api_servers = random.sample(CONF.glance.glance_api_servers,
len(CONF.glance.glance_api_servers))
return itertools.cycle(api_servers)
def _get_api_server():
"""Return a Glance API server.
:returns: for an API server, the tuple (host-or-IP, port, use_ssl), where
use_ssl is True to use the 'https' scheme, and False to use 'http'.
"""
global _GLANCE_API_SERVER
if not _GLANCE_API_SERVER:
_GLANCE_API_SERVER = _get_api_server_iterator()
return six.next(_GLANCE_API_SERVER)
def parse_image_ref(image_href):
"""Parse an image href.
:param image_href: href of an image :param image_href: href of an image
:returns: a tuple (image ID, glance URL, whether to use SSL) :returns: image id parsed from image_href
:raises ValueError: when input image href is invalid :raises InvalidImageRef: when input image href is invalid
""" """
if '/' not in six.text_type(image_href): image_href = six.text_type(image_href)
endpoint = _get_api_server() if uuidutils.is_uuid_like(image_href):
return (image_href, endpoint, endpoint.startswith('https')) image_id = image_href
else: elif image_href.startswith('glance://'):
try: image_id = image_href.split('/')[-1]
url = urlparse.urlparse(image_href) if not uuidutils.is_uuid_like(image_id):
if url.scheme == 'glance':
endpoint = _get_api_server()
image_id = image_href.split('/')[-1]
return (image_id, endpoint, endpoint.startswith('https'))
else:
glance_port = url.port or 80
glance_host = url.netloc.split(':', 1)[0]
image_id = url.path.split('/')[-1]
use_ssl = (url.scheme == 'https')
endpoint = '%s://%s:%s' % (url.scheme, glance_host,
glance_port)
return (image_id, endpoint, use_ssl)
except ValueError:
raise exception.InvalidImageRef(image_href=image_href) raise exception.InvalidImageRef(image_href=image_href)
else:
raise exception.InvalidImageRef(image_href=image_href)
return image_id
# TODO(pas-ha) remove in Rocky
def get_glance_api_server(image_href):
"""Construct a glance API url from config options
Returns a random server from the CONF.glance.glance_api_servers list
of servers.
:param image_href: href of an image
:returns: glance API URL
:raises InvalidImageRef: when input image href is invalid
"""
image_href = six.text_type(image_href)
if not is_glance_image(image_href):
raise exception.InvalidImageRef(image_href=image_href)
global _GLANCE_API_SERVER
if not _GLANCE_API_SERVER:
_GLANCE_API_SERVER = itertools.cycle(
random.sample(CONF.glance.glance_api_servers,
len(CONF.glance.glance_api_servers)))
return six.next(_GLANCE_API_SERVER)
def translate_from_glance(image): def translate_from_glance(image):
@ -176,7 +152,9 @@ def is_image_available(context, image):
# request and we need not handle the noauth use-case. # request and we need not handle the noauth use-case.
if hasattr(context, 'auth_token') and context.auth_token: if hasattr(context, 'auth_token') and context.auth_token:
return True return True
if image.is_public or context.is_admin:
if ((getattr(image, 'is_public', None) or
getattr(image, 'visibility', None) == 'public') or context.is_admin):
return True return True
properties = image.properties properties = image.properties
if context.project_id and ('owner_id' in properties): if context.project_id and ('owner_id' in properties):

View File

@ -20,7 +20,9 @@ import datetime
import os import os
import shutil import shutil
from oslo_log import log
from oslo_utils import importutils from oslo_utils import importutils
from oslo_utils import uuidutils
import requests import requests
import sendfile import sendfile
import six import six
@ -29,23 +31,15 @@ import six.moves.urllib.parse as urlparse
from ironic.common import exception from ironic.common import exception
from ironic.common.i18n import _ from ironic.common.i18n import _
from ironic.common import keystone
from ironic.common import utils from ironic.common import utils
from ironic.conf import CONF from ironic.conf import CONF
IMAGE_CHUNK_SIZE = 1024 * 1024 # 1mb IMAGE_CHUNK_SIZE = 1024 * 1024 # 1mb
LOG = log.getLogger(__name__)
_GLANCE_SESSION = None
def _get_glance_session():
global _GLANCE_SESSION
if not _GLANCE_SESSION:
auth = keystone.get_auth('glance')
_GLANCE_SESSION = keystone.get_session('glance', auth=auth)
return _GLANCE_SESSION
# TODO(pas-ha) in Queens change default to '2',
# but keep the versioned import in place (less work for possible Glance v3)
def GlanceImageService(client=None, version=None, context=None): def GlanceImageService(client=None, version=None, context=None):
module_str = 'ironic.common.glance_service' module_str = 'ironic.common.glance_service'
if version is None: if version is None:
@ -54,9 +48,6 @@ def GlanceImageService(client=None, version=None, context=None):
module = importutils.import_versioned_module(module_str, version, module = importutils.import_versioned_module(module_str, version,
'image_service') 'image_service')
service_class = getattr(module, 'GlanceImageService') service_class = getattr(module, 'GlanceImageService')
if (context is not None and CONF.glance.auth_strategy == 'keystone'
and not context.auth_token):
context.auth_token = _get_glance_session().get_token()
return service_class(client, version, context) return service_class(client, version, context)
@ -279,14 +270,21 @@ def get_image_service(image_href, client=None, version=None, context=None):
specified image. specified image.
""" """
scheme = urlparse.urlparse(image_href).scheme.lower() scheme = urlparse.urlparse(image_href).scheme.lower()
try:
cls = protocol_mapping[scheme or 'glance'] if not scheme:
except KeyError: if uuidutils.is_uuid_like(six.text_type(image_href)):
raise exception.ImageRefValidationFailed( cls = GlanceImageService
image_href=image_href, else:
reason=_('Image download protocol ' raise exception.ImageRefValidationFailed(
'%s is not supported.') % scheme image_href=image_href,
) reason=_('Scheme-less image href is not a UUID.'))
else:
cls = protocol_mapping.get(scheme)
if not cls:
raise exception.ImageRefValidationFailed(
image_href=image_href,
reason=_('Image download protocol %s is not supported.'
) % scheme)
if cls == GlanceImageService: if cls == GlanceImageService:
return cls(client, version, context) return cls(client, version, context)

View File

@ -16,6 +16,8 @@
from keystoneauth1 import exceptions as kaexception from keystoneauth1 import exceptions as kaexception
from keystoneauth1 import loading as kaloading from keystoneauth1 import loading as kaloading
from keystoneauth1 import service_token
from keystoneauth1 import token_endpoint
from oslo_log import log as logging from oslo_log import log as logging
import six import six
@ -102,7 +104,23 @@ def get_adapter(group, **adapter_kwargs):
**adapter_kwargs) **adapter_kwargs)
# NOTE(pas-ha) Used by neutronclient and glanceclient only def get_service_auth(context, endpoint, service_auth):
"""Create auth plugin wrapping both user and service auth.
When properly configured and using auth_token middleware,
requests with valid service auth will not fail
if the user token is expired.
Ideally we would use the plugin provided by auth_token middleware
however this plugin isn't serialized yet.
"""
# TODO(pas-ha) use auth plugin from context when it is available
user_auth = token_endpoint.Token(endpoint, context.auth_token)
return service_token.ServiceTokenAuthWrapper(user_auth=user_auth,
service_auth=service_auth)
# NOTE(pas-ha) Used by neutronclient only
# FIXME(pas-ha) remove this while moving to kesytoneauth adapters # FIXME(pas-ha) remove this while moving to kesytoneauth adapters
@ks_exceptions @ks_exceptions
def get_service_url(session, **kwargs): def get_service_url(session, **kwargs):

View File

@ -104,14 +104,17 @@ opts = [
'multiple containers to store images, and this value ' 'multiple containers to store images, and this value '
'will determine how many containers are created.')), 'will determine how many containers are created.')),
cfg.ListOpt('glance_api_servers', cfg.ListOpt('glance_api_servers',
deprecated_for_removal=True,
deprecated_reason=_("Use [glance]/endpoint_override option "
"to set the full load-balanced glance API "
"URL instead."),
help=_('A list of the glance api servers available to ironic. ' help=_('A list of the glance api servers available to ironic. '
'Prefix with https:// for SSL-based glance API ' 'Prefix with https:// for SSL-based glance API '
'servers. Format is [hostname|IP]:port. ' 'servers. Format is [hostname|IP]:port.')),
'If this option is not set, the service '
'catalog is used. It is recommended to rely on the '
'service catalog, if possible.')),
cfg.BoolOpt('glance_api_insecure', cfg.BoolOpt('glance_api_insecure',
default=False, default=False,
deprecated_for_removal=True,
deprecated_reason=_("Use [glance]/insecure option instead."),
help=_('Allow to perform insecure SSL (https) requests to ' help=_('Allow to perform insecure SSL (https) requests to '
'glance.')), 'glance.')),
cfg.IntOpt('glance_num_retries', cfg.IntOpt('glance_num_retries',
@ -121,9 +124,16 @@ opts = [
cfg.StrOpt('auth_strategy', cfg.StrOpt('auth_strategy',
default='keystone', default='keystone',
choices=['keystone', 'noauth'], choices=['keystone', 'noauth'],
deprecated_for_removal=True,
deprecated_reason=_("To configure glance in noauth mode, "
"set [glance]/auth_type=none and "
"[glance]/endpoint_override="
"<GLANCE_API_ADDRESS> instead."),
help=_('Authentication strategy to use when connecting to ' help=_('Authentication strategy to use when connecting to '
'glance.')), 'glance.')),
cfg.StrOpt('glance_cafile', cfg.StrOpt('glance_cafile',
deprecated_for_removal=True,
deprecated_reason=_("Use [glance]/cafile option instead."),
help=_('Optional path to a CA certificate bundle to be used to ' help=_('Optional path to a CA certificate bundle to be used to '
'validate the SSL certificate served by glance. It is ' 'validate the SSL certificate served by glance. It is '
'used when glance_api_insecure is set to False.')), 'used when glance_api_insecure is set to False.')),
@ -138,8 +148,8 @@ opts = [
def register_opts(conf): def register_opts(conf):
conf.register_opts(opts, group='glance') conf.register_opts(opts, group='glance')
auth.register_auth_opts(conf, 'glance') auth.register_auth_opts(conf, 'glance', service_type='image')
def list_opts(): def list_opts():
return auth.add_auth_opts(opts) return auth.add_auth_opts(opts, service_type='image')

View File

@ -92,7 +92,7 @@ class ImageCache(object):
# NOTE(vdrok): File name is converted to UUID if it's not UUID already, # NOTE(vdrok): File name is converted to UUID if it's not UUID already,
# so that two images with same file names do not collide # so that two images with same file names do not collide
if service_utils.is_glance_image(href): if service_utils.is_glance_image(href):
master_file_name = service_utils.parse_image_ref(href)[0] master_file_name = service_utils.parse_image_id(href)
else: else:
# NOTE(vdrok): Doing conversion of href in case it's unicode # NOTE(vdrok): Doing conversion of href in case it's unicode
# string, UUID cannot be generated for unicode strings on python 2. # string, UUID cannot be generated for unicode strings on python 2.

View File

@ -19,6 +19,7 @@ import time
from glanceclient import client as glance_client from glanceclient import client as glance_client
from glanceclient import exc as glance_exc from glanceclient import exc as glance_exc
from keystoneauth1 import loading as kaloading
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import uuidutils from oslo_utils import uuidutils
@ -98,15 +99,7 @@ class TestGlanceImageService(base.TestCase):
self.service = service.GlanceImageService(client, 1, self.context) self.service = service.GlanceImageService(client, 1, self.context)
self.config(glance_api_servers=['http://localhost'], group='glance') self.config(glance_api_servers=['http://localhost'], group='glance')
try: self.config(auth_strategy='keystone', group='glance')
self.config(auth_strategy='keystone', group='glance')
except Exception:
opts = [
cfg.StrOpt('auth_strategy', default='keystone'),
]
CONF.register_opts(opts)
return
@staticmethod @staticmethod
def _make_fixture(**kwargs): def _make_fixture(**kwargs):
@ -196,7 +189,7 @@ class TestGlanceImageService(base.TestCase):
stub_context.user_id = 'fake' stub_context.user_id = 'fake'
stub_context.project_id = 'fake' stub_context.project_id = 'fake'
stub_service = service.GlanceImageService(stub_client, 1, stub_context) stub_service = service.GlanceImageService(stub_client, 1, stub_context)
image_id = 1 # doesn't matter image_id = uuidutils.generate_uuid()
writer = NullWriter() writer = NullWriter()
# When retries are disabled, we should get an exception # When retries are disabled, we should get an exception
@ -233,7 +226,7 @@ class TestGlanceImageService(base.TestCase):
stub_service = service.GlanceImageService(stub_client, stub_service = service.GlanceImageService(stub_client,
context=stub_context, context=stub_context,
version=2) version=2)
image_id = 1 # doesn't matter image_id = uuidutils.generate_uuid()
self.config(allowed_direct_url_schemes=['file'], group='glance') self.config(allowed_direct_url_schemes=['file'], group='glance')
@ -268,7 +261,7 @@ class TestGlanceImageService(base.TestCase):
stub_context.user_id = 'fake' stub_context.user_id = 'fake'
stub_context.project_id = 'fake' stub_context.project_id = 'fake'
stub_service = service.GlanceImageService(stub_client, 1, stub_context) stub_service = service.GlanceImageService(stub_client, 1, stub_context)
image_id = 1 # doesn't matter image_id = uuidutils.generate_uuid()
writer = NullWriter() writer = NullWriter()
self.assertRaises(exception.ImageNotAuthorized, stub_service.download, self.assertRaises(exception.ImageNotAuthorized, stub_service.download,
image_id, writer) image_id, writer)
@ -284,7 +277,7 @@ class TestGlanceImageService(base.TestCase):
stub_context.user_id = 'fake' stub_context.user_id = 'fake'
stub_context.project_id = 'fake' stub_context.project_id = 'fake'
stub_service = service.GlanceImageService(stub_client, 1, stub_context) stub_service = service.GlanceImageService(stub_client, 1, stub_context)
image_id = 1 # doesn't matter image_id = uuidutils.generate_uuid()
writer = NullWriter() writer = NullWriter()
self.assertRaises(exception.ImageNotAuthorized, stub_service.download, self.assertRaises(exception.ImageNotAuthorized, stub_service.download,
image_id, writer) image_id, writer)
@ -300,7 +293,7 @@ class TestGlanceImageService(base.TestCase):
stub_context.user_id = 'fake' stub_context.user_id = 'fake'
stub_context.project_id = 'fake' stub_context.project_id = 'fake'
stub_service = service.GlanceImageService(stub_client, 1, stub_context) stub_service = service.GlanceImageService(stub_client, 1, stub_context)
image_id = 1 # doesn't matter image_id = uuidutils.generate_uuid()
writer = NullWriter() writer = NullWriter()
self.assertRaises(exception.ImageNotFound, stub_service.download, self.assertRaises(exception.ImageNotFound, stub_service.download,
image_id, writer) image_id, writer)
@ -316,12 +309,44 @@ class TestGlanceImageService(base.TestCase):
stub_context.user_id = 'fake' stub_context.user_id = 'fake'
stub_context.project_id = 'fake' stub_context.project_id = 'fake'
stub_service = service.GlanceImageService(stub_client, 1, stub_context) stub_service = service.GlanceImageService(stub_client, 1, stub_context)
image_id = 1 # doesn't matter image_id = uuidutils.generate_uuid()
writer = NullWriter() writer = NullWriter()
self.assertRaises(exception.ImageNotFound, stub_service.download, self.assertRaises(exception.ImageNotFound, stub_service.download,
image_id, writer) image_id, writer)
def test_check_image_service_client_set(self):
@mock.patch('ironic.common.keystone.get_auth', autospec=True,
return_value=mock.sentinel.auth)
@mock.patch('ironic.common.keystone.get_service_auth', autospec=True,
return_value=mock.sentinel.sauth)
@mock.patch('ironic.common.keystone.get_adapter', autospec=True)
@mock.patch('ironic.common.keystone.get_session', autospec=True,
return_value=mock.sentinel.session)
@mock.patch.object(glance_client, 'Client', autospec=True)
class CheckImageServiceTestCase(base.TestCase):
def setUp(self):
super(CheckImageServiceTestCase, self).setUp()
self.context = context.RequestContext(global_request_id='global')
self.service = service.GlanceImageService(None, 1, self.context)
# NOTE(pas-ha) register keystoneauth dynamic options manually
plugin = kaloading.get_plugin_loader('password')
opts = kaloading.get_auth_plugin_conf_options(plugin)
self.cfg_fixture.register_opts(opts, group='glance')
self.config(auth_type='password',
auth_url='viking',
username='spam',
password='ham',
project_name='parrot',
service_type='image',
region_name='SomeRegion',
interface='internal',
auth_strategy='keystone',
group='glance')
base_image_service._GLANCE_SESSION = None
def test_check_image_service_client_already_set(self, mock_gclient,
mock_sess, mock_adapter,
mock_sauth, mock_auth):
def func(self): def func(self):
return True return True
@ -329,70 +354,118 @@ class TestGlanceImageService(base.TestCase):
wrapped_func = base_image_service.check_image_service(func) wrapped_func = base_image_service.check_image_service(func)
self.assertTrue(wrapped_func(self.service)) self.assertTrue(wrapped_func(self.service))
self.assertEqual(0, mock_gclient.call_count)
self.assertEqual(0, mock_sess.call_count)
self.assertEqual(0, mock_adapter.call_count)
self.assertEqual(0, mock_auth.call_count)
self.assertEqual(0, mock_sauth.call_count)
@mock.patch.object(glance_client, 'Client', autospec=True) def _assert_client_call(self, mock_gclient, url, user=False):
def test_check_image_service__no_client_set_http(self, mock_gclient):
def func(service, *args, **kwargs):
return (self.endpoint, args, kwargs)
endpoint = 'http://123.123.123.123:9292'
mock_gclient.return_value.endpoint = endpoint
self.service.client = None
params = {'image_href': '%s/image_uuid' % endpoint}
self.config(auth_strategy='keystone', group='glance')
wrapped_func = base_image_service.check_image_service(func)
self.assertEqual((endpoint, (), params),
wrapped_func(self.service, **params))
mock_gclient.assert_called_once_with( mock_gclient.assert_called_once_with(
1, endpoint, 1,
**{'insecure': CONF.glance.glance_api_insecure, session=mock.sentinel.session,
'token': self.context.auth_token}) global_request_id='global',
auth=mock.sentinel.sauth if user else mock.sentinel.auth,
endpoint_override=url)
@mock.patch.object(glance_client, 'Client', autospec=True) def test_check_image_service__config_auth(self, mock_gclient, mock_sess,
def test_get_image_service__no_client_set_https_insecure(self, mock_adapter, mock_sauth,
mock_gclient): mock_auth):
def func(service, *args, **kwargs): def func(service, *args, **kwargs):
return (self.endpoint, args, kwargs) return args, kwargs
endpoint = 'https://123.123.123.123:9292' mock_adapter.return_value = adapter = mock.Mock()
mock_gclient.return_value.endpoint = endpoint adapter.get_endpoint.return_value = 'glance_url'
self.service.client = None uuid = uuidutils.generate_uuid()
params = {'image_href': uuid}
params = {'image_href': '%s/image_uuid' % endpoint}
self.config(auth_strategy='keystone', group='glance')
self.config(glance_api_insecure=True, group='glance')
wrapped_func = base_image_service.check_image_service(func) wrapped_func = base_image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params))
self._assert_client_call(mock_gclient, 'glance_url')
mock_auth.assert_called_once_with('glance')
mock_sess.assert_called_once_with('glance')
mock_adapter.assert_called_once_with('glance',
session=mock.sentinel.session,
auth=mock.sentinel.auth)
adapter.get_endpoint.assert_called_once_with()
self.assertEqual(0, mock_sauth.call_count)
self.assertEqual((endpoint, (), params), def test_check_image_service__token_auth(self, mock_gclient, mock_sess,
wrapped_func(self.service, **params)) mock_adapter, mock_sauth,
mock_gclient.assert_called_once_with( mock_auth):
1, endpoint,
**{'insecure': CONF.glance.glance_api_insecure,
'token': self.context.auth_token})
@mock.patch.object(glance_client, 'Client', autospec=True)
def test_get_image_service__no_client_set_https_secure(self, mock_gclient):
def func(service, *args, **kwargs): def func(service, *args, **kwargs):
return (self.endpoint, args, kwargs) return args, kwargs
endpoint = 'https://123.123.123.123:9292' self.service.context = context.RequestContext(
mock_gclient.return_value.endpoint = endpoint auth_token='token', global_request_id='global')
self.service.client = None mock_adapter.return_value = adapter = mock.Mock()
adapter.get_endpoint.return_value = 'glance_url'
uuid = uuidutils.generate_uuid()
params = {'image_href': uuid}
params = {'image_href': '%s/image_uuid' % endpoint}
self.config(auth_strategy='keystone', group='glance')
self.config(glance_api_insecure=False, group='glance')
self.config(glance_cafile='/path/to/certfile', group='glance')
wrapped_func = base_image_service.check_image_service(func) wrapped_func = base_image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params))
self._assert_client_call(mock_gclient, 'glance_url', user=True)
mock_sess.assert_called_once_with('glance')
mock_adapter.assert_called_once_with('glance',
session=mock.sentinel.session,
auth=mock.sentinel.auth)
mock_sauth.assert_called_once_with(self.service.context, 'glance_url',
mock.sentinel.auth)
mock_auth.assert_called_once_with('glance')
self.assertEqual((endpoint, (), params), def test_check_image_service__deprecated_opts(self, mock_gclient,
wrapped_func(self.service, **params)) mock_sess, mock_adapter,
mock_gclient.assert_called_once_with( mock_sauth, mock_auth):
1, endpoint, def func(service, *args, **kwargs):
**{'cacert': CONF.glance.glance_cafile, return args, kwargs
'insecure': CONF.glance.glance_api_insecure,
'token': self.context.auth_token}) mock_adapter.return_value = adapter = mock.Mock()
adapter.get_endpoint.return_value = 'glance_url'
uuid = uuidutils.generate_uuid()
params = {'image_href': uuid}
self.config(glance_api_servers='https://localhost:1234',
glance_api_insecure=True,
glance_cafile='cafile',
region_name=None,
group='glance')
self.config(region_name='OtherRegion', group='keystone')
wrapped_func = base_image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params))
self.assertEqual('https://localhost:1234',
base_image_service.CONF.glance.endpoint_override)
self._assert_client_call(mock_gclient, 'glance_url')
mock_sess.assert_called_once_with('glance', insecure=True,
cacert='cafile')
mock_adapter.assert_called_once_with(
'glance', session=mock.sentinel.session,
auth=mock.sentinel.auth, region_name='OtherRegion')
self.assertEqual(0, mock_sauth.call_count)
mock_auth.assert_called_once_with('glance')
def test_check_image_service__no_auth(self, mock_gclient, mock_sess,
mock_adapter, mock_sauth, mock_auth):
def func(service, *args, **kwargs):
return args, kwargs
self.config(endpoint_override='foo',
auth_strategy='noauth',
group='glance')
mock_adapter.return_value = adapter = mock.Mock()
adapter.get_endpoint.return_value = 'foo'
uuid = uuidutils.generate_uuid()
params = {'image_href': uuid}
wrapped_func = base_image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params))
self.assertEqual('none', base_image_service.CONF.glance.auth_type)
self._assert_client_call(mock_gclient, 'foo')
mock_sess.assert_called_once_with('glance')
mock_adapter.assert_called_once_with('glance',
session=mock.sentinel.session,
auth=mock.sentinel.auth)
self.assertEqual(0, mock_sauth.call_count)
def _create_failing_glance_client(info): def _create_failing_glance_client(info):
@ -811,19 +884,43 @@ class TestSwiftTempUrlCache(base.TestCase):
class TestServiceUtils(base.TestCase): class TestServiceUtils(base.TestCase):
def test_parse_image_ref_no_ssl(self): def setUp(self):
image_href = u'http://127.0.0.1:9292/image_path/'\ super(TestServiceUtils, self).setUp()
u'image_\u00F9\u00FA\u00EE\u0111' service_utils._GLANCE_API_SERVER = None
parsed_href = service_utils.parse_image_ref(image_href)
self.assertEqual((u'image_\u00F9\u00FA\u00EE\u0111',
'http://127.0.0.1:9292', False), parsed_href)
def test_parse_image_ref_ssl(self): def test_parse_image_id_from_uuid(self):
image_href = 'https://127.0.0.1:9292/image_path/'\ image_href = uuidutils.generate_uuid()
u'image_\u00F9\u00FA\u00EE\u0111' parsed_id = service_utils.parse_image_id(image_href)
parsed_href = service_utils.parse_image_ref(image_href) self.assertEqual(image_href, parsed_id)
self.assertEqual((u'image_\u00F9\u00FA\u00EE\u0111',
'https://127.0.0.1:9292', True), parsed_href) def test_parse_image_id_from_glance(self):
uuid = uuidutils.generate_uuid()
image_href = u'glance://some-stuff/%s' % uuid
parsed_id = service_utils.parse_image_id(image_href)
self.assertEqual(uuid, parsed_id)
def test_parse_image_id_from_glance_fail(self):
self.assertRaises(exception.InvalidImageRef,
service_utils.parse_image_id, u'glance://not-a-uuid')
def test_parse_image_id_fail(self):
self.assertRaises(exception.InvalidImageRef,
service_utils.parse_image_id,
u'http://spam.ham/eggs')
def test_get_glance_api_server_fail(self):
self.assertRaises(exception.InvalidImageRef,
service_utils.get_glance_api_server,
u'http://spam.ham/eggs')
# TODO(pas-ha) remove in Rocky
def test_get_glance_api_server(self):
self.config(glance_api_servers='http://spam:1234, https://ham',
group='glance')
api_servers = {service_utils.get_glance_api_server(
uuidutils.generate_uuid()) for i in range(2)}
self.assertEqual({'http://spam:1234', 'https://ham'},
api_servers)
def test_is_glance_image(self): def test_is_glance_image(self):
image_href = u'uui\u0111' image_href = u'uui\u0111'
@ -850,51 +947,3 @@ class TestServiceUtils(base.TestCase):
u'file://\u0111eploy_iso',): u'file://\u0111eploy_iso',):
result = service_utils.is_image_href_ordinary_file_name(image) result = service_utils.is_image_href_ordinary_file_name(image)
self.assertFalse(result) self.assertFalse(result)
class TestGlanceAPIServers(base.TestCase):
def setUp(self):
super(TestGlanceAPIServers, self).setUp()
service_utils._GLANCE_API_SERVER = None
@mock.patch.object(service_utils.keystone, 'get_service_url',
autospec=True)
@mock.patch.object(service_utils.keystone, 'get_session', autospec=True)
@mock.patch.object(service_utils.keystone, 'get_auth', autospec=True)
def test__get_api_servers_with_keystone(self, mock_auth, mock_session,
mock_service_url):
mock_service_url.return_value = 'https://example.com'
endpoint = service_utils._get_api_server()
self.assertEqual('https://example.com', endpoint)
mock_auth.assert_called_once_with('glance')
mock_session.assert_called_once_with('glance',
auth=mock_auth.return_value)
mock_service_url.assert_called_once_with(mock_session.return_value,
service_type='image',
endpoint_type='public')
def test__get_api_servers_one(self):
CONF.set_override('glance_api_servers', ['https://10.0.0.1:9293'],
'glance')
s1 = service_utils._get_api_server()
s2 = service_utils._get_api_server()
self.assertEqual('https://10.0.0.1:9293', s1)
# Only one server, should always get the same one
self.assertEqual(s1, s2)
def test__get_api_servers_two(self):
CONF.set_override('glance_api_servers',
['http://10.0.0.1:9293', 'http://10.0.0.2:9294'],
'glance')
s1 = service_utils._get_api_server()
s2 = service_utils._get_api_server()
s3 = service_utils._get_api_server()
self.assertNotEqual(s1, s2)
# 2 servers, so cycles to the first again
self.assertEqual(s1, s3)

View File

@ -16,6 +16,7 @@ import shutil
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import uuidutils
import requests import requests
import sendfile import sendfile
import six import six
@ -265,73 +266,30 @@ class FileImageServiceTestCase(base.TestCase):
class ServiceGetterTestCase(base.TestCase): class ServiceGetterTestCase(base.TestCase):
@mock.patch.object(image_service, '_get_glance_session', autospec=True)
@mock.patch.object(glance_v2_service.GlanceImageService, '__init__', @mock.patch.object(glance_v2_service.GlanceImageService, '__init__',
return_value=None, autospec=True) return_value=None, autospec=True)
def test_get_glance_image_service(self, glance_service_mock, def test_get_glance_image_service(self, glance_service_mock):
session_mock): image_href = uuidutils.generate_uuid()
image_href = 'image-uuid'
self.context.auth_token = 'fake'
image_service.get_image_service(image_href, context=self.context) image_service.get_image_service(image_href, context=self.context)
glance_service_mock.assert_called_once_with(mock.ANY, None, 2, glance_service_mock.assert_called_once_with(mock.ANY, None, 2,
self.context) self.context)
self.assertFalse(session_mock.called)
@mock.patch.object(image_service, '_get_glance_session', autospec=True)
@mock.patch.object(glance_v1_service.GlanceImageService, '__init__', @mock.patch.object(glance_v1_service.GlanceImageService, '__init__',
return_value=None, autospec=True) return_value=None, autospec=True)
def test_get_glance_image_service_default_v1(self, glance_service_mock, def test_get_glance_image_service_default_v1(self, glance_service_mock):
session_mock):
self.config(glance_api_version=1, group='glance') self.config(glance_api_version=1, group='glance')
image_href = 'image-uuid' image_href = uuidutils.generate_uuid()
self.context.auth_token = 'fake'
image_service.get_image_service(image_href, context=self.context) image_service.get_image_service(image_href, context=self.context)
glance_service_mock.assert_called_once_with(mock.ANY, None, 1, glance_service_mock.assert_called_once_with(mock.ANY, None, 1,
self.context) self.context)
self.assertFalse(session_mock.called)
@mock.patch.object(image_service, '_get_glance_session', autospec=True)
@mock.patch.object(glance_v2_service.GlanceImageService, '__init__', @mock.patch.object(glance_v2_service.GlanceImageService, '__init__',
return_value=None, autospec=True) return_value=None, autospec=True)
def test_get_glance_image_service_url(self, glance_service_mock, def test_get_glance_image_service_url(self, glance_service_mock):
session_mock): image_href = 'glance://%s' % uuidutils.generate_uuid()
image_href = 'glance://image-uuid'
self.context.auth_token = 'fake'
image_service.get_image_service(image_href, context=self.context) image_service.get_image_service(image_href, context=self.context)
glance_service_mock.assert_called_once_with(mock.ANY, None, 2, glance_service_mock.assert_called_once_with(mock.ANY, None, 2,
self.context) self.context)
self.assertFalse(session_mock.called)
@mock.patch.object(image_service, '_get_glance_session', autospec=True)
@mock.patch.object(glance_v2_service.GlanceImageService, '__init__',
return_value=None, autospec=True)
def test_get_glance_image_service_no_token(self, glance_service_mock,
session_mock):
image_href = 'image-uuid'
self.context.auth_token = None
sess = mock.Mock()
sess.get_token.return_value = 'admin-token'
session_mock.return_value = sess
image_service.get_image_service(image_href, context=self.context)
glance_service_mock.assert_called_once_with(mock.ANY, None, 2,
self.context)
sess.get_token.assert_called_once_with()
self.assertEqual('admin-token', self.context.auth_token)
@mock.patch.object(image_service, '_get_glance_session', autospec=True)
@mock.patch.object(glance_v2_service.GlanceImageService, '__init__',
return_value=None, autospec=True)
def test_get_glance_image_service_token_not_needed(self,
glance_service_mock,
session_mock):
image_href = 'image-uuid'
self.context.auth_token = None
self.config(auth_strategy='noauth', group='glance')
image_service.get_image_service(image_href, context=self.context)
glance_service_mock.assert_called_once_with(mock.ANY, None, 2,
self.context)
self.assertFalse(session_mock.called)
self.assertIsNone(self.context.auth_token)
@mock.patch.object(image_service.HttpImageService, '__init__', @mock.patch.object(image_service.HttpImageService, '__init__',
return_value=None, autospec=True) return_value=None, autospec=True)
@ -354,10 +312,13 @@ class ServiceGetterTestCase(base.TestCase):
image_service.get_image_service(image_href) image_service.get_image_service(image_href)
local_service_mock.assert_called_once_with() local_service_mock.assert_called_once_with()
def test_get_image_service_unknown_protocol(self): def test_get_image_service_invalid_image_ref(self):
image_href = 'usenet://alt.binaries.dvd/image.qcow2' invalid_refs = (
self.assertRaises(exception.ImageRefValidationFailed, 'usenet://alt.binaries.dvd/image.qcow2',
image_service.get_image_service, image_href) 'no scheme, no uuid')
for image_ref in invalid_refs:
self.assertRaises(exception.ImageRefValidationFailed,
image_service.get_image_service, image_ref)
def test_out_range_auth_strategy(self): def test_out_range_auth_strategy(self):
self.assertRaises(ValueError, cfg.CONF.set_override, self.assertRaises(ValueError, cfg.CONF.set_override,

View File

@ -17,6 +17,7 @@ import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_config import fixture from oslo_config import fixture
from ironic.common import context
from ironic.common import exception from ironic.common import exception
from ironic.common import keystone from ironic.common import keystone
from ironic.conf import auth as ironic_auth from ironic.conf import auth as ironic_auth
@ -92,3 +93,14 @@ class KeystoneTestCase(base.TestCase):
interface='admin') interface='admin')
self.assertEqual('admin', adapter.interface) self.assertEqual('admin', adapter.interface)
self.assertEqual(session, adapter.session) self.assertEqual(session, adapter.session)
@mock.patch('keystoneauth1.service_token.ServiceTokenAuthWrapper')
@mock.patch('keystoneauth1.token_endpoint.Token')
def test_get_service_auth(self, token_mock, service_auth_mock):
ctxt = context.RequestContext(auth_token='spam')
mock_auth = mock.Mock()
self.assertEqual(service_auth_mock.return_value,
keystone.get_service_auth(ctxt, 'ham', mock_auth))
token_mock.assert_called_once_with('ham', 'spam')
service_auth_mock.assert_called_once_with(
user_auth=token_mock.return_value, service_auth=mock_auth)

View File

@ -38,8 +38,6 @@ def touch(filename):
open(filename, 'w').close() open(filename, 'w').close()
@mock.patch('ironic.common.glance_service.service_utils.parse_image_ref',
lambda image: (image, 'example.com', True))
class TestImageCacheFetch(base.TestCase): class TestImageCacheFetch(base.TestCase):
def setUp(self): def setUp(self):

View File

@ -0,0 +1,7 @@
---
other:
- |
Support for parsing the glance API endpoint from the full REST path
to a glance image was removed as it was not working anyway.
The image service API is now always resolved from keystone catalog or
via the options in the ``[glance]`` section in ironic configuration file.

View File

@ -0,0 +1,31 @@
---
deprecations:
- |
Configuration option ``glance_api_servers`` from the ``[glance]``
section in the configuration file is deprecated
and will be ignored in the Rocky release.
Instead, use ``[glance]/endpoint_override`` configuration option to set
a specific (possibly load-balanced) glance API address when automatic
discovery of glance API endpoint from keystone catalog is not desired.
This new option defaults to ``None`` and must be set explicitly if needed.
This new option is mostly suited for standalone ironic deployments without
keystone and its service catalog, and it is generally recommended to
rely on keystone service catalog for service endpoint discovery.
- |
Configuration option ``[glance]/glance_api_insecure`` is deprecated
and will be ignored in the Rocky release.
Instead, use ``[glance]/insecure`` configuration option
(its default is ``False``).
- |
Configuration option ``[glance]/glance_cafile`` is deprecated
and will be ignored in the Rocky release.
Instead, use ``[glance]/cafile`` configuration option
(its default is ``None``).
- |
Configuration option ``[glance]/auth_strategy`` is deprecated
and will be ignored in the Rocky release.
Instead, to setup glance in noauth mode set ``[glance]/auth_type``
configuration option to ``none`` and provide glance API address as
``[glance]/endpoint_override`` configuration option.

View File

@ -0,0 +1,6 @@
---
features:
- |
Adds the ability to set keystoneauth settings in the
``[glance]`` configuration section for service automatic
discovery.