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 datetime 14import functools 15import hashlib 16import json 17import logging 18import os 19import platform 20import socket 21import sys 22import time 23import uuid 24 25import requests 26import six 27from six.moves import urllib 28 29import keystoneauth1 30from keystoneauth1 import _utils as utils 31from keystoneauth1 import discover 32from keystoneauth1 import exceptions 33 34try: 35 import netaddr 36except ImportError: 37 netaddr = None 38 39try: 40 import osprofiler.web as osprofiler_web 41except ImportError: 42 osprofiler_web = None 43 44DEFAULT_USER_AGENT = 'keystoneauth1/%s %s %s/%s' % ( 45 keystoneauth1.__version__, requests.utils.default_user_agent(), 46 platform.python_implementation(), platform.python_version()) 47 48# NOTE(jamielennox): Clients will likely want to print more than json. Please 49# propose a patch if you have a content type you think is reasonable to print 50# here and we'll add it to the list as required. 51_LOG_CONTENT_TYPES = set(['application/json']) 52 53_MAX_RETRY_INTERVAL = 60.0 54_EXPONENTIAL_DELAY_START = 0.5 55 56# NOTE(efried): This is defined in oslo_middleware.request_id.INBOUND_HEADER, 57# but it didn't seem worth adding oslo_middleware to requirements just for that 58_REQUEST_ID_HEADER = 'X-Openstack-Request-Id' 59 60 61def _construct_session(session_obj=None): 62 # NOTE(morganfainberg): if the logic in this function changes be sure to 63 # update the betamax fixture's '_construct_session_with_betamax" function 64 # as well. 65 if not session_obj: 66 session_obj = requests.Session() 67 # Use TCPKeepAliveAdapter to fix bug 1323862 68 for scheme in list(session_obj.adapters): 69 session_obj.mount(scheme, TCPKeepAliveAdapter()) 70 return session_obj 71 72 73def _mv_legacy_headers_for_service(mv_service_type): 74 """Workaround for services that predate standardization. 75 76 TODO(sdague): eventually convert this to using os-service-types 77 and put the logic there. However, right now this is so little 78 logic, inlining it for release is a better call. 79 80 """ 81 headers = [] 82 if mv_service_type == "compute": 83 headers.append("X-OpenStack-Nova-API-Version") 84 elif mv_service_type == "baremetal": 85 headers.append("X-OpenStack-Ironic-API-Version") 86 elif mv_service_type in ["sharev2", "shared-file-system"]: 87 headers.append("X-OpenStack-Manila-API-Version") 88 return headers 89 90 91def _sanitize_headers(headers): 92 """Ensure headers are strings and not bytes.""" 93 str_dict = {} 94 for k, v in headers.items(): 95 if six.PY3: 96 # requests expects headers to be str type in python3, which means 97 # if we get a bytes we need to decode it into a str 98 k = k.decode('ASCII') if isinstance(k, six.binary_type) else k 99 if v is not None: 100 v = v.decode('ASCII') if isinstance(v, six.binary_type) else v 101 else: 102 # requests expects headers to be str type in python2, which means 103 # if we get a unicode we need to encode it to ASCII into a str 104 k = k.encode('ASCII') if isinstance(k, six.text_type) else k 105 if v is not None: 106 v = v.encode('ASCII') if isinstance(v, six.text_type) else v 107 str_dict[k] = v 108 return str_dict 109 110 111class NoOpSemaphore(object): 112 """Empty context manager for use as a default semaphore.""" 113 114 def __enter__(self): 115 """Enter the context manager and do nothing.""" 116 pass 117 118 def __exit__(self, exc_type, exc_value, traceback): 119 """Exit the context manager and do nothing.""" 120 pass 121 122 123class _JSONEncoder(json.JSONEncoder): 124 125 def default(self, o): 126 if isinstance(o, datetime.datetime): 127 return o.isoformat() 128 if isinstance(o, uuid.UUID): 129 return six.text_type(o) 130 if netaddr and isinstance(o, netaddr.IPAddress): 131 return six.text_type(o) 132 133 return super(_JSONEncoder, self).default(o) 134 135 136class _StringFormatter(object): 137 """A String formatter that fetches values on demand.""" 138 139 def __init__(self, session, auth): 140 self.session = session 141 self.auth = auth 142 143 def __getitem__(self, item): 144 if item == 'project_id': 145 value = self.session.get_project_id(self.auth) 146 elif item == 'user_id': 147 value = self.session.get_user_id(self.auth) 148 else: 149 raise AttributeError(item) 150 151 if not value: 152 raise ValueError("This type of authentication does not provide a " 153 "%s that can be substituted" % item) 154 155 return value 156 157 158def _determine_calling_package(): 159 """Walk the call frames trying to identify what is using this module.""" 160 # Create a lookup table mapping file name to module name. The ``inspect`` 161 # module does this but is far less efficient. Same story with the 162 # frame walking below. One could use ``inspect.stack()`` but it 163 # has far more overhead. 164 mod_lookup = dict((m.__file__, n) for n, m in sys.modules.items() 165 if hasattr(m, '__file__')) 166 167 # NOTE(shaleh): these are not useful because they hide the real 168 # user of the code. debtcollector did not import keystoneauth but 169 # it will show up in the call stack. Similarly we do not want to 170 # report ourselves or keystone client as the user agent. The real 171 # user is the code importing them. 172 ignored = ('debtcollector', 'keystoneauth1', 'keystoneclient') 173 174 i = 0 175 while True: 176 i += 1 177 178 try: 179 # NOTE(shaleh): this is safe in CPython but could break in 180 # other implementations of Python. Yes, the `inspect` 181 # module could be used instead. But it does a lot more 182 # work so it has worse performance. 183 f = sys._getframe(i) 184 try: 185 name = mod_lookup[f.f_code.co_filename] 186 # finds the full name module.foo.bar but all we need 187 # is the module name. 188 name, _, _ = name.partition('.') 189 if name not in ignored: 190 return name 191 except KeyError: 192 pass # builtin or the like 193 except ValueError: 194 # hit the bottom of the frame stack 195 break 196 197 return '' 198 199 200def _determine_user_agent(): 201 """Attempt to programmatically generate a user agent string. 202 203 First, look at the name of the process. Return this unless it is in 204 the `ignored` list. Otherwise, look at the function call stack and 205 try to find the name of the code that invoked this module. 206 """ 207 # NOTE(shaleh): mod_wsgi is not any more useful than just 208 # reporting "keystoneauth". Ignore it and perform the package name 209 # heuristic. 210 ignored = ('mod_wsgi', ) 211 212 try: 213 name = sys.argv[0] 214 except IndexError: 215 # sys.argv is empty, usually the Python interpreter prevents this. 216 return '' 217 218 if not name: 219 return '' 220 221 name = os.path.basename(name) 222 if name in ignored: 223 name = _determine_calling_package() 224 return name 225 226 227class RequestTiming(object): 228 """Contains timing information for an HTTP interaction.""" 229 230 #: HTTP method used for the call (GET, POST, etc) 231 method = None 232 233 #: URL against which the call was made 234 url = None 235 236 #: Elapsed time information 237 elapsed = None # type: datetime.timedelta 238 239 def __init__(self, method, url, elapsed): 240 self.method = method 241 self.url = url 242 self.elapsed = elapsed 243 244 245class _Retries(object): 246 __slots__ = ('_fixed_delay', '_current') 247 248 def __init__(self, fixed_delay=None): 249 self._fixed_delay = fixed_delay 250 self.reset() 251 252 def __next__(self): 253 value = self._current 254 if not self._fixed_delay: 255 self._current = min(value * 2, _MAX_RETRY_INTERVAL) 256 return value 257 258 def reset(self): 259 if self._fixed_delay: 260 self._current = self._fixed_delay 261 else: 262 self._current = _EXPONENTIAL_DELAY_START 263 264 # Python 2 compatibility 265 next = __next__ 266 267 268class Session(object): 269 """Maintains client communication state and common functionality. 270 271 As much as possible the parameters to this class reflect and are passed 272 directly to the :mod:`requests` library. 273 274 :param auth: An authentication plugin to authenticate the session with. 275 (optional, defaults to None) 276 :type auth: keystoneauth1.plugin.BaseAuthPlugin 277 :param requests.Session session: A requests session object that can be used 278 for issuing requests. (optional) 279 :param str original_ip: The original IP of the requesting user which will 280 be sent to identity service in a 'Forwarded' 281 header. (optional) 282 :param verify: The verification arguments to pass to requests. These are of 283 the same form as requests expects, so True or False to 284 verify (or not) against system certificates or a path to a 285 bundle or CA certs to check against or None for requests to 286 attempt to locate and use certificates. (optional, defaults 287 to True) 288 :param cert: A client certificate to pass to requests. These are of the 289 same form as requests expects. Either a single filename 290 containing both the certificate and key or a tuple containing 291 the path to the certificate then a path to the key. (optional) 292 :param float timeout: A timeout to pass to requests. This should be a 293 numerical value indicating some amount (or fraction) 294 of seconds or 0 for no timeout. (optional, defaults 295 to 0) 296 :param str user_agent: A User-Agent header string to use for the request. 297 If not provided, a default of 298 :attr:`~keystoneauth1.session.DEFAULT_USER_AGENT` is 299 used, which contains the keystoneauth1 version as 300 well as those of the requests library and which 301 Python is being used. When a non-None value is 302 passed, it will be prepended to the default. 303 :param int/bool redirect: Controls the maximum number of redirections that 304 can be followed by a request. Either an integer 305 for a specific count or True/False for 306 forever/never. (optional, default to 30) 307 :param dict additional_headers: Additional headers that should be attached 308 to every request passing through the 309 session. Headers of the same name specified 310 per request will take priority. 311 :param str app_name: The name of the application that is creating the 312 session. This will be used to create the user_agent. 313 :param str app_version: The version of the application creating the 314 session. This will be used to create the 315 user_agent. 316 :param list additional_user_agent: A list of tuple of name, version that 317 will be added to the user agent. This 318 can be used by libraries that are part 319 of the communication process. 320 :param dict discovery_cache: A dict to be used for caching of discovery 321 information. This is normally managed 322 transparently, but if the user wants to 323 share a single cache across multiple sessions 324 that do not share an auth plugin, it can 325 be provided here. (optional, defaults to 326 None which means automatically manage) 327 :param bool split_loggers: Split the logging of requests across multiple 328 loggers instead of just one. Defaults to False. 329 :param bool collect_timing: Whether or not to collect per-method timing 330 information for each API call. (optional, 331 defaults to False) 332 :param rate_semaphore: Semaphore to be used to control concurrency 333 and rate limiting of requests. (optional, 334 defaults to no concurrency or rate control) 335 :param int connect_retries: the maximum number of retries that should 336 be attempted for connection errors. 337 (optional, defaults to 0 - never retry). 338 """ 339 340 user_agent = None 341 342 _REDIRECT_STATUSES = (301, 302, 303, 305, 307, 308) 343 344 _DEFAULT_REDIRECT_LIMIT = 30 345 346 def __init__(self, auth=None, session=None, original_ip=None, verify=True, 347 cert=None, timeout=None, user_agent=None, 348 redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None, 349 app_name=None, app_version=None, additional_user_agent=None, 350 discovery_cache=None, split_loggers=None, 351 collect_timing=False, rate_semaphore=None, 352 connect_retries=0): 353 354 self.auth = auth 355 self.session = _construct_session(session) 356 # NOTE(mwhahaha): keep a reference to the session object so we can 357 # clean it up when this object goes away. We don't want to close the 358 # session if it was passed into us as it may be reused externally. 359 # See LP#1838704 360 self._session = None 361 if not session: 362 self._session = self.session 363 self.original_ip = original_ip 364 self.verify = verify 365 self.cert = cert 366 self.timeout = None 367 self.redirect = redirect 368 self.additional_headers = additional_headers or {} 369 self.app_name = app_name 370 self.app_version = app_version 371 self.additional_user_agent = additional_user_agent or [] 372 self._determined_user_agent = None 373 if discovery_cache is None: 374 discovery_cache = {} 375 self._discovery_cache = discovery_cache 376 # NOTE(mordred) split_loggers kwarg default is None rather than False 377 # so we can distinguish between the value being set or not. 378 self._split_loggers = split_loggers 379 self._collect_timing = collect_timing 380 self._connect_retries = connect_retries 381 self._api_times = [] 382 self._rate_semaphore = rate_semaphore or NoOpSemaphore() 383 384 if timeout is not None: 385 self.timeout = float(timeout) 386 387 if user_agent is not None: 388 self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) 389 390 self._json = _JSONEncoder() 391 392 def __del__(self): 393 """Clean up resources on delete.""" 394 if self._session: 395 # If we created a requests.Session, try to close it out correctly 396 try: 397 self._session.close() 398 except Exception: 399 pass 400 finally: 401 self._session = None 402 403 @property 404 def adapters(self): 405 return self.session.adapters 406 407 @adapters.setter 408 def adapters(self, value): 409 self.session.adapters = value 410 411 def mount(self, scheme, adapter): 412 self.session.mount(scheme, adapter) 413 414 def _remove_service_catalog(self, body): 415 try: 416 data = json.loads(body) 417 418 # V3 token 419 if 'token' in data and 'catalog' in data['token']: 420 data['token']['catalog'] = '<removed>' 421 return self._json.encode(data) 422 423 # V2 token 424 if 'serviceCatalog' in data['access']: 425 data['access']['serviceCatalog'] = '<removed>' 426 return self._json.encode(data) 427 428 except Exception: 429 # Don't fail trying to clean up the request body. 430 pass 431 return body 432 433 @staticmethod 434 def _process_header(header): 435 """Redact the secure headers to be logged.""" 436 secure_headers = ('authorization', 'x-auth-token', 437 'x-subject-token', 'x-service-token') 438 if header[0].lower() in secure_headers: 439 token_hasher = hashlib.sha256() 440 token_hasher.update(header[1].encode('utf-8')) 441 token_hash = token_hasher.hexdigest() 442 return (header[0], '{SHA256}%s' % token_hash) 443 return header 444 445 def _get_split_loggers(self, split_loggers): 446 """Get a boolean value from the various argument sources. 447 448 We default split_loggers to None in the kwargs of the Session 449 constructor so we can track set vs. not set. We also accept 450 split_loggers as a parameter in a few other places. In each place 451 we want the parameter, if given by the user, to win. 452 """ 453 # None is the default value in each method's kwarg. None means "unset". 454 if split_loggers is None: 455 # If no value was given, try the value set on the instance. 456 split_loggers = self._split_loggers 457 if split_loggers is None: 458 # If neither a value was given on the method, nor a value was given 459 # on the Session constructor, then the value is False. 460 split_loggers = False 461 return split_loggers 462 463 def _http_log_request(self, url, method=None, data=None, 464 json=None, headers=None, query_params=None, 465 logger=None, split_loggers=None): 466 string_parts = [] 467 468 if self._get_split_loggers(split_loggers): 469 logger = utils.get_logger(__name__ + '.request') 470 else: 471 # Only a single logger was passed in, use string prefixing. 472 string_parts.append('REQ:') 473 if not logger: 474 logger = utils.get_logger(__name__) 475 476 if not logger.isEnabledFor(logging.DEBUG): 477 # NOTE(morganfainberg): This whole debug section is expensive, 478 # there is no need to do the work if we're not going to emit a 479 # debug log. 480 return 481 482 string_parts.append('curl -g -i') 483 484 # NOTE(jamielennox): None means let requests do its default validation 485 # so we need to actually check that this is False. 486 if self.verify is False: 487 string_parts.append('--insecure') 488 elif isinstance(self.verify, six.string_types): 489 string_parts.append('--cacert "%s"' % self.verify) 490 491 if method: 492 string_parts.extend(['-X', method]) 493 494 if query_params: 495 # Don't check against `is not None` as this can be 496 # an empty dictionary, which we shouldn't bother logging. 497 url = url + '?' + urllib.parse.urlencode(query_params) 498 # URLs with query strings need to be wrapped in quotes in order 499 # for the CURL command to run properly. 500 string_parts.append('"%s"' % url) 501 else: 502 string_parts.append(url) 503 504 if headers: 505 # Sort headers so that testing can work consistently. 506 for header in sorted(headers.items()): 507 string_parts.append('-H "%s: %s"' 508 % self._process_header(header)) 509 if json: 510 data = self._json.encode(json) 511 if data: 512 if isinstance(data, six.binary_type): 513 try: 514 data = data.decode("ascii") 515 except UnicodeDecodeError: 516 data = "<binary_data>" 517 string_parts.append("-d '%s'" % data) 518 519 logger.debug(' '.join(string_parts)) 520 521 def _http_log_response(self, response=None, json=None, 522 status_code=None, headers=None, text=None, 523 logger=None, split_loggers=True): 524 string_parts = [] 525 body_parts = [] 526 if self._get_split_loggers(split_loggers): 527 logger = utils.get_logger(__name__ + '.response') 528 body_logger = utils.get_logger(__name__ + '.body') 529 else: 530 # Only a single logger was passed in, use string prefixing. 531 string_parts.append('RESP:') 532 body_parts.append('RESP BODY:') 533 body_logger = logger 534 535 if not logger.isEnabledFor(logging.DEBUG): 536 return 537 538 if response is not None: 539 if not status_code: 540 status_code = response.status_code 541 if not headers: 542 headers = response.headers 543 544 if status_code: 545 string_parts.append('[%s]' % status_code) 546 if headers: 547 # Sort headers so that testing can work consistently. 548 for header in sorted(headers.items()): 549 string_parts.append('%s: %s' % self._process_header(header)) 550 logger.debug(' '.join(string_parts)) 551 552 if not body_logger.isEnabledFor(logging.DEBUG): 553 return 554 555 if response is not None: 556 if not text: 557 # NOTE(samueldmq): If the response does not provide enough info 558 # about the content type to decide whether it is useful and 559 # safe to log it or not, just do not log the body. Trying to 560 # read the response body anyways may result on reading a long 561 # stream of bytes and getting an unexpected MemoryError. See 562 # bug 1616105 for further details. 563 content_type = response.headers.get('content-type', None) 564 565 # NOTE(lamt): Per [1], the Content-Type header can be of the 566 # form Content-Type := type "/" subtype *[";" parameter] 567 # [1] https://www.w3.org/Protocols/rfc1341/4_Content-Type.html 568 for log_type in _LOG_CONTENT_TYPES: 569 if content_type is not None and content_type.startswith( 570 log_type): 571 text = self._remove_service_catalog(response.text) 572 break 573 else: 574 text = ('Omitted, Content-Type is set to %s. Only ' 575 '%s responses have their bodies logged.') 576 text = text % (content_type, ', '.join(_LOG_CONTENT_TYPES)) 577 if json: 578 text = self._json.encode(json) 579 580 if text: 581 body_parts.append(text) 582 body_logger.debug(' '.join(body_parts)) 583 584 @staticmethod 585 def _set_microversion_headers( 586 headers, microversion, service_type, endpoint_filter): 587 # We're converting it to normalized version number for two reasons. 588 # First, to validate it's a real version number. Second, so that in 589 # the future we can pre-validate that it is within the range of 590 # available microversions before we send the request. 591 # TODO(mordred) Validate when we get the response back that 592 # the server executed in the microversion we expected. 593 # TODO(mordred) Validate that the requested microversion works 594 # with the microversion range we found in discovery. 595 microversion = discover.normalize_version_number(microversion) 596 # Can't specify a M.latest microversion 597 if (microversion[0] != discover.LATEST and 598 discover.LATEST in microversion[1:]): 599 raise TypeError( 600 "Specifying a '{major}.latest' microversion is not allowed.") 601 microversion = discover.version_to_string(microversion) 602 if not service_type: 603 if endpoint_filter and 'service_type' in endpoint_filter: 604 service_type = endpoint_filter['service_type'] 605 else: 606 raise TypeError( 607 "microversion {microversion} was requested but no" 608 " service_type information is available. Either provide a" 609 " service_type in endpoint_filter or pass" 610 " microversion_service_type as an argument.".format( 611 microversion=microversion)) 612 613 # TODO(mordred) cinder uses volume in its microversion header. This 614 # logic should be handled in the future by os-service-types but for 615 # now hard-code for cinder. 616 if (service_type.startswith('volume') or 617 service_type == 'block-storage'): 618 service_type = 'volume' 619 elif service_type.startswith('share'): 620 # NOTE(gouthamr) manila doesn't honor the "OpenStack-API-Version" 621 # header yet, but sending it does no harm - when the service 622 # honors this header, it'll use the standardized name in the 623 # service-types-authority and not the legacy name in the cloud's 624 # service catalog 625 service_type = 'shared-file-system' 626 627 headers.setdefault('OpenStack-API-Version', 628 '{service_type} {microversion}'.format( 629 service_type=service_type, 630 microversion=microversion)) 631 header_names = _mv_legacy_headers_for_service(service_type) 632 for h in header_names: 633 headers.setdefault(h, microversion) 634 635 def request(self, url, method, json=None, original_ip=None, 636 user_agent=None, redirect=None, authenticated=None, 637 endpoint_filter=None, auth=None, requests_auth=None, 638 raise_exc=True, allow_reauth=True, log=True, 639 endpoint_override=None, connect_retries=None, logger=None, 640 allow=None, client_name=None, client_version=None, 641 microversion=None, microversion_service_type=None, 642 status_code_retries=0, retriable_status_codes=None, 643 rate_semaphore=None, global_request_id=None, 644 connect_retry_delay=None, status_code_retry_delay=None, 645 **kwargs): 646 """Send an HTTP request with the specified characteristics. 647 648 Wrapper around `requests.Session.request` to handle tasks such as 649 setting headers, JSON encoding/decoding, and error handling. 650 651 Arguments that are not handled are passed through to the requests 652 library. 653 654 :param str url: Path or fully qualified URL of HTTP request. If only a 655 path is provided then endpoint_filter must also be 656 provided such that the base URL can be determined. If a 657 fully qualified URL is provided then endpoint_filter 658 will be ignored. 659 :param str method: The http method to use. (e.g. 'GET', 'POST') 660 :param str original_ip: Mark this request as forwarded for this ip. 661 (optional) 662 :param dict headers: Headers to be included in the request. (optional) 663 :param json: Some data to be represented as JSON. (optional) 664 :param str user_agent: A user_agent to use for the request. If present 665 will override one present in headers. (optional) 666 :param int/bool redirect: the maximum number of redirections that 667 can be followed by a request. Either an 668 integer for a specific count or True/False 669 for forever/never. (optional) 670 :param int connect_retries: the maximum number of retries that should 671 be attempted for connection errors. 672 (optional, defaults to None - never retry). 673 :param bool authenticated: True if a token should be attached to this 674 request, False if not or None for attach if 675 an auth_plugin is available. 676 (optional, defaults to None) 677 :param dict endpoint_filter: Data to be provided to an auth plugin with 678 which it should be able to determine an 679 endpoint to use for this request. If not 680 provided then URL is expected to be a 681 fully qualified URL. (optional) 682 :param str endpoint_override: The URL to use instead of looking up the 683 endpoint in the auth plugin. This will be 684 ignored if a fully qualified URL is 685 provided but take priority over an 686 endpoint_filter. This string may contain 687 the values ``%(project_id)s`` and 688 ``%(user_id)s`` to have those values 689 replaced by the project_id/user_id of the 690 current authentication. (optional) 691 :param auth: The auth plugin to use when authenticating this request. 692 This will override the plugin that is attached to the 693 session (if any). (optional) 694 :type auth: keystoneauth1.plugin.BaseAuthPlugin 695 :param requests_auth: A requests library auth plugin that cannot be 696 passed via kwarg because the `auth` kwarg 697 collides with our own auth plugins. (optional) 698 :type requests_auth: :py:class:`requests.auth.AuthBase` 699 :param bool raise_exc: If True then raise an appropriate exception for 700 failed HTTP requests. If False then return the 701 request object. (optional, default True) 702 :param bool allow_reauth: Allow fetching a new token and retrying the 703 request on receiving a 401 Unauthorized 704 response. (optional, default True) 705 :param bool log: If True then log the request and response data to the 706 debug log. (optional, default True) 707 :param logger: The logger object to use to log request and responses. 708 If not provided the keystoneauth1.session default 709 logger will be used. 710 :type logger: logging.Logger 711 :param dict allow: Extra filters to pass when discovering API 712 versions. (optional) 713 :param microversion: Microversion to send for this request. 714 microversion can be given as a string or a tuple. 715 (optional) 716 :param str microversion_service_type: The service_type to be sent in 717 the microversion header, if a microversion is given. 718 Defaults to the value of service_type from 719 endpoint_filter if one exists. If endpoint_filter is not 720 provided or does not have a service_type, microversion 721 is given and microversion_service_type is not provided, 722 an exception will be raised. 723 :param int status_code_retries: the maximum number of retries that 724 should be attempted for retriable 725 HTTP status codes (optional, defaults 726 to 0 - never retry). 727 :param list retriable_status_codes: list of HTTP status codes that 728 should be retried (optional, 729 defaults to HTTP 503, has no effect 730 when status_code_retries is 0). 731 :param rate_semaphore: Semaphore to be used to control concurrency 732 and rate limiting of requests. (optional, 733 defaults to no concurrency or rate control) 734 :param global_request_id: Value for the X-Openstack-Request-Id header. 735 :param float connect_retry_delay: Delay (in seconds) between two 736 connect retries (if enabled). 737 By default exponential retry starting 738 with 0.5 seconds up to a maximum of 739 60 seconds is used. 740 :param float status_code_retry_delay: Delay (in seconds) between two 741 status code retries (if enabled). 742 By default exponential retry 743 starting with 0.5 seconds up to 744 a maximum of 60 seconds is used. 745 :param kwargs: any other parameter that can be passed to 746 :meth:`requests.Session.request` (such as `headers`). 747 Except: 748 749 - `data` will be overwritten by the data in the `json` 750 param. 751 - `allow_redirects` is ignored as redirects are handled 752 by the session. 753 754 :raises keystoneauth1.exceptions.base.ClientException: For connection 755 failure, or to indicate an error response code. 756 757 :returns: The response to the request. 758 """ 759 # If a logger is passed in, use it and do not log requests, responses 760 # and bodies separately. 761 if logger: 762 split_loggers = False 763 else: 764 split_loggers = None 765 logger = logger or utils.get_logger(__name__) 766 # NOTE(gmann): Convert r initlize the headers to 767 # CaseInsensitiveDict to make sure headers are 768 # case insensitive. 769 if kwargs.get('headers'): 770 kwargs['headers'] = requests.structures.CaseInsensitiveDict( 771 kwargs['headers']) 772 else: 773 kwargs['headers'] = requests.structures.CaseInsensitiveDict() 774 if connect_retries is None: 775 connect_retries = self._connect_retries 776 # HTTP 503 - Service Unavailable 777 retriable_status_codes = retriable_status_codes or [503] 778 rate_semaphore = rate_semaphore or self._rate_semaphore 779 780 headers = kwargs.setdefault('headers', dict()) 781 if microversion: 782 self._set_microversion_headers( 783 headers, microversion, microversion_service_type, 784 endpoint_filter) 785 786 if authenticated is None: 787 authenticated = bool(auth or self.auth) 788 789 if authenticated: 790 auth_headers = self.get_auth_headers(auth) 791 792 if auth_headers is None: 793 msg = 'No valid authentication is available' 794 raise exceptions.AuthorizationFailure(msg) 795 796 headers.update(auth_headers) 797 798 if osprofiler_web: 799 headers.update(osprofiler_web.get_trace_id_headers()) 800 801 # if we are passed a fully qualified URL and an endpoint_filter we 802 # should ignore the filter. This will make it easier for clients who 803 # want to overrule the default endpoint_filter data added to all client 804 # requests. We check fully qualified here by the presence of a host. 805 if not urllib.parse.urlparse(url).netloc: 806 base_url = None 807 808 if endpoint_override: 809 base_url = endpoint_override % _StringFormatter(self, auth) 810 elif endpoint_filter: 811 base_url = self.get_endpoint(auth, allow=allow, 812 **endpoint_filter) 813 814 if not base_url: 815 raise exceptions.EndpointNotFound() 816 817 url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) 818 819 if self.cert: 820 kwargs.setdefault('cert', self.cert) 821 822 if self.timeout is not None: 823 kwargs.setdefault('timeout', self.timeout) 824 825 if user_agent: 826 headers['User-Agent'] = user_agent 827 elif self.user_agent: 828 user_agent = headers.setdefault('User-Agent', self.user_agent) 829 else: 830 # Per RFC 7231 Section 5.5.3, identifiers in a user-agent should be 831 # ordered by decreasing significance. If a user sets their product 832 # that value will be used. Otherwise we attempt to derive a useful 833 # product value. The value will be prepended it to the KSA version, 834 # requests version, and then the Python version. 835 836 agent = [] 837 838 if self.app_name and self.app_version: 839 agent.append('%s/%s' % (self.app_name, self.app_version)) 840 elif self.app_name: 841 agent.append(self.app_name) 842 843 if client_name and client_version: 844 agent.append('%s/%s' % (client_name, client_version)) 845 elif client_name: 846 agent.append(client_name) 847 848 for additional in self.additional_user_agent: 849 agent.append('%s/%s' % additional) 850 851 if not agent: 852 # NOTE(jamielennox): determine_user_agent will return an empty 853 # string on failure so checking for None will ensure it is only 854 # called once even on failure. 855 if self._determined_user_agent is None: 856 self._determined_user_agent = _determine_user_agent() 857 858 if self._determined_user_agent: 859 agent.append(self._determined_user_agent) 860 861 agent.append(DEFAULT_USER_AGENT) 862 user_agent = headers.setdefault('User-Agent', ' '.join(agent)) 863 864 if self.original_ip: 865 headers.setdefault('Forwarded', 866 'for=%s;by=%s' % (self.original_ip, user_agent)) 867 868 if json is not None: 869 headers.setdefault('Content-Type', 'application/json') 870 kwargs['data'] = self._json.encode(json) 871 872 if global_request_id is not None: 873 # NOTE(efried): This does *not* setdefault. If a global_request_id 874 # kwarg was explicitly specified, it should override any value 875 # previously configured (e.g. in Adapter.global_request_id). 876 headers[_REQUEST_ID_HEADER] = global_request_id 877 878 for k, v in self.additional_headers.items(): 879 headers.setdefault(k, v) 880 881 # Bug #1766235: some headers may be bytes 882 headers = _sanitize_headers(headers) 883 kwargs['headers'] = headers 884 885 kwargs.setdefault('verify', self.verify) 886 887 if requests_auth: 888 kwargs['auth'] = requests_auth 889 890 # Query parameters that are included in the url string will 891 # be logged properly, but those sent in the `params` parameter 892 # (which the requests library handles) need to be explicitly 893 # picked out so they can be included in the URL that gets loggged. 894 query_params = kwargs.get('params', dict()) 895 896 if log: 897 self._http_log_request(url, method=method, 898 data=kwargs.get('data'), 899 headers=headers, 900 query_params=query_params, 901 logger=logger, split_loggers=split_loggers) 902 903 # Force disable requests redirect handling. We will manage this below. 904 kwargs['allow_redirects'] = False 905 906 if redirect is None: 907 redirect = self.redirect 908 909 connect_retry_delays = _Retries(connect_retry_delay) 910 status_code_retry_delays = _Retries(status_code_retry_delay) 911 912 send = functools.partial(self._send_request, 913 url, method, redirect, log, logger, 914 split_loggers, connect_retries, 915 status_code_retries, retriable_status_codes, 916 rate_semaphore, connect_retry_delays, 917 status_code_retry_delays) 918 919 try: 920 connection_params = self.get_auth_connection_params(auth=auth) 921 except exceptions.MissingAuthPlugin: 922 # NOTE(jamielennox): If we've gotten this far without an auth 923 # plugin then we should be happy with allowing no additional 924 # connection params. This will be the typical case for plugins 925 # anyway. 926 pass 927 else: 928 if connection_params: 929 kwargs.update(connection_params) 930 931 resp = send(**kwargs) 932 933 # log callee and caller request-id for each api call 934 if log: 935 # service_name should be fetched from endpoint_filter if it is not 936 # present then use service_type as service_name. 937 service_name = None 938 if endpoint_filter: 939 service_name = endpoint_filter.get('service_name') 940 if not service_name: 941 service_name = endpoint_filter.get('service_type') 942 943 # Nova uses 'x-compute-request-id' and other services like 944 # Glance, Cinder etc are using 'x-openstack-request-id' to store 945 # request-id in the header 946 request_id = (resp.headers.get('x-openstack-request-id') or 947 resp.headers.get('x-compute-request-id')) 948 if request_id: 949 if self._get_split_loggers(split_loggers): 950 id_logger = utils.get_logger(__name__ + '.request-id') 951 else: 952 id_logger = logger 953 if service_name: 954 id_logger.debug( 955 '%(method)s call to %(service_name)s for ' 956 '%(url)s used request id ' 957 '%(response_request_id)s', { 958 'method': resp.request.method, 959 'service_name': service_name, 960 'url': resp.url, 961 'response_request_id': request_id 962 }) 963 else: 964 id_logger.debug( 965 '%(method)s call to ' 966 '%(url)s used request id ' 967 '%(response_request_id)s', { 968 'method': resp.request.method, 969 'url': resp.url, 970 'response_request_id': request_id 971 }) 972 973 # handle getting a 401 Unauthorized response by invalidating the plugin 974 # and then retrying the request. This is only tried once. 975 if resp.status_code == 401 and authenticated and allow_reauth: 976 if self.invalidate(auth): 977 auth_headers = self.get_auth_headers(auth) 978 979 if auth_headers is not None: 980 headers.update(auth_headers) 981 resp = send(**kwargs) 982 983 if raise_exc and resp.status_code >= 400: 984 logger.debug('Request returned failure status: %s', 985 resp.status_code) 986 raise exceptions.from_response(resp, method, url) 987 988 if self._collect_timing: 989 for h in resp.history: 990 self._api_times.append(RequestTiming( 991 method=h.request.method, 992 url=h.request.url, 993 elapsed=h.elapsed, 994 )) 995 self._api_times.append(RequestTiming( 996 method=resp.request.method, 997 url=resp.request.url, 998 elapsed=resp.elapsed, 999 )) 1000 1001 return resp 1002 1003 def _send_request(self, url, method, redirect, log, logger, split_loggers, 1004 connect_retries, status_code_retries, 1005 retriable_status_codes, rate_semaphore, 1006 connect_retry_delays, status_code_retry_delays, 1007 **kwargs): 1008 # NOTE(jamielennox): We handle redirection manually because the 1009 # requests lib follows some browser patterns where it will redirect 1010 # POSTs as GETs for certain statuses which is not want we want for an 1011 # API. See: https://en.wikipedia.org/wiki/Post/Redirect/Get 1012 1013 # NOTE(jamielennox): The interaction between retries and redirects are 1014 # handled naively. We will attempt only a maximum number of retries and 1015 # redirects rather than per request limits. Otherwise the extreme case 1016 # could be redirects * retries requests. This will be sufficient in 1017 # most cases and can be fixed properly if there's ever a need. 1018 1019 try: 1020 try: 1021 with rate_semaphore: 1022 resp = self.session.request(method, url, **kwargs) 1023 except requests.exceptions.SSLError as e: 1024 msg = 'SSL exception connecting to %(url)s: %(error)s' % { 1025 'url': url, 'error': e} 1026 raise exceptions.SSLError(msg) 1027 except requests.exceptions.Timeout: 1028 msg = 'Request to %s timed out' % url 1029 raise exceptions.ConnectTimeout(msg) 1030 except requests.exceptions.ConnectionError as e: 1031 # NOTE(sdague): urllib3/requests connection error is a 1032 # translation of SocketError. However, SocketError 1033 # happens for many different reasons, and that low 1034 # level message is often really important in figuring 1035 # out the difference between network misconfigurations 1036 # and firewall blocking. 1037 msg = 'Unable to establish connection to %s: %s' % (url, e) 1038 raise exceptions.ConnectFailure(msg) 1039 except requests.exceptions.RequestException as e: 1040 msg = 'Unexpected exception for %(url)s: %(error)s' % { 1041 'url': url, 'error': e} 1042 raise exceptions.UnknownConnectionError(msg, e) 1043 1044 except exceptions.RetriableConnectionFailure as e: 1045 if connect_retries <= 0: 1046 raise 1047 1048 delay = next(connect_retry_delays) 1049 logger.info('Failure: %(e)s. Retrying in %(delay).1fs.', 1050 {'e': e, 'delay': delay}) 1051 time.sleep(delay) 1052 1053 return self._send_request( 1054 url, method, redirect, log, logger, split_loggers, 1055 status_code_retries=status_code_retries, 1056 retriable_status_codes=retriable_status_codes, 1057 rate_semaphore=rate_semaphore, 1058 connect_retries=connect_retries - 1, 1059 connect_retry_delays=connect_retry_delays, 1060 status_code_retry_delays=status_code_retry_delays, 1061 **kwargs) 1062 1063 if log: 1064 self._http_log_response( 1065 response=resp, logger=logger, 1066 split_loggers=split_loggers) 1067 1068 if resp.status_code in self._REDIRECT_STATUSES: 1069 # be careful here in python True == 1 and False == 0 1070 if isinstance(redirect, bool): 1071 redirect_allowed = redirect 1072 else: 1073 redirect -= 1 1074 redirect_allowed = redirect >= 0 1075 1076 if not redirect_allowed: 1077 return resp 1078 1079 try: 1080 location = resp.headers['location'] 1081 except KeyError: 1082 logger.warning("Failed to redirect request to %s as new " 1083 "location was not provided.", resp.url) 1084 else: 1085 # NOTE(jamielennox): We don't keep increasing delays. 1086 # This request actually worked so we can reset the delay count. 1087 connect_retry_delays.reset() 1088 status_code_retry_delays.reset() 1089 new_resp = self._send_request( 1090 location, method, redirect, log, logger, split_loggers, 1091 rate_semaphore=rate_semaphore, 1092 connect_retries=connect_retries, 1093 status_code_retries=status_code_retries, 1094 retriable_status_codes=retriable_status_codes, 1095 connect_retry_delays=connect_retry_delays, 1096 status_code_retry_delays=status_code_retry_delays, 1097 **kwargs) 1098 1099 if not isinstance(new_resp.history, list): 1100 new_resp.history = list(new_resp.history) 1101 new_resp.history.insert(0, resp) 1102 resp = new_resp 1103 elif (resp.status_code in retriable_status_codes and 1104 status_code_retries > 0): 1105 1106 delay = next(status_code_retry_delays) 1107 logger.info('Retriable status code %(code)s. Retrying in ' 1108 '%(delay).1fs.', 1109 {'code': resp.status_code, 'delay': delay}) 1110 time.sleep(delay) 1111 1112 # NOTE(jamielennox): We don't keep increasing connection delays. 1113 # This request actually worked so we can reset the delay count. 1114 connect_retry_delays.reset() 1115 return self._send_request( 1116 url, method, redirect, log, logger, split_loggers, 1117 connect_retries=connect_retries, 1118 status_code_retries=status_code_retries - 1, 1119 retriable_status_codes=retriable_status_codes, 1120 rate_semaphore=rate_semaphore, 1121 connect_retry_delays=connect_retry_delays, 1122 status_code_retry_delays=status_code_retry_delays, 1123 **kwargs) 1124 1125 return resp 1126 1127 def head(self, url, **kwargs): 1128 """Perform a HEAD request. 1129 1130 This calls :py:meth:`.request()` with ``method`` set to ``HEAD``. 1131 1132 """ 1133 return self.request(url, 'HEAD', **kwargs) 1134 1135 def get(self, url, **kwargs): 1136 """Perform a GET request. 1137 1138 This calls :py:meth:`.request()` with ``method`` set to ``GET``. 1139 1140 """ 1141 return self.request(url, 'GET', **kwargs) 1142 1143 def post(self, url, **kwargs): 1144 """Perform a POST request. 1145 1146 This calls :py:meth:`.request()` with ``method`` set to ``POST``. 1147 1148 """ 1149 return self.request(url, 'POST', **kwargs) 1150 1151 def put(self, url, **kwargs): 1152 """Perform a PUT request. 1153 1154 This calls :py:meth:`.request()` with ``method`` set to ``PUT``. 1155 1156 """ 1157 return self.request(url, 'PUT', **kwargs) 1158 1159 def delete(self, url, **kwargs): 1160 """Perform a DELETE request. 1161 1162 This calls :py:meth:`.request()` with ``method`` set to ``DELETE``. 1163 1164 """ 1165 return self.request(url, 'DELETE', **kwargs) 1166 1167 def patch(self, url, **kwargs): 1168 """Perform a PATCH request. 1169 1170 This calls :py:meth:`.request()` with ``method`` set to ``PATCH``. 1171 1172 """ 1173 return self.request(url, 'PATCH', **kwargs) 1174 1175 def _auth_required(self, auth, msg): 1176 if not auth: 1177 auth = self.auth 1178 1179 if not auth: 1180 msg_fmt = 'An auth plugin is required to %s' 1181 raise exceptions.MissingAuthPlugin(msg_fmt % msg) 1182 1183 return auth 1184 1185 def get_auth_headers(self, auth=None, **kwargs): 1186 """Return auth headers as provided by the auth plugin. 1187 1188 :param auth: The auth plugin to use for token. Overrides the plugin 1189 on the session. (optional) 1190 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1191 1192 :raises keystoneauth1.exceptions.auth.AuthorizationFailure: 1193 if a new token fetch fails. 1194 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1195 if a plugin is not available. 1196 1197 :returns: Authentication headers or None for failure. 1198 :rtype: :class:`dict` 1199 """ 1200 auth = self._auth_required(auth, 'fetch a token') 1201 return auth.get_headers(self, **kwargs) 1202 1203 def get_token(self, auth=None): 1204 """Return a token as provided by the auth plugin. 1205 1206 :param auth: The auth plugin to use for token. Overrides the plugin 1207 on the session. (optional) 1208 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1209 1210 :raises keystoneauth1.exceptions.auth.AuthorizationFailure: 1211 if a new token fetch fails. 1212 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1213 if a plugin is not available. 1214 1215 .. warning:: 1216 **DEPRECATED**: This assumes that the only header that is used to 1217 authenticate a message is ``X-Auth-Token``. This may not be 1218 correct. Use :meth:`get_auth_headers` instead. 1219 1220 :returns: A valid token. 1221 :rtype: string 1222 """ 1223 return (self.get_auth_headers(auth) or {}).get('X-Auth-Token') 1224 1225 def get_endpoint(self, auth=None, **kwargs): 1226 """Get an endpoint as provided by the auth plugin. 1227 1228 :param auth: The auth plugin to use for token. Overrides the plugin on 1229 the session. (optional) 1230 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1231 1232 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1233 if a plugin is not available. 1234 1235 :returns: An endpoint if available or None. 1236 :rtype: string 1237 """ 1238 if 'endpoint_override' in kwargs: 1239 return kwargs['endpoint_override'] 1240 1241 auth = self._auth_required(auth, 'determine endpoint URL') 1242 1243 return auth.get_endpoint(self, **kwargs) 1244 1245 def get_endpoint_data(self, auth=None, **kwargs): 1246 """Get endpoint data as provided by the auth plugin. 1247 1248 :param auth: The auth plugin to use for token. Overrides the plugin on 1249 the session. (optional) 1250 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1251 1252 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1253 if a plugin is not available. 1254 :raises TypeError: If arguments are invalid 1255 1256 :returns: Endpoint data if available or None. 1257 :rtype: keystoneauth1.discover.EndpointData 1258 """ 1259 auth = self._auth_required(auth, 'determine endpoint URL') 1260 return auth.get_endpoint_data(self, **kwargs) 1261 1262 def get_api_major_version(self, auth=None, **kwargs): 1263 """Get the major API version as provided by the auth plugin. 1264 1265 :param auth: The auth plugin to use for token. Overrides the plugin on 1266 the session. (optional) 1267 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1268 1269 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: if a 1270 plugin is not available. 1271 1272 :return: The major version of the API of the service discovered. 1273 :rtype: tuple or None 1274 """ 1275 auth = self._auth_required(auth, 'determine endpoint URL') 1276 return auth.get_api_major_version(self, **kwargs) 1277 1278 def get_all_version_data(self, auth=None, interface='public', 1279 region_name=None, service_type=None, 1280 **kwargs): 1281 """Get version data for all services in the catalog. 1282 1283 :param auth: 1284 The auth plugin to use for token. Overrides the plugin on 1285 the session. (optional) 1286 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1287 :param interface: 1288 Type of endpoint to get version data for. Can be a single value 1289 or a list of values. A value of None indicates that all interfaces 1290 should be queried. (optional, defaults to public) 1291 :param string region_name: 1292 Region of endpoints to get version data for. A valueof None 1293 indicates that all regions should be queried. (optional, defaults 1294 to None) 1295 :param string service_type: 1296 Limit the version data to a single service. (optional, defaults 1297 to None) 1298 :returns: A dictionary keyed by region_name with values containing 1299 dictionaries keyed by interface with values being a list of 1300 `~keystoneauth1.discover.VersionData`. 1301 """ 1302 auth = self._auth_required(auth, 'determine endpoint URL') 1303 return auth.get_all_version_data( 1304 self, 1305 interface=interface, 1306 region_name=region_name, 1307 service_type=service_type, 1308 **kwargs) 1309 1310 def get_auth_connection_params(self, auth=None, **kwargs): 1311 """Return auth connection params as provided by the auth plugin. 1312 1313 An auth plugin may specify connection parameters to the request like 1314 providing a client certificate for communication. 1315 1316 We restrict the values that may be returned from this function to 1317 prevent an auth plugin overriding values unrelated to connection 1318 parmeters. The values that are currently accepted are: 1319 1320 - `cert`: a path to a client certificate, or tuple of client 1321 certificate and key pair that are used with this request. 1322 - `verify`: a boolean value to indicate verifying SSL certificates 1323 against the system CAs or a path to a CA file to verify with. 1324 1325 These values are passed to the requests library and further information 1326 on accepted values may be found there. 1327 1328 :param auth: The auth plugin to use for tokens. Overrides the plugin 1329 on the session. (optional) 1330 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1331 1332 :raises keystoneauth1.exceptions.auth.AuthorizationFailure: 1333 if a new token fetch fails. 1334 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1335 if a plugin is not available. 1336 :raises keystoneauth1.exceptions.auth_plugins.UnsupportedParameters: 1337 if the plugin returns a parameter that is not supported by this 1338 session. 1339 1340 :returns: Authentication headers or None for failure. 1341 :rtype: :class:`dict` 1342 """ 1343 auth = self._auth_required(auth, 'fetch connection params') 1344 params = auth.get_connection_params(self, **kwargs) 1345 1346 # NOTE(jamielennox): There needs to be some consensus on what 1347 # parameters are allowed to be modified by the auth plugin here. 1348 # Ideally I think it would be only the send() parts of the request 1349 # flow. For now lets just allow certain elements. 1350 params_copy = params.copy() 1351 1352 for arg in ('cert', 'verify'): 1353 try: 1354 kwargs[arg] = params_copy.pop(arg) 1355 except KeyError: 1356 pass 1357 1358 if params_copy: 1359 raise exceptions.UnsupportedParameters(list(params_copy.keys())) 1360 1361 return params 1362 1363 def invalidate(self, auth=None): 1364 """Invalidate an authentication plugin. 1365 1366 :param auth: The auth plugin to invalidate. Overrides the plugin on the 1367 session. (optional) 1368 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1369 1370 """ 1371 auth = self._auth_required(auth, 'validate') 1372 return auth.invalidate() 1373 1374 def get_user_id(self, auth=None): 1375 """Return the authenticated user_id as provided by the auth plugin. 1376 1377 :param auth: The auth plugin to use for token. Overrides the plugin 1378 on the session. (optional) 1379 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1380 1381 :raises keystoneauth1.exceptions.auth.AuthorizationFailure: 1382 if a new token fetch fails. 1383 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1384 if a plugin is not available. 1385 1386 :returns: Current user_id or None if not supported by plugin. 1387 :rtype: :class:`str` 1388 """ 1389 auth = self._auth_required(auth, 'get user_id') 1390 return auth.get_user_id(self) 1391 1392 def get_project_id(self, auth=None): 1393 """Return the authenticated project_id as provided by the auth plugin. 1394 1395 :param auth: The auth plugin to use for token. Overrides the plugin 1396 on the session. (optional) 1397 :type auth: keystoneauth1.plugin.BaseAuthPlugin 1398 1399 :raises keystoneauth1.exceptions.auth.AuthorizationFailure: 1400 if a new token fetch fails. 1401 :raises keystoneauth1.exceptions.auth_plugins.MissingAuthPlugin: 1402 if a plugin is not available. 1403 1404 :returns: Current project_id or None if not supported by plugin. 1405 :rtype: :class:`str` 1406 """ 1407 auth = self._auth_required(auth, 'get project_id') 1408 return auth.get_project_id(self) 1409 1410 def get_timings(self): 1411 """Return collected API timing information. 1412 1413 :returns: List of `RequestTiming` objects. 1414 """ 1415 return self._api_times 1416 1417 def reset_timings(self): 1418 """Clear API timing information.""" 1419 self._api_times = [] 1420 1421 1422REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.')) 1423 1424 1425class TCPKeepAliveAdapter(requests.adapters.HTTPAdapter): 1426 """The custom adapter used to set TCP Keep-Alive on all connections. 1427 1428 This Adapter also preserves the default behaviour of Requests which 1429 disables Nagle's Algorithm. See also: 1430 https://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx 1431 """ 1432 1433 def init_poolmanager(self, *args, **kwargs): 1434 if 'socket_options' not in kwargs and REQUESTS_VERSION >= (2, 4, 1): 1435 socket_options = [ 1436 # Keep Nagle's algorithm off 1437 (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), 1438 # Turn on TCP Keep-Alive 1439 (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), 1440 ] 1441 1442 # Some operating systems (e.g., OSX) do not support setting 1443 # keepidle 1444 if hasattr(socket, 'TCP_KEEPIDLE'): 1445 socket_options += [ 1446 # Wait 60 seconds before sending keep-alive probes 1447 (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) 1448 ] 1449 1450 # Windows subsystem for Linux does not support this feature 1451 if (hasattr(socket, 'TCP_KEEPCNT') and 1452 not utils.is_windows_linux_subsystem): 1453 socket_options += [ 1454 # Set the maximum number of keep-alive probes 1455 (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4), 1456 ] 1457 1458 if hasattr(socket, 'TCP_KEEPINTVL'): 1459 socket_options += [ 1460 # Send keep-alive probes every 15 seconds 1461 (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15), 1462 ] 1463 1464 # After waiting 60 seconds, and then sending a probe once every 15 1465 # seconds 4 times, these options should ensure that a connection 1466 # hands for no longer than 2 minutes before a ConnectionError is 1467 # raised. 1468 kwargs['socket_options'] = socket_options 1469 super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs) 1470