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