Adding Keystone support for Glance client.
Implements bp pluggable-auth Change-Id: I2a6e3b1ab4c50200ece64a2e07bf81e9e6467efd
This commit is contained in:
parent
ecbcc09ce5
commit
be6d6294f9
20
bin/glance
20
bin/glance
@ -59,7 +59,14 @@ def catch_error(action):
|
|||||||
try:
|
try:
|
||||||
ret = func(*args, **kwargs)
|
ret = func(*args, **kwargs)
|
||||||
return SUCCESS if ret is None else ret
|
return SUCCESS if ret is None else ret
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
print "Not authorized to make this request. Check "\
|
||||||
|
"your credentials (OS_AUTH_USER, OS_AUTH_KEY, ...)."
|
||||||
|
return FAILURE
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
|
options = args[0]
|
||||||
|
if options.debug:
|
||||||
|
raise
|
||||||
print "Failed to %s. Got error:" % action
|
print "Failed to %s. Got error:" % action
|
||||||
pieces = unicode(e).split('\n')
|
pieces = unicode(e).split('\n')
|
||||||
for piece in pieces:
|
for piece in pieces:
|
||||||
@ -963,9 +970,14 @@ def get_client(options):
|
|||||||
specified by the --host and --port options
|
specified by the --host and --port options
|
||||||
supplied to the CLI
|
supplied to the CLI
|
||||||
"""
|
"""
|
||||||
return glance_client.Client(host=options.host,
|
creds = dict(username=os.getenv('OS_AUTH_USER'),
|
||||||
port=options.port,
|
password=os.getenv('OS_AUTH_KEY'),
|
||||||
auth_tok=options.auth_token)
|
tenant=os.getenv('OS_AUTH_TENANT'),
|
||||||
|
auth_url=os.getenv('OS_AUTH_URL'),
|
||||||
|
strategy=os.getenv('OS_AUTH_STRATEGY', 'noauth'))
|
||||||
|
|
||||||
|
return glance_client.Client(host=options.host, port=options.port,
|
||||||
|
auth_tok=options.auth_token, creds=creds)
|
||||||
|
|
||||||
|
|
||||||
def create_options(parser):
|
def create_options(parser):
|
||||||
@ -977,6 +989,8 @@ def create_options(parser):
|
|||||||
"""
|
"""
|
||||||
parser.add_option('-v', '--verbose', default=False, action="store_true",
|
parser.add_option('-v', '--verbose', default=False, action="store_true",
|
||||||
help="Print more verbose output")
|
help="Print more verbose output")
|
||||||
|
parser.add_option('-d', '--debug', default=False, action="store_true",
|
||||||
|
help="Print more verbose output")
|
||||||
parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
|
parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
|
||||||
help="Address of Glance API host. "
|
help="Address of Glance API host. "
|
||||||
"Default: %default")
|
"Default: %default")
|
||||||
|
@ -36,26 +36,7 @@ class V1Client(base_client.BaseClient):
|
|||||||
"""Main client class for accessing Glance resources"""
|
"""Main client class for accessing Glance resources"""
|
||||||
|
|
||||||
DEFAULT_PORT = 9292
|
DEFAULT_PORT = 9292
|
||||||
|
DEFAULT_DOC_ROOT = "/v1"
|
||||||
def __init__(self, host, port=None, use_ssl=False, doc_root="/v1",
|
|
||||||
auth_tok=None):
|
|
||||||
"""
|
|
||||||
Creates a new client to a Glance API service.
|
|
||||||
|
|
||||||
:param host: The host where Glance resides
|
|
||||||
:param port: The port where Glance resides (defaults to 9292)
|
|
||||||
:param use_ssl: Should we use HTTPS? (defaults to False)
|
|
||||||
:param doc_root: Prefix for all URLs we request from host
|
|
||||||
:param auth_tok: The auth token to pass to the server
|
|
||||||
"""
|
|
||||||
port = port or self.DEFAULT_PORT
|
|
||||||
self.doc_root = doc_root
|
|
||||||
super(Client, self).__init__(host, port, use_ssl, auth_tok)
|
|
||||||
|
|
||||||
def do_request(self, method, action, body=None, headers=None, params=None):
|
|
||||||
action = "%s/%s" % (self.doc_root, action.lstrip("/"))
|
|
||||||
return super(V1Client, self).do_request(method, action, body,
|
|
||||||
headers, params)
|
|
||||||
|
|
||||||
def get_images(self, **kwargs):
|
def get_images(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
202
glance/common/auth.py
Normal file
202
glance/common/auth.py
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This auth module is intended to allow Openstack client-tools to select from a
|
||||||
|
variety of authentication strategies, including NoAuth (the default), and
|
||||||
|
Keystone (an identity management system).
|
||||||
|
|
||||||
|
> auth_plugin = AuthPlugin(creds)
|
||||||
|
|
||||||
|
> auth_plugin.authenticate()
|
||||||
|
|
||||||
|
> auth_plugin.auth_token
|
||||||
|
abcdefg
|
||||||
|
|
||||||
|
> auth_plugin.management_url
|
||||||
|
http://service_endpoint/
|
||||||
|
"""
|
||||||
|
import httplib2
|
||||||
|
import json
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from glance.common import exception
|
||||||
|
|
||||||
|
|
||||||
|
class BaseStrategy(object):
|
||||||
|
def __init__(self, creds):
|
||||||
|
self.creds = creds
|
||||||
|
self.auth_token = None
|
||||||
|
|
||||||
|
# TODO(sirp): For now we're just dealing with one endpoint, eventually
|
||||||
|
# this should expose the entire service catalog so that the client can
|
||||||
|
# choose which service/region/(public/private net) combo they want.
|
||||||
|
self.management_url = None
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class NoAuthStrategy(BaseStrategy):
|
||||||
|
def authenticate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class KeystoneStrategy(BaseStrategy):
|
||||||
|
MAX_REDIRECTS = 10
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
"""Authenticate with the Keystone service.
|
||||||
|
|
||||||
|
There are a few scenarios to consider here:
|
||||||
|
|
||||||
|
1. Which version of Keystone are we using? v1 which uses headers to
|
||||||
|
pass the credentials, or v2 which uses a JSON encoded request body?
|
||||||
|
|
||||||
|
2. Keystone may respond back with a redirection using a 305 status
|
||||||
|
code.
|
||||||
|
|
||||||
|
3. We may attempt a v1 auth when v2 is what's called for. In this
|
||||||
|
case, we rewrite the url to contain /v2.0/ and retry using the v2
|
||||||
|
protocol.
|
||||||
|
"""
|
||||||
|
def _authenticate(auth_url):
|
||||||
|
token_url = urlparse.urljoin(auth_url, "tokens")
|
||||||
|
|
||||||
|
# 1. Check Keystone version
|
||||||
|
is_v2 = auth_url.rstrip('/').endswith('v2.0')
|
||||||
|
if is_v2:
|
||||||
|
self._v2_auth(token_url)
|
||||||
|
else:
|
||||||
|
self._v1_auth(token_url)
|
||||||
|
|
||||||
|
for required in ('username', 'password', 'auth_url'):
|
||||||
|
if required not in self.creds:
|
||||||
|
raise Exception(_("'%s' must be included in creds") %
|
||||||
|
required)
|
||||||
|
|
||||||
|
auth_url = self.creds['auth_url']
|
||||||
|
for _ in range(self.MAX_REDIRECTS):
|
||||||
|
try:
|
||||||
|
_authenticate(auth_url)
|
||||||
|
except exception.RedirectException as e:
|
||||||
|
# 2. Keystone may redirect us
|
||||||
|
auth_url = e.url
|
||||||
|
except exception.AuthorizationFailure:
|
||||||
|
# 3. In some configurations nova makes redirection to
|
||||||
|
# v2.0 keystone endpoint. Also, new location does not
|
||||||
|
# contain real endpoint, only hostname and port.
|
||||||
|
if 'v2.0' not in auth_url:
|
||||||
|
auth_url = urlparse.urljoin(auth_url, 'v2.0/')
|
||||||
|
else:
|
||||||
|
# If we sucessfully auth'd, then memorize the correct auth_url
|
||||||
|
# for future use.
|
||||||
|
self.creds['auth_url'] = auth_url
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Guard against a redirection loop
|
||||||
|
raise Exception(_("Exceeded max redirects %s") % MAX_REDIRECTS)
|
||||||
|
|
||||||
|
def _v1_auth(self, token_url):
|
||||||
|
creds = self.creds
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
headers['X-Auth-User'] = creds['username']
|
||||||
|
headers['X-Auth-Key'] = creds['password']
|
||||||
|
|
||||||
|
tenant = creds.get('tenant')
|
||||||
|
if tenant:
|
||||||
|
headers['X-Auth-Tenant'] = tenant
|
||||||
|
|
||||||
|
resp, resp_body = self._do_request(token_url, 'GET', headers=headers)
|
||||||
|
|
||||||
|
if resp.status in (200, 204):
|
||||||
|
try:
|
||||||
|
self.management_url = resp['x-server-management-url']
|
||||||
|
self.auth_token = resp['x-auth-token']
|
||||||
|
except KeyError:
|
||||||
|
raise exception.AuthorizationFailure()
|
||||||
|
elif resp.status == 305:
|
||||||
|
raise exception.RedirectException(resp['location'])
|
||||||
|
elif resp.status == 401:
|
||||||
|
raise exception.NotAuthorized()
|
||||||
|
else:
|
||||||
|
raise Exception(_('Unexpected response: %s' % resp.status))
|
||||||
|
|
||||||
|
def _v2_auth(self, token_url):
|
||||||
|
creds = self.creds
|
||||||
|
|
||||||
|
creds = {"passwordCredentials": {"username": creds['username'],
|
||||||
|
"password": creds['password']}}
|
||||||
|
|
||||||
|
tenant = creds.get('tenant')
|
||||||
|
if tenant:
|
||||||
|
creds['passwordCredentials']['tenantId'] = tenant
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
req_body = json.dumps(creds)
|
||||||
|
|
||||||
|
resp, resp_body = self._do_request(
|
||||||
|
token_url, 'POST', headers=headers, body=req_body)
|
||||||
|
|
||||||
|
if resp.status == 200:
|
||||||
|
resp_auth = json.loads(resp_body)['auth']
|
||||||
|
|
||||||
|
# FIXME(sirp): for now just using the first endpoint we get back
|
||||||
|
# from the service catalog for glance, and using the public url.
|
||||||
|
glance_info = resp_auth['serviceCatalog']['glance']
|
||||||
|
glance_endpoint = glance_info[0]['publicURL']
|
||||||
|
|
||||||
|
self.management_url = glance_endpoint
|
||||||
|
self.auth_token = resp_auth['token']['id']
|
||||||
|
elif resp.status == 305:
|
||||||
|
raise RedirectException(resp['location'])
|
||||||
|
elif resp.status == 401:
|
||||||
|
raise exception.NotAuthorized()
|
||||||
|
else:
|
||||||
|
raise Exception(_('Unexpected response: %s') % resp.status)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return self.auth_token is not None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _do_request(url, method, headers=None, body=None):
|
||||||
|
headers = headers or {}
|
||||||
|
conn = httplib2.Http()
|
||||||
|
conn.force_exception_to_status_code = True
|
||||||
|
headers['User-Agent'] = 'glance-client'
|
||||||
|
resp, resp_body = conn.request(url, method, headers=headers, body=body)
|
||||||
|
return resp, resp_body
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_from_strategy(strategy):
|
||||||
|
if strategy == 'noauth':
|
||||||
|
return NoAuthStrategy
|
||||||
|
elif strategy == 'keystone':
|
||||||
|
return KeystoneStrategy
|
||||||
|
else:
|
||||||
|
raise Exception(_("Unknown auth strategy '%s'") % strategy)
|
@ -2,6 +2,7 @@ import httplib
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import urllib
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
|
||||||
# See http://code.google.com/p/python-nose/issues/detail?id=373
|
# See http://code.google.com/p/python-nose/issues/detail?id=373
|
||||||
# The code below enables glance.client standalone to work with i18n _() blocks
|
# The code below enables glance.client standalone to work with i18n _() blocks
|
||||||
@ -9,6 +10,7 @@ import __builtin__
|
|||||||
if not hasattr(__builtin__, '_'):
|
if not hasattr(__builtin__, '_'):
|
||||||
setattr(__builtin__, '_', lambda x: x)
|
setattr(__builtin__, '_', lambda x: x)
|
||||||
|
|
||||||
|
from glance.common import auth
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
|
|
||||||
|
|
||||||
@ -46,8 +48,11 @@ class BaseClient(object):
|
|||||||
"""A base client class"""
|
"""A base client class"""
|
||||||
|
|
||||||
CHUNKSIZE = 65536
|
CHUNKSIZE = 65536
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_DOC_ROOT = None
|
||||||
|
|
||||||
def __init__(self, host, port, use_ssl, auth_tok):
|
def __init__(self, host, port=None, use_ssl=False, auth_tok=None,
|
||||||
|
creds=None, doc_root=None):
|
||||||
"""
|
"""
|
||||||
Creates a new client to some service.
|
Creates a new client to some service.
|
||||||
|
|
||||||
@ -55,19 +60,53 @@ class BaseClient(object):
|
|||||||
:param port: The port where service resides
|
:param port: The port where service resides
|
||||||
:param use_ssl: Should we use HTTPS?
|
:param use_ssl: Should we use HTTPS?
|
||||||
:param auth_tok: The auth token to pass to the server
|
:param auth_tok: The auth token to pass to the server
|
||||||
|
:param creds: The credentials to pass to the auth plugin
|
||||||
|
:param doc_root: Prefix for all URLs we request from host
|
||||||
"""
|
"""
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port or self.DEFAULT_PORT
|
||||||
self.use_ssl = use_ssl
|
self.use_ssl = use_ssl
|
||||||
self.auth_tok = auth_tok
|
self.auth_tok = auth_tok
|
||||||
|
self.creds = creds or {}
|
||||||
self.connection = None
|
self.connection = None
|
||||||
|
self.doc_root = self.DEFAULT_DOC_ROOT if doc_root is None else doc_root
|
||||||
|
self.auth_plugin = self.make_auth_plugin(self.creds)
|
||||||
|
|
||||||
def set_auth_token(self, auth_tok):
|
def set_auth_token(self, auth_tok):
|
||||||
"""
|
"""
|
||||||
Updates the authentication token for this client connection.
|
Updates the authentication token for this client connection.
|
||||||
"""
|
"""
|
||||||
|
# FIXME(sirp): Nova image/glance.py currently calls this. Since this
|
||||||
|
# method isn't really doing anything useful[1], we should go ahead and
|
||||||
|
# rip it out, first in Nova, then here. Steps:
|
||||||
|
#
|
||||||
|
# 1. Change auth_tok in Glance to auth_token
|
||||||
|
# 2. Change image/glance.py in Nova to use client.auth_token
|
||||||
|
# 3. Remove this method
|
||||||
|
#
|
||||||
|
# [1] http://mail.python.org/pipermail/tutor/2003-October/025932.html
|
||||||
self.auth_tok = auth_tok
|
self.auth_tok = auth_tok
|
||||||
|
|
||||||
|
def configure_from_url(self, url):
|
||||||
|
"""
|
||||||
|
Setups the connection based on the given url.
|
||||||
|
|
||||||
|
The form is:
|
||||||
|
|
||||||
|
<http|https>://<host>:port/doc_root
|
||||||
|
"""
|
||||||
|
parsed = urlparse.urlparse(url)
|
||||||
|
self.use_ssl = parsed.scheme == 'https'
|
||||||
|
self.host = parsed.hostname
|
||||||
|
self.port = parsed.port or 80
|
||||||
|
self.doc_root = parsed.path
|
||||||
|
|
||||||
|
def make_auth_plugin(self, creds):
|
||||||
|
strategy = creds.get('strategy', 'noauth')
|
||||||
|
plugin_class = auth.get_plugin_from_strategy(strategy)
|
||||||
|
plugin = plugin_class(creds)
|
||||||
|
return plugin
|
||||||
|
|
||||||
def get_connection_type(self):
|
def get_connection_type(self):
|
||||||
"""
|
"""
|
||||||
Returns the proper connection type
|
Returns the proper connection type
|
||||||
@ -77,8 +116,38 @@ class BaseClient(object):
|
|||||||
else:
|
else:
|
||||||
return httplib.HTTPConnection
|
return httplib.HTTPConnection
|
||||||
|
|
||||||
|
def _authenticate(self, force_reauth=False):
|
||||||
|
auth_plugin = self.auth_plugin
|
||||||
|
|
||||||
|
if not auth_plugin.is_authenticated or force_reauth:
|
||||||
|
auth_plugin.authenticate()
|
||||||
|
|
||||||
|
self.auth_tok = auth_plugin.auth_token
|
||||||
|
|
||||||
|
management_url = auth_plugin.management_url
|
||||||
|
if management_url:
|
||||||
|
self.configure_from_url(management_url)
|
||||||
|
|
||||||
def do_request(self, method, action, body=None, headers=None,
|
def do_request(self, method, action, body=None, headers=None,
|
||||||
params=None):
|
params=None):
|
||||||
|
headers = headers or {}
|
||||||
|
|
||||||
|
if not self.auth_tok:
|
||||||
|
self._authenticate()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._do_request(
|
||||||
|
method, action, body=body, headers=headers, params=params)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
self._authenticate(force_reauth=True)
|
||||||
|
try:
|
||||||
|
return self._do_request(
|
||||||
|
method, action, body=body, headers=headers, params=params)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _do_request(self, method, action, body=None, headers=None,
|
||||||
|
params=None):
|
||||||
"""
|
"""
|
||||||
Connects to the server and issues a request. Handles converting
|
Connects to the server and issues a request. Handles converting
|
||||||
any returned HTTP error status codes to OpenStack/Glance exceptions
|
any returned HTTP error status codes to OpenStack/Glance exceptions
|
||||||
@ -113,10 +182,15 @@ class BaseClient(object):
|
|||||||
try:
|
try:
|
||||||
connection_type = self.get_connection_type()
|
connection_type = self.get_connection_type()
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
|
|
||||||
if 'x-auth-token' not in headers and self.auth_tok:
|
if 'x-auth-token' not in headers and self.auth_tok:
|
||||||
headers['x-auth-token'] = self.auth_tok
|
headers['x-auth-token'] = self.auth_tok
|
||||||
|
|
||||||
c = connection_type(self.host, self.port)
|
c = connection_type(self.host, self.port)
|
||||||
|
|
||||||
|
if self.doc_root:
|
||||||
|
action = '/'.join([self.doc_root, action.lstrip('/')])
|
||||||
|
|
||||||
# Do a simple request or a chunked request, depending
|
# Do a simple request or a chunked request, depending
|
||||||
# on whether the body param is a file-like object and
|
# on whether the body param is a file-like object and
|
||||||
# the method is PUT or POST
|
# the method is PUT or POST
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from glance.common import config
|
from glance.common import config
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
@ -28,11 +27,13 @@ class RequestContext(object):
|
|||||||
accesses the system, as well as additional request information.
|
accesses the system, as well as additional request information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False,
|
def __init__(self, auth_tok=None, user=None, tenant=None, roles=None,
|
||||||
read_only=False, show_deleted=False, owner_is_tenant=True):
|
is_admin=False, read_only=False, show_deleted=False,
|
||||||
|
owner_is_tenant=True):
|
||||||
self.auth_tok = auth_tok
|
self.auth_tok = auth_tok
|
||||||
self.user = user
|
self.user = user
|
||||||
self.tenant = tenant
|
self.tenant = tenant
|
||||||
|
self.roles = roles or []
|
||||||
self.is_admin = is_admin
|
self.is_admin = is_admin
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
self.show_deleted = show_deleted
|
self.show_deleted = show_deleted
|
||||||
@ -70,10 +71,47 @@ class ContextMiddleware(wsgi.Middleware):
|
|||||||
"""
|
"""
|
||||||
Extract any authentication information in the request and
|
Extract any authentication information in the request and
|
||||||
construct an appropriate context from it.
|
construct an appropriate context from it.
|
||||||
|
|
||||||
|
A few scenarios exist:
|
||||||
|
|
||||||
|
1. If X-Auth-Token is passed in, then consult TENANT and ROLE headers
|
||||||
|
to determine permissions.
|
||||||
|
|
||||||
|
2. An X-Auth-Token was passed in, but the Identity-Status is not
|
||||||
|
confirmed. For now, just raising a NotAuthorized exception.
|
||||||
|
|
||||||
|
3. X-Auth-Token is omitted. If we were using Keystone, then the
|
||||||
|
tokenauth middleware would have rejected the request, so we must be
|
||||||
|
using NoAuth. In that case, assume that is_admin=True.
|
||||||
"""
|
"""
|
||||||
# Use the default empty context, with admin turned on for
|
# TODO(sirp): should we be using the glance_tokeauth shim from
|
||||||
# backwards compatibility
|
# Keystone here? If we do, we need to make sure it handles the NoAuth
|
||||||
req.context = self.make_context(is_admin=True)
|
# case
|
||||||
|
auth_tok = req.headers.get('X-Auth-Token',
|
||||||
|
req.headers.get('X-Storage-Token'))
|
||||||
|
if auth_tok:
|
||||||
|
if req.headers.get('X-Identity-Status') == 'Confirmed':
|
||||||
|
# 1. Auth-token is passed, check other headers
|
||||||
|
user = req.headers.get('X-User')
|
||||||
|
tenant = req.headers.get('X-Tenant')
|
||||||
|
roles = [r.strip()
|
||||||
|
for r in req.headers.get('X-Role', '').split(',')]
|
||||||
|
is_admin = 'Admin' in roles
|
||||||
|
else:
|
||||||
|
# 2. Indentity-Status not confirmed
|
||||||
|
# FIXME(sirp): not sure what the correct behavior in this case
|
||||||
|
# is; just raising NotAuthorized for now
|
||||||
|
raise exception.NotAuthorized()
|
||||||
|
else:
|
||||||
|
# 3. Auth-token is ommited, assume NoAuth
|
||||||
|
user = None
|
||||||
|
tenant = None
|
||||||
|
roles = []
|
||||||
|
is_admin = True
|
||||||
|
|
||||||
|
req.context = self.make_context(
|
||||||
|
auth_tok=auth_tok, user=user, tenant=tenant, roles=roles,
|
||||||
|
is_admin=is_admin)
|
||||||
|
|
||||||
|
|
||||||
def filter_factory(global_conf, **local_conf):
|
def filter_factory(global_conf, **local_conf):
|
||||||
|
@ -76,6 +76,10 @@ class Duplicate(Error):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationFailure(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotAuthorized(Error):
|
class NotAuthorized(Error):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -88,6 +92,11 @@ class Invalid(Error):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectException(Error):
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
class BadInputError(Exception):
|
class BadInputError(Exception):
|
||||||
"""Error resulting from a client sending bad input to a server"""
|
"""Error resulting from a client sending bad input to a server"""
|
||||||
pass
|
pass
|
||||||
|
@ -26,10 +26,10 @@ from glance.registry import client
|
|||||||
logger = logging.getLogger('glance.registry')
|
logger = logging.getLogger('glance.registry')
|
||||||
|
|
||||||
|
|
||||||
def get_registry_client(options, cxt):
|
def get_registry_client(options, context):
|
||||||
host = options['registry_host']
|
host = options['registry_host']
|
||||||
port = int(options['registry_port'])
|
port = int(options['registry_port'])
|
||||||
return client.RegistryClient(host, port, auth_tok=cxt.auth_tok)
|
return client.RegistryClient(host, port, auth_tok=context.auth_tok)
|
||||||
|
|
||||||
|
|
||||||
def get_images_list(options, context, **kwargs):
|
def get_images_list(options, context, **kwargs):
|
||||||
|
@ -33,18 +33,6 @@ class RegistryClient(BaseClient):
|
|||||||
|
|
||||||
DEFAULT_PORT = 9191
|
DEFAULT_PORT = 9191
|
||||||
|
|
||||||
def __init__(self, host, port=None, use_ssl=False, auth_tok=None):
|
|
||||||
"""
|
|
||||||
Creates a new client to a Glance Registry service.
|
|
||||||
|
|
||||||
:param host: The host where Glance resides
|
|
||||||
:param port: The port where Glance resides (defaults to 9191)
|
|
||||||
:param use_ssl: Should we use HTTPS? (defaults to False)
|
|
||||||
:param auth_tok: The auth token to pass to the server
|
|
||||||
"""
|
|
||||||
port = port or self.DEFAULT_PORT
|
|
||||||
super(RegistryClient, self).__init__(host, port, use_ssl, auth_tok)
|
|
||||||
|
|
||||||
def get_images(self, **kwargs):
|
def get_images(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns a list of image id/name mappings from Registry
|
Returns a list of image id/name mappings from Registry
|
||||||
|
@ -26,9 +26,17 @@ from glance.tests.utils import execute
|
|||||||
|
|
||||||
|
|
||||||
class TestBinGlance(functional.FunctionalTest):
|
class TestBinGlance(functional.FunctionalTest):
|
||||||
|
|
||||||
"""Functional tests for the bin/glance CLI tool"""
|
"""Functional tests for the bin/glance CLI tool"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestBinGlance, self).setUp()
|
||||||
|
|
||||||
|
# NOTE(sirp): This is needed in case we are running the tests under an
|
||||||
|
# environment in which OS_AUTH_STRATEGY=keystone. The test server we
|
||||||
|
# spin up won't have keystone support, so we need to switch to the
|
||||||
|
# NoAuth strategy.
|
||||||
|
os.environ['OS_AUTH_STRATEGY'] = 'noauth'
|
||||||
|
|
||||||
def test_add_list_delete_list(self):
|
def test_add_list_delete_list(self):
|
||||||
"""
|
"""
|
||||||
We test the following:
|
We test the following:
|
||||||
|
@ -1725,3 +1725,33 @@ class TestClient(unittest.TestCase):
|
|||||||
"""Tests deleting image members"""
|
"""Tests deleting image members"""
|
||||||
self.assertRaises(exception.NotAuthorized,
|
self.assertRaises(exception.NotAuthorized,
|
||||||
self.client.delete_member, 2, 'pattieblack')
|
self.client.delete_member, 2, 'pattieblack')
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfigureClientFromURL(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = client.Client("0.0.0.0", doc_root="")
|
||||||
|
|
||||||
|
def assertConfiguration(self, url, host, port, use_ssl, doc_root):
|
||||||
|
self.client.configure_from_url(url)
|
||||||
|
self.assertEquals(host, self.client.host)
|
||||||
|
self.assertEquals(port, self.client.port)
|
||||||
|
self.assertEquals(use_ssl, self.client.use_ssl)
|
||||||
|
self.assertEquals(doc_root, self.client.doc_root)
|
||||||
|
|
||||||
|
def test_no_port_no_ssl_no_doc_root(self):
|
||||||
|
self.assertConfiguration(
|
||||||
|
url='http://www.example.com',
|
||||||
|
host='www.example.com',
|
||||||
|
port=80,
|
||||||
|
use_ssl=False,
|
||||||
|
doc_root=''
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_port_ssl_doc_root(self):
|
||||||
|
self.assertConfiguration(
|
||||||
|
url='https://www.example.com:8000/prefix/',
|
||||||
|
host='www.example.com',
|
||||||
|
port=8000,
|
||||||
|
use_ssl=True,
|
||||||
|
doc_root='/prefix/'
|
||||||
|
)
|
||||||
|
10
tools/nova_to_os_env.sh
Normal file
10
tools/nova_to_os_env.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# This file is intended to be sourced to convert old-style NOVA environment
|
||||||
|
# variables to new-style OS.
|
||||||
|
#
|
||||||
|
# The plan is to add this to novarc, but until that lands, it's useful to have
|
||||||
|
# this in Glance.
|
||||||
|
export OS_AUTH_USER=$NOVA_USERNAME
|
||||||
|
export OS_AUTH_KEY=$NOVA_API_KEY
|
||||||
|
export OS_AUTH_TENANT=$NOVA_PROJECT_ID
|
||||||
|
export OS_AUTH_URL=$NOVA_URL
|
||||||
|
export OS_AUTH_STRATEGY=$NOVA_AUTH_STRATEGY
|
Loading…
Reference in New Issue
Block a user