OpInstanceSetParams: Merge {off,on}line_inst parameters
[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=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 _BuildOpcodeAttributes():
52   """Builds list of attributes used for per-handler opcodes.
53
54   """
55   return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
56            "Get%sOpInput" % method.capitalize())
57           for method in _SUPPORTED_METHODS]
58
59
60 _OPCODE_ATTRS = _BuildOpcodeAttributes()
61
62
63 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
64   """Builds a URI list as used by index resources.
65
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
69
70   """
71   (field_id, field_uri) = uri_fields
72
73   def _MapId(m_id):
74     return {
75       field_id: m_id,
76       field_uri: uri_format % m_id,
77       }
78
79   # Make sure the result is sorted, makes it nicer to look at and simplifies
80   # unittests.
81   ids.sort()
82
83   return map(_MapId, ids)
84
85
86 def ExtractField(sequence, index):
87   """Creates a list containing one column out of a list of lists.
88
89   @param sequence: sequence of lists
90   @param index: index of field
91
92   """
93   return map(lambda item: item[index], sequence)
94
95
96 def MapFields(names, data):
97   """Maps two lists into one dictionary.
98
99   Example::
100       >>> MapFields(["a", "b"], ["foo", 123])
101       {'a': 'foo', 'b': 123}
102
103   @param names: field names (list of strings)
104   @param data: field data (list)
105
106   """
107   if len(names) != len(data):
108     raise AttributeError("Names and data must have the same length")
109   return dict(zip(names, data))
110
111
112 def MapBulkFields(itemslist, fields):
113   """Map value to field name in to one dictionary.
114
115   @param itemslist: a list of items values
116   @param fields: a list of items names
117
118   @return: a list of mapped dictionaries
119
120   """
121   items_details = []
122   for item in itemslist:
123     mapped = MapFields(fields, item)
124     items_details.append(mapped)
125   return items_details
126
127
128 def MakeParamsDict(opts, params):
129   """Makes params dictionary out of a option set.
130
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
133   from constants.
134
135   @type opts: dict
136   @param opts: selected options
137   @type params: frozenset
138   @param params: subset of options
139   @rtype: dict
140   @return: dictionary of options, filtered by given subset.
141
142   """
143   result = {}
144
145   for p in params:
146     try:
147       value = opts[p]
148     except KeyError:
149       continue
150     result[p] = value
151
152   return result
153
154
155 def FillOpcode(opcls, body, static, rename=None):
156   """Fills an opcode with body parameters.
157
158   Parameter types are checked.
159
160   @type opcls: L{opcodes.OpCode}
161   @param opcls: Opcode class
162   @type body: dict
163   @param body: Body parameters as received from client
164   @type static: dict
165   @param static: Static parameters which can't be modified by client
166   @type rename: dict
167   @param rename: Renamed parameters, key as old name, value as new name
168   @return: Opcode object
169
170   """
171   if body is None:
172     params = {}
173   else:
174     CheckType(body, dict, "Body contents")
175
176     # Make copy to be modified
177     params = body.copy()
178
179   if rename:
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" %
184                                   (old, new))
185       if old in params:
186         assert new not in params
187         params[new] = params.pop(old)
188
189   if static:
190     overwritten = set(params.keys()) & set(static.keys())
191     if overwritten:
192       raise http.HttpBadRequest("Can't overwrite static parameters %r" %
193                                 overwritten)
194
195     params.update(static)
196
197   # Convert keys to strings (simplejson decodes them as unicode)
198   params = dict((str(key), value) for (key, value) in params.items())
199
200   try:
201     op = opcls(**params) # pylint: disable=W0142
202     op.Validate(False)
203   except (errors.OpPrereqError, TypeError), err:
204     raise http.HttpBadRequest("Invalid body parameters: %s" % err)
205
206   return op
207
208
209 def HandleItemQueryErrors(fn, *args, **kwargs):
210   """Converts errors when querying a single item.
211
212   """
213   try:
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()
218
219     raise
220
221
222 def FeedbackFn(msg):
223   """Feedback logging function for jobs.
224
225   We don't have a stdout for printing log messages, so log them to the
226   http log at least.
227
228   @param msg: the message
229
230   """
231   (_, log_type, log_msg) = msg
232   logging.info("%s: %s", log_type, log_msg)
233
234
235 def CheckType(value, exptype, descr):
236   """Abort request if value type doesn't match expected type.
237
238   @param value: Value
239   @type exptype: type
240   @param exptype: Expected type
241   @type descr: string
242   @param descr: Description of value
243   @return: Value (allows inline usage)
244
245   """
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__))
249
250   return value
251
252
253 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
254   """Check and return the value for a given parameter.
255
256   If no default value was given and the parameter doesn't exist in the input
257   data, an error is raise.
258
259   @type data: dict
260   @param data: Dictionary containing input data
261   @type name: string
262   @param name: Parameter name
263   @param default: Default value (can be None)
264   @param exptype: Expected type (can be None)
265
266   """
267   try:
268     value = data[name]
269   except KeyError:
270     if default is not _DEFAULT:
271       return default
272
273     raise http.HttpBadRequest("Required parameter '%s' is missing" %
274                               name)
275
276   if exptype is _DEFAULT:
277     return value
278
279   return CheckType(value, exptype, "'%s' parameter" % name)
280
281
282 class ResourceBase(object):
283   """Generic class for resources.
284
285   """
286   # Default permission requirements
287   GET_ACCESS = []
288   PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
289   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
290   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
291
292   def __init__(self, items, queryargs, req, _client_cls=luxi.Client):
293     """Generic resource constructor.
294
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)
299
300     """
301     self.items = items
302     self.queryargs = queryargs
303     self._req = req
304     self._client_cls = _client_cls
305
306   def _GetRequestBody(self):
307     """Returns the body data.
308
309     """
310     return self._req.private.body_data
311
312   request_body = property(fget=_GetRequestBody)
313
314   def _checkIntVariable(self, name, default=0):
315     """Return the parsed value of an int argument.
316
317     """
318     val = self.queryargs.get(name, default)
319     if isinstance(val, list):
320       if val:
321         val = val[0]
322       else:
323         val = default
324     try:
325       val = int(val)
326     except (ValueError, TypeError):
327       raise http.HttpBadRequest("Invalid value for the"
328                                 " '%s' parameter" % (name,))
329     return val
330
331   def _checkStringVariable(self, name, default=None):
332     """Return the parsed value of an int argument.
333
334     """
335     val = self.queryargs.get(name, default)
336     if isinstance(val, list):
337       if val:
338         val = val[0]
339       else:
340         val = default
341     return val
342
343   def getBodyParameter(self, name, *args):
344     """Check and return the value for a given parameter.
345
346     If a second parameter is not given, an error will be returned,
347     otherwise this parameter specifies the default value.
348
349     @param name: the required parameter
350
351     """
352     if args:
353       return CheckParameter(self.request_body, name, default=args[0])
354
355     return CheckParameter(self.request_body, name)
356
357   def useLocking(self):
358     """Check if the request specifies locking.
359
360     """
361     return bool(self._checkIntVariable("lock"))
362
363   def useBulk(self):
364     """Check if the request specifies bulk querying.
365
366     """
367     return bool(self._checkIntVariable("bulk"))
368
369   def useForce(self):
370     """Check if the request specifies a forced operation.
371
372     """
373     return bool(self._checkIntVariable("force"))
374
375   def dryRun(self):
376     """Check if the request specifies dry-run mode.
377
378     """
379     return bool(self._checkIntVariable("dry-run"))
380
381   def GetClient(self):
382     """Wrapper for L{luxi.Client} with HTTP-specific error handling.
383
384     """
385     # Could be a function, pylint: disable=R0201
386     try:
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")
393
394   def SubmitJob(self, op, cl=None):
395     """Generic wrapper for submit job, for better http compatibility.
396
397     @type op: list
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
401     @rtype: string
402     @return: the job ID
403
404     """
405     if cl is None:
406       cl = self.GetClient()
407     try:
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"
420                                     " daemon: %s" % err)
421
422
423 def GetResourceOpcodes(cls):
424   """Returns all opcodes used by a resource.
425
426   """
427   return frozenset(filter(None, (getattr(cls, op_attr, None)
428                                  for (_, op_attr, _, _) in _OPCODE_ATTRS)))
429
430
431 class _MetaOpcodeResource(type):
432   """Meta class for RAPI resources.
433
434   """
435   def __call__(mcs, *args, **kwargs):
436     """Instantiates class and patches it for use by the RAPI daemon.
437
438     """
439     # Access to private attributes of a client class, pylint: disable=W0212
440     obj = type.__call__(mcs, *args, **kwargs)
441
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
446         # handler)
447         assert not hasattr(obj, rename_attr)
448         assert not hasattr(obj, fn_attr)
449       else:
450         # Try to generate handler method on handler instance
451         try:
452           opcode = getattr(obj, op_attr)
453         except AttributeError:
454           pass
455         else:
456           setattr(obj, method,
457                   compat.partial(obj._GenericHandler, opcode,
458                                  getattr(obj, rename_attr, None),
459                                  getattr(obj, fn_attr, obj._GetDefaultData)))
460
461     return obj
462
463
464 class OpcodeResource(ResourceBase):
465   """Base class for opcode-based RAPI resources.
466
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
472   L{FillOpcode}).
473
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
477     L{FillOpcode})
478   @ivar GetGetOpInput: Define this to override the default method for
479     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
480
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
484     L{FillOpcode})
485   @ivar GetPutOpInput: Define this to override the default method for
486     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
487
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
491     L{FillOpcode})
492   @ivar GetPostOpInput: Define this to override the default method for
493     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
494
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
498     L{FillOpcode})
499   @ivar GetDeleteOpInput: Define this to override the default method for
500     getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
501
502   """
503   __metaclass__ = _MetaOpcodeResource
504
505   def _GetDefaultData(self):
506     return (self.request_body, None)
507
508   def _GenericHandler(self, opcode, rename, fn):
509     (body, static) = fn()
510     op = FillOpcode(opcode, body, static, rename=rename)
511     return self.SubmitJob([op])