4 # Copyright (C) 2006, 2007, 2008, 2012 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
37 from ganeti import constants
38 from ganeti import pathutils
41 # Dummy value to detect unchanged parameters
44 #: Supported HTTP methods
45 _SUPPORTED_METHODS = compat.UniqueFrozenset([
53 def _BuildOpcodeAttributes():
54 """Builds list of attributes used for per-handler opcodes.
57 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
58 "Get%sOpInput" % method.capitalize())
59 for method in _SUPPORTED_METHODS]
62 _OPCODE_ATTRS = _BuildOpcodeAttributes()
65 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
66 """Builds a URI list as used by index resources.
68 @param ids: list of ids as strings
69 @param uri_format: format to be applied for URI
70 @param uri_fields: optional parameter for field IDs
73 (field_id, field_uri) = uri_fields
78 field_uri: uri_format % m_id,
81 # Make sure the result is sorted, makes it nicer to look at and simplifies
85 return map(_MapId, ids)
88 def MapFields(names, data):
89 """Maps two lists into one dictionary.
92 >>> MapFields(["a", "b"], ["foo", 123])
93 {'a': 'foo', 'b': 123}
95 @param names: field names (list of strings)
96 @param data: field data (list)
99 if len(names) != len(data):
100 raise AttributeError("Names and data must have the same length")
101 return dict(zip(names, data))
104 def MapBulkFields(itemslist, fields):
105 """Map value to field name in to one dictionary.
107 @param itemslist: a list of items values
108 @param fields: a list of items names
110 @return: a list of mapped dictionaries
114 for item in itemslist:
115 mapped = MapFields(fields, item)
116 items_details.append(mapped)
120 def FillOpcode(opcls, body, static, rename=None):
121 """Fills an opcode with body parameters.
123 Parameter types are checked.
125 @type opcls: L{opcodes.OpCode}
126 @param opcls: Opcode class
128 @param body: Body parameters as received from client
130 @param static: Static parameters which can't be modified by client
132 @param rename: Renamed parameters, key as old name, value as new name
133 @return: Opcode object
139 CheckType(body, dict, "Body contents")
141 # Make copy to be modified
145 for old, new in rename.items():
146 if new in params and old in params:
147 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
148 " both are specified" %
151 assert new not in params
152 params[new] = params.pop(old)
155 overwritten = set(params.keys()) & set(static.keys())
157 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
160 params.update(static)
162 # Convert keys to strings (simplejson decodes them as unicode)
163 params = dict((str(key), value) for (key, value) in params.items())
166 op = opcls(**params) # pylint: disable=W0142
168 except (errors.OpPrereqError, TypeError), err:
169 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
174 def HandleItemQueryErrors(fn, *args, **kwargs):
175 """Converts errors when querying a single item.
179 return fn(*args, **kwargs)
180 except errors.OpPrereqError, err:
181 if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
182 raise http.HttpNotFound()
188 """Feedback logging function for jobs.
190 We don't have a stdout for printing log messages, so log them to the
193 @param msg: the message
196 (_, log_type, log_msg) = msg
197 logging.info("%s: %s", log_type, log_msg)
200 def CheckType(value, exptype, descr):
201 """Abort request if value type doesn't match expected type.
205 @param exptype: Expected type
207 @param descr: Description of value
208 @return: Value (allows inline usage)
211 if not isinstance(value, exptype):
212 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
213 (descr, type(value).__name__, exptype.__name__))
218 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
219 """Check and return the value for a given parameter.
221 If no default value was given and the parameter doesn't exist in the input
222 data, an error is raise.
225 @param data: Dictionary containing input data
227 @param name: Parameter name
228 @param default: Default value (can be None)
229 @param exptype: Expected type (can be None)
235 if default is not _DEFAULT:
238 raise http.HttpBadRequest("Required parameter '%s' is missing" %
241 if exptype is _DEFAULT:
244 return CheckType(value, exptype, "'%s' parameter" % name)
247 class ResourceBase(object):
248 """Generic class for resources.
251 # Default permission requirements
253 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
254 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
255 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
257 def __init__(self, items, queryargs, req, _client_cls=None):
258 """Generic resource constructor.
260 @param items: a list with variables encoded in the URL
261 @param queryargs: a dictionary with additional options from URL
262 @param req: Request context
263 @param _client_cls: L{luxi} client class (unittests only)
267 self.queryargs = queryargs
270 if _client_cls is None:
271 _client_cls = luxi.Client
273 self._client_cls = _client_cls
275 def _GetRequestBody(self):
276 """Returns the body data.
279 return self._req.private.body_data
281 request_body = property(fget=_GetRequestBody)
283 def _checkIntVariable(self, name, default=0):
284 """Return the parsed value of an int argument.
287 val = self.queryargs.get(name, default)
288 if isinstance(val, list):
295 except (ValueError, TypeError):
296 raise http.HttpBadRequest("Invalid value for the"
297 " '%s' parameter" % (name,))
300 def _checkStringVariable(self, name, default=None):
301 """Return the parsed value of an int argument.
304 val = self.queryargs.get(name, default)
305 if isinstance(val, list):
312 def getBodyParameter(self, name, *args):
313 """Check and return the value for a given parameter.
315 If a second parameter is not given, an error will be returned,
316 otherwise this parameter specifies the default value.
318 @param name: the required parameter
322 return CheckParameter(self.request_body, name, default=args[0])
324 return CheckParameter(self.request_body, name)
326 def useLocking(self):
327 """Check if the request specifies locking.
330 return bool(self._checkIntVariable("lock"))
333 """Check if the request specifies bulk querying.
336 return bool(self._checkIntVariable("bulk"))
339 """Check if the request specifies a forced operation.
342 return bool(self._checkIntVariable("force"))
345 """Check if the request specifies dry-run mode.
348 return bool(self._checkIntVariable("dry-run"))
350 def GetClient(self, query=False):
351 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
353 @param query: this signifies that the client will only be used for
354 queries; if the build-time parameter enable-split-queries is
355 enabled, then the client will be connected to the query socket
356 instead of the masterd socket
359 if query and constants.ENABLE_SPLIT_QUERY:
360 address = pathutils.QUERY_SOCKET
363 # Could be a function, pylint: disable=R0201
365 return self._client_cls(address=address)
366 except luxi.NoMasterError, err:
367 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
368 except luxi.PermissionError:
369 raise http.HttpInternalServerError("Internal error: no permission to"
370 " connect to the master daemon")
372 def SubmitJob(self, op, cl=None):
373 """Generic wrapper for submit job, for better http compatibility.
376 @param op: the list of opcodes for the job
377 @type cl: None or luxi.Client
378 @param cl: optional luxi client to use
384 cl = self.GetClient()
386 return cl.SubmitJob(op)
387 except errors.JobQueueFull:
388 raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
389 except errors.JobQueueDrainError:
390 raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
391 except luxi.NoMasterError, err:
392 raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
393 except luxi.PermissionError:
394 raise http.HttpInternalServerError("Internal error: no permission to"
395 " connect to the master daemon")
396 except luxi.TimeoutError, err:
397 raise http.HttpGatewayTimeout("Timeout while talking to the master"
401 def GetResourceOpcodes(cls):
402 """Returns all opcodes used by a resource.
405 return frozenset(filter(None, (getattr(cls, op_attr, None)
406 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
409 class _MetaOpcodeResource(type):
410 """Meta class for RAPI resources.
413 def __call__(mcs, *args, **kwargs):
414 """Instantiates class and patches it for use by the RAPI daemon.
417 # Access to private attributes of a client class, pylint: disable=W0212
418 obj = type.__call__(mcs, *args, **kwargs)
420 for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
421 if hasattr(obj, method):
422 # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
423 # shouldn't be (they're only used by the automatically generated
425 assert not hasattr(obj, rename_attr)
426 assert not hasattr(obj, fn_attr)
428 # Try to generate handler method on handler instance
430 opcode = getattr(obj, op_attr)
431 except AttributeError:
435 compat.partial(obj._GenericHandler, opcode,
436 getattr(obj, rename_attr, None),
437 getattr(obj, fn_attr, obj._GetDefaultData)))
442 class OpcodeResource(ResourceBase):
443 """Base class for opcode-based RAPI resources.
445 Instances of this class automatically gain handler functions through
446 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
447 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
448 method to do their own opcode input processing (e.g. for static values). The
449 C{$METHOD$_RENAME} variable defines which values are renamed (see
450 L{baserlib.FillOpcode}).
452 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
453 automatically generate a GET handler submitting the opcode
454 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
455 L{baserlib.FillOpcode})
456 @ivar GetGetOpInput: Define this to override the default method for
457 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
459 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
460 automatically generate a PUT handler submitting the opcode
461 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
462 L{baserlib.FillOpcode})
463 @ivar GetPutOpInput: Define this to override the default method for
464 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
466 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
467 automatically generate a POST handler submitting the opcode
468 @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
469 L{baserlib.FillOpcode})
470 @ivar GetPostOpInput: Define this to override the default method for
471 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
473 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
474 automatically generate a GET handler submitting the opcode
475 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
476 L{baserlib.FillOpcode})
477 @ivar GetDeleteOpInput: Define this to override the default method for
478 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
481 __metaclass__ = _MetaOpcodeResource
483 def _GetDefaultData(self):
484 return (self.request_body, None)
486 def _GenericHandler(self, opcode, rename, fn):
487 (body, static) = fn()
488 op = FillOpcode(opcode, body, static, rename=rename)
489 return self.SubmitJob([op])