Don't call CS if a token + URL are provided

Adds --os-auth-token (matching Glance client at least) to accept a
pre-obtained authentication token.

CS will be called iff:
      One or both of token and URL are not provided;
  AND the cache is empty/disabled or we don't have a tenant-id.

Removed some code altering the auth request to a GET if a token is
supplied - did not work for me and I think the path was dead previously.
Fixed test to account for this change.

Completes blueprint token-endpoint-instantiation

Change-Id: I67410b80e506bb80c152223cd113b7139a62a536
This commit is contained in:
Alexis Lee 2013-12-19 19:26:06 +00:00
parent 1a20d2964d
commit bd9ebc5d27
6 changed files with 127 additions and 64 deletions

@ -48,13 +48,19 @@ class HTTPClient(object):
timings=False, bypass_url=None, timings=False, bypass_url=None,
os_cache=False, no_cache=True, os_cache=False, no_cache=True,
http_log_debug=False, auth_system='keystone', http_log_debug=False, auth_system='keystone',
auth_plugin=None, auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None): cacert=None, tenant_id=None):
self.user = user self.user = user
self.password = password self.password = password
self.projectid = projectid self.projectid = projectid
self.tenant_id = tenant_id self.tenant_id = tenant_id
# This will be called by #_get_password if self.password is None.
# EG if a password can only be obtained by prompting the user, but a
# token is available, you don't want to prompt until the token has
# been proven invalid
self.password_func = None
if auth_system and auth_system != 'keystone' and not auth_plugin: if auth_system and auth_system != 'keystone' and not auth_plugin:
raise exceptions.AuthSystemNotFound(auth_system) raise exceptions.AuthSystemNotFound(auth_system)
@ -80,8 +86,8 @@ class HTTPClient(object):
self.times = [] # [("item", starttime, endtime), ...] self.times = [] # [("item", starttime, endtime), ...]
self.management_url = None self.management_url = self.bypass_url or None
self.auth_token = None self.auth_token = auth_token
self.proxy_token = proxy_token self.proxy_token = proxy_token
self.proxy_tenant_id = proxy_tenant_id self.proxy_tenant_id = proxy_tenant_id
self.keyring_saver = None self.keyring_saver = None
@ -239,6 +245,11 @@ class HTTPClient(object):
except exceptions.Unauthorized: except exceptions.Unauthorized:
raise e raise e
def _get_password(self):
if not self.password and self.password_func:
self.password = self.password_func()
return self.password
def get(self, url, **kwargs): def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs) return self._cs_request(url, 'GET', **kwargs)
@ -324,6 +335,10 @@ class HTTPClient(object):
self.version = part self.version = part
break break
if self.auth_token and self.management_url:
self._save_keys()
return
# TODO(sandy): Assume admin endpoint is 35357 for now. # TODO(sandy): Assume admin endpoint is 35357 for now.
# Ideally this is going to have to be provided by the service catalog. # Ideally this is going to have to be provided by the service catalog.
new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,))
@ -367,8 +382,13 @@ class HTTPClient(object):
elif not self.management_url: elif not self.management_url:
raise exceptions.Unauthorized('Nova Client') raise exceptions.Unauthorized('Nova Client')
self._save_keys()
def _save_keys(self):
# Store the token/mgmt url in the keyring for later requests. # Store the token/mgmt url in the keyring for later requests.
if self.keyring_saver and self.os_cache and not self.keyring_saved: if (self.keyring_saver and self.os_cache and not self.keyring_saved
and self.auth_token and self.management_url
and self.tenant_id):
self.keyring_saver.save(self.auth_token, self.keyring_saver.save(self.auth_token,
self.management_url, self.management_url,
self.tenant_id) self.tenant_id)
@ -380,7 +400,7 @@ class HTTPClient(object):
raise exceptions.NoTokenLookupException() raise exceptions.NoTokenLookupException()
headers = {'X-Auth-User': self.user, headers = {'X-Auth-User': self.user,
'X-Auth-Key': self.password} 'X-Auth-Key': self._get_password()}
if self.projectid: if self.projectid:
headers['X-Auth-Project-Id'] = self.projectid headers['X-Auth-Project-Id'] = self.projectid
@ -409,7 +429,7 @@ class HTTPClient(object):
else: else:
body = {"auth": { body = {"auth": {
"passwordCredentials": {"username": self.user, "passwordCredentials": {"username": self.user,
"password": self.password}}} "password": self._get_password()}}}
if self.tenant_id: if self.tenant_id:
body['auth']['tenantId'] = self.tenant_id body['auth']['tenantId'] = self.tenant_id
@ -423,16 +443,6 @@ class HTTPClient(object):
method = "POST" method = "POST"
token_url = url + "/tokens" token_url = url + "/tokens"
# if we have a valid auth token, use it instead of generating a new one
if self.auth_token:
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
token_url += "/" + self.auth_token
method = "GET"
body = None
if self.auth_token and self.tenant_id and self.management_url:
return None
# Make sure we follow redirects when trying to reach Keystone # Make sure we follow redirects when trying to reach Keystone
resp, respbody = self._time_request( resp, respbody = self._time_request(
token_url, token_url,

@ -82,6 +82,7 @@ class SecretsHelper(object):
self.args = args self.args = args
self.client = client self.client = client
self.key = None self.key = None
self._password = None
def _validate_string(self, text): def _validate_string(self, text):
if text is None or len(text) == 0: if text is None or len(text) == 0:
@ -143,11 +144,21 @@ class SecretsHelper(object):
@property @property
def password(self): def password(self):
if self._validate_string(self.args.os_password): # Cache password so we prompt user at most once
return self.args.os_password if self._password:
verify_pass = strutils.bool_from_string( pass
utils.env("OS_VERIFY_PASSWORD", default=False), True) elif self._validate_string(self.args.os_password):
return self._prompt_password(verify_pass) self._password = self.args.os_password
else:
verify_pass = strutils.bool_from_string(
utils.env("OS_VERIFY_PASSWORD", default=False), True)
self._password = self._prompt_password(verify_pass)
if not self._password:
raise exc.CommandError(
'Expecting a password provided via either '
'--os-password, env[OS_PASSWORD], or '
'prompted response')
return self._password
@property @property
def management_url(self): def management_url(self):
@ -260,6 +271,10 @@ class OpenStackComputeShell(object):
type=positive_non_zero_float, type=positive_non_zero_float,
help="Set HTTP call timeout (in seconds)") help="Set HTTP call timeout (in seconds)")
parser.add_argument('--os-auth-token',
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN]')
parser.add_argument('--os-username', parser.add_argument('--os-username',
metavar='<auth-user-name>', metavar='<auth-user-name>',
default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), default=utils.env('OS_USERNAME', 'NOVA_USERNAME'),
@ -491,7 +506,6 @@ class OpenStackComputeShell(object):
format=streamformat) format=streamformat)
def main(self, argv): def main(self, argv):
# Parse args once to find version and debug settings # Parse args once to find version and debug settings
parser = self.get_base_parser() parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv) (options, args) = parser.parse_known_args(argv)
@ -532,27 +546,37 @@ class OpenStackComputeShell(object):
self.do_bash_completion(args) self.do_bash_completion(args)
return 0 return 0
(os_username, os_tenant_name, os_tenant_id, os_auth_url, os_username = args.os_username
os_region_name, os_auth_system, endpoint_type, insecure, os_password = None # Fetched and set later as needed
service_type, service_name, volume_service_name, os_tenant_name = args.os_tenant_name
bypass_url, os_cache, cacert, timeout) = ( os_tenant_id = args.os_tenant_id
args.os_username, os_auth_url = args.os_auth_url
args.os_tenant_name, args.os_tenant_id, os_region_name = args.os_region_name
args.os_auth_url, os_auth_system = args.os_auth_system
args.os_region_name, args.os_auth_system, endpoint_type = args.endpoint_type
args.endpoint_type, args.insecure, args.service_type, insecure = args.insecure
args.service_name, args.volume_service_name, service_type = args.service_type
args.bypass_url, args.os_cache, service_name = args.service_name
args.os_cacert, args.timeout) volume_service_name = args.volume_service_name
bypass_url = args.bypass_url
os_cache = args.os_cache
cacert = args.os_cacert
timeout = args.timeout
# We may have either, both or none of these.
# If we have both, we don't need USERNAME, PASSWORD etc.
# Fill in the blanks from the SecretsHelper if possible.
# Finally, authenticate unless we have both.
# Note if we don't auth we probably don't have a tenant ID so we can't
# cache the token.
auth_token = args.os_auth_token if args.os_auth_token else None
management_url = bypass_url if bypass_url else None
if os_auth_system and os_auth_system != "keystone": if os_auth_system and os_auth_system != "keystone":
auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system) auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system)
else: else:
auth_plugin = None auth_plugin = None
# Fetched and set later as needed
os_password = None
if not endpoint_type: if not endpoint_type:
endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE
@ -567,9 +591,14 @@ class OpenStackComputeShell(object):
DEFAULT_OS_COMPUTE_API_VERSION] DEFAULT_OS_COMPUTE_API_VERSION]
service_type = utils.get_service_type(args.func) or service_type service_type = utils.get_service_type(args.func) or service_type
# If we have an auth token but no management_url, we must auth anyway.
# Expired tokens are handled by client.py:_cs_request
must_auth = not (utils.isunauthenticated(args.func)
or (auth_token and management_url))
#FIXME(usrleon): Here should be restrict for project id same as #FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not. # for os_username or os_password but for compatibility it is not.
if not utils.isunauthenticated(args.func): if must_auth:
if auth_plugin: if auth_plugin:
auth_plugin.parse_opts(args) auth_plugin.parse_opts(args)
@ -613,7 +642,7 @@ class OpenStackComputeShell(object):
region_name=os_region_name, endpoint_type=endpoint_type, region_name=os_region_name, endpoint_type=endpoint_type,
extensions=self.extensions, service_type=service_type, extensions=self.extensions, service_type=service_type,
service_name=service_name, auth_system=os_auth_system, service_name=service_name, auth_system=os_auth_system,
auth_plugin=auth_plugin, auth_plugin=auth_plugin, auth_token=auth_token,
volume_service_name=volume_service_name, volume_service_name=volume_service_name,
timings=args.timings, bypass_url=bypass_url, timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=options.debug, os_cache=os_cache, http_log_debug=options.debug,
@ -621,7 +650,7 @@ class OpenStackComputeShell(object):
# Now check for the password/token of which pieces of the # Now check for the password/token of which pieces of the
# identifying keyring key can come from the underlying client # identifying keyring key can come from the underlying client
if not utils.isunauthenticated(args.func): if must_auth:
helper = SecretsHelper(args, self.cs.client) helper = SecretsHelper(args, self.cs.client)
if (auth_plugin and auth_plugin.opts and if (auth_plugin and auth_plugin.opts and
"os_password" not in auth_plugin.opts): "os_password" not in auth_plugin.opts):
@ -629,31 +658,26 @@ class OpenStackComputeShell(object):
else: else:
use_pw = True use_pw = True
tenant_id, auth_token, management_url = (helper.tenant_id, tenant_id = helper.tenant_id
helper.auth_token, # Allow commandline to override cache
helper.management_url) if not auth_token:
auth_token = helper.auth_token
if not management_url:
management_url = helper.management_url
if tenant_id and auth_token and management_url: if tenant_id and auth_token and management_url:
self.cs.client.tenant_id = tenant_id self.cs.client.tenant_id = tenant_id
self.cs.client.auth_token = auth_token self.cs.client.auth_token = auth_token
self.cs.client.management_url = management_url self.cs.client.management_url = management_url
# authenticate just sets up some values in this case, no REST self.cs.client.password_fun = lambda: helper.password
# calls elif use_pw:
self.cs.authenticate() # We're missing something, so auth with user/pass and save
if use_pw: # the result in our helper.
# Auth using token must have failed or not happened self.cs.client.password = helper.password
# at all, so now switch to password mode and save
# the token when its gotten... using our keyring
# saver
os_password = helper.password
if not os_password:
raise exc.CommandError(
'Expecting a password provided via either '
'--os-password, env[OS_PASSWORD], or '
'prompted response')
self.cs.client.password = os_password
self.cs.client.keyring_saver = helper self.cs.client.keyring_saver = helper
try: try:
# This does a couple of bits which are useful even if we've
# got the token + service URL already. It exits fast in that case.
if not utils.isunauthenticated(args.func): if not utils.isunauthenticated(args.func):
self.cs.authenticate() self.cs.authenticate()
except exc.Unauthorized: except exc.Unauthorized:

@ -196,3 +196,23 @@ class ClientTest(utils.TestCase):
auth_url="foo/v2") auth_url="foo/v2")
cs.authenticate() cs.authenticate()
self.assertTrue(mock_authenticate.called) self.assertTrue(mock_authenticate.called)
def test_get_password_simple(self):
cs = novaclient.client.HTTPClient("user", "password", "", "")
cs.password_func = mock.Mock()
self.assertEqual(cs._get_password(), "password")
self.assertFalse(cs.password_func.called)
def test_get_password_none(self):
cs = novaclient.client.HTTPClient("user", None, "", "")
self.assertEqual(cs._get_password(), None)
def test_get_password_func(self):
cs = novaclient.client.HTTPClient("user", None, "", "")
cs.password_func = mock.Mock(return_value="password")
self.assertEqual(cs._get_password(), "password")
cs.password_func.assert_called_once_with()
cs.password_func = mock.Mock()
self.assertEqual(cs._get_password(), "password")
self.assertFalse(cs.password_func.called)

@ -379,15 +379,22 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
'User-Agent': cs.client.USER_AGENT, 'User-Agent': cs.client.USER_AGENT,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'X-Auth-Token': "FAKE_ID", }
body = {
'auth': {
'token': {
'id': cs.client.auth_token,
},
'tenantName': cs.client.projectid,
},
} }
token_url = cs.client.auth_url + "/tokens/FAKE_ID" token_url = cs.client.auth_url + "/tokens"
mock_request.assert_called_with( mock_request.assert_called_with(
"GET", "POST",
token_url, token_url,
headers=headers, headers=headers,
data="null", data=json.dumps(body),
allow_redirects=True, allow_redirects=True,
**self.TEST_REQUEST_BASE) **self.TEST_REQUEST_BASE)

@ -73,7 +73,7 @@ class Client(object):
volume_service_name=None, timings=False, volume_service_name=None, timings=False,
bypass_url=None, os_cache=False, no_cache=True, bypass_url=None, os_cache=False, no_cache=True,
http_log_debug=False, auth_system='keystone', http_log_debug=False, auth_system='keystone',
auth_plugin=None, auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None): cacert=None, tenant_id=None):
# FIXME(comstud): Rename the api_key argument above when we # FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument # know it's not being used as keyword argument
@ -131,6 +131,7 @@ class Client(object):
projectid=project_id, projectid=project_id,
tenant_id=tenant_id, tenant_id=tenant_id,
auth_url=auth_url, auth_url=auth_url,
auth_token=auth_token,
insecure=insecure, insecure=insecure,
timeout=timeout, timeout=timeout,
auth_system=auth_system, auth_system=auth_system,

@ -57,7 +57,7 @@ class Client(object):
volume_service_name=None, timings=False, volume_service_name=None, timings=False,
bypass_url=None, os_cache=False, no_cache=True, bypass_url=None, os_cache=False, no_cache=True,
http_log_debug=False, auth_system='keystone', http_log_debug=False, auth_system='keystone',
auth_plugin=None, auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None): cacert=None, tenant_id=None):
self.projectid = project_id self.projectid = project_id
self.tenant_id = tenant_id self.tenant_id = tenant_id
@ -96,6 +96,7 @@ class Client(object):
timeout=timeout, timeout=timeout,
auth_system=auth_system, auth_system=auth_system,
auth_plugin=auth_plugin, auth_plugin=auth_plugin,
auth_token=auth_token,
proxy_token=proxy_token, proxy_token=proxy_token,
proxy_tenant_id=proxy_tenant_id, proxy_tenant_id=proxy_tenant_id,
region_name=region_name, region_name=region_name,