4 # Copyright (C) 2006, 2007, 2008 Google Inc.
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.
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.
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
22 """Remote API base resources library.
26 # pylint: disable-msg=C0103
28 # C0103: Invalid name, since the R_* names are not conforming
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
41 # Dummy value to detect unchanged parameters
45 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
46 """Builds a URI list as used by index resources.
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
53 (field_id, field_uri) = uri_fields
56 return { field_id: m_id, field_uri: uri_format % m_id, }
58 # Make sure the result is sorted, makes it nicer to look at and simplifies
62 return map(_MapId, ids)
65 def ExtractField(sequence, index):
66 """Creates a list containing one column out of a list of lists.
68 @param sequence: sequence of lists
69 @param index: index of field
72 return map(lambda item: item[index], sequence)
75 def MapFields(names, data):
76 """Maps two lists into one dictionary.
79 >>> MapFields(["a", "b"], ["foo", 123])
80 {'a': 'foo', 'b': 123}
82 @param names: field names (list of strings)
83 @param data: field data (list)
86 if len(names) != len(data):
87 raise AttributeError("Names and data must have the same length")
88 return dict(zip(names, data))
91 def _Tags_GET(kind, name):
92 """Helper function to retrieve tags.
95 if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
97 raise http.HttpBadRequest("Missing name on tag request")
99 if kind == constants.TAG_INSTANCE:
100 fn = cl.QueryInstances
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")
107 elif kind == constants.TAG_CLUSTER:
108 ssc = ssconf.SimpleStore()
109 tags = ssc.GetClusterTags()
114 def _Tags_PUT(kind, tags, name, dry_run):
115 """Helper function to set tags.
118 return SubmitJob([opcodes.OpTagsSet(kind=kind, name=name,
119 tags=tags, dry_run=dry_run)])
122 def _Tags_DELETE(kind, tags, name, dry_run):
123 """Helper function to delete tags.
126 return SubmitJob([opcodes.OpTagsDel(kind=kind, name=name,
127 tags=tags, dry_run=dry_run)])
130 def MapBulkFields(itemslist, fields):
131 """Map value to field name in to one dictionary.
133 @param itemslist: a list of items values
134 @param fields: a list of items names
136 @return: a list of mapped dictionaries
140 for item in itemslist:
141 mapped = MapFields(fields, item)
142 items_details.append(mapped)
146 def MakeParamsDict(opts, params):
147 """Makes params dictionary out of a option set.
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
154 @param opts: selected options
155 @type params: frozenset
156 @param params: subset of options
158 @return: dictionary of options, filtered by given subset.
173 def FillOpcode(opcls, body, static, rename=None):
174 """Fills an opcode with body parameters.
176 Parameter types are checked.
178 @type opcls: L{opcodes.OpCode}
179 @param opcls: Opcode class
181 @param body: Body parameters as received from client
183 @param static: Static parameters which can't be modified by client
185 @param rename: Renamed parameters, key as old name, value as new name
186 @return: Opcode object
189 CheckType(body, dict, "Body contents")
191 # Make copy to be modified
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" %
201 assert new not in params
202 params[new] = params.pop(old)
205 overwritten = set(params.keys()) & set(static.keys())
207 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
210 params.update(static)
212 # Convert keys to strings (simplejson decodes them as unicode)
213 params = dict((str(key), value) for (key, value) in params.items())
216 op = opcls(**params) # pylint: disable-msg=W0142
218 except (errors.OpPrereqError, TypeError), err:
219 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
224 def SubmitJob(op, cl=None):
225 """Generic wrapper for submit job, for better http compatibility.
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
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))
253 def HandleItemQueryErrors(fn, *args, **kwargs):
254 """Converts errors when querying a single item.
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()
267 """Geric wrapper for luxi.Client(), for better http compatiblity.
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")
280 """Feedback logging function for jobs.
282 We don't have a stdout for printing log messages, so log them to the
285 @param msg: the message
288 (_, log_type, log_msg) = msg
289 logging.info("%s: %s", log_type, log_msg)
292 def CheckType(value, exptype, descr):
293 """Abort request if value type doesn't match expected type.
297 @param exptype: Expected type
299 @param descr: Description of value
300 @return: Value (allows inline usage)
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__))
310 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
311 """Check and return the value for a given parameter.
313 If no default value was given and the parameter doesn't exist in the input
314 data, an error is raise.
317 @param data: Dictionary containing input data
319 @param name: Parameter name
320 @param default: Default value (can be None)
321 @param exptype: Expected type (can be None)
327 if default is not _DEFAULT:
330 raise http.HttpBadRequest("Required parameter '%s' is missing" %
333 if exptype is _DEFAULT:
336 return CheckType(value, exptype, "'%s' parameter" % name)
339 class R_Generic(object):
340 """Generic class for resources.
343 # Default permission requirements
345 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
346 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
347 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349 def __init__(self, items, queryargs, req):
350 """Generic resource constructor.
352 @param items: a list with variables encoded in the URL
353 @param queryargs: a dictionary with additional options from URL
357 self.queryargs = queryargs
360 def _GetRequestBody(self):
361 """Returns the body data.
364 return self._req.private.body_data
366 request_body = property(fget=_GetRequestBody)
368 def _checkIntVariable(self, name, default=0):
369 """Return the parsed value of an int argument.
372 val = self.queryargs.get(name, default)
373 if isinstance(val, list):
380 except (ValueError, TypeError):
381 raise http.HttpBadRequest("Invalid value for the"
382 " '%s' parameter" % (name,))
385 def _checkStringVariable(self, name, default=None):
386 """Return the parsed value of an int argument.
389 val = self.queryargs.get(name, default)
390 if isinstance(val, list):
397 def getBodyParameter(self, name, *args):
398 """Check and return the value for a given parameter.
400 If a second parameter is not given, an error will be returned,
401 otherwise this parameter specifies the default value.
403 @param name: the required parameter
407 return CheckParameter(self.request_body, name, default=args[0])
409 return CheckParameter(self.request_body, name)
411 def useLocking(self):
412 """Check if the request specifies locking.
415 return bool(self._checkIntVariable("lock"))
418 """Check if the request specifies bulk querying.
421 return bool(self._checkIntVariable("bulk"))
424 """Check if the request specifies a forced operation.
427 return bool(self._checkIntVariable("force"))
430 """Check if the request specifies dry-run mode.
433 return bool(self._checkIntVariable("dry-run"))