launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #12531
[Merge] lp:~allenap/maas/default-crud into lp:maas
Gavin Panella has proposed merging lp:~allenap/maas/default-crud into lp:maas.
Commit message:
Defines a new operations dispatch mechanism on top of Piston.
The existing class decoration code was confusing. This is still somewhat complex, but much less so, and the flow of control is easier to trace. This also takes over the definition of allowed_methods: all operations are exported, and CRUD methods that have not been erased (set to None) are also exported.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1049933 in MAAS: "The public-facing API code confusingly conflates CRUD, HTTP methods, and piggybacked operations"
https://bugs.launchpad.net/maas/+bug/1049933
For more details, see:
https://code.launchpad.net/~allenap/maas/default-crud/+merge/126574
There are three parts to the new dispatch mechanism:
- OperationsResource ensures that handler.dispatch() is called for all methods, rather than GET->read and so forth that the default Resource (from Piston) does.
- OperationsHandlerType is a metaclass that sets a handler's export attribute to a simple mapping of (http-method, op) to functions, where op is from the request parameters, or None if not specified. If op is None the function is one of the CRUD methods, otherwise it's a custom operation.
- OperationsHandlerMixin gets mixed with Piston's BaseHandler and AnonymousBaseHandler to give OperationsHandler and AnonymousOperationsHandler. All it provides is a dispatch method to match the expectations of OperationsResource from earlier. This consults the exports mapping so carefully constructed by OperationsHandlerType, and dispatches accordingly.
--
https://code.launchpad.net/~allenap/maas/default-crud/+merge/126574
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/default-crud into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-09-26 10:00:14 +0000
+++ src/maasserver/api.py 2012-09-26 23:47:21 +0000
@@ -57,6 +57,7 @@
"AccountHandler",
"AnonNodeGroupsHandler",
"AnonNodesHandler",
+ "AnonymousOperationsHandler",
"api_doc",
"api_doc_title",
"BootImagesHandler",
@@ -69,6 +70,7 @@
"NodeMacHandler",
"NodeMacsHandler",
"NodesHandler",
+ "OperationsHandler",
"TagHandler",
"TagsHandler",
"pxeconfig",
@@ -76,15 +78,17 @@
]
from base64 import b64decode
+from cStringIO import StringIO
from datetime import (
datetime,
timedelta,
)
+from functools import partial
import httplib
+from inspect import getdoc
import json
import sys
from textwrap import dedent
-import types
from celery.app import app_or_default
from django.conf import settings
@@ -158,6 +162,7 @@
from piston.handler import (
AnonymousBaseHandler,
BaseHandler,
+ HandlerMetaClass,
)
from piston.models import Token
from piston.resource import Resource
@@ -165,15 +170,18 @@
from provisioningserver.kernel_opts import KernelParameters
-dispatch_methods = {
- 'GET': 'read',
- 'POST': 'create',
- 'PUT': 'update',
- 'DELETE': 'delete',
- }
-
-
-class RestrictedResource(Resource):
+class OperationsResource(Resource):
+ """A resource supporting operation dispatch.
+
+ All requests are passed onto the handler's `dispatch` method. See
+ :class:`OperationsHandler`.
+ """
+
+ crudmap = Resource.callmap
+ callmap = dict.fromkeys(crudmap, "dispatch")
+
+
+class RestrictedResource(OperationsResource):
def authenticate(self, request, rm):
actor, anonymous = super(
@@ -202,123 +210,99 @@
:param exported_as: Optional operation name; defaults to the name of the
exported method.
- See also _`api_operations`.
"""
def _decorator(func):
- if method not in dispatch_methods:
+ if method not in OperationsResource.callmap:
raise ValueError("Invalid method: '%s'" % method)
if exported_as is None:
func._api_exported = {method: func.__name__}
else:
func._api_exported = {method: exported_as}
- if func._api_exported.get(method) == dispatch_methods.get(method):
- raise ValueError(
- "Cannot define a '%s' operation." % dispatch_methods.get(
- method))
return func
return _decorator
-# The parameter used to specify the requested operation for POST API calls.
-OP_PARAM = 'op'
-
-
-def is_api_exported(thing, method='POST'):
- # Check for functions and methods; the latter may be from base classes.
- op_types = types.FunctionType, types.MethodType
- return (
- isinstance(thing, op_types) and
- getattr(thing, "_api_exported", None) is not None and
- getattr(thing, "_api_exported", None).get(method, None) is not None)
-
-
-# Define a method that will route requests to the methods registered in
-# handler._available_api_methods.
-def perform_api_operation(handler, request, method='POST', *args, **kwargs):
- if method == 'POST':
- data = request.POST
- else:
- data = request.GET
- op = data.get(OP_PARAM, None)
- if not isinstance(op, unicode):
- return HttpResponseBadRequest("Unknown operation.")
- elif method not in handler._available_api_methods:
- return HttpResponseBadRequest("Unknown operation: '%s'." % op)
- elif op not in handler._available_api_methods[method]:
- return HttpResponseBadRequest("Unknown operation: '%s'." % op)
- else:
- method = handler._available_api_methods[method][op]
- return method(handler, request, *args, **kwargs)
-
-
-def api_operations(cls):
- """Class decorator (PEP 3129) to be used on piston-based handler classes
- (i.e. classes inheriting from piston.handler.BaseHandler). It will add
- the required methods {'create','read','update','delete} to the class.
- These methods (called by piston to handle POST/GET/PUT/DELETE requests),
- will route requests to methods decorated with
- @api_exported(method={'POST','GET','PUT','DELETE'} depending on the
- operation requested using the 'op' parameter.
-
- E.g.:
-
- >>> @api_operations
- >>> class MyHandler(BaseHandler):
- >>>
- >>> @api_exported(method='POST', exported_as='exported_post_name')
- >>> def do_x(self, request):
- >>> # process request...
- >>>
- >>> @api_exported(method='GET')
- >>> def do_y(self, request):
- >>> # process request...
-
- MyHandler's method 'do_x' will service POST requests with
- 'op=exported_post_name' in its request parameters.
-
- POST /api/path/to/MyHandler/
- op=exported_post_name¶m1=1
-
- MyHandler's method 'do_y' will service GET requests with
- 'op=do_y' in its request parameters.
-
- GET /api/path/to/MyHandler/?op=do_y¶m1=1
-
+class OperationsHandlerType(HandlerMetaClass):
+ """Type for handlers that dispatch operations.
+
+ Collects all the exported operations, CRUD and custom, into the class's
+ `exports` attribute. This is a signature:function mapping, where signature
+ is an (http-method, operation-name) tuple. If operation-name is None, it's
+ a CRUD method.
+
+ The `allowed_methods` attribute is calculated as the union of all HTTP
+ methods required for the exported CRUD and custom operations.
"""
- # Compute the list of methods ('GET', 'POST', etc.) that need to be
- # overriden.
- overriden_methods = set()
- for name, value in vars(cls).items():
- overriden_methods.update(getattr(value, '_api_exported', {}))
- # Override the appropriate methods with a 'dispatcher' method.
- for method in overriden_methods:
+
+ def __new__(metaclass, name, bases, namespace):
+ cls = super(OperationsHandlerType, metaclass).__new__(
+ metaclass, name, bases, namespace)
+
+ # Create an http-method:function mapping for CRUD operations.
+ crud = {
+ http_method: getattr(cls, method)
+ for http_method, method in OperationsResource.crudmap.items()
+ if getattr(cls, method, None) is not None
+ }
+
+ # Create a operation-name:function mapping for non-CRUD operations.
+ # These functions contain an _api_exported attribute that will be
+ # used later on.
operations = {
- name: value
- for name, value in vars(cls).items()
- if is_api_exported(value, method)}
- cls._available_api_methods = getattr(
- cls, "_available_api_methods", {}).copy()
- cls._available_api_methods[method] = {
- op._api_exported[method]: op
- for name, op in operations.items()
- if method in op._api_exported}
-
- def dispatcher(self, request, *args, **kwargs):
- return perform_api_operation(
- self, request, request.method, *args, **kwargs)
-
- method_name = str(dispatch_methods[method])
- dispatcher.__name__ = method_name
- dispatcher.__doc__ = (
- "The actual operation to execute depends on the value of the '%s' "
- "parameter:\n\n" % OP_PARAM)
- dispatcher.__doc__ += "\n".join(
- "- Operation '%s' (op=%s):\n\t%s" % (name, name, op.__doc__)
- for name, op in cls._available_api_methods[method].items())
-
- # Add {'create','read','update','delete'} method.
- setattr(cls, method_name, dispatcher)
- return cls
+ name: attribute for name, attribute in vars(cls).items()
+ if getattr(attribute, "_api_exported", None) is not None
+ }
+
+ # Create the exports mapping.
+ exports = {}
+ exports.update(
+ ((http_method, None), function)
+ for http_method, function in crud.items())
+ exports.update(
+ (signature, function)
+ for name, function in operations.items()
+ for signature in function._api_exported.items())
+
+ # Update the class.
+ cls.exports = exports
+ cls.allowed_methods = frozenset(
+ http_method for http_method, name in exports)
+
+ return cls
+
+
+class OperationsHandlerMixin:
+ """Handler mixin for operations dispatch.
+
+ This enabled dispatch to custom functions that piggyback on HTTP methods
+ that ordinarily, in Piston, are used for CRUD operations.
+
+ This must be used in cooperation with :class:`OperationsResource` and
+ :class:`OperationsHandlerType`.
+ """
+
+ def dispatch(self, request, *args, **kwargs):
+ signature = request.method.upper(), request.REQUEST.get("op")
+ function = self.exports.get(signature)
+ if function is None:
+ return HttpResponseBadRequest(
+ "Unrecognised signature: %s %s" % signature)
+ else:
+ return function(self, request, *args, **kwargs)
+
+
+class OperationsHandler(
+ OperationsHandlerMixin, BaseHandler):
+ """Base handler that supports operation dispatch."""
+
+ __metaclass__ = OperationsHandlerType
+
+
+class AnonymousOperationsHandler(
+ OperationsHandlerMixin, AnonymousBaseHandler):
+ """Anonymous base handler that supports operation dispatch."""
+
+ __metaclass__ = OperationsHandlerType
def get_mandatory_param(data, key, validator=None):
@@ -438,10 +422,9 @@
)
-@api_operations
-class NodeHandler(BaseHandler):
+class NodeHandler(OperationsHandler):
"""Manage individual Nodes."""
- allowed_methods = ('GET', 'DELETE', 'POST', 'PUT')
+ create = None # Disable create.
model = Node
fields = DISPLAYED_NODE_FIELDS
@@ -586,10 +569,9 @@
raise ValidationError(form.errors)
-@api_operations
-class AnonNodesHandler(AnonymousBaseHandler):
+class AnonNodesHandler(AnonymousOperationsHandler):
"""Create Nodes."""
- allowed_methods = ('GET', 'POST',)
+ create = read = update = delete = None
fields = DISPLAYED_NODE_FIELDS
@api_exported('POST')
@@ -657,10 +639,9 @@
if constraint in request_params}
-@api_operations
-class NodesHandler(BaseHandler):
+class NodesHandler(OperationsHandler):
"""Manage collection of Nodes."""
- allowed_methods = ('GET', 'POST',)
+ create = read = update = delete = None
anonymous = AnonNodesHandler
@api_exported('POST')
@@ -776,13 +757,13 @@
return ('nodes_handler', [])
-class NodeMacsHandler(BaseHandler):
+class NodeMacsHandler(OperationsHandler):
"""
Manage all the MAC addresses linked to a Node / Create a new MAC address
for a Node.
"""
- allowed_methods = ('GET', 'POST',)
+ update = delete = None
def read(self, request, system_id):
"""Read all MAC addresses related to a Node."""
@@ -803,9 +784,9 @@
return ('node_macs_handler', ['system_id'])
-class NodeMacHandler(BaseHandler):
+class NodeMacHandler(OperationsHandler):
"""Manage a MAC address linked to a Node."""
- allowed_methods = ('GET', 'DELETE')
+ create = update = None
fields = ('mac_address',)
model = MACAddress
@@ -855,8 +836,7 @@
return HttpResponse(db_file.data.read(), status=httplib.OK)
-@api_operations
-class AnonFilesHandler(AnonymousBaseHandler):
+class AnonFilesHandler(AnonymousOperationsHandler):
"""Anonymous file operations.
This is needed for Juju. The story goes something like this:
@@ -868,15 +848,14 @@
without credentials.
"""
- allowed_methods = ('GET',)
+ create = read = update = delete = None
get = api_exported('GET', exported_as='get')(get_file)
-@api_operations
-class FilesHandler(BaseHandler):
+class FilesHandler(OperationsHandler):
"""File management operations."""
- allowed_methods = ('GET', 'POST',)
+ create = read = update = delete = None
anonymous = AnonFilesHandler
get = api_exported('GET', exported_as='get')(get_file)
@@ -914,10 +893,9 @@
DISPLAYED_NODEGROUP_FIELDS = ('uuid', 'status', 'name')
-@api_operations
-class AnonNodeGroupsHandler(AnonymousBaseHandler):
+class AnonNodeGroupsHandler(AnonymousOperationsHandler):
"""Anon Node-groups API."""
- allowed_methods = ('GET', 'POST')
+ create = read = update = delete = None
fields = DISPLAYED_NODEGROUP_FIELDS
@api_exported('GET')
@@ -999,11 +977,10 @@
"Awaiting admin approval.", status=httplib.ACCEPTED)
-@api_operations
-class NodeGroupsHandler(BaseHandler):
+class NodeGroupsHandler(OperationsHandler):
"""Node-groups API."""
anonymous = AnonNodeGroupsHandler
- allowed_methods = ('GET', 'POST')
+ create = read = update = delete = None
fields = DISPLAYED_NODEGROUP_FIELDS
@api_exported('GET')
@@ -1069,11 +1046,10 @@
"Only allowed for the %r worker." % nodegroup.name)
-@api_operations
-class NodeGroupHandler(BaseHandler):
+class NodeGroupHandler(OperationsHandler):
"""Node-group API."""
- allowed_methods = ('GET', 'POST')
+ create = update = delete = None
fields = DISPLAYED_NODEGROUP_FIELDS
def read(self, request, uuid):
@@ -1106,10 +1082,9 @@
'broadcast_ip', 'ip_range_low', 'ip_range_high')
-@api_operations
-class NodeGroupInterfacesHandler(BaseHandler):
+class NodeGroupInterfacesHandler(OperationsHandler):
"""NodeGroupInterfaces API."""
- allowed_methods = ('GET', 'POST')
+ create = read = update = delete = None
fields = DISPLAYED_NODEGROUP_FIELDS
@api_exported('GET')
@@ -1156,9 +1131,9 @@
return ('nodegroupinterfaces_handler', [uuid])
-class NodeGroupInterfaceHandler(BaseHandler):
+class NodeGroupInterfaceHandler(OperationsHandler):
"""NodeGroupInterface API."""
- allowed_methods = ('GET', 'PUT')
+ create = delete = None
fields = DISPLAYED_NODEGROUP_FIELDS
def read(self, request, uuid, interface):
@@ -1212,10 +1187,9 @@
return ('nodegroupinterface_handler', [uuid, interface_name])
-@api_operations
-class AccountHandler(BaseHandler):
+class AccountHandler(OperationsHandler):
"""Manage the current logged-in user."""
- allowed_methods = ('POST',)
+ create = read = update = delete = None
@api_exported('POST')
def create_authorisation_token(self, request):
@@ -1252,10 +1226,9 @@
return ('account_handler', [])
-@api_operations
-class TagHandler(BaseHandler):
+class TagHandler(OperationsHandler):
"""Manage individual Tags."""
- allowed_methods = ('GET', 'DELETE', 'POST', 'PUT')
+ create = None
model = Tag
fields = (
'name',
@@ -1314,10 +1287,9 @@
return ('tag_handler', (tag_name, ))
-@api_operations
-class TagsHandler(BaseHandler):
+class TagsHandler(OperationsHandler):
"""Manage collection of Tags."""
- allowed_methods = ('GET', 'POST')
+ create = read = update = delete = None
@api_exported('POST')
def new(self, request):
@@ -1357,10 +1329,9 @@
raise ValidationError(form.errors)
-@api_operations
-class MAASHandler(BaseHandler):
+class MAASHandler(OperationsHandler):
"""Manage the MAAS' itself."""
- allowed_methods = ('POST', 'GET')
+ create = read = update = delete = None
@api_exported('POST')
def set_config(self, request):
@@ -1409,22 +1380,32 @@
:rtype: :class:`unicode`
"""
module = sys.modules[__name__]
- messages = [
- __doc__.strip(),
- '',
- '',
- 'Operations',
- '----------',
- '',
- ]
+ output = StringIO()
+ line = partial(print, file=output)
+
+ line(getdoc(module))
+ line()
+ line()
+ line('Operations')
+ line('----------')
+ line()
+
handlers = find_api_handlers(module)
for doc in generate_api_docs(handlers):
- for method in doc.get_methods():
- messages.append(
- "%s %s\n %s\n" % (
- method.http_name, doc.resource_uri_template,
- method.doc))
- return '\n'.join(messages)
+ uri_template = doc.resource_uri_template
+ exports = doc.handler.exports.items()
+ for (http_method, operation), function in sorted(exports):
+ line("``%s %s``" % (http_method, uri_template), end="")
+ if operation is not None:
+ line(" ``op=%s``" % operation)
+ line()
+ docstring = getdoc(function)
+ if docstring is not None:
+ for docline in docstring.splitlines():
+ line(" ", docline, sep="")
+ line()
+
+ return output.getvalue()
def reST_to_html_fragment(a_str):
@@ -1512,8 +1493,9 @@
content_type="application/json")
-@api_operations
-class BootImagesHandler(BaseHandler):
+class BootImagesHandler(OperationsHandler):
+
+ create = replace = update = delete = None
@classmethod
def resource_uri(cls):
=== modified file 'src/maasserver/apidoc.py'
--- src/maasserver/apidoc.py 2012-09-13 12:05:54 +0000
+++ src/maasserver/apidoc.py 2012-09-26 23:47:21 +0000
@@ -28,7 +28,8 @@
def find_api_handlers(module):
"""Find the API handlers defined in `module`.
- Handlers are of type :class:`HandlerMetaClass`.
+ Handlers are of type :class:`HandlerMetaClass`, and must define a
+ `resource_uri` method.
:rtype: Generator, yielding handlers.
"""
@@ -41,7 +42,8 @@
for name in names:
candidate = getattr(module, name)
if isinstance(candidate, HandlerMetaClass):
- yield candidate
+ if getattr(candidate, "resource_uri", None) is not None:
+ yield candidate
def generate_api_docs(handlers):
@@ -95,32 +97,21 @@
restful: Indicates if this is a CRUD/ReSTful action.
"""
- from maasserver.api import dispatch_methods # Avoid circular imports.
- operation_methods = getattr(handler, "_available_api_methods", {})
- for http_method in handler.allowed_methods:
- desc_base = dict(method=http_method)
- if http_method in operation_methods:
- # Default Piston CRUD method has been overridden; inspect
- # custom operations instead.
- operations = handler._available_api_methods[http_method]
- for op, func in operations.items():
- yield dict(
- desc_base, name=op, doc=getdoc(func),
- op=op, restful=False)
- else:
- # Default Piston CRUD method still stands.
- name = dispatch_methods[http_method]
- func = getattr(handler, name)
- yield dict(
- desc_base, name=name, doc=getdoc(func),
- op=None, restful=True)
+ from maasserver.api import OperationsResource
+ getname = OperationsResource.crudmap.get
+ for signature, function in handler.exports.items():
+ http_method, operation = signature
+ name = getname(http_method) if operation is None else operation
+ yield dict(
+ method=http_method, name=name, doc=getdoc(function),
+ op=operation, restful=(operation is None))
def describe_handler(handler):
"""Return a serialisable description of a handler.
- :type handler: :class:`BaseHandler` instance that has been decorated by
- `api_operations`.
+ :type handler: :class:`OperationsHandler` or
+ :class:`AnonymousOperationsHandler` instance.
"""
uri_template = generate_doc(handler).resource_uri_template
if uri_template is None:
@@ -128,8 +119,8 @@
else:
uri_template = urljoin(settings.DEFAULT_MAAS_URL, uri_template)
- view_name, uri_params, uri_kw = merge(
- handler.resource_uri(), (None, (), {}))
+ resource_uri = getattr(handler, "resource_uri", lambda: ())
+ view_name, uri_params, uri_kw = merge(resource_uri(), (None, (), {}))
assert uri_kw == {}, (
"Resource URI specifications with keyword parameters are not yet "
"supported: handler=%r; view_name=%r" % (handler, view_name))
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-09-26 10:00:14 +0000
+++ src/maasserver/tests/test_api.py 2012-09-26 23:47:21 +0000
@@ -358,7 +358,9 @@
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
self.assertIn('text/html', response['Content-Type'])
- self.assertEqual("Unknown operation.", response.content)
+ self.assertEqual(
+ "Unrecognised signature: POST None",
+ response.content)
def test_POST_fails_if_mac_duplicated(self):
# Mac Addresses should be unique.
@@ -394,7 +396,8 @@
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
self.assertEqual(
- "Unknown operation: 'invalid_operation'.", response.content)
+ "Unrecognised signature: POST invalid_operation",
+ response.content)
def test_POST_new_rejects_invalid_data(self):
# If the data provided to create a node with an invalid MAC
=== modified file 'src/maasserver/tests/test_api_mechanism.py'
--- src/maasserver/tests/test_api_mechanism.py 2012-06-25 09:03:14 +0000
+++ src/maasserver/tests/test_api_mechanism.py 2012-09-26 23:47:21 +0000
@@ -14,10 +14,7 @@
__metaclass__ = type
__all__ = []
-from maasserver.api import (
- api_exported,
- dispatch_methods,
- )
+from maasserver.api import api_exported
from maasserver.testing.factory import factory
from maastesting.testcase import TestCase
@@ -32,23 +29,6 @@
decorate = api_exported(random_method)
self.assertRaises(ValueError, decorate, lambda: None)
- def test_disallowed_methods(self):
- # HTTP methods in dispatch_methods should not be allowed.
- for method in dispatch_methods:
- decorate = api_exported(method=dispatch_methods.get(method))
- self.assertRaises(ValueError, decorate, lambda: None)
-
- def test_defaulted_names_are_checked_for_validity(self):
- # When not using exported_as, the validity check should apply to
- # the method name too.
-
- def read():
- # 'read' is prohibited for "GET"
- pass
-
- decorate = api_exported("GET")
- self.assertRaises(ValueError, decorate, read)
-
def test_valid_decoration(self):
value = "value" + factory.getRandomString()
decorate = api_exported()
=== modified file 'src/maasserver/tests/test_apidoc.py'
--- src/maasserver/tests/test_apidoc.py 2012-09-13 12:05:54 +0000
+++ src/maasserver/tests/test_apidoc.py 2012-09-26 23:47:21 +0000
@@ -17,7 +17,7 @@
from django.conf import settings
from maasserver.api import (
api_exported,
- api_operations,
+ OperationsHandler,
)
from maasserver.apidoc import (
describe_handler,
@@ -59,12 +59,22 @@
self.assertSequenceEqual(
[], list(find_api_handlers(module)))
+ def test_module_with_incomplete_handler(self):
+ # Handlers that don't have a resource_uri method are ignored.
+ module = self.make_module()
+ module.handler = BaseHandler
+ self.assertSequenceEqual(
+ [], list(find_api_handlers(module)))
+
def test_module_with_handler(self):
- # Handlers are discovered in a module and returned.
+ # Handlers with resource_uri attributes are discovered in a module and
+ # returned. The type of resource_uri is not checked; it must only be
+ # present and not None.
module = self.make_module()
- module.handler = BaseHandler
+ module.handler = type(
+ b"MetalHander", (BaseHandler,), {"resource_uri": True})
self.assertSequenceEqual(
- [BaseHandler], list(find_api_handlers(module)))
+ [module.handler], list(find_api_handlers(module)))
def test_module_with_handler_not_in_all(self):
# When __all__ is defined, only the names it defines are searched for
@@ -135,11 +145,10 @@
# describe_handler() returns a description of a handler that can be
# readily serialised into JSON, for example.
- @api_operations
- class MegadethHandler(BaseHandler):
+ class MegadethHandler(OperationsHandler):
"""The mighty 'deth."""
- allowed_methods = "GET", "POST", "PUT"
+ create = read = delete = None
@api_exported("POST")
def peace_sells_but_whos_buying(self, request, vic, rattlehead):
@@ -181,7 +190,7 @@
self.assertEqual(MegadethHandler.__doc__, observed["doc"])
self.assertEqual(MegadethHandler.__name__, observed["name"])
self.assertEqual(["vic", "rattlehead"], observed["params"])
- self.assertSequenceEqual(expected_actions, observed["actions"])
+ self.assertItemsEqual(expected_actions, observed["actions"])
def test_describe_handler_with_maas_handler(self):
# Ensure that describe_handler() yields something sensible with a
=== modified file 'src/metadataserver/api.py'
--- src/metadataserver/api.py 2012-09-24 11:44:49 +0000
+++ src/metadataserver/api.py 2012-09-26 23:47:21 +0000
@@ -24,9 +24,9 @@
from django.shortcuts import get_object_or_404
from maasserver.api import (
api_exported,
- api_operations,
extract_oauth_key,
get_mandatory_param,
+ OperationsHandler,
)
from maasserver.enum import (
NODE_STATUS,
@@ -53,7 +53,6 @@
NodeKey,
NodeUserData,
)
-from piston.handler import BaseHandler
from piston.utils import rc
@@ -131,8 +130,8 @@
raise UnknownMetadataVersion("Unknown metadata version: %s" % version)
-class MetadataViewHandler(BaseHandler):
- allowed_methods = ('GET',)
+class MetadataViewHandler(OperationsHandler):
+ create = update = delete = None
def read(self, request, mac=None):
return make_list_response(sorted(self.fields))
@@ -144,10 +143,9 @@
fields = ('latest', '2012-03-01')
-@api_operations
class VersionIndexHandler(MetadataViewHandler):
"""Listing for a given metadata version."""
- allowed_methods = ('GET', 'POST')
+ create = update = delete = None
fields = ('meta-data', 'user-data')
# States in which a node is allowed to signal commissioning status.
@@ -332,12 +330,12 @@
raise MAASAPINotFound("No user data available for this node.")
-class EnlistMetaDataHandler(BaseHandler):
+class EnlistMetaDataHandler(OperationsHandler):
"""this has to handle the 'meta-data' portion of the meta-data api
for enlistment only. It should mimic the read-only portion
of /VersionIndexHandler"""
- allowed_methods = ('GET',)
+ create = update = delete = None
data = {
'instance-id': 'i-maas-enlistment',
@@ -357,7 +355,7 @@
return make_text_response(self.data[item])
-class EnlistUserDataHandler(BaseHandler):
+class EnlistUserDataHandler(OperationsHandler):
"""User-data for the enlistment environment"""
def read(self, request, version):
@@ -365,15 +363,14 @@
return HttpResponse(get_enlist_userdata(), mimetype="text/plain")
-class EnlistVersionIndexHandler(BaseHandler):
- allowed_methods = ('GET',)
+class EnlistVersionIndexHandler(OperationsHandler):
+ create = update = delete = None
fields = ('meta-data', 'user-data')
def read(self, request, version):
return make_list_response(sorted(self.fields))
-@api_operations
class AnonMetaDataHandler(VersionIndexHandler):
"""Anonymous metadata."""
=== modified file 'src/metadataserver/urls.py'
--- src/metadataserver/urls.py 2012-08-03 15:17:01 +0000
+++ src/metadataserver/urls.py 2012-09-26 23:47:21 +0000
@@ -18,6 +18,7 @@
patterns,
url,
)
+from maasserver.api import OperationsResource
from maasserver.api_auth import api_auth
from metadataserver.api import (
AnonMetaDataHandler,
@@ -29,29 +30,32 @@
UserDataHandler,
VersionIndexHandler,
)
-from piston.resource import Resource
# Handlers for nodes requesting their own metadata.
-meta_data_handler = Resource(MetaDataHandler, authentication=api_auth)
-user_data_handler = Resource(UserDataHandler, authentication=api_auth)
-version_index_handler = Resource(VersionIndexHandler, authentication=api_auth)
-index_handler = Resource(IndexHandler, authentication=api_auth)
+meta_data_handler = OperationsResource(
+ MetaDataHandler, authentication=api_auth)
+user_data_handler = OperationsResource(
+ UserDataHandler, authentication=api_auth)
+version_index_handler = OperationsResource(
+ VersionIndexHandler, authentication=api_auth)
+index_handler = OperationsResource(
+ IndexHandler, authentication=api_auth)
# Handlers for anonymous metadata operations.
-meta_data_anon_handler = Resource(AnonMetaDataHandler)
+meta_data_anon_handler = OperationsResource(AnonMetaDataHandler)
# Handlers for UNSAFE anonymous random metadata access.
-meta_data_by_mac_handler = Resource(MetaDataHandler)
-user_data_by_mac_handler = Resource(UserDataHandler)
-version_index_by_mac_handler = Resource(VersionIndexHandler)
+meta_data_by_mac_handler = OperationsResource(MetaDataHandler)
+user_data_by_mac_handler = OperationsResource(UserDataHandler)
+version_index_by_mac_handler = OperationsResource(VersionIndexHandler)
# Handlers for the anonymous enlistment metadata service
-enlist_meta_data_handler = Resource(EnlistMetaDataHandler)
-enlist_user_data_handler = Resource(EnlistUserDataHandler)
-enlist_index_handler = Resource(IndexHandler)
-enlist_version_index_handler = Resource(EnlistVersionIndexHandler)
+enlist_meta_data_handler = OperationsResource(EnlistMetaDataHandler)
+enlist_user_data_handler = OperationsResource(EnlistUserDataHandler)
+enlist_index_handler = OperationsResource(IndexHandler)
+enlist_version_index_handler = OperationsResource(EnlistVersionIndexHandler)
# Normal metadata access, available to a node querying its own metadata.
node_patterns = patterns(