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