1""" 2 :codeauthor: Pedro Algarvio (pedro@algarvio.me) 3 4 5 salt.utils.parsers 6 ~~~~~~~~~~~~~~~~~~ 7 8 This is where all the black magic happens on all of salt's CLI tools. 9""" 10# pylint: disable=missing-docstring,protected-access,too-many-ancestors,too-few-public-methods 11# pylint: disable=attribute-defined-outside-init,no-self-use 12 13 14import getpass 15import logging 16import optparse 17import os 18import signal 19import sys 20import traceback 21import types 22from functools import partial 23 24import salt.config as config 25import salt.defaults.exitcodes 26import salt.exceptions 27import salt.features 28import salt.log.setup as log 29import salt.syspaths as syspaths 30import salt.utils.args 31import salt.utils.data 32import salt.utils.files 33import salt.utils.jid 34import salt.utils.platform 35import salt.utils.process 36import salt.utils.stringutils 37import salt.utils.user 38import salt.utils.win_functions 39import salt.utils.xdg 40import salt.utils.yaml 41import salt.version as version 42from salt.defaults import DEFAULT_TARGET_DELIM 43from salt.utils.validate.path import is_writeable 44from salt.utils.verify import verify_log_files 45 46logger = logging.getLogger(__name__) 47 48 49def _sorted(mixins_or_funcs): 50 return sorted(mixins_or_funcs, key=lambda mf: getattr(mf, "_mixin_prio_", 1000)) 51 52 53class MixinFuncsContainer(list): 54 def append(self, func): 55 if isinstance(func, types.MethodType): 56 # We only care about unbound methods 57 func = func.__func__ 58 if func not in self: 59 # And no duplicates please 60 list.append(self, func) 61 62 63class MixInMeta(type): 64 # This attribute here won't actually do anything. But, if you need to 65 # specify an order or a dependency within the mix-ins, please define the 66 # attribute on your own MixIn 67 _mixin_prio_ = 0 68 69 def __new__(mcs, name, bases, attrs): 70 instance = super().__new__(mcs, name, bases, attrs) 71 if not hasattr(instance, "_mixin_setup"): 72 raise RuntimeError( 73 "Don't subclass {} in {} if you're not going " 74 "to use it as a salt parser mix-in.".format(mcs.__name__, name) 75 ) 76 return instance 77 78 79class OptionParserMeta(MixInMeta): 80 def __new__(mcs, name, bases, attrs): 81 instance = super().__new__(mcs, name, bases, attrs) 82 if not hasattr(instance, "_mixin_setup_funcs"): 83 instance._mixin_setup_funcs = MixinFuncsContainer() 84 if not hasattr(instance, "_mixin_process_funcs"): 85 instance._mixin_process_funcs = MixinFuncsContainer() 86 if not hasattr(instance, "_mixin_after_parsed_funcs"): 87 instance._mixin_after_parsed_funcs = MixinFuncsContainer() 88 if not hasattr(instance, "_mixin_before_exit_funcs"): 89 instance._mixin_before_exit_funcs = MixinFuncsContainer() 90 91 for base in _sorted(bases + (instance,)): 92 func = getattr(base, "_mixin_setup", None) 93 if func is not None and func not in instance._mixin_setup_funcs: 94 instance._mixin_setup_funcs.append(func) 95 96 func = getattr(base, "_mixin_after_parsed", None) 97 if func is not None and func not in instance._mixin_after_parsed_funcs: 98 instance._mixin_after_parsed_funcs.append(func) 99 100 func = getattr(base, "_mixin_before_exit", None) 101 if func is not None and func not in instance._mixin_before_exit_funcs: 102 instance._mixin_before_exit_funcs.append(func) 103 104 # Mark process_<opt> functions with the base priority for sorting 105 for func in dir(base): 106 if not func.startswith("process_"): 107 continue 108 109 func = getattr(base, func) 110 if getattr(func, "_mixin_prio_", None) is not None: 111 # Function already has the attribute set, don't override it 112 continue 113 114 func._mixin_prio_ = getattr(base, "_mixin_prio_", 1000) 115 116 return instance 117 118 119class CustomOption(optparse.Option): 120 def take_action( 121 self, action, dest, *args, **kwargs 122 ): # pylint: disable=arguments-differ 123 # see https://github.com/python/cpython/blob/master/Lib/optparse.py#L786 124 self.explicit = True 125 return optparse.Option.take_action(self, action, dest, *args, **kwargs) 126 127 128class OptionParser(optparse.OptionParser): 129 VERSION = version.__saltstack_version__.formatted_version 130 131 usage = "%prog [options]" 132 133 epilog = ( 134 'You can find additional help about %prog issuing "man %prog" ' 135 "or on https://docs.saltproject.io" 136 ) 137 description = None 138 139 # Private attributes 140 _mixin_prio_ = 100 141 142 # Setup multiprocessing logging queue listener 143 _setup_mp_logging_listener_ = False 144 145 def __init__(self, *args, **kwargs): 146 kwargs.setdefault("version", "%prog {}".format(self.VERSION)) 147 kwargs.setdefault("usage", self.usage) 148 if self.description: 149 kwargs.setdefault("description", self.description) 150 151 if self.epilog: 152 kwargs.setdefault("epilog", self.epilog) 153 154 kwargs.setdefault("option_class", CustomOption) 155 optparse.OptionParser.__init__(self, *args, **kwargs) 156 157 if self.epilog and "%prog" in self.epilog: 158 self.epilog = self.epilog.replace("%prog", self.get_prog_name()) 159 160 def add_option_group(self, *args, **kwargs): 161 option_group = optparse.OptionParser.add_option_group(self, *args, **kwargs) 162 option_group.option_class = CustomOption 163 return option_group 164 165 def parse_args(self, args=None, values=None): 166 options, args = optparse.OptionParser.parse_args(self, args, values) 167 if "args_stdin" in options.__dict__ and options.args_stdin is True: 168 # Read additional options and/or arguments from stdin and combine 169 # them with the options and arguments from the command line. 170 new_inargs = sys.stdin.readlines() 171 new_inargs = [arg.rstrip("\r\n") for arg in new_inargs] 172 new_options, new_args = optparse.OptionParser.parse_args(self, new_inargs) 173 options.__dict__.update(new_options.__dict__) 174 args.extend(new_args) 175 176 if options.versions_report: 177 self.print_versions_report() 178 179 self.options, self.args = options, args 180 181 # Let's get some proper sys.stderr logging as soon as possible!!! 182 # This logging handler will be removed once the proper console or 183 # logfile logging is setup. 184 temp_log_level = getattr(self.options, "log_level", None) 185 log.setup_temp_logger("error" if temp_log_level is None else temp_log_level) 186 187 # Gather and run the process_<option> functions in the proper order 188 process_option_funcs = [] 189 for option_key in options.__dict__: 190 process_option_func = getattr(self, "process_{}".format(option_key), None) 191 if process_option_func is not None: 192 process_option_funcs.append(process_option_func) 193 194 for process_option_func in _sorted(process_option_funcs): 195 try: 196 process_option_func() 197 except Exception as err: # pylint: disable=broad-except 198 logger.exception(err) 199 self.error( 200 "Error while processing {}: {}".format( 201 process_option_func, traceback.format_exc() 202 ) 203 ) 204 205 # Run the functions on self._mixin_after_parsed_funcs 206 for ( 207 mixin_after_parsed_func 208 ) in self._mixin_after_parsed_funcs: # pylint: disable=no-member 209 try: 210 mixin_after_parsed_func(self) 211 except Exception as err: # pylint: disable=broad-except 212 logger.exception(err) 213 self.error( 214 "Error while processing {}: {}".format( 215 mixin_after_parsed_func, traceback.format_exc() 216 ) 217 ) 218 219 if self.config.get("conf_file", None) is not None: # pylint: disable=no-member 220 logger.debug( 221 "Configuration file path: %s", 222 self.config["conf_file"], # pylint: disable=no-member 223 ) 224 # Retain the standard behavior of optparse to return options and args 225 return options, args 226 227 def _populate_option_list(self, option_list, add_help=True): 228 optparse.OptionParser._populate_option_list( 229 self, option_list, add_help=add_help 230 ) 231 for mixin_setup_func in self._mixin_setup_funcs: # pylint: disable=no-member 232 mixin_setup_func(self) 233 234 def _add_version_option(self): 235 optparse.OptionParser._add_version_option(self) 236 self.add_option( 237 "--versions-report", 238 "-V", 239 action="store_true", 240 help="Show program's dependencies version number and exit.", 241 ) 242 243 def print_versions_report( 244 self, file=sys.stdout 245 ): # pylint: disable=redefined-builtin 246 print("\n".join(version.versions_report()), file=file, flush=True) 247 self.exit(salt.defaults.exitcodes.EX_OK) 248 249 def exit(self, status=0, msg=None): 250 # Run the functions on self._mixin_after_parsed_funcs 251 for ( 252 mixin_before_exit_func 253 ) in self._mixin_before_exit_funcs: # pylint: disable=no-member 254 try: 255 mixin_before_exit_func(self) 256 except Exception as err: # pylint: disable=broad-except 257 logger.exception(err) 258 logger.error( 259 "Error while processing %s: %s", 260 str(mixin_before_exit_func), 261 traceback.format_exc(), 262 ) 263 if self._setup_mp_logging_listener_ is True: 264 # Stop logging through the queue 265 log.shutdown_multiprocessing_logging() 266 # Stop the logging queue listener process 267 log.shutdown_multiprocessing_logging_listener(daemonizing=True) 268 if isinstance(msg, str) and msg and msg[-1] != "\n": 269 msg = "{}\n".format(msg) 270 optparse.OptionParser.exit(self, status, msg) 271 272 def error(self, msg): 273 """ 274 error(msg : string) 275 276 Print a usage message incorporating 'msg' to stderr and exit. 277 This keeps option parsing exit status uniform for all parsing errors. 278 """ 279 self.print_usage(sys.stderr) 280 self.exit( 281 salt.defaults.exitcodes.EX_USAGE, 282 "{}: error: {}\n".format(self.get_prog_name(), msg), 283 ) 284 285 286class MergeConfigMixIn(metaclass=MixInMeta): 287 """ 288 This mix-in will simply merge the CLI-passed options, by overriding the 289 configuration file loaded settings. 290 291 This mix-in should run last. 292 """ 293 294 _mixin_prio_ = sys.maxsize 295 296 def _mixin_setup(self): 297 if not hasattr(self, "setup_config") and not hasattr(self, "config"): 298 # No configuration was loaded on this parser. 299 # There's nothing to do here. 300 return 301 302 # Add an additional function that will merge the shell options with 303 # the config options and if needed override them 304 self._mixin_after_parsed_funcs.append(self.__merge_config_with_cli) 305 306 def __merge_config_with_cli(self): 307 # Merge parser options 308 for option in self.option_list: 309 if option.dest is None: 310 # --version does not have dest attribute set for example. 311 # All options defined by us, even if not explicitly(by kwarg), 312 # will have the dest attribute set 313 continue 314 315 # Get the passed value from shell. If empty get the default one 316 default = self.defaults.get(option.dest) 317 value = getattr(self.options, option.dest, default) 318 319 if option.dest not in self.config: 320 # There's no value in the configuration file 321 if value is not None: 322 # There's an actual value, add it to the config 323 self.config[option.dest] = value 324 elif value is not None and getattr(option, "explicit", False): 325 # Only set the value in the config file IF it was explicitly 326 # specified by the user, this makes it possible to tweak settings 327 # on the configuration files bypassing the shell option flags' 328 # defaults 329 self.config[option.dest] = value 330 elif option.dest in self.config: 331 # Let's update the option value with the one from the 332 # configuration file. This allows the parsers to make use of 333 # the updated value by using self.options.<option> 334 setattr(self.options, option.dest, self.config[option.dest]) 335 336 # Merge parser group options if any 337 for group in self.option_groups: 338 for option in group.option_list: 339 if option.dest is None: 340 continue 341 # Get the passed value from shell. If empty get the default one 342 default = self.defaults.get(option.dest) 343 value = getattr(self.options, option.dest, default) 344 if option.dest not in self.config: 345 # There's no value in the configuration file 346 if value is not None: 347 # There's an actual value, add it to the config 348 self.config[option.dest] = value 349 elif value is not None and getattr(option, "explicit", False): 350 # Only set the value in the config file IF it was explicitly 351 # specified by the user, this makes it possible to tweak 352 # settings on the configuration files bypassing the shell 353 # option flags' defaults 354 self.config[option.dest] = value 355 elif option.dest in self.config: 356 # Let's update the option value with the one from the 357 # configuration file. This allows the parsers to make use 358 # of the updated value by using self.options.<option> 359 setattr(self.options, option.dest, self.config[option.dest]) 360 361 362class SaltfileMixIn(metaclass=MixInMeta): 363 _mixin_prio_ = -20 364 365 def _mixin_setup(self): 366 self.add_option( 367 "--saltfile", 368 default=None, 369 help=( 370 "Specify the path to a Saltfile. If not passed, one will be " 371 "searched for in the current working directory." 372 ), 373 ) 374 375 def process_saltfile(self): 376 if self.options.saltfile is None: 377 # No one passed a Saltfile as an option, environment variable!? 378 self.options.saltfile = os.environ.get("SALT_SALTFILE", None) 379 380 if self.options.saltfile is None: 381 # If we're here, no one passed a Saltfile either to the CLI tool or 382 # as an environment variable. 383 # Is there a Saltfile in the current directory? 384 try: # cwd may not exist if it was removed but salt was run from it 385 saltfile = os.path.join(os.getcwd(), "Saltfile") 386 except OSError: 387 saltfile = "" 388 if os.path.isfile(saltfile): 389 self.options.saltfile = saltfile 390 else: 391 saltfile = os.path.join(os.path.expanduser("~"), ".salt", "Saltfile") 392 if os.path.isfile(saltfile): 393 self.options.saltfile = saltfile 394 else: 395 saltfile = self.options.saltfile 396 397 if not self.options.saltfile: 398 # There's still no valid Saltfile? No need to continue... 399 return 400 401 if not os.path.isfile(self.options.saltfile): 402 self.error("'{}' file does not exist.\n".format(self.options.saltfile)) 403 404 # Make sure we have an absolute path 405 self.options.saltfile = os.path.abspath(self.options.saltfile) 406 407 # Make sure we let the user know that we will be loading a Saltfile 408 logger.info("Loading Saltfile from '%s'", str(self.options.saltfile)) 409 410 try: 411 saltfile_config = config._read_conf_file(saltfile) 412 except salt.exceptions.SaltConfigurationError as error: 413 self.error(error.message) 414 self.exit( 415 salt.defaults.exitcodes.EX_GENERIC, 416 "{}: error: {}\n".format(self.get_prog_name(), error.message), 417 ) 418 419 if not saltfile_config: 420 # No configuration was loaded from the Saltfile 421 return 422 423 if self.get_prog_name() not in saltfile_config: 424 # There's no configuration specific to the CLI tool. Stop! 425 return 426 427 # We just want our own configuration 428 cli_config = saltfile_config[self.get_prog_name()] 429 430 # If there are any options, who's names match any key from the loaded 431 # Saltfile, we need to update its default value 432 for option in self.option_list: 433 if option.dest is None: 434 # --version does not have dest attribute set for example. 435 continue 436 437 if option.dest not in cli_config: 438 # If we don't have anything in Saltfile for this option, let's 439 # continue processing right now 440 continue 441 442 # Get the passed value from shell. If empty get the default one 443 default = self.defaults.get(option.dest) 444 value = getattr(self.options, option.dest, default) 445 if value != default: 446 # The user passed an argument, we won't override it with the 447 # one from Saltfile, if any 448 continue 449 450 # We reached this far! Set the Saltfile value on the option 451 setattr(self.options, option.dest, cli_config[option.dest]) 452 option.explicit = True 453 454 # Let's also search for options referred in any option groups 455 for group in self.option_groups: 456 for option in group.option_list: 457 if option.dest is None: 458 continue 459 460 if option.dest not in cli_config: 461 # If we don't have anything in Saltfile for this option, 462 # let's continue processing right now 463 continue 464 465 # Get the passed value from shell. If empty get the default one 466 default = self.defaults.get(option.dest) 467 value = getattr(self.options, option.dest, default) 468 if value != default: 469 # The user passed an argument, we won't override it with 470 # the one from Saltfile, if any 471 continue 472 473 setattr(self.options, option.dest, cli_config[option.dest]) 474 option.explicit = True 475 476 # Any left over value in the saltfile can now be safely added 477 for key in cli_config: 478 setattr(self.options, key, cli_config[key]) 479 480 481class HardCrashMixin(metaclass=MixInMeta): 482 _mixin_prio_ = 40 483 _config_filename_ = None 484 485 def _mixin_setup(self): 486 hard_crash = os.environ.get("SALT_HARD_CRASH", False) 487 self.add_option( 488 "--hard-crash", 489 action="store_true", 490 default=hard_crash, 491 help=( 492 "Raise any original exception rather than exiting gracefully. Default:" 493 " %default." 494 ), 495 ) 496 497 498class NoParseMixin(metaclass=MixInMeta): 499 _mixin_prio_ = 50 500 501 def _mixin_setup(self): 502 no_parse = os.environ.get("SALT_NO_PARSE", "") 503 self.add_option( 504 "--no-parse", 505 default=no_parse, 506 help=( 507 "Comma-separated list of named CLI arguments (i.e. argname=value) " 508 "which should not be parsed as Python data types" 509 ), 510 metavar="argname1,argname2,...", 511 ) 512 513 def process_no_parse(self): 514 if self.options.no_parse: 515 try: 516 self.options.no_parse = [ 517 x.strip() for x in self.options.no_parse.split(",") 518 ] 519 except AttributeError: 520 self.options.no_parse = [] 521 else: 522 self.options.no_parse = [] 523 524 525class ConfigDirMixIn(metaclass=MixInMeta): 526 _mixin_prio_ = -10 527 _config_filename_ = None 528 _default_config_dir_ = syspaths.CONFIG_DIR 529 _default_config_dir_env_var_ = "SALT_CONFIG_DIR" 530 531 def _mixin_setup(self): 532 config_dir = os.environ.get(self._default_config_dir_env_var_, None) 533 if not config_dir: 534 config_dir = self._default_config_dir_ 535 logger.debug("SYSPATHS setup as: %s", str(syspaths.CONFIG_DIR)) 536 self.add_option( 537 "-c", 538 "--config-dir", 539 default=config_dir, 540 help="Pass in an alternative configuration directory. Default: '%default'.", 541 ) 542 543 def process_config_dir(self): 544 self.options.config_dir = os.path.expanduser(self.options.config_dir) 545 if not os.path.isdir(self.options.config_dir): 546 # No logging is configured yet 547 sys.stderr.write( 548 "WARNING: CONFIG '{}' directory does not exist.\n".format( 549 self.options.config_dir 550 ) 551 ) 552 553 # Make sure we have an absolute path 554 self.options.config_dir = os.path.abspath(self.options.config_dir) 555 556 if hasattr(self, "setup_config"): 557 if not hasattr(self, "config"): 558 self.config = {} 559 try: 560 self.config.update(self.setup_config()) 561 except OSError as exc: 562 self.error("Failed to load configuration: {}".format(exc)) 563 564 def get_config_file_path(self, configfile=None): 565 if configfile is None: 566 configfile = self._config_filename_ 567 return os.path.join(self.options.config_dir, configfile) 568 569 570class LogLevelMixIn(metaclass=MixInMeta): 571 _mixin_prio_ = 10 572 _default_logging_level_ = "warning" 573 _default_logging_logfile_ = None 574 _logfile_config_setting_name_ = "log_file" 575 _loglevel_config_setting_name_ = "log_level" 576 _logfile_loglevel_config_setting_name_ = ( 577 "log_level_logfile" # pylint: disable=invalid-name 578 ) 579 _skip_console_logging_config_ = False 580 581 def _mixin_setup(self): 582 if self._default_logging_logfile_ is None: 583 # This is an attribute available for programmers, so, raise a 584 # RuntimeError to let them know about the proper usage. 585 raise RuntimeError( 586 "Please set {}._default_logging_logfile_".format( 587 self.__class__.__name__ 588 ) 589 ) 590 group = self.logging_options_group = optparse.OptionGroup( 591 self, 592 "Logging Options", 593 "Logging options which override any settings defined on the " 594 "configuration files.", 595 ) 596 self.add_option_group(group) 597 598 if not getattr(self, "_skip_console_logging_config_", False): 599 group.add_option( 600 "-l", 601 "--log-level", 602 dest=self._loglevel_config_setting_name_, 603 choices=list(log.LOG_LEVELS), 604 help="Console logging log level. One of {}. Default: '{}'.".format( 605 ", ".join(["'{}'".format(n) for n in log.SORTED_LEVEL_NAMES]), 606 self._default_logging_level_, 607 ), 608 ) 609 610 def _logfile_callback(option, opt, value, parser, *args, **kwargs): 611 if not os.path.dirname(value): 612 # if the path is only a file name (no parent directory), assume current directory 613 value = os.path.join(os.path.curdir, value) 614 setattr(parser.values, self._logfile_config_setting_name_, value) 615 616 group.add_option( 617 "--log-file", 618 dest=self._logfile_config_setting_name_, 619 default=None, 620 action="callback", 621 type="string", 622 callback=_logfile_callback, 623 help="Log file path. Default: '{}'.".format(self._default_logging_logfile_), 624 ) 625 626 group.add_option( 627 "--log-file-level", 628 dest=self._logfile_loglevel_config_setting_name_, 629 choices=list(log.LOG_LEVELS), 630 help="Logfile logging log level. One of {}. Default: '{}'.".format( 631 ", ".join(["'{}'".format(n) for n in log.SORTED_LEVEL_NAMES]), 632 self._default_logging_level_, 633 ), 634 ) 635 636 def process_log_level(self): 637 if not getattr(self.options, self._loglevel_config_setting_name_, None): 638 # Log level is not set via CLI, checking loaded configuration 639 if self.config.get(self._loglevel_config_setting_name_, None): 640 # Is the regular log level setting set? 641 setattr( 642 self.options, 643 self._loglevel_config_setting_name_, 644 self.config.get(self._loglevel_config_setting_name_), 645 ) 646 else: 647 # Nothing is set on the configuration? Let's use the CLI tool 648 # defined default 649 setattr( 650 self.options, 651 self._loglevel_config_setting_name_, 652 self._default_logging_level_, 653 ) 654 655 # Setup extended logging right before the last step 656 self._mixin_after_parsed_funcs.append(self.__setup_extended_logging) 657 # Setup the console and log file configuration before the MP logging 658 # listener because the MP logging listener may need that config. 659 self._mixin_after_parsed_funcs.append(self.__setup_logfile_logger_config) 660 self._mixin_after_parsed_funcs.append(self.__setup_console_logger_config) 661 # Setup the multiprocessing log queue listener if enabled 662 self._mixin_after_parsed_funcs.append(self._setup_mp_logging_listener) 663 # Setup the multiprocessing log queue client if listener is enabled 664 # and using Windows 665 self._mixin_after_parsed_funcs.append(self._setup_mp_logging_client) 666 # Setup the console as the last _mixin_after_parsed_func to run 667 self._mixin_after_parsed_funcs.append(self.__setup_console_logger) 668 669 def process_log_file(self): 670 if not getattr(self.options, self._logfile_config_setting_name_, None): 671 # Log file is not set via CLI, checking loaded configuration 672 if self.config.get(self._logfile_config_setting_name_, None): 673 # Is the regular log file setting set? 674 setattr( 675 self.options, 676 self._logfile_config_setting_name_, 677 self.config.get(self._logfile_config_setting_name_), 678 ) 679 else: 680 # Nothing is set on the configuration? Let's use the CLI tool 681 # defined default 682 setattr( 683 self.options, 684 self._logfile_config_setting_name_, 685 self._default_logging_logfile_, 686 ) 687 if self._logfile_config_setting_name_ in self.config: 688 # Remove it from config so it inherits from log_file 689 self.config.pop(self._logfile_config_setting_name_) 690 691 def process_log_level_logfile(self): 692 if not getattr(self.options, self._logfile_loglevel_config_setting_name_, None): 693 # Log file level is not set via CLI, checking loaded configuration 694 if self.config.get(self._logfile_loglevel_config_setting_name_, None): 695 # Is the regular log file level setting set? 696 setattr( 697 self.options, 698 self._logfile_loglevel_config_setting_name_, 699 self.config.get(self._logfile_loglevel_config_setting_name_), 700 ) 701 else: 702 # Nothing is set on the configuration? Let's use the CLI tool 703 # defined default 704 setattr( 705 self.options, 706 self._logfile_loglevel_config_setting_name_, 707 # From the console log level config setting 708 self.config.get( 709 self._loglevel_config_setting_name_, 710 self._default_logging_level_, 711 ), 712 ) 713 if self._logfile_loglevel_config_setting_name_ in self.config: 714 # Remove it from config so it inherits from log_level_logfile 715 self.config.pop(self._logfile_loglevel_config_setting_name_) 716 717 def __setup_logfile_logger_config(self): 718 if ( 719 self._logfile_loglevel_config_setting_name_ in self.config 720 and not self.config.get(self._logfile_loglevel_config_setting_name_) 721 ): 722 # Remove it from config so it inherits from log_level 723 self.config.pop(self._logfile_loglevel_config_setting_name_) 724 725 loglevel = getattr( 726 self.options, 727 # From the options setting 728 self._logfile_loglevel_config_setting_name_, 729 # From the default setting 730 self._default_logging_level_, 731 ) 732 733 logfile = getattr( 734 self.options, 735 # From the options setting 736 self._logfile_config_setting_name_, 737 # From the default setting 738 self._default_logging_logfile_, 739 ) 740 741 cli_log_path = "cli_{}_log_file".format(self.get_prog_name().replace("-", "_")) 742 if cli_log_path in self.config and not self.config.get(cli_log_path): 743 # Remove it from config so it inherits from log_level_logfile 744 self.config.pop(cli_log_path) 745 746 if self._logfile_config_setting_name_ in self.config and not self.config.get( 747 self._logfile_config_setting_name_ 748 ): 749 # Remove it from config so it inherits from log_file 750 self.config.pop(self._logfile_config_setting_name_) 751 752 if self.config["verify_env"] and self.config["log_level"] not in ("quiet",): 753 # Verify the logfile if it was explicitly set but do not try to 754 # verify the default 755 if logfile is not None: 756 # Logfile is not using Syslog, verify 757 with salt.utils.files.set_umask(0o027): 758 verify_log_files([logfile], self.config["user"]) 759 760 if logfile is None: 761 # Use the default setting if the logfile wasn't explicity set 762 logfile = self._default_logging_logfile_ 763 764 cli_log_file_fmt = "cli_{}_log_file_fmt".format( 765 self.get_prog_name().replace("-", "_") 766 ) 767 if cli_log_file_fmt in self.config and not self.config.get(cli_log_file_fmt): 768 # Remove it from config so it inherits from log_fmt_logfile 769 self.config.pop(cli_log_file_fmt) 770 771 if self.config.get("log_fmt_logfile", None) is None: 772 # Remove it from config so it inherits from log_fmt_console 773 self.config.pop("log_fmt_logfile", None) 774 775 log_file_fmt = self.config.get( 776 "log_fmt_logfile", 777 self.config.get( 778 "log_fmt_console", 779 self.config.get("log_fmt", config._DFLT_LOG_FMT_CONSOLE), 780 ), 781 ) 782 783 if self.config.get("log_datefmt_logfile", None) is None: 784 # Remove it from config so it inherits from log_datefmt_console 785 self.config.pop("log_datefmt_logfile", None) 786 787 if self.config.get("log_datefmt_console", None) is None: 788 # Remove it from config so it inherits from log_datefmt 789 self.config.pop("log_datefmt_console", None) 790 791 log_file_datefmt = self.config.get( 792 "log_datefmt_logfile", 793 self.config.get( 794 "log_datefmt_console", 795 self.config.get("log_datefmt", "%Y-%m-%d %H:%M:%S"), 796 ), 797 ) 798 799 if not is_writeable(logfile, check_parent=True): 800 # Since we're not be able to write to the log file or its parent 801 # directory (if the log file does not exit), are we the same user 802 # as the one defined in the configuration file? 803 current_user = salt.utils.user.get_user() 804 if self.config["user"] != current_user: 805 # Yep, not the same user! 806 # Is the current user in ACL? 807 acl = self.config["publisher_acl"] 808 if salt.utils.stringutils.check_whitelist_blacklist( 809 current_user, whitelist=acl.keys() 810 ): 811 # Yep, the user is in ACL! 812 # Let's write the logfile to its home directory instead. 813 xdg_dir = salt.utils.xdg.xdg_config_dir() 814 user_salt_dir = ( 815 xdg_dir 816 if os.path.isdir(xdg_dir) 817 else os.path.expanduser("~/.salt") 818 ) 819 820 if not os.path.isdir(user_salt_dir): 821 os.makedirs(user_salt_dir, 0o750) 822 logfile_basename = os.path.basename(self._default_logging_logfile_) 823 logger.debug( 824 "The user '%s' is not allowed to write to '%s'. " 825 "The log file will be stored in '~/.salt/'%s'.log'", 826 str(current_user), 827 str(logfile), 828 str(logfile_basename), 829 ) 830 logfile = os.path.join( 831 user_salt_dir, "{}.log".format(logfile_basename) 832 ) 833 834 # If we haven't changed the logfile path and it's not writeable, 835 # salt will fail once we try to setup the logfile logging. 836 837 # Log rotate options 838 log_rotate_max_bytes = self.config.get("log_rotate_max_bytes", 0) 839 log_rotate_backup_count = self.config.get("log_rotate_backup_count", 0) 840 if not salt.utils.platform.is_windows(): 841 # Not supported on platforms other than Windows. 842 # Other platforms may use an external tool such as 'logrotate' 843 if log_rotate_max_bytes != 0: 844 logger.warning("'log_rotate_max_bytes' is only supported on Windows") 845 log_rotate_max_bytes = 0 846 if log_rotate_backup_count != 0: 847 logger.warning("'log_rotate_backup_count' is only supported on Windows") 848 log_rotate_backup_count = 0 849 850 # Save the settings back to the configuration 851 self.config[self._logfile_config_setting_name_] = logfile 852 self.config[self._logfile_loglevel_config_setting_name_] = loglevel 853 self.config["log_fmt_logfile"] = log_file_fmt 854 self.config["log_datefmt_logfile"] = log_file_datefmt 855 self.config["log_rotate_max_bytes"] = log_rotate_max_bytes 856 self.config["log_rotate_backup_count"] = log_rotate_backup_count 857 858 def setup_logfile_logger(self): 859 if salt.utils.platform.is_windows() and self._setup_mp_logging_listener_: 860 # On Windows when using a logging listener, all log file logging 861 # will go through the logging listener. 862 return 863 864 logfile = self.config[self._logfile_config_setting_name_] 865 loglevel = self.config[self._logfile_loglevel_config_setting_name_] 866 log_file_fmt = self.config["log_fmt_logfile"] 867 log_file_datefmt = self.config["log_datefmt_logfile"] 868 log_rotate_max_bytes = self.config["log_rotate_max_bytes"] 869 log_rotate_backup_count = self.config["log_rotate_backup_count"] 870 871 log.setup_logfile_logger( 872 logfile, 873 loglevel, 874 log_format=log_file_fmt, 875 date_format=log_file_datefmt, 876 max_bytes=log_rotate_max_bytes, 877 backup_count=log_rotate_backup_count, 878 ) 879 for name, level in self.config.get("log_granular_levels", {}).items(): 880 log.set_logger_level(name, level) 881 882 def __setup_extended_logging(self): 883 if salt.utils.platform.is_windows() and self._setup_mp_logging_listener_: 884 # On Windows when using a logging listener, all extended logging 885 # will go through the logging listener. 886 return 887 log.setup_extended_logging(self.config) 888 889 def _get_mp_logging_listener_queue(self): 890 return log.get_multiprocessing_logging_queue() 891 892 def _setup_mp_logging_listener(self): 893 if self._setup_mp_logging_listener_: 894 log.setup_multiprocessing_logging_listener( 895 self.config, self._get_mp_logging_listener_queue() 896 ) 897 898 def _setup_mp_logging_client(self): 899 if self._setup_mp_logging_listener_: 900 # Set multiprocessing logging level even in non-Windows 901 # environments. In non-Windows environments, this setting will 902 # propogate from process to process via fork behavior and will be 903 # used by child processes if they invoke the multiprocessing 904 # logging client. 905 log.set_multiprocessing_logging_level_by_opts(self.config) 906 907 if salt.utils.platform.is_windows(): 908 # On Windows, all logging including console and 909 # log file logging will go through the multiprocessing 910 # logging listener if it exists. 911 # This will allow log file rotation on Windows 912 # since only one process can own the log file 913 # for log file rotation to work. 914 log.setup_multiprocessing_logging(self._get_mp_logging_listener_queue()) 915 # Remove the temp logger and any other configured loggers since 916 # all of our logging is going through the multiprocessing 917 # logging listener. 918 log.shutdown_temp_logging() 919 log.shutdown_console_logging() 920 log.shutdown_logfile_logging() 921 922 def __setup_console_logger_config(self): 923 # Since we're not going to be a daemon, setup the console logger 924 logfmt = self.config.get( 925 "log_fmt_console", self.config.get("log_fmt", config._DFLT_LOG_FMT_CONSOLE) 926 ) 927 928 if self.config.get("log_datefmt_console", None) is None: 929 # Remove it from config so it inherits from log_datefmt 930 self.config.pop("log_datefmt_console", None) 931 932 datefmt = self.config.get( 933 "log_datefmt_console", self.config.get("log_datefmt", "%Y-%m-%d %H:%M:%S") 934 ) 935 936 # Save the settings back to the configuration 937 self.config["log_fmt_console"] = logfmt 938 self.config["log_datefmt_console"] = datefmt 939 940 def __setup_console_logger(self): 941 # If daemon is set force console logger to quiet 942 if getattr(self.options, "daemon", False) is True: 943 return 944 945 if salt.utils.platform.is_windows() and self._setup_mp_logging_listener_: 946 # On Windows when using a logging listener, all console logging 947 # will go through the logging listener. 948 return 949 950 # ensure that yaml stays valid with log output 951 if getattr(self.options, "output", None) == "yaml": 952 log_format = "# {}".format(self.config["log_fmt_console"]) 953 else: 954 log_format = self.config["log_fmt_console"] 955 956 log.setup_console_logger( 957 self.config["log_level"], 958 log_format=log_format, 959 date_format=self.config["log_datefmt_console"], 960 ) 961 for name, level in self.config.get("log_granular_levels", {}).items(): 962 log.set_logger_level(name, level) 963 964 965class RunUserMixin(metaclass=MixInMeta): 966 _mixin_prio_ = 20 967 968 def _mixin_setup(self): 969 self.add_option( 970 "-u", "--user", help="Specify user to run {}.".format(self.get_prog_name()) 971 ) 972 973 974class DaemonMixIn(metaclass=MixInMeta): 975 _mixin_prio_ = 30 976 977 def _mixin_setup(self): 978 self.add_option( 979 "-d", 980 "--daemon", 981 default=False, 982 action="store_true", 983 help="Run the {} as a daemon.".format(self.get_prog_name()), 984 ) 985 self.add_option( 986 "--pid-file", 987 dest="pidfile", 988 default=os.path.join( 989 syspaths.PIDFILE_DIR, "{}.pid".format(self.get_prog_name()) 990 ), 991 help="Specify the location of the pidfile. Default: '%default'.", 992 ) 993 994 def _mixin_before_exit(self): 995 if hasattr(self, "config") and self.config.get("pidfile"): 996 # We've loaded and merged options into the configuration, it's safe 997 # to query about the pidfile 998 if self.check_pidfile(): 999 try: 1000 os.unlink(self.config["pidfile"]) 1001 except OSError as err: 1002 # Log error only when running salt-master as a root user. 1003 # Otherwise this can be ignored, since salt-master is able to 1004 # overwrite the PIDfile on the next start. 1005 err_msg = ( 1006 "PIDfile could not be deleted: %s", 1007 str(self.config["pidfile"]), 1008 ) 1009 if salt.utils.platform.is_windows(): 1010 user = salt.utils.win_functions.get_current_user() 1011 if salt.utils.win_functions.is_admin(user): 1012 logger.info(*err_msg) 1013 logger.debug(str(err)) 1014 else: 1015 if not os.getuid(): 1016 logger.info(*err_msg) 1017 logger.debug(str(err)) 1018 1019 def set_pidfile(self): 1020 from salt.utils.process import set_pidfile 1021 1022 set_pidfile(self.config["pidfile"], self.config["user"]) 1023 1024 def check_pidfile(self): 1025 """ 1026 Report whether a pidfile exists 1027 """ 1028 from salt.utils.process import check_pidfile 1029 1030 return check_pidfile(self.config["pidfile"]) 1031 1032 def get_pidfile(self): 1033 """ 1034 Return a pid contained in a pidfile 1035 """ 1036 from salt.utils.process import get_pidfile 1037 1038 return get_pidfile(self.config["pidfile"]) 1039 1040 def daemonize_if_required(self): 1041 if self.options.daemon: 1042 if self._setup_mp_logging_listener_ is True: 1043 # Stop the logging queue listener for the current process 1044 # We'll restart it once forked 1045 log.shutdown_multiprocessing_logging_listener(daemonizing=True) 1046 1047 # Late import so logging works correctly 1048 salt.utils.process.daemonize() 1049 1050 # Setup the multiprocessing log queue listener if enabled 1051 self._setup_mp_logging_listener() 1052 1053 def check_running(self): 1054 """ 1055 Check if a pid file exists and if it is associated with 1056 a running process. 1057 """ 1058 1059 if self.check_pidfile(): 1060 pid = self.get_pidfile() 1061 if not salt.utils.platform.is_windows(): 1062 if ( 1063 self.check_pidfile() 1064 and self.is_daemonized(pid) 1065 and os.getppid() != pid 1066 ): 1067 return True 1068 else: 1069 # We have no os.getppid() on Windows. Use salt.utils.win_functions.get_parent_pid 1070 if ( 1071 self.check_pidfile() 1072 and self.is_daemonized(pid) 1073 and salt.utils.win_functions.get_parent_pid() != pid 1074 ): 1075 return True 1076 return False 1077 1078 def claim_process_responsibility(self): 1079 """ 1080 This will stop from more than on prcoess from doing the same task 1081 """ 1082 responsibility_file = os.path.split(self.config["pidfile"]) 1083 responsibility_file = os.path.join( 1084 responsibility_file[0], "process_responsibility_" + responsibility_file[1] 1085 ) 1086 return salt.utils.process.claim_mantle_of_responsibility(responsibility_file) 1087 1088 def is_daemonized(self, pid): 1089 from salt.utils.process import os_is_running 1090 1091 return os_is_running(pid) 1092 1093 # Common methods for scripts which can daemonize 1094 def _install_signal_handlers(self): 1095 signal.signal(signal.SIGTERM, self._handle_signals) 1096 signal.signal(signal.SIGINT, self._handle_signals) 1097 1098 def prepare(self): 1099 self.parse_args() 1100 1101 def start(self): 1102 self.prepare() 1103 self._install_signal_handlers() 1104 1105 def _handle_signals(self, signum, sigframe): # pylint: disable=unused-argument 1106 msg = self.__class__.__name__ 1107 if signum == signal.SIGINT: 1108 msg += " received a SIGINT." 1109 elif signum == signal.SIGTERM: 1110 msg += " received a SIGTERM." 1111 logging.getLogger(__name__).warning("%s Exiting.", msg) 1112 self.shutdown(exitmsg="{} Exited.".format(msg)) 1113 1114 def shutdown(self, exitcode=0, exitmsg=None): 1115 self.exit(exitcode, exitmsg) 1116 1117 1118class TargetOptionsMixIn(metaclass=MixInMeta): 1119 1120 _mixin_prio_ = 20 1121 1122 selected_target_option = None 1123 1124 def _mixin_setup(self): 1125 group = self.target_options_group = optparse.OptionGroup( 1126 self, "Target Options", "Target selection options." 1127 ) 1128 self.add_option_group(group) 1129 group.add_option( 1130 "-H", 1131 "--hosts", 1132 default=False, 1133 action="store_true", 1134 dest="list_hosts", 1135 help="List all known hosts to currently visible or other specified rosters", 1136 ) 1137 group.add_option( 1138 "-E", 1139 "--pcre", 1140 default=False, 1141 action="store_true", 1142 help=( 1143 "Instead of using shell globs to evaluate the target " 1144 "servers, use pcre regular expressions." 1145 ), 1146 ) 1147 group.add_option( 1148 "-L", 1149 "--list", 1150 default=False, 1151 action="store_true", 1152 help=( 1153 "Instead of using shell globs to evaluate the target " 1154 "servers, take a comma or whitespace delimited list of " 1155 "servers." 1156 ), 1157 ) 1158 group.add_option( 1159 "-G", 1160 "--grain", 1161 default=False, 1162 action="store_true", 1163 help=( 1164 "Instead of using shell globs to evaluate the target " 1165 "use a grain value to identify targets, the syntax " 1166 "for the target is the grain key followed by a glob" 1167 'expression: "os:Arch*".' 1168 ), 1169 ) 1170 group.add_option( 1171 "-P", 1172 "--grain-pcre", 1173 default=False, 1174 action="store_true", 1175 help=( 1176 "Instead of using shell globs to evaluate the target " 1177 "use a grain value to identify targets, the syntax " 1178 "for the target is the grain key followed by a pcre " 1179 'regular expression: "os:Arch.*".' 1180 ), 1181 ) 1182 group.add_option( 1183 "-N", 1184 "--nodegroup", 1185 default=False, 1186 action="store_true", 1187 help=( 1188 "Instead of using shell globs to evaluate the target " 1189 "use one of the predefined nodegroups to identify a " 1190 "list of targets." 1191 ), 1192 ) 1193 group.add_option( 1194 "-R", 1195 "--range", 1196 default=False, 1197 action="store_true", 1198 help=( 1199 "Instead of using shell globs to evaluate the target " 1200 "use a range expression to identify targets. " 1201 "Range expressions look like %cluster." 1202 ), 1203 ) 1204 1205 group = self.additional_target_options_group = optparse.OptionGroup( 1206 self, 1207 "Additional Target Options", 1208 "Additional options for minion targeting.", 1209 ) 1210 self.add_option_group(group) 1211 group.add_option( 1212 "--delimiter", 1213 default=DEFAULT_TARGET_DELIM, 1214 help=( 1215 "Change the default delimiter for matching in multi-level " 1216 "data structures. Default: '%default'." 1217 ), 1218 ) 1219 1220 self._create_process_functions() 1221 1222 def _create_process_functions(self): 1223 for option in self.target_options_group.option_list: 1224 1225 def process(opt): 1226 if getattr(self.options, opt.dest): 1227 self.selected_target_option = opt.dest 1228 1229 funcname = "process_{}".format(option.dest) 1230 if not hasattr(self, funcname): 1231 setattr(self, funcname, partial(process, option)) 1232 1233 def _mixin_after_parsed(self): 1234 group_options_selected = [ 1235 option 1236 for option in self.target_options_group.option_list 1237 if getattr(self.options, option.dest) is True 1238 ] 1239 if len(group_options_selected) > 1: 1240 self.error( 1241 "The options {} are mutually exclusive. Please only choose " 1242 "one of them".format( 1243 "/".join( 1244 [option.get_opt_string() for option in group_options_selected] 1245 ) 1246 ) 1247 ) 1248 self.config["selected_target_option"] = self.selected_target_option 1249 1250 1251class ExtendedTargetOptionsMixIn(TargetOptionsMixIn): 1252 def _mixin_setup(self): 1253 TargetOptionsMixIn._mixin_setup(self) 1254 group = self.target_options_group 1255 group.add_option( 1256 "-C", 1257 "--compound", 1258 default=False, 1259 action="store_true", 1260 help=( 1261 "The compound target option allows for multiple target " 1262 "types to be evaluated, allowing for greater granularity in " 1263 "target matching. The compound target is space delimited, " 1264 "targets other than globs are preceded with an identifier " 1265 "matching the specific targets argument type: salt " 1266 "'G@os:RedHat and webser* or E@database.*'." 1267 ), 1268 ) 1269 group.add_option( 1270 "-I", 1271 "--pillar", 1272 default=False, 1273 dest="pillar_target", 1274 action="store_true", 1275 help=( 1276 "Instead of using shell globs to evaluate the target " 1277 "use a pillar value to identify targets, the syntax " 1278 "for the target is the pillar key followed by a glob " 1279 'expression: "role:production*".' 1280 ), 1281 ) 1282 group.add_option( 1283 "-J", 1284 "--pillar-pcre", 1285 default=False, 1286 action="store_true", 1287 help=( 1288 "Instead of using shell globs to evaluate the target " 1289 "use a pillar value to identify targets, the syntax " 1290 "for the target is the pillar key followed by a pcre " 1291 'regular expression: "role:prod.*".' 1292 ), 1293 ) 1294 group.add_option( 1295 "-S", 1296 "--ipcidr", 1297 default=False, 1298 action="store_true", 1299 help="Match based on Subnet (CIDR notation) or IP address.", 1300 ) 1301 1302 self._create_process_functions() 1303 1304 def process_pillar_target(self): 1305 if self.options.pillar_target: 1306 self.selected_target_option = "pillar" 1307 1308 1309class TimeoutMixIn(metaclass=MixInMeta): 1310 _mixin_prio_ = 10 1311 1312 def _mixin_setup(self): 1313 if not hasattr(self, "default_timeout"): 1314 raise RuntimeError( 1315 "You need to define the 'default_timeout' attribute on {}".format( 1316 self.__class__.__name__ 1317 ) 1318 ) 1319 self.add_option( 1320 "-t", 1321 "--timeout", 1322 type=int, 1323 default=self.default_timeout, 1324 help=( 1325 "Change the timeout, if applicable, for the running " 1326 "command (in seconds). Default: %default." 1327 ), 1328 ) 1329 1330 1331class ArgsStdinMixIn(metaclass=MixInMeta): 1332 _mixin_prio_ = 10 1333 1334 def _mixin_setup(self): 1335 self.add_option( 1336 "--args-stdin", 1337 default=False, 1338 dest="args_stdin", 1339 action="store_true", 1340 help=( 1341 "Read additional options and/or arguments from stdin. " 1342 "Each entry is newline separated." 1343 ), 1344 ) 1345 1346 1347class ProxyIdMixIn(metaclass=MixInMeta): 1348 _mixin_prio = 40 1349 1350 def _mixin_setup(self): 1351 self.add_option( 1352 "--proxyid", default=None, dest="proxyid", help="Id for this proxy." 1353 ) 1354 1355 1356class ExecutorsMixIn(metaclass=MixInMeta): 1357 _mixin_prio = 10 1358 1359 def _mixin_setup(self): 1360 self.add_option( 1361 "--module-executors", 1362 dest="module_executors", 1363 default=None, 1364 metavar="EXECUTOR_LIST", 1365 help=( 1366 "Set an alternative list of executors to override the one " 1367 "set in minion config." 1368 ), 1369 ) 1370 self.add_option( 1371 "--executor-opts", 1372 dest="executor_opts", 1373 default=None, 1374 metavar="EXECUTOR_OPTS", 1375 help=( 1376 "Set alternate executor options if supported by executor. " 1377 "Options set by minion config are used by default." 1378 ), 1379 ) 1380 1381 1382class CacheDirMixIn(metaclass=MixInMeta): 1383 _mixin_prio = 40 1384 1385 def _mixin_setup(self): 1386 self.add_option( 1387 "--cachedir", 1388 default="/var/cache/salt/", 1389 dest="cachedir", 1390 help="Cache Directory", 1391 ) 1392 1393 1394class OutputOptionsMixIn(metaclass=MixInMeta): 1395 1396 _mixin_prio_ = 40 1397 _include_text_out_ = False 1398 1399 selected_output_option = None 1400 1401 def _mixin_setup(self): 1402 group = self.output_options_group = optparse.OptionGroup( 1403 self, "Output Options", "Configure your preferred output format." 1404 ) 1405 self.add_option_group(group) 1406 1407 group.add_option( 1408 "--out", 1409 "--output", 1410 dest="output", 1411 help=( 1412 "Print the output from the '{}' command using the " 1413 "specified outputter.".format( 1414 self.get_prog_name(), 1415 ) 1416 ), 1417 ) 1418 group.add_option( 1419 "--out-indent", 1420 "--output-indent", 1421 dest="output_indent", 1422 default=None, 1423 type=int, 1424 help=( 1425 "Print the output indented by the provided value in spaces. " 1426 "Negative values disables indentation. Only applicable in " 1427 "outputters that support indentation." 1428 ), 1429 ) 1430 group.add_option( 1431 "--out-file", 1432 "--output-file", 1433 dest="output_file", 1434 default=None, 1435 help="Write the output to the specified file.", 1436 ) 1437 group.add_option( 1438 "--out-file-append", 1439 "--output-file-append", 1440 action="store_true", 1441 dest="output_file_append", 1442 default=False, 1443 help="Append the output to the specified file.", 1444 ) 1445 group.add_option( 1446 "--no-color", 1447 "--no-colour", 1448 default=False, 1449 action="store_true", 1450 help="Disable all colored output.", 1451 ) 1452 group.add_option( 1453 "--force-color", 1454 "--force-colour", 1455 default=False, 1456 action="store_true", 1457 help="Force colored output.", 1458 ) 1459 group.add_option( 1460 "--state-output", 1461 "--state_output", 1462 default=None, 1463 help=( 1464 "Override the configured state_output value for minion " 1465 "output. One of 'full', 'terse', 'mixed', 'changes' or 'filter'. " 1466 "Default: '%default'." 1467 ), 1468 ) 1469 group.add_option( 1470 "--state-verbose", 1471 "--state_verbose", 1472 default=None, 1473 help=( 1474 "Override the configured state_verbose value for minion " 1475 "output. Set to True or False. Default: %default." 1476 ), 1477 ) 1478 1479 for option in self.output_options_group.option_list: 1480 1481 def process(opt): 1482 default = self.defaults.get(opt.dest) 1483 if getattr(self.options, opt.dest, default) is False: 1484 return 1485 self.selected_output_option = opt.dest 1486 1487 funcname = "process_{}".format(option.dest) 1488 if not hasattr(self, funcname): 1489 setattr(self, funcname, partial(process, option)) 1490 1491 def process_output(self): 1492 self.selected_output_option = self.options.output 1493 1494 def process_output_file(self): 1495 if ( 1496 self.options.output_file is not None 1497 and self.options.output_file_append is False 1498 ): 1499 if os.path.isfile(self.options.output_file): 1500 try: 1501 with salt.utils.files.fopen(self.options.output_file, "w"): 1502 # Make this a zero length filename instead of removing 1503 # it. This way we keep the file permissions. 1504 pass 1505 except OSError as exc: 1506 self.error( 1507 "{}: Access denied: {}".format(self.options.output_file, exc) 1508 ) 1509 1510 def process_state_verbose(self): 1511 if self.options.state_verbose == "True" or self.options.state_verbose == "true": 1512 self.options.state_verbose = True 1513 elif ( 1514 self.options.state_verbose == "False" 1515 or self.options.state_verbose == "false" 1516 ): 1517 self.options.state_verbose = False 1518 1519 def _mixin_after_parsed(self): 1520 group_options_selected = [ 1521 option 1522 for option in self.output_options_group.option_list 1523 if ( 1524 getattr(self.options, option.dest) 1525 and (option.dest.endswith("_out") or option.dest == "output") 1526 ) 1527 ] 1528 if len(group_options_selected) > 1: 1529 self.error( 1530 "The options {} are mutually exclusive. Please only choose " 1531 "one of them".format( 1532 "/".join( 1533 [option.get_opt_string() for option in group_options_selected] 1534 ) 1535 ) 1536 ) 1537 self.config["selected_output_option"] = self.selected_output_option 1538 1539 1540class ExecutionOptionsMixIn(metaclass=MixInMeta): 1541 _mixin_prio_ = 10 1542 1543 def _mixin_setup(self): 1544 group = self.execution_group = optparse.OptionGroup( 1545 self, 1546 "Execution Options", 1547 # Include description here as a string 1548 ) 1549 group.add_option( 1550 "-L", "--location", default=None, help="Specify which region to connect to." 1551 ) 1552 group.add_option( 1553 "-a", 1554 "--action", 1555 default=None, 1556 help=( 1557 "Perform an action that may be specific to this cloud " 1558 "provider. This argument requires one or more instance " 1559 "names to be specified." 1560 ), 1561 ) 1562 group.add_option( 1563 "-f", 1564 "--function", 1565 nargs=2, 1566 default=None, 1567 metavar="<FUNC-NAME> <PROVIDER>", 1568 help=( 1569 "Perform a function that may be specific to this cloud " 1570 "provider, that does not apply to an instance. This " 1571 "argument requires a provider to be specified (i.e.: nova)." 1572 ), 1573 ) 1574 group.add_option( 1575 "-p", 1576 "--profile", 1577 default=None, 1578 help="Create an instance using the specified profile.", 1579 ) 1580 group.add_option( 1581 "-m", 1582 "--map", 1583 default=None, 1584 help=( 1585 "Specify a cloud map file to use for deployment. This option " 1586 "may be used alone, or in conjunction with -Q, -F, -S or -d. " 1587 "The map can also be filtered by a list of VM names." 1588 ), 1589 ) 1590 group.add_option( 1591 "-H", 1592 "--hard", 1593 default=False, 1594 action="store_true", 1595 help=( 1596 "Delete all VMs that are not defined in the map file. " 1597 "CAUTION!!! This operation can irrevocably destroy VMs! It " 1598 "must be explicitly enabled in the cloud config file." 1599 ), 1600 ) 1601 group.add_option( 1602 "-d", 1603 "--destroy", 1604 default=False, 1605 action="store_true", 1606 help="Destroy the specified instance(s).", 1607 ) 1608 group.add_option( 1609 "--no-deploy", 1610 default=True, 1611 dest="deploy", 1612 action="store_false", 1613 help="Don't run a deploy script after instance creation.", 1614 ) 1615 group.add_option( 1616 "-P", 1617 "--parallel", 1618 default=False, 1619 action="store_true", 1620 help="Build all of the specified instances in parallel.", 1621 ) 1622 group.add_option( 1623 "-u", 1624 "--update-bootstrap", 1625 default=False, 1626 action="store_true", 1627 help="Update salt-bootstrap to the latest stable bootstrap release.", 1628 ) 1629 group.add_option( 1630 "-y", 1631 "--assume-yes", 1632 default=False, 1633 action="store_true", 1634 help='Default "yes" in answer to all confirmation questions.', 1635 ) 1636 group.add_option( 1637 "-k", 1638 "--keep-tmp", 1639 default=False, 1640 action="store_true", 1641 help="Do not remove files from /tmp/ after deploy.sh finishes.", 1642 ) 1643 group.add_option( 1644 "--show-deploy-args", 1645 default=False, 1646 action="store_true", 1647 help="Include the options used to deploy the minion in the data returned.", 1648 ) 1649 group.add_option( 1650 "--script-args", 1651 default=None, 1652 help=( 1653 "Script arguments to be fed to the bootstrap script when " 1654 "deploying the VM." 1655 ), 1656 ) 1657 group.add_option( 1658 "-b", 1659 "--bootstrap", 1660 nargs=1, 1661 default=False, 1662 metavar="<HOST> [MINION_ID] [OPTIONS...]", 1663 help="Bootstrap an existing machine.", 1664 ) 1665 self.add_option_group(group) 1666 1667 def process_function(self): 1668 if self.options.function: 1669 self.function_name, self.function_provider = self.options.function 1670 if self.function_provider.startswith("-") or "=" in self.function_provider: 1671 self.error( 1672 "--function expects two arguments: <function-name> <provider>" 1673 ) 1674 1675 1676class CloudQueriesMixIn(metaclass=MixInMeta): 1677 _mixin_prio_ = 20 1678 1679 selected_query_option = None 1680 1681 def _mixin_setup(self): 1682 group = self.cloud_queries_group = optparse.OptionGroup( 1683 self, 1684 "Query Options", 1685 # Include description here as a string 1686 ) 1687 group.add_option( 1688 "-Q", 1689 "--query", 1690 default=False, 1691 action="store_true", 1692 help=( 1693 "Execute a query and return some information about the " 1694 "nodes running on configured cloud providers." 1695 ), 1696 ) 1697 group.add_option( 1698 "-F", 1699 "--full-query", 1700 default=False, 1701 action="store_true", 1702 help=( 1703 "Execute a query and return all information about the " 1704 "nodes running on configured cloud providers." 1705 ), 1706 ) 1707 group.add_option( 1708 "-S", 1709 "--select-query", 1710 default=False, 1711 action="store_true", 1712 help=( 1713 "Execute a query and return select information about " 1714 "the nodes running on configured cloud providers." 1715 ), 1716 ) 1717 group.add_option( 1718 "--list-providers", 1719 default=False, 1720 action="store_true", 1721 help="Display a list of configured providers.", 1722 ) 1723 group.add_option( 1724 "--list-profiles", 1725 default=None, 1726 action="store", 1727 help=( 1728 "Display a list of configured profiles. Pass in a cloud " 1729 "provider to view the provider's associated profiles, " 1730 'such as digitalocean, or pass in "all" to list all the ' 1731 "configured profiles." 1732 ), 1733 ) 1734 self.add_option_group(group) 1735 self._create_process_functions() 1736 1737 def _create_process_functions(self): 1738 for option in self.cloud_queries_group.option_list: 1739 1740 def process(opt): 1741 if getattr(self.options, opt.dest): 1742 query = "list_nodes" 1743 if opt.dest == "full_query": 1744 query += "_full" 1745 elif opt.dest == "select_query": 1746 query += "_select" 1747 elif opt.dest == "list_providers": 1748 query = "list_providers" 1749 if self.args: 1750 self.error( 1751 "'--list-providers' does not accept any arguments" 1752 ) 1753 elif opt.dest == "list_profiles": 1754 query = "list_profiles" 1755 option_dict = vars(self.options) 1756 if option_dict.get("list_profiles") == "--list-providers": 1757 self.error( 1758 "'--list-profiles' does not accept " 1759 "'--list-providers' as an argument" 1760 ) 1761 self.selected_query_option = query 1762 1763 funcname = "process_{}".format(option.dest) 1764 if not hasattr(self, funcname): 1765 setattr(self, funcname, partial(process, option)) 1766 1767 def _mixin_after_parsed(self): 1768 group_options_selected = [ 1769 option 1770 for option in self.cloud_queries_group.option_list 1771 if getattr(self.options, option.dest) is not False 1772 and getattr(self.options, option.dest) is not None 1773 ] 1774 if len(group_options_selected) > 1: 1775 self.error( 1776 "The options {} are mutually exclusive. Please only choose " 1777 "one of them".format( 1778 "/".join( 1779 [option.get_opt_string() for option in group_options_selected] 1780 ) 1781 ) 1782 ) 1783 self.config["selected_query_option"] = self.selected_query_option 1784 1785 1786class CloudProvidersListsMixIn(metaclass=MixInMeta): 1787 _mixin_prio_ = 30 1788 1789 def _mixin_setup(self): 1790 group = self.providers_listings_group = optparse.OptionGroup( 1791 self, 1792 "Cloud Providers Listings", 1793 # Include description here as a string 1794 ) 1795 group.add_option( 1796 "--list-locations", 1797 default=None, 1798 help=( 1799 "Display a list of locations available in configured cloud " 1800 "providers. Pass the cloud provider that available " 1801 'locations are desired on, such as "linode", or pass "all" to ' 1802 "list locations for all configured cloud providers." 1803 ), 1804 ) 1805 group.add_option( 1806 "--list-images", 1807 default=None, 1808 help=( 1809 "Display a list of images available in configured cloud " 1810 "providers. Pass the cloud provider that available images " 1811 'are desired on, such as "linode", or pass "all" to list images ' 1812 "for all configured cloud providers." 1813 ), 1814 ) 1815 group.add_option( 1816 "--list-sizes", 1817 default=None, 1818 help=( 1819 "Display a list of sizes available in configured cloud " 1820 "providers. Pass the cloud provider that available sizes " 1821 'are desired on, such as "AWS", or pass "all" to list sizes ' 1822 "for all configured cloud providers." 1823 ), 1824 ) 1825 self.add_option_group(group) 1826 1827 def _mixin_after_parsed(self): 1828 list_options_selected = [ 1829 option 1830 for option in self.providers_listings_group.option_list 1831 if getattr(self.options, option.dest) is not None 1832 ] 1833 if len(list_options_selected) > 1: 1834 self.error( 1835 "The options {} are mutually exclusive. Please only choose " 1836 "one of them".format( 1837 "/".join( 1838 [option.get_opt_string() for option in list_options_selected] 1839 ) 1840 ) 1841 ) 1842 1843 1844class ProfilingPMixIn(metaclass=MixInMeta): 1845 _mixin_prio_ = 130 1846 1847 def _mixin_setup(self): 1848 group = self.profiling_group = optparse.OptionGroup( 1849 self, 1850 "Profiling support", 1851 # Include description here as a string 1852 ) 1853 1854 group.add_option( 1855 "--profiling-path", 1856 dest="profiling_path", 1857 default="/tmp/stats", 1858 help=( 1859 "Folder that will hold all stats generations path. Default: '%default'." 1860 ), 1861 ) 1862 group.add_option( 1863 "--enable-profiling", 1864 dest="profiling_enabled", 1865 default=False, 1866 action="store_true", 1867 help="Enable generating profiling stats. See also: --profiling-path.", 1868 ) 1869 self.add_option_group(group) 1870 1871 1872class CloudCredentialsMixIn(metaclass=MixInMeta): 1873 _mixin_prio_ = 30 1874 1875 def _mixin_setup(self): 1876 group = self.cloud_credentials_group = optparse.OptionGroup( 1877 self, 1878 "Cloud Credentials", 1879 # Include description here as a string 1880 ) 1881 group.add_option( 1882 "--set-password", 1883 default=None, 1884 nargs=2, 1885 metavar="<USERNAME> <PROVIDER>", 1886 help=( 1887 "Configure password for a cloud provider and save it to the keyring. " 1888 "PROVIDER can be specified with or without a driver, for example: " 1889 '"--set-password bob rackspace" or more specific ' 1890 '"--set-password bob rackspace:openstack" ' 1891 "Deprecated." 1892 ), 1893 ) 1894 self.add_option_group(group) 1895 1896 def process_set_password(self): 1897 if self.options.set_password: 1898 raise RuntimeError( 1899 "This functionality is not supported; please see the keyring module at" 1900 " https://docs.saltproject.io/en/latest/topics/sdb/" 1901 ) 1902 1903 1904class EAuthMixIn(metaclass=MixInMeta): 1905 _mixin_prio_ = 30 1906 1907 def _mixin_setup(self): 1908 group = self.eauth_group = optparse.OptionGroup( 1909 self, 1910 "External Authentication", 1911 # Include description here as a string 1912 ) 1913 group.add_option( 1914 "-a", 1915 "--auth", 1916 "--eauth", 1917 "--external-auth", 1918 default="", 1919 dest="eauth", 1920 help="Specify an external authentication system to use.", 1921 ) 1922 group.add_option( 1923 "-T", 1924 "--make-token", 1925 default=False, 1926 dest="mktoken", 1927 action="store_true", 1928 help=( 1929 "Generate and save an authentication token for re-use. The " 1930 "token is generated and made available for the period " 1931 "defined in the Salt Master." 1932 ), 1933 ) 1934 group.add_option( 1935 "--username", 1936 dest="username", 1937 nargs=1, 1938 help="Username for external authentication.", 1939 ) 1940 group.add_option( 1941 "--password", 1942 dest="password", 1943 nargs=1, 1944 help="Password for external authentication.", 1945 ) 1946 self.add_option_group(group) 1947 1948 1949class JIDMixin: 1950 1951 _mixin_prio_ = 30 1952 1953 def _mixin_setup(self): 1954 self.add_option( 1955 "--jid", 1956 default=None, 1957 help="Pass a JID to be used instead of generating one.", 1958 ) 1959 1960 def process_jid(self): 1961 if self.options.jid is not None: 1962 if not salt.utils.jid.is_jid(self.options.jid): 1963 self.error("'{}' is not a valid JID".format(self.options.jid)) 1964 1965 1966class MasterOptionParser( 1967 OptionParser, 1968 ConfigDirMixIn, 1969 MergeConfigMixIn, 1970 LogLevelMixIn, 1971 RunUserMixin, 1972 DaemonMixIn, 1973 SaltfileMixIn, 1974 metaclass=OptionParserMeta, 1975): 1976 1977 description = "The Salt Master, used to control the Salt Minions" 1978 1979 # ConfigDirMixIn config filename attribute 1980 _config_filename_ = "master" 1981 # LogLevelMixIn attributes 1982 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS["log_file"] 1983 _setup_mp_logging_listener_ = True 1984 1985 def setup_config(self): 1986 opts = config.master_config(self.get_config_file_path()) 1987 salt.features.setup_features(opts) 1988 return opts 1989 1990 1991class MinionOptionParser( 1992 MasterOptionParser, metaclass=OptionParserMeta 1993): # pylint: disable=no-init 1994 1995 description = "The Salt Minion, receives commands from a remote Salt Master" 1996 1997 # ConfigDirMixIn config filename attribute 1998 _config_filename_ = "minion" 1999 # LogLevelMixIn attributes 2000 _default_logging_logfile_ = config.DEFAULT_MINION_OPTS["log_file"] 2001 _setup_mp_logging_listener_ = True 2002 2003 def setup_config(self): 2004 opts = config.minion_config( 2005 self.get_config_file_path(), # pylint: disable=no-member 2006 cache_minion_id=True, 2007 ignore_config_errors=False, 2008 ) 2009 # Optimization: disable multiprocessing logging if running as a 2010 # daemon, without engines and without multiprocessing 2011 if ( 2012 not opts.get("engines") 2013 and not opts.get("multiprocessing", True) 2014 and self.options.daemon 2015 ): # pylint: disable=no-member 2016 self._setup_mp_logging_listener_ = False 2017 salt.features.setup_features(opts) 2018 return opts 2019 2020 2021class ProxyMinionOptionParser( 2022 OptionParser, 2023 ProxyIdMixIn, 2024 ConfigDirMixIn, 2025 MergeConfigMixIn, 2026 LogLevelMixIn, 2027 RunUserMixin, 2028 DaemonMixIn, 2029 SaltfileMixIn, 2030 metaclass=OptionParserMeta, 2031): # pylint: disable=no-init 2032 2033 description = ( 2034 "The Salt Proxy Minion, connects to and controls devices not able to run a" 2035 " minion.\nReceives commands from a remote Salt Master." 2036 ) 2037 2038 # ConfigDirMixIn config filename attribute 2039 _config_filename_ = "proxy" 2040 # LogLevelMixIn attributes 2041 _default_logging_logfile_ = config.DEFAULT_PROXY_MINION_OPTS["log_file"] 2042 2043 def setup_config(self): 2044 try: 2045 minion_id = self.values.proxyid 2046 except AttributeError: 2047 minion_id = None 2048 2049 opts = config.proxy_config( 2050 self.get_config_file_path(), cache_minion_id=False, minion_id=minion_id 2051 ) 2052 salt.features.setup_features(opts) 2053 return opts 2054 2055 2056class SyndicOptionParser( 2057 OptionParser, 2058 ConfigDirMixIn, 2059 MergeConfigMixIn, 2060 LogLevelMixIn, 2061 RunUserMixin, 2062 DaemonMixIn, 2063 SaltfileMixIn, 2064 metaclass=OptionParserMeta, 2065): 2066 2067 description = ( 2068 "The Salt Syndic daemon, a special Minion that passes through commands from" 2069 " a\nhigher Master. Scale Salt to thousands of hosts or across many different" 2070 " networks." 2071 ) 2072 2073 # ConfigDirMixIn config filename attribute 2074 _config_filename_ = "master" 2075 # LogLevelMixIn attributes 2076 _logfile_config_setting_name_ = "syndic_log_file" 2077 _default_logging_level_ = config.DEFAULT_MASTER_OPTS["log_level"] 2078 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS[ 2079 _logfile_config_setting_name_ 2080 ] 2081 _setup_mp_logging_listener_ = True 2082 2083 def setup_config(self): 2084 opts = config.syndic_config( 2085 self.get_config_file_path(), self.get_config_file_path("minion") 2086 ) 2087 salt.features.setup_features(opts) 2088 return opts 2089 2090 2091class SaltCMDOptionParser( 2092 OptionParser, 2093 ConfigDirMixIn, 2094 MergeConfigMixIn, 2095 TimeoutMixIn, 2096 ExtendedTargetOptionsMixIn, 2097 OutputOptionsMixIn, 2098 LogLevelMixIn, 2099 ExecutorsMixIn, 2100 HardCrashMixin, 2101 SaltfileMixIn, 2102 ArgsStdinMixIn, 2103 EAuthMixIn, 2104 NoParseMixin, 2105 metaclass=OptionParserMeta, 2106): 2107 2108 default_timeout = 5 2109 2110 description = ( 2111 "Salt allows for commands to be executed across a swath of remote systems in\n" 2112 "parallel, so they can be both controlled and queried with ease." 2113 ) 2114 2115 usage = "%prog [options] '<target>' <function> [arguments]" 2116 2117 # ConfigDirMixIn config filename attribute 2118 _config_filename_ = "master" 2119 2120 # LogLevelMixIn attributes 2121 _default_logging_level_ = config.DEFAULT_MASTER_OPTS["log_level"] 2122 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS["log_file"] 2123 2124 try: 2125 os.getcwd() 2126 except OSError: 2127 sys.exit("Cannot access current working directory. Exiting!") 2128 2129 def _mixin_setup(self): 2130 self.add_option( 2131 "-s", 2132 "--static", 2133 default=False, 2134 action="store_true", 2135 help="Return the data from minions as a group after they all return.", 2136 ) 2137 self.add_option( 2138 "-p", 2139 "--progress", 2140 default=False, 2141 action="store_true", 2142 help='Display a progress graph. Requires "progressbar" python package.', 2143 ) 2144 self.add_option( 2145 "--failhard", 2146 default=False, 2147 action="store_true", 2148 help='Stop batch execution upon first "bad" return.', 2149 ) 2150 self.add_option( 2151 "--async", 2152 default=False, 2153 dest="async", 2154 action="store_true", 2155 help="Run the salt command but don't wait for a reply.", 2156 ) 2157 self.add_option( 2158 "--subset", 2159 default=0, 2160 type=int, 2161 help=( 2162 "Execute the routine on a random subset of the targeted " 2163 "minions. The minions will be verified that they have the " 2164 "named function before executing." 2165 ), 2166 ) 2167 self.add_option( 2168 "-v", 2169 "--verbose", 2170 default=False, 2171 action="store_true", 2172 help="Turn on command verbosity, display jid and active job queries.", 2173 ) 2174 self.add_option( 2175 "--hide-timeout", 2176 dest="show_timeout", 2177 default=True, 2178 action="store_false", 2179 help="Hide minions that timeout.", 2180 ) 2181 self.add_option( 2182 "--show-jid", 2183 default=False, 2184 action="store_true", 2185 help="Display jid without the additional output of --verbose.", 2186 ) 2187 self.add_option( 2188 "-b", 2189 "--batch", 2190 "--batch-size", 2191 default="", 2192 dest="batch", 2193 help=( 2194 "Execute the salt job in batch mode, pass either the number " 2195 "of minions to batch at a time, or the percentage of " 2196 "minions to have running." 2197 ), 2198 ) 2199 self.add_option( 2200 "--batch-wait", 2201 default=0, 2202 dest="batch_wait", 2203 type=float, 2204 help=( 2205 "Wait the specified time in seconds after each job is done " 2206 "before freeing the slot in the batch for the next one." 2207 ), 2208 ) 2209 self.add_option( 2210 "--batch-safe-limit", 2211 default=0, 2212 dest="batch_safe_limit", 2213 type=int, 2214 help=( 2215 "Execute the salt job in batch mode if the job would have " 2216 "executed on at least this many minions." 2217 ), 2218 ) 2219 self.add_option( 2220 "--batch-safe-size", 2221 default=8, 2222 dest="batch_safe_size", 2223 help="Batch size to use for batch jobs created by batch-safe-limit.", 2224 ) 2225 self.add_option( 2226 "--return", 2227 default="", 2228 metavar="RETURNER", 2229 help=( 2230 "Set an alternative return method. By default salt will " 2231 "send the return data from the command back to the master, " 2232 "but the return data can be redirected into any number of " 2233 "systems, databases or applications." 2234 ), 2235 ) 2236 self.add_option( 2237 "--return_config", 2238 default="", 2239 metavar="RETURNER_CONF", 2240 help=( 2241 "Set an alternative return method. By default salt will " 2242 "send the return data from the command back to the master, " 2243 "but the return data can be redirected into any number of " 2244 "systems, databases or applications." 2245 ), 2246 ) 2247 self.add_option( 2248 "--return_kwargs", 2249 default={}, 2250 metavar="RETURNER_KWARGS", 2251 help="Set any returner options at the command line.", 2252 ) 2253 self.add_option( 2254 "-d", 2255 "--doc", 2256 "--documentation", 2257 dest="doc", 2258 default=False, 2259 action="store_true", 2260 help=( 2261 "Return the documentation for the specified module or for " 2262 "all modules if none are specified." 2263 ), 2264 ) 2265 self.add_option( 2266 "--args-separator", 2267 dest="args_separator", 2268 default=",", 2269 help=( 2270 "Set the special argument used as a delimiter between " 2271 "command arguments of compound commands. This is useful " 2272 "when one wants to pass commas as arguments to " 2273 "some of the commands in a compound command." 2274 ), 2275 ) 2276 self.add_option( 2277 "--summary", 2278 dest="cli_summary", 2279 default=False, 2280 action="store_true", 2281 help="Display summary information about a salt command.", 2282 ) 2283 self.add_option( 2284 "--metadata", 2285 default="", 2286 metavar="METADATA", 2287 help="Pass metadata into Salt, used to search jobs.", 2288 ) 2289 self.add_option( 2290 "--output-diff", 2291 dest="state_output_diff", 2292 action="store_true", 2293 default=False, 2294 help="Report only those states that have changed.", 2295 ) 2296 self.add_option( 2297 "--config-dump", 2298 dest="config_dump", 2299 action="store_true", 2300 default=False, 2301 help="Dump the master configuration values", 2302 ) 2303 self.add_option( 2304 "--preview-target", 2305 dest="preview_target", 2306 action="store_true", 2307 default=False, 2308 help=( 2309 "Show the minions expected to match a target. Does not issue any" 2310 " command." 2311 ), 2312 ) 2313 2314 def _mixin_after_parsed(self): 2315 if ( 2316 len(self.args) <= 1 2317 and not self.options.doc 2318 and not self.options.preview_target 2319 ): 2320 try: 2321 self.print_help() 2322 except Exception: # pylint: disable=broad-except 2323 # We get an argument that Python's optparser just can't deal 2324 # with. Perhaps stdout was redirected, or a file glob was 2325 # passed in. Regardless, we're in an unknown state here. 2326 sys.stdout.write( 2327 "Invalid options passed. Please try -h for help." 2328 ) # Try to warn if we can. 2329 sys.exit(salt.defaults.exitcodes.EX_GENERIC) 2330 2331 # Dump the master configuration file, exit normally at the end. 2332 if self.options.config_dump: 2333 cfg = config.master_config(self.get_config_file_path()) 2334 sys.stdout.write(salt.utils.yaml.safe_dump(cfg, default_flow_style=False)) 2335 sys.exit(salt.defaults.exitcodes.EX_OK) 2336 2337 if self.options.preview_target: 2338 # Insert dummy arg which won't be used 2339 self.args.append("not_a_valid_command") 2340 2341 if self.options.doc: 2342 # Include the target 2343 if not self.args: 2344 self.args.insert(0, "*") 2345 if len(self.args) < 2: 2346 # Include the function 2347 self.args.insert(1, "sys.doc") 2348 if self.args[1] != "sys.doc": 2349 self.args.insert(1, "sys.doc") 2350 if len(self.args) > 3: 2351 self.error("You can only get documentation for one method at one time.") 2352 2353 if self.options.list: 2354 try: 2355 if "," in self.args[0]: 2356 self.config["tgt"] = self.args[0].replace(" ", "").split(",") 2357 else: 2358 self.config["tgt"] = self.args[0].split() 2359 except IndexError: 2360 self.exit(42, "\nCannot execute command without defining a target.\n\n") 2361 else: 2362 try: 2363 self.config["tgt"] = self.args[0] 2364 except IndexError: 2365 self.exit(42, "\nCannot execute command without defining a target.\n\n") 2366 # Detect compound command and set up the data for it 2367 if self.args: 2368 try: 2369 if "," in self.args[1]: 2370 self.config["fun"] = self.args[1].split(",") 2371 self.config["arg"] = [[]] 2372 cmd_index = 0 2373 if ( 2374 self.args[2:].count(self.options.args_separator) 2375 == len(self.config["fun"]) - 1 2376 ): 2377 # new style parsing: standalone argument separator 2378 for arg in self.args[2:]: 2379 if arg == self.options.args_separator: 2380 cmd_index += 1 2381 self.config["arg"].append([]) 2382 else: 2383 self.config["arg"][cmd_index].append(arg) 2384 else: 2385 # old style parsing: argument separator can be inside args 2386 for arg in self.args[2:]: 2387 if self.options.args_separator in arg: 2388 sub_args = arg.split(self.options.args_separator) 2389 for sub_arg_index, sub_arg in enumerate(sub_args): 2390 if sub_arg: 2391 self.config["arg"][cmd_index].append(sub_arg) 2392 if sub_arg_index != len(sub_args) - 1: 2393 cmd_index += 1 2394 self.config["arg"].append([]) 2395 else: 2396 self.config["arg"][cmd_index].append(arg) 2397 if len(self.config["fun"]) > len(self.config["arg"]): 2398 self.exit( 2399 42, 2400 "Cannot execute compound command without " 2401 "defining all arguments.\n", 2402 ) 2403 elif len(self.config["fun"]) < len(self.config["arg"]): 2404 self.exit( 2405 42, 2406 "Cannot execute compound command with more " 2407 "arguments than commands.\n", 2408 ) 2409 # parse the args and kwargs before sending to the publish 2410 # interface 2411 for i in range(len(self.config["arg"])): 2412 self.config["arg"][i] = salt.utils.args.parse_input( 2413 self.config["arg"][i], no_parse=self.options.no_parse 2414 ) 2415 else: 2416 self.config["fun"] = self.args[1] 2417 self.config["arg"] = self.args[2:] 2418 # parse the args and kwargs before sending to the publish 2419 # interface 2420 self.config["arg"] = salt.utils.args.parse_input( 2421 self.config["arg"], no_parse=self.options.no_parse 2422 ) 2423 except IndexError: 2424 self.exit(42, "\nIncomplete options passed.\n\n") 2425 2426 def setup_config(self): 2427 opts = config.client_config(self.get_config_file_path()) 2428 salt.features.setup_features(opts) 2429 return opts 2430 2431 2432class SaltCPOptionParser( 2433 OptionParser, 2434 OutputOptionsMixIn, 2435 ConfigDirMixIn, 2436 MergeConfigMixIn, 2437 TimeoutMixIn, 2438 TargetOptionsMixIn, 2439 LogLevelMixIn, 2440 HardCrashMixin, 2441 SaltfileMixIn, 2442 metaclass=OptionParserMeta, 2443): 2444 description = ( 2445 "salt-cp is NOT intended to broadcast large files, it is intended to handle" 2446 " text\nfiles. salt-cp can be used to distribute configuration files." 2447 ) 2448 2449 usage = "%prog [options] '<target>' SOURCE DEST" 2450 2451 default_timeout = 5 2452 2453 # ConfigDirMixIn config filename attribute 2454 _config_filename_ = "master" 2455 2456 # LogLevelMixIn attributes 2457 _default_logging_level_ = config.DEFAULT_MASTER_OPTS["log_level"] 2458 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS["log_file"] 2459 2460 def _mixin_setup(self): 2461 file_opts_group = optparse.OptionGroup(self, "File Options") 2462 file_opts_group.add_option( 2463 "-C", 2464 "--chunked", 2465 default=False, 2466 dest="chunked", 2467 action="store_true", 2468 help=( 2469 "Use chunked files transfer. Supports big files, recursive " 2470 "lookup and directories creation." 2471 ), 2472 ) 2473 file_opts_group.add_option( 2474 "-n", 2475 "--no-compression", 2476 default=True, 2477 dest="gzip", 2478 action="store_false", 2479 help="Disable gzip compression.", 2480 ) 2481 self.add_option_group(file_opts_group) 2482 2483 def _mixin_after_parsed(self): 2484 # salt-cp needs arguments 2485 if len(self.args) <= 1: 2486 self.print_help() 2487 self.error("Insufficient arguments") 2488 2489 if self.options.list: 2490 if "," in self.args[0]: 2491 self.config["tgt"] = self.args[0].split(",") 2492 else: 2493 self.config["tgt"] = self.args[0].split() 2494 else: 2495 self.config["tgt"] = self.args[0] 2496 self.config["src"] = [os.path.realpath(x) for x in self.args[1:-1]] 2497 self.config["dest"] = self.args[-1] 2498 2499 def setup_config(self): 2500 opts = config.master_config(self.get_config_file_path()) 2501 salt.features.setup_features(opts) 2502 return opts 2503 2504 2505class SaltKeyOptionParser( 2506 OptionParser, 2507 ConfigDirMixIn, 2508 MergeConfigMixIn, 2509 LogLevelMixIn, 2510 OutputOptionsMixIn, 2511 RunUserMixin, 2512 HardCrashMixin, 2513 SaltfileMixIn, 2514 EAuthMixIn, 2515 metaclass=OptionParserMeta, 2516): 2517 2518 description = "salt-key is used to manage Salt authentication keys" 2519 2520 # ConfigDirMixIn config filename attribute 2521 _config_filename_ = "master" 2522 2523 # LogLevelMixIn attributes 2524 _skip_console_logging_config_ = True 2525 _logfile_config_setting_name_ = "key_logfile" 2526 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS[ 2527 _logfile_config_setting_name_ 2528 ] 2529 2530 def _mixin_setup(self): 2531 actions_group = optparse.OptionGroup(self, "Actions") 2532 actions_group.set_conflict_handler("resolve") 2533 actions_group.add_option( 2534 "-l", 2535 "--list", 2536 default="", 2537 metavar="ARG", 2538 help=( 2539 "List the public keys. The args " 2540 "'pre', 'un', and 'unaccepted' will list " 2541 "unaccepted/unsigned keys. " 2542 "'acc' or 'accepted' will list accepted/signed keys. " 2543 "'rej' or 'rejected' will list rejected keys. " 2544 "'den' or 'denied' will list denied keys. " 2545 "Finally, 'all' will list all keys." 2546 ), 2547 ) 2548 2549 actions_group.add_option( 2550 "-L", 2551 "--list-all", 2552 default=False, 2553 action="store_true", 2554 help='List all public keys. Deprecated: use "--list all".', 2555 ) 2556 2557 actions_group.add_option( 2558 "-a", 2559 "--accept", 2560 default="", 2561 help=( 2562 "Accept the specified public key (use --include-rejected and " 2563 "--include-denied to match rejected and denied keys in " 2564 "addition to pending keys). Globs are supported." 2565 ), 2566 ) 2567 2568 actions_group.add_option( 2569 "-A", 2570 "--accept-all", 2571 default=False, 2572 action="store_true", 2573 help="Accept all pending keys.", 2574 ) 2575 2576 actions_group.add_option( 2577 "-r", 2578 "--reject", 2579 default="", 2580 help=( 2581 "Reject the specified public key. Use --include-accepted and " 2582 "--include-denied to match accepted and denied keys in " 2583 "addition to pending keys. Globs are supported." 2584 ), 2585 ) 2586 2587 actions_group.add_option( 2588 "-R", 2589 "--reject-all", 2590 default=False, 2591 action="store_true", 2592 help="Reject all pending keys.", 2593 ) 2594 2595 actions_group.add_option( 2596 "--include-all", 2597 default=False, 2598 action="store_true", 2599 help=( 2600 "Include rejected/accepted keys when accepting/rejecting. " 2601 'Deprecated: use "--include-rejected" and "--include-accepted".' 2602 ), 2603 ) 2604 2605 actions_group.add_option( 2606 "--include-accepted", 2607 default=False, 2608 action="store_true", 2609 help="Include accepted keys when rejecting.", 2610 ) 2611 2612 actions_group.add_option( 2613 "--include-rejected", 2614 default=False, 2615 action="store_true", 2616 help="Include rejected keys when accepting.", 2617 ) 2618 2619 actions_group.add_option( 2620 "--include-denied", 2621 default=False, 2622 action="store_true", 2623 help="Include denied keys when accepting/rejecting.", 2624 ) 2625 2626 actions_group.add_option( 2627 "-p", "--print", default="", help="Print the specified public key." 2628 ) 2629 2630 actions_group.add_option( 2631 "-P", 2632 "--print-all", 2633 default=False, 2634 action="store_true", 2635 help="Print all public keys.", 2636 ) 2637 2638 actions_group.add_option( 2639 "-d", 2640 "--delete", 2641 default="", 2642 help="Delete the specified key. Globs are supported.", 2643 ) 2644 2645 actions_group.add_option( 2646 "-D", 2647 "--delete-all", 2648 default=False, 2649 action="store_true", 2650 help="Delete all keys.", 2651 ) 2652 2653 actions_group.add_option( 2654 "-f", "--finger", default="", help="Print the specified key's fingerprint." 2655 ) 2656 2657 actions_group.add_option( 2658 "-F", 2659 "--finger-all", 2660 default=False, 2661 action="store_true", 2662 help="Print all keys' fingerprints.", 2663 ) 2664 self.add_option_group(actions_group) 2665 2666 self.add_option( 2667 "-q", "--quiet", default=False, action="store_true", help="Suppress output." 2668 ) 2669 2670 self.add_option( 2671 "-y", 2672 "--yes", 2673 default=False, 2674 action="store_true", 2675 help='Answer "Yes" to all questions presented. Default: %default.', 2676 ) 2677 2678 self.add_option( 2679 "--rotate-aes-key", 2680 default=True, 2681 help=( 2682 "Setting this to False prevents the master from refreshing " 2683 "the key session when keys are deleted or rejected, this " 2684 "lowers the security of the key deletion/rejection operation. " 2685 "Default: %default." 2686 ), 2687 ) 2688 2689 self.add_option( 2690 "--preserve-minions", 2691 default=False, 2692 help=( 2693 "Setting this to True prevents the master from deleting " 2694 "the minion cache when keys are deleted, this may have " 2695 "security implications if compromised minions auth with " 2696 "a previous deleted minion ID. " 2697 "Default: %default." 2698 ), 2699 ) 2700 2701 key_options_group = optparse.OptionGroup(self, "Key Generation Options") 2702 self.add_option_group(key_options_group) 2703 key_options_group.add_option( 2704 "--gen-keys", 2705 default="", 2706 help="Set a name to generate a keypair for use with salt.", 2707 ) 2708 2709 key_options_group.add_option( 2710 "--gen-keys-dir", 2711 default=".", 2712 help=( 2713 "Set the directory to save the generated keypair, only " 2714 "works with \"gen_keys_dir\" option. Default: '%default'." 2715 ), 2716 ) 2717 2718 key_options_group.add_option( 2719 "--keysize", 2720 default=2048, 2721 type=int, 2722 help=( 2723 "Set the keysize for the generated key, only works with " 2724 'the "--gen-keys" option, the key size must be 2048 or ' 2725 "higher, otherwise it will be rounded up to 2048. " 2726 "Default: %default." 2727 ), 2728 ) 2729 2730 key_options_group.add_option( 2731 "--gen-signature", 2732 default=False, 2733 action="store_true", 2734 help=( 2735 "Create a signature file of the masters public-key named " 2736 "master_pubkey_signature. The signature can be send to a " 2737 "minion in the masters auth-reply and enables the minion " 2738 "to verify the masters public-key cryptographically. " 2739 "This requires a new signing-key-pair which can be auto-created " 2740 "with the --auto-create parameter." 2741 ), 2742 ) 2743 2744 key_options_group.add_option( 2745 "--priv", 2746 default="", 2747 type=str, 2748 help="The private-key file to create a signature with.", 2749 ) 2750 2751 key_options_group.add_option( 2752 "--signature-path", 2753 default="", 2754 type=str, 2755 help="The path where the signature file should be written.", 2756 ) 2757 2758 key_options_group.add_option( 2759 "--pub", 2760 default="", 2761 type=str, 2762 help="The public-key file to create a signature for.", 2763 ) 2764 2765 key_options_group.add_option( 2766 "--auto-create", 2767 default=False, 2768 action="store_true", 2769 help="Auto-create a signing key-pair if it does not yet exist.", 2770 ) 2771 2772 def process_config_dir(self): 2773 if self.options.gen_keys: 2774 # We're generating keys, override the default behavior of this 2775 # function if we don't have any access to the configuration 2776 # directory. 2777 if not os.access(self.options.config_dir, os.R_OK): 2778 if not os.path.isdir(self.options.gen_keys_dir): 2779 # This would be done at a latter stage, but we need it now 2780 # so no errors are thrown 2781 os.makedirs(self.options.gen_keys_dir) 2782 self.options.config_dir = self.options.gen_keys_dir 2783 super().process_config_dir() 2784 2785 # Don't change its mixin priority! 2786 process_config_dir._mixin_prio_ = ConfigDirMixIn._mixin_prio_ 2787 2788 def setup_config(self): 2789 keys_config = config.client_config(self.get_config_file_path()) 2790 if self.options.gen_keys: 2791 # Since we're generating the keys, some defaults can be assumed 2792 # or tweaked 2793 keys_config[self._logfile_config_setting_name_] = os.devnull 2794 keys_config["pki_dir"] = self.options.gen_keys_dir 2795 salt.features.setup_features(keys_config) 2796 return keys_config 2797 2798 def process_rotate_aes_key(self): 2799 if hasattr(self.options, "rotate_aes_key") and isinstance( 2800 self.options.rotate_aes_key, str 2801 ): 2802 if self.options.rotate_aes_key.lower() == "true": 2803 self.options.rotate_aes_key = True 2804 elif self.options.rotate_aes_key.lower() == "false": 2805 self.options.rotate_aes_key = False 2806 2807 def process_preserve_minions(self): 2808 if hasattr(self.options, "preserve_minions") and isinstance( 2809 self.options.preserve_minions, str 2810 ): 2811 if self.options.preserve_minions.lower() == "true": 2812 self.options.preserve_minions = True 2813 elif self.options.preserve_minions.lower() == "false": 2814 self.options.preserve_minions = False 2815 2816 def process_list(self): 2817 # Filter accepted list arguments as soon as possible 2818 if not self.options.list: 2819 return 2820 if not self.options.list.startswith(("acc", "pre", "un", "rej", "den", "all")): 2821 self.error( 2822 "'{}' is not a valid argument to '--list'".format(self.options.list) 2823 ) 2824 2825 def process_keysize(self): 2826 if self.options.keysize < 2048: 2827 self.error("The minimum value for keysize is 2048") 2828 elif self.options.keysize > 32768: 2829 self.error("The maximum value for keysize is 32768") 2830 2831 def process_gen_keys_dir(self): 2832 # Schedule __create_keys_dir() to run if there's a value for 2833 # --create-keys-dir 2834 self._mixin_after_parsed_funcs.append( 2835 self.__create_keys_dir 2836 ) # pylint: disable=no-member 2837 2838 def _mixin_after_parsed(self): 2839 # It was decided to always set this to info, since it really all is 2840 # info or error. 2841 self.config["loglevel"] = "info" 2842 2843 def __create_keys_dir(self): 2844 if not os.path.isdir(self.config["gen_keys_dir"]): 2845 os.makedirs(self.config["gen_keys_dir"]) 2846 2847 2848class SaltCallOptionParser( 2849 OptionParser, 2850 ProxyIdMixIn, 2851 ConfigDirMixIn, 2852 ExecutorsMixIn, 2853 MergeConfigMixIn, 2854 LogLevelMixIn, 2855 OutputOptionsMixIn, 2856 HardCrashMixin, 2857 SaltfileMixIn, 2858 ArgsStdinMixIn, 2859 ProfilingPMixIn, 2860 NoParseMixin, 2861 CacheDirMixIn, 2862 metaclass=OptionParserMeta, 2863): 2864 2865 description = ( 2866 "salt-call is used to execute module functions locally on a Salt Minion" 2867 ) 2868 2869 usage = "%prog [options] <function> [arguments]" 2870 2871 # ConfigDirMixIn config filename attribute 2872 _config_filename_ = "minion" 2873 2874 # LogLevelMixIn attributes 2875 _default_logging_level_ = config.DEFAULT_MINION_OPTS["log_level"] 2876 _default_logging_logfile_ = config.DEFAULT_MINION_OPTS["log_file"] 2877 2878 def _mixin_setup(self): 2879 self.add_option( 2880 "-g", 2881 "--grains", 2882 dest="grains_run", 2883 default=False, 2884 action="store_true", 2885 help="Return the information generated by the salt grains.", 2886 ) 2887 self.add_option( 2888 "-m", 2889 "--module-dirs", 2890 default=[], 2891 action="append", 2892 help=( 2893 "Specify an additional directory to pull modules from. " 2894 "Multiple directories can be provided by passing " 2895 "`-m/--module-dirs` multiple times." 2896 ), 2897 ) 2898 self.add_option( 2899 "-d", 2900 "--doc", 2901 "--documentation", 2902 dest="doc", 2903 default=False, 2904 action="store_true", 2905 help=( 2906 "Return the documentation for the specified module or for " 2907 "all modules if none are specified." 2908 ), 2909 ) 2910 self.add_option( 2911 "--master", 2912 default="", 2913 dest="master", 2914 help=( 2915 "Specify the master to use. The minion must be " 2916 "authenticated with the master. If this option is omitted, " 2917 "the master options from the minion config will be used. " 2918 "If multi masters are set up the first listed master that " 2919 "responds will be used." 2920 ), 2921 ) 2922 self.add_option( 2923 "--return", 2924 default="", 2925 metavar="RETURNER", 2926 help=( 2927 "Set salt-call to pass the return data to one or many " 2928 "returner interfaces." 2929 ), 2930 ) 2931 self.add_option( 2932 "--local", 2933 default=False, 2934 action="store_true", 2935 help="Run salt-call locally, as if there was no master running.", 2936 ) 2937 self.add_option( 2938 "--file-root", 2939 default=None, 2940 help="Set this directory as the base file root.", 2941 ) 2942 self.add_option( 2943 "--pillar-root", 2944 default=None, 2945 help="Set this directory as the base pillar root.", 2946 ) 2947 self.add_option( 2948 "--states-dir", 2949 default=None, 2950 help="Set this directory to search for additional states.", 2951 ) 2952 self.add_option( 2953 "--retcode-passthrough", 2954 default=False, 2955 action="store_true", 2956 help="Exit with the salt call retcode and not the salt binary retcode.", 2957 ) 2958 self.add_option( 2959 "--metadata", 2960 default=False, 2961 dest="print_metadata", 2962 action="store_true", 2963 help=( 2964 "Print out the execution metadata as well as the return. " 2965 "This will print out the outputter data, the return code, " 2966 "etc." 2967 ), 2968 ) 2969 self.add_option( 2970 "--set-metadata", 2971 dest="metadata", 2972 default=None, 2973 metavar="METADATA", 2974 help="Pass metadata into Salt, used to search jobs.", 2975 ) 2976 self.add_option( 2977 "--id", 2978 default="", 2979 dest="id", 2980 help=( 2981 "Specify the minion id to use. If this option is omitted, " 2982 "the id option from the minion config will be used." 2983 ), 2984 ) 2985 self.add_option( 2986 "--skip-grains", 2987 default=False, 2988 action="store_true", 2989 help="Do not load grains.", 2990 ) 2991 self.add_option( 2992 "--refresh-grains-cache", 2993 default=False, 2994 action="store_true", 2995 help="Force a refresh of the grains cache.", 2996 ) 2997 self.add_option( 2998 "-t", 2999 "--timeout", 3000 default=60, 3001 dest="auth_timeout", 3002 type=int, 3003 help=( 3004 "Change the timeout, if applicable, for the running " 3005 "command. Default: %default." 3006 ), 3007 ) 3008 self.add_option( 3009 "--output-diff", 3010 dest="state_output_diff", 3011 action="store_true", 3012 default=False, 3013 help="Report only those states that have changed.", 3014 ) 3015 3016 def _mixin_after_parsed(self): 3017 if not self.args and not self.options.grains_run and not self.options.doc: 3018 self.print_help() 3019 self.error("Requires function, --grains or --doc") 3020 3021 elif len(self.args) >= 1: 3022 if self.options.grains_run: 3023 self.error("-g/--grains does not accept any arguments") 3024 3025 if self.options.doc and len(self.args) > 1: 3026 self.error("You can only get documentation for one method at one time") 3027 3028 self.config["fun"] = self.args[0] 3029 self.config["arg"] = self.args[1:] 3030 3031 def setup_config(self): 3032 if self.options.proxyid: 3033 opts = config.proxy_config( 3034 self.get_config_file_path(configfile="proxy"), 3035 cache_minion_id=True, 3036 minion_id=self.options.proxyid, 3037 ) 3038 else: 3039 opts = config.minion_config( 3040 self.get_config_file_path(), cache_minion_id=True 3041 ) 3042 salt.features.setup_features(opts) 3043 return opts 3044 3045 def process_module_dirs(self): 3046 for module_dir in self.options.module_dirs: 3047 # Provide some backwards compatibility with previous comma 3048 # delimited format 3049 if "," in module_dir: 3050 self.config.setdefault("module_dirs", []).extend( 3051 os.path.abspath(x) for x in module_dir.split(",") 3052 ) 3053 continue 3054 self.config.setdefault("module_dirs", []).append( 3055 os.path.abspath(module_dir) 3056 ) 3057 3058 3059class SaltRunOptionParser( 3060 OptionParser, 3061 ConfigDirMixIn, 3062 MergeConfigMixIn, 3063 TimeoutMixIn, 3064 LogLevelMixIn, 3065 HardCrashMixin, 3066 SaltfileMixIn, 3067 OutputOptionsMixIn, 3068 ArgsStdinMixIn, 3069 ProfilingPMixIn, 3070 EAuthMixIn, 3071 NoParseMixin, 3072 JIDMixin, 3073 metaclass=OptionParserMeta, 3074): 3075 3076 default_timeout = 1 3077 3078 description = ( 3079 "salt-run is the frontend command for executing Salt Runners.\nSalt Runners are" 3080 " modules used to execute convenience functions on the Salt Master" 3081 ) 3082 3083 usage = "%prog [options] <function> [arguments]" 3084 3085 # ConfigDirMixIn config filename attribute 3086 _config_filename_ = "master" 3087 3088 # LogLevelMixIn attributes 3089 _default_logging_level_ = config.DEFAULT_MASTER_OPTS["log_level"] 3090 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS["log_file"] 3091 3092 def _mixin_setup(self): 3093 self.add_option( 3094 "-d", 3095 "--doc", 3096 "--documentation", 3097 dest="doc", 3098 default=False, 3099 action="store_true", 3100 help=( 3101 "Display documentation for runners, pass a runner or " 3102 "runner.function to see documentation on only that runner " 3103 "or function." 3104 ), 3105 ) 3106 self.add_option( 3107 "--async", 3108 default=False, 3109 action="store_true", 3110 help="Start the runner operation and immediately return control.", 3111 ) 3112 self.add_option( 3113 "--skip-grains", 3114 default=False, 3115 action="store_true", 3116 help="Do not load grains.", 3117 ) 3118 group = self.output_options_group = optparse.OptionGroup( 3119 self, "Output Options", "Configure your preferred output format." 3120 ) 3121 self.add_option_group(group) 3122 3123 group.add_option( 3124 "--quiet", 3125 default=False, 3126 action="store_true", 3127 help="Do not display the results of the run.", 3128 ) 3129 3130 def _mixin_after_parsed(self): 3131 if self.options.doc and len(self.args) > 1: 3132 self.error("You can only get documentation for one method at one time") 3133 3134 if self.args: 3135 self.config["fun"] = self.args[0] 3136 else: 3137 self.config["fun"] = "" 3138 if len(self.args) > 1: 3139 self.config["arg"] = self.args[1:] 3140 else: 3141 self.config["arg"] = [] 3142 3143 def setup_config(self): 3144 opts = config.client_config(self.get_config_file_path()) 3145 salt.features.setup_features(opts) 3146 return opts 3147 3148 3149class SaltSSHOptionParser( 3150 OptionParser, 3151 ConfigDirMixIn, 3152 MergeConfigMixIn, 3153 LogLevelMixIn, 3154 TargetOptionsMixIn, 3155 OutputOptionsMixIn, 3156 SaltfileMixIn, 3157 HardCrashMixin, 3158 NoParseMixin, 3159 JIDMixin, 3160 metaclass=OptionParserMeta, 3161): 3162 3163 usage = "%prog [options] '<target>' <function> [arguments]" 3164 3165 # ConfigDirMixIn config filename attribute 3166 _config_filename_ = "master" 3167 3168 # LogLevelMixIn attributes 3169 _logfile_config_setting_name_ = "ssh_log_file" 3170 _default_logging_level_ = config.DEFAULT_MASTER_OPTS["log_level"] 3171 _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS[ 3172 _logfile_config_setting_name_ 3173 ] 3174 3175 def _mixin_setup(self): 3176 self.add_option( 3177 "-r", 3178 "--raw", 3179 "--raw-shell", 3180 dest="raw_shell", 3181 default=False, 3182 action="store_true", 3183 help=( 3184 "Don't execute a salt routine on the targets, execute a " 3185 "raw shell command." 3186 ), 3187 ) 3188 self.add_option( 3189 "--roster", 3190 dest="roster", 3191 default="flat", 3192 help=( 3193 "Define which roster system to use, this defines if a " 3194 "database backend, scanner, or custom roster system is " 3195 "used. Default: 'flat'." 3196 ), 3197 ) 3198 self.add_option( 3199 "--roster-file", 3200 dest="roster_file", 3201 default="", 3202 help=( 3203 "Define an alternative location for the default roster " 3204 "file location. The default roster file is called roster " 3205 "and is found in the same directory as the master config " 3206 "file." 3207 ), 3208 ) 3209 self.add_option( 3210 "--refresh", 3211 "--refresh-cache", 3212 dest="refresh_cache", 3213 default=False, 3214 action="store_true", 3215 help=( 3216 "Force a refresh of the master side data cache of the " 3217 "target's data. This is needed if a target's grains have " 3218 "been changed and the auto refresh timeframe has not been " 3219 "reached." 3220 ), 3221 ) 3222 self.add_option( 3223 "--max-procs", 3224 dest="ssh_max_procs", 3225 default=25, 3226 type=int, 3227 help=( 3228 "Set the number of concurrent minions to communicate with. " 3229 "This value defines how many processes are opened up at a " 3230 "time to manage connections, the more running processes the " 3231 "faster communication should be. Default: %default." 3232 ), 3233 ) 3234 self.add_option( 3235 "--extra-filerefs", 3236 dest="extra_filerefs", 3237 default=None, 3238 help="Pass in extra files to include in the state tarball.", 3239 ) 3240 self.add_option( 3241 "--min-extra-modules", 3242 dest="min_extra_mods", 3243 default=None, 3244 help=( 3245 "One or comma-separated list of extra Python modules " 3246 "to be included into Minimal Salt." 3247 ), 3248 ) 3249 self.add_option( 3250 "--thin-extra-modules", 3251 dest="thin_extra_mods", 3252 default=None, 3253 help=( 3254 "One or comma-separated list of extra Python modules " 3255 "to be included into Thin Salt." 3256 ), 3257 ) 3258 self.add_option( 3259 "-v", 3260 "--verbose", 3261 default=False, 3262 action="store_true", 3263 help="Turn on command verbosity, display jid.", 3264 ) 3265 self.add_option( 3266 "-s", 3267 "--static", 3268 default=False, 3269 action="store_true", 3270 help="Return the data from minions as a group after they all return.", 3271 ) 3272 self.add_option( 3273 "-w", 3274 "--wipe", 3275 default=False, 3276 action="store_true", 3277 dest="ssh_wipe", 3278 help="Remove the deployment of the salt files when done executing.", 3279 ) 3280 self.add_option( 3281 "-W", 3282 "--rand-thin-dir", 3283 default=False, 3284 action="store_true", 3285 help=( 3286 "Select a random temp dir to deploy on the remote system. " 3287 "The dir will be cleaned after the execution." 3288 ), 3289 ) 3290 self.add_option( 3291 "-t", 3292 "--regen-thin", 3293 "--thin", 3294 dest="regen_thin", 3295 default=False, 3296 action="store_true", 3297 help=( 3298 "Trigger a thin tarball regeneration. This is needed if " 3299 "custom grains/modules/states have been added or updated." 3300 ), 3301 ) 3302 self.add_option( 3303 "--python2-bin", 3304 default="python2", 3305 help="Path to a python2 binary which has salt installed.", 3306 ) 3307 self.add_option( 3308 "--python3-bin", 3309 default="python3", 3310 help="Path to a python3 binary which has salt installed.", 3311 ) 3312 3313 self.add_option( 3314 "--pre-flight", 3315 default=False, 3316 action="store_true", 3317 dest="ssh_run_pre_flight", 3318 help="Run the defined ssh_pre_flight script in the roster", 3319 ) 3320 3321 ssh_group = optparse.OptionGroup( 3322 self, "SSH Options", "Parameters for the SSH client." 3323 ) 3324 ssh_group.add_option( 3325 "--remote-port-forwards", 3326 dest="ssh_remote_port_forwards", 3327 help=( 3328 "Setup remote port forwarding using the same syntax as with " 3329 "the -R parameter of ssh. A comma separated list of port " 3330 "forwarding definitions will be translated into multiple " 3331 "-R parameters." 3332 ), 3333 ) 3334 ssh_group.add_option( 3335 "--ssh-option", 3336 dest="ssh_options", 3337 action="append", 3338 help=( 3339 "Equivalent to the -o ssh command option. Passes options to " 3340 "the SSH client in the format used in the client configuration file. " 3341 "Can be used multiple times." 3342 ), 3343 ) 3344 self.add_option_group(ssh_group) 3345 3346 auth_group = optparse.OptionGroup( 3347 self, "Authentication Options", "Parameters affecting authentication." 3348 ) 3349 auth_group.add_option("--priv", dest="ssh_priv", help="Ssh private key file.") 3350 auth_group.add_option( 3351 "--priv-passwd", 3352 dest="ssh_priv_passwd", 3353 default="", 3354 help="Passphrase for ssh private key file.", 3355 ) 3356 auth_group.add_option( 3357 "-i", 3358 "--ignore-host-keys", 3359 dest="ignore_host_keys", 3360 default=False, 3361 action="store_true", 3362 help=( 3363 "By default ssh host keys are honored and connections will " 3364 "ask for approval. Use this option to disable " 3365 "StrictHostKeyChecking." 3366 ), 3367 ) 3368 auth_group.add_option( 3369 "--no-host-keys", 3370 dest="no_host_keys", 3371 default=False, 3372 action="store_true", 3373 help="Removes all host key checking functionality from SSH session.", 3374 ) 3375 auth_group.add_option( 3376 "--user", 3377 dest="ssh_user", 3378 default="root", 3379 help="Set the default user to attempt to use when authenticating.", 3380 ) 3381 auth_group.add_option( 3382 "--passwd", 3383 dest="ssh_passwd", 3384 default="", 3385 help="Set the default password to attempt to use when authenticating.", 3386 ) 3387 auth_group.add_option( 3388 "--askpass", 3389 dest="ssh_askpass", 3390 default=False, 3391 action="store_true", 3392 help=( 3393 "Interactively ask for the SSH password with no echo - avoids " 3394 "password in process args and stored in history." 3395 ), 3396 ) 3397 auth_group.add_option( 3398 "--key-deploy", 3399 dest="ssh_key_deploy", 3400 default=False, 3401 action="store_true", 3402 help=( 3403 "Set this flag to attempt to deploy the authorized ssh key " 3404 "with all minions. This combined with --passwd can make " 3405 "initial deployment of keys very fast and easy." 3406 ), 3407 ) 3408 auth_group.add_option( 3409 "--identities-only", 3410 dest="ssh_identities_only", 3411 default=False, 3412 action="store_true", 3413 help=( 3414 "Use the only authentication identity files configured in the " 3415 "ssh_config files. See IdentitiesOnly flag in man ssh_config." 3416 ), 3417 ) 3418 auth_group.add_option( 3419 "--sudo", 3420 dest="ssh_sudo", 3421 default=False, 3422 action="store_true", 3423 help="Run command via sudo.", 3424 ) 3425 auth_group.add_option( 3426 "--update-roster", 3427 dest="ssh_update_roster", 3428 default=False, 3429 action="store_true", 3430 help=( 3431 "If hostname is not found in the roster, store the information " 3432 "into the default roster file (flat)." 3433 ), 3434 ) 3435 self.add_option_group(auth_group) 3436 3437 scan_group = optparse.OptionGroup( 3438 self, "Scan Roster Options", "Parameters affecting scan roster." 3439 ) 3440 scan_group.add_option( 3441 "--scan-ports", 3442 default="22", 3443 dest="ssh_scan_ports", 3444 help="Comma-separated list of ports to scan in the scan roster.", 3445 ) 3446 scan_group.add_option( 3447 "--scan-timeout", 3448 default=0.01, 3449 dest="ssh_scan_timeout", 3450 help="Scanning socket timeout for the scan roster.", 3451 ) 3452 self.add_option_group(scan_group) 3453 3454 def _mixin_after_parsed(self): 3455 if not self.args: 3456 self.print_help() 3457 self.error("Insufficient arguments") 3458 3459 if self.options.list: 3460 if "," in self.args[0]: 3461 self.config["tgt"] = self.args[0].split(",") 3462 else: 3463 self.config["tgt"] = self.args[0].split() 3464 else: 3465 self.config["tgt"] = self.args[0] 3466 3467 self.config["argv"] = self.args[1:] 3468 if not self.config["argv"] or not self.config["tgt"]: 3469 self.print_help() 3470 self.error("Insufficient arguments") 3471 3472 # Add back the --no-parse options so that shimmed/wrapped commands 3473 # handle the arguments correctly. 3474 if self.options.no_parse: 3475 self.config["argv"].append("--no-parse=" + ",".join(self.options.no_parse)) 3476 3477 if self.options.ssh_askpass: 3478 self.options.ssh_passwd = getpass.getpass("Password: ") 3479 for group in self.option_groups: 3480 for option in group.option_list: 3481 if option.dest == "ssh_passwd": 3482 option.explicit = True 3483 break 3484 3485 def setup_config(self): 3486 opts = config.master_config(self.get_config_file_path()) 3487 salt.features.setup_features(opts) 3488 return opts 3489 3490 3491class SaltCloudParser( 3492 OptionParser, 3493 LogLevelMixIn, 3494 MergeConfigMixIn, 3495 OutputOptionsMixIn, 3496 ConfigDirMixIn, 3497 CloudQueriesMixIn, 3498 ExecutionOptionsMixIn, 3499 CloudProvidersListsMixIn, 3500 CloudCredentialsMixIn, 3501 HardCrashMixin, 3502 SaltfileMixIn, 3503 metaclass=OptionParserMeta, 3504): 3505 3506 description = ( 3507 "Salt Cloud is the system used to provision virtual machines on various" 3508 " public\nclouds via a cleanly controlled profile and mapping system" 3509 ) 3510 3511 usage = "%prog [options] <-m MAP | -p PROFILE> <NAME> [NAME2 ...]" 3512 3513 # ConfigDirMixIn attributes 3514 _config_filename_ = "cloud" 3515 3516 # LogLevelMixIn attributes 3517 _default_logging_level_ = config.DEFAULT_CLOUD_OPTS["log_level"] 3518 _default_logging_logfile_ = config.DEFAULT_CLOUD_OPTS["log_file"] 3519 3520 def print_versions_report( 3521 self, file=sys.stdout 3522 ): # pylint: disable=redefined-builtin 3523 print("\n".join(version.versions_report(include_salt_cloud=True)), file=file) 3524 self.exit(salt.defaults.exitcodes.EX_OK) 3525 3526 def parse_args(self, args=None, values=None): 3527 try: 3528 # Late import in order not to break setup 3529 from salt.cloud import libcloudfuncs 3530 3531 libcloudfuncs.check_libcloud_version() 3532 except ImportError as exc: 3533 self.error(exc) 3534 return super().parse_args(args, values) 3535 3536 def _mixin_after_parsed(self): 3537 if "DUMP_SALT_CLOUD_CONFIG" in os.environ: 3538 import pprint 3539 3540 print("Salt Cloud configuration dump (INCLUDES SENSIBLE DATA):") 3541 pprint.pprint(self.config) 3542 self.exit(salt.defaults.exitcodes.EX_OK) 3543 3544 if self.args: 3545 self.config["names"] = self.args 3546 3547 def setup_config(self): 3548 try: 3549 opts = config.cloud_config(self.get_config_file_path()) 3550 except salt.exceptions.SaltCloudConfigError as exc: 3551 self.error(exc) 3552 salt.features.setup_features(opts) 3553 return opts 3554 3555 3556class SPMParser( 3557 OptionParser, 3558 ConfigDirMixIn, 3559 LogLevelMixIn, 3560 MergeConfigMixIn, 3561 SaltfileMixIn, 3562 metaclass=OptionParserMeta, 3563): 3564 """ 3565 The CLI parser object used to fire up the Salt SPM system. 3566 """ 3567 3568 description = "SPM is used to manage 3rd party formulas and other Salt components" 3569 3570 usage = "%prog [options] <function> <argument>" 3571 3572 # ConfigDirMixIn config filename attribute 3573 _config_filename_ = "spm" 3574 # LogLevelMixIn attributes 3575 _logfile_config_setting_name_ = "spm_logfile" 3576 _default_logging_logfile_ = config.DEFAULT_SPM_OPTS[_logfile_config_setting_name_] 3577 3578 def _mixin_setup(self): 3579 self.add_option( 3580 "-y", 3581 "--assume-yes", 3582 default=False, 3583 action="store_true", 3584 help='Default "yes" in answer to all confirmation questions.', 3585 ) 3586 self.add_option( 3587 "-f", 3588 "--force", 3589 default=False, 3590 action="store_true", 3591 help='Default "yes" in answer to all confirmation questions.', 3592 ) 3593 self.add_option( 3594 "-v", 3595 "--verbose", 3596 default=False, 3597 action="store_true", 3598 help="Display more detailed information.", 3599 ) 3600 3601 def _mixin_after_parsed(self): 3602 # spm needs arguments 3603 if len(self.args) <= 1: 3604 if not self.args or self.args[0] not in ("update_repo",): 3605 self.print_help() 3606 self.error("Insufficient arguments") 3607 3608 def setup_config(self): 3609 opts = salt.config.spm_config(self.get_config_file_path()) 3610 salt.features.setup_features(opts) 3611 return opts 3612 3613 3614class SaltAPIParser( 3615 OptionParser, 3616 ConfigDirMixIn, 3617 LogLevelMixIn, 3618 DaemonMixIn, 3619 MergeConfigMixIn, 3620 metaclass=OptionParserMeta, 3621): 3622 """ 3623 The CLI parser object used to fire up the Salt API system. 3624 """ 3625 3626 description = ( 3627 "The Salt API system manages network API connectors for the Salt Master" 3628 ) 3629 3630 # ConfigDirMixIn config filename attribute 3631 _config_filename_ = "master" 3632 # LogLevelMixIn attributes 3633 _logfile_config_setting_name_ = "api_logfile" 3634 _default_logging_logfile_ = config.DEFAULT_API_OPTS[_logfile_config_setting_name_] 3635 3636 def setup_config(self): 3637 opts = salt.config.api_config( 3638 self.get_config_file_path() 3639 ) # pylint: disable=no-member 3640 salt.features.setup_features(opts) 3641 return opts 3642