Helper to retrieve access permissions for RAPI resource
[ganeti-local] / lib / rapi / baserlib.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2012 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Remote API base resources library.
23
24 """
25
26 # pylint: disable=C0103
27
28 # C0103: Invalid name, since the R_* names are not conforming
29
30 import logging
31
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
39
40
41 # Dummy value to detect unchanged parameters
42 _DEFAULT = object()
43
44 #: Supported HTTP methods
45 _SUPPORTED_METHODS = compat.UniqueFrozenset([
46   http.HTTP_DELETE,
47   http.HTTP_GET,
48   http.HTTP_POST,
49   http.HTTP_PUT,
50   ])
51
52
53 def _BuildOpcodeAttributes():
54   """Builds list of attributes used for per-handler opcodes.
55
56   """
57   return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
58            "Get%sOpInput" % method.capitalize())
59           for method in _SUPPORTED_METHODS]
60
61
62 _OPCODE_ATTRS = _BuildOpcodeAttributes()
63
64
65 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
66   """Builds a URI list as used by index resources.
67
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
71
72   """
73   (field_id, field_uri) = uri_fields
74
75   def _MapId(m_id):
76     return {
77       field_id: m_id,
78       field_uri: uri_format % m_id,
79       }
80
81   # Make sure the result is sorted, makes it nicer to look at and simplifies
82   # unittests.
83   ids.sort()
84
85   return map(_MapId, ids)
86
87
88 def MapFields(names, data):
89   """Maps two lists into one dictionary.
90
91   Example::
92       >>> MapFields(["a", "b"], ["foo", 123])
93       {'a': 'foo', 'b': 123}
94
95   @param names: field names (list of strings)
96   @param data: field data (list)
97
98   """
99   if len(names) != len(data):
100     raise AttributeError("Names and data must have the same length")
101   return dict(zip(names, data))
102
103
104 def MapBulkFields(itemslist, fields):
105   """Map value to field name in to one dictionary.
106
107   @param itemslist: a list of items values
108   @param fields: a list of items names
109
110   @return: a list of mapped dictionaries
111
112   """
113   items_details = []
114   for item in itemslist:
115     mapped = MapFields(fields, item)
116     items_details.append(mapped)
117   return items_details
118
119
120 def FillOpcode(opcls, body, static, rename=None):
121   """Fills an opcode with body parameters.
122
123   Parameter types are checked.
124
125   @type opcls: L{opcodes.OpCode}
126   @param opcls: Opcode class
127   @type body: dict
128   @param body: Body parameters as received from client
129   @type static: dict
130   @param static: Static parameters which can't be modified by client
131   @type rename: dict
132   @param rename: Renamed parameters, key as old name, value as new name
133   @return: Opcode object
134
135   """
136   if body is None:
137     params = {}
138   else:
139     CheckType(body, dict, "Body contents")
140
141     # Make copy to be modified
142     params = body.copy()
143
144   if rename:
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" %
149                                   (old, new))
150       if old in params:
151         assert new not in params
152         params[new] = params.pop(old)
153
154   if static:
155     overwritten = set(params.keys()) & set(static.keys())
156     if overwritten:
157       raise http.HttpBadRequest("Can't overwrite static parameters %r" %
158                                 overwritten)
159
160     params.update(static)
161
162   # Convert keys to strings (simplejson decodes them as unicode)
163   params = dict((str(key), value) for (key, value) in params.items())
164
165   try:
166     op = opcls(**params) # pylint: disable=W0142
167     op.Validate(False)
168   except (errors.OpPrereqError, TypeError), err:
169     raise http.HttpBadRequest("Invalid body parameters: %s" % err)
170
171   return op
172
173
174 def HandleItemQueryErrors(fn, *args, **kwargs):
175   """Converts errors when querying a single item.
176
177   """
178   try:
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()
183
184     raise
185
186
187 def FeedbackFn(msg):
188   """Feedback logging function for jobs.
189
190   We don't have a stdout for printing log messages, so log them to the
191   http log at least.
192
193   @param msg: the message
194
195   """
196   (_, log_type, log_msg) = msg
197   logging.info("%s: %s", log_type, log_msg)
198
199
200 def CheckType(value, exptype, descr):
201   """Abort request if value type doesn't match expected type.
202
203   @param value: Value
204   @type exptype: type
205   @param exptype: Expected type
206   @type descr: string
207   @param descr: Description of value
208   @return: Value (allows inline usage)
209
210   """
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__))
214
215   return value
216
217
218 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
219   """Check and return the value for a given parameter.
220
221   If no default value was given and the parameter doesn't exist in the input
222   data, an error is raise.
223
224   @type data: dict
225   @param data: Dictionary containing input data
226   @type name: string
227   @param name: Parameter name
228   @param default: Default value (can be None)
229   @param exptype: Expected type (can be None)
230
231   """
232   try:
233     value = data[name]
234   except KeyError:
235     if default is not _DEFAULT:
236       return default
237
238     raise http.HttpBadRequest("Required parameter '%s' is missing" %
239                               name)
240
241   if exptype is _DEFAULT:
242     return value
243
244   return CheckType(value, exptype, "'%s' parameter" % name)
245
246
247 class ResourceBase(object):
248   """Generic class for resources.
249
250   """
251   # Default permission requirements
252   GET_ACCESS = []
253   PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
254   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
255   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
256
257   def __init__(self, items, queryargs, req, _client_cls=None):
258     """Generic resource constructor.
259
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)
264
265     """
266     self.items = items
267     self.queryargs = queryargs
268     self._req = req
269
270     if _client_cls is None:
271       _client_cls = luxi.Client
272
273     self._client_cls = _client_cls
274
275   def _GetRequestBody(self):
276     """Returns the body data.
277
278     """
279     return self._req.private.body_data
280
281   request_body = property(fget=_GetRequestBody)
282
283   def _checkIntVariable(self, name, default=0):
284     """Return the parsed value of an int argument.
285
286     """
287     val = self.queryargs.get(name, default)
288     if isinstance(val, list):
289       if val:
290         val = val[0]
291       else:
292         val = default
293     try:
294       val = int(val)
295     except (ValueError, TypeError):
296       raise http.HttpBadRequest("Invalid value for the"
297                                 " '%s' parameter" % (name,))
298     return val
299
300   def _checkStringVariable(self, name, default=None):
301     """Return the parsed value of a string argument.
302
303     """
304     val = self.queryargs.get(name, default)
305     if isinstance(val, list):
306       if val:
307         val = val[0]
308       else:
309         val = default
310     return val
311
312   def getBodyParameter(self, name, *args):
313     """Check and return the value for a given parameter.
314
315     If a second parameter is not given, an error will be returned,
316     otherwise this parameter specifies the default value.
317
318     @param name: the required parameter
319
320     """
321     if args:
322       return CheckParameter(self.request_body, name, default=args[0])
323
324     return CheckParameter(self.request_body, name)
325
326   def useLocking(self):
327     """Check if the request specifies locking.
328
329     """
330     return bool(self._checkIntVariable("lock"))
331
332   def useBulk(self):
333     """Check if the request specifies bulk querying.
334
335     """
336     return bool(self._checkIntVariable("bulk"))
337
338   def useForce(self):
339     """Check if the request specifies a forced operation.
340
341     """
342     return bool(self._checkIntVariable("force"))
343
344   def dryRun(self):
345     """Check if the request specifies dry-run mode.
346
347     """
348     return bool(self._checkIntVariable("dry-run"))
349
350   def GetClient(self, query=False):
351     """Wrapper for L{luxi.Client} with HTTP-specific error handling.
352
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
357
358     """
359     if query and constants.ENABLE_SPLIT_QUERY:
360       address = pathutils.QUERY_SOCKET
361     else:
362       address = None
363     # Could be a function, pylint: disable=R0201
364     try:
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")
371
372   def SubmitJob(self, op, cl=None):
373     """Generic wrapper for submit job, for better http compatibility.
374
375     @type op: list
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
379     @rtype: string
380     @return: the job ID
381
382     """
383     if cl is None:
384       cl = self.GetClient()
385     try:
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"
398                                     " daemon: %s" % err)
399
400
401 def GetResourceOpcodes(cls):
402   """Returns all opcodes used by a resource.
403
404   """
405   return frozenset(filter(None, (getattr(cls, op_attr, None)
406                                  for (_, op_attr, _, _) in _OPCODE_ATTRS)))
407
408
409 def GetHandlerAccess(handler, method):
410   """Returns the access rights for a method on a handler.
411
412   @type handler: L{ResourceBase}
413   @type method: string
414   @rtype: string or None
415
416   """
417   return getattr(handler, "%s_ACCESS" % method, None)
418
419
420 class _MetaOpcodeResource(type):
421   """Meta class for RAPI resources.
422
423   """
424   def __call__(mcs, *args, **kwargs):
425     """Instantiates class and patches it for use by the RAPI daemon.
426
427     """
428     # Access to private attributes of a client class, pylint: disable=W0212
429     obj = type.__call__(mcs, *args, **kwargs)
430
431     for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
432       if hasattr(obj, method):
433         # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
434         # shouldn't be (they're only used by the automatically generated
435         # handler)
436         assert not hasattr(obj, rename_attr)
437         assert not hasattr(obj, fn_attr)
438       else:
439         # Try to generate handler method on handler instance
440         try:
441           opcode = getattr(obj, op_attr)
442         except AttributeError:
443           pass
444         else:
445           setattr(obj, method,
446                   compat.partial(obj._GenericHandler, opcode,
447                                  getattr(obj, rename_attr, None),
448                                  getattr(obj, fn_attr, obj._GetDefaultData)))
449
450     return obj
451
452
453 class OpcodeResource(ResourceBase):
454   """Base class for opcode-based RAPI resources.
455
456   Instances of this class automatically gain handler functions through
457   L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
458   is defined at class level. Subclasses can define a C{Get$Method$OpInput}
459   method to do their own opcode input processing (e.g. for static values). The
460   C{$METHOD$_RENAME} variable defines which values are renamed (see
461   L{baserlib.FillOpcode}).
462
463   @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
464     automatically generate a GET handler submitting the opcode
465   @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
466     L{baserlib.FillOpcode})
467   @ivar GetGetOpInput: Define this to override the default method for
468     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
469
470   @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
471     automatically generate a PUT handler submitting the opcode
472   @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
473     L{baserlib.FillOpcode})
474   @ivar GetPutOpInput: Define this to override the default method for
475     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
476
477   @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
478     automatically generate a POST handler submitting the opcode
479   @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
480     L{baserlib.FillOpcode})
481   @ivar GetPostOpInput: Define this to override the default method for
482     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
483
484   @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
485     automatically generate a DELETE handler submitting the opcode
486   @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
487     L{baserlib.FillOpcode})
488   @ivar GetDeleteOpInput: Define this to override the default method for
489     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
490
491   """
492   __metaclass__ = _MetaOpcodeResource
493
494   def _GetDefaultData(self):
495     return (self.request_body, None)
496
497   def _GenericHandler(self, opcode, rename, fn):
498     (body, static) = fn()
499     op = FillOpcode(opcode, body, static, rename=rename)
500     return self.SubmitJob([op])