Merge "Replace HttpConnection in auth_token with Requests"

This commit is contained in:
Jenkins
2013-09-13 05:11:02 +00:00
committed by Gerrit Code Review
3 changed files with 66 additions and 69 deletions

View File

@@ -193,6 +193,10 @@ Configuration Options
* ``certfile``: (required, if Keystone server requires client cert) * ``certfile``: (required, if Keystone server requires client cert)
* ``keyfile``: (required, if Keystone server requires client cert) This can be * ``keyfile``: (required, if Keystone server requires client cert) This can be
the same as the certfile if the certfile includes the private key. the same as the certfile if the certfile includes the private key.
* ``cafile``: (optional, defaults to use system CA bundle) the path to a PEM
encoded CA file/bundle that will be used to verify HTTPS connections.
* ``insecure``: (optional, default `False`) Don't verify HTTPS connections
(overrides `cafile`).
Caching for improved response Caching for improved response
----------------------------- -----------------------------

View File

@@ -145,9 +145,9 @@ keystone.token_info
""" """
import datetime import datetime
import httplib
import logging import logging
import os import os
import requests
import stat import stat
import tempfile import tempfile
import time import time
@@ -259,6 +259,10 @@ opts = [
help='Required if Keystone server requires client certificate'), help='Required if Keystone server requires client certificate'),
cfg.StrOpt('keyfile', cfg.StrOpt('keyfile',
help='Required if Keystone server requires client certificate'), help='Required if Keystone server requires client certificate'),
cfg.StrOpt('cafile', default=None,
help='A PEM encoded Certificate Authority to use when '
'verifying HTTPs connections. Defaults to system CAs.'),
cfg.BoolOpt('insecure', default=False, help='Verify HTTPS connections.'),
cfg.StrOpt('signing_dir', cfg.StrOpt('signing_dir',
help='Directory used to cache files related to PKI tokens'), help='Directory used to cache files related to PKI tokens'),
cfg.ListOpt('memcached_servers', cfg.ListOpt('memcached_servers',
@@ -354,43 +358,35 @@ class AuthProtocol(object):
(True, 'true', 't', '1', 'on', 'yes', 'y')) (True, 'true', 't', '1', 'on', 'yes', 'y'))
# where to find the auth service (we use this to validate tokens) # where to find the auth service (we use this to validate tokens)
self.auth_host = self._conf_get('auth_host') auth_host = self._conf_get('auth_host')
self.auth_port = int(self._conf_get('auth_port')) auth_port = int(self._conf_get('auth_port'))
self.auth_protocol = self._conf_get('auth_protocol') auth_protocol = self._conf_get('auth_protocol')
if not self._conf_get('http_handler'):
if self.auth_protocol == 'http':
self.http_client_class = httplib.HTTPConnection
else:
self.http_client_class = httplib.HTTPSConnection
else:
# Really only used for unit testing, since we need to
# have a fake handler set up before we issue an http
# request to get the list of versions supported by the
# server at the end of this initialization
self.http_client_class = self._conf_get('http_handler')
self.auth_admin_prefix = self._conf_get('auth_admin_prefix') self.auth_admin_prefix = self._conf_get('auth_admin_prefix')
self.auth_uri = self._conf_get('auth_uri') self.auth_uri = self._conf_get('auth_uri')
if netaddr.valid_ipv6(auth_host):
# Note(dzyu) it is an IPv6 address, so it needs to be wrapped
# with '[]' to generate a valid IPv6 URL, based on
# http://www.ietf.org/rfc/rfc2732.txt
auth_host = '[%s]' % auth_host
self.request_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_port)
if self.auth_uri is None: if self.auth_uri is None:
self.LOG.warning( self.LOG.warning(
'Configuring auth_uri to point to the public identity ' 'Configuring auth_uri to point to the public identity '
'endpoint is required; clients may not be able to ' 'endpoint is required; clients may not be able to '
'authenticate against an admin endpoint') 'authenticate against an admin endpoint')
host = self.auth_host
if netaddr.valid_ipv6(host):
# Note(dzyu) it is an IPv6 address, so it needs to be wrapped
# with '[]' to generate a valid IPv6 URL, based on
# http://www.ietf.org/rfc/rfc2732.txt
host = '[%s]' % host
# FIXME(dolph): drop support for this fallback behavior as # FIXME(dolph): drop support for this fallback behavior as
# documented in bug 1207517 # documented in bug 1207517
self.auth_uri = '%s://%s:%s' % (self.auth_protocol, self.auth_uri = self.request_uri
host,
self.auth_port)
# SSL # SSL
self.cert_file = self._conf_get('certfile') self.cert_file = self._conf_get('certfile')
self.key_file = self._conf_get('keyfile') self.key_file = self._conf_get('keyfile')
self.ssl_ca_file = self._conf_get('cafile')
self.ssl_insecure = self._conf_get('insecure')
# signing # signing
self.signing_dirname = self._conf_get('signing_dir') self.signing_dirname = self._conf_get('signing_dir')
@@ -403,7 +399,7 @@ class AuthProtocol(object):
val = '%s/signing_cert.pem' % self.signing_dirname val = '%s/signing_cert.pem' % self.signing_dirname
self.signing_cert_file_name = val self.signing_cert_file_name = val
val = '%s/cacert.pem' % self.signing_dirname val = '%s/cacert.pem' % self.signing_dirname
self.ca_file_name = val self.signing_ca_file_name = val
val = '%s/revoked.pem' % self.signing_dirname val = '%s/revoked.pem' % self.signing_dirname
self.revoked_file_name = val self.revoked_file_name = val
@@ -505,12 +501,12 @@ class AuthProtocol(object):
def _get_supported_versions(self): def _get_supported_versions(self):
versions = [] versions = []
response, data = self._json_request('GET', '/') response, data = self._json_request('GET', '/')
if response.status == 501: if response.status_code == 501:
self.LOG.warning("Old keystone installation found...assuming v2.0") self.LOG.warning("Old keystone installation found...assuming v2.0")
versions.append("v2.0") versions.append("v2.0")
elif response.status != 300: elif response.status_code != 300:
self.LOG.error('Unable to get version info from keystone: %s' % self.LOG.error('Unable to get version info from keystone: %s' %
response.status) response.status_code)
raise ServiceError('Unable to get version info from keystone') raise ServiceError('Unable to get version info from keystone')
else: else:
try: try:
@@ -648,17 +644,6 @@ class AuthProtocol(object):
return self.admin_token return self.admin_token
def _get_http_connection(self):
if self.auth_protocol == 'http':
return self.http_client_class(self.auth_host, self.auth_port,
timeout=self.http_connect_timeout)
else:
return self.http_client_class(self.auth_host,
self.auth_port,
self.key_file,
self.cert_file,
timeout=self.http_connect_timeout)
def _http_request(self, method, path, **kwargs): def _http_request(self, method, path, **kwargs):
"""HTTP request helper used to make unspecified content type requests. """HTTP request helper used to make unspecified content type requests.
@@ -668,28 +653,35 @@ class AuthProtocol(object):
:raise ServerError when unable to communicate with keystone :raise ServerError when unable to communicate with keystone
""" """
conn = self._get_http_connection() url = "%s/%s" % (self.request_uri, path.lstrip('/'))
kwargs.setdefault('timeout', self.http_connect_timeout)
if self.cert_file and self.key_file:
kwargs['cert'] = (self.cert_file, self.key_file)
elif self.cert_file or self.key_file:
self.LOG.warn('Cannot use only a cert or key file. '
'Please provide both. Ignoring.')
kwargs['verify'] = self.ssl_ca_file or True
if self.ssl_insecure:
kwargs['verify'] = False
RETRIES = self.http_request_max_retries RETRIES = self.http_request_max_retries
retry = 0 retry = 0
while True: while True:
try: try:
conn.request(method, path, **kwargs) response = requests.request(method, url, **kwargs)
response = conn.getresponse()
body = response.read()
break break
except Exception as e: except Exception as e:
if retry == RETRIES: if retry >= RETRIES:
self.LOG.error('HTTP connection exception: %s' % e) self.LOG.error('HTTP connection exception: %s', e)
raise NetworkError('Unable to communicate with keystone') raise NetworkError('Unable to communicate with keystone')
# NOTE(vish): sleep 0.5, 1, 2 # NOTE(vish): sleep 0.5, 1, 2
self.LOG.warn('Retrying on HTTP connection exception: %s' % e) self.LOG.warn('Retrying on HTTP connection exception: %s' % e)
time.sleep(2.0 ** retry / 2) time.sleep(2.0 ** retry / 2)
retry += 1 retry += 1
finally:
conn.close()
return response, body return response
def _json_request(self, method, path, body=None, additional_headers=None): def _json_request(self, method, path, body=None, additional_headers=None):
"""HTTP request helper used to make json requests. """HTTP request helper used to make json requests.
@@ -714,14 +706,14 @@ class AuthProtocol(object):
kwargs['headers'].update(additional_headers) kwargs['headers'].update(additional_headers)
if body: if body:
kwargs['body'] = jsonutils.dumps(body) kwargs['data'] = jsonutils.dumps(body)
path = self.auth_admin_prefix + path path = self.auth_admin_prefix + path
response, body = self._http_request(method, path, **kwargs) response = self._http_request(method, path, **kwargs)
try: try:
data = jsonutils.loads(body) data = jsonutils.loads(response.text)
except ValueError: except ValueError:
self.LOG.debug('Keystone did not return json-encoded body') self.LOG.debug('Keystone did not return json-encoded body')
data = {} data = {}
@@ -1090,18 +1082,18 @@ class AuthProtocol(object):
'/v2.0/tokens/%s' % safe_quote(user_token), '/v2.0/tokens/%s' % safe_quote(user_token),
additional_headers=headers) additional_headers=headers)
if response.status == 200: if response.status_code == 200:
return data return data
if response.status == 404: if response.status_code == 404:
self.LOG.warn("Authorization failed for token %s", user_token) self.LOG.warn("Authorization failed for token %s", user_token)
raise InvalidUserToken('Token authorization failed') raise InvalidUserToken('Token authorization failed')
if response.status == 401: if response.status_code == 401:
self.LOG.info( self.LOG.info(
'Keystone rejected admin token %s, resetting', headers) 'Keystone rejected admin token %s, resetting', headers)
self.admin_token = None self.admin_token = None
else: else:
self.LOG.error('Bad response code while validating token: %s' % self.LOG.error('Bad response code while validating token: %s' %
response.status) response.status_code)
if retry: if retry:
self.LOG.info('Retrying validation') self.LOG.info('Retrying validation')
return self._validate_user_token(user_token, False) return self._validate_user_token(user_token, False)
@@ -1135,13 +1127,14 @@ class AuthProtocol(object):
while True: while True:
try: try:
output = cms.cms_verify(data, self.signing_cert_file_name, output = cms.cms_verify(data, self.signing_cert_file_name,
self.ca_file_name) self.signing_ca_file_name)
except cms.subprocess.CalledProcessError as err: except cms.subprocess.CalledProcessError as err:
if self.cert_file_missing(err.output, if self.cert_file_missing(err.output,
self.signing_cert_file_name): self.signing_cert_file_name):
self.fetch_signing_cert() self.fetch_signing_cert()
continue continue
if self.cert_file_missing(err.output, self.ca_file_name): if self.cert_file_missing(err.output,
self.signing_ca_file_name):
self.fetch_ca_cert() self.fetch_ca_cert()
continue continue
self.LOG.warning('Verify error: %s' % err) self.LOG.warning('Verify error: %s' % err)
@@ -1221,14 +1214,14 @@ class AuthProtocol(object):
headers = {'X-Auth-Token': self.get_admin_token()} headers = {'X-Auth-Token': self.get_admin_token()}
response, data = self._json_request('GET', '/v2.0/tokens/revoked', response, data = self._json_request('GET', '/v2.0/tokens/revoked',
additional_headers=headers) additional_headers=headers)
if response.status == 401: if response.status_code == 401:
if retry: if retry:
self.LOG.info( self.LOG.info(
'Keystone rejected admin token %s, resetting admin token', 'Keystone rejected admin token %s, resetting admin token',
headers) headers)
self.admin_token = None self.admin_token = None
return self.fetch_revocation_list(retry=False) return self.fetch_revocation_list(retry=False)
if response.status != 200: if response.status_code != 200:
raise ServiceError('Unable to fetch token revocation list.') raise ServiceError('Unable to fetch token revocation list.')
if 'signed' not in data: if 'signed' not in data:
raise ServiceError('Revocation list improperly formatted.') raise ServiceError('Revocation list improperly formatted.')
@@ -1237,7 +1230,7 @@ class AuthProtocol(object):
def fetch_signing_cert(self): def fetch_signing_cert(self):
path = self.auth_admin_prefix.rstrip('/') path = self.auth_admin_prefix.rstrip('/')
path += '/v2.0/certificates/signing' path += '/v2.0/certificates/signing'
response, data = self._http_request('GET', path) response = self._http_request('GET', path)
def write_cert_file(data): def write_cert_file(data):
with open(self.signing_cert_file_name, 'w') as certfile: with open(self.signing_cert_file_name, 'w') as certfile:
@@ -1246,26 +1239,26 @@ class AuthProtocol(object):
try: try:
#todo check response #todo check response
try: try:
write_cert_file(data) write_cert_file(response.text)
except IOError: except IOError:
self.verify_signing_dir() self.verify_signing_dir()
write_cert_file(data) write_cert_file(response.text)
except (AssertionError, KeyError): except (AssertionError, KeyError):
self.LOG.warn( self.LOG.warn(
"Unexpected response from keystone service: %s", data) "Unexpected response from keystone service: %s", response.text)
raise ServiceError('invalid json response') raise ServiceError('invalid json response')
def fetch_ca_cert(self): def fetch_ca_cert(self):
path = self.auth_admin_prefix.rstrip('/') + '/v2.0/certificates/ca' path = self.auth_admin_prefix.rstrip('/') + '/v2.0/certificates/ca'
response, data = self._http_request('GET', path) response = self._http_request('GET', path)
try: try:
#todo check response #todo check response
with open(self.ca_file_name, 'w') as certfile: with open(self.signing_ca_file_name, 'w') as certfile:
certfile.write(data) certfile.write(response.text)
except (AssertionError, KeyError): except (AssertionError, KeyError):
self.LOG.warn( self.LOG.warn(
"Unexpected response from keystone service: %s", data) "Unexpected response from keystone service: %s", response.text)
raise ServiceError('invalid json response') raise ServiceError('invalid json response')

View File

@@ -864,7 +864,7 @@ class CertDownloadMiddlewareTest(BaseAuthTokenMiddlewareTest):
body=data) body=data)
self.middleware.fetch_ca_cert() self.middleware.fetch_ca_cert()
with open(self.middleware.ca_file_name, 'r') as f: with open(self.middleware.signing_ca_file_name, 'r') as f:
self.assertEqual(f.read(), data) self.assertEqual(f.read(), data)
self.assertEqual("/testadmin/v2.0/certificates/ca", self.assertEqual("/testadmin/v2.0/certificates/ca",