Package ganeti :: Package rapi :: Module baserlib
[hide private]
[frames] | no frames]

Source Code for Module ganeti.rapi.baserlib

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2006, 2007, 2008, 2012 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Remote API base resources library. 
 32   
 33  """ 
 34   
 35  # pylint: disable=C0103 
 36   
 37  # C0103: Invalid name, since the R_* names are not conforming 
 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  # Dummy value to detect unchanged parameters 
 52  _DEFAULT = object() 
 53   
 54  #: Supported HTTP methods 
 55  _SUPPORTED_METHODS = compat.UniqueFrozenset([ 
 56    http.HTTP_DELETE, 
 57    http.HTTP_GET, 
 58    http.HTTP_POST, 
 59    http.HTTP_PUT, 
 60    ]) 
 61   
 62   
63 -class OpcodeAttributes(object):
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
76 - def __init__(self, method_name):
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
87 - def GetModifiers(self):
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
94 - def GetAll(self):
95 return [self.method] + self.GetModifiers()
96 97
98 -def _BuildOpcodeAttributes():
99 """Builds list of attributes used for per-handler opcodes. 100 101 """ 102 return [OpcodeAttributes(method) for method in _SUPPORTED_METHODS]
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 # Make sure the result is sorted, makes it nicer to look at and simplifies 125 # unittests. 126 ids.sort() 127 128 return map(_MapId, ids) 129 130
131 -def MapFields(names, data):
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
147 -def MapBulkFields(itemslist, fields):
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
163 -def FillOpcode(opcls, body, static, rename=None):
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 # Make copy to be modified 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 # Convert keys to strings (simplejson decodes them as unicode) 206 params = dict((str(key), value) for (key, value) in params.items()) 207 208 try: 209 op = opcls(**params) # pylint: disable=W0142 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
217 -def HandleItemQueryErrors(fn, *args, **kwargs):
218 """Converts errors when querying a single item. 219 220 """ 221 try: 222 result = fn(*args, **kwargs) 223 except errors.OpPrereqError, err: 224 if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT: 225 raise http.HttpNotFound() 226 227 raise 228 229 # In case split query mechanism is used 230 if not result: 231 raise http.HttpNotFound() 232 233 return result
234 235
236 -def FeedbackFn(msg):
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
249 -def CheckType(value, exptype, descr):
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
267 -def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
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
296 -class ResourceBase(object):
297 """Generic class for resources. 298 299 """ 300 # Default permission requirements 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
336 - def _checkIntVariable(self, name, default=0):
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
353 - def _checkStringVariable(self, name, default=None):
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
379 - def useLocking(self):
380 """Check if the request specifies locking. 381 382 """ 383 return bool(self._checkIntVariable("lock"))
384
385 - def useBulk(self):
386 """Check if the request specifies bulk querying. 387 388 """ 389 return bool(self._checkIntVariable("bulk"))
390
391 - def useForce(self):
392 """Check if the request specifies a forced operation. 393 394 """ 395 return bool(self._checkIntVariable("force"))
396
397 - def dryRun(self):
398 """Check if the request specifies dry-run mode. 399 400 """ 401 return bool(self._checkIntVariable("dry-run"))
402
403 - def GetClient(self):
404 """Wrapper for L{luxi.Client} with HTTP-specific error handling. 405 406 """ 407 # Could be a function, pylint: disable=R0201 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
416 - def GetAuthReason(self):
420
421 - def SubmitJob(self, op, cl=None):
422 """Generic wrapper for submit job, for better http compatibility. 423 424 @type op: list 425 @param op: the list of opcodes for the job 426 @type cl: None or luxi.Client 427 @param cl: optional luxi client to use 428 @rtype: string 429 @return: the job ID 430 431 """ 432 if cl is None: 433 cl = self.GetClient() 434 try: 435 for opcode in op: 436 # Add an authorized user name to the reason trail 437 trail = getattr(opcode, constants.OPCODE_REASON, []) 438 trail.append(self.GetAuthReason()) 439 setattr(opcode, constants.OPCODE_REASON, trail) 440 return cl.SubmitJob(op) 441 except errors.JobQueueFull: 442 raise http.HttpServiceUnavailable("Job queue is full, needs archiving") 443 except errors.JobQueueDrainError: 444 raise http.HttpServiceUnavailable("Job queue is drained, cannot submit") 445 except rpcerr.NoMasterError, err: 446 raise http.HttpBadGateway("Master seems to be unreachable: %s" % err) 447 except rpcerr.PermissionError: 448 raise http.HttpInternalServerError("Internal error: no permission to" 449 " connect to the master daemon") 450 except rpcerr.TimeoutError, err: 451 raise http.HttpGatewayTimeout("Timeout while talking to the master" 452 " daemon: %s" % err)
453 454
455 -def GetResourceOpcodes(cls):
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
463 -def GetHandlerAccess(handler, method):
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
474 -def GetHandler(get_fn, aliases):
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 # Constant used to denote that a parameter cannot be set 491 ALL_VALUES_FORBIDDEN = "all_values_forbidden" 492 493
494 -def ProduceForbiddenParamDict(class_name, method_name, param_list):
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 # A simple error-raising function 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
531 -def InspectParams(params_dict, forbidden_params, rename_dict):
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 # Check for possible renames to ensure nothing slips through 548 if rename_dict is not None and param in rename_dict: 549 param = rename_dict[param] 550 551 # Now see if there are restrictions on this parameter 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
564 -class _MetaOpcodeResource(type):
565 """Meta class for RAPI resources. 566 567 """
568 - def __call__(mcs, *args, **kwargs):
569 """Instantiates class and patches it for use by the RAPI daemon. 570 571 """ 572 # Access to private attributes of a client class, pylint: disable=W0212 573 obj = type.__call__(mcs, *args, **kwargs) 574 575 for m_attrs in OPCODE_ATTRS: 576 method, op_attr, rename_attr, aliases_attr, _, fn_attr = m_attrs.GetAll() 577 if hasattr(obj, method): 578 # If the method handler is already defined, "*_RENAME" or 579 # "Get*OpInput" shouldn't be (they're only used by the automatically 580 # generated handler) 581 assert not hasattr(obj, rename_attr) 582 assert not hasattr(obj, fn_attr) 583 584 # The aliases are allowed only on GET calls 585 assert not hasattr(obj, aliases_attr) or method == http.HTTP_GET 586 587 # GET methods can add aliases of values they return under a different 588 # name 589 if method == http.HTTP_GET and hasattr(obj, aliases_attr): 590 setattr(obj, method, 591 compat.partial(GetHandler, getattr(obj, method), 592 getattr(obj, aliases_attr))) 593 else: 594 # Try to generate handler method on handler instance 595 try: 596 opcode = getattr(obj, op_attr) 597 except AttributeError: 598 pass 599 else: 600 setattr(obj, method, 601 compat.partial(obj._GenericHandler, opcode, 602 getattr(obj, rename_attr, None), 603 getattr(obj, fn_attr, obj._GetDefaultData))) 604 605 # Finally, the method (generated or not) should be wrapped to handle 606 # forbidden values 607 if hasattr(obj, m_attrs.forbidden): 608 forbidden_dict = ProduceForbiddenParamDict( 609 obj.__class__.__name__, method, getattr(obj, m_attrs.forbidden) 610 ) 611 setattr( 612 obj, method, compat.partial(obj._ForbiddenHandler, 613 getattr(obj, method), 614 forbidden_dict, 615 getattr(obj, m_attrs.rename, None)) 616 ) 617 618 return obj
619 620
621 -class OpcodeResource(ResourceBase):
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
682 - def _ForbiddenHandler(self, method_fn, forbidden_params, rename_dict):
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
690 - def _GetDefaultData(self):
691 return (self.request_body, None)
692
693 - def _GetRapiOpName(self):
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
701 - def _GetCommonStatic(self):
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
724 - def _GetDepends(self):
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
732 - def _GenericHandler(self, opcode, rename, fn):
733 (body, specific_static) = fn() 734 if isinstance(body, dict): 735 body.update(self._GetDepends()) 736 static = self._GetCommonStatic() 737 if specific_static: 738 static.update(specific_static) 739 op = FillOpcode(opcode, body, static, rename=rename) 740 return self.SubmitJob([op])
741