1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Remote API base resources library.
32
33 """
34
35
36
37
38
39 import logging
40
41 from ganeti import luxi
42 import ganeti.rpc.errors as rpcerr
43 from ganeti import rapi
44 from ganeti import http
45 from ganeti import errors
46 from ganeti import compat
47 from ganeti import constants
48 from ganeti import pathutils
49 from ganeti import utils
50
51
52
53 _DEFAULT = object()
54
55
56 _SUPPORTED_METHODS = compat.UniqueFrozenset([
57 http.HTTP_DELETE,
58 http.HTTP_GET,
59 http.HTTP_POST,
60 http.HTTP_PUT,
61 ])
62
63
65 """Builds list of attributes used for per-handler opcodes.
66
67 """
68 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
69 "%s_ALIASES" % method, "Get%sOpInput" % method.capitalize())
70 for method in _SUPPORTED_METHODS]
71
72
73 OPCODE_ATTRS = _BuildOpcodeAttributes()
74
75
76 -def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
77 """Builds a URI list as used by index resources.
78
79 @param ids: list of ids as strings
80 @param uri_format: format to be applied for URI
81 @param uri_fields: optional parameter for field IDs
82
83 """
84 (field_id, field_uri) = uri_fields
85
86 def _MapId(m_id):
87 return {
88 field_id: m_id,
89 field_uri: uri_format % m_id,
90 }
91
92
93
94 ids.sort()
95
96 return map(_MapId, ids)
97
98
100 """Maps two lists into one dictionary.
101
102 Example::
103 >>> MapFields(["a", "b"], ["foo", 123])
104 {'a': 'foo', 'b': 123}
105
106 @param names: field names (list of strings)
107 @param data: field data (list)
108
109 """
110 if len(names) != len(data):
111 raise AttributeError("Names and data must have the same length")
112 return dict(zip(names, data))
113
114
116 """Map value to field name in to one dictionary.
117
118 @param itemslist: a list of items values
119 @param fields: a list of items names
120
121 @return: a list of mapped dictionaries
122
123 """
124 items_details = []
125 for item in itemslist:
126 mapped = MapFields(fields, item)
127 items_details.append(mapped)
128 return items_details
129
130
132 """Fills an opcode with body parameters.
133
134 Parameter types are checked.
135
136 @type opcls: L{opcodes.OpCode}
137 @param opcls: Opcode class
138 @type body: dict
139 @param body: Body parameters as received from client
140 @type static: dict
141 @param static: Static parameters which can't be modified by client
142 @type rename: dict
143 @param rename: Renamed parameters, key as old name, value as new name
144 @return: Opcode object
145
146 """
147 if body is None:
148 params = {}
149 else:
150 CheckType(body, dict, "Body contents")
151
152
153 params = body.copy()
154
155 if rename:
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" %
160 (old, new))
161 if old in params:
162 assert new not in params
163 params[new] = params.pop(old)
164
165 if static:
166 overwritten = set(params.keys()) & set(static.keys())
167 if overwritten:
168 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
169 overwritten)
170
171 params.update(static)
172
173
174 params = dict((str(key), value) for (key, value) in params.items())
175
176 try:
177 op = opcls(**params)
178 op.Validate(False)
179 except (errors.OpPrereqError, TypeError), err:
180 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
181
182 return op
183
184
202
203
205 """Feedback logging function for jobs.
206
207 We don't have a stdout for printing log messages, so log them to the
208 http log at least.
209
210 @param msg: the message
211
212 """
213 (_, log_type, log_msg) = msg
214 logging.info("%s: %s", log_type, log_msg)
215
216
218 """Abort request if value type doesn't match expected type.
219
220 @param value: Value
221 @type exptype: type
222 @param exptype: Expected type
223 @type descr: string
224 @param descr: Description of value
225 @return: Value (allows inline usage)
226
227 """
228 if not isinstance(value, exptype):
229 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
230 (descr, type(value).__name__, exptype.__name__))
231
232 return value
233
234
236 """Check and return the value for a given parameter.
237
238 If no default value was given and the parameter doesn't exist in the input
239 data, an error is raise.
240
241 @type data: dict
242 @param data: Dictionary containing input data
243 @type name: string
244 @param name: Parameter name
245 @param default: Default value (can be None)
246 @param exptype: Expected type (can be None)
247
248 """
249 try:
250 value = data[name]
251 except KeyError:
252 if default is not _DEFAULT:
253 return default
254
255 raise http.HttpBadRequest("Required parameter '%s' is missing" %
256 name)
257
258 if exptype is _DEFAULT:
259 return value
260
261 return CheckType(value, exptype, "'%s' parameter" % name)
262
263
265 """Generic class for resources.
266
267 """
268
269 GET_ACCESS = []
270 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
271 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
272 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
273
274 - def __init__(self, items, queryargs, req, _client_cls=None):
275 """Generic resource constructor.
276
277 @param items: a list with variables encoded in the URL
278 @param queryargs: a dictionary with additional options from URL
279 @param req: Request context
280 @param _client_cls: L{luxi} client class (unittests only)
281
282 """
283 assert isinstance(queryargs, dict)
284
285 self.items = items
286 self.queryargs = queryargs
287 self._req = req
288
289 if _client_cls is None:
290 _client_cls = luxi.Client
291
292 self._client_cls = _client_cls
293
294 - def _GetRequestBody(self):
295 """Returns the body data.
296
297 """
298 return self._req.private.body_data
299
300 request_body = property(fget=_GetRequestBody)
301
303 """Return the parsed value of an int argument.
304
305 """
306 val = self.queryargs.get(name, default)
307 if isinstance(val, list):
308 if val:
309 val = val[0]
310 else:
311 val = default
312 try:
313 val = int(val)
314 except (ValueError, TypeError):
315 raise http.HttpBadRequest("Invalid value for the"
316 " '%s' parameter" % (name,))
317 return val
318
320 """Return the parsed value of a string argument.
321
322 """
323 val = self.queryargs.get(name, default)
324 if isinstance(val, list):
325 if val:
326 val = val[0]
327 else:
328 val = default
329 return val
330
331 - def getBodyParameter(self, name, *args):
332 """Check and return the value for a given parameter.
333
334 If a second parameter is not given, an error will be returned,
335 otherwise this parameter specifies the default value.
336
337 @param name: the required parameter
338
339 """
340 if args:
341 return CheckParameter(self.request_body, name, default=args[0])
342
343 return CheckParameter(self.request_body, name)
344
346 """Check if the request specifies locking.
347
348 """
349 return bool(self._checkIntVariable("lock"))
350
352 """Check if the request specifies bulk querying.
353
354 """
355 return bool(self._checkIntVariable("bulk"))
356
358 """Check if the request specifies a forced operation.
359
360 """
361 return bool(self._checkIntVariable("force"))
362
364 """Check if the request specifies dry-run mode.
365
366 """
367 return bool(self._checkIntVariable("dry-run"))
368
370 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
371
372 @param query: this signifies that the client will only be used for
373 queries; if the build-time parameter enable-split-queries is
374 enabled, then the client will be connected to the query socket
375 instead of the masterd socket
376
377 """
378 if query:
379 address = pathutils.QUERY_SOCKET
380 else:
381 address = None
382
383 try:
384 return self._client_cls(address=address)
385 except rpcerr.NoMasterError, err:
386 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
387 except rpcerr.PermissionError:
388 raise http.HttpInternalServerError("Internal error: no permission to"
389 " connect to the master daemon")
390
418
419
421 """Returns all opcodes used by a resource.
422
423 """
424 return frozenset(filter(None, (getattr(cls, op_attr, None)
425 for (_, op_attr, _, _, _) in OPCODE_ATTRS)))
426
427
429 """Returns the access rights for a method on a handler.
430
431 @type handler: L{ResourceBase}
432 @type method: string
433 @rtype: string or None
434
435 """
436 return getattr(handler, "%s_ACCESS" % method, None)
437
438
440 result = get_fn()
441 if not isinstance(result, dict) or aliases is None:
442 return result
443
444 for (param, alias) in aliases.items():
445 if param in result:
446 if alias in result:
447 raise http.HttpBadRequest("Parameter '%s' has an alias of '%s', but"
448 " both values are present in response" %
449 (param, alias))
450 result[alias] = result[param]
451
452 return result
453
454
496
497
499 """Base class for opcode-based RAPI resources.
500
501 Instances of this class automatically gain handler functions through
502 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
503 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
504 method to do their own opcode input processing (e.g. for static values). The
505 C{$METHOD$_RENAME} variable defines which values are renamed (see
506 L{baserlib.FillOpcode}).
507 Still default behavior cannot be totally overriden. There are opcode params
508 that are available to all opcodes, e.g. "depends". In case those params
509 (currently only "depends") are found in the original request's body, they are
510 added to the dictionary of parsed parameters and eventually passed to the
511 opcode. If the parsed body is not represented as a dictionary object, the
512 values are not added.
513
514 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
515 automatically generate a GET handler submitting the opcode
516 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
517 L{baserlib.FillOpcode})
518 @cvar GET_ALIASES: Set this to duplicate return values in GET results (see
519 L{baserlib.GetHandler})
520 @ivar GetGetOpInput: Define this to override the default method for
521 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
522
523 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
524 automatically generate a PUT handler submitting the opcode
525 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
526 L{baserlib.FillOpcode})
527 @ivar GetPutOpInput: Define this to override the default method for
528 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
529
530 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
531 automatically generate a POST handler submitting the opcode
532 @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
533 L{baserlib.FillOpcode})
534 @ivar GetPostOpInput: Define this to override the default method for
535 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
536
537 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
538 automatically generate a DELETE handler submitting the opcode
539 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
540 L{baserlib.FillOpcode})
541 @ivar GetDeleteOpInput: Define this to override the default method for
542 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
543
544 """
545 __metaclass__ = _MetaOpcodeResource
546
549
551 """Extracts the name of the RAPI operation from the class name
552
553 """
554 if self.__class__.__name__.startswith("R_2_"):
555 return self.__class__.__name__[4:]
556 return self.__class__.__name__
557
559 """Return the static parameters common to all the RAPI calls
560
561 The reason is a parameter present in all the RAPI calls, and the reason
562 trail has to be build for all of them, so the parameter is read here and
563 used to build the reason trail, that is the actual parameter passed
564 forward.
565
566 """
567 trail = []
568 usr_reason = self._checkStringVariable("reason", default=None)
569 if usr_reason:
570 trail.append((constants.OPCODE_REASON_SRC_USER,
571 usr_reason,
572 utils.EpochNano()))
573 reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
574 self._GetRapiOpName())
575 trail.append((reason_src, "", utils.EpochNano()))
576 common_static = {
577 "reason": trail,
578 }
579 return common_static
580
582 ret = {}
583 if isinstance(self.request_body, dict):
584 depends = self.getBodyParameter("depends", None)
585 if depends:
586 ret.update({"depends": depends})
587 return ret
588
590 (body, specific_static) = fn()
591 if isinstance(body, dict):
592 body.update(self._GetDepends())
593 static = self._GetCommonStatic()
594 if specific_static:
595 static.update(specific_static)
596 op = FillOpcode(opcode, body, static, rename=rename)
597 return self.SubmitJob([op])
598