Skip to content

Commit 17f13f7

Browse files
author
Dean Troyer
committed
Create a new base REST API interface
* restapi module provides basic REST API support * uses dicts rather than Resource classes * JSON serialization/deserialization * log requests in 'curl' format * basic API boilerplate for create/delete/list/set/show verbs * ignore H302 due to urllib import Change-Id: I3cb91e44e631ee19e9f5dea19b6bac5d599d19ce
1 parent b440986 commit 17f13f7

6 files changed

Lines changed: 552 additions & 1 deletion

File tree

‎openstackclient/common/restapi.py‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright 2013 Nebula Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
#
15+
16+
"""REST API bits"""
17+
18+
import json
19+
import logging
20+
import requests
21+
22+
try:
23+
from urllib.parse import urlencode
24+
except ImportError:
25+
from urllib import urlencode
26+
27+
28+
_logger = logging.getLogger(__name__)
29+
30+
31+
class RESTApi(object):
32+
"""A REST api client that handles the interface from us to the server
33+
34+
RESTApi is an extension of a requests.Session that knows
35+
how to do:
36+
* JSON serialization/deserialization
37+
* log requests in 'curl' format
38+
* basic API boilerplate for create/delete/list/set/show verbs
39+
40+
* authentication is handled elsewhere and a token is passed in
41+
42+
The expectation that there will be a RESTApi object per authentication
43+
token in use, i.e. project/username/auth_endpoint
44+
45+
On the other hand, a Client knows details about the specific REST Api that
46+
it communicates with, such as the available endpoints, API versions, etc.
47+
"""
48+
49+
USER_AGENT = 'RAPI'
50+
51+
def __init__(
52+
self,
53+
os_auth=None,
54+
user_agent=USER_AGENT,
55+
debug=None,
56+
**kwargs
57+
):
58+
self.set_auth(os_auth)
59+
self.debug = debug
60+
self.session = requests.Session(**kwargs)
61+
62+
self.set_header('User-Agent', user_agent)
63+
self.set_header('Content-Type', 'application/json')
64+
65+
def set_auth(self, os_auth):
66+
"""Sets the current auth blob"""
67+
self.os_auth = os_auth
68+
69+
def set_header(self, header, content):
70+
"""Sets passed in headers into the session headers
71+
72+
Replaces existing headers!!
73+
"""
74+
if content is None:
75+
del self.session.headers[header]
76+
else:
77+
self.session.headers[header] = content
78+
79+
def request(self, method, url, **kwargs):
80+
if self.os_auth:
81+
self.session.headers.setdefault('X-Auth-Token', self.os_auth)
82+
if 'data' in kwargs and isinstance(kwargs['data'], type({})):
83+
kwargs['data'] = json.dumps(kwargs['data'])
84+
log_request(method, url, headers=self.session.headers, **kwargs)
85+
response = self.session.request(method, url, **kwargs)
86+
log_response(response)
87+
return self._error_handler(response)
88+
89+
def create(self, url, data=None, response_key=None, **kwargs):
90+
response = self.request('POST', url, data=data, **kwargs)
91+
if response_key:
92+
return response.json()[response_key]
93+
else:
94+
return response.json()
95+
96+
#with self.completion_cache('human_id', self.resource_class, mode="a"):
97+
# with self.completion_cache('uuid', self.resource_class, mode="a"):
98+
# return self.resource_class(self, body[response_key])
99+
100+
def delete(self, url):
101+
self.request('DELETE', url)
102+
103+
def list(self, url, data=None, response_key=None, **kwargs):
104+
if data:
105+
response = self.request('POST', url, data=data, **kwargs)
106+
else:
107+
kwargs.setdefault('allow_redirects', True)
108+
response = self.request('GET', url, **kwargs)
109+
110+
return response.json()[response_key]
111+
112+
###hack this for keystone!!!
113+
#data = body[response_key]
114+
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
115+
# unlike other services which just return the list...
116+
#if isinstance(data, dict):
117+
# try:
118+
# data = data['values']
119+
# except KeyError:
120+
# pass
121+
122+
#with self.completion_cache('human_id', obj_class, mode="w"):
123+
# with self.completion_cache('uuid', obj_class, mode="w"):
124+
# return [obj_class(self, res, loaded=True)
125+
# for res in data if res]
126+
127+
def set(self, url, data=None, response_key=None, **kwargs):
128+
response = self.request('PUT', url, data=data)
129+
if data:
130+
if response_key:
131+
return response.json()[response_key]
132+
else:
133+
return response.json()
134+
else:
135+
return None
136+
137+
def show(self, url, response_key=None, **kwargs):
138+
response = self.request('GET', url, **kwargs)
139+
if response_key:
140+
return response.json()[response_key]
141+
else:
142+
return response.json()
143+
144+
def _error_handler(self, response):
145+
if response.status_code < 200 or response.status_code >= 300:
146+
_logger.debug(
147+
"ERROR: %s",
148+
response.text,
149+
)
150+
response.raise_for_status()
151+
return response
152+
153+
154+
def log_request(method, url, **kwargs):
155+
# put in an early exit if debugging is not enabled?
156+
if 'params' in kwargs and kwargs['params'] != {}:
157+
url += '?' + urlencode(kwargs['params'])
158+
159+
string_parts = [
160+
"curl -i",
161+
"-X '%s'" % method,
162+
"'%s'" % url,
163+
]
164+
165+
for element in kwargs['headers']:
166+
header = " -H '%s: %s'" % (element, kwargs['headers'][element])
167+
string_parts.append(header)
168+
169+
_logger.debug("REQ: %s" % " ".join(string_parts))
170+
if 'data' in kwargs:
171+
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
172+
173+
174+
def log_response(response):
175+
_logger.debug(
176+
"RESP: [%s] %s\n",
177+
response.status_code,
178+
response.headers,
179+
)
180+
if response._content_consumed:
181+
_logger.debug(
182+
"RESP BODY: %s\n",
183+
response.text,
184+
)
185+
_logger.debug(
186+
"encoding: %s",
187+
response.encoding,
188+
)

‎openstackclient/common/utils.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,30 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
115115
return tuple(row)
116116

117117

118+
def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}):
119+
"""Return a tuple containing the item properties.
120+
121+
:param item: a single dict resource
122+
:param fields: tuple of strings with the desired field names
123+
:param mixed_case_fields: tuple of field names to preserve case
124+
:param formatters: dictionary mapping field names to callables
125+
to format the values
126+
"""
127+
row = []
128+
129+
for field in fields:
130+
if field in mixed_case_fields:
131+
field_name = field.replace(' ', '_')
132+
else:
133+
field_name = field.lower().replace(' ', '_')
134+
data = item[field_name] if field_name in item else ''
135+
if field in formatters:
136+
row.append(formatters[field](data))
137+
else:
138+
row.append(data)
139+
return tuple(row)
140+
141+
118142
def string_to_bool(arg):
119143
return arg.strip().lower() in ('t', 'true', 'yes', '1')
120144

‎openstackclient/shell.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from openstackclient.common import commandmanager
3030
from openstackclient.common import exceptions as exc
3131
from openstackclient.common import openstackkeyring
32+
from openstackclient.common import restapi
3233
from openstackclient.common import utils
3334

3435

@@ -368,6 +369,9 @@ def initialize_app(self, argv):
368369
if self.options.deferred_help:
369370
self.DeferredHelpAction(self.parser, self.parser, None, None)
370371

372+
# Set up common client session
373+
self.restapi = restapi.RESTApi()
374+
371375
# If the user is not asking for help, make sure they
372376
# have given us auth.
373377
cmd_name = None
@@ -376,6 +380,7 @@ def initialize_app(self, argv):
376380
cmd_factory, cmd_name, sub_argv = cmd_info
377381
if self.interactive_mode or cmd_name != 'help':
378382
self.authenticate_user()
383+
self.restapi.set_auth(self.client_manager.identity.auth_token)
379384

380385
def prepare_to_run_command(self, cmd):
381386
"""Set up auth and API versions"""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2013 OpenStack Foundation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
#

0 commit comments

Comments
 (0)