181
193
from maasserver.utils.orm import get_one
194
from metadataserver.fields import Bin
195
from metadataserver.models import (
197
NodeCommissionResult,
182
199
from piston.emitters import JSONEmitter
183
from piston.handler import (
184
AnonymousBaseHandler,
189
from piston.models import Token
190
from piston.resource import Resource
200
from piston.handler import typemapper
191
201
from piston.utils import rc
192
202
from provisioningserver.enum import POWER_TYPE
193
203
from provisioningserver.kernel_opts import KernelParameters
194
204
import simplejson as json
197
class OperationsResource(Resource):
198
"""A resource supporting operation dispatch.
200
All requests are passed onto the handler's `dispatch` method. See
201
:class:`OperationsHandler`.
204
crudmap = Resource.callmap
205
callmap = dict.fromkeys(crudmap, "dispatch")
208
class RestrictedResource(OperationsResource):
210
def authenticate(self, request, rm):
211
actor, anonymous = super(
212
RestrictedResource, self).authenticate(request, rm)
213
if not anonymous and not request.user.is_active:
214
raise PermissionDenied("User is not allowed access to this API.")
216
return actor, anonymous
219
class AdminRestrictedResource(RestrictedResource):
221
def authenticate(self, request, rm):
222
actor, anonymous = super(
223
AdminRestrictedResource, self).authenticate(request, rm)
224
if anonymous or not request.user.is_superuser:
225
raise PermissionDenied("User is not allowed access to this API.")
227
return actor, anonymous
230
def operation(idempotent, exported_as=None):
231
"""Decorator to make a method available on the API.
233
:param idempotent: If this operation is idempotent. Idempotent operations
234
are made available via HTTP GET, non-idempotent operations via HTTP
236
:param exported_as: Optional operation name; defaults to the name of the
239
method = "GET" if idempotent else "POST"
241
def _decorator(func):
242
if exported_as is None:
243
func.export = method, func.__name__
245
func.export = method, exported_as
251
class OperationsHandlerType(HandlerMetaClass):
252
"""Type for handlers that dispatch operations.
254
Collects all the exported operations, CRUD and custom, into the class's
255
`exports` attribute. This is a signature:function mapping, where signature
256
is an (http-method, operation-name) tuple. If operation-name is None, it's
259
The `allowed_methods` attribute is calculated as the union of all HTTP
260
methods required for the exported CRUD and custom operations.
263
def __new__(metaclass, name, bases, namespace):
264
cls = super(OperationsHandlerType, metaclass).__new__(
265
metaclass, name, bases, namespace)
267
# Create a signature:function mapping for CRUD operations.
269
(http_method, None): getattr(cls, method)
270
for http_method, method in OperationsResource.crudmap.items()
271
if getattr(cls, method, None) is not None
274
# Create a signature:function mapping for non-CRUD operations.
276
attribute.export: attribute
277
for attribute in vars(cls).values()
278
if getattr(attribute, "export", None) is not None
281
# Create the exports mapping.
284
exports.update(operations)
287
cls.exports = exports
288
cls.allowed_methods = frozenset(
289
http_method for http_method, name in exports)
294
class OperationsHandlerMixin:
295
"""Handler mixin for operations dispatch.
297
This enabled dispatch to custom functions that piggyback on HTTP methods
298
that ordinarily, in Piston, are used for CRUD operations.
300
This must be used in cooperation with :class:`OperationsResource` and
301
:class:`OperationsHandlerType`.
304
def dispatch(self, request, *args, **kwargs):
305
signature = request.method.upper(), request.REQUEST.get("op")
306
function = self.exports.get(signature)
308
return HttpResponseBadRequest(
309
"Unrecognised signature: %s %s" % signature)
311
return function(self, request, *args, **kwargs)
314
class OperationsHandler(
315
OperationsHandlerMixin, BaseHandler):
316
"""Base handler that supports operation dispatch."""
318
__metaclass__ = OperationsHandlerType
321
class AnonymousOperationsHandler(
322
OperationsHandlerMixin, AnonymousBaseHandler):
323
"""Anonymous base handler that supports operation dispatch."""
325
__metaclass__ = OperationsHandlerType
328
def get_mandatory_param(data, key, validator=None):
329
"""Get the parameter from the provided data dict or raise a ValidationError
330
if this parameter is not present.
332
:param data: The data dict (usually request.data or request.GET where
333
request is a django.http.HttpRequest).
335
:param key: The parameter's key.
336
:type key: basestring
337
:param validator: An optional validator that will be used to validate the
339
:type validator: formencode.validators.Validator
340
:return: The value of the parameter.
341
:raises: ValidationError
343
value = data.get(key, None)
345
raise ValidationError("No provided %s!" % key)
346
if validator is not None:
348
return validator.to_python(value)
350
raise ValidationError("Invalid %s: %s" % (key, e.msg))
355
def get_optional_list(data, key, default=None):
356
"""Get the list from the provided data dict or return a default value.
358
value = data.getlist(key)
365
def get_list_from_dict_or_multidict(data, key, default=None):
366
"""Get a list from 'data'.
368
If data is a MultiDict, then we use 'getlist' if the data is a plain dict,
369
then we just use __getitem__.
371
The rationale is that data POSTed as multipart/form-data gets parsed into a
372
MultiDict, but data POSTed as application/json gets parsed into a plain
375
getlist = getattr(data, 'getlist', None)
376
if getlist is not None:
377
return get_optional_list(data, key, default)
378
return data.get(key, default)
381
def extract_oauth_key_from_auth_header(auth_data):
382
"""Extract the oauth key from auth data in HTTP header.
384
:param auth_data: {string} The HTTP Authorization header.
386
:return: The oauth key from the header, or None.
388
for entry in auth_data.split():
389
key_value = entry.split('=', 1)
390
if len(key_value) == 2:
391
key, value = key_value
392
if key == 'oauth_token':
393
return value.rstrip(',').strip('"')
397
def extract_oauth_key(request):
398
"""Extract the oauth key from a request's headers.
400
Raises :class:`Unauthorized` if no key is found.
402
auth_header = request.META.get('HTTP_AUTHORIZATION')
403
if auth_header is None:
404
raise Unauthorized("No authorization header received.")
405
key = extract_oauth_key_from_auth_header(auth_header)
407
raise Unauthorized("Did not find request's oauth token.")
411
def get_oauth_token(request):
412
"""Get the OAuth :class:`piston.models.Token` used for `request`.
414
Raises :class:`Unauthorized` if no key is found, or if the token is
418
return Token.objects.get(key=extract_oauth_key(request))
419
except Token.DoesNotExist:
420
raise Unauthorized("Unknown OAuth token.")
423
def get_overrided_query_dict(defaults, data):
424
"""Returns a QueryDict with the values of 'defaults' overridden by the
427
:param defaults: The dictionary containing the default values.
429
:param data: The data used to override the defaults.
430
:type data: :class:`django.http.QueryDict`
431
:return: The updated QueryDict.
432
:raises: :class:`django.http.QueryDict`
434
# Create a writable query dict.
435
new_data = QueryDict('').copy()
436
# Missing fields will be taken from the node's current values. This
437
# is to circumvent Django's ModelForm (form created from a model)
438
# default behaviour that requires all the fields to be defined.
439
new_data.update(defaults)
440
# We can't use update here because data is a QueryDict and 'update'
441
# does not replaces the old values with the new as one would expect.
442
for k, v in data.items():
447
206
# Node's fields exposed on the API.
448
207
DISPLAYED_NODE_FIELDS = (
2145
1939
return HttpResponse("OK")
1942
def get_content_parameter(request):
1943
"""Get the "content" parameter from a CommissioningScript POST or PUT."""
1944
content_file = get_mandatory_param(request.FILES, 'content')
1945
return content_file.read()
1948
class CommissioningScriptsHandler(OperationsHandler):
1949
"""Manage custom commissioning scripts.
1951
This functionality is only available to administrators.
1954
update = delete = None
1956
def read(self, request):
1957
"""List commissioning scripts."""
1960
for script in CommissioningScript.objects.all().order_by('name')]
1962
def create(self, request):
1963
"""Create a new commissioning script.
1965
Each commissioning script is identified by a unique name.
1967
By convention the name should consist of a two-digit number, a dash,
1968
and a brief descriptive identifier consisting only of ASCII
1969
characters. You don't need to follow this convention, but not doing
1970
so opens you up to risks w.r.t. encoding and ordering. The name must
1971
not contain any whitespace, quotes, or apostrophes.
1973
A commissioning node will run each of the scripts in lexicographical
1974
order. There are no promises about how non-ASCII characters are
1975
sorted, or even how upper-case letters are sorted relative to
1976
lower-case letters. So where ordering matters, use unique numbers.
1978
Scripts built into MAAS will have names starting with "00-maas" or
1979
"99-maas" to ensure that they run first or last, respectively.
1981
Usually a commissioning script will be just that, a script. Ideally a
1982
script should be ASCII text to avoid any confusion over encoding. But
1983
in some cases a commissioning script might consist of a binary tool
1984
provided by a hardware vendor. Either way, the script gets passed to
1985
the commissioning node in the exact form in which it was uploaded.
1987
:param name: Unique identifying name for the script. Names should
1988
follow the pattern of "25-burn-in-hard-disk" (all ASCII, and with
1989
numbers greater than zero, and generally no "weird" characters).
1990
:param content: A script file, to be uploaded in binary form. Note:
1991
this is not a normal parameter, but a file upload. Its filename
1992
is ignored; MAAS will know it by the name you pass to the request.
1994
name = get_mandatory_param(request.data, 'name')
1995
content = Bin(get_content_parameter(request))
1996
return CommissioningScript.objects.create(name=name, content=content)
1999
def resource_uri(cls):
2000
return ('commissioning_scripts_handler', [])
2003
class CommissioningScriptHandler(OperationsHandler):
2004
"""Manage a custom commissioning script.
2006
This functionality is only available to administrators.
2009
model = CommissioningScript
2010
fields = ('name', 'content')
2012
# Relies on Piston's built-in DELETE implementation. There is no POST.
2015
def read(self, request, name):
2016
"""Read a commissioning script."""
2017
script = get_object_or_404(CommissioningScript, name=name)
2018
return HttpResponse(script.content, content_type='application/binary')
2020
def delete(self, request, name):
2021
"""Delete a commissioning script."""
2022
script = get_object_or_404(CommissioningScript, name=name)
2026
def update(self, request, name):
2027
"""Update a commissioning script."""
2028
content = Bin(get_content_parameter(request))
2029
script = get_object_or_404(CommissioningScript, name=name)
2030
script.content = content
2034
def resource_uri(cls, script=None):
2035
# See the comment in NodeHandler.resource_uri
2036
script_name = 'name'
2037
if script is not None:
2038
script_name = script.name
2039
return ('commissioning_script_handler', (script_name, ))
2042
class CommissioningResultsHandler(OperationsHandler):
2043
"""Read the collection of NodeCommissionResult in the MAAS."""
2044
create = read = update = delete = None
2046
model = NodeCommissionResult
2047
fields = ('name', 'script_result', 'updated', 'created', 'node', 'data')
2049
@operation(idempotent=True)
2050
def list(self, request):
2051
"""List NodeCommissionResult visible to the user, optionally filtered.
2053
:param system_id: An optional list of system ids. Only the
2054
commissioning results related to the nodes with these system ids
2056
:type system_id: iterable
2057
:param name: An optional list of names. Only the commissioning
2058
results with the specified names will be returned.
2059
:type name: iterable
2061
# Get filters from request.
2062
system_ids = get_optional_list(request.GET, 'system_id')
2063
names = get_optional_list(request.GET, 'name')
2064
nodes = Node.objects.get_nodes(
2065
request.user, NODE_PERMISSION.VIEW, ids=system_ids)
2066
results = NodeCommissionResult.objects.filter(node_id__in=nodes)
2067
if names is not None:
2068
results = results.filter(name__in=names)
2072
def resource_uri(cls, result=None):
2073
return ('commissioning_results_handler', [])
2148
2076
def describe(request):
2149
2077
"""Return a description of the whole MAAS API.