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