2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright [2010] [Anso Labs, LLC]
5
# Licensed under the Apache License, Version 2.0 (the "License");
6
# you may not use this file except in compliance with the License.
7
# You may obtain a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
18
Tornado REST API Request Handlers for Nova functions
19
Most calls are proxied into the responsible controller.
23
import multiprocessing
27
# TODO(termie): replace minidom with etree
28
from xml.dom import minidom
30
from nova import vendor
32
from twisted.internet import defer
34
from nova import crypto
35
from nova import exception
36
from nova import flags
37
from nova import utils
38
from nova.endpoint import cloud
39
from nova.auth import users
42
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
45
_log = logging.getLogger("api")
46
_log.setLevel(logging.DEBUG)
49
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
52
def _camelcase_to_underscore(str):
53
return _c2u.sub(r'_\1', str).lower().strip('_')
56
def _underscore_to_camelcase(str):
57
return ''.join([x[:1].upper() + x[1:] for x in str.split('_')])
60
def _underscore_to_xmlcase(str):
61
res = _underscore_to_camelcase(str)
62
return res[:1].lower() + res[1:]
65
class APIRequestContext(object):
66
def __init__(self, handler, user):
67
self.handler = handler
69
self.request_id = ''.join(
70
[random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-')
75
class APIRequest(object):
76
def __init__(self, handler, controller, action):
77
self.handler = handler
78
self.controller = controller
81
def send(self, user, **kwargs):
82
context = APIRequestContext(self.handler, user)
85
method = getattr(self.controller,
86
_camelcase_to_underscore(self.action))
87
except AttributeError:
88
_error = ('Unsupported API request: controller = %s,'
89
'action = %s') % (self.controller, self.action)
91
# TODO: Raise custom exception, trap in apiserver,
92
# and reraise as 400 error.
93
raise Exception(_error)
96
for key, value in kwargs.items():
97
parts = key.split(".")
98
key = _camelcase_to_underscore(parts[0])
100
d = args.get(key, {})
101
d[parts[1]] = value[0]
107
for key in args.keys():
108
if isinstance(args[key], dict):
109
if args[key] != {} and args[key].keys()[0].isdigit():
110
s = args[key].items()
112
args[key] = [v for k, v in s]
114
d = defer.maybeDeferred(method, context, **args)
115
d.addCallback(self._render_response, context.request_id)
118
def _render_response(self, response_data, request_id):
119
xml = minidom.Document()
121
response_el = xml.createElement(self.action + 'Response')
122
response_el.setAttribute('xmlns',
123
'http://ec2.amazonaws.com/doc/2009-11-30/')
124
request_id_el = xml.createElement('requestId')
125
request_id_el.appendChild(xml.createTextNode(request_id))
126
response_el.appendChild(request_id_el)
127
if(response_data == True):
128
self._render_dict(xml, response_el, {'return': 'true'})
130
self._render_dict(xml, response_el, response_data)
132
xml.appendChild(response_el)
134
response = xml.toxml()
139
def _render_dict(self, xml, el, data):
141
for key in data.keys():
143
el.appendChild(self._render_data(xml, key, val))
148
def _render_data(self, xml, el_name, data):
149
el_name = _underscore_to_xmlcase(el_name)
150
data_el = xml.createElement(el_name)
152
if isinstance(data, list):
154
data_el.appendChild(self._render_data(xml, 'item', item))
155
elif isinstance(data, dict):
156
self._render_dict(xml, data_el, data)
157
elif hasattr(data, '__dict__'):
158
self._render_dict(xml, data_el, data.__dict__)
159
elif isinstance(data, bool):
160
data_el.appendChild(xml.createTextNode(str(data).lower()))
162
data_el.appendChild(xml.createTextNode(str(data)))
167
class RootRequestHandler(tornado.web.RequestHandler):
169
# available api versions
181
for version in versions:
182
self.write('%s\n' % version)
186
class MetadataRequestHandler(tornado.web.RequestHandler):
187
def print_data(self, data):
188
if isinstance(data, dict):
194
if isinstance(data[key], dict):
195
if '_name' in data[key]:
196
output += '=' + str(data[key]['_name'])
200
self.write(output[:-1]) # cut off last \n
201
elif isinstance(data, list):
202
self.write('\n'.join(data))
204
self.write(str(data))
206
def lookup(self, path, data):
207
items = path.split('/')
210
if not isinstance(data, dict):
218
cc = self.application.controllers['Cloud']
219
meta_data = cc.get_metadata(self.request.remote_ip)
220
if meta_data is None:
221
_log.error('Failed to get metadata for ip: %s' %
222
self.request.remote_ip)
223
raise tornado.web.HTTPError(404)
224
data = self.lookup(path, meta_data)
226
raise tornado.web.HTTPError(404)
227
self.print_data(data)
231
class APIRequestHandler(tornado.web.RequestHandler):
232
def get(self, controller_name):
233
self.execute(controller_name)
235
@tornado.web.asynchronous
236
def execute(self, controller_name):
237
# Obtain the appropriate controller for this request.
239
controller = self.application.controllers[controller_name]
241
self._error('unhandled', 'no controller named %s' % controller_name)
244
args = self.request.arguments
246
# Read request signature.
248
signature = args.pop('Signature')[0]
250
raise tornado.web.HTTPError(400)
252
# Make a copy of args for authentication and signature verification.
254
for key, value in args.items():
255
auth_params[key] = value[0]
257
# Get requested action and remove authentication args for final request.
259
action = args.pop('Action')[0]
260
args.pop('AWSAccessKeyId')
261
args.pop('SignatureMethod')
262
args.pop('SignatureVersion')
264
args.pop('Timestamp')
266
raise tornado.web.HTTPError(400)
268
# Authenticate the request.
269
user = self.application.user_manager.authenticate(
278
raise tornado.web.HTTPError(403)
280
_log.debug('action: %s' % action)
282
for key, value in args.items():
283
_log.debug('arg: %s\t\tval: %s' % (key, value))
285
request = APIRequest(self, controller, action)
286
d = request.send(user, **args)
287
# d.addCallback(utils.debug)
289
# TODO: Wrap response in AWS XML format
290
d.addCallbacks(self._write_callback, self._error_callback)
292
def _write_callback(self, data):
293
self.set_header('Content-Type', 'text/xml')
297
def _error_callback(self, failure):
299
failure.raiseException()
300
except exception.ApiError as ex:
301
self._error(type(ex).__name__ + "." + ex.code, ex.message)
302
# TODO(vish): do something more useful with unknown exceptions
303
except Exception as ex:
304
self._error(type(ex).__name__, str(ex))
307
def post(self, controller_name):
308
self.execute(controller_name)
310
def _error(self, code, message):
311
self._status_code = 400
312
self.set_header('Content-Type', 'text/xml')
313
self.write('<?xml version="1.0"?>\n')
314
self.write('<Response><Errors><Error><Code>%s</Code>'
315
'<Message>%s</Message></Error></Errors>'
316
'<RequestID>?</RequestID></Response>' % (code, message))
320
class APIServerApplication(tornado.web.Application):
321
def __init__(self, user_manager, controllers):
322
tornado.web.Application.__init__(self, [
323
(r'/', RootRequestHandler),
324
(r'/services/([A-Za-z0-9]+)/', APIRequestHandler),
325
(r'/latest/([-A-Za-z0-9/]*)', MetadataRequestHandler),
326
(r'/2009-04-04/([-A-Za-z0-9/]*)', MetadataRequestHandler),
327
(r'/2008-09-01/([-A-Za-z0-9/]*)', MetadataRequestHandler),
328
(r'/2008-02-01/([-A-Za-z0-9/]*)', MetadataRequestHandler),
329
(r'/2007-12-15/([-A-Za-z0-9/]*)', MetadataRequestHandler),
330
(r'/2007-10-10/([-A-Za-z0-9/]*)', MetadataRequestHandler),
331
(r'/2007-08-29/([-A-Za-z0-9/]*)', MetadataRequestHandler),
332
(r'/2007-03-01/([-A-Za-z0-9/]*)', MetadataRequestHandler),
333
(r'/2007-01-19/([-A-Za-z0-9/]*)', MetadataRequestHandler),
334
(r'/1.0/([-A-Za-z0-9/]*)', MetadataRequestHandler),
335
], pool=multiprocessing.Pool(4))
336
self.user_manager = user_manager
337
self.controllers = controllers