1
0

test_thin.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390
  1. """
  2. :codeauthor: :email:`Bo Maryniuk <bo@suse.de>`
  3. """
  4. import copy
  5. import os
  6. import pathlib
  7. import shutil
  8. import sys
  9. import tarfile
  10. import tempfile
  11. import jinja2
  12. import salt.exceptions
  13. import salt.ext.six
  14. import salt.utils.hashutils
  15. import salt.utils.json
  16. import salt.utils.platform
  17. import salt.utils.stringutils
  18. from salt.ext.six.moves import range
  19. from salt.utils import thin
  20. from salt.utils.stringutils import to_bytes as bts
  21. from tests.support.helpers import TstSuiteLoggingHandler, VirtualEnv
  22. from tests.support.mock import MagicMock, patch
  23. from tests.support.runtests import RUNTIME_VARS
  24. from tests.support.unit import TestCase, skipIf
  25. try:
  26. import pytest
  27. except ImportError:
  28. pytest = None
  29. @skipIf(pytest is None, "PyTest is missing")
  30. class SSHThinTestCase(TestCase):
  31. """
  32. TestCase for SaltSSH-related parts.
  33. """
  34. def setUp(self):
  35. self.jinja_fp = os.path.dirname(jinja2.__file__)
  36. self.ext_conf = {
  37. "test": {
  38. "py-version": [2, 7],
  39. "path": os.path.join(RUNTIME_VARS.CODE_DIR, "salt"),
  40. "dependencies": {"jinja2": self.jinja_fp},
  41. }
  42. }
  43. self.tops = copy.deepcopy(self.ext_conf)
  44. self.tops["test"]["dependencies"] = [self.jinja_fp]
  45. self.tar = self._tarfile(None).open()
  46. self.digest = salt.utils.hashutils.DigestCollector()
  47. self.exp_files = [
  48. os.path.join("salt", "payload.py"),
  49. os.path.join("jinja2", "__init__.py"),
  50. ]
  51. lib_root = os.path.join(RUNTIME_VARS.TMP, "fake-libs")
  52. self.fake_libs = {
  53. "distro": os.path.join(lib_root, "distro"),
  54. "jinja2": os.path.join(lib_root, "jinja2"),
  55. "yaml": os.path.join(lib_root, "yaml"),
  56. "tornado": os.path.join(lib_root, "tornado"),
  57. "msgpack": os.path.join(lib_root, "msgpack"),
  58. }
  59. code_dir = pathlib.Path(RUNTIME_VARS.CODE_DIR).resolve()
  60. self.exp_ret = {
  61. "distro": str(code_dir / "distro.py"),
  62. "jinja2": str(code_dir / "jinja2"),
  63. "yaml": str(code_dir / "yaml"),
  64. "tornado": str(code_dir / "tornado"),
  65. "msgpack": str(code_dir / "msgpack"),
  66. "certifi": str(code_dir / "certifi"),
  67. "singledispatch": str(code_dir / "singledispatch.py"),
  68. }
  69. self.exc_libs = ["jinja2", "yaml"]
  70. def tearDown(self):
  71. for lib, fp in self.fake_libs.items():
  72. if os.path.exists(fp):
  73. shutil.rmtree(fp)
  74. self.exc_libs = None
  75. self.jinja_fp = None
  76. self.ext_conf = None
  77. self.tops = None
  78. self.tar = None
  79. self.digest = None
  80. self.exp_files = None
  81. self.fake_libs = None
  82. self.exp_ret = None
  83. def _popen(self, return_value=None, side_effect=None, returncode=0):
  84. """
  85. Fake subprocess.Popen
  86. :return:
  87. """
  88. proc = MagicMock()
  89. proc.communicate = MagicMock(return_value=return_value, side_effect=side_effect)
  90. proc.returncode = returncode
  91. popen = MagicMock(return_value=proc)
  92. return popen
  93. def _version_info(self, major=None, minor=None):
  94. """
  95. Fake version info.
  96. :param major:
  97. :param minor:
  98. :return:
  99. """
  100. class VersionInfo(tuple):
  101. pass
  102. vi = VersionInfo([major, minor])
  103. vi.major = major or sys.version_info.major
  104. vi.minor = minor or sys.version_info.minor
  105. return vi
  106. def _tarfile(self, getinfo=False):
  107. """
  108. Fake tarfile handler.
  109. :return:
  110. """
  111. spec = ["add", "close"]
  112. if getinfo:
  113. spec.append("getinfo")
  114. tf = MagicMock()
  115. tf.open = MagicMock(return_value=MagicMock(spec=spec))
  116. return tf
  117. @patch("salt.exceptions.SaltSystemExit", Exception)
  118. @patch("salt.utils.thin.log", MagicMock())
  119. @patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
  120. def test_get_ext_tops_cfg_missing_dependencies(self):
  121. """
  122. Test thin.get_ext_tops contains all required dependencies.
  123. :return:
  124. """
  125. cfg = {"namespace": {"py-version": [0, 0], "path": "/foo", "dependencies": []}}
  126. with pytest.raises(Exception) as err:
  127. thin.get_ext_tops(cfg)
  128. self.assertIn("Missing dependencies", str(err.value))
  129. self.assertTrue(thin.log.error.called)
  130. self.assertIn("Missing dependencies", thin.log.error.call_args[0][0])
  131. self.assertIn("jinja2, yaml, tornado, msgpack", thin.log.error.call_args[0][0])
  132. @patch("salt.exceptions.SaltSystemExit", Exception)
  133. @patch("salt.utils.thin.log", MagicMock())
  134. @patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
  135. def test_get_ext_tops_cfg_missing_interpreter(self):
  136. """
  137. Test thin.get_ext_tops contains interpreter configuration.
  138. :return:
  139. """
  140. cfg = {"namespace": {"path": "/foo", "dependencies": []}}
  141. with pytest.raises(salt.exceptions.SaltSystemExit) as err:
  142. thin.get_ext_tops(cfg)
  143. self.assertIn("missing specific locked Python version", str(err.value))
  144. @patch("salt.exceptions.SaltSystemExit", Exception)
  145. @patch("salt.utils.thin.log", MagicMock())
  146. @patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
  147. def test_get_ext_tops_cfg_wrong_interpreter(self):
  148. """
  149. Test thin.get_ext_tops contains correct interpreter configuration.
  150. :return:
  151. """
  152. cfg = {"namespace": {"path": "/foo", "py-version": 2, "dependencies": []}}
  153. with pytest.raises(salt.exceptions.SaltSystemExit) as err:
  154. thin.get_ext_tops(cfg)
  155. self.assertIn(
  156. "specific locked Python version should be a list of " "major/minor version",
  157. str(err.value),
  158. )
  159. @patch("salt.exceptions.SaltSystemExit", Exception)
  160. @patch("salt.utils.thin.log", MagicMock())
  161. @patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
  162. def test_get_ext_tops_cfg_interpreter(self):
  163. """
  164. Test thin.get_ext_tops interpreter configuration.
  165. :return:
  166. """
  167. cfg = {
  168. "namespace": {
  169. "path": "/foo",
  170. "py-version": [2, 6],
  171. "dependencies": {
  172. "jinja2": "",
  173. "yaml": "",
  174. "tornado": "",
  175. "msgpack": "",
  176. },
  177. }
  178. }
  179. with pytest.raises(salt.exceptions.SaltSystemExit):
  180. thin.get_ext_tops(cfg)
  181. assert len(thin.log.warning.mock_calls) == 4
  182. assert sorted([x[1][1] for x in thin.log.warning.mock_calls]) == [
  183. "jinja2",
  184. "msgpack",
  185. "tornado",
  186. "yaml",
  187. ]
  188. assert (
  189. "Module test has missing configuration"
  190. == thin.log.warning.mock_calls[0][1][0] % "test"
  191. )
  192. @patch("salt.exceptions.SaltSystemExit", Exception)
  193. @patch("salt.utils.thin.log", MagicMock())
  194. @patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
  195. def test_get_ext_tops_dependency_config_check(self):
  196. """
  197. Test thin.get_ext_tops dependencies are importable
  198. :return:
  199. """
  200. cfg = {
  201. "namespace": {
  202. "path": "/foo",
  203. "py-version": [2, 6],
  204. "dependencies": {
  205. "jinja2": "/jinja/foo.py",
  206. "yaml": "/yaml/",
  207. "tornado": "/tornado/wrong.rb",
  208. "msgpack": "msgpack.sh",
  209. },
  210. }
  211. }
  212. with pytest.raises(salt.exceptions.SaltSystemExit) as err:
  213. thin.get_ext_tops(cfg)
  214. self.assertIn(
  215. "Missing dependencies for the alternative version in the "
  216. "external configuration",
  217. str(err.value),
  218. )
  219. messages = {}
  220. for cl in thin.log.warning.mock_calls:
  221. messages[cl[1][1]] = cl[1][0] % (cl[1][1], cl[1][2])
  222. for mod in ["tornado", "yaml", "msgpack"]:
  223. self.assertIn("not a Python importable module", messages[mod])
  224. self.assertIn(
  225. "configured with not a file or does not exist", messages["jinja2"]
  226. )
  227. @patch("salt.exceptions.SaltSystemExit", Exception)
  228. @patch("salt.utils.thin.log", MagicMock())
  229. @patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=True))
  230. def test_get_ext_tops_config_pass(self):
  231. """
  232. Test thin.get_ext_tops configuration
  233. :return:
  234. """
  235. cfg = {
  236. "namespace": {
  237. "path": "/foo",
  238. "py-version": [2, 6],
  239. "dependencies": {
  240. "jinja2": "/jinja/foo.py",
  241. "yaml": "/yaml/",
  242. "tornado": "/tornado/tornado.py",
  243. "msgpack": "msgpack.py",
  244. "distro": "distro.py",
  245. },
  246. }
  247. }
  248. out = thin.get_ext_tops(cfg)
  249. assert out["namespace"]["py-version"] == cfg["namespace"]["py-version"]
  250. assert out["namespace"]["path"] == cfg["namespace"]["path"]
  251. assert sorted(out["namespace"]["dependencies"]) == sorted(
  252. [
  253. "/tornado/tornado.py",
  254. "/jinja/foo.py",
  255. "/yaml/",
  256. "msgpack.py",
  257. "distro.py",
  258. ]
  259. )
  260. @patch("salt.utils.thin.sys.argv", [None, '{"foo": "bar"}'])
  261. @patch("salt.utils.thin.get_tops", lambda **kw: kw)
  262. def test_gte(self):
  263. """
  264. Test thin.gte external call for processing the info about tops per interpreter.
  265. :return:
  266. """
  267. assert salt.utils.json.loads(thin.gte()).get("foo") == "bar"
  268. def test_add_dep_path(self):
  269. """
  270. Test thin._add_dependency function to setup dependency paths
  271. :return:
  272. """
  273. container = []
  274. for pth in ["/foo/bar.py", "/something/else/__init__.py"]:
  275. thin._add_dependency(container, type("obj", (), {"__file__": pth})())
  276. assert "__init__" not in container[1]
  277. assert container == ["/foo/bar.py", "/something/else"]
  278. def test_thin_path(self):
  279. """
  280. Test thin.thin_path returns the expected path.
  281. :return:
  282. """
  283. path = os.sep + os.path.join("path", "to")
  284. expected = os.path.join(path, "thin", "thin.tgz")
  285. self.assertEqual(thin.thin_path(path), expected)
  286. def test_get_salt_call_script(self):
  287. """
  288. Test get salt-call script rendered.
  289. :return:
  290. """
  291. out = thin._get_salt_call("foo", "bar", py26=[2, 6], py27=[2, 7], py34=[3, 4])
  292. for line in salt.utils.stringutils.to_str(out).split(os.linesep):
  293. if line.startswith("namespaces = {"):
  294. data = salt.utils.json.loads(line.replace("namespaces = ", "").strip())
  295. assert data.get("py26") == [2, 6]
  296. assert data.get("py27") == [2, 7]
  297. assert data.get("py34") == [3, 4]
  298. if line.startswith("syspaths = "):
  299. data = salt.utils.json.loads(line.replace("syspaths = ", ""))
  300. assert data == ["foo", "bar"]
  301. def test_get_ext_namespaces_empty(self):
  302. """
  303. Test thin._get_ext_namespaces function returns an empty dictionary on nothing
  304. :return:
  305. """
  306. for obj in [None, {}, []]:
  307. assert thin._get_ext_namespaces(obj) == {}
  308. def test_get_ext_namespaces(self):
  309. """
  310. Test thin._get_ext_namespaces function returns namespaces properly out of the config.
  311. :return:
  312. """
  313. cfg = {"ns": {"py-version": [2, 7]}}
  314. assert thin._get_ext_namespaces(cfg).get("ns") == (2, 7,)
  315. assert isinstance(thin._get_ext_namespaces(cfg).get("ns"), tuple)
  316. def test_get_ext_namespaces_failure(self):
  317. """
  318. Test thin._get_ext_namespaces function raises an exception
  319. if python major/minor version is not configured.
  320. :return:
  321. """
  322. with pytest.raises(salt.exceptions.SaltSystemExit):
  323. thin._get_ext_namespaces({"ns": {}})
  324. @patch(
  325. "salt.utils.thin.distro",
  326. type("distro", (), {"__file__": "/site-packages/distro"}),
  327. )
  328. @patch(
  329. "salt.utils.thin.salt", type("salt", (), {"__file__": "/site-packages/salt"}),
  330. )
  331. @patch(
  332. "salt.utils.thin.jinja2",
  333. type("jinja2", (), {"__file__": "/site-packages/jinja2"}),
  334. )
  335. @patch(
  336. "salt.utils.thin.yaml", type("yaml", (), {"__file__": "/site-packages/yaml"}),
  337. )
  338. @patch(
  339. "salt.utils.thin.tornado",
  340. type("tornado", (), {"__file__": "/site-packages/tornado"}),
  341. )
  342. @patch(
  343. "salt.utils.thin.msgpack",
  344. type("msgpack", (), {"__file__": "/site-packages/msgpack"}),
  345. )
  346. @patch(
  347. "salt.utils.thin.certifi",
  348. type("certifi", (), {"__file__": "/site-packages/certifi"}),
  349. )
  350. @patch(
  351. "salt.utils.thin.singledispatch",
  352. type("singledispatch", (), {"__file__": "/site-packages/sdp"}),
  353. )
  354. @patch(
  355. "salt.utils.thin.singledispatch_helpers",
  356. type("singledispatch_helpers", (), {"__file__": "/site-packages/sdp_hlp"}),
  357. )
  358. @patch(
  359. "salt.utils.thin.ssl_match_hostname",
  360. type("ssl_match_hostname", (), {"__file__": "/site-packages/ssl_mh"}),
  361. )
  362. @patch(
  363. "salt.utils.thin.markupsafe",
  364. type("markupsafe", (), {"__file__": "/site-packages/markupsafe"}),
  365. )
  366. @patch(
  367. "salt.utils.thin.backports_abc",
  368. type("backports_abc", (), {"__file__": "/site-packages/backports_abc"}),
  369. )
  370. @patch(
  371. "salt.utils.thin.concurrent",
  372. type("concurrent", (), {"__file__": "/site-packages/concurrent"}),
  373. )
  374. @patch("salt.utils.thin.log", MagicMock())
  375. def test_get_tops(self):
  376. """
  377. Test thin.get_tops to get top directories, based on the interpreter.
  378. :return:
  379. """
  380. base_tops = [
  381. "/site-packages/distro",
  382. "/site-packages/salt",
  383. "/site-packages/jinja2",
  384. "/site-packages/yaml",
  385. "/site-packages/tornado",
  386. "/site-packages/msgpack",
  387. "/site-packages/certifi",
  388. "/site-packages/sdp",
  389. "/site-packages/sdp_hlp",
  390. "/site-packages/ssl_mh",
  391. "/site-packages/markupsafe",
  392. "/site-packages/backports_abc",
  393. "/site-packages/concurrent",
  394. ]
  395. tops = thin.get_tops()
  396. assert len(tops) == len(base_tops)
  397. assert sorted(tops) == sorted(base_tops)
  398. @patch(
  399. "salt.utils.thin.distro",
  400. type("distro", (), {"__file__": "/site-packages/distro"}),
  401. )
  402. @patch(
  403. "salt.utils.thin.salt", type("salt", (), {"__file__": "/site-packages/salt"}),
  404. )
  405. @patch(
  406. "salt.utils.thin.jinja2",
  407. type("jinja2", (), {"__file__": "/site-packages/jinja2"}),
  408. )
  409. @patch(
  410. "salt.utils.thin.yaml", type("yaml", (), {"__file__": "/site-packages/yaml"}),
  411. )
  412. @patch(
  413. "salt.utils.thin.tornado",
  414. type("tornado", (), {"__file__": "/site-packages/tornado"}),
  415. )
  416. @patch(
  417. "salt.utils.thin.msgpack",
  418. type("msgpack", (), {"__file__": "/site-packages/msgpack"}),
  419. )
  420. @patch(
  421. "salt.utils.thin.certifi",
  422. type("certifi", (), {"__file__": "/site-packages/certifi"}),
  423. )
  424. @patch(
  425. "salt.utils.thin.singledispatch",
  426. type("singledispatch", (), {"__file__": "/site-packages/sdp"}),
  427. )
  428. @patch(
  429. "salt.utils.thin.singledispatch_helpers",
  430. type("singledispatch_helpers", (), {"__file__": "/site-packages/sdp_hlp"}),
  431. )
  432. @patch(
  433. "salt.utils.thin.ssl_match_hostname",
  434. type("ssl_match_hostname", (), {"__file__": "/site-packages/ssl_mh"}),
  435. )
  436. @patch(
  437. "salt.utils.thin.markupsafe",
  438. type("markupsafe", (), {"__file__": "/site-packages/markupsafe"}),
  439. )
  440. @patch(
  441. "salt.utils.thin.backports_abc",
  442. type("backports_abc", (), {"__file__": "/site-packages/backports_abc"}),
  443. )
  444. @patch(
  445. "salt.utils.thin.concurrent",
  446. type("concurrent", (), {"__file__": "/site-packages/concurrent"}),
  447. )
  448. @patch("salt.utils.thin.log", MagicMock())
  449. def test_get_tops_extra_mods(self):
  450. """
  451. Test thin.get_tops to get extra-modules alongside the top directories, based on the interpreter.
  452. :return:
  453. """
  454. base_tops = [
  455. "/site-packages/distro",
  456. "/site-packages/salt",
  457. "/site-packages/jinja2",
  458. "/site-packages/yaml",
  459. "/site-packages/tornado",
  460. "/site-packages/msgpack",
  461. "/site-packages/certifi",
  462. "/site-packages/sdp",
  463. "/site-packages/sdp_hlp",
  464. "/site-packages/ssl_mh",
  465. "/site-packages/concurrent",
  466. "/site-packages/markupsafe",
  467. "/site-packages/backports_abc",
  468. os.sep + os.path.join("custom", "foo"),
  469. os.sep + os.path.join("custom", "bar.py"),
  470. ]
  471. builtins = sys.version_info.major == 3 and "builtins" or "__builtin__"
  472. foo = {"__file__": os.sep + os.path.join("custom", "foo", "__init__.py")}
  473. bar = {"__file__": os.sep + os.path.join("custom", "bar")}
  474. with patch(
  475. "{}.__import__".format(builtins),
  476. MagicMock(side_effect=[type("foo", (), foo), type("bar", (), bar)]),
  477. ):
  478. tops = thin.get_tops(extra_mods="foo,bar")
  479. self.assertEqual(len(tops), len(base_tops))
  480. self.assertListEqual(sorted(tops), sorted(base_tops))
  481. @patch(
  482. "salt.utils.thin.distro",
  483. type("distro", (), {"__file__": "/site-packages/distro"}),
  484. )
  485. @patch(
  486. "salt.utils.thin.salt", type("salt", (), {"__file__": "/site-packages/salt"}),
  487. )
  488. @patch(
  489. "salt.utils.thin.jinja2",
  490. type("jinja2", (), {"__file__": "/site-packages/jinja2"}),
  491. )
  492. @patch(
  493. "salt.utils.thin.yaml", type("yaml", (), {"__file__": "/site-packages/yaml"}),
  494. )
  495. @patch(
  496. "salt.utils.thin.tornado",
  497. type("tornado", (), {"__file__": "/site-packages/tornado"}),
  498. )
  499. @patch(
  500. "salt.utils.thin.msgpack",
  501. type("msgpack", (), {"__file__": "/site-packages/msgpack"}),
  502. )
  503. @patch(
  504. "salt.utils.thin.certifi",
  505. type("certifi", (), {"__file__": "/site-packages/certifi"}),
  506. )
  507. @patch(
  508. "salt.utils.thin.singledispatch",
  509. type("singledispatch", (), {"__file__": "/site-packages/sdp"}),
  510. )
  511. @patch(
  512. "salt.utils.thin.singledispatch_helpers",
  513. type("singledispatch_helpers", (), {"__file__": "/site-packages/sdp_hlp"}),
  514. )
  515. @patch(
  516. "salt.utils.thin.ssl_match_hostname",
  517. type("ssl_match_hostname", (), {"__file__": "/site-packages/ssl_mh"}),
  518. )
  519. @patch(
  520. "salt.utils.thin.markupsafe",
  521. type("markupsafe", (), {"__file__": "/site-packages/markupsafe"}),
  522. )
  523. @patch(
  524. "salt.utils.thin.backports_abc",
  525. type("backports_abc", (), {"__file__": "/site-packages/backports_abc"}),
  526. )
  527. @patch(
  528. "salt.utils.thin.concurrent",
  529. type("concurrent", (), {"__file__": "/site-packages/concurrent"}),
  530. )
  531. @patch("salt.utils.thin.log", MagicMock())
  532. def test_get_tops_so_mods(self):
  533. """
  534. Test thin.get_tops to get extra-modules alongside the top directories, based on the interpreter.
  535. :return:
  536. """
  537. base_tops = [
  538. "/site-packages/distro",
  539. "/site-packages/salt",
  540. "/site-packages/jinja2",
  541. "/site-packages/yaml",
  542. "/site-packages/tornado",
  543. "/site-packages/msgpack",
  544. "/site-packages/certifi",
  545. "/site-packages/sdp",
  546. "/site-packages/sdp_hlp",
  547. "/site-packages/ssl_mh",
  548. "/site-packages/concurrent",
  549. "/site-packages/markupsafe",
  550. "/site-packages/backports_abc",
  551. "/custom/foo.so",
  552. "/custom/bar.so",
  553. ]
  554. builtins = sys.version_info.major == 3 and "builtins" or "__builtin__"
  555. with patch(
  556. "{}.__import__".format(builtins),
  557. MagicMock(
  558. side_effect=[
  559. type("salt", (), {"__file__": "/custom/foo.so"}),
  560. type("salt", (), {"__file__": "/custom/bar.so"}),
  561. ]
  562. ),
  563. ):
  564. tops = thin.get_tops(so_mods="foo,bar")
  565. assert len(tops) == len(base_tops)
  566. assert sorted(tops) == sorted(base_tops)
  567. @patch("salt.utils.thin.gen_thin", MagicMock(return_value="/path/to/thin/thin.tgz"))
  568. @patch("salt.utils.hashutils.get_hash", MagicMock(return_value=12345))
  569. def test_thin_sum(self):
  570. """
  571. Test thin.thin_sum function.
  572. :return:
  573. """
  574. assert thin.thin_sum("/cachedir", form="sha256")[1] == 12345
  575. thin.salt.utils.hashutils.get_hash.assert_called()
  576. assert thin.salt.utils.hashutils.get_hash.call_count == 1
  577. path, form = thin.salt.utils.hashutils.get_hash.call_args[0]
  578. assert path == "/path/to/thin/thin.tgz"
  579. assert form == "sha256"
  580. @patch("salt.utils.thin.gen_min", MagicMock(return_value="/path/to/thin/min.tgz"))
  581. @patch("salt.utils.hashutils.get_hash", MagicMock(return_value=12345))
  582. def test_min_sum(self):
  583. """
  584. Test thin.thin_sum function.
  585. :return:
  586. """
  587. assert thin.min_sum("/cachedir", form="sha256") == 12345
  588. thin.salt.utils.hashutils.get_hash.assert_called()
  589. assert thin.salt.utils.hashutils.get_hash.call_count == 1
  590. path, form = thin.salt.utils.hashutils.get_hash.call_args[0]
  591. assert path == "/path/to/thin/min.tgz"
  592. assert form == "sha256"
  593. @patch("salt.utils.thin.sys.version_info", (2, 5))
  594. @patch("salt.exceptions.SaltSystemExit", Exception)
  595. def test_gen_thin_fails_ancient_python_version(self):
  596. """
  597. Test thin.gen_thin function raises an exception
  598. if Python major/minor version is lower than 2.6
  599. :return:
  600. """
  601. with pytest.raises(salt.exceptions.SaltSystemExit) as err:
  602. thin.sys.exc_clear = lambda: None
  603. thin.gen_thin("")
  604. self.assertIn(
  605. "The minimum required python version to run salt-ssh is " '"2.6"',
  606. str(err.value),
  607. )
  608. @skipIf(
  609. salt.utils.platform.is_windows() and thin._six.PY2, "Dies on Python2 on Windows"
  610. )
  611. @patch("salt.exceptions.SaltSystemExit", Exception)
  612. @patch("salt.utils.thin.os.makedirs", MagicMock())
  613. @patch("salt.utils.files.fopen", MagicMock())
  614. @patch("salt.utils.thin._get_salt_call", MagicMock())
  615. @patch("salt.utils.thin._get_ext_namespaces", MagicMock())
  616. @patch("salt.utils.thin.get_tops", MagicMock(return_value=["/foo3", "/bar3"]))
  617. @patch("salt.utils.thin.get_ext_tops", MagicMock(return_value={}))
  618. @patch("salt.utils.thin.os.path.isfile", MagicMock())
  619. @patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=True))
  620. @patch("salt.utils.thin.os.remove", MagicMock())
  621. @patch("salt.utils.thin.os.path.exists", MagicMock())
  622. @patch("salt.utils.path.os_walk", MagicMock(return_value=[]))
  623. @patch(
  624. "salt.utils.thin.subprocess.Popen",
  625. _popen(
  626. None,
  627. side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
  628. ),
  629. )
  630. @patch("salt.utils.thin.tarfile", MagicMock())
  631. @patch("salt.utils.thin.zipfile", MagicMock())
  632. @patch("salt.utils.thin.os.getcwd", MagicMock())
  633. @patch("salt.utils.thin.os.access", MagicMock(return_value=True))
  634. @patch("salt.utils.thin.os.chdir", MagicMock())
  635. @patch("salt.utils.thin.tempfile.mkdtemp", MagicMock())
  636. @patch(
  637. "salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
  638. )
  639. @patch("salt.utils.thin.shutil", MagicMock())
  640. @patch("salt.utils.path.which", MagicMock(return_value=""))
  641. @patch("salt.utils.thin._get_thintar_prefix", MagicMock())
  642. def test_gen_thin_python_exist_or_not(self):
  643. """
  644. Test thin.gen_thin function if the opposite python
  645. binary does not exist
  646. """
  647. with TstSuiteLoggingHandler() as handler:
  648. thin.gen_thin("")
  649. salt.utils.thin.subprocess.Popen.assert_not_called()
  650. if salt.ext.six.PY2:
  651. self.assertIn(
  652. "DEBUG:python3 binary does not exist. Will not attempt to generate "
  653. "tops for Python 3",
  654. handler.messages,
  655. )
  656. if salt.ext.six.PY3:
  657. self.assertIn(
  658. "DEBUG:python2 binary does not exist. Will not "
  659. "detect Python 2 version",
  660. handler.messages,
  661. )
  662. self.assertIn(
  663. "DEBUG:python2 binary does not exist. Will not attempt to generate "
  664. "tops for Python 2",
  665. handler.messages,
  666. )
  667. @skipIf(
  668. salt.utils.platform.is_windows() and thin._six.PY2, "Dies on Python2 on Windows"
  669. )
  670. @patch("salt.exceptions.SaltSystemExit", Exception)
  671. @patch("salt.utils.thin.log", MagicMock())
  672. @patch("salt.utils.thin.os.makedirs", MagicMock())
  673. @patch("salt.utils.files.fopen", MagicMock())
  674. @patch("salt.utils.thin._get_salt_call", MagicMock())
  675. @patch("salt.utils.thin._get_ext_namespaces", MagicMock())
  676. @patch("salt.utils.thin.get_tops", MagicMock(return_value=["/foo3", "/bar3"]))
  677. @patch("salt.utils.thin.get_ext_tops", MagicMock(return_value={}))
  678. @patch("salt.utils.thin.os.path.isfile", MagicMock())
  679. @patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=True))
  680. @patch("salt.utils.thin.log", MagicMock())
  681. @patch("salt.utils.thin.os.remove", MagicMock())
  682. @patch("salt.utils.thin.os.path.exists", MagicMock())
  683. @patch("salt.utils.path.os_walk", MagicMock(return_value=[]))
  684. @patch(
  685. "salt.utils.thin.subprocess.Popen",
  686. _popen(
  687. None,
  688. side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
  689. ),
  690. )
  691. @patch("salt.utils.thin.tarfile", MagicMock())
  692. @patch("salt.utils.thin.zipfile", MagicMock())
  693. @patch("salt.utils.thin.os.getcwd", MagicMock())
  694. @patch("salt.utils.thin.os.access", MagicMock(return_value=True))
  695. @patch("salt.utils.thin.os.chdir", MagicMock())
  696. @patch("salt.utils.thin.os.close", MagicMock())
  697. @patch("salt.utils.thin.tempfile.mkdtemp", MagicMock())
  698. @patch(
  699. "salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
  700. )
  701. @patch("salt.utils.thin.shutil", MagicMock())
  702. @patch("salt.utils.thin._six.PY3", True)
  703. @patch("salt.utils.thin._six.PY2", False)
  704. @patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
  705. @patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
  706. def test_gen_thin_compression_fallback_py3(self):
  707. """
  708. Test thin.gen_thin function if fallbacks to the gzip compression, once setup wrong.
  709. NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
  710. :return:
  711. """
  712. thin.gen_thin("", compress="arj")
  713. thin.log.warning.assert_called()
  714. pt, msg = thin.log.warning.mock_calls[0][1]
  715. assert (
  716. pt % msg
  717. == 'Unknown compression type: "arj". Falling back to "gzip" compression.'
  718. )
  719. thin.zipfile.ZipFile.assert_not_called()
  720. thin.tarfile.open.assert_called()
  721. @patch("salt.exceptions.SaltSystemExit", Exception)
  722. @patch("salt.utils.thin.log", MagicMock())
  723. @patch("salt.utils.thin.os.makedirs", MagicMock())
  724. @patch("salt.utils.files.fopen", MagicMock())
  725. @patch("salt.utils.thin._get_salt_call", MagicMock())
  726. @patch("salt.utils.thin._get_ext_namespaces", MagicMock())
  727. @patch("salt.utils.thin.get_tops", MagicMock(return_value=["/foo3", "/bar3"]))
  728. @patch("salt.utils.thin.get_ext_tops", MagicMock(return_value={}))
  729. @patch("salt.utils.thin.os.path.isfile", MagicMock())
  730. @patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=False))
  731. @patch("salt.utils.thin.log", MagicMock())
  732. @patch("salt.utils.thin.os.remove", MagicMock())
  733. @patch("salt.utils.thin.os.path.exists", MagicMock())
  734. @patch("salt.utils.path.os_walk", MagicMock(return_value=[]))
  735. @patch(
  736. "salt.utils.thin.subprocess.Popen",
  737. _popen(
  738. None,
  739. side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
  740. ),
  741. )
  742. @patch("salt.utils.thin.tarfile", MagicMock())
  743. @patch("salt.utils.thin.zipfile", MagicMock())
  744. @patch("salt.utils.thin.os.getcwd", MagicMock())
  745. @patch("salt.utils.thin.os.access", MagicMock(return_value=True))
  746. @patch("salt.utils.thin.os.chdir", MagicMock())
  747. @patch("salt.utils.thin.os.close", MagicMock())
  748. @patch("salt.utils.thin.tempfile.mkdtemp", MagicMock(return_value=""))
  749. @patch(
  750. "salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
  751. )
  752. @patch("salt.utils.thin.shutil", MagicMock())
  753. @patch("salt.utils.thin._six.PY3", True)
  754. @patch("salt.utils.thin._six.PY2", False)
  755. @patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
  756. @patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
  757. def test_gen_thin_control_files_written_py3(self):
  758. """
  759. Test thin.gen_thin function if control files are written (version, salt-call etc).
  760. NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
  761. :return:
  762. """
  763. thin.gen_thin("")
  764. arc_name, arc_mode = thin.tarfile.method_calls[0][1]
  765. self.assertEqual(arc_name, ".temporary")
  766. self.assertEqual(arc_mode, "w:gz")
  767. for idx, fname in enumerate(
  768. ["version", ".thin-gen-py-version", "salt-call", "supported-versions"]
  769. ):
  770. name = thin.tarfile.open().method_calls[idx + 4][1][0]
  771. self.assertEqual(name, fname)
  772. thin.tarfile.open().close.assert_called()
  773. @patch("salt.exceptions.SaltSystemExit", Exception)
  774. @patch("salt.utils.thin.log", MagicMock())
  775. @patch("salt.utils.thin.os.makedirs", MagicMock())
  776. @patch("salt.utils.files.fopen", MagicMock())
  777. @patch("salt.utils.thin._get_salt_call", MagicMock())
  778. @patch("salt.utils.thin._get_ext_namespaces", MagicMock())
  779. @patch("salt.utils.thin.get_tops", MagicMock(return_value=["/salt", "/bar3"]))
  780. @patch("salt.utils.thin.get_ext_tops", MagicMock(return_value={}))
  781. @patch("salt.utils.thin.os.path.isfile", MagicMock())
  782. @patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=True))
  783. @patch("salt.utils.thin.log", MagicMock())
  784. @patch("salt.utils.thin.os.remove", MagicMock())
  785. @patch("salt.utils.thin.os.path.exists", MagicMock())
  786. @patch(
  787. "salt.utils.path.os_walk",
  788. MagicMock(
  789. return_value=(
  790. ("root", [], ["r1", "r2", "r3"]),
  791. ("root2", [], ["r4", "r5", "r6"]),
  792. )
  793. ),
  794. )
  795. @patch(
  796. "salt.utils.thin.subprocess.Popen",
  797. _popen(
  798. None,
  799. side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
  800. ),
  801. )
  802. @patch("salt.utils.thin.tarfile", _tarfile(None))
  803. @patch("salt.utils.thin.zipfile", MagicMock())
  804. @patch(
  805. "salt.utils.thin.os.getcwd",
  806. MagicMock(return_value=os.path.join(RUNTIME_VARS.TMP, "fake-cwd")),
  807. )
  808. @patch("salt.utils.thin.os.chdir", MagicMock())
  809. @patch("salt.utils.thin.os.close", MagicMock())
  810. @patch("salt.utils.thin.tempfile.mkdtemp", MagicMock(return_value=""))
  811. @patch(
  812. "salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
  813. )
  814. @patch("salt.utils.thin.shutil", MagicMock())
  815. @patch("salt.utils.thin._six.PY3", True)
  816. @patch("salt.utils.thin._six.PY2", False)
  817. @patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
  818. @patch("salt.utils.hashutils.DigestCollector", MagicMock())
  819. @patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
  820. def test_gen_thin_main_content_files_written_py3(self):
  821. """
  822. Test thin.gen_thin function if main content files are written.
  823. NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
  824. :return:
  825. """
  826. thin.gen_thin("")
  827. files = []
  828. for py in ("py2", "py2", "py3", "pyall"):
  829. for i in range(1, 4):
  830. files.append(os.path.join(py, "root", "r{}".format(i)))
  831. for i in range(4, 7):
  832. files.append(os.path.join(py, "root2", "r{}".format(i)))
  833. for cl in thin.tarfile.open().method_calls[:-6]:
  834. arcname = cl[2].get("arcname")
  835. self.assertIn(arcname, files)
  836. files.pop(files.index(arcname))
  837. self.assertFalse(files)
  838. @patch("salt.exceptions.SaltSystemExit", Exception)
  839. @patch("salt.utils.thin.log", MagicMock())
  840. @patch("salt.utils.thin.os.makedirs", MagicMock())
  841. @patch("salt.utils.files.fopen", MagicMock())
  842. @patch("salt.utils.thin._get_salt_call", MagicMock())
  843. @patch("salt.utils.thin._get_ext_namespaces", MagicMock())
  844. @patch("salt.utils.thin.get_tops", MagicMock(return_value=[]))
  845. @patch(
  846. "salt.utils.thin.get_ext_tops",
  847. MagicMock(
  848. return_value={
  849. "namespace": {
  850. "py-version": [2, 7],
  851. "path": "/opt/2015.8/salt",
  852. "dependencies": ["/opt/certifi", "/opt/whatever"],
  853. }
  854. }
  855. ),
  856. )
  857. @patch("salt.utils.thin.os.path.isfile", MagicMock())
  858. @patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=True))
  859. @patch("salt.utils.thin.log", MagicMock())
  860. @patch("salt.utils.thin.os.remove", MagicMock())
  861. @patch("salt.utils.thin.os.path.exists", MagicMock())
  862. @patch(
  863. "salt.utils.path.os_walk",
  864. MagicMock(
  865. return_value=(
  866. ("root", [], ["r1", "r2", "r3"]),
  867. ("root2", [], ["r4", "r5", "r6"]),
  868. )
  869. ),
  870. )
  871. @patch(
  872. "salt.utils.thin.subprocess.Popen",
  873. _popen(
  874. None,
  875. side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
  876. ),
  877. )
  878. @patch("salt.utils.thin.tarfile", _tarfile(None))
  879. @patch("salt.utils.thin.zipfile", MagicMock())
  880. @patch(
  881. "salt.utils.thin.os.getcwd",
  882. MagicMock(return_value=os.path.join(RUNTIME_VARS.TMP, "fake-cwd")),
  883. )
  884. @patch("salt.utils.thin.os.chdir", MagicMock())
  885. @patch("salt.utils.thin.os.close", MagicMock())
  886. @patch("salt.utils.thin.tempfile.mkdtemp", MagicMock(return_value=""))
  887. @patch(
  888. "salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
  889. )
  890. @patch("salt.utils.thin.shutil", MagicMock())
  891. @patch("salt.utils.thin._six.PY3", True)
  892. @patch("salt.utils.thin._six.PY2", False)
  893. @patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
  894. @patch("salt.utils.hashutils.DigestCollector", MagicMock())
  895. @patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
  896. def test_gen_thin_ext_alternative_content_files_written_py3(self):
  897. """
  898. Test thin.gen_thin function if external alternative content files are written.
  899. NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
  900. :return:
  901. """
  902. ext_conf = {
  903. "namespace": {
  904. "py-version": [2, 7],
  905. "path": "/opt/2015.8/salt",
  906. "dependencies": {
  907. "certifi": "/opt/certifi",
  908. "whatever": "/opt/whatever",
  909. },
  910. }
  911. }
  912. thin.gen_thin("", extended_cfg=ext_conf)
  913. files = []
  914. for py in ("pyall", "pyall", "py2"):
  915. for i in range(1, 4):
  916. files.append(os.path.join("namespace", py, "root", "r{}".format(i)))
  917. for i in range(4, 7):
  918. files.append(os.path.join("namespace", py, "root2", "r{}".format(i)))
  919. for idx, cl in enumerate(thin.tarfile.open().method_calls[12:-6]):
  920. arcname = cl[2].get("arcname")
  921. self.assertIn(arcname, files)
  922. files.pop(files.index(arcname))
  923. self.assertFalse(files)
  924. def test_get_supported_py_config_typecheck(self):
  925. """
  926. Test collecting proper py-versions. Should return bytes type.
  927. :return:
  928. """
  929. tops = {}
  930. ext_cfg = {}
  931. out = thin._get_supported_py_config(tops=tops, extended_cfg=ext_cfg)
  932. assert type(salt.utils.stringutils.to_bytes("")) == type(out)
  933. def test_get_supported_py_config_base_tops(self):
  934. """
  935. Test collecting proper py-versions. Should return proper base tops.
  936. :return:
  937. """
  938. tops = {"3": ["/groundkeepers", "/stole"], "2": ["/the-root", "/password"]}
  939. ext_cfg = {}
  940. out = (
  941. salt.utils.stringutils.to_str(
  942. thin._get_supported_py_config(tops=tops, extended_cfg=ext_cfg)
  943. )
  944. .strip()
  945. .split(os.linesep)
  946. )
  947. self.assertEqual(len(out), 2)
  948. for t_line in ["py3:3:0", "py2:2:7"]:
  949. self.assertIn(t_line, out)
  950. def test_get_supported_py_config_ext_tops(self):
  951. """
  952. Test collecting proper py-versions. Should return proper ext conf tops.
  953. :return:
  954. """
  955. tops = {}
  956. ext_cfg = {
  957. "solar-interference": {"py-version": [2, 6]},
  958. "second-system-effect": {"py-version": [2, 7]},
  959. }
  960. out = (
  961. salt.utils.stringutils.to_str(
  962. thin._get_supported_py_config(tops=tops, extended_cfg=ext_cfg)
  963. )
  964. .strip()
  965. .split(os.linesep)
  966. )
  967. for t_line in ["second-system-effect:2:7", "solar-interference:2:6"]:
  968. self.assertIn(t_line, out)
  969. @patch("salt.exceptions.SaltSystemExit", Exception)
  970. @patch("salt.utils.thin.log", MagicMock())
  971. @patch("salt.utils.thin.os.makedirs", MagicMock())
  972. @patch("salt.utils.files.fopen", MagicMock())
  973. @patch("salt.utils.thin._get_salt_call", MagicMock())
  974. @patch("salt.utils.thin._get_ext_namespaces", MagicMock())
  975. @patch("salt.utils.thin.get_tops", MagicMock(return_value=["/foo3", "/bar3"]))
  976. @patch("salt.utils.thin.get_ext_tops", MagicMock(return_value={}))
  977. @patch("salt.utils.thin.os.path.isfile", MagicMock())
  978. @patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=False))
  979. @patch("salt.utils.thin.log", MagicMock())
  980. @patch("salt.utils.thin.os.remove", MagicMock())
  981. @patch("salt.utils.thin.os.path.exists", MagicMock())
  982. @patch("salt.utils.path.os_walk", MagicMock(return_value=[]))
  983. @patch(
  984. "salt.utils.thin.subprocess.Popen",
  985. _popen(
  986. None,
  987. side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
  988. ),
  989. )
  990. @patch("salt.utils.thin.tarfile", MagicMock())
  991. @patch("salt.utils.thin.zipfile", MagicMock())
  992. @patch("salt.utils.thin.os.getcwd", MagicMock())
  993. @patch("salt.utils.thin.os.access", MagicMock(return_value=False))
  994. @patch("salt.utils.thin.os.chdir", MagicMock())
  995. @patch("salt.utils.thin.os.close", MagicMock())
  996. @patch("salt.utils.thin.tempfile.mkdtemp", MagicMock(return_value=""))
  997. @patch(
  998. "salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
  999. )
  1000. @patch("salt.utils.thin.shutil", MagicMock())
  1001. @patch("salt.utils.thin._six.PY3", True)
  1002. @patch("salt.utils.thin._six.PY2", False)
  1003. @patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
  1004. def test_gen_thin_control_files_written_access_denied_cwd(self):
  1005. """
  1006. Test thin.gen_thin function if control files are written (version, salt-call etc)
  1007. when the current working directory is inaccessible, eg. Salt is configured to run as
  1008. a non-root user but the command is executed in a directory that the user does not
  1009. have permissions to. Issue #54317.
  1010. NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
  1011. :return:
  1012. """
  1013. thin.gen_thin("")
  1014. arc_name, arc_mode = thin.tarfile.method_calls[0][1]
  1015. self.assertEqual(arc_name, ".temporary")
  1016. self.assertEqual(arc_mode, "w:gz")
  1017. for idx, fname in enumerate(
  1018. ["version", ".thin-gen-py-version", "salt-call", "supported-versions"]
  1019. ):
  1020. name = thin.tarfile.open().method_calls[idx + 2][1][0]
  1021. self.assertEqual(name, fname)
  1022. thin.tarfile.open().close.assert_called()
  1023. def test_get_tops_python(self):
  1024. """
  1025. test get_tops_python
  1026. """
  1027. patch_proc = patch(
  1028. "salt.utils.thin.subprocess.Popen",
  1029. self._popen(
  1030. None,
  1031. side_effect=[
  1032. (bts("distro.py"), bts("")),
  1033. (bts("jinja2/__init__.py"), bts("")),
  1034. (bts("yaml/__init__.py"), bts("")),
  1035. (bts("tornado/__init__.py"), bts("")),
  1036. (bts("msgpack/__init__.py"), bts("")),
  1037. (bts("certifi/__init__.py"), bts("")),
  1038. (bts("singledispatch.py"), bts("")),
  1039. (bts(""), bts("")),
  1040. (bts(""), bts("")),
  1041. (bts(""), bts("")),
  1042. (bts(""), bts("")),
  1043. (bts(""), bts("")),
  1044. ],
  1045. ),
  1046. )
  1047. patch_os = patch("os.path.exists", return_value=True)
  1048. patch_which = patch("salt.utils.path.which", return_value=True)
  1049. with patch_proc, patch_os, patch_which:
  1050. with TstSuiteLoggingHandler() as log_handler:
  1051. exp_ret = copy.deepcopy(self.exp_ret)
  1052. ret = thin.get_tops_python("python2.7")
  1053. if salt.utils.platform.is_windows():
  1054. for key, value in ret.items():
  1055. ret[key] = str(pathlib.Path(value).resolve(strict=False))
  1056. for key, value in exp_ret.items():
  1057. exp_ret[key] = str(pathlib.Path(value).resolve(strict=False))
  1058. assert ret == exp_ret
  1059. assert (
  1060. "ERROR:Could not auto detect file location for module concurrent for python version python2.7"
  1061. in log_handler.messages
  1062. )
  1063. def test_get_tops_python_exclude(self):
  1064. """
  1065. test get_tops_python when excluding modules
  1066. """
  1067. patch_proc = patch(
  1068. "salt.utils.thin.subprocess.Popen",
  1069. self._popen(
  1070. None,
  1071. side_effect=[
  1072. (bts("distro.py"), bts("")),
  1073. (bts("tornado/__init__.py"), bts("")),
  1074. (bts("msgpack/__init__.py"), bts("")),
  1075. (bts("certifi/__init__.py"), bts("")),
  1076. (bts("singledispatch.py"), bts("")),
  1077. (bts(""), bts("")),
  1078. (bts(""), bts("")),
  1079. (bts(""), bts("")),
  1080. (bts(""), bts("")),
  1081. (bts(""), bts("")),
  1082. ],
  1083. ),
  1084. )
  1085. exp_ret = copy.deepcopy(self.exp_ret)
  1086. for lib in self.exc_libs:
  1087. exp_ret.pop(lib)
  1088. patch_os = patch("os.path.exists", return_value=True)
  1089. patch_which = patch("salt.utils.path.which", return_value=True)
  1090. with patch_proc, patch_os, patch_which:
  1091. ret = thin.get_tops_python("python2.7", exclude=self.exc_libs)
  1092. if salt.utils.platform.is_windows():
  1093. for key, value in ret.items():
  1094. ret[key] = str(pathlib.Path(value).resolve(strict=False))
  1095. for key, value in exp_ret.items():
  1096. exp_ret[key] = str(pathlib.Path(value).resolve(strict=False))
  1097. assert ret == exp_ret
  1098. def test_pack_alternatives_exclude(self):
  1099. """
  1100. test pack_alternatives when mixing
  1101. manually set dependencies and auto
  1102. detecting other modules.
  1103. """
  1104. patch_proc = patch(
  1105. "salt.utils.thin.subprocess.Popen",
  1106. self._popen(
  1107. None,
  1108. side_effect=[
  1109. (bts(self.fake_libs["distro"]), bts("")),
  1110. (bts(self.fake_libs["yaml"]), bts("")),
  1111. (bts(self.fake_libs["tornado"]), bts("")),
  1112. (bts(self.fake_libs["msgpack"]), bts("")),
  1113. (bts(""), bts("")),
  1114. (bts(""), bts("")),
  1115. (bts(""), bts("")),
  1116. (bts(""), bts("")),
  1117. (bts(""), bts("")),
  1118. (bts(""), bts("")),
  1119. (bts(""), bts("")),
  1120. ],
  1121. ),
  1122. )
  1123. patch_os = patch("os.path.exists", return_value=True)
  1124. ext_conf = copy.deepcopy(self.ext_conf)
  1125. ext_conf["test"]["auto_detect"] = True
  1126. for lib in self.fake_libs.values():
  1127. os.makedirs(lib)
  1128. with salt.utils.files.fopen(os.path.join(lib, "__init__.py"), "w+") as fp_:
  1129. fp_.write("test")
  1130. exp_files = self.exp_files.copy()
  1131. exp_files.extend(
  1132. [
  1133. os.path.join("yaml", "__init__.py"),
  1134. os.path.join("tornado", "__init__.py"),
  1135. os.path.join("msgpack", "__init__.py"),
  1136. ]
  1137. )
  1138. patch_which = patch("salt.utils.path.which", return_value=True)
  1139. with patch_os, patch_proc, patch_which:
  1140. thin._pack_alternative(ext_conf, self.digest, self.tar)
  1141. calls = self.tar.mock_calls
  1142. for _file in exp_files:
  1143. assert [x for x in calls if "{}".format(_file) in x.args]
  1144. def test_pack_alternatives(self):
  1145. """
  1146. test thin._pack_alternatives
  1147. """
  1148. with patch("salt.utils.thin.get_ext_tops", MagicMock(return_value=self.tops)):
  1149. thin._pack_alternative(self.ext_conf, self.digest, self.tar)
  1150. calls = self.tar.mock_calls
  1151. for _file in self.exp_files:
  1152. assert [x for x in calls if "{}".format(_file) in x.args]
  1153. assert [
  1154. x
  1155. for x in calls
  1156. if os.path.join("test", "pyall", _file) in x.kwargs["arcname"]
  1157. ]
  1158. def test_pack_alternatives_not_normalized(self):
  1159. """
  1160. test thin._pack_alternatives when the path
  1161. is not normalized
  1162. """
  1163. tops = copy.deepcopy(self.tops)
  1164. tops["test"]["dependencies"] = [self.jinja_fp + "/"]
  1165. with patch("salt.utils.thin.get_ext_tops", MagicMock(return_value=tops)):
  1166. thin._pack_alternative(self.ext_conf, self.digest, self.tar)
  1167. calls = self.tar.mock_calls
  1168. for _file in self.exp_files:
  1169. assert [x for x in calls if "{}".format(_file) in x.args]
  1170. assert [
  1171. x
  1172. for x in calls
  1173. if os.path.join("test", "pyall", _file) in x.kwargs["arcname"]
  1174. ]
  1175. def test_pack_alternatives_path_doesnot_exist(self):
  1176. """
  1177. test thin._pack_alternatives when the path
  1178. doesnt exist. Check error log message
  1179. and expect that because the directory
  1180. does not exist jinja2 does not get
  1181. added to the tar
  1182. """
  1183. bad_path = os.path.join(tempfile.gettempdir(), "doesnotexisthere")
  1184. tops = copy.deepcopy(self.tops)
  1185. tops["test"]["dependencies"] = [bad_path]
  1186. with patch("salt.utils.thin.get_ext_tops", MagicMock(return_value=tops)):
  1187. with TstSuiteLoggingHandler() as log_handler:
  1188. thin._pack_alternative(self.ext_conf, self.digest, self.tar)
  1189. msg = "ERROR:File path {} does not exist. Unable to add to salt-ssh thin".format(
  1190. bad_path
  1191. )
  1192. assert msg in log_handler.messages
  1193. calls = self.tar.mock_calls
  1194. for _file in self.exp_files:
  1195. arg = [x for x in calls if "{}".format(_file) in x.args]
  1196. kwargs = [
  1197. x
  1198. for x in calls
  1199. if os.path.join("test", "pyall", _file) in x.kwargs["arcname"]
  1200. ]
  1201. if "jinja2" in _file:
  1202. assert not arg
  1203. assert not kwargs
  1204. else:
  1205. assert arg
  1206. assert kwargs
  1207. def test_pack_alternatives_auto_detect(self):
  1208. """
  1209. test thin._pack_alternatives when auto_detect
  1210. is enabled
  1211. """
  1212. ext_conf = copy.deepcopy(self.ext_conf)
  1213. ext_conf["test"]["auto_detect"] = True
  1214. for lib in self.fake_libs.values():
  1215. os.makedirs(lib)
  1216. with salt.utils.files.fopen(os.path.join(lib, "__init__.py"), "w+") as fp_:
  1217. fp_.write("test")
  1218. patch_tops_py = patch(
  1219. "salt.utils.thin.get_tops_python", return_value=self.fake_libs
  1220. )
  1221. exp_files = self.exp_files.copy()
  1222. exp_files.extend(
  1223. [
  1224. os.path.join("yaml", "__init__.py"),
  1225. os.path.join("tornado", "__init__.py"),
  1226. os.path.join("msgpack", "__init__.py"),
  1227. ]
  1228. )
  1229. with patch_tops_py:
  1230. thin._pack_alternative(ext_conf, self.digest, self.tar)
  1231. calls = self.tar.mock_calls
  1232. for _file in exp_files:
  1233. assert [x for x in calls if "{}".format(_file) in x.args]
  1234. def test_pack_alternatives_empty_dependencies(self):
  1235. """
  1236. test _pack_alternatives when dependencies is not
  1237. set in the config.
  1238. """
  1239. ext_conf = copy.deepcopy(self.ext_conf)
  1240. ext_conf["test"]["auto_detect"] = True
  1241. ext_conf["test"].pop("dependencies")
  1242. for lib in self.fake_libs.values():
  1243. os.makedirs(lib)
  1244. with salt.utils.files.fopen(os.path.join(lib, "__init__.py"), "w+") as fp_:
  1245. fp_.write("test")
  1246. patch_tops_py = patch(
  1247. "salt.utils.thin.get_tops_python", return_value=self.fake_libs
  1248. )
  1249. exp_files = self.exp_files.copy()
  1250. exp_files.extend(
  1251. [
  1252. os.path.join("yaml", "__init__.py"),
  1253. os.path.join("tornado", "__init__.py"),
  1254. os.path.join("msgpack", "__init__.py"),
  1255. ]
  1256. )
  1257. with patch_tops_py:
  1258. thin._pack_alternative(ext_conf, self.digest, self.tar)
  1259. calls = self.tar.mock_calls
  1260. for _file in exp_files:
  1261. assert [x for x in calls if "{}".format(_file) in x.args]
  1262. @skipIf(
  1263. salt.utils.platform.is_windows(), "salt-ssh does not deploy to/from windows"
  1264. )
  1265. def test_thin_dir(self):
  1266. """
  1267. Test the thin dir to make sure salt-call can run
  1268. Run salt call via a python in a new virtual environment to ensure
  1269. salt-call has all dependencies needed.
  1270. """
  1271. # This was previously an integration test and is now here, as a unit test.
  1272. # Should actually be a functional test
  1273. with VirtualEnv() as venv:
  1274. salt.utils.thin.gen_thin(venv.venv_dir)
  1275. thin_dir = os.path.join(venv.venv_dir, "thin")
  1276. thin_archive = os.path.join(thin_dir, "thin.tgz")
  1277. tar = tarfile.open(thin_archive)
  1278. tar.extractall(thin_dir)
  1279. tar.close()
  1280. ret = venv.run(
  1281. venv.venv_python,
  1282. os.path.join(thin_dir, "salt-call"),
  1283. "--version",
  1284. check=False,
  1285. )
  1286. assert ret.exitcode == 0, ret