scripts.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. # -*- coding: utf-8 -*-
  2. """
  3. This module contains the function calls to execute command line scripts
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import functools
  7. import logging
  8. import os
  9. import signal
  10. import sys
  11. import threading
  12. import time
  13. import traceback
  14. from random import randint
  15. import salt.defaults.exitcodes # pylint: disable=unused-import
  16. from salt.exceptions import SaltClientError, SaltReqTimeoutError, SaltSystemExit
  17. log = logging.getLogger(__name__)
  18. if sys.version_info < (3,):
  19. raise SystemExit(salt.defaults.exitcodes.EX_GENERIC)
  20. def _handle_interrupt(exc, original_exc, hardfail=False, trace=""):
  21. """
  22. if hardfailing:
  23. If we got the original stacktrace, log it
  24. If all cases, raise the original exception
  25. but this is logically part the initial
  26. stack.
  27. else just let salt exit gracefully
  28. """
  29. if hardfail:
  30. if trace:
  31. log.error(trace)
  32. raise original_exc
  33. else:
  34. raise exc
  35. def _handle_signals(client, signum, sigframe):
  36. try:
  37. # This raises AttributeError on Python 3.4 and 3.5 if there is no current exception.
  38. # Ref: https://bugs.python.org/issue23003
  39. trace = traceback.format_exc()
  40. except AttributeError:
  41. trace = ""
  42. try:
  43. hardcrash = client.options.hard_crash
  44. except (AttributeError, KeyError):
  45. hardcrash = False
  46. if signum == signal.SIGINT:
  47. exit_msg = "\nExiting gracefully on Ctrl-c"
  48. try:
  49. jid = client.local_client.pub_data["jid"]
  50. exit_msg += (
  51. "\n"
  52. "This job's jid is: {0}\n"
  53. "The minions may not have all finished running and any remaining "
  54. "minions will return upon completion. To look up the return data "
  55. "for this job later, run the following command:\n\n"
  56. "salt-run jobs.lookup_jid {0}".format(jid)
  57. )
  58. except (AttributeError, KeyError):
  59. pass
  60. else:
  61. exit_msg = None
  62. _handle_interrupt(
  63. SystemExit(exit_msg),
  64. Exception("\nExiting with hard crash on Ctrl-c"),
  65. hardcrash,
  66. trace=trace,
  67. )
  68. def _install_signal_handlers(client):
  69. # Install the SIGINT/SIGTERM handlers if not done so far
  70. if signal.getsignal(signal.SIGINT) is signal.SIG_DFL:
  71. # No custom signal handling was added, install our own
  72. signal.signal(signal.SIGINT, functools.partial(_handle_signals, client))
  73. if signal.getsignal(signal.SIGTERM) is signal.SIG_DFL:
  74. # No custom signal handling was added, install our own
  75. signal.signal(signal.SIGINT, functools.partial(_handle_signals, client))
  76. def salt_master():
  77. """
  78. Start the salt master.
  79. """
  80. import salt.cli.daemons
  81. # Fix for setuptools generated scripts, so that it will
  82. # work with multiprocessing fork emulation.
  83. # (see multiprocessing.forking.get_preparation_data())
  84. if __name__ != "__main__":
  85. sys.modules["__main__"] = sys.modules[__name__]
  86. master = salt.cli.daemons.Master()
  87. master.start()
  88. def minion_process():
  89. """
  90. Start a minion process
  91. """
  92. import salt.utils.platform
  93. import salt.utils.process
  94. import salt.cli.daemons
  95. # salt_minion spawns this function in a new process
  96. salt.utils.process.appendproctitle("KeepAlive")
  97. def handle_hup(manager, sig, frame):
  98. manager.minion.reload()
  99. lock = threading.RLock()
  100. def suicide_when_without_parent(parent_pid):
  101. """
  102. Have the minion suicide if the parent process is gone
  103. NOTE: small race issue where the parent PID could be replace
  104. with another process with same PID!
  105. """
  106. while lock.acquire(blocking=False):
  107. lock.release()
  108. time.sleep(5)
  109. try:
  110. # check pid alive (Unix only trick!)
  111. if os.getuid() == 0 and not salt.utils.platform.is_windows():
  112. os.kill(parent_pid, 0)
  113. except OSError as exc:
  114. # forcibly exit, regular sys.exit raises an exception-- which
  115. # isn't sufficient in a thread
  116. log.error("Minion process encountered exception: %s", exc)
  117. os._exit(salt.defaults.exitcodes.EX_GENERIC)
  118. try:
  119. if not salt.utils.platform.is_windows():
  120. thread = threading.Thread(
  121. target=suicide_when_without_parent, args=(os.getppid(),)
  122. )
  123. thread.start()
  124. minion = salt.cli.daemons.Minion()
  125. signal.signal(signal.SIGHUP, functools.partial(handle_hup, minion))
  126. minion.start()
  127. except (SaltClientError, SaltReqTimeoutError, SaltSystemExit) as exc:
  128. lock.acquire(blocking=True)
  129. log.warning(
  130. "Fatal functionality error caught by minion handler:\n", exc_info=True
  131. )
  132. log.warning("** Restarting minion **")
  133. delay = 60
  134. if minion is not None and hasattr(minion, "config"):
  135. delay = minion.config.get("random_reauth_delay", 60)
  136. delay = randint(1, delay)
  137. log.info("waiting random_reauth_delay %ss", delay)
  138. time.sleep(delay)
  139. sys.exit(salt.defaults.exitcodes.SALT_KEEPALIVE)
  140. finally:
  141. lock.acquire(blocking=True)
  142. def salt_minion():
  143. """
  144. Start the salt minion in a subprocess.
  145. Auto restart minion on error.
  146. """
  147. import signal
  148. import salt.utils.platform
  149. import salt.utils.process
  150. salt.utils.process.notify_systemd()
  151. import salt.cli.daemons
  152. import multiprocessing
  153. # Fix for setuptools generated scripts, so that it will
  154. # work with multiprocessing fork emulation.
  155. # (see multiprocessing.forking.get_preparation_data())
  156. if __name__ != "__main__":
  157. sys.modules["__main__"] = sys.modules[__name__]
  158. if "" in sys.path:
  159. sys.path.remove("")
  160. if salt.utils.platform.is_windows():
  161. minion = salt.cli.daemons.Minion()
  162. minion.start()
  163. return
  164. if "--disable-keepalive" in sys.argv:
  165. sys.argv.remove("--disable-keepalive")
  166. minion = salt.cli.daemons.Minion()
  167. minion.start()
  168. return
  169. def escalate_signal_to_process(
  170. pid, signum, sigframe
  171. ): # pylint: disable=unused-argument
  172. """
  173. Escalate the signal received to the multiprocessing process that
  174. is actually running the minion
  175. """
  176. # escalate signal
  177. os.kill(pid, signum)
  178. # keep one minion subprocess running
  179. prev_sigint_handler = signal.getsignal(signal.SIGINT)
  180. prev_sigterm_handler = signal.getsignal(signal.SIGTERM)
  181. while True:
  182. try:
  183. process = multiprocessing.Process(target=minion_process)
  184. process.start()
  185. signal.signal(
  186. signal.SIGTERM,
  187. functools.partial(escalate_signal_to_process, process.pid),
  188. )
  189. signal.signal(
  190. signal.SIGINT,
  191. functools.partial(escalate_signal_to_process, process.pid),
  192. )
  193. signal.signal(
  194. signal.SIGHUP,
  195. functools.partial(escalate_signal_to_process, process.pid),
  196. )
  197. except Exception: # pylint: disable=broad-except
  198. # if multiprocessing does not work
  199. minion = salt.cli.daemons.Minion()
  200. minion.start()
  201. break
  202. process.join()
  203. # Process exited or was terminated. Since we're going to try to restart
  204. # it, we MUST, reset signal handling to the previous handlers
  205. signal.signal(signal.SIGINT, prev_sigint_handler)
  206. signal.signal(signal.SIGTERM, prev_sigterm_handler)
  207. if not process.exitcode == salt.defaults.exitcodes.SALT_KEEPALIVE:
  208. sys.exit(process.exitcode)
  209. # ontop of the random_reauth_delay already preformed
  210. # delay extra to reduce flooding and free resources
  211. # NOTE: values are static but should be fine.
  212. time.sleep(2 + randint(1, 10))
  213. # need to reset logging because new minion objects
  214. # cause extra log handlers to accumulate
  215. rlogger = logging.getLogger()
  216. for handler in rlogger.handlers:
  217. rlogger.removeHandler(handler)
  218. logging.basicConfig()
  219. def proxy_minion_process(queue):
  220. """
  221. Start a proxy minion process
  222. """
  223. import salt.cli.daemons
  224. import salt.utils.platform
  225. # salt_minion spawns this function in a new process
  226. lock = threading.RLock()
  227. def suicide_when_without_parent(parent_pid):
  228. """
  229. Have the minion suicide if the parent process is gone
  230. NOTE: there is a small race issue where the parent PID could be replace
  231. with another process with the same PID!
  232. """
  233. while lock.acquire(blocking=False):
  234. lock.release()
  235. time.sleep(5)
  236. try:
  237. # check pid alive (Unix only trick!)
  238. os.kill(parent_pid, 0)
  239. except OSError:
  240. # forcibly exit, regular sys.exit raises an exception-- which
  241. # isn't sufficient in a thread
  242. os._exit(999)
  243. try:
  244. if not salt.utils.platform.is_windows():
  245. thread = threading.Thread(
  246. target=suicide_when_without_parent, args=(os.getppid(),)
  247. )
  248. thread.start()
  249. restart = False
  250. proxyminion = None
  251. status = salt.defaults.exitcodes.EX_OK
  252. proxyminion = salt.cli.daemons.ProxyMinion()
  253. proxyminion.start()
  254. # pylint: disable=broad-except
  255. except (Exception, SaltClientError, SaltReqTimeoutError, SaltSystemExit,) as exc:
  256. # pylint: enable=broad-except
  257. log.error("Proxy Minion failed to start: ", exc_info=True)
  258. restart = True
  259. # status is superfluous since the process will be restarted
  260. status = salt.defaults.exitcodes.SALT_KEEPALIVE
  261. except SystemExit as exc:
  262. restart = False
  263. status = exc.code
  264. finally:
  265. lock.acquire(blocking=True)
  266. if restart is True:
  267. log.warning("** Restarting proxy minion **")
  268. delay = 60
  269. if proxyminion is not None:
  270. if hasattr(proxyminion, "config"):
  271. delay = proxyminion.config.get("random_reauth_delay", 60)
  272. random_delay = randint(1, delay)
  273. log.info("Sleeping random_reauth_delay of %s seconds", random_delay)
  274. # preform delay after minion resources have been cleaned
  275. queue.put(random_delay)
  276. else:
  277. queue.put(0)
  278. sys.exit(status)
  279. def salt_proxy():
  280. """
  281. Start a proxy minion.
  282. """
  283. import salt.cli.daemons
  284. import salt.utils.platform
  285. import multiprocessing
  286. if "" in sys.path:
  287. sys.path.remove("")
  288. if salt.utils.platform.is_windows():
  289. proxyminion = salt.cli.daemons.ProxyMinion()
  290. proxyminion.start()
  291. return
  292. if "--disable-keepalive" in sys.argv:
  293. sys.argv.remove("--disable-keepalive")
  294. proxyminion = salt.cli.daemons.ProxyMinion()
  295. proxyminion.start()
  296. return
  297. # keep one minion subprocess running
  298. while True:
  299. try:
  300. queue = multiprocessing.Queue()
  301. except Exception: # pylint: disable=broad-except
  302. # This breaks in containers
  303. proxyminion = salt.cli.daemons.ProxyMinion()
  304. proxyminion.start()
  305. return
  306. process = multiprocessing.Process(target=proxy_minion_process, args=(queue,))
  307. process.start()
  308. try:
  309. process.join()
  310. try:
  311. restart_delay = queue.get(block=False)
  312. except Exception: # pylint: disable=broad-except
  313. if process.exitcode == 0:
  314. # Minion process ended naturally, Ctrl+C or --version
  315. break
  316. restart_delay = 60
  317. if restart_delay == 0:
  318. # Minion process ended naturally, Ctrl+C, --version, etc.
  319. sys.exit(process.exitcode)
  320. # delay restart to reduce flooding and allow network resources to close
  321. time.sleep(restart_delay)
  322. except KeyboardInterrupt:
  323. break
  324. # need to reset logging because new minion objects
  325. # cause extra log handlers to accumulate
  326. rlogger = logging.getLogger()
  327. for handler in rlogger.handlers:
  328. rlogger.removeHandler(handler)
  329. logging.basicConfig()
  330. def salt_syndic():
  331. """
  332. Start the salt syndic.
  333. """
  334. import salt.utils.process
  335. salt.utils.process.notify_systemd()
  336. import salt.cli.daemons
  337. pid = os.getpid()
  338. try:
  339. syndic = salt.cli.daemons.Syndic()
  340. syndic.start()
  341. except KeyboardInterrupt:
  342. os.kill(pid, 15)
  343. def salt_key():
  344. """
  345. Manage the authentication keys with salt-key.
  346. """
  347. import salt.cli.key
  348. try:
  349. client = salt.cli.key.SaltKey()
  350. _install_signal_handlers(client)
  351. client.run()
  352. except Exception as err: # pylint: disable=broad-except
  353. sys.stderr.write("Error: {0}\n".format(err))
  354. def salt_cp():
  355. """
  356. Publish commands to the salt system from the command line on the
  357. master.
  358. """
  359. import salt.cli.cp
  360. client = salt.cli.cp.SaltCPCli()
  361. _install_signal_handlers(client)
  362. client.run()
  363. def salt_call():
  364. """
  365. Directly call a salt command in the modules, does not require a running
  366. salt minion to run.
  367. """
  368. import salt.cli.call
  369. if "" in sys.path:
  370. sys.path.remove("")
  371. client = salt.cli.call.SaltCall()
  372. _install_signal_handlers(client)
  373. client.run()
  374. def salt_run():
  375. """
  376. Execute a salt convenience routine.
  377. """
  378. import salt.cli.run
  379. if "" in sys.path:
  380. sys.path.remove("")
  381. client = salt.cli.run.SaltRun()
  382. _install_signal_handlers(client)
  383. client.run()
  384. def salt_ssh():
  385. """
  386. Execute the salt-ssh system
  387. """
  388. import salt.cli.ssh
  389. if "" in sys.path:
  390. sys.path.remove("")
  391. try:
  392. client = salt.cli.ssh.SaltSSH()
  393. _install_signal_handlers(client)
  394. client.run()
  395. except SaltClientError as err:
  396. trace = traceback.format_exc()
  397. try:
  398. hardcrash = client.options.hard_crash
  399. except (AttributeError, KeyError):
  400. hardcrash = False
  401. _handle_interrupt(SystemExit(err), err, hardcrash, trace=trace)
  402. def salt_cloud():
  403. """
  404. The main function for salt-cloud
  405. """
  406. # Define 'salt' global so we may use it after ImportError. Otherwise,
  407. # UnboundLocalError will be raised.
  408. global salt # pylint: disable=W0602
  409. try:
  410. # Late-imports for CLI performance
  411. import salt.cloud
  412. import salt.cloud.cli
  413. except ImportError as e:
  414. # No salt cloud on Windows
  415. log.error("Error importing salt cloud: %s", e)
  416. print("salt-cloud is not available in this system")
  417. sys.exit(salt.defaults.exitcodes.EX_UNAVAILABLE)
  418. if "" in sys.path:
  419. sys.path.remove("")
  420. client = salt.cloud.cli.SaltCloud()
  421. _install_signal_handlers(client)
  422. client.run()
  423. def salt_api():
  424. """
  425. The main function for salt-api
  426. """
  427. import salt.utils.process
  428. salt.utils.process.notify_systemd()
  429. import salt.cli.api
  430. sapi = salt.cli.api.SaltAPI() # pylint: disable=E1120
  431. sapi.start()
  432. def salt_main():
  433. """
  434. Publish commands to the salt system from the command line on the
  435. master.
  436. """
  437. import salt.cli.salt
  438. if "" in sys.path:
  439. sys.path.remove("")
  440. client = salt.cli.salt.SaltCMD()
  441. _install_signal_handlers(client)
  442. client.run()
  443. def salt_spm():
  444. """
  445. The main function for spm, the Salt Package Manager
  446. .. versionadded:: 2015.8.0
  447. """
  448. import salt.cli.spm
  449. spm = salt.cli.spm.SPM() # pylint: disable=E1120
  450. spm.run()
  451. def salt_extend(extension, name, description, salt_dir, merge):
  452. """
  453. Quickstart for developing on the saltstack installation
  454. .. versionadded:: 2016.11.0
  455. """
  456. import salt.utils.extend
  457. salt.utils.extend.run(
  458. extension=extension,
  459. name=name,
  460. description=description,
  461. salt_dir=salt_dir,
  462. merge=merge,
  463. )
  464. def salt_unity():
  465. """
  466. Change the args and redirect to another salt script
  467. """
  468. avail = []
  469. for fun in dir(sys.modules[__name__]):
  470. if fun.startswith("salt"):
  471. avail.append(fun[5:])
  472. if len(sys.argv) < 2:
  473. msg = "Must pass in a salt command, available commands are:"
  474. for cmd in avail:
  475. msg += "\n{0}".format(cmd)
  476. print(msg)
  477. sys.exit(1)
  478. cmd = sys.argv[1]
  479. if cmd not in avail:
  480. # Fall back to the salt command
  481. sys.argv[0] = "salt"
  482. s_fun = salt_main
  483. else:
  484. sys.argv[0] = "salt-{0}".format(cmd)
  485. sys.argv.pop(1)
  486. s_fun = getattr(sys.modules[__name__], "salt_{0}".format(cmd))
  487. s_fun()