baserlib: Move GetClient/SubmitJob into base class
[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
37
38 # Dummy value to detect unchanged parameters
39 _DEFAULT = object()
40
41
42 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
43   """Builds a URI list as used by index resources.
44
45   @param ids: list of ids as strings
46   @param uri_format: format to be applied for URI
47   @param uri_fields: optional parameter for field IDs
48
49   """
50   (field_id, field_uri) = uri_fields
51
52   def _MapId(m_id):
53     return {
54       field_id: m_id,
55       field_uri: uri_format % m_id,
56       }
57
58   # Make sure the result is sorted, makes it nicer to look at and simplifies
59   # unittests.
60   ids.sort()
61
62   return map(_MapId, ids)
63
64
65 def ExtractField(sequence, index):
66   """Creates a list containing one column out of a list of lists.
67
68   @param sequence: sequence of lists
69   @param index: index of field
70
71   """
72   return map(lambda item: item[index], sequence)
73
74
75 def MapFields(names, data):
76   """Maps two lists into one dictionary.
77
78   Example::
79       >>> MapFields(["a", "b"], ["foo", 123])
80       {'a': 'foo', 'b': 123}
81
82   @param names: field names (list of strings)
83   @param data: field data (list)
84
85   """
86   if len(names) != len(data):
87     raise AttributeError("Names and data must have the same length")
88   return dict(zip(names, data))
89
90
91 def MapBulkFields(itemslist, fields):
92   """Map value to field name in to one dictionary.
93
94   @param itemslist: a list of items values
95   @param fields: a list of items names
96
97   @return: a list of mapped dictionaries
98
99   """
100   items_details = []
101   for item in itemslist:
102     mapped = MapFields(fields, item)
103     items_details.append(mapped)
104   return items_details
105
106
107 def MakeParamsDict(opts, params):
108   """Makes params dictionary out of a option set.
109
110   This function returns a dictionary needed for hv or be parameters. But only
111   those fields which provided in the option set. Takes parameters frozensets
112   from constants.
113
114   @type opts: dict
115   @param opts: selected options
116   @type params: frozenset
117   @param params: subset of options
118   @rtype: dict
119   @return: dictionary of options, filtered by given subset.
120
121   """
122   result = {}
123
124   for p in params:
125     try:
126       value = opts[p]
127     except KeyError:
128       continue
129     result[p] = value
130
131   return result
132
133
134 def FillOpcode(opcls, body, static, rename=None):
135   """Fills an opcode with body parameters.
136
137   Parameter types are checked.
138
139   @type opcls: L{opcodes.OpCode}
140   @param opcls: Opcode class
141   @type body: dict
142   @param body: Body parameters as received from client
143   @type static: dict
144   @param static: Static parameters which can't be modified by client
145   @type rename: dict
146   @param rename: Renamed parameters, key as old name, value as new name
147   @return: Opcode object
148
149   """
150   CheckType(body, dict, "Body contents")
151
152   # Make copy to be modified
153   params = body.copy()
154
155   if rename:
156     for old, new in rename.items():
157       if new in params and old in params:
158         raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
159                                   " both are specified" %
160                                   (old, new))
161       if old in params:
162         assert new not in params
163         params[new] = params.pop(old)
164
165   if static:
166     overwritten = set(params.keys()) & set(static.keys())
167     if overwritten:
168       raise http.HttpBadRequest("Can't overwrite static parameters %r" %
169                                 overwritten)
170
171     params.update(static)
172
173   # Convert keys to strings (simplejson decodes them as unicode)
174   params = dict((str(key), value) for (key, value) in params.items())
175
176   try:
177     op = opcls(**params) # pylint: disable-msg=W0142
178     op.Validate(False)
179   except (errors.OpPrereqError, TypeError), err:
180     raise http.HttpBadRequest("Invalid body parameters: %s" % err)
181
182   return op
183
184
185 def HandleItemQueryErrors(fn, *args, **kwargs):
186   """Converts errors when querying a single item.
187
188   """
189   try:
190     return fn(*args, **kwargs)
191   except errors.OpPrereqError, err:
192     if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
193       raise http.HttpNotFound()
194
195     raise
196
197
198 def FeedbackFn(msg):
199   """Feedback logging function for jobs.
200
201   We don't have a stdout for printing log messages, so log them to the
202   http log at least.
203
204   @param msg: the message
205
206   """
207   (_, log_type, log_msg) = msg
208   logging.info("%s: %s", log_type, log_msg)
209
210
211 def CheckType(value, exptype, descr):
212   """Abort request if value type doesn't match expected type.
213
214   @param value: Value
215   @type exptype: type
216   @param exptype: Expected type
217   @type descr: string
218   @param descr: Description of value
219   @return: Value (allows inline usage)
220
221   """
222   if not isinstance(value, exptype):
223     raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
224                               (descr, type(value).__name__, exptype.__name__))
225
226   return value
227
228
229 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
230   """Check and return the value for a given parameter.
231
232   If no default value was given and the parameter doesn't exist in the input
233   data, an error is raise.
234
235   @type data: dict
236   @param data: Dictionary containing input data
237   @type name: string
238   @param name: Parameter name
239   @param default: Default value (can be None)
240   @param exptype: Expected type (can be None)
241
242   """
243   try:
244     value = data[name]
245   except KeyError:
246     if default is not _DEFAULT:
247       return default
248
249     raise http.HttpBadRequest("Required parameter '%s' is missing" %
250                               name)
251
252   if exptype is _DEFAULT:
253     return value
254
255   return CheckType(value, exptype, "'%s' parameter" % name)
256
257
258 class R_Generic(object):
259   """Generic class for resources.
260
261   """
262   # Default permission requirements
263   GET_ACCESS = []
264   PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
265   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
266   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
267
268   def __init__(self, items, queryargs, req):
269     """Generic resource constructor.
270
271     @param items: a list with variables encoded in the URL
272     @param queryargs: a dictionary with additional options from URL
273
274     """
275     self.items = items
276     self.queryargs = queryargs
277     self._req = req
278
279   def _GetRequestBody(self):
280     """Returns the body data.
281
282     """
283     return self._req.private.body_data
284
285   request_body = property(fget=_GetRequestBody)
286
287   def _checkIntVariable(self, name, default=0):
288     """Return the parsed value of an int argument.
289
290     """
291     val = self.queryargs.get(name, default)
292     if isinstance(val, list):
293       if val:
294         val = val[0]
295       else:
296         val = default
297     try:
298       val = int(val)
299     except (ValueError, TypeError):
300       raise http.HttpBadRequest("Invalid value for the"
301                                 " '%s' parameter" % (name,))
302     return val
303
304   def _checkStringVariable(self, name, default=None):
305     """Return the parsed value of an int argument.
306
307     """
308     val = self.queryargs.get(name, default)
309     if isinstance(val, list):
310       if val:
311         val = val[0]
312       else:
313         val = default
314     return val
315
316   def getBodyParameter(self, name, *args):
317     """Check and return the value for a given parameter.
318
319     If a second parameter is not given, an error will be returned,
320     otherwise this parameter specifies the default value.
321
322     @param name: the required parameter
323
324     """
325     if args:
326       return CheckParameter(self.request_body, name, default=args[0])
327
328     return CheckParameter(self.request_body, name)
329
330   def useLocking(self):
331     """Check if the request specifies locking.
332
333     """
334     return bool(self._checkIntVariable("lock"))
335
336   def useBulk(self):
337     """Check if the request specifies bulk querying.
338
339     """
340     return bool(self._checkIntVariable("bulk"))
341
342   def useForce(self):
343     """Check if the request specifies a forced operation.
344
345     """
346     return bool(self._checkIntVariable("force"))
347
348   def dryRun(self):
349     """Check if the request specifies dry-run mode.
350
351     """
352     return bool(self._checkIntVariable("dry-run"))
353
354   def GetClient(self):
355     """Wrapper for L{luxi.Client} with HTTP-specific error handling.
356
357     """
358     # Could be a function, pylint: disable=R0201
359     try:
360       return luxi.Client()
361     except luxi.NoMasterError, err:
362       raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
363     except luxi.PermissionError:
364       raise http.HttpInternalServerError("Internal error: no permission to"
365                                          " connect to the master daemon")
366
367   def SubmitJob(self, op, cl=None):
368     """Generic wrapper for submit job, for better http compatibility.
369
370     @type op: list
371     @param op: the list of opcodes for the job
372     @type cl: None or luxi.Client
373     @param cl: optional luxi client to use
374     @rtype: string
375     @return: the job ID
376
377     """
378     if cl is None:
379       cl = self.GetClient()
380     try:
381       return cl.SubmitJob(op)
382     except errors.JobQueueFull:
383       raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
384     except errors.JobQueueDrainError:
385       raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
386     except luxi.NoMasterError, err:
387       raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
388     except luxi.PermissionError:
389       raise http.HttpInternalServerError("Internal error: no permission to"
390                                          " connect to the master daemon")
391     except luxi.TimeoutError, err:
392       raise http.HttpGatewayTimeout("Timeout while talking to the master"
393                                     " daemon: %s" % err)