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
 |