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 errors
38 # Dummy value to detect unchanged parameters
42 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
43 """Builds a URI list as used by index resources.
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
50 (field_id, field_uri) = uri_fields
55 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 MapBulkFields(itemslist, fields):
92 """Map value to field name in to one dictionary.
94 @param itemslist: a list of items values
95 @param fields: a list of items names
97 @return: a list of mapped dictionaries
101 for item in itemslist:
102 mapped = MapFields(fields, item)
103 items_details.append(mapped)
107 def MakeParamsDict(opts, params):
108 """Makes params dictionary out of a option set.
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
115 @param opts: selected options
116 @type params: frozenset
117 @param params: subset of options
119 @return: dictionary of options, filtered by given subset.
134 def FillOpcode(opcls, body, static, rename=None):
135 """Fills an opcode with body parameters.
137 Parameter types are checked.
139 @type opcls: L{opcodes.OpCode}
140 @param opcls: Opcode class
142 @param body: Body parameters as received from client
144 @param static: Static parameters which can't be modified by client
146 @param rename: Renamed parameters, key as old name, value as new name
147 @return: Opcode object
150 CheckType(body, dict, "Body contents")
152 # Make copy to be modified
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" %
162 assert new not in params
163 params[new] = params.pop(old)
166 overwritten = set(params.keys()) & set(static.keys())
168 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
171 params.update(static)
173 # Convert keys to strings (simplejson decodes them as unicode)
174 params = dict((str(key), value) for (key, value) in params.items())
177 op = opcls(**params) # pylint: disable-msg=W0142
179 except (errors.OpPrereqError, TypeError), err:
180 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
185 def HandleItemQueryErrors(fn, *args, **kwargs):
186 """Converts errors when querying a single item.
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()
199 """Feedback logging function for jobs.
201 We don't have a stdout for printing log messages, so log them to the
204 @param msg: the message
207 (_, log_type, log_msg) = msg
208 logging.info("%s: %s", log_type, log_msg)
211 def CheckType(value, exptype, descr):
212 """Abort request if value type doesn't match expected type.
216 @param exptype: Expected type
218 @param descr: Description of value
219 @return: Value (allows inline usage)
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__))
229 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
230 """Check and return the value for a given parameter.
232 If no default value was given and the parameter doesn't exist in the input
233 data, an error is raise.
236 @param data: Dictionary containing input data
238 @param name: Parameter name
239 @param default: Default value (can be None)
240 @param exptype: Expected type (can be None)
246 if default is not _DEFAULT:
249 raise http.HttpBadRequest("Required parameter '%s' is missing" %
252 if exptype is _DEFAULT:
255 return CheckType(value, exptype, "'%s' parameter" % name)
258 class R_Generic(object):
259 """Generic class for resources.
262 # Default permission requirements
264 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
265 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
266 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
268 def __init__(self, items, queryargs, req):
269 """Generic resource constructor.
271 @param items: a list with variables encoded in the URL
272 @param queryargs: a dictionary with additional options from URL
276 self.queryargs = queryargs
279 def _GetRequestBody(self):
280 """Returns the body data.
283 return self._req.private.body_data
285 request_body = property(fget=_GetRequestBody)
287 def _checkIntVariable(self, name, default=0):
288 """Return the parsed value of an int argument.
291 val = self.queryargs.get(name, default)
292 if isinstance(val, list):
299 except (ValueError, TypeError):
300 raise http.HttpBadRequest("Invalid value for the"
301 " '%s' parameter" % (name,))
304 def _checkStringVariable(self, name, default=None):
305 """Return the parsed value of an int argument.
308 val = self.queryargs.get(name, default)
309 if isinstance(val, list):
316 def getBodyParameter(self, name, *args):
317 """Check and return the value for a given parameter.
319 If a second parameter is not given, an error will be returned,
320 otherwise this parameter specifies the default value.
322 @param name: the required parameter
326 return CheckParameter(self.request_body, name, default=args[0])
328 return CheckParameter(self.request_body, name)
330 def useLocking(self):
331 """Check if the request specifies locking.
334 return bool(self._checkIntVariable("lock"))
337 """Check if the request specifies bulk querying.
340 return bool(self._checkIntVariable("bulk"))
343 """Check if the request specifies a forced operation.
346 return bool(self._checkIntVariable("force"))
349 """Check if the request specifies dry-run mode.
352 return bool(self._checkIntVariable("dry-run"))
355 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
358 # Could be a function, pylint: disable=R0201
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")
367 def SubmitJob(self, op, cl=None):
368 """Generic wrapper for submit job, for better http compatibility.
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
379 cl = self.GetClient()
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"