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
This commit is contained in:
briancurtin 2015-12-10 10:42:37 -06:00
parent 30f9a02fa2
commit bbd85fded7
4 changed files with 39 additions and 7 deletions

View File

@ -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()

View File

@ -15,6 +15,7 @@ import functools
import hashlib import hashlib
import json import json
import logging import logging
import platform
import socket import socket
import time import time
import uuid import uuid
@ -23,6 +24,7 @@ import requests
import six import six
from six.moves import urllib from six.moves import urllib
import keystoneauth1
from keystoneauth1 import _utils as utils from keystoneauth1 import _utils as utils
from keystoneauth1 import exceptions from keystoneauth1 import exceptions
@ -36,7 +38,9 @@ try:
except ImportError: except ImportError:
osprofiler_web = None 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__) _logger = utils.get_logger(__name__)
@ -95,8 +99,12 @@ class Session(object):
of seconds or 0 for no timeout. (optional, defaults of seconds or 0 for no timeout. (optional, defaults
to 0) to 0)
:param string user_agent: A User-Agent header string to use for the :param string user_agent: A User-Agent header string to use for the
request. If not provided a default is used. request. If not provided, a default of
(optional, defaults to 'keystoneauth1') :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 :param int/bool redirect: Controls the maximum number of redirections that
can be followed by a request. Either an integer can be followed by a request. Either an integer
for a specific count or True/False for for a specific count or True/False for
@ -127,7 +135,11 @@ class Session(object):
# don't override the class variable if none provided # don't override the class variable if none provided
if user_agent is not None: 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() self._json = _JSONEncoder()
@ -367,7 +379,7 @@ class Session(object):
elif self.user_agent: elif self.user_agent:
user_agent = headers.setdefault('User-Agent', self.user_agent) user_agent = headers.setdefault('User-Agent', self.user_agent)
else: else:
user_agent = headers.setdefault('User-Agent', USER_AGENT) user_agent = headers.setdefault('User-Agent', DEFAULT_USER_AGENT)
if self.original_ip: if self.original_ip:
headers.setdefault('Forwarded', headers.setdefault('Forwarded',

View File

@ -89,12 +89,15 @@ class SessionTests(utils.TestCase):
self.assertRequestBodyIs(json={'hello': 'world'}) self.assertRequestBodyIs(json={'hello': 'world'})
def test_user_agent(self): 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') self.stub_url('GET', text='response')
resp = session.get(self.TEST_URL) resp = session.get(self.TEST_URL)
self.assertTrue(resp.ok) 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'}) resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'})
self.assertTrue(resp.ok) self.assertTrue(resp.ok)

View File

@ -2,6 +2,7 @@
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
pbr>=1.6
argparse argparse
iso8601>=0.1.9 iso8601>=0.1.9
requests>=2.8.1 requests>=2.8.1