Merge branch 'devel-2.1'
[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 ssconf
36 from ganeti import constants
37 from ganeti import opcodes
38 from ganeti import errors
39
40
41 # Dummy value to detect unchanged parameters
42 _DEFAULT = object()
43
44
45 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
46   """Builds a URI list as used by index resources.
47
48   @param ids: list of ids as strings
49   @param uri_format: format to be applied for URI
50   @param uri_fields: optional parameter for field IDs
51
52   """
53   (field_id, field_uri) = uri_fields
54
55   def _MapId(m_id):
56     return { field_id: m_id, field_uri: uri_format % m_id, }
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 _Tags_GET(kind, name):
92   """Helper function to retrieve tags.
93
94   """
95   if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
96     if not name:
97       raise http.HttpBadRequest("Missing name on tag request")
98     cl = GetClient()
99     if kind == constants.TAG_INSTANCE:
100       fn = cl.QueryInstances
101     else:
102       fn = cl.QueryNodes
103     result = fn(names=[name], fields=["tags"], use_locking=False)
104     if not result or not result[0]:
105       raise http.HttpBadGateway("Invalid response from tag query")
106     tags = result[0][0]
107   elif kind == constants.TAG_CLUSTER:
108     ssc = ssconf.SimpleStore()
109     tags = ssc.GetClusterTags()
110
111   return list(tags)
112
113
114 def _Tags_PUT(kind, tags, name, dry_run):
115   """Helper function to set tags.
116
117   """
118   return SubmitJob([opcodes.OpAddTags(kind=kind, name=name,
119                                       tags=tags, dry_run=dry_run)])
120
121
122 def _Tags_DELETE(kind, tags, name, dry_run):
123   """Helper function to delete tags.
124
125   """
126   return SubmitJob([opcodes.OpDelTags(kind=kind, name=name,
127                                       tags=tags, dry_run=dry_run)])
128
129
130 def MapBulkFields(itemslist, fields):
131   """Map value to field name in to one dictionary.
132
133   @param itemslist: a list of items values
134   @param fields: a list of items names
135
136   @return: a list of mapped dictionaries
137
138   """
139   items_details = []
140   for item in itemslist:
141     mapped = MapFields(fields, item)
142     items_details.append(mapped)
143   return items_details
144
145
146 def MakeParamsDict(opts, params):
147   """Makes params dictionary out of a option set.
148
149   This function returns a dictionary needed for hv or be parameters. But only
150   those fields which provided in the option set. Takes parameters frozensets
151   from constants.
152
153   @type opts: dict
154   @param opts: selected options
155   @type params: frozenset
156   @param params: subset of options
157   @rtype: dict
158   @return: dictionary of options, filtered by given subset.
159
160   """
161   result = {}
162
163   for p in params:
164     try:
165       value = opts[p]
166     except KeyError:
167       continue
168     result[p] = value
169
170   return result
171
172
173 def SubmitJob(op, cl=None):
174   """Generic wrapper for submit job, for better http compatibility.
175
176   @type op: list
177   @param op: the list of opcodes for the job
178   @type cl: None or luxi.Client
179   @param cl: optional luxi client to use
180   @rtype: string
181   @return: the job ID
182
183   """
184   try:
185     if cl is None:
186       cl = GetClient()
187     return cl.SubmitJob(op)
188   except errors.JobQueueFull:
189     raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
190   except errors.JobQueueDrainError:
191     raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
192   except luxi.NoMasterError, err:
193     raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
194   except luxi.TimeoutError, err:
195     raise http.HttpGatewayTimeout("Timeout while talking to the master"
196                                   " daemon. Error: %s" % str(err))
197
198 def GetClient():
199   """Geric wrapper for luxi.Client(), for better http compatiblity.
200
201   """
202   try:
203     return luxi.Client()
204   except luxi.NoMasterError, err:
205     raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
206
207
208 def FeedbackFn(ts, log_type, log_msg): # pylint: disable-msg=W0613
209   """Feedback logging function for http case.
210
211   We don't have a stdout for printing log messages, so log them to the
212   http log at least.
213
214   @param ts: the timestamp (unused)
215
216   """
217   logging.info("%s: %s", log_type, log_msg)
218
219
220 def CheckType(value, exptype, descr):
221   """Abort request if value type doesn't match expected type.
222
223   @param value: Value
224   @type exptype: type
225   @param exptype: Expected type
226   @type descr: string
227   @param descr: Description of value
228   @return: Value (allows inline usage)
229
230   """
231   if not isinstance(value, exptype):
232     raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
233                               (descr, type(value).__name__, exptype.__name__))
234
235   return value
236
237
238 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
239   """Check and return the value for a given parameter.
240
241   If no default value was given and the parameter doesn't exist in the input
242   data, an error is raise.
243
244   @type data: dict
245   @param data: Dictionary containing input data
246   @type name: string
247   @param name: Parameter name
248   @param default: Default value (can be None)
249   @param exptype: Expected type (can be None)
250
251   """
252   try:
253     value = data[name]
254   except KeyError:
255     if default is not _DEFAULT:
256       return default
257
258     raise http.HttpBadRequest("Required parameter '%s' is missing" %
259                               name)
260
261   if exptype is _DEFAULT:
262     return value
263
264   return CheckType(value, exptype, "'%s' parameter" % name)
265
266
267 class R_Generic(object):
268   """Generic class for resources.
269
270   """
271   # Default permission requirements
272   GET_ACCESS = []
273   PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
274   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
275   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
276
277   def __init__(self, items, queryargs, req):
278     """Generic resource constructor.
279
280     @param items: a list with variables encoded in the URL
281     @param queryargs: a dictionary with additional options from URL
282
283     """
284     self.items = items
285     self.queryargs = queryargs
286     self._req = req
287
288   def _GetRequestBody(self):
289     """Returns the body data.
290
291     """
292     return self._req.private.body_data
293
294   request_body = property(fget=_GetRequestBody)
295
296   def _checkIntVariable(self, name, default=0):
297     """Return the parsed value of an int argument.
298
299     """
300     val = self.queryargs.get(name, default)
301     if isinstance(val, list):
302       if val:
303         val = val[0]
304       else:
305         val = default
306     try:
307       val = int(val)
308     except (ValueError, TypeError):
309       raise http.HttpBadRequest("Invalid value for the"
310                                 " '%s' parameter" % (name,))
311     return val
312
313   def _checkStringVariable(self, name, default=None):
314     """Return the parsed value of an int argument.
315
316     """
317     val = self.queryargs.get(name, default)
318     if isinstance(val, list):
319       if val:
320         val = val[0]
321       else:
322         val = default
323     return val
324
325   def getBodyParameter(self, name, *args):
326     """Check and return the value for a given parameter.
327
328     If a second parameter is not given, an error will be returned,
329     otherwise this parameter specifies the default value.
330
331     @param name: the required parameter
332
333     """
334     if args:
335       return CheckParameter(self.request_body, name, default=args[0])
336
337     return CheckParameter(self.request_body, name)
338
339   def useLocking(self):
340     """Check if the request specifies locking.
341
342     """
343     return self._checkIntVariable('lock')
344
345   def useBulk(self):
346     """Check if the request specifies bulk querying.
347
348     """
349     return self._checkIntVariable('bulk')
350
351   def useForce(self):
352     """Check if the request specifies a forced operation.
353
354     """
355     return self._checkIntVariable('force')
356
357   def dryRun(self):
358     """Check if the request specifies dry-run mode.
359
360     """
361     return self._checkIntVariable('dry-run')