#!/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)