342 lines
11 KiB
Python
Executable File
342 lines
11 KiB
Python
Executable File
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Tornado REST API Request Handlers for Nova functions
|
|
Most calls are proxied into the responsible controller.
|
|
"""
|
|
|
|
import logging
|
|
import multiprocessing
|
|
import random
|
|
import re
|
|
import tornado.web
|
|
from twisted.internet import defer
|
|
import urllib
|
|
# TODO(termie): replace minidom with etree
|
|
from xml.dom import minidom
|
|
|
|
from nova import crypto
|
|
from nova import exception
|
|
from nova import flags
|
|
from nova import utils
|
|
from nova.auth import users
|
|
import nova.cloudpipe.api
|
|
from nova.endpoint import cloud
|
|
|
|
|
|
FLAGS = flags.FLAGS
|
|
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
|
|
|
|
_log = logging.getLogger("api")
|
|
_log.setLevel(logging.DEBUG)
|
|
|
|
|
|
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
|
|
|
|
|
|
def _camelcase_to_underscore(str):
|
|
return _c2u.sub(r'_\1', str).lower().strip('_')
|
|
|
|
|
|
def _underscore_to_camelcase(str):
|
|
return ''.join([x[:1].upper() + x[1:] for x in str.split('_')])
|
|
|
|
|
|
def _underscore_to_xmlcase(str):
|
|
res = _underscore_to_camelcase(str)
|
|
return res[:1].lower() + res[1:]
|
|
|
|
|
|
class APIRequestContext(object):
|
|
def __init__(self, handler, user, project):
|
|
self.handler = handler
|
|
self.user = user
|
|
self.project = project
|
|
self.request_id = ''.join(
|
|
[random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-')
|
|
for x in xrange(20)]
|
|
)
|
|
|
|
|
|
class APIRequest(object):
|
|
def __init__(self, controller, action):
|
|
self.controller = controller
|
|
self.action = action
|
|
|
|
def send(self, context, **kwargs):
|
|
|
|
try:
|
|
method = getattr(self.controller,
|
|
_camelcase_to_underscore(self.action))
|
|
except AttributeError:
|
|
_error = ('Unsupported API request: controller = %s,'
|
|
'action = %s') % (self.controller, self.action)
|
|
_log.warning(_error)
|
|
# TODO: Raise custom exception, trap in apiserver,
|
|
# and reraise as 400 error.
|
|
raise Exception(_error)
|
|
|
|
args = {}
|
|
for key, value in kwargs.items():
|
|
parts = key.split(".")
|
|
key = _camelcase_to_underscore(parts[0])
|
|
if len(parts) > 1:
|
|
d = args.get(key, {})
|
|
d[parts[1]] = value[0]
|
|
value = d
|
|
else:
|
|
value = value[0]
|
|
args[key] = value
|
|
|
|
for key in args.keys():
|
|
if isinstance(args[key], dict):
|
|
if args[key] != {} and args[key].keys()[0].isdigit():
|
|
s = args[key].items()
|
|
s.sort()
|
|
args[key] = [v for k, v in s]
|
|
|
|
d = defer.maybeDeferred(method, context, **args)
|
|
d.addCallback(self._render_response, context.request_id)
|
|
return d
|
|
|
|
def _render_response(self, response_data, request_id):
|
|
xml = minidom.Document()
|
|
|
|
response_el = xml.createElement(self.action + 'Response')
|
|
response_el.setAttribute('xmlns',
|
|
'http://ec2.amazonaws.com/doc/2009-11-30/')
|
|
request_id_el = xml.createElement('requestId')
|
|
request_id_el.appendChild(xml.createTextNode(request_id))
|
|
response_el.appendChild(request_id_el)
|
|
if(response_data == True):
|
|
self._render_dict(xml, response_el, {'return': 'true'})
|
|
else:
|
|
self._render_dict(xml, response_el, response_data)
|
|
|
|
xml.appendChild(response_el)
|
|
|
|
response = xml.toxml()
|
|
xml.unlink()
|
|
_log.debug(response)
|
|
return response
|
|
|
|
def _render_dict(self, xml, el, data):
|
|
try:
|
|
for key in data.keys():
|
|
val = data[key]
|
|
el.appendChild(self._render_data(xml, key, val))
|
|
except:
|
|
_log.debug(data)
|
|
raise
|
|
|
|
def _render_data(self, xml, el_name, data):
|
|
el_name = _underscore_to_xmlcase(el_name)
|
|
data_el = xml.createElement(el_name)
|
|
|
|
if isinstance(data, list):
|
|
for item in data:
|
|
data_el.appendChild(self._render_data(xml, 'item', item))
|
|
elif isinstance(data, dict):
|
|
self._render_dict(xml, data_el, data)
|
|
elif hasattr(data, '__dict__'):
|
|
self._render_dict(xml, data_el, data.__dict__)
|
|
elif isinstance(data, bool):
|
|
data_el.appendChild(xml.createTextNode(str(data).lower()))
|
|
elif data != None:
|
|
data_el.appendChild(xml.createTextNode(str(data)))
|
|
|
|
return data_el
|
|
|
|
|
|
class RootRequestHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
# available api versions
|
|
versions = [
|
|
'1.0',
|
|
'2007-01-19',
|
|
'2007-03-01',
|
|
'2007-08-29',
|
|
'2007-10-10',
|
|
'2007-12-15',
|
|
'2008-02-01',
|
|
'2008-09-01',
|
|
'2009-04-04',
|
|
]
|
|
for version in versions:
|
|
self.write('%s\n' % version)
|
|
self.finish()
|
|
|
|
|
|
class MetadataRequestHandler(tornado.web.RequestHandler):
|
|
def print_data(self, data):
|
|
if isinstance(data, dict):
|
|
output = ''
|
|
for key in data:
|
|
if key == '_name':
|
|
continue
|
|
output += key
|
|
if isinstance(data[key], dict):
|
|
if '_name' in data[key]:
|
|
output += '=' + str(data[key]['_name'])
|
|
else:
|
|
output += '/'
|
|
output += '\n'
|
|
self.write(output[:-1]) # cut off last \n
|
|
elif isinstance(data, list):
|
|
self.write('\n'.join(data))
|
|
else:
|
|
self.write(str(data))
|
|
|
|
def lookup(self, path, data):
|
|
items = path.split('/')
|
|
for item in items:
|
|
if item:
|
|
if not isinstance(data, dict):
|
|
return data
|
|
if not item in data:
|
|
return None
|
|
data = data[item]
|
|
return data
|
|
|
|
def get(self, path):
|
|
cc = self.application.controllers['Cloud']
|
|
meta_data = cc.get_metadata(self.request.remote_ip)
|
|
if meta_data is None:
|
|
_log.error('Failed to get metadata for ip: %s' %
|
|
self.request.remote_ip)
|
|
raise tornado.web.HTTPError(404)
|
|
data = self.lookup(path, meta_data)
|
|
if data is None:
|
|
raise tornado.web.HTTPError(404)
|
|
self.print_data(data)
|
|
self.finish()
|
|
|
|
class APIRequestHandler(tornado.web.RequestHandler):
|
|
def get(self, controller_name):
|
|
self.execute(controller_name)
|
|
|
|
@tornado.web.asynchronous
|
|
def execute(self, controller_name):
|
|
# Obtain the appropriate controller for this request.
|
|
try:
|
|
controller = self.application.controllers[controller_name]
|
|
except KeyError:
|
|
self._error('unhandled', 'no controller named %s' % controller_name)
|
|
return
|
|
|
|
args = self.request.arguments
|
|
|
|
# Read request signature.
|
|
try:
|
|
signature = args.pop('Signature')[0]
|
|
except:
|
|
raise tornado.web.HTTPError(400)
|
|
|
|
# Make a copy of args for authentication and signature verification.
|
|
auth_params = {}
|
|
for key, value in args.items():
|
|
auth_params[key] = value[0]
|
|
|
|
# Get requested action and remove authentication args for final request.
|
|
try:
|
|
action = args.pop('Action')[0]
|
|
access = args.pop('AWSAccessKeyId')[0]
|
|
args.pop('SignatureMethod')
|
|
args.pop('SignatureVersion')
|
|
args.pop('Version')
|
|
args.pop('Timestamp')
|
|
except:
|
|
raise tornado.web.HTTPError(400)
|
|
|
|
# Authenticate the request.
|
|
try:
|
|
(user, project) = users.UserManager.instance().authenticate(
|
|
access,
|
|
signature,
|
|
auth_params,
|
|
self.request.method,
|
|
self.request.host,
|
|
self.request.path
|
|
)
|
|
|
|
except exception.Error, ex:
|
|
logging.debug("Authentication Failure: %s" % ex)
|
|
raise tornado.web.HTTPError(403)
|
|
|
|
_log.debug('action: %s' % action)
|
|
|
|
for key, value in args.items():
|
|
_log.debug('arg: %s\t\tval: %s' % (key, value))
|
|
|
|
request = APIRequest(controller, action)
|
|
context = APIRequestContext(self, user, project)
|
|
d = request.send(context, **args)
|
|
# d.addCallback(utils.debug)
|
|
|
|
# TODO: Wrap response in AWS XML format
|
|
d.addCallbacks(self._write_callback, self._error_callback)
|
|
|
|
def _write_callback(self, data):
|
|
self.set_header('Content-Type', 'text/xml')
|
|
self.write(data)
|
|
self.finish()
|
|
|
|
def _error_callback(self, failure):
|
|
try:
|
|
failure.raiseException()
|
|
except exception.ApiError as ex:
|
|
self._error(type(ex).__name__ + "." + ex.code, ex.message)
|
|
# TODO(vish): do something more useful with unknown exceptions
|
|
except Exception as ex:
|
|
self._error(type(ex).__name__, str(ex))
|
|
raise
|
|
|
|
def post(self, controller_name):
|
|
self.execute(controller_name)
|
|
|
|
def _error(self, code, message):
|
|
self._status_code = 400
|
|
self.set_header('Content-Type', 'text/xml')
|
|
self.write('<?xml version="1.0"?>\n')
|
|
self.write('<Response><Errors><Error><Code>%s</Code>'
|
|
'<Message>%s</Message></Error></Errors>'
|
|
'<RequestID>?</RequestID></Response>' % (code, message))
|
|
self.finish()
|
|
|
|
|
|
class APIServerApplication(tornado.web.Application):
|
|
def __init__(self, controllers):
|
|
tornado.web.Application.__init__(self, [
|
|
(r'/', RootRequestHandler),
|
|
(r'/cloudpipe/(.*)', nova.cloudpipe.api.CloudPipeRequestHandler),
|
|
(r'/cloudpipe', nova.cloudpipe.api.CloudPipeRequestHandler),
|
|
(r'/services/([A-Za-z0-9]+)/', APIRequestHandler),
|
|
(r'/latest/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2009-04-04/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2008-09-01/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2008-02-01/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2007-12-15/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2007-10-10/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2007-08-29/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2007-03-01/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/2007-01-19/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
(r'/1.0/([-A-Za-z0-9/]*)', MetadataRequestHandler),
|
|
], pool=multiprocessing.Pool(4))
|
|
self.controllers = controllers
|