1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Remote API base resources library.
23
24 """
25
26
27
28
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 from ganeti import compat
37 from ganeti import constants
38 from ganeti import pathutils
39
40
41
42 _DEFAULT = object()
43
44
45 _SUPPORTED_METHODS = compat.UniqueFrozenset([
46 http.HTTP_DELETE,
47 http.HTTP_GET,
48 http.HTTP_POST,
49 http.HTTP_PUT,
50 ])
51
52
54 """Builds list of attributes used for per-handler opcodes.
55
56 """
57 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
58 "Get%sOpInput" % method.capitalize())
59 for method in _SUPPORTED_METHODS]
60
61
62 _OPCODE_ATTRS = _BuildOpcodeAttributes()
63
64
65 -def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
66 """Builds a URI list as used by index resources.
67
68 @param ids: list of ids as strings
69 @param uri_format: format to be applied for URI
70 @param uri_fields: optional parameter for field IDs
71
72 """
73 (field_id, field_uri) = uri_fields
74
75 def _MapId(m_id):
76 return {
77 field_id: m_id,
78 field_uri: uri_format % m_id,
79 }
80
81
82
83 ids.sort()
84
85 return map(_MapId, ids)
86
87
89 """Maps two lists into one dictionary.
90
91 Example::
92 >>> MapFields(["a", "b"], ["foo", 123])
93 {'a': 'foo', 'b': 123}
94
95 @param names: field names (list of strings)
96 @param data: field data (list)
97
98 """
99 if len(names) != len(data):
100 raise AttributeError("Names and data must have the same length")
101 return dict(zip(names, data))
102
103
105 """Map value to field name in to one dictionary.
106
107 @param itemslist: a list of items values
108 @param fields: a list of items names
109
110 @return: a list of mapped dictionaries
111
112 """
113 items_details = []
114 for item in itemslist:
115 mapped = MapFields(fields, item)
116 items_details.append(mapped)
117 return items_details
118
119
121 """Fills an opcode with body parameters.
122
123 Parameter types are checked.
124
125 @type opcls: L{opcodes.OpCode}
126 @param opcls: Opcode class
127 @type body: dict
128 @param body: Body parameters as received from client
129 @type static: dict
130 @param static: Static parameters which can't be modified by client
131 @type rename: dict
132 @param rename: Renamed parameters, key as old name, value as new name
133 @return: Opcode object
134
135 """
136 if body is None:
137 params = {}
138 else:
139 CheckType(body, dict, "Body contents")
140
141
142 params = body.copy()
143
144 if rename:
145 for old, new in rename.items():
146 if new in params and old in params:
147 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
148 " both are specified" %
149 (old, new))
150 if old in params:
151 assert new not in params
152 params[new] = params.pop(old)
153
154 if static:
155 overwritten = set(params.keys()) & set(static.keys())
156 if overwritten:
157 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
158 overwritten)
159
160 params.update(static)
161
162
163 params = dict((str(key), value) for (key, value) in params.items())
164
165 try:
166 op = opcls(**params)
167 op.Validate(False)
168 except (errors.OpPrereqError, TypeError), err:
169 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
170
171 return op
172
173
185
186
188 """Feedback logging function for jobs.
189
190 We don't have a stdout for printing log messages, so log them to the
191 http log at least.
192
193 @param msg: the message
194
195 """
196 (_, log_type, log_msg) = msg
197 logging.info("%s: %s", log_type, log_msg)
198
199
201 """Abort request if value type doesn't match expected type.
202
203 @param value: Value
204 @type exptype: type
205 @param exptype: Expected type
206 @type descr: string
207 @param descr: Description of value
208 @return: Value (allows inline usage)
209
210 """
211 if not isinstance(value, exptype):
212 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
213 (descr, type(value).__name__, exptype.__name__))
214
215 return value
216
217
219 """Check and return the value for a given parameter.
220
221 If no default value was given and the parameter doesn't exist in the input
222 data, an error is raise.
223
224 @type data: dict
225 @param data: Dictionary containing input data
226 @type name: string
227 @param name: Parameter name
228 @param default: Default value (can be None)
229 @param exptype: Expected type (can be None)
230
231 """
232 try:
233 value = data[name]
234 except KeyError:
235 if default is not _DEFAULT:
236 return default
237
238 raise http.HttpBadRequest("Required parameter '%s' is missing" %
239 name)
240
241 if exptype is _DEFAULT:
242 return value
243
244 return CheckType(value, exptype, "'%s' parameter" % name)
245
246
248 """Generic class for resources.
249
250 """
251
252 GET_ACCESS = []
253 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
254 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
255 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
256
257 - def __init__(self, items, queryargs, req, _client_cls=None):
258 """Generic resource constructor.
259
260 @param items: a list with variables encoded in the URL
261 @param queryargs: a dictionary with additional options from URL
262 @param req: Request context
263 @param _client_cls: L{luxi} client class (unittests only)
264
265 """
266 self.items = items
267 self.queryargs = queryargs
268 self._req = req
269
270 if _client_cls is None:
271 _client_cls = luxi.Client
272
273 self._client_cls = _client_cls
274
275 - def _GetRequestBody(self):
276 """Returns the body data.
277
278 """
279 return self._req.private.body_data
280
281 request_body = property(fget=_GetRequestBody)
282
284 """Return the parsed value of an int argument.
285
286 """
287 val = self.queryargs.get(name, default)
288 if isinstance(val, list):
289 if val:
290 val = val[0]
291 else:
292 val = default
293 try:
294 val = int(val)
295 except (ValueError, TypeError):
296 raise http.HttpBadRequest("Invalid value for the"
297 " '%s' parameter" % (name,))
298 return val
299
301 """Return the parsed value of a string argument.
302
303 """
304 val = self.queryargs.get(name, default)
305 if isinstance(val, list):
306 if val:
307 val = val[0]
308 else:
309 val = default
310 return val
311
312 - def getBodyParameter(self, name, *args):
313 """Check and return the value for a given parameter.
314
315 If a second parameter is not given, an error will be returned,
316 otherwise this parameter specifies the default value.
317
318 @param name: the required parameter
319
320 """
321 if args:
322 return CheckParameter(self.request_body, name, default=args[0])
323
324 return CheckParameter(self.request_body, name)
325
327 """Check if the request specifies locking.
328
329 """
330 return bool(self._checkIntVariable("lock"))
331
333 """Check if the request specifies bulk querying.
334
335 """
336 return bool(self._checkIntVariable("bulk"))
337
339 """Check if the request specifies a forced operation.
340
341 """
342 return bool(self._checkIntVariable("force"))
343
345 """Check if the request specifies dry-run mode.
346
347 """
348 return bool(self._checkIntVariable("dry-run"))
349
351 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
352
353 @param query: this signifies that the client will only be used for
354 queries; if the build-time parameter enable-split-queries is
355 enabled, then the client will be connected to the query socket
356 instead of the masterd socket
357
358 """
359 if query and constants.ENABLE_SPLIT_QUERY:
360 address = pathutils.QUERY_SOCKET
361 else:
362 address = None
363
364 try:
365 return self._client_cls(address=address)
366 except luxi.NoMasterError, err:
367 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
368 except luxi.PermissionError:
369 raise http.HttpInternalServerError("Internal error: no permission to"
370 " connect to the master daemon")
371
399
400
402 """Returns all opcodes used by a resource.
403
404 """
405 return frozenset(filter(None, (getattr(cls, op_attr, None)
406 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
407
408
440
441
443 """Base class for opcode-based RAPI resources.
444
445 Instances of this class automatically gain handler functions through
446 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
447 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
448 method to do their own opcode input processing (e.g. for static values). The
449 C{$METHOD$_RENAME} variable defines which values are renamed (see
450 L{baserlib.FillOpcode}).
451
452 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
453 automatically generate a GET handler submitting the opcode
454 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
455 L{baserlib.FillOpcode})
456 @ivar GetGetOpInput: Define this to override the default method for
457 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
458
459 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
460 automatically generate a PUT handler submitting the opcode
461 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
462 L{baserlib.FillOpcode})
463 @ivar GetPutOpInput: Define this to override the default method for
464 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
465
466 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
467 automatically generate a POST handler submitting the opcode
468 @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
469 L{baserlib.FillOpcode})
470 @ivar GetPostOpInput: Define this to override the default method for
471 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
472
473 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
474 automatically generate a DELETE handler submitting the opcode
475 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
476 L{baserlib.FillOpcode})
477 @ivar GetDeleteOpInput: Define this to override the default method for
478 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
479
480 """
481 __metaclass__ = _MetaOpcodeResource
482
485
487 (body, static) = fn()
488 op = FillOpcode(opcode, body, static, rename=rename)
489 return self.SubmitJob([op])
490