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