Clean up API

* Merge api.KeystoneSession into api.BaseAPI
* Beef up testing coverage

Change-Id: I304871c95c1dc82cd8ce2afbbda2320643ff4d27
This commit is contained in:
Dean Troyer 2016-05-20 13:32:44 -05:00
parent de84ddacca
commit 26ff8c98d7
4 changed files with 159 additions and 79 deletions

View File

@ -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``

View File

@ -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(

View File

@ -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()

View File

@ -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',