rlib2: Convert /2/instances/[inst] to OpcodeResource
[ganeti-local] / lib / rapi / baserlib.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 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-msg=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
38
39 # Dummy value to detect unchanged parameters
40 _DEFAULT = object()
41
42 #: Supported HTTP methods
43 _SUPPORTED_METHODS = frozenset([
44   http.HTTP_DELETE,
45   http.HTTP_GET,
46   http.HTTP_POST,
47   http.HTTP_PUT,
48   ])
49
50
51 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
52   """Builds a URI list as used by index resources.
53
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
57
58   """
59   (field_id, field_uri) = uri_fields
60
61   def _MapId(m_id):
62     return {
63       field_id: m_id,
64       field_uri: uri_format % m_id,
65       }
66
67   # Make sure the result is sorted, makes it nicer to look at and simplifies
68   # unittests.
69   ids.sort()
70
71   return map(_MapId, ids)
72
73
74 def ExtractField(sequence, index):
75   """Creates a list containing one column out of a list of lists.
76
77   @param sequence: sequence of lists
78   @param index: index of field
79
80   """
81   return map(lambda item: item[index], sequence)
82
83
84 def MapFields(names, data):
85   """Maps two lists into one dictionary.
86
87   Example::
88       >>> MapFields(["a", "b"], ["foo", 123])
89       {'a': 'foo', 'b': 123}
90
91   @param names: field names (list of strings)
92   @param data: field data (list)
93
94   """
95   if len(names) != len(data):
96     raise AttributeError("Names and data must have the same length")
97   return dict(zip(names, data))
98
99
100 def MapBulkFields(itemslist, fields):
101   """Map value to field name in to one dictionary.
102
103   @param itemslist: a list of items values
104   @param fields: a list of items names
105
106   @return: a list of mapped dictionaries
107
108   """
109   items_details = []
110   for item in itemslist:
111     mapped = MapFields(fields, item)
112     items_details.append(mapped)
113   return items_details
114
115
116 def MakeParamsDict(opts, params):
117   """Makes params dictionary out of a option set.
118
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
121   from constants.
122
123   @type opts: dict
124   @param opts: selected options
125   @type params: frozenset
126   @param params: subset of options
127   @rtype: dict
128   @return: dictionary of options, filtered by given subset.
129
130   """
131   result = {}
132
133   for p in params:
134     try:
135       value = opts[p]
136     except KeyError:
137       continue
138     result[p] = value
139
140   return result
141
142
143 def FillOpcode(opcls, body, static, rename=None):
144   """Fills an opcode with body parameters.
145
146   Parameter types are checked.
147
148   @type opcls: L{opcodes.OpCode}
149   @param opcls: Opcode class
150   @type body: dict
151   @param body: Body parameters as received from client
152   @type static: dict
153   @param static: Static parameters which can't be modified by client
154   @type rename: dict
155   @param rename: Renamed parameters, key as old name, value as new name
156   @return: Opcode object
157
158   """
159   if body is None:
160     params = {}
161   else:
162     CheckType(body, dict, "Body contents")
163
164     # Make copy to be modified
165     params = body.copy()
166
167   if rename:
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" %
172                                   (old, new))
173       if old in params:
174         assert new not in params
175         params[new] = params.pop(old)
176
177   if static:
178     overwritten = set(params.keys()) & set(static.keys())
179     if overwritten:
180       raise http.HttpBadRequest("Can't overwrite static parameters %r" %
181                                 overwritten)
182
183     params.update(static)
184
185   # Convert keys to strings (simplejson decodes them as unicode)
186   params = dict((str(key), value) for (key, value) in params.items())
187
188   try:
189     op = opcls(**params) # pylint: disable-msg=W0142
190     op.Validate(False)
191   except (errors.OpPrereqError, TypeError), err:
192     raise http.HttpBadRequest("Invalid body parameters: %s" % err)
193
194   return op
195
196
197 def HandleItemQueryErrors(fn, *args, **kwargs):
198   """Converts errors when querying a single item.
199
200   """
201   try:
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()
206
207     raise
208
209
210 def FeedbackFn(msg):
211   """Feedback logging function for jobs.
212
213   We don't have a stdout for printing log messages, so log them to the
214   http log at least.
215
216   @param msg: the message
217
218   """
219   (_, log_type, log_msg) = msg
220   logging.info("%s: %s", log_type, log_msg)
221
222
223 def CheckType(value, exptype, descr):
224   """Abort request if value type doesn't match expected type.
225
226   @param value: Value
227   @type exptype: type
228   @param exptype: Expected type
229   @type descr: string
230   @param descr: Description of value
231   @return: Value (allows inline usage)
232
233   """
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__))
237
238   return value
239
240
241 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
242   """Check and return the value for a given parameter.
243
244   If no default value was given and the parameter doesn't exist in the input
245   data, an error is raise.
246
247   @type data: dict
248   @param data: Dictionary containing input data
249   @type name: string
250   @param name: Parameter name
251   @param default: Default value (can be None)
252   @param exptype: Expected type (can be None)
253
254   """
255   try:
256     value = data[name]
257   except KeyError:
258     if default is not _DEFAULT:
259       return default
260
261     raise http.HttpBadRequest("Required parameter '%s' is missing" %
262                               name)
263
264   if exptype is _DEFAULT:
265     return value
266
267   return CheckType(value, exptype, "'%s' parameter" % name)
268
269
270 class ResourceBase(object):
271   """Generic class for resources.
272
273   """
274   # Default permission requirements
275   GET_ACCESS = []
276   PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
277   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
278   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
279
280   def __init__(self, items, queryargs, req, _client_cls=luxi.Client):
281     """Generic resource constructor.
282
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)
287
288     """
289     self.items = items
290     self.queryargs = queryargs
291     self._req = req
292     self._client_cls = _client_cls
293
294   def _GetRequestBody(self):
295     """Returns the body data.
296
297     """
298     return self._req.private.body_data
299
300   request_body = property(fget=_GetRequestBody)
301
302   def _checkIntVariable(self, name, default=0):
303     """Return the parsed value of an int argument.
304
305     """
306     val = self.queryargs.get(name, default)
307     if isinstance(val, list):
308       if val:
309         val = val[0]
310       else:
311         val = default
312     try:
313       val = int(val)
314     except (ValueError, TypeError):
315       raise http.HttpBadRequest("Invalid value for the"
316                                 " '%s' parameter" % (name,))
317     return val
318
319   def _checkStringVariable(self, name, default=None):
320     """Return the parsed value of an int argument.
321
322     """
323     val = self.queryargs.get(name, default)
324     if isinstance(val, list):
325       if val:
326         val = val[0]
327       else:
328         val = default
329     return val
330
331   def getBodyParameter(self, name, *args):
332     """Check and return the value for a given parameter.
333
334     If a second parameter is not given, an error will be returned,
335     otherwise this parameter specifies the default value.
336
337     @param name: the required parameter
338
339     """
340     if args:
341       return CheckParameter(self.request_body, name, default=args[0])
342
343     return CheckParameter(self.request_body, name)
344
345   def useLocking(self):
346     """Check if the request specifies locking.
347
348     """
349     return bool(self._checkIntVariable("lock"))
350
351   def useBulk(self):
352     """Check if the request specifies bulk querying.
353
354     """
355     return bool(self._checkIntVariable("bulk"))
356
357   def useForce(self):
358     """Check if the request specifies a forced operation.
359
360     """
361     return bool(self._checkIntVariable("force"))
362
363   def dryRun(self):
364     """Check if the request specifies dry-run mode.
365
366     """
367     return bool(self._checkIntVariable("dry-run"))
368
369   def GetClient(self):
370     """Wrapper for L{luxi.Client} with HTTP-specific error handling.
371
372     """
373     # Could be a function, pylint: disable=R0201
374     try:
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")
381
382   def SubmitJob(self, op, cl=None):
383     """Generic wrapper for submit job, for better http compatibility.
384
385     @type op: list
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
389     @rtype: string
390     @return: the job ID
391
392     """
393     if cl is None:
394       cl = self.GetClient()
395     try:
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"
408                                     " daemon: %s" % err)
409
410
411 class _MetaOpcodeResource(type):
412   """Meta class for RAPI resources.
413
414   """
415   _ATTRS = [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
416              "Get%sOpInput" % method.capitalize())
417             for method in _SUPPORTED_METHODS]
418
419   def __call__(mcs, *args, **kwargs):
420     """Instantiates class and patches it for use by the RAPI daemon.
421
422     """
423     # Access to private attributes of a client class, pylint: disable=W0212
424     obj = type.__call__(mcs, *args, **kwargs)
425
426     for (method, op_attr, rename_attr, fn_attr) in mcs._ATTRS:
427       try:
428         opcode = getattr(obj, op_attr)
429       except AttributeError:
430         # If the "*_OPCODE" attribute isn't set, "*_RENAME" or "Get*OpInput"
431         # shouldn't either
432         assert not hasattr(obj, rename_attr)
433         assert not hasattr(obj, fn_attr)
434         continue
435
436       assert not hasattr(obj, method)
437
438       # Generate handler method on handler instance
439       setattr(obj, method,
440               compat.partial(obj._GenericHandler, opcode,
441                              getattr(obj, rename_attr, None),
442                              getattr(obj, fn_attr, obj._GetDefaultData)))
443
444     return obj
445
446
447 class OpcodeResource(ResourceBase):
448   """Base class for opcode-based RAPI resources.
449
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
455   L{FillOpcode}).
456
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
460     L{FillOpcode})
461   @ivar GetGetOpInput: Define this to override the default method for
462     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
463
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
467     L{FillOpcode})
468   @ivar GetPutOpInput: Define this to override the default method for
469     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
470
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
474     L{FillOpcode})
475   @ivar GetPostOpInput: Define this to override the default method for
476     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
477
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
481     L{FillOpcode})
482   @ivar GetDeleteOpInput: Define this to override the default method for
483     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
484
485   """
486   __metaclass__ = _MetaOpcodeResource
487
488   def _GetDefaultData(self):
489     return (self.request_body, None)
490
491   def _GenericHandler(self, opcode, rename, fn):
492     (body, static) = fn()
493     op = FillOpcode(opcode, body, static, rename=rename)
494     return self.SubmitJob([op])