test_ssh.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: :email:`Daniel Wallace <dwallace@saltstack.com`
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import os
  7. import re
  8. import shutil
  9. import tempfile
  10. import salt.config
  11. import salt.roster
  12. import salt.utils.files
  13. import salt.utils.path
  14. import salt.utils.thin
  15. import salt.utils.yaml
  16. from salt.client import ssh
  17. from tests.support.case import ShellCase
  18. from tests.support.helpers import slowTest
  19. from tests.support.mock import MagicMock, call, patch
  20. from tests.support.runtests import RUNTIME_VARS
  21. from tests.support.unit import TestCase, skipIf
  22. ROSTER = """
  23. localhost:
  24. host: 127.0.0.1
  25. port: 2827
  26. self:
  27. host: 0.0.0.0
  28. port: 42
  29. """
  30. @skipIf(not salt.utils.path.which("ssh"), "No ssh binary found in path")
  31. class SSHPasswordTests(ShellCase):
  32. @slowTest
  33. def test_password_failure(self):
  34. """
  35. Check password failures when trying to deploy keys
  36. """
  37. opts = salt.config.client_config(self.get_config_file_path("master"))
  38. opts["list_hosts"] = False
  39. opts["argv"] = ["test.ping"]
  40. opts["selected_target_option"] = "glob"
  41. opts["tgt"] = "localhost"
  42. opts["arg"] = []
  43. roster = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "roster")
  44. handle_ssh_ret = [
  45. {
  46. "localhost": {
  47. "retcode": 255,
  48. "stderr": "Permission denied (publickey).\r\n",
  49. "stdout": "",
  50. }
  51. },
  52. ]
  53. expected = {"localhost": "Permission denied (publickey)"}
  54. display_output = MagicMock()
  55. with patch(
  56. "salt.roster.get_roster_file", MagicMock(return_value=roster)
  57. ), patch(
  58. "salt.client.ssh.SSH.handle_ssh", MagicMock(return_value=handle_ssh_ret)
  59. ), patch(
  60. "salt.client.ssh.SSH.key_deploy", MagicMock(return_value=expected)
  61. ), patch(
  62. "salt.output.display_output", display_output
  63. ):
  64. client = ssh.SSH(opts)
  65. ret = next(client.run_iter())
  66. with self.assertRaises(SystemExit):
  67. client.run()
  68. display_output.assert_called_once_with(expected, "nested", opts)
  69. self.assertIs(ret, handle_ssh_ret[0])
  70. class SSHRosterDefaults(TestCase):
  71. def test_roster_defaults_flat(self):
  72. """
  73. Test Roster Defaults on the flat roster
  74. """
  75. tempdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  76. expected = {
  77. "self": {"host": "0.0.0.0", "user": "daniel", "port": 42},
  78. "localhost": {"host": "127.0.0.1", "user": "daniel", "port": 2827},
  79. }
  80. try:
  81. root_dir = os.path.join(tempdir, "foo", "bar")
  82. os.makedirs(root_dir)
  83. fpath = os.path.join(root_dir, "config")
  84. with salt.utils.files.fopen(fpath, "w") as fp_:
  85. fp_.write(
  86. """
  87. roster_defaults:
  88. user: daniel
  89. """
  90. )
  91. opts = salt.config.master_config(fpath)
  92. with patch("salt.roster.get_roster_file", MagicMock(return_value=ROSTER)):
  93. with patch(
  94. "salt.template.compile_template",
  95. MagicMock(return_value=salt.utils.yaml.safe_load(ROSTER)),
  96. ):
  97. roster = salt.roster.Roster(opts=opts)
  98. self.assertEqual(roster.targets("*", "glob"), expected)
  99. finally:
  100. if os.path.isdir(tempdir):
  101. shutil.rmtree(tempdir)
  102. class SSHSingleTests(TestCase):
  103. def setUp(self):
  104. self.tmp_cachedir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  105. self.argv = [
  106. "ssh.set_auth_key",
  107. "root",
  108. "hobn+amNAXSBTiOXEqlBjGB...rsa root@master",
  109. ]
  110. self.opts = {
  111. "argv": self.argv,
  112. "__role": "master",
  113. "cachedir": self.tmp_cachedir,
  114. "extension_modules": os.path.join(self.tmp_cachedir, "extmods"),
  115. }
  116. self.target = {
  117. "passwd": "abc123",
  118. "ssh_options": None,
  119. "sudo": False,
  120. "identities_only": False,
  121. "host": "login1",
  122. "user": "root",
  123. "timeout": 65,
  124. "remote_port_forwards": None,
  125. "sudo_user": "",
  126. "port": "22",
  127. "priv": "/etc/salt/pki/master/ssh/salt-ssh.rsa",
  128. }
  129. def test_single_opts(self):
  130. """ Sanity check for ssh.Single options
  131. """
  132. single = ssh.Single(
  133. self.opts,
  134. self.opts["argv"],
  135. "localhost",
  136. mods={},
  137. fsclient=None,
  138. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  139. mine=False,
  140. **self.target
  141. )
  142. self.assertEqual(single.shell._ssh_opts(), "")
  143. self.assertEqual(
  144. single.shell._cmd_str("date +%s"),
  145. "ssh login1 "
  146. "-o KbdInteractiveAuthentication=no -o "
  147. "PasswordAuthentication=yes -o ConnectTimeout=65 -o Port=22 "
  148. "-o IdentityFile=/etc/salt/pki/master/ssh/salt-ssh.rsa "
  149. "-o User=root date +%s",
  150. )
  151. def test_run_with_pre_flight(self):
  152. """
  153. test Single.run() when ssh_pre_flight is set
  154. and script successfully runs
  155. """
  156. target = self.target.copy()
  157. target["ssh_pre_flight"] = os.path.join(RUNTIME_VARS.TMP, "script.sh")
  158. single = ssh.Single(
  159. self.opts,
  160. self.opts["argv"],
  161. "localhost",
  162. mods={},
  163. fsclient=None,
  164. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  165. mine=False,
  166. **target
  167. )
  168. cmd_ret = ("Success", "", 0)
  169. mock_flight = MagicMock(return_value=cmd_ret)
  170. mock_cmd = MagicMock(return_value=cmd_ret)
  171. patch_flight = patch("salt.client.ssh.Single.run_ssh_pre_flight", mock_flight)
  172. patch_cmd = patch("salt.client.ssh.Single.cmd_block", mock_cmd)
  173. patch_exec_cmd = patch(
  174. "salt.client.ssh.shell.Shell.exec_cmd", return_value=("", "", 1)
  175. )
  176. patch_os = patch("os.path.exists", side_effect=[True])
  177. with patch_os, patch_flight, patch_cmd, patch_exec_cmd:
  178. ret = single.run()
  179. mock_cmd.assert_called()
  180. mock_flight.assert_called()
  181. assert ret == cmd_ret
  182. def test_run_with_pre_flight_stderr(self):
  183. """
  184. test Single.run() when ssh_pre_flight is set
  185. and script errors when run
  186. """
  187. target = self.target.copy()
  188. target["ssh_pre_flight"] = os.path.join(RUNTIME_VARS.TMP, "script.sh")
  189. single = ssh.Single(
  190. self.opts,
  191. self.opts["argv"],
  192. "localhost",
  193. mods={},
  194. fsclient=None,
  195. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  196. mine=False,
  197. **target
  198. )
  199. cmd_ret = ("", "Error running script", 1)
  200. mock_flight = MagicMock(return_value=cmd_ret)
  201. mock_cmd = MagicMock(return_value=cmd_ret)
  202. patch_flight = patch("salt.client.ssh.Single.run_ssh_pre_flight", mock_flight)
  203. patch_cmd = patch("salt.client.ssh.Single.cmd_block", mock_cmd)
  204. patch_exec_cmd = patch(
  205. "salt.client.ssh.shell.Shell.exec_cmd", return_value=("", "", 1)
  206. )
  207. patch_os = patch("os.path.exists", side_effect=[True])
  208. with patch_os, patch_flight, patch_cmd, patch_exec_cmd:
  209. ret = single.run()
  210. mock_cmd.assert_not_called()
  211. mock_flight.assert_called()
  212. assert ret == cmd_ret
  213. def test_run_with_pre_flight_script_doesnot_exist(self):
  214. """
  215. test Single.run() when ssh_pre_flight is set
  216. and the script does not exist
  217. """
  218. target = self.target.copy()
  219. target["ssh_pre_flight"] = os.path.join(RUNTIME_VARS.TMP, "script.sh")
  220. single = ssh.Single(
  221. self.opts,
  222. self.opts["argv"],
  223. "localhost",
  224. mods={},
  225. fsclient=None,
  226. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  227. mine=False,
  228. **target
  229. )
  230. cmd_ret = ("Success", "", 0)
  231. mock_flight = MagicMock(return_value=cmd_ret)
  232. mock_cmd = MagicMock(return_value=cmd_ret)
  233. patch_flight = patch("salt.client.ssh.Single.run_ssh_pre_flight", mock_flight)
  234. patch_cmd = patch("salt.client.ssh.Single.cmd_block", mock_cmd)
  235. patch_exec_cmd = patch(
  236. "salt.client.ssh.shell.Shell.exec_cmd", return_value=("", "", 1)
  237. )
  238. patch_os = patch("os.path.exists", side_effect=[False])
  239. with patch_os, patch_flight, patch_cmd, patch_exec_cmd:
  240. ret = single.run()
  241. mock_cmd.assert_called()
  242. mock_flight.assert_not_called()
  243. assert ret == cmd_ret
  244. def test_run_with_pre_flight_thin_dir_exists(self):
  245. """
  246. test Single.run() when ssh_pre_flight is set
  247. and thin_dir already exists
  248. """
  249. target = self.target.copy()
  250. target["ssh_pre_flight"] = os.path.join(RUNTIME_VARS.TMP, "script.sh")
  251. single = ssh.Single(
  252. self.opts,
  253. self.opts["argv"],
  254. "localhost",
  255. mods={},
  256. fsclient=None,
  257. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  258. mine=False,
  259. **target
  260. )
  261. cmd_ret = ("", "", 0)
  262. mock_flight = MagicMock(return_value=cmd_ret)
  263. mock_cmd = MagicMock(return_value=cmd_ret)
  264. patch_flight = patch("salt.client.ssh.Single.run_ssh_pre_flight", mock_flight)
  265. patch_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_cmd)
  266. patch_cmd_block = patch("salt.client.ssh.Single.cmd_block", mock_cmd)
  267. patch_os = patch("os.path.exists", return_value=True)
  268. with patch_os, patch_flight, patch_cmd, patch_cmd_block:
  269. ret = single.run()
  270. mock_cmd.assert_called()
  271. mock_flight.assert_not_called()
  272. assert ret == cmd_ret
  273. def test_execute_script(self):
  274. """
  275. test Single.execute_script()
  276. """
  277. single = ssh.Single(
  278. self.opts,
  279. self.opts["argv"],
  280. "localhost",
  281. mods={},
  282. fsclient=None,
  283. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  284. mine=False,
  285. winrm=False,
  286. **self.target
  287. )
  288. exp_ret = ("Success", "", 0)
  289. mock_cmd = MagicMock(return_value=exp_ret)
  290. patch_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_cmd)
  291. script = os.path.join(RUNTIME_VARS.TMP, "script.sh")
  292. with patch_cmd:
  293. ret = single.execute_script(script=script)
  294. assert ret == exp_ret
  295. assert mock_cmd.call_count == 2
  296. assert [
  297. call("/bin/sh '{0}'".format(script)),
  298. call("rm '{0}'".format(script)),
  299. ] == mock_cmd.call_args_list
  300. def test_shim_cmd(self):
  301. """
  302. test Single.shim_cmd()
  303. """
  304. single = ssh.Single(
  305. self.opts,
  306. self.opts["argv"],
  307. "localhost",
  308. mods={},
  309. fsclient=None,
  310. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  311. mine=False,
  312. winrm=False,
  313. tty=True,
  314. **self.target
  315. )
  316. exp_ret = ("Success", "", 0)
  317. mock_cmd = MagicMock(return_value=exp_ret)
  318. patch_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_cmd)
  319. patch_send = patch("salt.client.ssh.shell.Shell.send", return_value=("", "", 0))
  320. patch_rand = patch("os.urandom", return_value=b"5\xd9l\xca\xc2\xff")
  321. with patch_cmd, patch_rand, patch_send:
  322. ret = single.shim_cmd(cmd_str="echo test")
  323. assert ret == exp_ret
  324. assert [
  325. call("/bin/sh '$HOME/.35d96ccac2ff.py'"),
  326. call("rm '$HOME/.35d96ccac2ff.py'"),
  327. ] == mock_cmd.call_args_list
  328. def test_run_ssh_pre_flight(self):
  329. """
  330. test Single.run_ssh_pre_flight
  331. """
  332. target = self.target.copy()
  333. target["ssh_pre_flight"] = os.path.join(RUNTIME_VARS.TMP, "script.sh")
  334. single = ssh.Single(
  335. self.opts,
  336. self.opts["argv"],
  337. "localhost",
  338. mods={},
  339. fsclient=None,
  340. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  341. mine=False,
  342. winrm=False,
  343. tty=True,
  344. **target
  345. )
  346. exp_ret = ("Success", "", 0)
  347. mock_cmd = MagicMock(return_value=exp_ret)
  348. patch_cmd = patch("salt.client.ssh.shell.Shell.exec_cmd", mock_cmd)
  349. patch_send = patch("salt.client.ssh.shell.Shell.send", return_value=exp_ret)
  350. exp_tmp = os.path.join(
  351. tempfile.gettempdir(), os.path.basename(target["ssh_pre_flight"])
  352. )
  353. with patch_cmd, patch_send:
  354. ret = single.run_ssh_pre_flight()
  355. assert ret == exp_ret
  356. assert [
  357. call("/bin/sh '{0}'".format(exp_tmp)),
  358. call("rm '{0}'".format(exp_tmp)),
  359. ] == mock_cmd.call_args_list
  360. @skipIf(salt.utils.platform.is_windows(), "SSH_PY_SHIM not set on windows")
  361. def test_cmd_run_set_path(self):
  362. """
  363. test when set_path is set
  364. """
  365. target = self.target
  366. target["set_path"] = "$PATH:/tmp/path/"
  367. single = ssh.Single(
  368. self.opts,
  369. self.opts["argv"],
  370. "localhost",
  371. mods={},
  372. fsclient=None,
  373. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  374. mine=False,
  375. **self.target
  376. )
  377. ret = single._cmd_str()
  378. assert re.search("\\" + target["set_path"], ret)
  379. @skipIf(salt.utils.platform.is_windows(), "SSH_PY_SHIM not set on windows")
  380. def test_cmd_run_not_set_path(self):
  381. """
  382. test when set_path is not set
  383. """
  384. target = self.target
  385. single = ssh.Single(
  386. self.opts,
  387. self.opts["argv"],
  388. "localhost",
  389. mods={},
  390. fsclient=None,
  391. thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
  392. mine=False,
  393. **self.target
  394. )
  395. ret = single._cmd_str()
  396. assert re.search('SET_PATH=""', ret)