1""" 2Helpful decorators for module writing 3""" 4 5 6import errno 7import inspect 8import logging 9import subprocess 10import sys 11import time 12from collections import defaultdict 13from functools import wraps 14 15import salt.utils.args 16import salt.utils.data 17import salt.utils.versions 18from salt.exceptions import ( 19 CommandExecutionError, 20 SaltConfigurationError, 21 SaltInvocationError, 22) 23from salt.log import LOG_LEVELS 24 25IS_WINDOWS = False 26if getattr(sys, "getwindowsversion", False): 27 IS_WINDOWS = True 28 29log = logging.getLogger(__name__) 30 31 32class Depends: 33 """ 34 This decorator will check the module when it is loaded and check that the 35 dependencies passed in are in the globals of the module. If not, it will 36 cause the function to be unloaded (or replaced). 37 """ 38 39 # kind -> Dependency -> list of things that depend on it 40 dependency_dict = defaultdict(lambda: defaultdict(dict)) 41 42 def __init__(self, *dependencies, **kwargs): 43 """ 44 The decorator is instantiated with a list of dependencies (string of 45 global name) 46 47 An example use of this would be: 48 49 .. code-block:: python 50 51 @depends('modulename') 52 def test(): 53 return 'foo' 54 55 OR 56 57 @depends('modulename', fallback_function=function) 58 def test(): 59 return 'foo' 60 61 .. code-block:: python 62 63 This can also be done with the retcode of a command, using the 64 ``retcode`` argument: 65 66 @depends('/opt/bin/check_cmd', retcode=0) 67 def test(): 68 return 'foo' 69 70 It is also possible to check for any nonzero retcode using the 71 ``nonzero_retcode`` argument: 72 73 @depends('/opt/bin/check_cmd', nonzero_retcode=True) 74 def test(): 75 return 'foo' 76 77 .. note:: 78 The command must be formatted as a string, not a list of args. 79 Additionally, I/O redirection and other shell-specific syntax are 80 not supported since this uses shell=False when calling 81 subprocess.Popen(). 82 83 """ 84 log.trace( 85 "Depends decorator instantiated with dep list of %s and kwargs %s", 86 dependencies, 87 kwargs, 88 ) 89 self.dependencies = dependencies 90 self.params = kwargs 91 92 def __call__(self, function): 93 """ 94 The decorator is "__call__"d with the function, we take that function 95 and determine which module and function name it is to store in the 96 class wide dependency_dict 97 """ 98 try: 99 # This inspect call may fail under certain conditions in the loader. 100 # Possibly related to a Python bug here: 101 # http://bugs.python.org/issue17735 102 frame = inspect.currentframe().f_back 103 # due to missing *.py files under esky we cannot use inspect.getmodule 104 # module name is something like salt.loaded.int.modules.test 105 _, kind, mod_name = frame.f_globals["__name__"].rsplit(".", 2) 106 fun_name = function.__name__ 107 for dep in self.dependencies: 108 self.dependency_dict[kind][dep][(mod_name, fun_name)] = ( 109 frame, 110 self.params, 111 ) 112 except Exception as exc: # pylint: disable=broad-except 113 log.exception( 114 "Exception encountered when attempting to inspect frame in " 115 "dependency decorator" 116 ) 117 return function 118 119 @staticmethod 120 def run_command(dependency, mod_name, func_name): 121 full_name = "{}.{}".format(mod_name, func_name) 122 log.trace("Running '%s' for '%s'", dependency, full_name) 123 if IS_WINDOWS: 124 args = salt.utils.args.shlex_split(dependency, posix=False) 125 else: 126 args = salt.utils.args.shlex_split(dependency) 127 log.trace("Command after shlex_split: %s", args) 128 proc = subprocess.Popen( 129 args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT 130 ) 131 output = proc.communicate()[0] 132 retcode = proc.returncode 133 log.trace("Output from '%s': %s", dependency, output) 134 log.trace("Retcode from '%s': %d", dependency, retcode) 135 return retcode 136 137 @classmethod 138 def enforce_dependencies(cls, functions, kind, tgt_mod): 139 """ 140 This is a class global method to enforce the dependencies that you 141 currently know about. 142 It will modify the "functions" dict and remove/replace modules that 143 are missing dependencies. 144 """ 145 for dependency, dependent_dict in cls.dependency_dict[kind].items(): 146 for (mod_name, func_name), (frame, params) in dependent_dict.items(): 147 if mod_name != tgt_mod: 148 continue 149 # Imports from local context take presedence over those from the global context. 150 dep_found = frame.f_locals.get(dependency) or frame.f_globals.get( 151 dependency 152 ) 153 # Default to version ``None`` if not found, which will be less than anything. 154 dep_version = getattr(dep_found, "__version__", None) 155 if "retcode" in params or "nonzero_retcode" in params: 156 try: 157 retcode = cls.run_command(dependency, mod_name, func_name) 158 except OSError as exc: 159 if exc.errno == errno.ENOENT: 160 log.trace( 161 "Failed to run command %s, %s not found", 162 dependency, 163 exc.filename, 164 ) 165 else: 166 log.trace("Failed to run command '%s': %s", dependency, exc) 167 retcode = -1 168 169 if "retcode" in params: 170 if params["retcode"] == retcode: 171 continue 172 173 elif "nonzero_retcode" in params: 174 if params["nonzero_retcode"]: 175 if retcode != 0: 176 continue 177 else: 178 if retcode == 0: 179 continue 180 181 # check if dependency is loaded 182 elif dependency is True: 183 log.trace( 184 "Dependency for %s.%s exists, not unloading", 185 mod_name, 186 func_name, 187 ) 188 continue 189 190 # check if you have the dependency 191 elif dep_found: 192 if "version" in params: 193 if ( 194 salt.utils.versions.version_cmp( 195 dep_version, params["version"] 196 ) 197 >= 0 198 ): 199 log.trace( 200 "Dependency (%s) already loaded inside %s with " 201 "version (%s), required (%s), skipping", 202 dependency, 203 mod_name, 204 dep_version, 205 params["version"], 206 ) 207 continue 208 else: 209 log.trace( 210 "Dependency (%s) already loaded inside %s, skipping", 211 dependency, 212 mod_name, 213 ) 214 continue 215 216 log.trace( 217 "Unloading %s.%s because dependency (%s%s) is not met", 218 mod_name, 219 func_name, 220 dependency, 221 " version {}".format(params["version"]) 222 if "version" in params 223 else "", 224 ) 225 # if not, unload the function 226 if frame: 227 try: 228 func_name = frame.f_globals["__func_alias__"][func_name] 229 except (AttributeError, KeyError): 230 pass 231 232 mod_key = "{}.{}".format(mod_name, func_name) 233 234 # if we don't have this module loaded, skip it! 235 if mod_key not in functions: 236 continue 237 238 try: 239 fallback_function = params.get("fallback_function") 240 if fallback_function is not None: 241 functions[mod_key] = fallback_function 242 else: 243 del functions[mod_key] 244 except AttributeError: 245 # we already did??? 246 log.trace("%s already removed, skipping", mod_key) 247 continue 248 249 250depends = Depends 251 252 253def timing(function): 254 """ 255 Decorator wrapper to log execution time, for profiling purposes 256 """ 257 258 @wraps(function) 259 def wrapped(*args, **kwargs): 260 start_time = time.time() 261 ret = function(*args, **salt.utils.args.clean_kwargs(**kwargs)) 262 end_time = time.time() 263 if function.__module__.startswith("salt.loaded.int."): 264 mod_name = function.__module__[16:] 265 else: 266 mod_name = function.__module__ 267 fstr = "Function %s.%s took %.{}f seconds to execute".format(sys.float_info.dig) 268 log.profile(fstr, mod_name, function.__name__, end_time - start_time) 269 return ret 270 271 return wrapped 272 273 274def memoize(func): 275 """ 276 Memoize aka cache the return output of a function 277 given a specific set of arguments 278 279 .. versionedited:: 2016.3.4 280 281 Added **kwargs support. 282 """ 283 cache = {} 284 285 @wraps(func) 286 def _memoize(*args, **kwargs): 287 str_args = [] 288 for arg in args: 289 if not isinstance(arg, str): 290 str_args.append(str(arg)) 291 else: 292 str_args.append(arg) 293 294 args_ = ",".join( 295 list(str_args) + ["{}={}".format(k, kwargs[k]) for k in sorted(kwargs)] 296 ) 297 if args_ not in cache: 298 cache[args_] = func(*args, **kwargs) 299 return cache[args_] 300 301 return _memoize 302 303 304class _DeprecationDecorator: 305 """ 306 Base mix-in class for the deprecation decorator. 307 Takes care of a common functionality, used in its derivatives. 308 """ 309 310 OPT_IN = 1 311 OPT_OUT = 2 312 313 def __init__(self, globals, version): 314 """ 315 Constructor. 316 317 :param globals: Module globals. Important for finding out replacement functions 318 :param version: Expiration version 319 :return: 320 """ 321 from salt.version import SaltStackVersion, __saltstack_version__ 322 323 self._globals = globals 324 self._exp_version_name = version 325 self._exp_version = SaltStackVersion.from_name(self._exp_version_name) 326 self._curr_version = __saltstack_version__.info 327 self._raise_later = None 328 self._function = None 329 self._orig_f_name = None 330 331 def _get_args(self, kwargs): 332 """ 333 Discard all keywords which aren't function-specific from the kwargs. 334 335 :param kwargs: 336 :return: 337 """ 338 _args = list() 339 _kwargs = salt.utils.args.clean_kwargs(**kwargs) 340 341 return _args, _kwargs 342 343 def _call_function(self, kwargs): 344 """ 345 Call target function that has been decorated. 346 347 :return: 348 """ 349 if self._raise_later: 350 raise self._raise_later # pylint: disable=E0702 351 352 if self._function: 353 args, kwargs = self._get_args(kwargs) 354 try: 355 return self._function(*args, **kwargs) 356 except TypeError as error: 357 error = str(error).replace( 358 self._function, self._orig_f_name 359 ) # Hide hidden functions 360 log.error( 361 'Function "%s" was not properly called: %s', 362 self._orig_f_name, 363 error, 364 ) 365 return self._function.__doc__ 366 except Exception as error: # pylint: disable=broad-except 367 log.error( 368 'Unhandled exception occurred in function "%s: %s', 369 self._function.__name__, 370 error, 371 ) 372 raise 373 else: 374 raise CommandExecutionError( 375 "Function is deprecated, but the successor function was not found." 376 ) 377 378 def __call__(self, function): 379 """ 380 Callable method of the decorator object when 381 the decorated function is gets called. 382 383 :param function: 384 :return: 385 """ 386 self._function = function 387 self._orig_f_name = self._function.__name__ 388 389 390class _IsDeprecated(_DeprecationDecorator): 391 """ 392 This decorator should be used only with the deprecated functions 393 to mark them as deprecated and alter its behavior a corresponding way. 394 The usage is only suitable if deprecation process is renaming 395 the function from one to another. In case function name or even function 396 signature stays the same, please use 'with_deprecated' decorator instead. 397 398 It has the following functionality: 399 400 1. Put a warning level message to the log, informing that 401 the deprecated function has been in use. 402 403 2. Raise an exception, if deprecated function is being called, 404 but the lifetime of it already expired. 405 406 3. Point to the successor of the deprecated function in the 407 log messages as well during the blocking it, once expired. 408 409 Usage of this decorator as follows. In this example no successor 410 is mentioned, hence the function "foo()" will be logged with the 411 warning each time is called and blocked completely, once EOF of 412 it is reached: 413 414 from salt.util.decorators import is_deprecated 415 416 @is_deprecated(globals(), "Beryllium") 417 def foo(): 418 pass 419 420 In the following example a successor function is mentioned, hence 421 every time the function "bar()" is called, message will suggest 422 to use function "baz()" instead. Once EOF is reached of the function 423 "bar()", an exception will ask to use function "baz()", in order 424 to continue: 425 426 from salt.util.decorators import is_deprecated 427 428 @is_deprecated(globals(), "Beryllium", with_successor="baz") 429 def bar(): 430 pass 431 432 def baz(): 433 pass 434 """ 435 436 def __init__(self, globals, version, with_successor=None): 437 """ 438 Constructor of the decorator 'is_deprecated'. 439 440 :param globals: Module globals 441 :param version: Version to be deprecated 442 :param with_successor: Successor function (optional) 443 :return: 444 """ 445 _DeprecationDecorator.__init__(self, globals, version) 446 self._successor = with_successor 447 448 def __call__(self, function): 449 """ 450 Callable method of the decorator object when 451 the decorated function is gets called. 452 453 :param function: 454 :return: 455 """ 456 _DeprecationDecorator.__call__(self, function) 457 458 @wraps(function) 459 def _decorate(*args, **kwargs): 460 """ 461 Decorator function. 462 463 :param args: 464 :param kwargs: 465 :return: 466 """ 467 if self._curr_version < self._exp_version: 468 msg = [ 469 'The function "{f_name}" is deprecated and will ' 470 'expire in version "{version_name}".'.format( 471 f_name=self._function.__name__, 472 version_name=self._exp_version_name, 473 ) 474 ] 475 if self._successor: 476 msg.append( 477 'Use successor "{successor}" instead.'.format( 478 successor=self._successor 479 ) 480 ) 481 log.warning(" ".join(msg)) 482 else: 483 msg = [ 484 'The lifetime of the function "{f_name}" expired.'.format( 485 f_name=self._function.__name__ 486 ) 487 ] 488 if self._successor: 489 msg.append( 490 'Please use its successor "{successor}" instead.'.format( 491 successor=self._successor 492 ) 493 ) 494 log.warning(" ".join(msg)) 495 raise CommandExecutionError(" ".join(msg)) 496 return self._call_function(kwargs) 497 498 return _decorate 499 500 501is_deprecated = _IsDeprecated 502 503 504class _WithDeprecated(_DeprecationDecorator): 505 """ 506 This decorator should be used with the successor functions 507 to mark them as a new and alter its behavior in a corresponding way. 508 It is used alone if a function content or function signature 509 needs to be replaced, leaving the name of the function same. 510 In case function needs to be renamed or just dropped, it has 511 to be used in pair with 'is_deprecated' decorator. 512 513 It has the following functionality: 514 515 1. Put a warning level message to the log, in case a component 516 is using its deprecated version. 517 518 2. Switch between old and new function in case an older version 519 is configured for the desired use. 520 521 3. Raise an exception, if deprecated version reached EOL and 522 point out for the new version. 523 524 Usage of this decorator as follows. If 'with_name' is not specified, 525 then the name of the deprecated function is assumed with the "_" prefix. 526 In this case, in order to deprecate a function, it is required: 527 528 - Add a prefix "_" to an existing function. E.g.: "foo()" to "_foo()". 529 530 - Implement a new function with exactly the same name, just without 531 the prefix "_". 532 533 Example: 534 535 from salt.util.decorators import with_deprecated 536 537 @with_deprecated(globals(), "Beryllium") 538 def foo(): 539 "This is a new function" 540 541 def _foo(): 542 "This is a deprecated function" 543 544 545 In case there is a need to deprecate a function and rename it, 546 the decorator should be used with the 'with_name' parameter. This 547 parameter is pointing to the existing deprecated function. In this 548 case deprecation process as follows: 549 550 - Leave a deprecated function without changes, as is. 551 552 - Implement a new function and decorate it with this decorator. 553 554 - Set a parameter 'with_name' to the deprecated function. 555 556 - If a new function has a different name than a deprecated, 557 decorate a deprecated function with the 'is_deprecated' decorator 558 in order to let the function have a deprecated behavior. 559 560 Example: 561 562 from salt.util.decorators import with_deprecated 563 564 @with_deprecated(globals(), "Beryllium", with_name="an_old_function") 565 def a_new_function(): 566 "This is a new function" 567 568 @is_deprecated(globals(), "Beryllium", with_successor="a_new_function") 569 def an_old_function(): 570 "This is a deprecated function" 571 572 """ 573 574 MODULE_NAME = "__virtualname__" 575 CFG_USE_DEPRECATED = "use_deprecated" 576 CFG_USE_SUPERSEDED = "use_superseded" 577 578 def __init__( 579 self, globals, version, with_name=None, policy=_DeprecationDecorator.OPT_OUT 580 ): 581 """ 582 Constructor of the decorator 'with_deprecated' 583 584 :param globals: 585 :param version: 586 :param with_name: 587 :param policy: 588 :return: 589 """ 590 _DeprecationDecorator.__init__(self, globals, version) 591 self._with_name = with_name 592 self._policy = policy 593 594 def _set_function(self, function): 595 """ 596 Based on the configuration, set to execute an old or a new function. 597 :return: 598 """ 599 full_name = "{m_name}.{f_name}".format( 600 m_name=self._globals.get(self.MODULE_NAME, "") 601 or self._globals["__name__"].split(".")[-1], 602 f_name=function.__name__, 603 ) 604 if full_name.startswith("."): 605 self._raise_later = CommandExecutionError( 606 'Module not found for function "{f_name}"'.format( 607 f_name=function.__name__ 608 ) 609 ) 610 611 opts = self._globals.get("__opts__", "{}") 612 pillar = self._globals.get("__pillar__", "{}") 613 614 use_deprecated = full_name in opts.get( 615 self.CFG_USE_DEPRECATED, list() 616 ) or full_name in pillar.get(self.CFG_USE_DEPRECATED, list()) 617 618 use_superseded = full_name in opts.get( 619 self.CFG_USE_SUPERSEDED, list() 620 ) or full_name in pillar.get(self.CFG_USE_SUPERSEDED, list()) 621 622 if use_deprecated and use_superseded: 623 raise SaltConfigurationError( 624 "Function '{}' is mentioned both in deprecated " 625 "and superseded sections. Please remove any of that.".format(full_name) 626 ) 627 old_function = self._globals.get( 628 self._with_name or "_{}".format(function.__name__) 629 ) 630 if self._policy == self.OPT_IN: 631 self._function = function if use_superseded else old_function 632 else: 633 self._function = old_function if use_deprecated else function 634 635 def _is_used_deprecated(self): 636 """ 637 Returns True, if a component configuration explicitly is 638 asking to use an old version of the deprecated function. 639 640 :return: 641 """ 642 func_path = "{m_name}.{f_name}".format( 643 m_name=self._globals.get(self.MODULE_NAME, "") 644 or self._globals["__name__"].split(".")[-1], 645 f_name=self._orig_f_name, 646 ) 647 648 return ( 649 func_path 650 in self._globals.get("__opts__").get(self.CFG_USE_DEPRECATED, list()) 651 or func_path 652 in self._globals.get("__pillar__").get(self.CFG_USE_DEPRECATED, list()) 653 or ( 654 self._policy == self.OPT_IN 655 and not ( 656 func_path 657 in self._globals.get("__opts__", {}).get( 658 self.CFG_USE_SUPERSEDED, list() 659 ) 660 ) 661 and not ( 662 func_path 663 in self._globals.get("__pillar__", {}).get( 664 self.CFG_USE_SUPERSEDED, list() 665 ) 666 ) 667 ), 668 func_path, 669 ) 670 671 def __call__(self, function): 672 """ 673 Callable method of the decorator object when 674 the decorated function is gets called. 675 676 :param function: 677 :return: 678 """ 679 _DeprecationDecorator.__call__(self, function) 680 681 @wraps(function) 682 def _decorate(*args, **kwargs): 683 """ 684 Decorator function. 685 686 :param args: 687 :param kwargs: 688 :return: 689 """ 690 self._set_function(function) 691 is_deprecated, func_path = self._is_used_deprecated() 692 if is_deprecated: 693 if self._curr_version < self._exp_version: 694 msg = list() 695 if self._with_name: 696 msg.append( 697 'The function "{f_name}" is deprecated and will ' 698 'expire in version "{version_name}".'.format( 699 f_name=self._with_name.startswith("_") 700 and self._orig_f_name 701 or self._with_name, 702 version_name=self._exp_version_name, 703 ) 704 ) 705 msg.append( 706 'Use its successor "{successor}" instead.'.format( 707 successor=self._orig_f_name 708 ) 709 ) 710 else: 711 msg.append( 712 'The function "{f_name}" is using its deprecated version' 713 ' and will expire in version "{version_name}".'.format( 714 f_name=func_path, version_name=self._exp_version_name 715 ) 716 ) 717 log.warning(" ".join(msg)) 718 else: 719 msg_patt = 'The lifetime of the function "{f_name}" expired.' 720 if "_" + self._orig_f_name == self._function.__name__: 721 msg = [ 722 msg_patt.format(f_name=self._orig_f_name), 723 "Please turn off its deprecated version in the" 724 " configuration", 725 ] 726 else: 727 msg = [ 728 'Although function "{f_name}" is called, an alias' 729 ' "{f_alias}" is configured as its deprecated version.'.format( 730 f_name=self._orig_f_name, 731 f_alias=self._with_name or self._orig_f_name, 732 ), 733 msg_patt.format( 734 f_name=self._with_name or self._orig_f_name 735 ), 736 'Please use its successor "{successor}" instead.'.format( 737 successor=self._orig_f_name 738 ), 739 ] 740 log.error(" ".join(msg)) 741 raise CommandExecutionError(" ".join(msg)) 742 return self._call_function(kwargs) 743 744 _decorate.__doc__ = self._function.__doc__ 745 _decorate.__wrapped__ = self._function 746 return _decorate 747 748 749with_deprecated = _WithDeprecated 750 751 752def require_one_of(*kwarg_names): 753 """ 754 Decorator to filter out exclusive arguments from the call. 755 756 kwarg_names: 757 Limit which combination of arguments may be passed to the call. 758 759 Example: 760 761 762 # Require one of the following arguments to be supplied to foo() 763 @require_one_of('arg1', 'arg2', 'arg3') 764 def foo(arg1, arg2, arg3): 765 766 """ 767 768 def wrapper(f): 769 @wraps(f) 770 def func(*args, **kwargs): 771 names = [key for key in kwargs if kwargs[key] and key in kwarg_names] 772 names.extend( 773 [ 774 args[i] 775 for i, arg in enumerate(args) 776 if args[i] and f.__code__.co_varnames[i] in kwarg_names 777 ] 778 ) 779 if len(names) > 1: 780 raise SaltInvocationError( 781 "Only one of the following is allowed: {}".format( 782 ", ".join(kwarg_names) 783 ) 784 ) 785 if not names: 786 raise SaltInvocationError( 787 "One of the following must be provided: {}".format( 788 ", ".join(kwarg_names) 789 ) 790 ) 791 return f(*args, **kwargs) 792 793 return func 794 795 return wrapper 796 797 798def allow_one_of(*kwarg_names): 799 """ 800 Decorator to filter out exclusive arguments from the call. 801 802 kwarg_names: 803 Limit which combination of arguments may be passed to the call. 804 805 Example: 806 807 808 # Allow only one of the following arguments to be supplied to foo() 809 @allow_one_of('arg1', 'arg2', 'arg3') 810 def foo(arg1, arg2, arg3): 811 812 """ 813 814 def wrapper(f): 815 @wraps(f) 816 def func(*args, **kwargs): 817 names = [key for key in kwargs if kwargs[key] and key in kwarg_names] 818 names.extend( 819 [ 820 args[i] 821 for i, arg in enumerate(args) 822 if args[i] and f.__code__.co_varnames[i] in kwarg_names 823 ] 824 ) 825 if len(names) > 1: 826 raise SaltInvocationError( 827 "Only of the following is allowed: {}".format( 828 ", ".join(kwarg_names) 829 ) 830 ) 831 return f(*args, **kwargs) 832 833 return func 834 835 return wrapper 836 837 838def ignores_kwargs(*kwarg_names): 839 """ 840 Decorator to filter out unexpected keyword arguments from the call 841 842 kwarg_names: 843 List of argument names to ignore 844 """ 845 846 def _ignores_kwargs(fn): 847 @wraps(fn) 848 def __ignores_kwargs(*args, **kwargs): 849 kwargs_filtered = kwargs.copy() 850 for name in kwarg_names: 851 if name in kwargs_filtered: 852 del kwargs_filtered[name] 853 return fn(*args, **kwargs_filtered) 854 855 return __ignores_kwargs 856 857 return _ignores_kwargs 858 859 860def ensure_unicode_args(function): 861 """ 862 Decodes all arguments passed to the wrapped function 863 """ 864 865 @wraps(function) 866 def wrapped(*args, **kwargs): 867 return function(*args, **kwargs) 868 869 return wrapped 870