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:
|
||||
ret = func(*args, **kwargs)
|
||||
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:
|
||||
options = args[0]
|
||||
if options.debug:
|
||||
raise
|
||||
print "Failed to %s. Got error:" % action
|
||||
pieces = unicode(e).split('\n')
|
||||
for piece in pieces:
|
||||
@ -963,9 +970,14 @@ def get_client(options):
|
||||
specified by the --host and --port options
|
||||
supplied to the CLI
|
||||
"""
|
||||
return glance_client.Client(host=options.host,
|
||||
port=options.port,
|
||||
auth_tok=options.auth_token)
|
||||
creds = dict(username=os.getenv('OS_AUTH_USER'),
|
||||
password=os.getenv('OS_AUTH_KEY'),
|
||||
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):
|
||||
@ -977,6 +989,8 @@ def create_options(parser):
|
||||
"""
|
||||
parser.add_option('-v', '--verbose', default=False, action="store_true",
|
||||
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",
|
||||
help="Address of Glance API host. "
|
||||
"Default: %default")
|
||||
|
@ -36,26 +36,7 @@ class V1Client(base_client.BaseClient):
|
||||
"""Main client class for accessing Glance resources"""
|
||||
|
||||
DEFAULT_PORT = 9292
|
||||
|
||||
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)
|
||||
DEFAULT_DOC_ROOT = "/v1"
|
||||
|
||||
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 socket
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
# See http://code.google.com/p/python-nose/issues/detail?id=373
|
||||
# The code below enables glance.client standalone to work with i18n _() blocks
|
||||
@ -9,6 +10,7 @@ import __builtin__
|
||||
if not hasattr(__builtin__, '_'):
|
||||
setattr(__builtin__, '_', lambda x: x)
|
||||
|
||||
from glance.common import auth
|
||||
from glance.common import exception
|
||||
|
||||
|
||||
@ -46,8 +48,11 @@ class BaseClient(object):
|
||||
"""A base client class"""
|
||||
|
||||
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.
|
||||
|
||||
@ -55,19 +60,53 @@ class BaseClient(object):
|
||||
:param port: The port where service resides
|
||||
:param use_ssl: Should we use HTTPS?
|
||||
: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.port = port
|
||||
self.port = port or self.DEFAULT_PORT
|
||||
self.use_ssl = use_ssl
|
||||
self.auth_tok = auth_tok
|
||||
self.creds = creds or {}
|
||||
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):
|
||||
"""
|
||||
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
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns the proper connection type
|
||||
@ -77,8 +116,38 @@ class BaseClient(object):
|
||||
else:
|
||||
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,
|
||||
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
|
||||
any returned HTTP error status codes to OpenStack/Glance exceptions
|
||||
@ -113,10 +182,15 @@ class BaseClient(object):
|
||||
try:
|
||||
connection_type = self.get_connection_type()
|
||||
headers = headers or {}
|
||||
|
||||
if 'x-auth-token' not in headers and self.auth_tok:
|
||||
headers['x-auth-token'] = self.auth_tok
|
||||
|
||||
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
|
||||
# on whether the body param is a file-like object and
|
||||
# the method is PUT or POST
|
||||
|
@ -14,7 +14,6 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from glance.common import config
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
@ -28,11 +27,13 @@ class RequestContext(object):
|
||||
accesses the system, as well as additional request information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False,
|
||||
read_only=False, show_deleted=False, owner_is_tenant=True):
|
||||
def __init__(self, auth_tok=None, user=None, tenant=None, roles=None,
|
||||
is_admin=False, read_only=False, show_deleted=False,
|
||||
owner_is_tenant=True):
|
||||
self.auth_tok = auth_tok
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
self.roles = roles or []
|
||||
self.is_admin = is_admin
|
||||
self.read_only = read_only
|
||||
self.show_deleted = show_deleted
|
||||
@ -70,10 +71,47 @@ class ContextMiddleware(wsgi.Middleware):
|
||||
"""
|
||||
Extract any authentication information in the request and
|
||||
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
|
||||
# backwards compatibility
|
||||
req.context = self.make_context(is_admin=True)
|
||||
# TODO(sirp): should we be using the glance_tokeauth shim from
|
||||
# Keystone here? If we do, we need to make sure it handles the NoAuth
|
||||
# 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):
|
||||
|
@ -76,6 +76,10 @@ class Duplicate(Error):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(Error):
|
||||
pass
|
||||
|
||||
|
||||
class NotAuthorized(Error):
|
||||
pass
|
||||
|
||||
@ -88,6 +92,11 @@ class Invalid(Error):
|
||||
pass
|
||||
|
||||
|
||||
class RedirectException(Error):
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
|
||||
class BadInputError(Exception):
|
||||
"""Error resulting from a client sending bad input to a server"""
|
||||
pass
|
||||
|
@ -26,10 +26,10 @@ from glance.registry import client
|
||||
logger = logging.getLogger('glance.registry')
|
||||
|
||||
|
||||
def get_registry_client(options, cxt):
|
||||
def get_registry_client(options, context):
|
||||
host = options['registry_host']
|
||||
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):
|
||||
|
@ -33,18 +33,6 @@ class RegistryClient(BaseClient):
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns a list of image id/name mappings from Registry
|
||||
|
@ -26,9 +26,17 @@ from glance.tests.utils import execute
|
||||
|
||||
|
||||
class TestBinGlance(functional.FunctionalTest):
|
||||
|
||||
"""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):
|
||||
"""
|
||||
We test the following:
|
||||
|
@ -1725,3 +1725,33 @@ class TestClient(unittest.TestCase):
|
||||
"""Tests deleting image members"""
|
||||
self.assertRaises(exception.NotAuthorized,
|
||||
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