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