Allow specifying client and service info to user_agent

Allow specifying a service name and version to the session and a client
name and version to the adapter. The way this will work is that
libraries such as keystoneclient will pass client_name and
client_version when creating their adapter. Then when nova or another
service creates a session it will provide the service name and version.
The combination of these will be used to provide a meaningful user
agent.

Change-Id: Ibe516d9b248513579d5e8ca94015c4ae9c00f3f9
Closes-Bug: #1614846
This commit is contained in:
Jamie Lennox 2016-08-19 16:32:03 +10:00
parent 811cd1f3e1
commit eb5571a6ca
4 changed files with 189 additions and 16 deletions

View File

@ -49,6 +49,11 @@ class Adapter(object):
to every request passing through the
adapter. Headers of the same name specified
per request will take priority.
:param str client_name: The name of the client that created the adapter.
This will be used to create the user_agent.
:param str client_version: The version of the client that created the
adapter. This will be used to create the
user_agent.
"""
@positional()
@ -56,7 +61,8 @@ class Adapter(object):
interface=None, region_name=None, endpoint_override=None,
version=None, auth=None, user_agent=None,
connect_retries=None, logger=None, allow={},
additional_headers=None):
additional_headers=None, client_name=None,
client_version=None):
# NOTE(jamielennox): when adding new parameters to adapter please also
# add them to the adapter call in httpclient.HTTPClient.__init__ as
# well as to load_adapter_from_argparse below if the argument is
@ -74,6 +80,8 @@ class Adapter(object):
self.connect_retries = connect_retries
self.logger = logger
self.allow = allow
self.client_name = client_name
self.client_version = client_version
self.additional_headers = additional_headers or {}
def _set_endpoint_filter_kwargs(self, kwargs):
@ -105,6 +113,10 @@ class Adapter(object):
kwargs.setdefault('logger', self.logger)
if self.allow:
kwargs.setdefault('allow', self.allow)
if self.client_name:
kwargs.setdefault('client_name', self.client_name)
if self.client_version:
kwargs.setdefault('client_version', self.client_version)
for k, v in self.additional_headers.items():
kwargs.setdefault('headers', {}).setdefault(k, v)

View File

@ -134,7 +134,7 @@ def _determine_calling_package():
# hit the bottom of the frame stack
break
return None
return ''
def _determine_user_agent():
@ -153,10 +153,10 @@ def _determine_user_agent():
name = sys.argv[0]
except IndexError:
# sys.argv is empty, usually the Python interpreter prevents this.
return None
return ''
if not name:
return None
return ''
name = os.path.basename(name)
if name in ignored:
@ -207,6 +207,15 @@ class Session(object):
to every request passing through the
session. Headers of the same name specified
per request will take priority.
:param str app_name: The name of the application that is creating the
session. This will be used to create the user_agent.
:param str app_version: The version of the application creating the
session. This will be used to create the
user_agent.
:param list additional_user_agent: A list of tuple of name, version that
will be added to the user agent. This
can be used by libraries that are part
of the communication process.
"""
user_agent = None
@ -218,7 +227,8 @@ class Session(object):
@positional(2)
def __init__(self, auth=None, session=None, original_ip=None, verify=True,
cert=None, timeout=None, user_agent=None,
redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None):
redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None,
app_name=None, app_version=None, additional_user_agent=None):
self.auth = auth
self.session = _construct_session(session)
@ -228,19 +238,14 @@ class Session(object):
self.timeout = None
self.redirect = redirect
self.additional_headers = additional_headers or {}
self.app_name = app_name
self.app_version = app_version
self.additional_user_agent = additional_user_agent or []
self._determined_user_agent = None
if timeout is not None:
self.timeout = float(timeout)
# Per RFC 7231 Section 5.5.3, identifiers in a user-agent should be
# ordered by decreasing significance. If a user sets their product
# that value will be used. Otherwise we attempt to derive a useful
# product value. The value will be prepended it to the KSA version,
# requests version, and then the Python version.
if user_agent is None:
user_agent = _determine_user_agent()
if user_agent is not None:
self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT)
@ -371,7 +376,7 @@ class Session(object):
endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, allow_reauth=True, log=True,
endpoint_override=None, connect_retries=0, logger=_logger,
allow={}, **kwargs):
allow={}, client_name=None, client_version=None, **kwargs):
"""Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
@ -499,7 +504,39 @@ class Session(object):
elif self.user_agent:
user_agent = headers.setdefault('User-Agent', self.user_agent)
else:
user_agent = headers.setdefault('User-Agent', DEFAULT_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
# that value will be used. Otherwise we attempt to derive a useful
# product value. The value will be prepended it to the KSA version,
# requests version, and then the Python version.
agent = []
if self.app_name and self.app_version:
agent.append('%s/%s' % (self.app_name, self.app_version))
elif self.app_name:
agent.append(self.app_name)
for additional in self.additional_user_agent:
agent.append('%s/%s' % additional)
if client_name and client_version:
agent.append('%s/%s' % (client_name, client_version))
elif client_name:
agent.append(client_name)
if not agent:
# NOTE(jamielennox): determine_user_agent will return an empty
# string on failure so checking for None will ensure it is only
# called once even on failure.
if self._determined_user_agent is None:
self._determined_user_agent = _determine_user_agent()
if self._determined_user_agent:
agent.append(self._determined_user_agent)
agent.append(DEFAULT_USER_AGENT)
user_agent = headers.setdefault('User-Agent', ' '.join(agent))
if self.original_ip:
headers.setdefault('Forwarded',

View File

@ -1005,6 +1005,113 @@ class AdapterTest(utils.TestCase):
self.assertEqual(request_val,
self.requests_mock.last_request.headers[header])
def test_adapter_user_agent_session_adapter(self):
sess = client_session.Session(app_name='ksatest', app_version='1.2.3')
adap = adapter.Adapter(client_name='testclient',
client_version='4.5.6',
session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
adap.get(url)
agent = 'ksatest/1.2.3 testclient/4.5.6'
self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT,
self.requests_mock.last_request.headers['User-Agent'])
def test_adapter_user_agent_session_adapter_no_app_version(self):
sess = client_session.Session(app_name='ksatest')
adap = adapter.Adapter(client_name='testclient',
client_version='4.5.6',
session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
adap.get(url)
agent = 'ksatest testclient/4.5.6'
self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT,
self.requests_mock.last_request.headers['User-Agent'])
def test_adapter_user_agent_session_adapter_no_client_version(self):
sess = client_session.Session(app_name='ksatest', app_version='1.2.3')
adap = adapter.Adapter(client_name='testclient', session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
adap.get(url)
agent = 'ksatest/1.2.3 testclient'
self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT,
self.requests_mock.last_request.headers['User-Agent'])
def test_adapter_user_agent_session_adapter_additional(self):
sess = client_session.Session(app_name='ksatest',
app_version='1.2.3',
additional_user_agent=[('one', '1.1.1'),
('two', '2.2.2')])
adap = adapter.Adapter(client_name='testclient',
client_version='4.5.6',
session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
adap.get(url)
agent = 'ksatest/1.2.3 one/1.1.1 two/2.2.2 testclient/4.5.6'
self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT,
self.requests_mock.last_request.headers['User-Agent'])
def test_adapter_user_agent_session(self):
sess = client_session.Session(app_name='ksatest', app_version='1.2.3')
adap = adapter.Adapter(session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
adap.get(url)
agent = 'ksatest/1.2.3'
self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT,
self.requests_mock.last_request.headers['User-Agent'])
def test_adapter_user_agent_adapter(self):
sess = client_session.Session()
adap = adapter.Adapter(client_name='testclient',
client_version='4.5.6',
session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
adap.get(url)
agent = 'testclient/4.5.6'
self.assertEqual(agent + ' ' + client_session.DEFAULT_USER_AGENT,
self.requests_mock.last_request.headers['User-Agent'])
def test_adapter_user_agent_session_override(self):
sess = client_session.Session(app_name='ksatest',
app_version='1.2.3',
additional_user_agent=[('one', '1.1.1'),
('two', '2.2.2')])
adap = adapter.Adapter(client_name='testclient',
client_version='4.5.6',
session=sess)
url = 'http://keystone.test.com'
self.requests_mock.get(url)
override_user_agent = '%s/%s' % (uuid.uuid4().hex, uuid.uuid4().hex)
adap.get(url, user_agent=override_user_agent)
self.assertEqual(override_user_agent,
self.requests_mock.last_request.headers['User-Agent'])
class TCPKeepAliveAdapterTest(utils.TestCase):

View File

@ -0,0 +1,17 @@
---
prelude: >
Allow adding client and application name and version to the session and
adapter that will generate a userful user agent string.
features:
- You can specify a ``app_name`` and ``app_version`` when creating a
session. This information will be encoded into the user agent.
- You can specify a ``client_name`` and ``client_version`` when creating an
adapter. This will be handled by client libraries and incluced into the user
agent.
- Libraries like shade that modify the way requests are made can add
themselves to additional_user_agent and have their version reflected in the
user agent string.
deprecations:
- We suggest you fill the name and version for the application and client
instead of specifying a custom ``user_agent``. This will then generate a
standard user agent string.