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 utils
49
50
51
52 _DEFAULT = object()
53
54
55 _SUPPORTED_METHODS = compat.UniqueFrozenset([
56 http.HTTP_DELETE,
57 http.HTTP_GET,
58 http.HTTP_POST,
59 http.HTTP_PUT,
60 ])
61
62
64 """Acts as a structure containing the per-method attribute names.
65
66 """
67 __slots__ = [
68 "method",
69 "opcode",
70 "rename",
71 "aliases",
72 "forbidden",
73 "get_input",
74 ]
75
77 """Initializes the opcode attributes for the given method name.
78
79 """
80 self.method = method_name
81 self.opcode = "%s_OPCODE" % method_name
82 self.rename = "%s_RENAME" % method_name
83 self.aliases = "%s_ALIASES" % method_name
84 self.forbidden = "%s_FORBIDDEN" % method_name
85 self.get_input = "Get%sOpInput" % method_name.capitalize()
86
88 """Returns the names of all the attributes that replace or modify a method.
89
90 """
91 return [self.opcode, self.rename, self.aliases, self.forbidden,
92 self.get_input]
93
96
97
103
104
105 OPCODE_ATTRS = _BuildOpcodeAttributes()
106
107
108 -def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
109 """Builds a URI list as used by index resources.
110
111 @param ids: list of ids as strings
112 @param uri_format: format to be applied for URI
113 @param uri_fields: optional parameter for field IDs
114
115 """
116 (field_id, field_uri) = uri_fields
117
118 def _MapId(m_id):
119 return {
120 field_id: m_id,
121 field_uri: uri_format % m_id,
122 }
123
124
125
126 ids.sort()
127
128 return map(_MapId, ids)
129
130
132 """Maps two lists into one dictionary.
133
134 Example::
135 >>> MapFields(["a", "b"], ["foo", 123])
136 {'a': 'foo', 'b': 123}
137
138 @param names: field names (list of strings)
139 @param data: field data (list)
140
141 """
142 if len(names) != len(data):
143 raise AttributeError("Names and data must have the same length")
144 return dict(zip(names, data))
145
146
148 """Map value to field name in to one dictionary.
149
150 @param itemslist: a list of items values
151 @param fields: a list of items names
152
153 @return: a list of mapped dictionaries
154
155 """
156 items_details = []
157 for item in itemslist:
158 mapped = MapFields(fields, item)
159 items_details.append(mapped)
160 return items_details
161
162
164 """Fills an opcode with body parameters.
165
166 Parameter types are checked.
167
168 @type opcls: L{opcodes.OpCode}
169 @param opcls: Opcode class
170 @type body: dict
171 @param body: Body parameters as received from client
172 @type static: dict
173 @param static: Static parameters which can't be modified by client
174 @type rename: dict
175 @param rename: Renamed parameters, key as old name, value as new name
176 @return: Opcode object
177
178 """
179 if body is None:
180 params = {}
181 else:
182 CheckType(body, dict, "Body contents")
183
184
185 params = body.copy()
186
187 if rename:
188 for old, new in rename.items():
189 if new in params and old in params:
190 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
191 " both are specified" %
192 (old, new))
193 if old in params:
194 assert new not in params
195 params[new] = params.pop(old)
196
197 if static:
198 overwritten = set(params.keys()) & set(static.keys())
199 if overwritten:
200 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
201 overwritten)
202
203 params.update(static)
204
205
206 params = dict((str(key), value) for (key, value) in params.items())
207
208 try:
209 op = opcls(**params)
210 op.Validate(False)
211 except (errors.OpPrereqError, TypeError), err:
212 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
213
214 return op
215
216
234
235
237 """Feedback logging function for jobs.
238
239 We don't have a stdout for printing log messages, so log them to the
240 http log at least.
241
242 @param msg: the message
243
244 """
245 (_, log_type, log_msg) = msg
246 logging.info("%s: %s", log_type, log_msg)
247
248
250 """Abort request if value type doesn't match expected type.
251
252 @param value: Value
253 @type exptype: type
254 @param exptype: Expected type
255 @type descr: string
256 @param descr: Description of value
257 @return: Value (allows inline usage)
258
259 """
260 if not isinstance(value, exptype):
261 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
262 (descr, type(value).__name__, exptype.__name__))
263
264 return value
265
266
268 """Check and return the value for a given parameter.
269
270 If no default value was given and the parameter doesn't exist in the input
271 data, an error is raise.
272
273 @type data: dict
274 @param data: Dictionary containing input data
275 @type name: string
276 @param name: Parameter name
277 @param default: Default value (can be None)
278 @param exptype: Expected type (can be None)
279
280 """
281 try:
282 value = data[name]
283 except KeyError:
284 if default is not _DEFAULT:
285 return default
286
287 raise http.HttpBadRequest("Required parameter '%s' is missing" %
288 name)
289
290 if exptype is _DEFAULT:
291 return value
292
293 return CheckType(value, exptype, "'%s' parameter" % name)
294
295
297 """Generic class for resources.
298
299 """
300
301 GET_ACCESS = []
302 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
303 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
304 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
305
306 - def __init__(self, items, queryargs, req, _client_cls=None):
307 """Generic resource constructor.
308
309 @param items: a list with variables encoded in the URL
310 @param queryargs: a dictionary with additional options from URL
311 @param req: Request context
312 @param _client_cls: L{luxi} client class (unittests only)
313
314 """
315 assert isinstance(queryargs, dict)
316
317 self.items = items
318 self.queryargs = queryargs
319 self._req = req
320
321 if _client_cls is None:
322 _client_cls = luxi.Client
323
324 self._client_cls = _client_cls
325
326 self.auth_user = ""
327
328 - def _GetRequestBody(self):
329 """Returns the body data.
330
331 """
332 return self._req.private.body_data
333
334 request_body = property(fget=_GetRequestBody)
335
337 """Return the parsed value of an int argument.
338
339 """
340 val = self.queryargs.get(name, default)
341 if isinstance(val, list):
342 if val:
343 val = val[0]
344 else:
345 val = default
346 try:
347 val = int(val)
348 except (ValueError, TypeError):
349 raise http.HttpBadRequest("Invalid value for the"
350 " '%s' parameter" % (name,))
351 return val
352
354 """Return the parsed value of a string argument.
355
356 """
357 val = self.queryargs.get(name, default)
358 if isinstance(val, list):
359 if val:
360 val = val[0]
361 else:
362 val = default
363 return val
364
365 - def getBodyParameter(self, name, *args):
366 """Check and return the value for a given parameter.
367
368 If a second parameter is not given, an error will be returned,
369 otherwise this parameter specifies the default value.
370
371 @param name: the required parameter
372
373 """
374 if args:
375 return CheckParameter(self.request_body, name, default=args[0])
376
377 return CheckParameter(self.request_body, name)
378
380 """Check if the request specifies locking.
381
382 """
383 return bool(self._checkIntVariable("lock"))
384
386 """Check if the request specifies bulk querying.
387
388 """
389 return bool(self._checkIntVariable("bulk"))
390
392 """Check if the request specifies a forced operation.
393
394 """
395 return bool(self._checkIntVariable("force"))
396
398 """Check if the request specifies dry-run mode.
399
400 """
401 return bool(self._checkIntVariable("dry-run"))
402
404 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
405
406 """
407
408 try:
409 return self._client_cls()
410 except rpcerr.NoMasterError, err:
411 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
412 except rpcerr.PermissionError:
413 raise http.HttpInternalServerError("Internal error: no permission to"
414 " connect to the master daemon")
415
420
453
454
456 """Returns all opcodes used by a resource.
457
458 """
459 return frozenset(filter(None, (getattr(cls, method_attrs.opcode, None)
460 for method_attrs in OPCODE_ATTRS)))
461
462
464 """Returns the access rights for a method on a handler.
465
466 @type handler: L{ResourceBase}
467 @type method: string
468 @rtype: string or None
469
470 """
471 return getattr(handler, "%s_ACCESS" % method, None)
472
473
475 result = get_fn()
476 if not isinstance(result, dict) or aliases is None:
477 return result
478
479 for (param, alias) in aliases.items():
480 if param in result:
481 if alias in result:
482 raise http.HttpBadRequest("Parameter '%s' has an alias of '%s', but"
483 " both values are present in response" %
484 (param, alias))
485 result[alias] = result[param]
486
487 return result
488
489
490
491 ALL_VALUES_FORBIDDEN = "all_values_forbidden"
492
493
495 """Turns a list of parameter names and possibly values into a dictionary.
496
497 @type class_name: string
498 @param class_name: The name of the handler class
499 @type method_name: string
500 @param method_name: The name of the HTTP method
501 @type param_list: list of string or tuple of (string, list of any)
502 @param param_list: A list of forbidden parameters, specified in the RAPI
503 handler class
504
505 @return: The dictionary of forbidden param names to values or
506 ALL_VALUES_FORBIDDEN
507
508 """
509
510 def _RaiseError(message):
511 raise errors.ProgrammerError(
512 "While examining the %s_FORBIDDEN field of class %s: %s" %
513 (method_name, class_name, message)
514 )
515
516 param_dict = {}
517 for value in param_list:
518 if isinstance(value, basestring):
519 param_dict[value] = ALL_VALUES_FORBIDDEN
520 elif isinstance(value, tuple):
521 if len(value) != 2:
522 _RaiseError("Tuples of only length 2 allowed")
523 param_name, forbidden_values = value
524 param_dict[param_name] = forbidden_values
525 else:
526 _RaiseError("Only strings or tuples allowed, found %s" % value)
527
528 return param_dict
529
530
532 """Inspects a dictionary of params, looking for forbidden values.
533
534 @type params_dict: dict of string to anything
535 @param params_dict: A dictionary of supplied parameters
536 @type forbidden_params: dict of string to string or list of any
537 @param forbidden_params: The forbidden parameters, with a list of forbidden
538 values or the constant ALL_VALUES_FORBIDDEN
539 signifying that all values are forbidden
540 @type rename_dict: None or dict of string to string
541 @param rename_dict: The list of parameter renamings used by the method
542
543 @raise http.HttpForbidden: If a forbidden param has been set
544
545 """
546 for param in params_dict:
547
548 if rename_dict is not None and param in rename_dict:
549 param = rename_dict[param]
550
551
552 if param in forbidden_params:
553 forbidden_values = forbidden_params[param]
554 if forbidden_values == ALL_VALUES_FORBIDDEN:
555 raise http.HttpForbidden("The parameter %s cannot be set via RAPI" %
556 param)
557
558 param_value = params_dict[param]
559 if param_value in forbidden_values:
560 raise http.HttpForbidden("The parameter %s cannot be set to the value"
561 " %s via RAPI" % (param, param_value))
562
563
619
620
622 """Base class for opcode-based RAPI resources.
623
624 Instances of this class automatically gain handler functions through
625 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
626 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
627 method to do their own opcode input processing (e.g. for static values). The
628 C{$METHOD$_RENAME} variable defines which values are renamed (see
629 L{baserlib.FillOpcode}).
630 Still default behavior cannot be totally overriden. There are opcode params
631 that are available to all opcodes, e.g. "depends". In case those params
632 (currently only "depends") are found in the original request's body, they are
633 added to the dictionary of parsed parameters and eventually passed to the
634 opcode. If the parsed body is not represented as a dictionary object, the
635 values are not added.
636
637 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
638 automatically generate a GET handler submitting the opcode
639 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
640 L{baserlib.FillOpcode})
641 @cvar GET_FORBIDDEN: Set this to disable listed parameters and optionally
642 specific values from being set through the GET handler (see
643 L{baserlib.InspectParams})
644 @cvar GET_ALIASES: Set this to duplicate return values in GET results (see
645 L{baserlib.GetHandler})
646 @ivar GetGetOpInput: Define this to override the default method for
647 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
648
649 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
650 automatically generate a PUT handler submitting the opcode
651 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
652 L{baserlib.FillOpcode})
653 @cvar PUT_FORBIDDEN: Set this to disable listed parameters and optionally
654 specific values from being set through the PUT handler (see
655 L{baserlib.InspectParams})
656 @ivar GetPutOpInput: Define this to override the default method for
657 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
658
659 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
660 automatically generate a POST handler submitting the opcode
661 @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
662 L{baserlib.FillOpcode})
663 @cvar POST_FORBIDDEN: Set this to disable listed parameters and optionally
664 specific values from being set through the POST handler (see
665 L{baserlib.InspectParams})
666 @ivar GetPostOpInput: Define this to override the default method for
667 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
668
669 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
670 automatically generate a DELETE handler submitting the opcode
671 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
672 L{baserlib.FillOpcode})
673 @cvar DELETE_FORBIDDEN: Set this to disable listed parameters and optionally
674 specific values from being set through the DELETE handler (see
675 L{baserlib.InspectParams})
676 @ivar GetDeleteOpInput: Define this to override the default method for
677 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
678
679 """
680 __metaclass__ = _MetaOpcodeResource
681
683 """Examines provided parameters for forbidden values.
684
685 """
686 InspectParams(self.queryargs, forbidden_params, rename_dict)
687 InspectParams(self.request_body, forbidden_params, rename_dict)
688 return method_fn()
689
692
694 """Extracts the name of the RAPI operation from the class name
695
696 """
697 if self.__class__.__name__.startswith("R_2_"):
698 return self.__class__.__name__[4:]
699 return self.__class__.__name__
700
702 """Return the static parameters common to all the RAPI calls
703
704 The reason is a parameter present in all the RAPI calls, and the reason
705 trail has to be build for all of them, so the parameter is read here and
706 used to build the reason trail, that is the actual parameter passed
707 forward.
708
709 """
710 trail = []
711 usr_reason = self._checkStringVariable("reason", default=None)
712 if usr_reason:
713 trail.append((constants.OPCODE_REASON_SRC_USER,
714 usr_reason,
715 utils.EpochNano()))
716 reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
717 self._GetRapiOpName())
718 trail.append((reason_src, "", utils.EpochNano()))
719 common_static = {
720 "reason": trail,
721 }
722 return common_static
723
725 ret = {}
726 if isinstance(self.request_body, dict):
727 depends = self.getBodyParameter("depends", None)
728 if depends:
729 ret.update({"depends": depends})
730 return ret
731
741