389 lines
12 KiB
Python
Executable File
389 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Copyright (c) 2013 VMware, Inc. 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.
|
|
#
|
|
|
|
import httplib
|
|
import json
|
|
import re
|
|
import uuid
|
|
import webob
|
|
import webob.dec
|
|
|
|
import ovs.vlog
|
|
vlog = ovs.vlog.Vlog(__name__)
|
|
|
|
|
|
NOT_SUPPORTED_RESPONSE = webob.Response(body="Method not supported",
|
|
status=httplib.NOT_IMPLEMENTED)
|
|
|
|
|
|
def errorResponse(status, error_code, description, data=None):
|
|
"""Construct and return an error response.
|
|
|
|
Args:
|
|
status: The HTTP status code of the response.
|
|
error_code: The application-specific error code.
|
|
description: Friendly G11N-enabled string corresponding ot error_code.
|
|
data: Additional data (not G11N-enabled) for the API consumer.
|
|
"""
|
|
data = {
|
|
'error_code': error_code,
|
|
'descripton': description,
|
|
'error_data': data
|
|
}
|
|
body = '%s\n' % json.dumps(data)
|
|
return webob.Response(body=body, status=status,
|
|
content_type='application/json')
|
|
|
|
|
|
class ApiApplication(object):
|
|
def __init__(self):
|
|
self.handlers = []
|
|
|
|
@webob.dec.wsgify(RequestClass=webob.Request)
|
|
def __call__(self, request):
|
|
handler = self.get_handler(request)
|
|
if handler:
|
|
vlog.dbg("Handling request '%s %s' with %s"
|
|
% (request.method, request.path, str(handler)))
|
|
return handler.handle_request(request)
|
|
else:
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
def register_handler(self, handler, search_index=None):
|
|
if search_index is not None:
|
|
self.handlers.insert(search_index, handler)
|
|
else:
|
|
self.handlers.append(handler)
|
|
|
|
def get_handler(self, request):
|
|
"""Find a handler for a REST request.
|
|
|
|
Args:
|
|
request: A webob request object.
|
|
|
|
Returns:
|
|
A handler instance or None.
|
|
"""
|
|
for h in self.handlers:
|
|
if h.handles_request(request):
|
|
return h
|
|
return None
|
|
|
|
|
|
class AbstractApiHandler(object):
|
|
def __init__(self, path_regex):
|
|
self.parent_handler = None
|
|
self.child_handlers = []
|
|
|
|
if path_regex[-1] != '$':
|
|
path_regex += "$"
|
|
# we only use 'match' so no need to mark the beginning of string
|
|
self.path_regex = path_regex
|
|
self.path_re = re.compile(path_regex)
|
|
|
|
def __str__(self):
|
|
return "%s(%s)" % (self.__class__.__name__, self.path_re.pattern)
|
|
|
|
def handles_request(self, request):
|
|
m = self.path_re.match(request.path)
|
|
return m is not None
|
|
|
|
def handle_request(self, request):
|
|
"""Handle a REST request.
|
|
|
|
Args:
|
|
request: A webob request object.
|
|
|
|
Returns:
|
|
A webob response object.
|
|
"""
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
|
|
class ElementHandler(AbstractApiHandler):
|
|
"""API handler for REST element resources.
|
|
"""
|
|
#TODO: validation
|
|
|
|
def __init__(self, path_regex, model, collection_handler=None):
|
|
"""Initialize an element handler.
|
|
|
|
Args:
|
|
path_regex: A regular expression that matches the full path
|
|
to the element. If multiple handlers match a request path,
|
|
the handler with the highhest registration search_index wins.
|
|
model: A resource data model instance
|
|
collection_handler: The collection handler this elemeent
|
|
is a member of or None if the element is not a member of a
|
|
collection.
|
|
|
|
"""
|
|
super(ElementHandler, self).__init__(path_regex)
|
|
self.model = model
|
|
self.collection_handler = collection_handler
|
|
|
|
def _get_element_id(self, request):
|
|
m = self.path_re.match(request.path)
|
|
if m.groups():
|
|
return m.groups()[-1] #TODO: make robust
|
|
return None
|
|
|
|
def handle_request(self, request):
|
|
"""Handle a REST request.
|
|
|
|
Args:
|
|
request: A webob request object.
|
|
|
|
Returns:
|
|
A webob response object.
|
|
"""
|
|
if request.method == 'GET':
|
|
return self.read(request)
|
|
#TODO(pjb): POST for controller semantics
|
|
elif request.method == 'PUT':
|
|
return self.replace(request)
|
|
elif request.method == 'PATCH':
|
|
return self.update(request)
|
|
elif request.method == 'DELETE':
|
|
return self.delete(request)
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
def read(self, request):
|
|
if not hasattr(self.model, 'get_item'):
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
id_ = self._get_element_id(request)
|
|
item = self.model.get_item(id_)
|
|
if item is None:
|
|
return errorResponse(httplib.NOT_FOUND, 404, 'Not found')
|
|
return webob.Response(body=json.dumps(item), status=httplib.OK,
|
|
content_type='application/json')
|
|
|
|
def replace(self, request):
|
|
if not hasattr(self.model, 'update_item'):
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
id_ = self._get_element_id(request)
|
|
try:
|
|
item = json.loads(request.body)
|
|
self.model.update_item(id_, item)
|
|
except KeyError:
|
|
if (self.collection_handler and
|
|
getattr(self.collection_handler, 'allow_named_create', False)):
|
|
return self.collection_handler.create_member(request, id_=id_)
|
|
return errorResponse(httplib.NOT_FOUND, 404, 'Not found')
|
|
return webob.Response(body=json.dumps(item), status=httplib.OK,
|
|
content_type='application/json')
|
|
|
|
def update(self, request):
|
|
if not (hasattr(self.model, 'update_item') or
|
|
hasattr(self.model, 'get_tiem')):
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
id_ = self._get_element_id(request)
|
|
item = self.model.get_item(id_)
|
|
if item is None:
|
|
return errorResponse(httplib.NOT_FOUND, 404, 'Not found')
|
|
|
|
updates = json.loads(request.body)
|
|
item.update(updates)
|
|
self.model.update_item(id_, item)
|
|
return webob.Response(body=json.dumps(item), status=httplib.OK,
|
|
content_type='application/json')
|
|
|
|
def delete(self, request):
|
|
if not hasattr(self.model, 'delete_item'):
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
id_ = self._get_element_id(request)
|
|
try:
|
|
item = self.model.delete_item(id_)
|
|
return webob.Response(body=json.dumps(item), status=httplib.OK,
|
|
content_type='application/json')
|
|
except KeyError:
|
|
return errorResponse(httplib.NOT_FOUND, 404, 'Not found')
|
|
|
|
|
|
class CollectionHandler(AbstractApiHandler):
|
|
"""API handler for REST collection resources.
|
|
"""
|
|
#TODO: validation
|
|
|
|
def __init__(self, path_regex, model, allow_named_create=True):
|
|
"""Initialize a collection handler.
|
|
|
|
Args:
|
|
path_regex: A regular expression matching the collection base path.
|
|
model: TODO
|
|
element_handler_factor: A callable that returns a new element
|
|
handler.
|
|
"""
|
|
super(CollectionHandler, self).__init__(path_regex)
|
|
self.model = model
|
|
self.allow_named_create = allow_named_create
|
|
|
|
def handle_request(self, request):
|
|
"""Handle a REST request.
|
|
|
|
Args:
|
|
request: A webob request object.
|
|
|
|
Returns:
|
|
A webob response object.
|
|
"""
|
|
if request.method == 'GET':
|
|
return self.list_members(request)
|
|
elif request.method == 'POST':
|
|
return self.create_member(request)
|
|
return NOT_SUPPORTED_RESPONSE
|
|
|
|
def list_members(self, request):
|
|
items = self.model.get_items().values()
|
|
body = "%s\n" % json.dumps(items, indent=2)
|
|
return webob.Response(body=body, status=httplib.OK,
|
|
content_type='application/json')
|
|
|
|
def create_member(self, request, id_=None):
|
|
item = json.loads(request.body)
|
|
try:
|
|
id_ = self.model.add_item(item, id_)
|
|
except KeyError:
|
|
return errorResponse(httplib.CONFLICT, httplib.CONFLICT,
|
|
'Element already exists')
|
|
item['id'] = id_
|
|
|
|
return webob.Response(body=json.dumps(item), status=httplib.CREATED,
|
|
content_type='application/json',
|
|
location="%s/%s" %(request.path, id_))
|
|
|
|
|
|
class RowCollectionHandler(CollectionHandler):
|
|
pass
|
|
|
|
|
|
class RowElementHandler(ElementHandler):
|
|
"""API handler for table row elements.
|
|
"""
|
|
|
|
def _get_element_id(self, request):
|
|
m = self.path_re.match(request.path)
|
|
print 'groups', m.groups()
|
|
if m.groups():
|
|
return m.groups()[-1] #TODO: make robust
|
|
return None
|
|
|
|
|
|
|
|
class SimpleDataModel(object):
|
|
"""An in-memory data model.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.items = {}
|
|
|
|
def get_items(self):
|
|
"""Get items in model.
|
|
|
|
Returns: A dict of {id, item} for all items in model.
|
|
"""
|
|
return self.items
|
|
|
|
def add_item(self, item, id_=None):
|
|
"""Add item to model.
|
|
|
|
Args:
|
|
item: The item to add to the model.
|
|
id_: The ID of the item, or None if an ID should be generated
|
|
|
|
Returns:
|
|
The ID of the newly added item.
|
|
|
|
Raises:
|
|
KeyError: ID already exists.
|
|
"""
|
|
if id_ is None:
|
|
id_ = str(uuid.uuid4())
|
|
if id_ in self.items:
|
|
raise KeyError("Cannot create item with ID '%s': "
|
|
"ID already exists")
|
|
self.items[id_] = item
|
|
return id_
|
|
|
|
def get_item(self, id_):
|
|
"""Retrieve item with id id_ from model.
|
|
|
|
Args:
|
|
id_: The ID of the item to retrieve.
|
|
|
|
Returns:
|
|
The matching item or None if item with id_ does not exist.
|
|
"""
|
|
return self.items.get(id_)
|
|
|
|
def update_item(self, id_, item):
|
|
"""Update item with id_ with new data.
|
|
|
|
Args:
|
|
id_: The ID of the item to be updated.
|
|
item: The new item.
|
|
|
|
Returns:
|
|
The updated item.
|
|
|
|
Raises:
|
|
KeyError: Item with specified id_ not present.
|
|
"""
|
|
if id_ not in self.items:
|
|
raise KeyError("Cannot update item with ID '%s': "
|
|
"ID does not exist")
|
|
self.items[id_] = item
|
|
return item
|
|
|
|
def delete_item(self, id_):
|
|
"""Remove item from model.
|
|
|
|
Args:
|
|
id_: The ID of the item to be removed.
|
|
|
|
Returns:
|
|
The removed item.
|
|
|
|
Raises:
|
|
KeyError: Item with specified id_ not present.
|
|
"""
|
|
ret = self.items[id_]
|
|
del self.items[id_]
|
|
return ret
|
|
|
|
|
|
|
|
|
|
class PolicyDataModel(object):
|
|
"""An in-memory policy data model.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.rules = []
|
|
|
|
def get_item(self, id_):
|
|
return {'rules': self.rules}
|
|
|
|
def update_item(self, id_, item):
|
|
self.rules = item['rules']
|
|
return self.get_item(None)
|
|
|
|
|