~0x44/nova/extdoc

« back to all changes in this revision

Viewing changes to nova/endpoint/api.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
3
# Copyright [2010] [Anso Labs, LLC]
 
4
 
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
 
8
 
9
#        http://www.apache.org/licenses/LICENSE-2.0
 
10
 
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.
 
16
 
 
17
"""
 
18
Tornado REST API Request Handlers for Nova functions
 
19
Most calls are proxied into the responsible controller.
 
20
"""
 
21
 
 
22
import logging
 
23
import multiprocessing
 
24
import random
 
25
import re
 
26
import urllib
 
27
# TODO(termie): replace minidom with etree
 
28
from xml.dom import minidom
 
29
 
 
30
from nova import vendor
 
31
import tornado.web
 
32
from twisted.internet import defer
 
33
 
 
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
 
40
 
 
41
FLAGS = flags.FLAGS
 
42
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
 
43
 
 
44
 
 
45
_log = logging.getLogger("api")
 
46
_log.setLevel(logging.DEBUG)
 
47
 
 
48
 
 
49
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
 
50
 
 
51
 
 
52
def _camelcase_to_underscore(str):
 
53
    return _c2u.sub(r'_\1', str).lower().strip('_')
 
54
 
 
55
 
 
56
def _underscore_to_camelcase(str):
 
57
    return ''.join([x[:1].upper() + x[1:] for x in str.split('_')])
 
58
 
 
59
 
 
60
def _underscore_to_xmlcase(str):
 
61
    res = _underscore_to_camelcase(str)
 
62
    return res[:1].lower() + res[1:]
 
63
 
 
64
 
 
65
class APIRequestContext(object):
 
66
    def __init__(self, handler, user):
 
67
        self.handler = handler
 
68
        self.user = user
 
69
        self.request_id = ''.join(
 
70
                [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-')
 
71
                 for x in xrange(20)]
 
72
                )
 
73
 
 
74
 
 
75
class APIRequest(object):
 
76
    def __init__(self, handler, controller, action):
 
77
        self.handler = handler
 
78
        self.controller = controller
 
79
        self.action = action
 
80
 
 
81
    def send(self, user, **kwargs):
 
82
        context = APIRequestContext(self.handler, user)
 
83
 
 
84
        try:
 
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)
 
90
            _log.warning(_error)
 
91
            # TODO: Raise custom exception, trap in apiserver,
 
92
            #       and reraise as 400 error.
 
93
            raise Exception(_error)
 
94
 
 
95
        args = {}
 
96
        for key, value in kwargs.items():
 
97
            parts = key.split(".")
 
98
            key = _camelcase_to_underscore(parts[0])
 
99
            if len(parts) > 1:
 
100
                d = args.get(key, {})
 
101
                d[parts[1]] = value[0]
 
102
                value = d
 
103
            else:
 
104
                value = value[0]
 
105
            args[key] = value
 
106
 
 
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()
 
111
                    s.sort()
 
112
                    args[key] = [v for k, v in s]
 
113
 
 
114
        d = defer.maybeDeferred(method, context, **args)
 
115
        d.addCallback(self._render_response, context.request_id)
 
116
        return d
 
117
 
 
118
    def _render_response(self, response_data, request_id):
 
119
        xml = minidom.Document()
 
120
 
 
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'})
 
129
        else:
 
130
            self._render_dict(xml, response_el, response_data)
 
131
 
 
132
        xml.appendChild(response_el)
 
133
 
 
134
        response = xml.toxml()
 
135
        xml.unlink()
 
136
        _log.debug(response)
 
137
        return response
 
138
 
 
139
    def _render_dict(self, xml, el, data):
 
140
        try:
 
141
            for key in data.keys():
 
142
                val = data[key]
 
143
                el.appendChild(self._render_data(xml, key, val))
 
144
        except:
 
145
            _log.debug(data)
 
146
            raise
 
147
 
 
148
    def _render_data(self, xml, el_name, data):
 
149
        el_name = _underscore_to_xmlcase(el_name)
 
150
        data_el = xml.createElement(el_name)
 
151
 
 
152
        if isinstance(data, list):
 
153
            for item in data:
 
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()))
 
161
        elif data != None:
 
162
            data_el.appendChild(xml.createTextNode(str(data)))
 
163
 
 
164
        return data_el
 
165
 
 
166
 
 
167
class RootRequestHandler(tornado.web.RequestHandler):
 
168
    def get(self):
 
169
        # available api versions
 
170
        versions = [
 
171
            '1.0',
 
172
            '2007-01-19',
 
173
            '2007-03-01',
 
174
            '2007-08-29',
 
175
            '2007-10-10',
 
176
            '2007-12-15',
 
177
            '2008-02-01',
 
178
            '2008-09-01',
 
179
            '2009-04-04',
 
180
        ]
 
181
        for version in versions:
 
182
            self.write('%s\n' % version)
 
183
        self.finish()
 
184
 
 
185
 
 
186
class MetadataRequestHandler(tornado.web.RequestHandler):
 
187
    def print_data(self, data):
 
188
        if isinstance(data, dict):
 
189
            output = ''
 
190
            for key in data:
 
191
                if key == '_name':
 
192
                    continue
 
193
                output += key
 
194
                if isinstance(data[key], dict):
 
195
                    if '_name' in data[key]:
 
196
                        output += '=' + str(data[key]['_name'])
 
197
                    else:
 
198
                        output += '/'
 
199
                output += '\n'
 
200
            self.write(output[:-1]) # cut off last \n
 
201
        elif isinstance(data, list):
 
202
            self.write('\n'.join(data))
 
203
        else:
 
204
            self.write(str(data))
 
205
 
 
206
    def lookup(self, path, data):
 
207
        items = path.split('/')
 
208
        for item in items:
 
209
            if item:
 
210
                if not isinstance(data, dict):
 
211
                    return data
 
212
                if not item in data:
 
213
                    return None
 
214
                data = data[item]
 
215
        return data
 
216
 
 
217
    def get(self, path):
 
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)
 
225
        if data is None:
 
226
            raise tornado.web.HTTPError(404)
 
227
        self.print_data(data)
 
228
        self.finish()
 
229
 
 
230
 
 
231
class APIRequestHandler(tornado.web.RequestHandler):
 
232
    def get(self, controller_name):
 
233
        self.execute(controller_name)
 
234
 
 
235
    @tornado.web.asynchronous
 
236
    def execute(self, controller_name):
 
237
        # Obtain the appropriate controller for this request.
 
238
        try:
 
239
            controller = self.application.controllers[controller_name]
 
240
        except KeyError:
 
241
            self._error('unhandled', 'no controller named %s' % controller_name)
 
242
            return
 
243
 
 
244
        args = self.request.arguments
 
245
 
 
246
        # Read request signature.
 
247
        try:
 
248
            signature = args.pop('Signature')[0]
 
249
        except:
 
250
            raise tornado.web.HTTPError(400)
 
251
 
 
252
        # Make a copy of args for authentication and signature verification.
 
253
        auth_params = {}
 
254
        for key, value in args.items():
 
255
            auth_params[key] = value[0]
 
256
 
 
257
        # Get requested action and remove authentication args for final request.
 
258
        try:
 
259
            action = args.pop('Action')[0]
 
260
            args.pop('AWSAccessKeyId')
 
261
            args.pop('SignatureMethod')
 
262
            args.pop('SignatureVersion')
 
263
            args.pop('Version')
 
264
            args.pop('Timestamp')
 
265
        except:
 
266
            raise tornado.web.HTTPError(400)
 
267
 
 
268
        # Authenticate the request.
 
269
        user = self.application.user_manager.authenticate(
 
270
            auth_params,
 
271
            signature,
 
272
            self.request.method,
 
273
            self.request.host,
 
274
            self.request.path
 
275
        )
 
276
 
 
277
        if not user:
 
278
            raise tornado.web.HTTPError(403)
 
279
 
 
280
        _log.debug('action: %s' % action)
 
281
 
 
282
        for key, value in args.items():
 
283
            _log.debug('arg: %s\t\tval: %s' % (key, value))
 
284
 
 
285
        request = APIRequest(self, controller, action)
 
286
        d = request.send(user, **args)
 
287
        # d.addCallback(utils.debug)
 
288
 
 
289
        # TODO: Wrap response in AWS XML format
 
290
        d.addCallbacks(self._write_callback, self._error_callback)
 
291
 
 
292
    def _write_callback(self, data):
 
293
        self.set_header('Content-Type', 'text/xml')
 
294
        self.write(data)
 
295
        self.finish()
 
296
 
 
297
    def _error_callback(self, failure):
 
298
        try:
 
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))
 
305
            raise
 
306
 
 
307
    def post(self, controller_name):
 
308
        self.execute(controller_name)
 
309
 
 
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))
 
317
        self.finish()
 
318
 
 
319
 
 
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