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 - def _GetRequestBody(self):
327 """Returns the body data.
328
329 """
330 return self._req.private.body_data
331
332 request_body = property(fget=_GetRequestBody)
333
335 """Return the parsed value of an int argument.
336
337 """
338 val = self.queryargs.get(name, default)
339 if isinstance(val, list):
340 if val:
341 val = val[0]
342 else:
343 val = default
344 try:
345 val = int(val)
346 except (ValueError, TypeError):
347 raise http.HttpBadRequest("Invalid value for the"
348 " '%s' parameter" % (name,))
349 return val
350
352 """Return the parsed value of a string argument.
353
354 """
355 val = self.queryargs.get(name, default)
356 if isinstance(val, list):
357 if val:
358 val = val[0]
359 else:
360 val = default
361 return val
362
363 - def getBodyParameter(self, name, *args):
364 """Check and return the value for a given parameter.
365
366 If a second parameter is not given, an error will be returned,
367 otherwise this parameter specifies the default value.
368
369 @param name: the required parameter
370
371 """
372 if args:
373 return CheckParameter(self.request_body, name, default=args[0])
374
375 return CheckParameter(self.request_body, name)
376
378 """Check if the request specifies locking.
379
380 """
381 return bool(self._checkIntVariable("lock"))
382
384 """Check if the request specifies bulk querying.
385
386 """
387 return bool(self._checkIntVariable("bulk"))
388
390 """Check if the request specifies a forced operation.
391
392 """
393 return bool(self._checkIntVariable("force"))
394
396 """Check if the request specifies dry-run mode.
397
398 """
399 return bool(self._checkIntVariable("dry-run"))
400
402 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
403
404 """
405
406 try:
407 return self._client_cls()
408 except rpcerr.NoMasterError, err:
409 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
410 except rpcerr.PermissionError:
411 raise http.HttpInternalServerError("Internal error: no permission to"
412 " connect to the master daemon")
413
441
442
444 """Returns all opcodes used by a resource.
445
446 """
447 return frozenset(filter(None, (getattr(cls, method_attrs.opcode, None)
448 for method_attrs in OPCODE_ATTRS)))
449
450
452 """Returns the access rights for a method on a handler.
453
454 @type handler: L{ResourceBase}
455 @type method: string
456 @rtype: string or None
457
458 """
459 return getattr(handler, "%s_ACCESS" % method, None)
460
461
463 result = get_fn()
464 if not isinstance(result, dict) or aliases is None:
465 return result
466
467 for (param, alias) in aliases.items():
468 if param in result:
469 if alias in result:
470 raise http.HttpBadRequest("Parameter '%s' has an alias of '%s', but"
471 " both values are present in response" %
472 (param, alias))
473 result[alias] = result[param]
474
475 return result
476
477
478
479 ALL_VALUES_FORBIDDEN = "all_values_forbidden"
480
481
483 """Turns a list of parameter names and possibly values into a dictionary.
484
485 @type class_name: string
486 @param class_name: The name of the handler class
487 @type method_name: string
488 @param method_name: The name of the HTTP method
489 @type param_list: list of string or tuple of (string, list of any)
490 @param param_list: A list of forbidden parameters, specified in the RAPI
491 handler class
492
493 @return: The dictionary of forbidden param names to values or
494 ALL_VALUES_FORBIDDEN
495
496 """
497
498 def _RaiseError(message):
499 raise errors.ProgrammerError(
500 "While examining the %s_FORBIDDEN field of class %s: %s" %
501 (method_name, class_name, message)
502 )
503
504 param_dict = {}
505 for value in param_list:
506 if isinstance(value, basestring):
507 param_dict[value] = ALL_VALUES_FORBIDDEN
508 elif isinstance(value, tuple):
509 if len(value) != 2:
510 _RaiseError("Tuples of only length 2 allowed")
511 param_name, forbidden_values = value
512 param_dict[param_name] = forbidden_values
513 else:
514 _RaiseError("Only strings or tuples allowed, found %s" % value)
515
516 return param_dict
517
518
520 """Inspects a dictionary of params, looking for forbidden values.
521
522 @type params_dict: dict of string to anything
523 @param params_dict: A dictionary of supplied parameters
524 @type forbidden_params: dict of string to string or list of any
525 @param forbidden_params: The forbidden parameters, with a list of forbidden
526 values or the constant ALL_VALUES_FORBIDDEN
527 signifying that all values are forbidden
528 @type rename_dict: None or dict of string to string
529 @param rename_dict: The list of parameter renamings used by the method
530
531 @raise http.HttpForbidden: If a forbidden param has been set
532
533 """
534 for param in params_dict:
535
536 if rename_dict is not None and param in rename_dict:
537 param = rename_dict[param]
538
539
540 if param in forbidden_params:
541 forbidden_values = forbidden_params[param]
542 if forbidden_values == ALL_VALUES_FORBIDDEN:
543 raise http.HttpForbidden("The parameter %s cannot be set via RAPI" %
544 param)
545
546 param_value = params_dict[param]
547 if param_value in forbidden_values:
548 raise http.HttpForbidden("The parameter %s cannot be set to the value"
549 " %s via RAPI" % (param, param_value))
550
551
607
608
610 """Base class for opcode-based RAPI resources.
611
612 Instances of this class automatically gain handler functions through
613 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
614 is defined at class level. Subclasses can define a C{Get$Method$OpInput}
615 method to do their own opcode input processing (e.g. for static values). The
616 C{$METHOD$_RENAME} variable defines which values are renamed (see
617 L{baserlib.FillOpcode}).
618 Still default behavior cannot be totally overriden. There are opcode params
619 that are available to all opcodes, e.g. "depends". In case those params
620 (currently only "depends") are found in the original request's body, they are
621 added to the dictionary of parsed parameters and eventually passed to the
622 opcode. If the parsed body is not represented as a dictionary object, the
623 values are not added.
624
625 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
626 automatically generate a GET handler submitting the opcode
627 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
628 L{baserlib.FillOpcode})
629 @cvar GET_FORBIDDEN: Set this to disable listed parameters and optionally
630 specific values from being set through the GET handler (see
631 L{baserlib.InspectParams})
632 @cvar GET_ALIASES: Set this to duplicate return values in GET results (see
633 L{baserlib.GetHandler})
634 @ivar GetGetOpInput: Define this to override the default method for
635 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
636
637 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
638 automatically generate a PUT handler submitting the opcode
639 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
640 L{baserlib.FillOpcode})
641 @cvar PUT_FORBIDDEN: Set this to disable listed parameters and optionally
642 specific values from being set through the PUT handler (see
643 L{baserlib.InspectParams})
644 @ivar GetPutOpInput: Define this to override the default method for
645 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
646
647 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
648 automatically generate a POST handler submitting the opcode
649 @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
650 L{baserlib.FillOpcode})
651 @cvar POST_FORBIDDEN: Set this to disable listed parameters and optionally
652 specific values from being set through the POST handler (see
653 L{baserlib.InspectParams})
654 @ivar GetPostOpInput: Define this to override the default method for
655 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
656
657 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
658 automatically generate a DELETE handler submitting the opcode
659 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
660 L{baserlib.FillOpcode})
661 @cvar DELETE_FORBIDDEN: Set this to disable listed parameters and optionally
662 specific values from being set through the DELETE handler (see
663 L{baserlib.InspectParams})
664 @ivar GetDeleteOpInput: Define this to override the default method for
665 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
666
667 """
668 __metaclass__ = _MetaOpcodeResource
669
671 """Examines provided parameters for forbidden values.
672
673 """
674 InspectParams(self.queryargs, forbidden_params, rename_dict)
675 InspectParams(self.request_body, forbidden_params, rename_dict)
676 return method_fn()
677
680
682 """Extracts the name of the RAPI operation from the class name
683
684 """
685 if self.__class__.__name__.startswith("R_2_"):
686 return self.__class__.__name__[4:]
687 return self.__class__.__name__
688
690 """Return the static parameters common to all the RAPI calls
691
692 The reason is a parameter present in all the RAPI calls, and the reason
693 trail has to be build for all of them, so the parameter is read here and
694 used to build the reason trail, that is the actual parameter passed
695 forward.
696
697 """
698 trail = []
699 usr_reason = self._checkStringVariable("reason", default=None)
700 if usr_reason:
701 trail.append((constants.OPCODE_REASON_SRC_USER,
702 usr_reason,
703 utils.EpochNano()))
704 reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
705 self._GetRapiOpName())
706 trail.append((reason_src, "", utils.EpochNano()))
707 common_static = {
708 "reason": trail,
709 }
710 return common_static
711
713 ret = {}
714 if isinstance(self.request_body, dict):
715 depends = self.getBodyParameter("depends", None)
716 if depends:
717 ret.update({"depends": depends})
718 return ret
719
729