diff --git a/openstack/profile.py b/openstack/profile.py index 9f842b3c..8e4dd260 100644 --- a/openstack/profile.py +++ b/openstack/profile.py @@ -186,6 +186,14 @@ class Profile(object): """ self._get_filter(service).version = version + def set_api_version(self, service, api_version): + """Set the desired API micro-version for the specified service. + + :param str service: Service type. + :param str api_version: Desired service API micro-version. + """ + self._setter(service, "api_version", api_version) + def set_interface(self, service, interface): """Set the desired interface for the specified service. diff --git a/openstack/service_filter.py b/openstack/service_filter.py index 47c8620b..b3d28d4e 100644 --- a/openstack/service_filter.py +++ b/openstack/service_filter.py @@ -72,7 +72,7 @@ class ServiceFilter(dict): valid_versions = [] def __init__(self, service_type, interface=PUBLIC, region=None, - service_name=None, version=None): + service_name=None, version=None, api_version=None): """Create a service identifier. :param string service_type: The desired type of service. @@ -81,12 +81,14 @@ class ServiceFilter(dict): :param string region: The desired region (optional). :param string service_name: Name of the service :param string version: Version of service to use. + :param string api_version: Microversion of service supported. """ self['service_type'] = service_type.lower() self['interface'] = interface self['region_name'] = region self['service_name'] = service_name self['version'] = version + self['api_version'] = api_version @property def service_type(self): @@ -124,6 +126,14 @@ class ServiceFilter(dict): def version(self, value): self['version'] = value + @property + def api_version(self): + return self['api_version'] + + @api_version.setter + def api_version(self, value): + self['api_version'] = value + @property def path(self): return self['path'] diff --git a/openstack/session.py b/openstack/session.py index c3cb3c7b..b91d34a8 100644 --- a/openstack/session.py +++ b/openstack/session.py @@ -28,6 +28,7 @@ from six.moves.urllib import parse DEFAULT_USER_AGENT = "openstacksdk/%s" % openstack_version.__version__ VERSION_PATTERN = re.compile('/v\d[\d.]*') +API_REQUEST_HEADER = "openstack-api-version" def parse_url(filt, url): @@ -89,9 +90,33 @@ class Session(_session.Session): self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT) else: self.user_agent = DEFAULT_USER_AGENT - super(Session, self).__init__(user_agent=self.user_agent, **kwargs) self.profile = profile + api_version_header = self._get_api_requests() + super(Session, self).__init__(user_agent=self.user_agent, + additional_headers=api_version_header, + **kwargs) + + def _get_api_requests(self): + """Get API micro-version requests. + + :param profile: A profile object that contains customizations about + service name, region, version, interface or + api_version. + :return: A standard header string if there is any specialization in + API microversion, or None if no such request exists. + """ + if self.profile is None: + return None + + req = [] + for svc in self.profile.get_services(): + if svc.service_type and svc.api_version: + req.append(" ".join([svc.service_type, svc.api_version])) + if req: + return {API_REQUEST_HEADER: ",".join(req)} + + return None def get_endpoint(self, auth=None, interface=None, **kwargs): """Override get endpoint to automate endpoint filtering""" diff --git a/openstack/tests/unit/test_profile.py b/openstack/tests/unit/test_profile.py index 8edb1d70..ac6b77b4 100644 --- a/openstack/tests/unit/test_profile.py +++ b/openstack/tests/unit/test_profile.py @@ -60,6 +60,16 @@ class TestProfile(base.TestCase): self.assertRaises(exceptions.SDKException, prof.set_version, 'bogus', 'v2') + def test_set_api_version(self): + # This tests that api_version is effective after explicit setting, or + # else it defaults to None. + prof = profile.Profile() + prof.set_api_version('clustering', '1.2') + svc = prof.get_filter('clustering') + self.assertEqual('1.2', svc.api_version) + svc = prof.get_filter('compute') + self.assertIsNone(svc.api_version) + def test_set_all(self): prof = profile.Profile() prof.set_name(prof.ALL, 'fee') diff --git a/openstack/tests/unit/test_service_filter.py b/openstack/tests/unit/test_service_filter.py index dde2f9ac..6f30aa2a 100644 --- a/openstack/tests/unit/test_service_filter.py +++ b/openstack/tests/unit/test_service_filter.py @@ -24,6 +24,16 @@ class TestValidVersion(testtools.TestCase): class TestServiceFilter(testtools.TestCase): + def test_init(self): + sot = service_filter.ServiceFilter( + 'ServiceType', region='REGION1', service_name='ServiceName', + version='1', api_version='1.23') + self.assertEqual('servicetype', sot.service_type) + self.assertEqual('REGION1', sot.region) + self.assertEqual('ServiceName', sot.service_name) + self.assertEqual('1', sot.version) + self.assertEqual('1.23', sot.api_version) + def test_get_module(self): sot = identity_service.IdentityService() self.assertEqual('openstack.identity.v3', sot.get_module()) diff --git a/openstack/tests/unit/test_session.py b/openstack/tests/unit/test_session.py index 31c57306..c8da6363 100644 --- a/openstack/tests/unit/test_session.py +++ b/openstack/tests/unit/test_session.py @@ -17,6 +17,7 @@ from keystoneauth1 import exceptions as _exceptions from openstack import exceptions from openstack.image import image_service +from openstack import profile from openstack import session @@ -41,14 +42,43 @@ class TestSession(testtools.TestCase): "http://127.0.0.1:9292/wot/v1/mytenant", session.parse_url(filt, "http://127.0.0.1:9292/wot/v2.0/mytenant")) - def test_user_agent_none(self): + def test_init_user_agent_none(self): sot = session.Session(None) self.assertTrue(sot.user_agent.startswith("openstacksdk")) - def test_user_agent_set(self): + def test_init_user_agent_set(self): sot = session.Session(None, user_agent="testing/123") self.assertTrue(sot.user_agent.startswith("testing/123 openstacksdk")) + def test_init_with_single_api_request(self): + prof = profile.Profile() + prof.set_api_version('clustering', '1.2') + + sot = session.Session(prof) + + # The assertion acutally tests the property assigned in parent class + self.assertEqual({'openstack-api-version': 'clustering 1.2'}, + sot.additional_headers) + + def test_init_with_multi_api_requests(self): + prof = profile.Profile() + prof.set_api_version('clustering', '1.2') + prof.set_api_version('compute', '2.15') + + sot = session.Session(prof) + + versions = sot.additional_headers['openstack-api-version'] + requests = [req.strip() for req in versions.split(',')] + self.assertIn('clustering 1.2', requests) + self.assertIn('compute 2.15', requests) + + def test_init_with_no_api_requests(self): + prof = profile.Profile() + + sot = session.Session(prof) + + self.assertEqual({}, sot.additional_headers) + def test_map_exceptions_not_found_exception(self): ksa_exc = _exceptions.HttpError(message="test", http_status=404) func = mock.Mock(side_effect=ksa_exc)