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

View File

@ -134,7 +134,7 @@ def _determine_calling_package():
# hit the bottom of the frame stack # hit the bottom of the frame stack
break break
return None return ''
def _determine_user_agent(): def _determine_user_agent():
@ -153,10 +153,10 @@ def _determine_user_agent():
name = sys.argv[0] name = sys.argv[0]
except IndexError: except IndexError:
# sys.argv is empty, usually the Python interpreter prevents this. # sys.argv is empty, usually the Python interpreter prevents this.
return None return ''
if not name: if not name:
return None return ''
name = os.path.basename(name) name = os.path.basename(name)
if name in ignored: if name in ignored:
@ -207,6 +207,15 @@ class Session(object):
to every request passing through the to every request passing through the
session. Headers of the same name specified session. Headers of the same name specified
per request will take priority. 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 user_agent = None
@ -218,7 +227,8 @@ class Session(object):
@positional(2) @positional(2)
def __init__(self, auth=None, session=None, original_ip=None, verify=True, def __init__(self, auth=None, session=None, original_ip=None, verify=True,
cert=None, timeout=None, user_agent=None, 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.auth = auth
self.session = _construct_session(session) self.session = _construct_session(session)
@ -228,19 +238,14 @@ class Session(object):
self.timeout = None self.timeout = None
self.redirect = redirect self.redirect = redirect
self.additional_headers = additional_headers or {} 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: if timeout is not None:
self.timeout = float(timeout) 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: if user_agent is not None:
self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) 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, endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, allow_reauth=True, log=True, raise_exc=True, allow_reauth=True, log=True,
endpoint_override=None, connect_retries=0, logger=_logger, 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. """Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as Wrapper around `requests.Session.request` to handle tasks such as
@ -499,7 +504,39 @@ 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', 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: if self.original_ip:
headers.setdefault('Forwarded', headers.setdefault('Forwarded',

View File

@ -1005,6 +1005,113 @@ class AdapterTest(utils.TestCase):
self.assertEqual(request_val, self.assertEqual(request_val,
self.requests_mock.last_request.headers[header]) 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): 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.