RAPI: Clean up instance creation, use generated docs
[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.OpTagsSet(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.OpTagsDel(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 FillOpcode(opcls, body, static, rename=None):
174   """Fills an opcode with body parameters.
175
176   Parameter types are checked.
177
178   @type opcls: L{opcodes.OpCode}
179   @param opcls: Opcode class
180   @type body: dict
181   @param body: Body parameters as received from client
182   @type static: dict
183   @param static: Static parameters which can't be modified by client
184   @type rename: dict
185   @param rename: Renamed parameters, key as old name, value as new name
186   @return: Opcode object
187
188   """
189   CheckType(body, dict, "Body contents")
190
191   # Make copy to be modified
192   params = body.copy()
193
194   if rename:
195     for old, new in rename.items():
196       if new in params and old in params:
197         raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
198                                   " both are specified" %
199                                   (old, new))
200       if old in params:
201         assert new not in params
202         params[new] = params.pop(old)
203
204   if static:
205     overwritten = set(params.keys()) & set(static.keys())
206     if overwritten:
207       raise http.HttpBadRequest("Can't overwrite static parameters %r" %
208                                 overwritten)
209
210     params.update(static)
211
212   # Convert keys to strings (simplejson decodes them as unicode)
213   params = dict((str(key), value) for (key, value) in params.items())
214
215   try:
216     op = opcls(**params) # pylint: disable-msg=W0142
217     op.Validate(False)
218   except (errors.OpPrereqError, TypeError), err:
219     raise http.HttpBadRequest("Invalid body parameters: %s" % err)
220
221   return op
222
223
224 def SubmitJob(op, cl=None):
225   """Generic wrapper for submit job, for better http compatibility.
226
227   @type op: list
228   @param op: the list of opcodes for the job
229   @type cl: None or luxi.Client
230   @param cl: optional luxi client to use
231   @rtype: string
232   @return: the job ID
233
234   """
235   try:
236     if cl is None:
237       cl = GetClient()
238     return cl.SubmitJob(op)
239   except errors.JobQueueFull:
240     raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
241   except errors.JobQueueDrainError:
242     raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
243   except luxi.NoMasterError, err:
244     raise http.HttpBadGateway("Master seems to be unreachable: %s" % str(err))
245   except luxi.PermissionError:
246     raise http.HttpInternalServerError("Internal error: no permission to"
247                                        " connect to the master daemon")
248   except luxi.TimeoutError, err:
249     raise http.HttpGatewayTimeout("Timeout while talking to the master"
250                                   " daemon. Error: %s" % str(err))
251
252
253 def HandleItemQueryErrors(fn, *args, **kwargs):
254   """Converts errors when querying a single item.
255
256   """
257   try:
258     return fn(*args, **kwargs)
259   except errors.OpPrereqError, err:
260     if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
261       raise http.HttpNotFound()
262
263     raise
264
265
266 def GetClient():
267   """Geric wrapper for luxi.Client(), for better http compatiblity.
268
269   """
270   try:
271     return luxi.Client()
272   except luxi.NoMasterError, err:
273     raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
274   except luxi.PermissionError:
275     raise http.HttpInternalServerError("Internal error: no permission to"
276                                        " connect to the master daemon")
277
278
279 def FeedbackFn(msg):
280   """Feedback logging function for jobs.
281
282   We don't have a stdout for printing log messages, so log them to the
283   http log at least.
284
285   @param msg: the message
286
287   """
288   (_, log_type, log_msg) = msg
289   logging.info("%s: %s", log_type, log_msg)
290
291
292 def CheckType(value, exptype, descr):
293   """Abort request if value type doesn't match expected type.
294
295   @param value: Value
296   @type exptype: type
297   @param exptype: Expected type
298   @type descr: string
299   @param descr: Description of value
300   @return: Value (allows inline usage)
301
302   """
303   if not isinstance(value, exptype):
304     raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
305                               (descr, type(value).__name__, exptype.__name__))
306
307   return value
308
309
310 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
311   """Check and return the value for a given parameter.
312
313   If no default value was given and the parameter doesn't exist in the input
314   data, an error is raise.
315
316   @type data: dict
317   @param data: Dictionary containing input data
318   @type name: string
319   @param name: Parameter name
320   @param default: Default value (can be None)
321   @param exptype: Expected type (can be None)
322
323   """
324   try:
325     value = data[name]
326   except KeyError:
327     if default is not _DEFAULT:
328       return default
329
330     raise http.HttpBadRequest("Required parameter '%s' is missing" %
331                               name)
332
333   if exptype is _DEFAULT:
334     return value
335
336   return CheckType(value, exptype, "'%s' parameter" % name)
337
338
339 class R_Generic(object):
340   """Generic class for resources.
341
342   """
343   # Default permission requirements
344   GET_ACCESS = []
345   PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
346   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
347   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
348
349   def __init__(self, items, queryargs, req):
350     """Generic resource constructor.
351
352     @param items: a list with variables encoded in the URL
353     @param queryargs: a dictionary with additional options from URL
354
355     """
356     self.items = items
357     self.queryargs = queryargs
358     self._req = req
359
360   def _GetRequestBody(self):
361     """Returns the body data.
362
363     """
364     return self._req.private.body_data
365
366   request_body = property(fget=_GetRequestBody)
367
368   def _checkIntVariable(self, name, default=0):
369     """Return the parsed value of an int argument.
370
371     """
372     val = self.queryargs.get(name, default)
373     if isinstance(val, list):
374       if val:
375         val = val[0]
376       else:
377         val = default
378     try:
379       val = int(val)
380     except (ValueError, TypeError):
381       raise http.HttpBadRequest("Invalid value for the"
382                                 " '%s' parameter" % (name,))
383     return val
384
385   def _checkStringVariable(self, name, default=None):
386     """Return the parsed value of an int argument.
387
388     """
389     val = self.queryargs.get(name, default)
390     if isinstance(val, list):
391       if val:
392         val = val[0]
393       else:
394         val = default
395     return val
396
397   def getBodyParameter(self, name, *args):
398     """Check and return the value for a given parameter.
399
400     If a second parameter is not given, an error will be returned,
401     otherwise this parameter specifies the default value.
402
403     @param name: the required parameter
404
405     """
406     if args:
407       return CheckParameter(self.request_body, name, default=args[0])
408
409     return CheckParameter(self.request_body, name)
410
411   def useLocking(self):
412     """Check if the request specifies locking.
413
414     """
415     return bool(self._checkIntVariable("lock"))
416
417   def useBulk(self):
418     """Check if the request specifies bulk querying.
419
420     """
421     return bool(self._checkIntVariable("bulk"))
422
423   def useForce(self):
424     """Check if the request specifies a forced operation.
425
426     """
427     return bool(self._checkIntVariable("force"))
428
429   def dryRun(self):
430     """Check if the request specifies dry-run mode.
431
432     """
433     return bool(self._checkIntVariable("dry-run"))