4 # Copyright (C) 2006, 2007, 2008 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Remote API base resources library.
26 # pylint: disable-msg=C0103
28 # C0103: Invalid name, since the R_* names are not conforming
32 from ganeti import luxi
33 from ganeti import rapi
34 from ganeti import http
35 from ganeti import errors
36 from ganeti import compat
39 # Dummy value to detect unchanged parameters
42 #: Supported HTTP methods
43 _SUPPORTED_METHODS = frozenset([
51 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
52 """Builds a URI list as used by index resources.
54 @param ids: list of ids as strings
55 @param uri_format: format to be applied for URI
56 @param uri_fields: optional parameter for field IDs
59 (field_id, field_uri) = uri_fields
64 field_uri: uri_format % m_id,
67 # Make sure the result is sorted, makes it nicer to look at and simplifies
71 return map(_MapId, ids)
74 def ExtractField(sequence, index):
75 """Creates a list containing one column out of a list of lists.
77 @param sequence: sequence of lists
78 @param index: index of field
81 return map(lambda item: item[index], sequence)
84 def MapFields(names, data):
85 """Maps two lists into one dictionary.
88 >>> MapFields(["a", "b"], ["foo", 123])
89 {'a': 'foo', 'b': 123}
91 @param names: field names (list of strings)
92 @param data: field data (list)
95 if len(names) != len(data):
96 raise AttributeError("Names and data must have the same length")
97 return dict(zip(names, data))
100 def MapBulkFields(itemslist, fields):
101 """Map value to field name in to one dictionary.
103 @param itemslist: a list of items values
104 @param fields: a list of items names
106 @return: a list of mapped dictionaries
110 for item in itemslist:
111 mapped = MapFields(fields, item)
112 items_details.append(mapped)
116 def MakeParamsDict(opts, params):
117 """Makes params dictionary out of a option set.
119 This function returns a dictionary needed for hv or be parameters. But only
120 those fields which provided in the option set. Takes parameters frozensets
124 @param opts: selected options
125 @type params: frozenset
126 @param params: subset of options
128 @return: dictionary of options, filtered by given subset.
143 def FillOpcode(opcls, body, static, rename=None):
144 """Fills an opcode with body parameters.
146 Parameter types are checked.
148 @type opcls: L{opcodes.OpCode}
149 @param opcls: Opcode class
151 @param body: Body parameters as received from client
153 @param static: Static parameters which can't be modified by client
155 @param rename: Renamed parameters, key as old name, value as new name
156 @return: Opcode object
162 CheckType(body, dict, "Body contents")
164 # Make copy to be modified
168 for old, new in rename.items():
169 if new in params and old in params:
170 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
171 " both are specified" %
174 assert new not in params
175 params[new] = params.pop(old)
178 overwritten = set(params.keys()) & set(static.keys())
180 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
183 params.update(static)
185 # Convert keys to strings (simplejson decodes them as unicode)
186 params = dict((str(key), value) for (key, value) in params.items())
189 op = opcls(**params) # pylint: disable-msg=W0142
191 except (errors.OpPrereqError, TypeError), err:
192 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
197 def HandleItemQueryErrors(fn, *args, **kwargs):
198 """Converts errors when querying a single item.
202 return fn(*args, **kwargs)
203 except errors.OpPrereqError, err:
204 if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
205 raise http.HttpNotFound()
211 """Feedback logging function for jobs.
213 We don't have a stdout for printing log messages, so log them to the
216 @param msg: the message
219 (_, log_type, log_msg) = msg
220 logging.info("%s: %s", log_type, log_msg)
223 def CheckType(value, exptype, descr):
224 """Abort request if value type doesn't match expected type.
228 @param exptype: Expected type
230 @param descr: Description of value
231 @return: Value (allows inline usage)
234 if not isinstance(value, exptype):
235 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
236 (descr, type(value).__name__, exptype.__name__))
241 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
242 """Check and return the value for a given parameter.
244 If no default value was given and the parameter doesn't exist in the input
245 data, an error is raise.
248 @param data: Dictionary containing input data
250 @param name: Parameter name
251 @param default: Default value (can be None)
252 @param exptype: Expected type (can be None)
258 if default is not _DEFAULT:
261 raise http.HttpBadRequest("Required parameter '%s' is missing" %
264 if exptype is _DEFAULT:
267 return CheckType(value, exptype, "'%s' parameter" % name)
270 class ResourceBase(object):
271 """Generic class for resources.
274 # Default permission requirements
276 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
277 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
278 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
280 def __init__(self, items, queryargs, req, _client_cls=luxi.Client):
281 """Generic resource constructor.
283 @param items: a list with variables encoded in the URL
284 @param queryargs: a dictionary with additional options from URL
285 @param req: Request context
286 @param _client_cls: L{luxi} client class (unittests only)
290 self.queryargs = queryargs
292 self._client_cls = _client_cls
294 def _GetRequestBody(self):
295 """Returns the body data.
298 return self._req.private.body_data
300 request_body = property(fget=_GetRequestBody)
302 def _checkIntVariable(self, name, default=0):
303 """Return the parsed value of an int argument.
306 val = self.queryargs.get(name, default)
307 if isinstance(val, list):
314 except (ValueError, TypeError):
315 raise http.HttpBadRequest("Invalid value for the"
316 " '%s' parameter" % (name,))
319 def _checkStringVariable(self, name, default=None):
320 """Return the parsed value of an int argument.
323 val = self.queryargs.get(name, default)
324 if isinstance(val, list):
331 def getBodyParameter(self, name, *args):
332 """Check and return the value for a given parameter.
334 If a second parameter is not given, an error will be returned,
335 otherwise this parameter specifies the default value.
337 @param name: the required parameter
341 return CheckParameter(self.request_body, name, default=args[0])
343 return CheckParameter(self.request_body, name)
345 def useLocking(self):
346 """Check if the request specifies locking.
349 return bool(self._checkIntVariable("lock"))
352 """Check if the request specifies bulk querying.
355 return bool(self._checkIntVariable("bulk"))
358 """Check if the request specifies a forced operation.
361 return bool(self._checkIntVariable("force"))
364 """Check if the request specifies dry-run mode.
367 return bool(self._checkIntVariable("dry-run"))
370 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
373 # Could be a function, pylint: disable=R0201
375 return self._client_cls()
376 except luxi.NoMasterError, err:
377 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
378 except luxi.PermissionError:
379 raise http.HttpInternalServerError("Internal error: no permission to"
380 " connect to the master daemon")
382 def SubmitJob(self, op, cl=None):
383 """Generic wrapper for submit job, for better http compatibility.
386 @param op: the list of opcodes for the job
387 @type cl: None or luxi.Client
388 @param cl: optional luxi client to use
394 cl = self.GetClient()
396 return cl.SubmitJob(op)
397 except errors.JobQueueFull:
398 raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
399 except errors.JobQueueDrainError:
400 raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
401 except luxi.NoMasterError, err:
402 raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
403 except luxi.PermissionError:
404 raise http.HttpInternalServerError("Internal error: no permission to"
405 " connect to the master daemon")
406 except luxi.TimeoutError, err:
407 raise http.HttpGatewayTimeout("Timeout while talking to the master"
411 class _MetaOpcodeResource(type):
412 """Meta class for RAPI resources.
415 _ATTRS = [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
416 "Get%sOpInput" % method.capitalize())
417 for method in _SUPPORTED_METHODS]
419 def __call__(mcs, *args, **kwargs):
420 """Instantiates class and patches it for use by the RAPI daemon.
423 # Access to private attributes of a client class, pylint: disable=W0212
424 obj = type.__call__(mcs, *args, **kwargs)
426 for (method, op_attr, rename_attr, fn_attr) in mcs._ATTRS:
428 opcode = getattr(obj, op_attr)
429 except AttributeError:
430 # If the "*_OPCODE" attribute isn't set, "*_RENAME" or "Get*OpInput"
432 assert not hasattr(obj, rename_attr)
433 assert not hasattr(obj, fn_attr)
436 assert not hasattr(obj, method)
438 # Generate handler method on handler instance
440 compat.partial(obj._GenericHandler, opcode,
441 getattr(obj, rename_attr, None),
442 getattr(obj, fn_attr, obj._GetDefaultData)))
447 class OpcodeResource(ResourceBase):
448 """Base class for opcode-based RAPI resources.
450 Instances of this class automatically gain handler functions through
451 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
452 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
453 method to do their own opcode input processing (e.g. for static values). The
454 C{$METHOD$_RENAME} variable defines which values are renamed (see
457 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
458 automatically generate a GET handler submitting the opcode
459 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
461 @ivar GetGetOpInput: Define this to override the default method for
462 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
464 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
465 automatically generate a PUT handler submitting the opcode
466 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
468 @ivar GetPutOpInput: Define this to override the default method for
469 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
471 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
472 automatically generate a POST handler submitting the opcode
473 @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
475 @ivar GetPostOpInput: Define this to override the default method for
476 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
478 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
479 automatically generate a GET handler submitting the opcode
480 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
482 @ivar GetDeleteOpInput: Define this to override the default method for
483 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
486 __metaclass__ = _MetaOpcodeResource
488 def _GetDefaultData(self):
489 return (self.request_body, None)
491 def _GenericHandler(self, opcode, rename, fn):
492 (body, static) = fn()
493 op = FillOpcode(opcode, body, static, rename=rename)
494 return self.SubmitJob([op])