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