1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2# not use this file except in compliance with the License. You may obtain 3# a copy of the License at 4# 5# http://www.apache.org/licenses/LICENSE-2.0 6# 7# Unless required by applicable law or agreed to in writing, software 8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10# License for the specific language governing permissions and limitations 11# under the License. 12 13import argparse 14import functools 15import hashlib 16import logging 17import os 18import socket 19import time 20import warnings 21 22from debtcollector import removals 23from oslo_config import cfg 24from oslo_serialization import jsonutils 25from oslo_utils import encodeutils 26from oslo_utils import importutils 27from oslo_utils import strutils 28import requests 29import six 30from six.moves import urllib 31 32from keystoneclient import exceptions 33from keystoneclient.i18n import _ 34 35osprofiler_web = importutils.try_import("osprofiler.web") 36 37USER_AGENT = 'python-keystoneclient' 38 39# NOTE(jamielennox): Clients will likely want to print more than json. Please 40# propose a patch if you have a content type you think is reasonable to print 41# here and we'll add it to the list as required. 42_LOG_CONTENT_TYPES = set(['application/json']) 43 44_logger = logging.getLogger(__name__) 45 46 47def _positive_non_zero_float(argument_value): 48 if argument_value is None: 49 return None 50 try: 51 value = float(argument_value) 52 except ValueError: 53 msg = _("%s must be a float") % argument_value 54 raise argparse.ArgumentTypeError(msg) 55 if value <= 0: 56 msg = _("%s must be greater than 0") % argument_value 57 raise argparse.ArgumentTypeError(msg) 58 return value 59 60 61def request(url, method='GET', **kwargs): 62 return Session().request(url, method=method, **kwargs) 63 64 65def _remove_service_catalog(body): 66 try: 67 data = jsonutils.loads(body) 68 69 # V3 token 70 if 'token' in data and 'catalog' in data['token']: 71 data['token']['catalog'] = '<removed>' 72 return jsonutils.dumps(data) 73 74 # V2 token 75 if 'serviceCatalog' in data['access']: 76 data['access']['serviceCatalog'] = '<removed>' 77 return jsonutils.dumps(data) 78 79 except Exception: # nosec(cjschaef): multiple exceptions can be raised 80 # Don't fail trying to clean up the request body. 81 pass 82 return body 83 84 85class Session(object): 86 """Maintains client communication state and common functionality. 87 88 As much as possible the parameters to this class reflect and are passed 89 directly to the requests library. 90 91 :param auth: An authentication plugin to authenticate the session with. 92 (optional, defaults to None) 93 :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` 94 :param requests.Session session: A requests session object that can be used 95 for issuing requests. (optional) 96 :param string original_ip: The original IP of the requesting user which 97 will be sent to identity service in a 98 'Forwarded' header. (optional) 99 :param verify: The verification arguments to pass to requests. These are of 100 the same form as requests expects, so True or False to 101 verify (or not) against system certificates or a path to a 102 bundle or CA certs to check against or None for requests to 103 attempt to locate and use certificates. (optional, defaults 104 to True) 105 :param cert: A client certificate to pass to requests. These are of the 106 same form as requests expects. Either a single filename 107 containing both the certificate and key or a tuple containing 108 the path to the certificate then a path to the key. (optional) 109 :param float timeout: A timeout to pass to requests. This should be a 110 numerical value indicating some amount (or fraction) 111 of seconds or 0 for no timeout. (optional, defaults 112 to 0) 113 :param string user_agent: A User-Agent header string to use for the 114 request. If not provided a default is used. 115 (optional, defaults to 'python-keystoneclient') 116 :param int/bool redirect: Controls the maximum number of redirections that 117 can be followed by a request. Either an integer 118 for a specific count or True/False for 119 forever/never. (optional, default to 30) 120 """ 121 122 user_agent = None 123 124 _REDIRECT_STATUSES = (301, 302, 303, 305, 307) 125 126 REDIRECT_STATUSES = _REDIRECT_STATUSES 127 """This property is deprecated as of the 1.7.0 release and may be removed 128 in the 2.0.0 release.""" 129 130 _DEFAULT_REDIRECT_LIMIT = 30 131 132 DEFAULT_REDIRECT_LIMIT = _DEFAULT_REDIRECT_LIMIT 133 """This property is deprecated as of the 1.7.0 release and may be removed 134 in the 2.0.0 release.""" 135 136 def __init__(self, auth=None, session=None, original_ip=None, verify=True, 137 cert=None, timeout=None, user_agent=None, 138 redirect=_DEFAULT_REDIRECT_LIMIT): 139 warnings.warn( 140 'keystoneclient.session.Session is deprecated as of the 2.1.0 ' 141 'release in favor of keystoneauth1.session.Session. It will be ' 142 'removed in future releases.', 143 DeprecationWarning) 144 145 if not session: 146 session = requests.Session() 147 # Use TCPKeepAliveAdapter to fix bug 1323862 148 for scheme in list(session.adapters): 149 session.mount(scheme, TCPKeepAliveAdapter()) 150 151 self.auth = auth 152 self.session = session 153 self.original_ip = original_ip 154 self.verify = verify 155 self.cert = cert 156 self.timeout = None 157 self.redirect = redirect 158 159 if timeout is not None: 160 self.timeout = float(timeout) 161 162 # don't override the class variable if none provided 163 if user_agent is not None: 164 self.user_agent = user_agent 165 166 @staticmethod 167 def _process_header(header): 168 """Redact the secure headers to be logged.""" 169 secure_headers = ('authorization', 'x-auth-token', 170 'x-subject-token', 'x-service-token') 171 if header[0].lower() in secure_headers: 172 # hashlib.sha1() bandit nosec, as it is HMAC-SHA1 in 173 # keystone, which is considered secure (unlike just sha1) 174 token_hasher = hashlib.sha1() # nosec(lhinds) 175 token_hasher.update(header[1].encode('utf-8')) 176 token_hash = token_hasher.hexdigest() 177 return (header[0], '{SHA1}%s' % token_hash) 178 return header 179 180 def _http_log_request(self, url, method=None, data=None, 181 headers=None, logger=_logger): 182 if not logger.isEnabledFor(logging.DEBUG): 183 # NOTE(morganfainberg): This whole debug section is expensive, 184 # there is no need to do the work if we're not going to emit a 185 # debug log. 186 return 187 188 string_parts = ['REQ: curl -g -i'] 189 190 # NOTE(jamielennox): None means let requests do its default validation 191 # so we need to actually check that this is False. 192 if self.verify is False: 193 string_parts.append('--insecure') 194 elif isinstance(self.verify, six.string_types): 195 string_parts.append('--cacert "%s"' % self.verify) 196 197 if method: 198 string_parts.extend(['-X', method]) 199 200 string_parts.append(url) 201 202 if headers: 203 for header in headers.items(): 204 string_parts.append('-H "%s: %s"' 205 % self._process_header(header)) 206 207 if data: 208 if isinstance(data, six.binary_type): 209 try: 210 data = data.decode("ascii") 211 except UnicodeDecodeError: 212 data = "<binary_data>" 213 string_parts.append("-d '%s'" % data) 214 try: 215 logger.debug(' '.join(string_parts)) 216 except UnicodeDecodeError: 217 logger.debug("Replaced characters that could not be decoded" 218 " in log output, original caused UnicodeDecodeError") 219 string_parts = [ 220 encodeutils.safe_decode( 221 part, errors='replace') for part in string_parts] 222 logger.debug(' '.join(string_parts)) 223 224 def _http_log_response(self, response, logger): 225 if not logger.isEnabledFor(logging.DEBUG): 226 return 227 228 # NOTE(samueldmq): If the response does not provide enough info about 229 # the content type to decide whether it is useful and safe to log it 230 # or not, just do not log the body. Trying to# read the response body 231 # anyways may result on reading a long stream of bytes and getting an 232 # unexpected MemoryError. See bug 1616105 for further details. 233 content_type = response.headers.get('content-type', None) 234 235 # NOTE(lamt): Per [1], the Content-Type header can be of the form 236 # Content-Type := type "/" subtype *[";" parameter] 237 # [1] https://www.w3.org/Protocols/rfc1341/4_Content-Type.html 238 for log_type in _LOG_CONTENT_TYPES: 239 if content_type is not None and content_type.startswith(log_type): 240 text = _remove_service_catalog(response.text) 241 break 242 else: 243 text = ('Omitted, Content-Type is set to %s. Only ' 244 '%s responses have their bodies logged.') 245 text = text % (content_type, ', '.join(_LOG_CONTENT_TYPES)) 246 247 string_parts = [ 248 'RESP:', 249 '[%s]' % response.status_code 250 ] 251 for header in response.headers.items(): 252 string_parts.append('%s: %s' % self._process_header(header)) 253 string_parts.append('\nRESP BODY: %s\n' % strutils.mask_password(text)) 254 255 logger.debug(' '.join(string_parts)) 256 257 # NOTE(artmr): parameter 'original_ip' value is never used 258 def request(self, url, method, json=None, original_ip=None, 259 user_agent=None, redirect=None, authenticated=None, 260 endpoint_filter=None, auth=None, requests_auth=None, 261 raise_exc=True, allow_reauth=True, log=True, 262 endpoint_override=None, connect_retries=0, logger=_logger, 263 **kwargs): 264 """Send an HTTP request with the specified characteristics. 265 266 Wrapper around `requests.Session.request` to handle tasks such as 267 setting headers, JSON encoding/decoding, and error handling. 268 269 Arguments that are not handled are passed through to the requests 270 library. 271 272 :param string url: Path or fully qualified URL of HTTP request. If only 273 a path is provided then endpoint_filter must also be 274 provided such that the base URL can be determined. 275 If a fully qualified URL is provided then 276 endpoint_filter will be ignored. 277 :param string method: The http method to use. (e.g. 'GET', 'POST') 278 :param string original_ip: Mark this request as forwarded for this ip. 279 (optional) 280 :param dict headers: Headers to be included in the request. (optional) 281 :param json: Some data to be represented as JSON. (optional) 282 :param string user_agent: A user_agent to use for the request. If 283 present will override one present in headers. 284 (optional) 285 :param int/bool redirect: the maximum number of redirections that 286 can be followed by a request. Either an 287 integer for a specific count or True/False 288 for forever/never. (optional) 289 :param int connect_retries: the maximum number of retries that should 290 be attempted for connection errors. 291 (optional, defaults to 0 - never retry). 292 :param bool authenticated: True if a token should be attached to this 293 request, False if not or None for attach if 294 an auth_plugin is available. 295 (optional, defaults to None) 296 :param dict endpoint_filter: Data to be provided to an auth plugin with 297 which it should be able to determine an 298 endpoint to use for this request. If not 299 provided then URL is expected to be a 300 fully qualified URL. (optional) 301 :param str endpoint_override: The URL to use instead of looking up the 302 endpoint in the auth plugin. This will be 303 ignored if a fully qualified URL is 304 provided but take priority over an 305 endpoint_filter. (optional) 306 :param auth: The auth plugin to use when authenticating this request. 307 This will override the plugin that is attached to the 308 session (if any). (optional) 309 :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` 310 :param requests_auth: A requests library auth plugin that cannot be 311 passed via kwarg because the `auth` kwarg 312 collides with our own auth plugins. (optional) 313 :type requests_auth: :py:class:`requests.auth.AuthBase` 314 :param bool raise_exc: If True then raise an appropriate exception for 315 failed HTTP requests. If False then return the 316 request object. (optional, default True) 317 :param bool allow_reauth: Allow fetching a new token and retrying the 318 request on receiving a 401 Unauthorized 319 response. (optional, default True) 320 :param bool log: If True then log the request and response data to the 321 debug log. (optional, default True) 322 :param logger: The logger object to use to log request and responses. 323 If not provided the keystoneclient.session default 324 logger will be used. 325 :type logger: logging.Logger 326 :param kwargs: any other parameter that can be passed to 327 requests.Session.request (such as `headers`). Except: 328 'data' will be overwritten by the data in 'json' param. 329 'allow_redirects' is ignored as redirects are handled 330 by the session. 331 332 :raises keystoneclient.exceptions.ClientException: For connection 333 failure, or to indicate an error response code. 334 335 :returns: The response to the request. 336 """ 337 headers = kwargs.setdefault('headers', dict()) 338 339 if authenticated is None: 340 authenticated = bool(auth or self.auth) 341 342 if authenticated: 343 auth_headers = self.get_auth_headers(auth) 344 345 if auth_headers is None: 346 msg = _('No valid authentication is available') 347 raise exceptions.AuthorizationFailure(msg) 348 349 headers.update(auth_headers) 350 351 if osprofiler_web: 352 headers.update(osprofiler_web.get_trace_id_headers()) 353 354 # if we are passed a fully qualified URL and an endpoint_filter we 355 # should ignore the filter. This will make it easier for clients who 356 # want to overrule the default endpoint_filter data added to all client 357 # requests. We check fully qualified here by the presence of a host. 358 if not urllib.parse.urlparse(url).netloc: 359 base_url = None 360 361 if endpoint_override: 362 base_url = endpoint_override 363 elif endpoint_filter: 364 base_url = self.get_endpoint(auth, **endpoint_filter) 365 366 if not base_url: 367 service_type = (endpoint_filter or {}).get('service_type', 368 'unknown') 369 msg = _('Endpoint for %s service') % service_type 370 raise exceptions.EndpointNotFound(msg) 371 372 url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) 373 374 if self.cert: 375 kwargs.setdefault('cert', self.cert) 376 377 if self.timeout is not None: 378 kwargs.setdefault('timeout', self.timeout) 379 380 if user_agent: 381 headers['User-Agent'] = user_agent 382 elif self.user_agent: 383 user_agent = headers.setdefault('User-Agent', self.user_agent) 384 else: 385 user_agent = headers.setdefault('User-Agent', USER_AGENT) 386 387 if self.original_ip: 388 headers.setdefault('Forwarded', 389 'for=%s;by=%s' % (self.original_ip, user_agent)) 390 391 if json is not None: 392 headers['Content-Type'] = 'application/json' 393 kwargs['data'] = jsonutils.dumps(json) 394 395 kwargs.setdefault('verify', self.verify) 396 397 if requests_auth: 398 kwargs['auth'] = requests_auth 399 400 if log: 401 self._http_log_request(url, method=method, 402 data=kwargs.get('data'), 403 headers=headers, 404 logger=logger) 405 406 # Force disable requests redirect handling. We will manage this below. 407 kwargs['allow_redirects'] = False 408 409 if redirect is None: 410 redirect = self.redirect 411 412 send = functools.partial(self._send_request, 413 url, method, redirect, log, logger, 414 connect_retries) 415 416 try: 417 connection_params = self.get_auth_connection_params(auth=auth) 418 except exceptions.MissingAuthPlugin: # nosec(cjschaef) 419 # NOTE(jamielennox): If we've gotten this far without an auth 420 # plugin then we should be happy with allowing no additional 421 # connection params. This will be the typical case for plugins 422 # anyway. 423 pass 424 else: 425 if connection_params: 426 kwargs.update(connection_params) 427 428 resp = send(**kwargs) 429 430 # handle getting a 401 Unauthorized response by invalidating the plugin 431 # and then retrying the request. This is only tried once. 432 if resp.status_code == 401 and authenticated and allow_reauth: 433 if self.invalidate(auth): 434 auth_headers = self.get_auth_headers(auth) 435 436 if auth_headers is not None: 437 headers.update(auth_headers) 438 resp = send(**kwargs) 439 440 if raise_exc and resp.status_code >= 400: 441 logger.debug('Request returned failure status: %s', 442 resp.status_code) 443 raise exceptions.from_response(resp, method, url) 444 445 return resp 446 447 def _send_request(self, url, method, redirect, log, logger, 448 connect_retries, connect_retry_delay=0.5, **kwargs): 449 # NOTE(jamielennox): We handle redirection manually because the 450 # requests lib follows some browser patterns where it will redirect 451 # POSTs as GETs for certain statuses which is not want we want for an 452 # API. See: https://en.wikipedia.org/wiki/Post/Redirect/Get 453 454 # NOTE(jamielennox): The interaction between retries and redirects are 455 # handled naively. We will attempt only a maximum number of retries and 456 # redirects rather than per request limits. Otherwise the extreme case 457 # could be redirects * retries requests. This will be sufficient in 458 # most cases and can be fixed properly if there's ever a need. 459 460 try: 461 try: 462 resp = self.session.request(method, url, **kwargs) 463 except requests.exceptions.SSLError as e: 464 msg = _('SSL exception connecting to %(url)s: ' 465 '%(error)s') % {'url': url, 'error': e} 466 raise exceptions.SSLError(msg) 467 except requests.exceptions.Timeout: 468 msg = _('Request to %s timed out') % url 469 raise exceptions.RequestTimeout(msg) 470 except requests.exceptions.ConnectionError: 471 msg = _('Unable to establish connection to %s') % url 472 raise exceptions.ConnectionRefused(msg) 473 except (exceptions.RequestTimeout, exceptions.ConnectionRefused) as e: 474 if connect_retries <= 0: 475 raise 476 477 logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', 478 {'e': e, 'delay': connect_retry_delay}) 479 time.sleep(connect_retry_delay) 480 481 return self._send_request( 482 url, method, redirect, log, logger, 483 connect_retries=connect_retries - 1, 484 connect_retry_delay=connect_retry_delay * 2, 485 **kwargs) 486 487 if log: 488 self._http_log_response(resp, logger) 489 490 if resp.status_code in self._REDIRECT_STATUSES: 491 # be careful here in python True == 1 and False == 0 492 if isinstance(redirect, bool): 493 redirect_allowed = redirect 494 else: 495 redirect -= 1 496 redirect_allowed = redirect >= 0 497 498 if not redirect_allowed: 499 return resp 500 501 try: 502 location = resp.headers['location'] 503 except KeyError: 504 logger.warning("Failed to redirect request to %s as new " 505 "location was not provided.", resp.url) 506 else: 507 # NOTE(jamielennox): We don't pass through connect_retry_delay. 508 # This request actually worked so we can reset the delay count. 509 new_resp = self._send_request( 510 location, method, redirect, log, logger, 511 connect_retries=connect_retries, 512 **kwargs) 513 514 if not isinstance(new_resp.history, list): 515 new_resp.history = list(new_resp.history) 516 new_resp.history.insert(0, resp) 517 resp = new_resp 518 519 return resp 520 521 def head(self, url, **kwargs): 522 """Perform a HEAD request. 523 524 This calls :py:meth:`.request()` with ``method`` set to ``HEAD``. 525 526 """ 527 return self.request(url, 'HEAD', **kwargs) 528 529 def get(self, url, **kwargs): 530 """Perform a GET request. 531 532 This calls :py:meth:`.request()` with ``method`` set to ``GET``. 533 534 """ 535 return self.request(url, 'GET', **kwargs) 536 537 def post(self, url, **kwargs): 538 """Perform a POST request. 539 540 This calls :py:meth:`.request()` with ``method`` set to ``POST``. 541 542 """ 543 return self.request(url, 'POST', **kwargs) 544 545 def put(self, url, **kwargs): 546 """Perform a PUT request. 547 548 This calls :py:meth:`.request()` with ``method`` set to ``PUT``. 549 550 """ 551 return self.request(url, 'PUT', **kwargs) 552 553 def delete(self, url, **kwargs): 554 """Perform a DELETE request. 555 556 This calls :py:meth:`.request()` with ``method`` set to ``DELETE``. 557 558 """ 559 return self.request(url, 'DELETE', **kwargs) 560 561 def patch(self, url, **kwargs): 562 """Perform a PATCH request. 563 564 This calls :py:meth:`.request()` with ``method`` set to ``PATCH``. 565 566 """ 567 return self.request(url, 'PATCH', **kwargs) 568 569 @classmethod 570 def construct(cls, kwargs): 571 """Handle constructing a session from both old and new arguments. 572 573 Support constructing a session from the old 574 :py:class:`~keystoneclient.httpclient.HTTPClient` args as well as the 575 new request-style arguments. 576 577 .. warning:: 578 579 *DEPRECATED as of 1.7.0*: This function is purely for bridging the 580 gap between older client arguments and the session arguments that 581 they relate to. It is not intended to be used as a generic Session 582 Factory. This function may be removed in the 2.0.0 release. 583 584 This function purposefully modifies the input kwargs dictionary so that 585 the remaining kwargs dict can be reused and passed on to other 586 functions without session arguments. 587 588 """ 589 warnings.warn( 590 'Session.construct() is deprecated as of the 1.7.0 release in ' 591 'favor of using session constructor and may be removed in the ' 592 '2.0.0 release.', DeprecationWarning) 593 return cls._construct(kwargs) 594 595 @classmethod 596 def _construct(cls, kwargs): 597 params = {} 598 599 for attr in ('verify', 'cacert', 'cert', 'key', 'insecure', 600 'timeout', 'session', 'original_ip', 'user_agent'): 601 try: 602 params[attr] = kwargs.pop(attr) 603 except KeyError: # nosec(cjschaef): we are brute force 604 # identifying possible attributes for kwargs 605 pass 606 607 return cls._make(**params) 608 609 @classmethod 610 def _make(cls, insecure=False, verify=None, cacert=None, cert=None, 611 key=None, **kwargs): 612 """Create a session with individual certificate parameters. 613 614 Some parameters used to create a session don't lend themselves to be 615 loaded from config/CLI etc. Create a session by converting those 616 parameters into session __init__ parameters. 617 """ 618 if verify is None: 619 if insecure: 620 verify = False 621 else: 622 verify = cacert or True 623 624 if cert and key: 625 warnings.warn( 626 'Passing cert and key together is deprecated as of the 1.7.0 ' 627 'release in favor of the requests library form of having the ' 628 'cert and key as a tuple and may be removed in the 2.0.0 ' 629 'release.', DeprecationWarning) 630 cert = (cert, key) 631 632 return cls(verify=verify, cert=cert, **kwargs) 633 634 def _auth_required(self, auth, msg): 635 if not auth: 636 auth = self.auth 637 638 if not auth: 639 raise exceptions.MissingAuthPlugin(msg) 640 641 return auth 642 643 def get_auth_headers(self, auth=None, **kwargs): 644 """Return auth headers as provided by the auth plugin. 645 646 :param auth: The auth plugin to use for token. Overrides the plugin 647 on the session. (optional) 648 :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` 649 650 :raises keystoneclient.exceptions.AuthorizationFailure: if a new token 651 fetch fails. 652 :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not 653 available. 654 655 :returns: Authentication headers or None for failure. 656 :rtype: dict 657 """ 658 msg = _('An auth plugin is required to fetch a token') 659 auth = self._auth_required(auth, msg) 660 return auth.get_headers(self, **kwargs) 661 662 @removals.remove(message='Use get_auth_headers instead.', version='1.7.0', 663 removal_version='2.0.0') 664 def get_token(self, auth=None): 665 """Return a token as provided by the auth plugin. 666 667 :param auth: The auth plugin to use for token. Overrides the plugin 668 on the session. (optional) 669 :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` 670 671 :raises keystoneclient.exceptions.AuthorizationFailure: if a new token 672 fetch fails. 673 :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not 674 available. 675 676 .. warning:: 677 678 This method is deprecated as of the 1.7.0 release in favor of 679 :meth:`get_auth_headers` and may be removed in the 2.0.0 release. 680 This method assumes that the only header that is used to 681 authenticate a message is 'X-Auth-Token' which may not be correct. 682 683 :returns: A valid token. 684 :rtype: string 685 """ 686 return (self.get_auth_headers(auth) or {}).get('X-Auth-Token') 687 688 def get_endpoint(self, auth=None, **kwargs): 689 """Get an endpoint as provided by the auth plugin. 690 691 :param auth: The auth plugin to use for token. Overrides the plugin on 692 the session. (optional) 693 :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` 694 695 :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not 696 available. 697 698 :returns: An endpoint if available or None. 699 :rtype: string 700 """ 701 msg = _('An auth plugin is required to determine endpoint URL') 702 auth = self._auth_required(auth, msg) 703 return auth.get_endpoint(self, **kwargs) 704 705 def get_auth_connection_params(self, auth=None, **kwargs): 706 """Return auth connection params as provided by the auth plugin. 707 708 An auth plugin may specify connection parameters to the request like 709 providing a client certificate for communication. 710 711 We restrict the values that may be returned from this function to 712 prevent an auth plugin overriding values unrelated to connection 713 parameters. The values that are currently accepted are: 714 715 - `cert`: a path to a client certificate, or tuple of client 716 certificate and key pair that are used with this request. 717 - `verify`: a boolean value to indicate verifying SSL certificates 718 against the system CAs or a path to a CA file to verify with. 719 720 These values are passed to the requests library and further information 721 on accepted values may be found there. 722 723 :param auth: The auth plugin to use for tokens. Overrides the plugin 724 on the session. (optional) 725 :type auth: keystoneclient.auth.base.BaseAuthPlugin 726 727 :raises keystoneclient.exceptions.AuthorizationFailure: if a new token 728 fetch fails. 729 :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not 730 available. 731 :raises keystoneclient.exceptions.UnsupportedParameters: if the plugin 732 returns a parameter that is not supported by this session. 733 734 :returns: Authentication headers or None for failure. 735 :rtype: dict 736 """ 737 msg = _('An auth plugin is required to fetch connection params') 738 auth = self._auth_required(auth, msg) 739 params = auth.get_connection_params(self, **kwargs) 740 741 # NOTE(jamielennox): There needs to be some consensus on what 742 # parameters are allowed to be modified by the auth plugin here. 743 # Ideally I think it would be only the send() parts of the request 744 # flow. For now lets just allow certain elements. 745 params_copy = params.copy() 746 747 for arg in ('cert', 'verify'): 748 try: 749 kwargs[arg] = params_copy.pop(arg) 750 except KeyError: # nosec(cjschaef): we are brute force 751 # identifying and removing values in params_copy 752 pass 753 754 if params_copy: 755 raise exceptions.UnsupportedParameters(list(params_copy)) 756 757 return params 758 759 def invalidate(self, auth=None): 760 """Invalidate an authentication plugin. 761 762 :param auth: The auth plugin to invalidate. Overrides the plugin on the 763 session. (optional) 764 :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin` 765 766 """ 767 msg = _('An auth plugin is required to validate') 768 auth = self._auth_required(auth, msg) 769 return auth.invalidate() 770 771 def get_user_id(self, auth=None): 772 """Return the authenticated user_id as provided by the auth plugin. 773 774 :param auth: The auth plugin to use for token. Overrides the plugin 775 on the session. (optional) 776 :type auth: keystoneclient.auth.base.BaseAuthPlugin 777 778 :raises keystoneclient.exceptions.AuthorizationFailure: 779 if a new token fetch fails. 780 :raises keystoneclient.exceptions.MissingAuthPlugin: 781 if a plugin is not available. 782 783 :returns string: Current user_id or None if not supported by plugin. 784 """ 785 msg = _('An auth plugin is required to get user_id') 786 auth = self._auth_required(auth, msg) 787 return auth.get_user_id(self) 788 789 def get_project_id(self, auth=None): 790 """Return the authenticated project_id as provided by the auth plugin. 791 792 :param auth: The auth plugin to use for token. Overrides the plugin 793 on the session. (optional) 794 :type auth: keystoneclient.auth.base.BaseAuthPlugin 795 796 :raises keystoneclient.exceptions.AuthorizationFailure: 797 if a new token fetch fails. 798 :raises keystoneclient.exceptions.MissingAuthPlugin: 799 if a plugin is not available. 800 801 :returns string: Current project_id or None if not supported by plugin. 802 """ 803 msg = _('An auth plugin is required to get project_id') 804 auth = self._auth_required(auth, msg) 805 return auth.get_project_id(self) 806 807 @classmethod 808 def get_conf_options(cls, deprecated_opts=None): 809 """Get oslo_config options that are needed for a :py:class:`.Session`. 810 811 These may be useful without being registered for config file generation 812 or to manipulate the options before registering them yourself. 813 814 The options that are set are: 815 :cafile: The certificate authority filename. 816 :certfile: The client certificate file to present. 817 :keyfile: The key for the client certificate. 818 :insecure: Whether to ignore SSL verification. 819 :timeout: The max time to wait for HTTP connections. 820 821 :param dict deprecated_opts: Deprecated options that should be included 822 in the definition of new options. This should be a dict from the 823 name of the new option to a list of oslo.DeprecatedOpts that 824 correspond to the new option. (optional) 825 826 For example, to support the ``ca_file`` option pointing to the new 827 ``cafile`` option name:: 828 829 old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') 830 deprecated_opts={'cafile': [old_opt]} 831 832 :returns: A list of oslo_config options. 833 """ 834 if deprecated_opts is None: 835 deprecated_opts = {} 836 837 return [cfg.StrOpt('cafile', 838 deprecated_opts=deprecated_opts.get('cafile'), 839 help='PEM encoded Certificate Authority to use ' 840 'when verifying HTTPs connections.'), 841 cfg.StrOpt('certfile', 842 deprecated_opts=deprecated_opts.get('certfile'), 843 help='PEM encoded client certificate cert file'), 844 cfg.StrOpt('keyfile', 845 deprecated_opts=deprecated_opts.get('keyfile'), 846 help='PEM encoded client certificate key file'), 847 cfg.BoolOpt('insecure', 848 default=False, 849 deprecated_opts=deprecated_opts.get('insecure'), 850 help='Verify HTTPS connections.'), 851 cfg.IntOpt('timeout', 852 deprecated_opts=deprecated_opts.get('timeout'), 853 help='Timeout value for http requests'), 854 ] 855 856 @classmethod 857 def register_conf_options(cls, conf, group, deprecated_opts=None): 858 """Register the oslo_config options that are needed for a session. 859 860 The options that are set are: 861 :cafile: The certificate authority filename. 862 :certfile: The client certificate file to present. 863 :keyfile: The key for the client certificate. 864 :insecure: Whether to ignore SSL verification. 865 :timeout: The max time to wait for HTTP connections. 866 867 :param oslo_config.Cfg conf: config object to register with. 868 :param string group: The ini group to register options in. 869 :param dict deprecated_opts: Deprecated options that should be included 870 in the definition of new options. This should be a dict from the 871 name of the new option to a list of oslo.DeprecatedOpts that 872 correspond to the new option. (optional) 873 874 For example, to support the ``ca_file`` option pointing to the new 875 ``cafile`` option name:: 876 877 old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group') 878 deprecated_opts={'cafile': [old_opt]} 879 880 :returns: The list of options that was registered. 881 """ 882 opts = cls.get_conf_options(deprecated_opts=deprecated_opts) 883 conf.register_group(cfg.OptGroup(group)) 884 conf.register_opts(opts, group=group) 885 return opts 886 887 @classmethod 888 def load_from_conf_options(cls, conf, group, **kwargs): 889 """Create a session object from an oslo_config object. 890 891 The options must have been previously registered with 892 register_conf_options. 893 894 :param oslo_config.Cfg conf: config object to register with. 895 :param string group: The ini group to register options in. 896 :param dict kwargs: Additional parameters to pass to session 897 construction. 898 :returns: A new session object. 899 :rtype: :py:class:`.Session` 900 """ 901 c = conf[group] 902 903 kwargs['insecure'] = c.insecure 904 kwargs['cacert'] = c.cafile 905 if c.certfile and c.keyfile: 906 kwargs['cert'] = (c.certfile, c.keyfile) 907 kwargs['timeout'] = c.timeout 908 909 return cls._make(**kwargs) 910 911 @staticmethod 912 def register_cli_options(parser): 913 """Register the argparse arguments that are needed for a session. 914 915 :param argparse.ArgumentParser parser: parser to add to. 916 """ 917 parser.add_argument('--insecure', 918 default=False, 919 action='store_true', 920 help='Explicitly allow client to perform ' 921 '"insecure" TLS (https) requests. The ' 922 'server\'s certificate will not be verified ' 923 'against any certificate authorities. This ' 924 'option should be used with caution.') 925 926 parser.add_argument('--os-cacert', 927 metavar='<ca-certificate>', 928 default=os.environ.get('OS_CACERT'), 929 help='Specify a CA bundle file to use in ' 930 'verifying a TLS (https) server certificate. ' 931 'Defaults to env[OS_CACERT].') 932 933 parser.add_argument('--os-cert', 934 metavar='<certificate>', 935 default=os.environ.get('OS_CERT'), 936 help='Defaults to env[OS_CERT].') 937 938 parser.add_argument('--os-key', 939 metavar='<key>', 940 default=os.environ.get('OS_KEY'), 941 help='Defaults to env[OS_KEY].') 942 943 parser.add_argument('--timeout', 944 default=600, 945 type=_positive_non_zero_float, 946 metavar='<seconds>', 947 help='Set request timeout (in seconds).') 948 949 @classmethod 950 def load_from_cli_options(cls, args, **kwargs): 951 """Create a :py:class:`.Session` object from CLI arguments. 952 953 The CLI arguments must have been registered with 954 :py:meth:`.register_cli_options`. 955 956 :param Namespace args: result of parsed arguments. 957 958 :returns: A new session object. 959 :rtype: :py:class:`.Session` 960 """ 961 kwargs['insecure'] = args.insecure 962 kwargs['cacert'] = args.os_cacert 963 if args.os_cert and args.os_key: 964 kwargs['cert'] = (args.os_cert, args.os_key) 965 kwargs['timeout'] = args.timeout 966 967 return cls._make(**kwargs) 968 969 970class TCPKeepAliveAdapter(requests.adapters.HTTPAdapter): 971 """The custom adapter used to set TCP Keep-Alive on all connections. 972 973 This Adapter also preserves the default behaviour of Requests which 974 disables Nagle's Algorithm. See also: 975 http://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx 976 """ 977 978 def init_poolmanager(self, *args, **kwargs): 979 if 'socket_options' not in kwargs: 980 socket_options = [ 981 # Keep Nagle's algorithm off 982 (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), 983 # Turn on TCP Keep-Alive 984 (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), 985 ] 986 987 # Some operating systems (e.g., OSX) do not support setting 988 # keepidle 989 if hasattr(socket, 'TCP_KEEPIDLE'): 990 socket_options += [ 991 # Wait 60 seconds before sending keep-alive probes 992 (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) 993 ] 994 995 # TODO(claudiub): Windows does not contain the TCP_KEEPCNT and 996 # TCP_KEEPINTVL socket attributes. Instead, it contains 997 # SIO_KEEPALIVE_VALS, which can be set via ioctl, which should be 998 # set once it is available in requests. 999 # https://msdn.microsoft.com/en-us/library/dd877220%28VS.85%29.aspx 1000 if hasattr(socket, 'TCP_KEEPCNT'): 1001 socket_options += [ 1002 # Set the maximum number of keep-alive probes 1003 (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4) 1004 ] 1005 1006 if hasattr(socket, 'TCP_KEEPINTVL'): 1007 socket_options += [ 1008 # Send keep-alive probes every 15 seconds 1009 (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15) 1010 ] 1011 1012 # After waiting 60 seconds, and then sending a probe once every 15 1013 # seconds 4 times, these options should ensure that a connection 1014 # hands for no longer than 2 minutes before a ConnectionError is 1015 # raised. 1016 1017 kwargs['socket_options'] = socket_options 1018 super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) 1019