diff --git a/keystoneauth1/adapter.py b/keystoneauth1/adapter.py index 4ae29448..21497920 100644 --- a/keystoneauth1/adapter.py +++ b/keystoneauth1/adapter.py @@ -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) diff --git a/keystoneauth1/session.py b/keystoneauth1/session.py index d1fe3697..54c0c65e 100644 --- a/keystoneauth1/session.py +++ b/keystoneauth1/session.py @@ -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', diff --git a/keystoneauth1/tests/unit/test_session.py b/keystoneauth1/tests/unit/test_session.py index 3a43abb9..1229f8c6 100644 --- a/keystoneauth1/tests/unit/test_session.py +++ b/keystoneauth1/tests/unit/test_session.py @@ -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): diff --git a/releasenotes/notes/user-agent-generation-b069100508c06177.yaml b/releasenotes/notes/user-agent-generation-b069100508c06177.yaml new file mode 100644 index 00000000..7e1a4435 --- /dev/null +++ b/releasenotes/notes/user-agent-generation-b069100508c06177.yaml @@ -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.