Skip to content

Commit e3b9b96

Browse files
author
Dean Troyer
committed
Add low-level API base class
Adds the foundation of a low-level REST API client. This is the final prep stage in the conversion of the object-store commands from the old restapi interface to the keystoneclient.session-based API. * api.api.BaseAPI holds the common operations Change-Id: I8fba980e3eb2d787344f766507a9d0dae49dcadf
1 parent 207c8cf commit e3b9b96

4 files changed

Lines changed: 711 additions & 0 deletions

File tree

openstackclient/api/__init__.py

Whitespace-only changes.

openstackclient/api/api.py

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
#
13+
14+
"""Base API Library"""
15+
16+
import simplejson as json
17+
18+
from keystoneclient.openstack.common.apiclient \
19+
import exceptions as ksc_exceptions
20+
from keystoneclient import session as ksc_session
21+
from openstackclient.common import exceptions
22+
23+
24+
class KeystoneSession(object):
25+
"""Wrapper for the Keystone Session
26+
27+
Restore some requests.session.Session compatibility;
28+
keystoneclient.session.Session.request() has the method and url
29+
arguments swapped from the rest of the requests-using world.
30+
31+
"""
32+
33+
def __init__(
34+
self,
35+
session=None,
36+
endpoint=None,
37+
**kwargs
38+
):
39+
"""Base object that contains some common API objects and methods
40+
41+
:param Session session:
42+
The default session to be used for making the HTTP API calls.
43+
:param string endpoint:
44+
The URL from the Service Catalog to be used as the base for API
45+
requests on this API.
46+
"""
47+
48+
super(KeystoneSession, self).__init__()
49+
50+
# a requests.Session-style interface
51+
self.session = session
52+
self.endpoint = endpoint
53+
54+
def _request(self, method, url, session=None, **kwargs):
55+
"""Perform call into session
56+
57+
All API calls are funneled through this method to provide a common
58+
place to finalize the passed URL and other things.
59+
60+
:param string method:
61+
The HTTP method name, i.e. ``GET``, ``PUT``, etc
62+
:param string url:
63+
The API-specific portion of the URL path
64+
:param Session session:
65+
HTTP client session
66+
:param kwargs:
67+
keyword arguments passed to requests.request().
68+
:return: the requests.Response object
69+
"""
70+
71+
if not session:
72+
session = self.session
73+
if not session:
74+
session = ksc_session.Session()
75+
76+
if self.endpoint:
77+
if url:
78+
url = '/'.join([self.endpoint.rstrip('/'), url.lstrip('/')])
79+
else:
80+
url = self.endpoint.rstrip('/')
81+
82+
# Why is ksc session backwards???
83+
return session.request(url, method, **kwargs)
84+
85+
86+
class BaseAPI(KeystoneSession):
87+
"""Base API"""
88+
89+
def __init__(
90+
self,
91+
session=None,
92+
service_type=None,
93+
endpoint=None,
94+
**kwargs
95+
):
96+
"""Base object that contains some common API objects and methods
97+
98+
:param Session session:
99+
The default session to be used for making the HTTP API calls.
100+
:param string service_type:
101+
API name, i.e. ``identity`` or ``compute``
102+
:param string endpoint:
103+
The URL from the Service Catalog to be used as the base for API
104+
requests on this API.
105+
"""
106+
107+
super(BaseAPI, self).__init__(session=session, endpoint=endpoint)
108+
109+
self.service_type = service_type
110+
111+
# The basic action methods all take a Session and return dict/lists
112+
113+
def create(
114+
self,
115+
url,
116+
session=None,
117+
method=None,
118+
**params
119+
):
120+
"""Create a new resource
121+
122+
:param string url:
123+
The API-specific portion of the URL path
124+
:param Session session:
125+
HTTP client session
126+
:param string method:
127+
HTTP method (default POST)
128+
"""
129+
130+
if not method:
131+
method = 'POST'
132+
ret = self._request(method, url, session=session, **params)
133+
# Should this move into _requests()?
134+
try:
135+
return ret.json()
136+
except json.JSONDecodeError:
137+
return ret
138+
139+
def delete(
140+
self,
141+
url,
142+
session=None,
143+
**params
144+
):
145+
"""Delete a resource
146+
147+
:param string url:
148+
The API-specific portion of the URL path
149+
:param Session session:
150+
HTTP client session
151+
"""
152+
153+
return self._request('DELETE', url, **params)
154+
155+
def list(
156+
self,
157+
path,
158+
session=None,
159+
body=None,
160+
detailed=False,
161+
**params
162+
):
163+
"""Return a list of resources
164+
165+
GET ${ENDPOINT}/${PATH}
166+
167+
path is often the object's plural resource type
168+
169+
:param string path:
170+
The API-specific portion of the URL path
171+
:param Session session:
172+
HTTP client session
173+
:param body: data that will be encoded as JSON and passed in POST
174+
request (GET will be sent by default)
175+
:param bool detailed:
176+
Adds '/details' to path for some APIs to return extended attributes
177+
:returns:
178+
JSON-decoded response, could be a list or a dict-wrapped-list
179+
"""
180+
181+
if detailed:
182+
path = '/'.join([path.rstrip('/'), 'details'])
183+
184+
if body:
185+
ret = self._request(
186+
'POST',
187+
path,
188+
# service=self.service_type,
189+
json=body,
190+
params=params,
191+
)
192+
else:
193+
ret = self._request(
194+
'GET',
195+
path,
196+
# service=self.service_type,
197+
params=params,
198+
)
199+
try:
200+
return ret.json()
201+
except json.JSONDecodeError:
202+
return ret
203+
204+
# Layered actions built on top of the basic action methods do not
205+
# explicitly take a Session but one may still be passed in kwargs
206+
207+
def find_attr(
208+
self,
209+
path,
210+
value=None,
211+
attr=None,
212+
resource=None,
213+
):
214+
"""Find a resource via attribute or ID
215+
216+
Most APIs return a list wrapped by a dict with the resource
217+
name as key. Some APIs (Identity) return a dict when a query
218+
string is present and there is one return value. Take steps to
219+
unwrap these bodies and return a single dict without any resource
220+
wrappers.
221+
222+
:param string path:
223+
The API-specific portion of the URL path
224+
:param string value:
225+
value to search for
226+
:param string attr:
227+
attribute to use for resource search
228+
:param string resource:
229+
plural of the object resource name; defaults to path
230+
For example:
231+
n = find(netclient, 'network', 'networks', 'matrix')
232+
"""
233+
234+
# Default attr is 'name'
235+
if attr is None:
236+
attr = 'name'
237+
238+
# Default resource is path - in many APIs they are the same
239+
if resource is None:
240+
resource = path
241+
242+
def getlist(kw):
243+
"""Do list call, unwrap resource dict if present"""
244+
ret = self.list(path, **kw)
245+
if type(ret) == dict and resource in ret:
246+
ret = ret[resource]
247+
return ret
248+
249+
# Search by attribute
250+
kwargs = {attr: value}
251+
data = getlist(kwargs)
252+
if type(data) == dict:
253+
return data
254+
if len(data) == 1:
255+
return data[0]
256+
if len(data) > 1:
257+
msg = "Multiple %s exist with %s='%s'"
258+
raise ksc_exceptions.CommandError(
259+
msg % (resource, attr, value),
260+
)
261+
262+
# Search by id
263+
kwargs = {'id': value}
264+
data = getlist(kwargs)
265+
if len(data) == 1:
266+
return data[0]
267+
msg = "No %s with a %s or ID of '%s' found"
268+
raise exceptions.CommandError(msg % (resource, attr, value))
269+
270+
def find_bulk(
271+
self,
272+
path,
273+
**kwargs
274+
):
275+
"""Bulk load and filter locally
276+
277+
:param string path:
278+
The API-specific portion of the URL path
279+
:param kwargs:
280+
A dict of AVPs to match - logical AND
281+
:returns: list of resource dicts
282+
"""
283+
284+
items = self.list(path)
285+
if type(items) == dict:
286+
# strip off the enclosing dict
287+
key = list(items.keys())[0]
288+
items = items[key]
289+
290+
ret = []
291+
for o in items:
292+
try:
293+
if all(o[attr] == kwargs[attr] for attr in kwargs.keys()):
294+
ret.append(o)
295+
except KeyError:
296+
continue
297+
298+
return ret
299+
300+
def find_one(
301+
self,
302+
path,
303+
**kwargs
304+
):
305+
"""Find a resource by name or ID
306+
307+
:param string path:
308+
The API-specific portion of the URL path
309+
:returns:
310+
resource dict
311+
"""
312+
313+
bulk_list = self.find_bulk(path, **kwargs)
314+
num_bulk = len(bulk_list)
315+
if num_bulk == 0:
316+
msg = "none found"
317+
raise ksc_exceptions.NotFound(msg)
318+
elif num_bulk > 1:
319+
msg = "many found"
320+
raise RuntimeError(msg)
321+
return bulk_list[0]
322+
323+
def find(
324+
self,
325+
path,
326+
value=None,
327+
attr=None,
328+
):
329+
"""Find a single resource by name or ID
330+
331+
:param string path:
332+
The API-specific portion of the URL path
333+
:param string search:
334+
search expression
335+
:param string attr:
336+
name of attribute for secondary search
337+
"""
338+
339+
try:
340+
ret = self._request('GET', "/%s/%s" % (path, value)).json()
341+
except ksc_exceptions.NotFound:
342+
kwargs = {attr: value}
343+
try:
344+
ret = self.find_one("/%s/detail" % (path), **kwargs)
345+
except ksc_exceptions.NotFound:
346+
msg = "%s not found" % value
347+
raise ksc_exceptions.NotFound(msg)
348+
349+
return ret

openstackclient/tests/api/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)