diff --git a/doc/source/transition.rst b/doc/source/transition.rst index 383c94c..8c4a0c2 100644 --- a/doc/source/transition.rst +++ b/doc/source/transition.rst @@ -96,3 +96,14 @@ Shell * Break up ``OpenStackShell.initialize_app()`` * leave all plugin initialization in OSC in ``_load_plugins()`` * leave all command loading in OSC in ``_load_commands()`` + +API +=== + +The API base layer is the common point for all API subclasses. It is a +wrapper around ``keystoneauth1.session.Session`` that fixes the ``request()`` +interface and provides simple endpoint handling that is useful when a Service +Catalog is either not available or is insufficient. It also adds simple +implementations of the common API CRUD operations: create(), delete(), etc. + +* ``KeystoneSession`` -> merged into ``BaseAPI`` diff --git a/osc_lib/api/api.py b/osc_lib/api/api.py index 74a3aba..ca45aee 100644 --- a/osc_lib/api/api.py +++ b/osc_lib/api/api.py @@ -21,34 +21,47 @@ from keystoneauth1 import session as ks_session from osc_lib import exceptions -class KeystoneSession(object): - """Wrapper for the Keystone Session - - Restore some requests.session.Session compatibility; - keystoneauth1.session.Session.request() has the method and url - arguments swapped from the rest of the requests-using world. +class BaseAPI(object): + """Base API wrapper for keystoneauth1.session.Session + Encapsulate the translation between keystoneauth1.session.Session + and requests.Session in a single layer: + * Restore some requests.session.Session compatibility; + keystoneauth1.session.Session.request() has the method and url + arguments swapped from the rest of the requests-using world. + * Provide basic endpoint handling when a Service Catalog is not available. """ def __init__( self, session=None, + service_type=None, endpoint=None, **kwargs ): """Base object that contains some common API objects and methods - :param Session session: - The default session to be used for making the HTTP API calls. + :param keystoneauth1.session.Session session: + The session to be used for making the HTTP API calls. If None, + a default keystoneauth1.session.Session will be created. + :param string service_type: + API name, i.e. ``identity`` or ``compute`` :param string endpoint: - The URL from the Service Catalog to be used as the base for API - requests on this API. + An optional URL to be used as the base for API requests on + this API. + :param kwargs: + Keyword arguments passed to keystoneauth1.session.Session(). """ - super(KeystoneSession, self).__init__() + super(BaseAPI, self).__init__() - # a requests.Session-style interface - self.session = session + # Create a keystoneauth1.session.Session if one is not supplied + if not session: + self.session = ks_session.Session(**kwargs) + else: + self.session = session + + self.service_type = service_type self.endpoint = endpoint def _request(self, method, url, session=None, **kwargs): @@ -60,54 +73,34 @@ class KeystoneSession(object): :param string method: The HTTP method name, i.e. ``GET``, ``PUT``, etc :param string url: - The API-specific portion of the URL path - :param Session session: - HTTP client session + The API-specific portion of the URL path, or a full URL if + ``endpoint`` was not supplied at initialization. + :param keystoneauth1.session.Session session: + An initialized session to override the one created at + initialization. :param kwargs: - keyword arguments passed to requests.request(). + Keyword arguments passed to requests.request(). :return: the requests.Response object """ + # If session arg is supplied, use it this time, but don't save it if not session: session = self.session - if not session: - session = ks_session.Session() + # Do the auto-endpoint magic if self.endpoint: if url: url = '/'.join([self.endpoint.rstrip('/'), url.lstrip('/')]) else: url = self.endpoint.rstrip('/') + else: + # Pass on the lack of URL unmolested to maintain the same error + # handling from keystoneauth: raise EndpointNotFound + pass # Why is ksc session backwards??? return session.request(url, method, **kwargs) - -class BaseAPI(KeystoneSession): - """Base API""" - - def __init__( - self, - session=None, - service_type=None, - endpoint=None, - **kwargs - ): - """Base object that contains some common API objects and methods - - :param Session session: - The default session to be used for making the HTTP API calls. - :param string service_type: - API name, i.e. ``identity`` or ``compute`` - :param string endpoint: - The URL from the Service Catalog to be used as the base for API - requests on this API. - """ - - super(BaseAPI, self).__init__(session=session, endpoint=endpoint) - - self.service_type = service_type - # The basic action methods all take a Session and return dict/lists def create( diff --git a/osc_lib/tests/api/fakes.py b/osc_lib/tests/api/fakes.py index b7294d5..72be212 100644 --- a/osc_lib/tests/api/fakes.py +++ b/osc_lib/tests/api/fakes.py @@ -48,7 +48,7 @@ LIST_BODY = { class TestSession(utils.TestCase): - BASE_URL = 'https://api.example.com:1234/vX' + BASE_URL = 'https://api.example.com:1234/test' def setUp(self): super(TestSession, self).setUp() diff --git a/osc_lib/tests/api/test_api.py b/osc_lib/tests/api/test_api.py index 9c9f9e3..585e635 100644 --- a/osc_lib/tests/api/test_api.py +++ b/osc_lib/tests/api/test_api.py @@ -13,21 +13,75 @@ """Base API Library Tests""" +from keystoneauth1 import exceptions as ks_exceptions +from keystoneauth1 import session + from osc_lib.api import api from osc_lib import exceptions from osc_lib.tests.api import fakes as api_fakes -class TestKeystoneSession(api_fakes.TestSession): +class TestBaseAPIDefault(api_fakes.TestSession): def setUp(self): - super(TestKeystoneSession, self).setUp() - self.api = api.KeystoneSession( + super(TestBaseAPIDefault, self).setUp() + self.api = api.BaseAPI() + + def test_baseapi_request_url(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=api_fakes.RESP_ITEM_1, + status_code=200, + ) + ret = self.api._request('GET', self.BASE_URL + '/qaz') + self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) + self.assertIsNotNone(self.api.session) + self.assertNotEqual(self.sess, self.api.session) + + def test_baseapi_request_url_path(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=api_fakes.RESP_ITEM_1, + status_code=200, + ) + self.assertRaises( + ks_exceptions.EndpointNotFound, + self.api._request, + 'GET', + '/qaz', + ) + self.assertIsNotNone(self.api.session) + self.assertNotEqual(self.sess, self.api.session) + + def test_baseapi_request_session(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=api_fakes.RESP_ITEM_1, + status_code=200, + ) + ret = self.api._request( + 'GET', + self.BASE_URL + '/qaz', + session=self.sess, + ) + self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) + self.assertIsNotNone(self.api.session) + self.assertNotEqual(self.sess, self.api.session) + + +class TestBaseAPIArgs(api_fakes.TestSession): + + def setUp(self): + super(TestBaseAPIArgs, self).setUp() + self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) - def test_session_request(self): + def test_baseapi_request_url_path(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', @@ -36,18 +90,33 @@ class TestKeystoneSession(api_fakes.TestSession): ) ret = self.api._request('GET', '/qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) + self.assertIsNotNone(self.api.session) + self.assertEqual(self.sess, self.api.session) + + def test_baseapi_request_session(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=api_fakes.RESP_ITEM_1, + status_code=200, + ) + new_session = session.Session() + ret = self.api._request('GET', '/qaz', session=new_session) + self.assertEqual(api_fakes.RESP_ITEM_1, ret.json()) + self.assertIsNotNone(self.api.session) + self.assertNotEqual(new_session, self.api.session) -class TestBaseAPI(api_fakes.TestSession): +class TestBaseAPICreate(api_fakes.TestSession): def setUp(self): - super(TestBaseAPI, self).setUp() + super(TestBaseAPICreate, self).setUp() self.api = api.BaseAPI( session=self.sess, endpoint=self.BASE_URL, ) - def test_create_post(self): + def test_baseapi_create_post(self): self.requests_mock.register_uri( 'POST', self.BASE_URL + '/qaz', @@ -57,7 +126,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.create('qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret) - def test_create_put(self): + def test_baseapi_create_put(self): self.requests_mock.register_uri( 'PUT', self.BASE_URL + '/qaz', @@ -67,7 +136,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.create('qaz', method='PUT') self.assertEqual(api_fakes.RESP_ITEM_1, ret) - def test_delete(self): + def test_baseapi_delete(self): self.requests_mock.register_uri( 'DELETE', self.BASE_URL + '/qaz', @@ -76,9 +145,17 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.delete('qaz') self.assertEqual(204, ret.status_code) - # find tests - def test_find_attr_by_id(self): +class TestBaseAPIFind(api_fakes.TestSession): + + def setUp(self): + super(TestBaseAPIFind, self).setUp() + self.api = api.BaseAPI( + session=self.sess, + endpoint=self.BASE_URL, + ) + + def test_baseapi_find_attr_by_id(self): # All first requests (by name) will fail in this test self.requests_mock.register_uri( @@ -128,7 +205,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_attr('qaz', value='UP', attr='status') self.assertEqual(api_fakes.RESP_ITEM_1, ret) - def test_find_attr_by_name(self): + def test_baseapi_find_attr_by_name(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?name=alpha', @@ -170,7 +247,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_attr('qaz', value='UP', attr='status') self.assertEqual(api_fakes.RESP_ITEM_1, ret) - def test_find_attr_path_resource(self): + def test_baseapi_find_attr_path_resource(self): # Test resource different than path self.requests_mock.register_uri( @@ -188,7 +265,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_attr('wsx', '1', resource='qaz') self.assertEqual(api_fakes.RESP_ITEM_1, ret) - def test_find_bulk_none(self): + def test_baseapi_find_bulk_none(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', @@ -198,7 +275,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_bulk('qaz') self.assertEqual(api_fakes.LIST_RESP, ret) - def test_find_bulk_one(self): + def test_baseapi_find_bulk_one(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', @@ -217,7 +294,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_bulk('qaz', error='bogus') self.assertEqual([], ret) - def test_find_bulk_two(self): + def test_baseapi_find_bulk_two(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', @@ -233,7 +310,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_bulk('qaz', id='1', error='beta') self.assertEqual([], ret) - def test_find_bulk_dict(self): + def test_baseapi_find_bulk_dict(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', @@ -243,28 +320,27 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.find_bulk('qaz', id='1') self.assertEqual([api_fakes.LIST_RESP[0]], ret) - # list tests - def test_list_no_body(self): - self.requests_mock.register_uri( - 'GET', - self.BASE_URL, - json=api_fakes.LIST_RESP, - status_code=200, +class TestBaseAPIList(api_fakes.TestSession): + + def setUp(self): + super(TestBaseAPIList, self).setUp() + self.api = api.BaseAPI( + session=self.sess, + endpoint=self.BASE_URL, ) - ret = self.api.list('') - self.assertEqual(api_fakes.LIST_RESP, ret) + def test_baseapi_list_no_args(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz', json=api_fakes.LIST_RESP, - status_code=200, + status_code=204, ) - ret = self.api.list('qaz') + ret = self.api.list('/qaz') self.assertEqual(api_fakes.LIST_RESP, ret) - def test_list_params(self): + def test_baseapi_list_params(self): params = {'format': 'json'} self.requests_mock.register_uri( 'GET', @@ -284,7 +360,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.list('qaz', **params) self.assertEqual(api_fakes.LIST_RESP, ret) - def test_list_body(self): + def test_baseapi_list_body(self): self.requests_mock.register_uri( 'POST', self.BASE_URL + '/qaz', @@ -294,7 +370,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.list('qaz', body=api_fakes.LIST_BODY) self.assertEqual(api_fakes.LIST_RESP, ret) - def test_list_detailed(self): + def test_baseapi_list_detailed(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz/details', @@ -304,7 +380,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.list('qaz', detailed=True) self.assertEqual(api_fakes.LIST_RESP, ret) - def test_list_filtered(self): + def test_baseapi_list_filtered(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?attr=value', @@ -314,7 +390,7 @@ class TestBaseAPI(api_fakes.TestSession): ret = self.api.list('qaz', attr='value') self.assertEqual(api_fakes.LIST_RESP, ret) - def test_list_wrapped(self): + def test_baseapi_list_wrapped(self): self.requests_mock.register_uri( 'GET', self.BASE_URL + '/qaz?attr=value',