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=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 _BuildOpcodeAttributes():
52 """Builds list of attributes used for per-handler opcodes.
55 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
56 "Get%sOpInput" % method.capitalize())
57 for method in _SUPPORTED_METHODS]
60 _OPCODE_ATTRS = _BuildOpcodeAttributes()
63 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
64 """Builds a URI list as used by index resources.
66 @param ids: list of ids as strings
67 @param uri_format: format to be applied for URI
68 @param uri_fields: optional parameter for field IDs
71 (field_id, field_uri) = uri_fields
76 field_uri: uri_format % m_id,
79 # Make sure the result is sorted, makes it nicer to look at and simplifies
83 return map(_MapId, ids)
86 def ExtractField(sequence, index):
87 """Creates a list containing one column out of a list of lists.
89 @param sequence: sequence of lists
90 @param index: index of field
93 return map(lambda item: item[index], sequence)
96 def MapFields(names, data):
97 """Maps two lists into one dictionary.
100 >>> MapFields(["a", "b"], ["foo", 123])
101 {'a': 'foo', 'b': 123}
103 @param names: field names (list of strings)
104 @param data: field data (list)
107 if len(names) != len(data):
108 raise AttributeError("Names and data must have the same length")
109 return dict(zip(names, data))
112 def MapBulkFields(itemslist, fields):
113 """Map value to field name in to one dictionary.
115 @param itemslist: a list of items values
116 @param fields: a list of items names
118 @return: a list of mapped dictionaries
122 for item in itemslist:
123 mapped = MapFields(fields, item)
124 items_details.append(mapped)
128 def MakeParamsDict(opts, params):
129 """Makes params dictionary out of a option set.
131 This function returns a dictionary needed for hv or be parameters. But only
132 those fields which provided in the option set. Takes parameters frozensets
136 @param opts: selected options
137 @type params: frozenset
138 @param params: subset of options
140 @return: dictionary of options, filtered by given subset.
155 def FillOpcode(opcls, body, static, rename=None):
156 """Fills an opcode with body parameters.
158 Parameter types are checked.
160 @type opcls: L{opcodes.OpCode}
161 @param opcls: Opcode class
163 @param body: Body parameters as received from client
165 @param static: Static parameters which can't be modified by client
167 @param rename: Renamed parameters, key as old name, value as new name
168 @return: Opcode object
174 CheckType(body, dict, "Body contents")
176 # Make copy to be modified
180 for old, new in rename.items():
181 if new in params and old in params:
182 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
183 " both are specified" %
186 assert new not in params
187 params[new] = params.pop(old)
190 overwritten = set(params.keys()) & set(static.keys())
192 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
195 params.update(static)
197 # Convert keys to strings (simplejson decodes them as unicode)
198 params = dict((str(key), value) for (key, value) in params.items())
201 op = opcls(**params) # pylint: disable=W0142
203 except (errors.OpPrereqError, TypeError), err:
204 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
209 def HandleItemQueryErrors(fn, *args, **kwargs):
210 """Converts errors when querying a single item.
214 return fn(*args, **kwargs)
215 except errors.OpPrereqError, err:
216 if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
217 raise http.HttpNotFound()
223 """Feedback logging function for jobs.
225 We don't have a stdout for printing log messages, so log them to the
228 @param msg: the message
231 (_, log_type, log_msg) = msg
232 logging.info("%s: %s", log_type, log_msg)
235 def CheckType(value, exptype, descr):
236 """Abort request if value type doesn't match expected type.
240 @param exptype: Expected type
242 @param descr: Description of value
243 @return: Value (allows inline usage)
246 if not isinstance(value, exptype):
247 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
248 (descr, type(value).__name__, exptype.__name__))
253 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
254 """Check and return the value for a given parameter.
256 If no default value was given and the parameter doesn't exist in the input
257 data, an error is raise.
260 @param data: Dictionary containing input data
262 @param name: Parameter name
263 @param default: Default value (can be None)
264 @param exptype: Expected type (can be None)
270 if default is not _DEFAULT:
273 raise http.HttpBadRequest("Required parameter '%s' is missing" %
276 if exptype is _DEFAULT:
279 return CheckType(value, exptype, "'%s' parameter" % name)
282 class ResourceBase(object):
283 """Generic class for resources.
286 # Default permission requirements
288 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
289 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
290 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
292 def __init__(self, items, queryargs, req, _client_cls=luxi.Client):
293 """Generic resource constructor.
295 @param items: a list with variables encoded in the URL
296 @param queryargs: a dictionary with additional options from URL
297 @param req: Request context
298 @param _client_cls: L{luxi} client class (unittests only)
302 self.queryargs = queryargs
304 self._client_cls = _client_cls
306 def _GetRequestBody(self):
307 """Returns the body data.
310 return self._req.private.body_data
312 request_body = property(fget=_GetRequestBody)
314 def _checkIntVariable(self, name, default=0):
315 """Return the parsed value of an int argument.
318 val = self.queryargs.get(name, default)
319 if isinstance(val, list):
326 except (ValueError, TypeError):
327 raise http.HttpBadRequest("Invalid value for the"
328 " '%s' parameter" % (name,))
331 def _checkStringVariable(self, name, default=None):
332 """Return the parsed value of an int argument.
335 val = self.queryargs.get(name, default)
336 if isinstance(val, list):
343 def getBodyParameter(self, name, *args):
344 """Check and return the value for a given parameter.
346 If a second parameter is not given, an error will be returned,
347 otherwise this parameter specifies the default value.
349 @param name: the required parameter
353 return CheckParameter(self.request_body, name, default=args[0])
355 return CheckParameter(self.request_body, name)
357 def useLocking(self):
358 """Check if the request specifies locking.
361 return bool(self._checkIntVariable("lock"))
364 """Check if the request specifies bulk querying.
367 return bool(self._checkIntVariable("bulk"))
370 """Check if the request specifies a forced operation.
373 return bool(self._checkIntVariable("force"))
376 """Check if the request specifies dry-run mode.
379 return bool(self._checkIntVariable("dry-run"))
382 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
385 # Could be a function, pylint: disable=R0201
387 return self._client_cls()
388 except luxi.NoMasterError, err:
389 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
390 except luxi.PermissionError:
391 raise http.HttpInternalServerError("Internal error: no permission to"
392 " connect to the master daemon")
394 def SubmitJob(self, op, cl=None):
395 """Generic wrapper for submit job, for better http compatibility.
398 @param op: the list of opcodes for the job
399 @type cl: None or luxi.Client
400 @param cl: optional luxi client to use
406 cl = self.GetClient()
408 return cl.SubmitJob(op)
409 except errors.JobQueueFull:
410 raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
411 except errors.JobQueueDrainError:
412 raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
413 except luxi.NoMasterError, err:
414 raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
415 except luxi.PermissionError:
416 raise http.HttpInternalServerError("Internal error: no permission to"
417 " connect to the master daemon")
418 except luxi.TimeoutError, err:
419 raise http.HttpGatewayTimeout("Timeout while talking to the master"
423 def GetResourceOpcodes(cls):
424 """Returns all opcodes used by a resource.
427 return frozenset(filter(None, (getattr(cls, op_attr, None)
428 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
431 class _MetaOpcodeResource(type):
432 """Meta class for RAPI resources.
435 def __call__(mcs, *args, **kwargs):
436 """Instantiates class and patches it for use by the RAPI daemon.
439 # Access to private attributes of a client class, pylint: disable=W0212
440 obj = type.__call__(mcs, *args, **kwargs)
442 for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
443 if hasattr(obj, method):
444 # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
445 # shouldn't be (they're only used by the automatically generated
447 assert not hasattr(obj, rename_attr)
448 assert not hasattr(obj, fn_attr)
450 # Try to generate handler method on handler instance
452 opcode = getattr(obj, op_attr)
453 except AttributeError:
457 compat.partial(obj._GenericHandler, opcode,
458 getattr(obj, rename_attr, None),
459 getattr(obj, fn_attr, obj._GetDefaultData)))
464 class OpcodeResource(ResourceBase):
465 """Base class for opcode-based RAPI resources.
467 Instances of this class automatically gain handler functions through
468 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
469 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
470 method to do their own opcode input processing (e.g. for static values). The
471 C{$METHOD$_RENAME} variable defines which values are renamed (see
474 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
475 automatically generate a GET handler submitting the opcode
476 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
478 @ivar GetGetOpInput: Define this to override the default method for
479 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
481 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
482 automatically generate a PUT handler submitting the opcode
483 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
485 @ivar GetPutOpInput: Define this to override the default method for
486 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
488 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
489 automatically generate a POST handler submitting the opcode
490 @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
492 @ivar GetPostOpInput: Define this to override the default method for
493 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
495 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
496 automatically generate a GET handler submitting the opcode
497 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
499 @ivar GetDeleteOpInput: Define this to override the default method for
500 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
503 __metaclass__ = _MetaOpcodeResource
505 def _GetDefaultData(self):
506 return (self.request_body, None)
508 def _GenericHandler(self, opcode, rename, fn):
509 (body, static) = fn()
510 op = FillOpcode(opcode, body, static, rename=rename)
511 return self.SubmitJob([op])