summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-02-15 21:14:28 +0000
committerGerrit Code Review <review@openstack.org>2018-02-15 21:14:28 +0000
commit43698b65c973dc8ae853f4b48a71eb3a72c44d18 (patch)
tree33f37d84b922d9899d13abd12cfd426d59983df8
parentbf6289b7f55ea3a5a31d367f3f7251e27d2a323d (diff)
parent56b2c89c39e45bc2307113ea12ae584d4f1e11a1 (diff)
Merge "Split request logging into four different loggers"
-rw-r--r--doc/source/using-sessions.rst161
-rw-r--r--keystoneauth1/session.py123
-rw-r--r--keystoneauth1/tests/unit/test_session.py54
-rw-r--r--tox.ini1
4 files changed, 307 insertions, 32 deletions
diff --git a/doc/source/using-sessions.rst b/doc/source/using-sessions.rst
index ace130b..a66cfff 100644
--- a/doc/source/using-sessions.rst
+++ b/doc/source/using-sessions.rst
@@ -39,6 +39,10 @@ Features
39 plugins. Discovery information is automatically cached in memory, so the user 39 plugins. Discovery information is automatically cached in memory, so the user
40 need not worry about excessive use of discovery metadata. 40 need not worry about excessive use of discovery metadata.
41 41
42- Safe logging of HTTP interactions
43
44 Clients need to be able to enable logging of the HTTP interactions, but some
45 things, such as the token or secrets, need to be ommitted.
42 46
43Sessions for Users 47Sessions for Users
44================== 48==================
@@ -433,8 +437,163 @@ expects ``volume``.
433 'min_version': '3', 437 'min_version': '3',
434 'max_version': 'latest'}) 438 'max_version': 'latest'})
435 439
440Logging
441=======
442
443The logging system uses standard `python logging`_ rooted on the
444``keystoneauth`` namespace as would be expected. There are two possibilities
445of where log messages about HTTP interactions will go.
446
447By default, all messages will go to the ``keystoneauth.session`` logger.
448
449If the ``split_loggers`` option on the :class:`keystoneauth1.session.Session`
450constructor is set to ``True``, the HTTP content will be split across four
451subloggers to allow for fine-grained control of what is logged and how:
452
453keystoneauth.session.request-id
454 Emits a log entry at the ``DEBUG`` level for every http request
455 including information about the URL, ``service-type`` and ``request-id``.
456
457keystoneauth.session.request
458 Emits a log entry at the ``DEBUG`` level for every http request including a
459 curl formatted string of the request.
460
461keystoneauth.session.response
462 Emits a log entry at the ``DEBUG`` level for every http response received,
463 including the status code, and the headers received.
464
465keystoneauth.session.body
466 Emits a log entry at the ``DEBUG`` level containing the contents of the
467 response body if the ``content-type`` is either ``text`` or ``json``.
468
469Using loggers
470-------------
471
472A full description of how to consume `python logging`_ is out of scope of this
473document, but a few simple examples are provided.
474
475If you would like to configure logging to log keystoneuath at the ``INFO``
476level with no ``DEBUG`` messages:
477
478.. code-block:: python
479
480 import keystoneauth1
481 import logging
482
483 logger = logging.getLogger('keystoneauth')
484 logger.addHandler(logging.StreamHandler())
485 logger.setLevel(logging.INFO)
486
487If you would like to get a full HTTP debug trace including bodies:
488
489.. code-block:: python
490
491 import keystoneauth1
492 import logging
493
494 logger = logging.getLogger('keystoneauth')
495 logger.addHandler(logging.StreamHandler())
496 logger.setLevel(logging.DEBUG)
497
498If you would like to get a full HTTP debug trace bug with no bodies:
499
500.. code-block:: python
501
502 import keystoneauth1
503 import keystoneauth1.session
504 import logging
505
506 logger = logging.getLogger('keystoneauth')
507 logger.addHandler(logging.StreamHandler())
508 logger.setLevel(logging.DEBUG)
509 body_logger = logging.getLogger('keystoneauth.session.body')
510 body_logger.setLevel(logging.WARN)
511 session = keystoneauth1.session.Session(split_loggers=True)
512
513Finally, if you would like to log request-ids and response headers to one file,
514request commands, response headers and response bodies to a different file,
515and everything else to the console:
516
517.. code-block:: python
518
519 import keystoneauth1
520 import keystoneauth1.session
521 import logging
522
523 # Create a handler that outputs only outputs INFO level messages to stdout
524 stream_handler = logging.StreamHandler()
525 stream_handler.setLevel(logging.INFO)
526
527 # Configure the default behavior of all keystoneauth logging to log at the
528 # INFO level.
529 logger = logging.getLogger('keystoneauth')
530 logger.setLevel(logging.INFO)
531
532 # Emit INFO messages from all keystoneauth loggers to stdout
533 logger.addHandler(stream_handler)
534
535 # Create an output formatter that includes logger name and timestamp.
536 formatter = logging.Formatter('%(asctime)s %(name)s %(message)s')
537
538 # Create a file output for request ids and response headers
539 request_handler = logging.FileHandler('request.log')
540 request_handler.setFormatter(formatter)
541
542 # Create a file output for request commands, response headers and bodies
543 body_handler = logging.FileHandler('response-body.log')
544 body_handler.setFormatter(formatter)
545
546 # Log all HTTP interactions at the DEBUG level
547 session_logger = logging.getLogger('keystoneauth.session')
548 session_logger.setLevel(logging.DEBUG)
549
550 # Emit request ids to the request log
551 request_id_logger = logging.getLogger('keystoneauth.session.request-id')
552 request_id_logger.addHandler(request_handler)
553
554 # Emit response headers to both the request log and the body log
555 header_logger = logging.getLogger('keystoneauth.session.response')
556 header_logger.addHandler(request_handler)
557 header_logger.addHandler(body_handler)
558
559 # Emit request commands to the body log
560 request_logger = logging.getLogger('keystoneauth.session.request')
561 request_logger.addHandler(body_handler)
562
563 # Emit bodies only to the body log
564 body_logger = logging.getLogger('keystoneauth.session.body')
565 body_logger.addHandler(body_handler)
566
567 session = keystoneauth1.session.Session(split_loggers=True)
568
569The above will produce messages like the following in request.log:
570
571::
572
573 2017-09-19 22:10:09,466 keystoneauth.session.request-id GET call to volumev2 for http://cloud.example.com/volume/v2/137155c35fb34172a284a3c2540c92ab/volumes/detail used request id req-f4f2058a-9308-4c4a-94e6-5ee1cd6c78bd
574 2017-09-19 22:10:09,751 keystoneauth.session.response [200] Date: Tue, 19 Sep 2017 22:10:09 GMT Server: Apache/2.4.18 (Ubuntu) x-compute-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Content-Type: application/json Content-Length: 15 x-openstack-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Connection: close
575
576And content like the following into response-body.log:
577
578::
579
580 2017-09-19 22:10:09,490 keystoneauth.session.request curl -g -i -X GET http://cloud.example.com/volume/v2/137155c35fb34172a284a3c2540c92ab/volumes/detail?marker=34cd00cf-bf67-4667-a900-5ce233e383d5 -H "User-Agent: os-client-config/1.28.0 shade/1.23.1 keystoneauth1/3.2.0 python-requests/2.18.4 CPython/2.7.12" -H "X-Auth-Token: {SHA1}a1d03d2a4cbee590a55f1786d452e1027d5fd781"
581 2017-09-19 22:10:09,751 keystoneauth.session.response [200] Date: Tue, 19 Sep 2017 22:10:09 GMT Server: Apache/2.4.18 (Ubuntu) x-compute-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Content-Type: application/json Content-Length: 15 x-openstack-request-id: req-2e9181d2-9f3e-404e-a12f-1f1566736ab3 Connection: close
582 2017-09-19 22:10:09,751 keystoneauth.session.body {"volumes": []}
583
584User Provided Loggers
585---------------------
586
587The HTTP methods (request, get, post, put, etc) on
588`keystoneauth1.session.Session` and `keystoneauth1.adapter.Adapter` all support
589a ``logger`` parameter. A user can provide their own `logger`_ which will
590override the session loggers mentioned above. If a single logger is provided
591in this manner, request, response and body content will all be logged to that
592logger at the ``DEBUG`` level, and the strings ``REQ:``, ``RESP:`` and
593``RESP BODY:`` will be pre-pended as appropriate.
436 594
437.. _API-WG Specs: http://specs.openstack.org/openstack/api-wg/ 595.. _API-WG Specs: http://specs.openstack.org/openstack/api-wg/
438.. _Consuming the Catalog: http://specs.openstack.org/openstack/api-wg/guidelines/consuming-catalog.html 596.. _Consuming the Catalog: http://specs.openstack.org/openstack/api-wg/guidelines/consuming-catalog.html
439.. _Microversions: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery 597.. _Microversions: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html#version-discovery
440 598.. _python logging: https://docs.python.org/3/library/logging.html
599.. _logger: https://docs.python.org/3/library/logging.html#logging.Logger
diff --git a/keystoneauth1/session.py b/keystoneauth1/session.py
index 317324e..eb6d873 100644
--- a/keystoneauth1/session.py
+++ b/keystoneauth1/session.py
@@ -50,8 +50,6 @@ DEFAULT_USER_AGENT = 'keystoneauth1/%s %s %s/%s' % (
50# here and we'll add it to the list as required. 50# here and we'll add it to the list as required.
51_LOG_CONTENT_TYPES = set(['application/json']) 51_LOG_CONTENT_TYPES = set(['application/json'])
52 52
53_logger = utils.get_logger(__name__)
54
55 53
56def _construct_session(session_obj=None): 54def _construct_session(session_obj=None):
57 # NOTE(morganfainberg): if the logic in this function changes be sure to 55 # NOTE(morganfainberg): if the logic in this function changes be sure to
@@ -244,6 +242,8 @@ class Session(object):
244 that do not share an auth plugin, it can 242 that do not share an auth plugin, it can
245 be provided here. (optional, defaults to 243 be provided here. (optional, defaults to
246 None which means automatically manage) 244 None which means automatically manage)
245 :param bool split_loggers: Split the logging of requests across multiple
246 loggers instead of just one. Defaults to False.
247 """ 247 """
248 248
249 user_agent = None 249 user_agent = None
@@ -256,7 +256,7 @@ class Session(object):
256 cert=None, timeout=None, user_agent=None, 256 cert=None, timeout=None, user_agent=None,
257 redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None, 257 redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None,
258 app_name=None, app_version=None, additional_user_agent=None, 258 app_name=None, app_version=None, additional_user_agent=None,
259 discovery_cache=None): 259 discovery_cache=None, split_loggers=None):
260 260
261 self.auth = auth 261 self.auth = auth
262 self.session = _construct_session(session) 262 self.session = _construct_session(session)
@@ -273,6 +273,7 @@ class Session(object):
273 if discovery_cache is None: 273 if discovery_cache is None:
274 discovery_cache = {} 274 discovery_cache = {}
275 self._discovery_cache = discovery_cache 275 self._discovery_cache = discovery_cache
276 self._split_loggers = split_loggers
276 277
277 if timeout is not None: 278 if timeout is not None:
278 self.timeout = float(timeout) 279 self.timeout = float(timeout)
@@ -324,16 +325,33 @@ class Session(object):
324 return (header[0], '{SHA1}%s' % token_hash) 325 return (header[0], '{SHA1}%s' % token_hash)
325 return header 326 return header
326 327
328 def _get_split_loggers(self, split_loggers):
329 if split_loggers is None:
330 split_loggers = self._split_loggers
331 if split_loggers is None:
332 split_loggers = False
333 return split_loggers
334
327 def _http_log_request(self, url, method=None, data=None, 335 def _http_log_request(self, url, method=None, data=None,
328 json=None, headers=None, query_params=None, 336 json=None, headers=None, query_params=None,
329 logger=_logger): 337 logger=None, split_loggers=None):
338 string_parts = []
339
340 if self._get_split_loggers(split_loggers):
341 logger = utils.get_logger(__name__ + '.request')
342 else:
343 # Only a single logger was passed in, use string prefixing.
344 string_parts.append('REQ:')
345 if not logger:
346 logger = utils.get_logger(__name__)
347
330 if not logger.isEnabledFor(logging.DEBUG): 348 if not logger.isEnabledFor(logging.DEBUG):
331 # NOTE(morganfainberg): This whole debug section is expensive, 349 # NOTE(morganfainberg): This whole debug section is expensive,
332 # there is no need to do the work if we're not going to emit a 350 # there is no need to do the work if we're not going to emit a
333 # debug log. 351 # debug log.
334 return 352 return
335 353
336 string_parts = ['REQ: curl -g -i'] 354 string_parts.append('curl -g -i')
337 355
338 # NOTE(jamielennox): None means let requests do its default validation 356 # NOTE(jamielennox): None means let requests do its default validation
339 # so we need to actually check that this is False. 357 # so we need to actually check that this is False.
@@ -356,7 +374,8 @@ class Session(object):
356 string_parts.append(url) 374 string_parts.append(url)
357 375
358 if headers: 376 if headers:
359 for header in headers.items(): 377 # Sort headers so that testing can work consistently.
378 for header in sorted(headers.items()):
360 string_parts.append('-H "%s: %s"' 379 string_parts.append('-H "%s: %s"'
361 % self._process_header(header)) 380 % self._process_header(header))
362 if json: 381 if json:
@@ -373,7 +392,18 @@ class Session(object):
373 392
374 def _http_log_response(self, response=None, json=None, 393 def _http_log_response(self, response=None, json=None,
375 status_code=None, headers=None, text=None, 394 status_code=None, headers=None, text=None,
376 logger=_logger): 395 logger=None, split_loggers=True):
396 string_parts = []
397 body_parts = []
398 if self._get_split_loggers(split_loggers):
399 logger = utils.get_logger(__name__ + '.response')
400 body_logger = utils.get_logger(__name__ + '.body')
401 else:
402 # Only a single logger was passed in, use string prefixing.
403 string_parts.append('RESP:')
404 body_parts.append('RESP BODY:')
405 body_logger = logger
406
377 if not logger.isEnabledFor(logging.DEBUG): 407 if not logger.isEnabledFor(logging.DEBUG):
378 return 408 return
379 409
@@ -382,6 +412,19 @@ class Session(object):
382 status_code = response.status_code 412 status_code = response.status_code
383 if not headers: 413 if not headers:
384 headers = response.headers 414 headers = response.headers
415
416 if status_code:
417 string_parts.append('[%s]' % status_code)
418 if headers:
419 # Sort headers so that testing can work consistently.
420 for header in sorted(headers.items()):
421 string_parts.append('%s: %s' % self._process_header(header))
422 logger.debug(' '.join(string_parts))
423
424 if not body_logger.isEnabledFor(logging.DEBUG):
425 return
426
427 if response is not None:
385 if not text: 428 if not text:
386 # NOTE(samueldmq): If the response does not provide enough info 429 # NOTE(samueldmq): If the response does not provide enough info
387 # about the content type to decide whether it is useful and 430 # about the content type to decide whether it is useful and
@@ -406,17 +449,9 @@ class Session(object):
406 if json: 449 if json:
407 text = self._json.encode(json) 450 text = self._json.encode(json)
408 451
409 string_parts = ['RESP:']
410
411 if status_code:
412 string_parts.append('[%s]' % status_code)
413 if headers:
414 for header in headers.items():
415 string_parts.append('%s: %s' % self._process_header(header))
416 if text: 452 if text:
417 string_parts.append('\nRESP BODY: %s\n' % text) 453 body_parts.append(text)
418 454 body_logger.debug(' '.join(body_parts))
419 logger.debug(' '.join(string_parts))
420 455
421 @staticmethod 456 @staticmethod
422 def _set_microversion_headers( 457 def _set_microversion_headers(
@@ -465,7 +500,7 @@ class Session(object):
465 user_agent=None, redirect=None, authenticated=None, 500 user_agent=None, redirect=None, authenticated=None,
466 endpoint_filter=None, auth=None, requests_auth=None, 501 endpoint_filter=None, auth=None, requests_auth=None,
467 raise_exc=True, allow_reauth=True, log=True, 502 raise_exc=True, allow_reauth=True, log=True,
468 endpoint_override=None, connect_retries=0, logger=_logger, 503 endpoint_override=None, connect_retries=0, logger=None,
469 allow=None, client_name=None, client_version=None, 504 allow=None, client_name=None, client_version=None,
470 microversion=None, microversion_service_type=None, 505 microversion=None, microversion_service_type=None,
471 **kwargs): 506 **kwargs):
@@ -560,6 +595,14 @@ class Session(object):
560 595
561 :returns: The response to the request. 596 :returns: The response to the request.
562 """ 597 """
598 # If a logger is passed in, use it and do not log requests, responses
599 # and bodies separately.
600 if logger:
601 split_loggers = False
602 else:
603 split_loggers = None
604 logger = logger or utils.get_logger(__name__)
605
563 headers = kwargs.setdefault('headers', dict()) 606 headers = kwargs.setdefault('headers', dict())
564 if microversion: 607 if microversion:
565 self._set_microversion_headers( 608 self._set_microversion_headers(
@@ -671,7 +714,7 @@ class Session(object):
671 data=kwargs.get('data'), 714 data=kwargs.get('data'),
672 headers=headers, 715 headers=headers,
673 query_params=query_params, 716 query_params=query_params,
674 logger=logger) 717 logger=logger, split_loggers=split_loggers)
675 718
676 # Force disable requests redirect handling. We will manage this below. 719 # Force disable requests redirect handling. We will manage this below.
677 kwargs['allow_redirects'] = False 720 kwargs['allow_redirects'] = False
@@ -681,7 +724,7 @@ class Session(object):
681 724
682 send = functools.partial(self._send_request, 725 send = functools.partial(self._send_request,
683 url, method, redirect, log, logger, 726 url, method, redirect, log, logger,
684 connect_retries) 727 split_loggers, connect_retries)
685 728
686 try: 729 try:
687 connection_params = self.get_auth_connection_params(auth=auth) 730 connection_params = self.get_auth_connection_params(auth=auth)
@@ -713,13 +756,29 @@ class Session(object):
713 request_id = (resp.headers.get('x-openstack-request-id') or 756 request_id = (resp.headers.get('x-openstack-request-id') or
714 resp.headers.get('x-compute-request-id')) 757 resp.headers.get('x-compute-request-id'))
715 if request_id: 758 if request_id:
716 logger.debug('%(method)s call to %(service_name)s for ' 759 if self._get_split_loggers(split_loggers):
717 '%(url)s used request id ' 760 id_logger = utils.get_logger(__name__ + '.request-id')
718 '%(response_request_id)s', 761 else:
719 {'method': resp.request.method, 762 id_logger = logger
720 'service_name': service_name, 763 if service_name:
721 'url': resp.url, 764 id_logger.debug(
722 'response_request_id': request_id}) 765 '%(method)s call to %(service_name)s for '
766 '%(url)s used request id '
767 '%(response_request_id)s', {
768 'method': resp.request.method,
769 'service_name': service_name,
770 'url': resp.url,
771 'response_request_id': request_id
772 })
773 else:
774 id_logger.debug(
775 '%(method)s call to '
776 '%(url)s used request id '
777 '%(response_request_id)s', {
778 'method': resp.request.method,
779 'url': resp.url,
780 'response_request_id': request_id
781 })
723 782
724 # handle getting a 401 Unauthorized response by invalidating the plugin 783 # handle getting a 401 Unauthorized response by invalidating the plugin
725 # and then retrying the request. This is only tried once. 784 # and then retrying the request. This is only tried once.
@@ -738,7 +797,7 @@ class Session(object):
738 797
739 return resp 798 return resp
740 799
741 def _send_request(self, url, method, redirect, log, logger, 800 def _send_request(self, url, method, redirect, log, logger, split_loggers,
742 connect_retries, connect_retry_delay=0.5, **kwargs): 801 connect_retries, connect_retry_delay=0.5, **kwargs):
743 # NOTE(jamielennox): We handle redirection manually because the 802 # NOTE(jamielennox): We handle redirection manually because the
744 # requests lib follows some browser patterns where it will redirect 803 # requests lib follows some browser patterns where it will redirect
@@ -784,13 +843,15 @@ class Session(object):
784 time.sleep(connect_retry_delay) 843 time.sleep(connect_retry_delay)
785 844
786 return self._send_request( 845 return self._send_request(
787 url, method, redirect, log, logger, 846 url, method, redirect, log, logger, split_loggers,
788 connect_retries=connect_retries - 1, 847 connect_retries=connect_retries - 1,
789 connect_retry_delay=connect_retry_delay * 2, 848 connect_retry_delay=connect_retry_delay * 2,
790 **kwargs) 849 **kwargs)
791 850
792 if log: 851 if log:
793 self._http_log_response(response=resp, logger=logger) 852 self._http_log_response(
853 response=resp, logger=logger,
854 split_loggers=split_loggers)
794 855
795 if resp.status_code in self._REDIRECT_STATUSES: 856 if resp.status_code in self._REDIRECT_STATUSES:
796 # be careful here in python True == 1 and False == 0 857 # be careful here in python True == 1 and False == 0
@@ -812,7 +873,7 @@ class Session(object):
812 # NOTE(jamielennox): We don't pass through connect_retry_delay. 873 # NOTE(jamielennox): We don't pass through connect_retry_delay.
813 # This request actually worked so we can reset the delay count. 874 # This request actually worked so we can reset the delay count.
814 new_resp = self._send_request( 875 new_resp = self._send_request(
815 location, method, redirect, log, logger, 876 location, method, redirect, log, logger, split_loggers,
816 connect_retries=connect_retries, 877 connect_retries=connect_retries,
817 **kwargs) 878 **kwargs)
818 879
diff --git a/keystoneauth1/tests/unit/test_session.py b/keystoneauth1/tests/unit/test_session.py
index c411f83..94cc7d1 100644
--- a/keystoneauth1/tests/unit/test_session.py
+++ b/keystoneauth1/tests/unit/test_session.py
@@ -956,6 +956,60 @@ class SessionAuthTests(utils.TestCase):
956 self.assertNotIn(list(response.keys())[0], self.logger.output) 956 self.assertNotIn(list(response.keys())[0], self.logger.output)
957 self.assertNotIn(list(response.values())[0], self.logger.output) 957 self.assertNotIn(list(response.values())[0], self.logger.output)
958 958
959 def test_split_loggers(self):
960
961 def get_logger_io(name):
962 logger_name = 'keystoneauth.session.{name}'.format(name=name)
963 logger = logging.getLogger(logger_name)
964 logger.setLevel(logging.DEBUG)
965
966 io = six.StringIO()
967 handler = logging.StreamHandler(io)
968 logger.addHandler(handler)
969 return io
970
971 io = {}
972 for name in ('request', 'body', 'response', 'request-id'):
973 io[name] = get_logger_io(name)
974
975 auth = AuthPlugin()
976 sess = client_session.Session(auth=auth, split_loggers=True)
977 response_key = uuid.uuid4().hex
978 response_val = uuid.uuid4().hex
979 response = {response_key: response_val}
980 request_id = uuid.uuid4().hex
981
982 self.stub_url(
983 'GET',
984 json=response,
985 headers={
986 'Content-Type': 'application/json',
987 'X-OpenStack-Request-ID': request_id,
988 })
989
990 resp = sess.get(self.TEST_URL)
991
992 self.assertEqual(response, resp.json())
993
994 request_output = io['request'].getvalue().strip()
995 response_output = io['response'].getvalue().strip()
996 body_output = io['body'].getvalue().strip()
997 id_output = io['request-id'].getvalue().strip()
998
999 self.assertIn('curl -g -i -X GET {url}'.format(url=self.TEST_URL),
1000 request_output)
1001 self.assertEqual('[200] Content-Type: application/json '
1002 'X-OpenStack-Request-ID: '
1003 '{id}'.format(id=request_id), response_output)
1004 self.assertEqual(
1005 'GET call to {url} used request id {id}'.format(
1006 url=self.TEST_URL, id=request_id),
1007 id_output)
1008 self.assertEqual(
1009 '{{"{key}": "{val}"}}'.format(
1010 key=response_key, val=response_val),
1011 body_output)
1012
959 1013
960class AdapterTest(utils.TestCase): 1014class AdapterTest(utils.TestCase):
961 1015
diff --git a/tox.ini b/tox.ini
index 6d9425a..3d19a9a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,6 +7,7 @@ envlist = py35,py27,pep8,releasenotes
7usedevelop = True 7usedevelop = True
8install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} 8install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
9setenv = VIRTUAL_ENV={envdir} 9setenv = VIRTUAL_ENV={envdir}
10 PYTHONHASHSEED=0
10 BRANCH_NAME=master 11 BRANCH_NAME=master
11 CLIENT_NAME=keystoneauth1 12 CLIENT_NAME=keystoneauth1
12 OS_STDOUT_NOCAPTURE=False 13 OS_STDOUT_NOCAPTURE=False