From bbd85fded7038435e7a26f2f12b29815fa61a8b2 Mon Sep 17 00:00:00 2001 From: briancurtin Date: Thu, 10 Dec 2015 10:42:37 -0600 Subject: [PATCH] Provide a RFC 7231 compliant user agent string The current default of "keystoneauth1" doesn't convey enough information, and additionally when the user of a Session supplies their own user agent, it stomps on any notion of keystoneauth1 being there. Per RFC 7231 Section 5.5.3 (https://tools.ietf.org/html/rfc7231#section-5.5.3), user agents should basically be a space-delimited list of product/version pairs in decreasing order of importance. This change makes the default user agent something like the following: keystoneauth1/2.1.1 python-requests/2.8.1 CPython/3.4.1+ Due to the decreasing order of importance, when a user creates a Session with something like Session(user_agent="my-product/1.0"), 'my-product/1.0' is then prepended to the above list. The only time this is not the case is if a user agent is provided directly to Session.request. In that case, the User-Agent header is set to whatever the provided argument is, verbatim. This was a change we had originally made to the Transport class in python-openstacksdk (I80ca26fff3f2522b8232472676396abb86166f91), but upon moving to keystoneauth instead of our own implementation, it was noticed that we lost this, and keystoneauth is a better place for this than for us to re-implement it inside of python-openstacksdk. Change-Id: I46f336f25fac5b524547bb13e4f5438ebf1d4320 --- keystoneauth1/__init__.py | 16 ++++++++++++++++ keystoneauth1/session.py | 22 +++++++++++++++++----- keystoneauth1/tests/unit/test_session.py | 7 +++++-- requirements.txt | 1 + 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/keystoneauth1/__init__.py b/keystoneauth1/__init__.py index e69de29b..312aa58a 100644 --- a/keystoneauth1/__init__.py +++ b/keystoneauth1/__init__.py @@ -0,0 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pbr.version + + +__version__ = pbr.version.VersionInfo('keystoneauth1').version_string() diff --git a/keystoneauth1/session.py b/keystoneauth1/session.py index eaa23002..3de4fc8c 100644 --- a/keystoneauth1/session.py +++ b/keystoneauth1/session.py @@ -15,6 +15,7 @@ import functools import hashlib import json import logging +import platform import socket import time import uuid @@ -23,6 +24,7 @@ import requests import six from six.moves import urllib +import keystoneauth1 from keystoneauth1 import _utils as utils from keystoneauth1 import exceptions @@ -36,7 +38,9 @@ try: except ImportError: osprofiler_web = None -USER_AGENT = 'keystoneauth1' +DEFAULT_USER_AGENT = 'keystoneauth1/%s %s %s/%s' % ( + keystoneauth1.__version__, requests.utils.default_user_agent(), + platform.python_implementation(), platform.python_version()) _logger = utils.get_logger(__name__) @@ -95,8 +99,12 @@ class Session(object): of seconds or 0 for no timeout. (optional, defaults to 0) :param string user_agent: A User-Agent header string to use for the - request. If not provided a default is used. - (optional, defaults to 'keystoneauth1') + request. If not provided, a default of + :attr:`~keystoneauth1.session.DEFAULT_USER_AGENT` + is used, which contains the keystoneauth1 version + as well as those of the requests library and + which Python is being used. When a non-None value + is passed, it will be prepended to the default. :param int/bool redirect: Controls the maximum number of redirections that can be followed by a request. Either an integer for a specific count or True/False for @@ -127,7 +135,11 @@ class Session(object): # don't override the class variable if none provided if user_agent is not None: - self.user_agent = user_agent + # Per RFC 7231 Section 5.5.3, identifiers in a user-agent + # should be ordered by decreasing significance. + # If a user sets their product, we prepend it to the KSA + # version, requests version, and then the Python version. + self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) self._json = _JSONEncoder() @@ -367,7 +379,7 @@ class Session(object): elif self.user_agent: user_agent = headers.setdefault('User-Agent', self.user_agent) else: - user_agent = headers.setdefault('User-Agent', USER_AGENT) + user_agent = headers.setdefault('User-Agent', DEFAULT_USER_AGENT) if self.original_ip: headers.setdefault('Forwarded', diff --git a/keystoneauth1/tests/unit/test_session.py b/keystoneauth1/tests/unit/test_session.py index c5a090c3..d09cf4f5 100644 --- a/keystoneauth1/tests/unit/test_session.py +++ b/keystoneauth1/tests/unit/test_session.py @@ -89,12 +89,15 @@ class SessionTests(utils.TestCase): self.assertRequestBodyIs(json={'hello': 'world'}) def test_user_agent(self): - session = client_session.Session(user_agent='test-agent') + custom_agent = 'custom-agent/1.0' + session = client_session.Session(user_agent=custom_agent) self.stub_url('GET', text='response') resp = session.get(self.TEST_URL) self.assertTrue(resp.ok) - self.assertRequestHeaderEqual('User-Agent', 'test-agent') + self.assertRequestHeaderEqual( + 'User-Agent', + '%s %s' % (custom_agent, client_session.DEFAULT_USER_AGENT)) resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'}) self.assertTrue(resp.ok) diff --git a/requirements.txt b/requirements.txt index ccbbd9ca..ae4e8730 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. +pbr>=1.6 argparse iso8601>=0.1.9 requests>=2.8.1