1import os
2import sys
3
4import salt.defaults.exitcodes
5import salt.log
6import salt.utils.job
7import salt.utils.parsers
8import salt.utils.stringutils
9from salt.exceptions import (
10    AuthenticationError,
11    AuthorizationError,
12    EauthAuthenticationError,
13    LoaderError,
14    SaltClientError,
15    SaltInvocationError,
16    SaltSystemExit,
17)
18from salt.utils.args import yamlify_arg
19from salt.utils.verify import verify_log
20
21sys.modules["pkg_resources"] = None
22
23
24class SaltCMD(salt.utils.parsers.SaltCMDOptionParser):
25    """
26    The execution of a salt command happens here
27    """
28
29    def run(self):
30        """
31        Execute the salt command line
32        """
33        import salt.client
34
35        self.parse_args()
36
37        if self.config["log_level"] not in ("quiet",):
38            # Setup file logging!
39            self.setup_logfile_logger()
40            verify_log(self.config)
41
42        try:
43            # We don't need to bail on config file permission errors
44            # if the CLI process is run with the -a flag
45            skip_perm_errors = self.options.eauth != ""
46
47            self.local_client = salt.client.get_local_client(
48                self.get_config_file_path(),
49                skip_perm_errors=skip_perm_errors,
50                auto_reconnect=True,
51            )
52        except SaltClientError as exc:
53            self.exit(2, "{}\n".format(exc))
54            return
55
56        if self.options.batch or self.options.static:
57            # _run_batch() will handle all output and
58            # exit with the appropriate error condition
59            # Execution will not continue past this point
60            # in batch mode.
61            self._run_batch()
62            return
63
64        if self.options.preview_target:
65            minion_list = self._preview_target()
66            self._output_ret(minion_list, self.config.get("output", "nested"))
67            return
68
69        if self.options.timeout <= 0:
70            self.options.timeout = self.local_client.opts["timeout"]
71
72        kwargs = {
73            "tgt": self.config["tgt"],
74            "fun": self.config["fun"],
75            "arg": self.config["arg"],
76            "timeout": self.options.timeout,
77            "show_timeout": self.options.show_timeout,
78            "show_jid": self.options.show_jid,
79        }
80
81        if "token" in self.config:
82            import salt.utils.files
83
84            try:
85                with salt.utils.files.fopen(
86                    os.path.join(self.config["cachedir"], ".root_key"), "r"
87                ) as fp_:
88                    kwargs["key"] = fp_.readline()
89            except OSError:
90                kwargs["token"] = self.config["token"]
91
92        kwargs["delimiter"] = self.options.delimiter
93
94        if self.selected_target_option:
95            kwargs["tgt_type"] = self.selected_target_option
96        else:
97            kwargs["tgt_type"] = "glob"
98
99        # If batch_safe_limit is set, check minions matching target and
100        # potentially switch to batch execution
101        if self.options.batch_safe_limit > 1:
102            if len(self._preview_target()) >= self.options.batch_safe_limit:
103                salt.utils.stringutils.print_cli(
104                    "\nNOTICE: Too many minions targeted, switching to batch execution."
105                )
106                self.options.batch = self.options.batch_safe_size
107                try:
108                    self._run_batch()
109                finally:
110                    self.local_client.destroy()
111                return
112
113        if getattr(self.options, "return"):
114            kwargs["ret"] = getattr(self.options, "return")
115
116        if getattr(self.options, "return_config"):
117            kwargs["ret_config"] = getattr(self.options, "return_config")
118
119        if getattr(self.options, "return_kwargs"):
120            kwargs["ret_kwargs"] = yamlify_arg(getattr(self.options, "return_kwargs"))
121
122        if getattr(self.options, "module_executors"):
123            kwargs["module_executors"] = yamlify_arg(
124                getattr(self.options, "module_executors")
125            )
126
127        if getattr(self.options, "executor_opts"):
128            kwargs["executor_opts"] = yamlify_arg(
129                getattr(self.options, "executor_opts")
130            )
131
132        if getattr(self.options, "metadata"):
133            kwargs["metadata"] = yamlify_arg(getattr(self.options, "metadata"))
134
135        # If using eauth and a token hasn't already been loaded into
136        # kwargs, prompt the user to enter auth credentials
137        if "token" not in kwargs and "key" not in kwargs and self.options.eauth:
138            # This is expensive. Don't do it unless we need to.
139            import salt.auth
140
141            resolver = salt.auth.Resolver(self.config)
142            res = resolver.cli(self.options.eauth)
143            if self.options.mktoken and res:
144                tok = resolver.token_cli(self.options.eauth, res)
145                if tok:
146                    kwargs["token"] = tok.get("token", "")
147            if not res:
148                sys.stderr.write("ERROR: Authentication failed\n")
149                sys.exit(2)
150            kwargs.update(res)
151            kwargs["eauth"] = self.options.eauth
152
153        if self.config["async"]:
154            jid = self.local_client.cmd_async(**kwargs)
155            salt.utils.stringutils.print_cli(
156                "Executed command with job ID: {}".format(jid)
157            )
158            return
159
160        # local will be None when there was an error
161        if not self.local_client:
162            return
163
164        retcodes = []
165        errors = []
166
167        try:
168            if self.options.subset:
169                cmd_func = self.local_client.cmd_subset
170                kwargs["subset"] = self.options.subset
171                kwargs["cli"] = True
172            else:
173                cmd_func = self.local_client.cmd_cli
174
175            if self.options.progress:
176                kwargs["progress"] = True
177                self.config["progress"] = True
178                ret = {}
179                for progress in cmd_func(**kwargs):
180                    out = "progress"
181                    try:
182                        self._progress_ret(progress, out)
183                    except LoaderError as exc:
184                        raise SaltSystemExit(exc)
185                    if "return_count" not in progress:
186                        ret.update(progress)
187                self._progress_end(out)
188                self._print_returns_summary(ret)
189            elif self.config["fun"] == "sys.doc":
190                ret = {}
191                out = ""
192                for full_ret in self.local_client.cmd_cli(**kwargs):
193                    ret_, out, retcode = self._format_ret(full_ret)
194                    ret.update(ret_)
195                self._output_ret(ret, out, retcode=retcode)
196            else:
197                if self.options.verbose:
198                    kwargs["verbose"] = True
199                ret = {}
200                for full_ret in cmd_func(**kwargs):
201                    try:
202                        ret_, out, retcode = self._format_ret(full_ret)
203                        retcodes.append(retcode)
204                        self._output_ret(ret_, out, retcode=retcode)
205                        ret.update(full_ret)
206                    except KeyError:
207                        errors.append(full_ret)
208
209            # Returns summary
210            if self.config["cli_summary"] is True:
211                if self.config["fun"] != "sys.doc":
212                    if self.options.output is None:
213                        self._print_returns_summary(ret)
214                        self._print_errors_summary(errors)
215
216            # NOTE: Return code is set here based on if all minions
217            # returned 'ok' with a retcode of 0.
218            # This is the final point before the 'salt' cmd returns,
219            # which is why we set the retcode here.
220            if not all(
221                exit_code == salt.defaults.exitcodes.EX_OK for exit_code in retcodes
222            ):
223                sys.stderr.write("ERROR: Minions returned with non-zero exit code\n")
224                sys.exit(salt.defaults.exitcodes.EX_GENERIC)
225
226        except (
227            AuthenticationError,
228            AuthorizationError,
229            SaltInvocationError,
230            EauthAuthenticationError,
231            SaltClientError,
232        ) as exc:
233            ret = str(exc)
234            self._output_ret(ret, "", retcode=1)
235        finally:
236            self.local_client.destroy()
237
238    def _preview_target(self):
239        """
240        Return a list of minions from a given target
241        """
242        return self.local_client.gather_minions(
243            self.config["tgt"], self.selected_target_option or "glob"
244        )
245
246    def _run_batch(self):
247        import salt.cli.batch
248
249        eauth = {}
250        if "token" in self.config:
251            eauth["token"] = self.config["token"]
252
253        # If using eauth and a token hasn't already been loaded into
254        # kwargs, prompt the user to enter auth credentials
255        if "token" not in eauth and self.options.eauth:
256            # This is expensive. Don't do it unless we need to.
257            import salt.auth
258
259            resolver = salt.auth.Resolver(self.config)
260            res = resolver.cli(self.options.eauth)
261            if self.options.mktoken and res:
262                tok = resolver.token_cli(self.options.eauth, res)
263                if tok:
264                    eauth["token"] = tok.get("token", "")
265            if not res:
266                sys.stderr.write("ERROR: Authentication failed\n")
267                sys.exit(2)
268            eauth.update(res)
269            eauth["eauth"] = self.options.eauth
270
271        if self.options.static:
272
273            if not self.options.batch:
274                self.config["batch"] = "100%"
275
276            try:
277                batch = salt.cli.batch.Batch(self.config, eauth=eauth, quiet=True)
278            except SaltClientError:
279                sys.exit(2)
280
281            ret = {}
282
283            for res in batch.run():
284                ret.update(res)
285
286            self._output_ret(ret, "")
287
288        else:
289            try:
290                self.config["batch"] = self.options.batch
291                batch = salt.cli.batch.Batch(
292                    self.config, eauth=eauth, _parser=self.options
293                )
294            except SaltClientError:
295                # We will print errors to the console further down the stack
296                sys.exit(1)
297            # Printing the output is already taken care of in run() itself
298            retcode = 0
299            for res in batch.run():
300                for ret in res.values():
301                    job_retcode = salt.utils.job.get_retcode(ret)
302                    if job_retcode > retcode:
303                        # Exit with the highest retcode we find
304                        retcode = job_retcode
305            sys.exit(retcode)
306
307    def _print_errors_summary(self, errors):
308        if errors:
309            salt.utils.stringutils.print_cli("\n")
310            salt.utils.stringutils.print_cli("---------------------------")
311            salt.utils.stringutils.print_cli("Errors")
312            salt.utils.stringutils.print_cli("---------------------------")
313            for error in errors:
314                salt.utils.stringutils.print_cli(self._format_error(error))
315
316    def _print_returns_summary(self, ret):
317        """
318        Display returns summary
319        """
320        return_counter = 0
321        not_return_counter = 0
322        not_return_minions = []
323        not_response_minions = []
324        not_connected_minions = []
325        failed_minions = []
326        for each_minion in ret:
327            minion_ret = ret[each_minion]
328            if isinstance(minion_ret, dict) and "ret" in minion_ret:
329                minion_ret = ret[each_minion].get("ret")
330            if isinstance(minion_ret, str) and minion_ret.startswith(
331                "Minion did not return"
332            ):
333                if "Not connected" in minion_ret:
334                    not_connected_minions.append(each_minion)
335                elif "No response" in minion_ret:
336                    not_response_minions.append(each_minion)
337                not_return_counter += 1
338                not_return_minions.append(each_minion)
339            else:
340                return_counter += 1
341                if self._get_retcode(ret[each_minion]):
342                    failed_minions.append(each_minion)
343        salt.utils.stringutils.print_cli("\n")
344        salt.utils.stringutils.print_cli("-------------------------------------------")
345        salt.utils.stringutils.print_cli("Summary")
346        salt.utils.stringutils.print_cli("-------------------------------------------")
347        salt.utils.stringutils.print_cli(
348            "# of minions targeted: {}".format(return_counter + not_return_counter)
349        )
350        salt.utils.stringutils.print_cli(
351            "# of minions returned: {}".format(return_counter)
352        )
353        salt.utils.stringutils.print_cli(
354            "# of minions that did not return: {}".format(not_return_counter)
355        )
356        salt.utils.stringutils.print_cli(
357            "# of minions with errors: {}".format(len(failed_minions))
358        )
359        if self.options.verbose:
360            if not_connected_minions:
361                salt.utils.stringutils.print_cli(
362                    "Minions not connected: {}".format(" ".join(not_connected_minions))
363                )
364            if not_response_minions:
365                salt.utils.stringutils.print_cli(
366                    "Minions not responding: {}".format(" ".join(not_response_minions))
367                )
368            if failed_minions:
369                salt.utils.stringutils.print_cli(
370                    "Minions with failures: {}".format(" ".join(failed_minions))
371                )
372        salt.utils.stringutils.print_cli("-------------------------------------------")
373
374    def _progress_end(self, out):
375        import salt.output
376
377        salt.output.progress_end(self.progress_bar)
378
379    def _progress_ret(self, progress, out):
380        """
381        Print progress events
382        """
383        import salt.output
384
385        # Get the progress bar
386        if not hasattr(self, "progress_bar"):
387            try:
388                self.progress_bar = salt.output.get_progress(self.config, out, progress)
389            except Exception:  # pylint: disable=broad-except
390                raise LoaderError(
391                    "\nWARNING: Install the `progressbar` python package. "
392                    "Requested job was still run but output cannot be displayed.\n"
393                )
394        salt.output.update_progress(self.config, progress, self.progress_bar, out)
395
396    def _output_ret(self, ret, out, retcode=0):
397        """
398        Print the output from a single return to the terminal
399        """
400        import salt.output
401
402        # Handle special case commands
403        if self.config["fun"] == "sys.doc" and not isinstance(ret, Exception):
404            self._print_docs(ret)
405        else:
406            # Determine the proper output method and run it
407            salt.output.display_output(ret, out=out, opts=self.config, _retcode=retcode)
408        if not ret:
409            sys.stderr.write("ERROR: No return received\n")
410            sys.exit(2)
411
412    def _format_ret(self, full_ret):
413        """
414        Take the full return data and format it to simple output
415        """
416        ret = {}
417        out = ""
418        retcode = 0
419        for key, data in full_ret.items():
420            ret[key] = data["ret"]
421            if "out" in data:
422                out = data["out"]
423            ret_retcode = self._get_retcode(data)
424            if ret_retcode > retcode:
425                retcode = ret_retcode
426        return ret, out, retcode
427
428    def _get_retcode(self, ret):
429        """
430        Determine a retcode for a given return
431        """
432        retcode = 0
433        # if there is a dict with retcode, use that
434        if isinstance(ret, dict) and ret.get("retcode", 0) != 0:
435            if isinstance(ret.get("retcode", 0), dict):
436                return max(ret.get("retcode", {0: 0}).values())
437            return ret["retcode"]
438        # if its a boolean, False means 1
439        elif isinstance(ret, bool) and not ret:
440            return 1
441        return retcode
442
443    def _format_error(self, minion_error):
444        for minion, error_doc in minion_error.items():
445            error = "Minion [{}] encountered exception '{}'".format(
446                minion, error_doc["message"]
447            )
448        return error
449
450    def _print_docs(self, ret):
451        """
452        Print out the docstrings for all of the functions on the minions
453        """
454        import salt.output
455
456        docs = {}
457        if not ret:
458            self.exit(2, "No minions found to gather docs from\n")
459        if isinstance(ret, str):
460            self.exit(2, "{}\n".format(ret))
461        for host in ret:
462            if isinstance(ret[host], str) and (
463                ret[host].startswith("Minion did not return")
464                or ret[host] == "VALUE_TRIMMED"
465            ):
466                continue
467            for fun in ret[host]:
468                if fun not in docs and ret[host][fun]:
469                    docs[fun] = ret[host][fun]
470        if self.options.output:
471            for fun in sorted(docs):
472                salt.output.display_output({fun: docs[fun]}, "nested", self.config)
473        else:
474            for fun in sorted(docs):
475                salt.utils.stringutils.print_cli("{}:".format(fun))
476                salt.utils.stringutils.print_cli(docs[fun])
477                salt.utils.stringutils.print_cli("")
478