drydock/drydock_provisioner/drydock_client/session.py
Bryan Strassner 0dacd50c3a [390136] Drydock client timeout options
Adds options to drydock client to allow for override
of both connect and read timeouts.
Adds logging to the drydock client for get and post
requests being made
Bring yapf changes current.

Change-Id: I5ff007315059e2087612e2209966815291433893
2018-03-07 16:59:15 -06:00

217 lines
8.4 KiB
Python

# Copyright 2017 AT&T Intellectual Property. All other 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.
import requests
import logging
from keystoneauth1 import session
from keystoneauth1.identity import v3
class DrydockSession(object):
"""
A session to the Drydock API maintaining credentials and API options
:param string host: The Drydock server hostname or IP
:param int port: (optional) The service port appended if specified
:param function auth_gen: Callable that will generate a list of authentication
header names and values (2 part tuple)
:param string marker: (optional) external context marker
:param tuple timeout: (optional) a tuple of connect, read timeout values
to use as the default for invocations using this session. A single
value may also be supplied instead of a tuple to indicate only the
read timeout to use
"""
def __init__(self,
host,
port=None,
scheme='http',
auth_gen=None,
marker=None,
timeout=None):
self.logger = logging.getLogger(__name__)
self.__session = requests.Session()
self.auth_gen = auth_gen
self.set_auth()
self.marker = marker
self.__session.headers.update({'X-Context-Marker': marker})
self.host = host
self.scheme = scheme
if port:
self.port = port
self.base_url = "%s://%s:%s/api/" % (self.scheme, self.host,
self.port)
else:
# assume default port for scheme
self.base_url = "%s://%s/api/" % (self.scheme, self.host)
self.default_timeout = self._calc_timeout_tuple((20, 30), timeout)
def set_auth(self):
"""Set the session's auth header."""
if self.auth_gen:
self.logger.debug("Updating session authentication header.")
auth_header = self.auth_gen()
self.__session.headers.update(auth_header)
else:
self.logger.debug("Cannot set auth header, no generator defined.")
def get(self, endpoint, query=None, timeout=None):
"""
Send a GET request to Drydock.
:param string endpoint: The URL string following the hostname and API prefix
:param dict query: A dict of k, v pairs to add to the query string
:param timeout: A single or tuple value for connect, read timeout.
A single value indicates the read timeout only
:return: A requests.Response object
"""
auth_refresh = False
while True:
url = self.base_url + endpoint
self.logger.debug('GET ' + url)
self.logger.debug('Query Params: ' + str(query))
resp = self.__session.get(
url, params=query, timeout=self._timeout(timeout))
if resp.status_code == 401 and not auth_refresh:
self.set_auth()
auth_refresh = True
else:
break
return resp
def post(self, endpoint, query=None, body=None, data=None, timeout=None):
"""
Send a POST request to Drydock. If both body and data are specified,
body will will be used.
:param string endpoint: The URL string following the hostname and API prefix
:param dict query: A dict of k, v parameters to add to the query string
:param string body: A string to use as the request body. Will be treated as raw
:param data: Something json.dumps(s) can serialize. Result will be used as the request body
:param timeout: A single or tuple value for connect, read timeout.
A single value indicates the read timeout only
:return: A requests.Response object
"""
auth_refresh = False
url = self.base_url + endpoint
while True:
self.logger.debug('POST ' + url)
self.logger.debug('Query Params: ' + str(query))
if body is not None:
self.logger.debug(
"Sending POST with explicit body: \n%s" % body)
resp = self.__session.post(
self.base_url + endpoint,
params=query,
data=body,
timeout=self._timeout(timeout))
else:
self.logger.debug(
"Sending POST with JSON body: \n%s" % str(data))
resp = self.__session.post(
self.base_url + endpoint,
params=query,
json=data,
timeout=self._timeout(timeout))
if resp.status_code == 401 and not auth_refresh:
self.set_auth()
auth_refresh = True
else:
break
return resp
def _timeout(self, timeout=None):
"""Calculate the default timeouts for this session
:param timeout: A single or tuple value for connect, read timeout.
A single value indicates the read timeout only
:return: the tuple of the default timeouts used for this session
"""
return self._calc_timeout_tuple(self.default_timeout, timeout)
def _calc_timeout_tuple(self, def_timeout, timeout=None):
"""Calculate the default timeouts for this session
:param def_timeout: The default timeout tuple to be used if no specific
timeout value is supplied
:param timeout: A single or tuple value for connect, read timeout.
A single value indicates the read timeout only
:return: the tuple of the timeouts calculated
"""
connect_timeout, read_timeout = def_timeout
try:
if isinstance(timeout, tuple):
if all(isinstance(v, int)
for v in timeout) and len(timeout) == 2:
connect_timeout, read_timeout = timeout
else:
raise ValueError("Tuple non-integer or wrong length")
elif isinstance(timeout, int):
read_timeout = timeout
elif timeout is not None:
raise ValueError("Non integer timeout value")
except ValueError:
self.logger.warn("Timeout value must be a tuple of integers or a "
"single integer. Proceeding with values of "
"(%s, %s)", connect_timeout, read_timeout)
return (connect_timeout, read_timeout)
class KeystoneClient(object):
@staticmethod
def get_endpoint(endpoint, ks_sess=None, auth_info=None):
"""
Wraps calls to keystone for lookup of an endpoint by service type
:param endpoint: The endpoint to look up
:param ks_sess: A keystone session to use for accessing endpoint catalogue
:param auth_info: Authentication info to use for building a token if a ``ks_sess`` is not specified
:returns: The url string of the endpoint
:rtype: str
"""
if ks_sess is None:
ks_sess = KeystoneClient.get_ks_session(**auth_info)
return ks_sess.get_endpoint(
interface='internal', service_type=endpoint)
@staticmethod
def get_token(ks_sess=None, auth_info=None):
"""
Returns the simple token string for a token acquired from keystone
:param ks_sess: an existing Keystone session to retrieve a token from
:param auth_info: dictionary of information required to generate a keystone token
"""
if ks_sess is None:
ks_sess = KeystoneClient.get_ks_session(**auth_info)
return ks_sess.get_auth_headers().get('X-Auth-Token')
@staticmethod
def get_ks_session(**kwargs):
# Establishes a keystone session
if 'token' in kwargs:
auth = v3.TokenMethod(token=kwargs.get('token'))
else:
auth = v3.Password(**kwargs)
return session.Session(auth=auth)