test_fileclient.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests for the salt fileclient
  4. """
  5. # Import Python libs
  6. from __future__ import absolute_import
  7. import errno
  8. import logging
  9. import os
  10. import shutil
  11. # Import Salt libs
  12. import salt.utils.files
  13. from salt import fileclient
  14. from salt.ext import six
  15. from salt.ext.six.moves import range
  16. from tests.integration import AdaptedConfigurationTestCaseMixin
  17. from tests.support.mixins import LoaderModuleMockMixin
  18. from tests.support.mock import MagicMock, Mock, patch
  19. # Import Salt Testing libs
  20. from tests.support.runtests import RUNTIME_VARS
  21. from tests.support.unit import TestCase
  22. log = logging.getLogger(__name__)
  23. class FileclientTestCase(TestCase):
  24. """
  25. Fileclient test
  26. """
  27. opts = {
  28. "extension_modules": "",
  29. "cachedir": "/__test__",
  30. }
  31. def _fake_makedir(self, num=errno.EEXIST):
  32. def _side_effect(*args, **kwargs):
  33. raise OSError(num, "Errno {0}".format(num))
  34. return Mock(side_effect=_side_effect)
  35. def test_cache_skips_makedirs_on_race_condition(self):
  36. """
  37. If cache contains already a directory, do not raise an exception.
  38. """
  39. with patch("os.path.isfile", lambda prm: False):
  40. for exists in range(2):
  41. with patch("os.makedirs", self._fake_makedir()):
  42. with fileclient.Client(self.opts)._cache_loc(
  43. "testfile"
  44. ) as c_ref_itr:
  45. assert c_ref_itr == os.sep + os.sep.join(
  46. ["__test__", "files", "base", "testfile"]
  47. )
  48. def test_cache_raises_exception_on_non_eexist_ioerror(self):
  49. """
  50. If makedirs raises other than EEXIST errno, an exception should be raised.
  51. """
  52. with patch("os.path.isfile", lambda prm: False):
  53. with patch("os.makedirs", self._fake_makedir(num=errno.EROFS)):
  54. with self.assertRaises(OSError):
  55. with fileclient.Client(self.opts)._cache_loc(
  56. "testfile"
  57. ) as c_ref_itr:
  58. assert c_ref_itr == "/__test__/files/base/testfile"
  59. def test_extrn_path_with_long_filename(self):
  60. safe_file_name = os.path.split(
  61. fileclient.Client(self.opts)._extrn_path(
  62. "https://test.com/" + ("A" * 254), "base"
  63. )
  64. )[-1]
  65. assert safe_file_name == "A" * 254
  66. oversized_file_name = os.path.split(
  67. fileclient.Client(self.opts)._extrn_path(
  68. "https://test.com/" + ("A" * 255), "base"
  69. )
  70. )[-1]
  71. assert len(oversized_file_name) < 256
  72. assert oversized_file_name != "A" * 255
  73. oversized_file_with_query_params = os.path.split(
  74. fileclient.Client(self.opts)._extrn_path(
  75. "https://test.com/file?" + ("A" * 255), "base"
  76. )
  77. )[-1]
  78. assert len(oversized_file_with_query_params) < 256
  79. SALTENVS = ("base", "dev")
  80. SUBDIR = "subdir"
  81. SUBDIR_FILES = ("foo.txt", "bar.txt", "baz.txt")
  82. def _get_file_roots(fs_root):
  83. return dict([(x, [os.path.join(fs_root, x)]) for x in SALTENVS])
  84. class FileClientTest(
  85. TestCase, AdaptedConfigurationTestCaseMixin, LoaderModuleMockMixin
  86. ):
  87. def setup_loader_modules(self):
  88. FS_ROOT = os.path.join(RUNTIME_VARS.TMP, "fileclient_fs_root")
  89. CACHE_ROOT = os.path.join(RUNTIME_VARS.TMP, "fileclient_cache_root")
  90. MOCKED_OPTS = {
  91. "file_roots": _get_file_roots(FS_ROOT),
  92. "fileserver_backend": ["roots"],
  93. "cachedir": CACHE_ROOT,
  94. "file_client": "local",
  95. }
  96. self.addCleanup(shutil.rmtree, FS_ROOT, ignore_errors=True)
  97. self.addCleanup(shutil.rmtree, CACHE_ROOT, ignore_errors=True)
  98. return {fileclient: {"__opts__": MOCKED_OPTS}}
  99. def setUp(self):
  100. self.file_client = fileclient.Client(self.master_opts)
  101. def tearDown(self):
  102. del self.file_client
  103. def test_file_list_emptydirs(self):
  104. """
  105. Ensure that the fileclient class won't allow a direct call to file_list_emptydirs()
  106. """
  107. with self.assertRaises(NotImplementedError):
  108. self.file_client.file_list_emptydirs()
  109. def test_get_file(self):
  110. """
  111. Ensure that the fileclient class won't allow a direct call to get_file()
  112. """
  113. with self.assertRaises(NotImplementedError):
  114. self.file_client.get_file(None)
  115. def test_get_file_client(self):
  116. minion_opts = self.get_temp_config("minion")
  117. minion_opts["file_client"] = "remote"
  118. with patch(
  119. "salt.fileclient.RemoteClient", MagicMock(return_value="remote_client")
  120. ):
  121. ret = fileclient.get_file_client(minion_opts)
  122. self.assertEqual("remote_client", ret)
  123. class FileclientCacheTest(
  124. TestCase, AdaptedConfigurationTestCaseMixin, LoaderModuleMockMixin
  125. ):
  126. """
  127. Tests for the fileclient caching. The LocalClient is the only thing we can
  128. test as it is the only way we can mock the fileclient (the tests run from
  129. the minion process, so the master cannot be mocked from test code).
  130. """
  131. def setup_loader_modules(self):
  132. self.FS_ROOT = os.path.join(RUNTIME_VARS.TMP, "fileclient_fs_root")
  133. self.CACHE_ROOT = os.path.join(RUNTIME_VARS.TMP, "fileclient_cache_root")
  134. self.MOCKED_OPTS = {
  135. "file_roots": _get_file_roots(self.FS_ROOT),
  136. "fileserver_backend": ["roots"],
  137. "cachedir": self.CACHE_ROOT,
  138. "file_client": "local",
  139. }
  140. self.addCleanup(shutil.rmtree, self.FS_ROOT, ignore_errors=True)
  141. self.addCleanup(shutil.rmtree, self.CACHE_ROOT, ignore_errors=True)
  142. return {fileclient: {"__opts__": self.MOCKED_OPTS}}
  143. def setUp(self):
  144. """
  145. No need to add a dummy foo.txt to muddy up the github repo, just make
  146. our own fileserver root on-the-fly.
  147. """
  148. def _new_dir(path):
  149. """
  150. Add a new dir at ``path`` using os.makedirs. If the directory
  151. already exists, remove it recursively and then try to create it
  152. again.
  153. """
  154. try:
  155. os.makedirs(path)
  156. except OSError as exc:
  157. if exc.errno == errno.EEXIST:
  158. # Just in case a previous test was interrupted, remove the
  159. # directory and try adding it again.
  160. shutil.rmtree(path)
  161. os.makedirs(path)
  162. else:
  163. raise
  164. # Crete the FS_ROOT
  165. for saltenv in SALTENVS:
  166. saltenv_root = os.path.join(self.FS_ROOT, saltenv)
  167. # Make sure we have a fresh root dir for this saltenv
  168. _new_dir(saltenv_root)
  169. path = os.path.join(saltenv_root, "foo.txt")
  170. with salt.utils.files.fopen(path, "w") as fp_:
  171. fp_.write("This is a test file in the '{0}' saltenv.\n".format(saltenv))
  172. subdir_abspath = os.path.join(saltenv_root, SUBDIR)
  173. os.makedirs(subdir_abspath)
  174. for subdir_file in SUBDIR_FILES:
  175. path = os.path.join(subdir_abspath, subdir_file)
  176. with salt.utils.files.fopen(path, "w") as fp_:
  177. fp_.write(
  178. "This is file '{0}' in subdir '{1} from saltenv "
  179. "'{2}'".format(subdir_file, SUBDIR, saltenv)
  180. )
  181. # Create the CACHE_ROOT
  182. _new_dir(self.CACHE_ROOT)
  183. def test_cache_dir(self):
  184. """
  185. Ensure entire directory is cached to correct location
  186. """
  187. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  188. patched_opts.update(self.MOCKED_OPTS)
  189. with patch.dict(fileclient.__opts__, patched_opts):
  190. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  191. for saltenv in SALTENVS:
  192. self.assertTrue(
  193. client.cache_dir(
  194. "salt://{0}".format(SUBDIR), saltenv, cachedir=None
  195. )
  196. )
  197. for subdir_file in SUBDIR_FILES:
  198. cache_loc = os.path.join(
  199. fileclient.__opts__["cachedir"],
  200. "files",
  201. saltenv,
  202. SUBDIR,
  203. subdir_file,
  204. )
  205. # Double check that the content of the cached file
  206. # identifies it as being from the correct saltenv. The
  207. # setUp function creates the file with the name of the
  208. # saltenv mentioned in the file, so a simple 'in' check is
  209. # sufficient here. If opening the file raises an exception,
  210. # this is a problem, so we are not catching the exception
  211. # and letting it be raised so that the test fails.
  212. with salt.utils.files.fopen(cache_loc) as fp_:
  213. content = fp_.read()
  214. log.debug("cache_loc = %s", cache_loc)
  215. log.debug("content = %s", content)
  216. self.assertTrue(subdir_file in content)
  217. self.assertTrue(SUBDIR in content)
  218. self.assertTrue(saltenv in content)
  219. def test_cache_dir_with_alternate_cachedir_and_absolute_path(self):
  220. """
  221. Ensure entire directory is cached to correct location when an alternate
  222. cachedir is specified and that cachedir is an absolute path
  223. """
  224. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  225. patched_opts.update(self.MOCKED_OPTS)
  226. alt_cachedir = os.path.join(RUNTIME_VARS.TMP, "abs_cachedir")
  227. with patch.dict(fileclient.__opts__, patched_opts):
  228. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  229. for saltenv in SALTENVS:
  230. self.assertTrue(
  231. client.cache_dir(
  232. "salt://{0}".format(SUBDIR), saltenv, cachedir=alt_cachedir
  233. )
  234. )
  235. for subdir_file in SUBDIR_FILES:
  236. cache_loc = os.path.join(
  237. alt_cachedir, "files", saltenv, SUBDIR, subdir_file
  238. )
  239. # Double check that the content of the cached file
  240. # identifies it as being from the correct saltenv. The
  241. # setUp function creates the file with the name of the
  242. # saltenv mentioned in the file, so a simple 'in' check is
  243. # sufficient here. If opening the file raises an exception,
  244. # this is a problem, so we are not catching the exception
  245. # and letting it be raised so that the test fails.
  246. with salt.utils.files.fopen(cache_loc) as fp_:
  247. content = fp_.read()
  248. log.debug("cache_loc = %s", cache_loc)
  249. log.debug("content = %s", content)
  250. self.assertTrue(subdir_file in content)
  251. self.assertTrue(SUBDIR in content)
  252. self.assertTrue(saltenv in content)
  253. def test_cache_dir_with_alternate_cachedir_and_relative_path(self):
  254. """
  255. Ensure entire directory is cached to correct location when an alternate
  256. cachedir is specified and that cachedir is a relative path
  257. """
  258. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  259. patched_opts.update(self.MOCKED_OPTS)
  260. alt_cachedir = "foo"
  261. with patch.dict(fileclient.__opts__, patched_opts):
  262. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  263. for saltenv in SALTENVS:
  264. self.assertTrue(
  265. client.cache_dir(
  266. "salt://{0}".format(SUBDIR), saltenv, cachedir=alt_cachedir
  267. )
  268. )
  269. for subdir_file in SUBDIR_FILES:
  270. cache_loc = os.path.join(
  271. fileclient.__opts__["cachedir"],
  272. alt_cachedir,
  273. "files",
  274. saltenv,
  275. SUBDIR,
  276. subdir_file,
  277. )
  278. # Double check that the content of the cached file
  279. # identifies it as being from the correct saltenv. The
  280. # setUp function creates the file with the name of the
  281. # saltenv mentioned in the file, so a simple 'in' check is
  282. # sufficient here. If opening the file raises an exception,
  283. # this is a problem, so we are not catching the exception
  284. # and letting it be raised so that the test fails.
  285. with salt.utils.files.fopen(cache_loc) as fp_:
  286. content = fp_.read()
  287. log.debug("cache_loc = %s", cache_loc)
  288. log.debug("content = %s", content)
  289. self.assertTrue(subdir_file in content)
  290. self.assertTrue(SUBDIR in content)
  291. self.assertTrue(saltenv in content)
  292. def test_cache_file(self):
  293. """
  294. Ensure file is cached to correct location
  295. """
  296. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  297. patched_opts.update(self.MOCKED_OPTS)
  298. with patch.dict(fileclient.__opts__, patched_opts):
  299. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  300. for saltenv in SALTENVS:
  301. self.assertTrue(
  302. client.cache_file("salt://foo.txt", saltenv, cachedir=None)
  303. )
  304. cache_loc = os.path.join(
  305. fileclient.__opts__["cachedir"], "files", saltenv, "foo.txt"
  306. )
  307. # Double check that the content of the cached file identifies
  308. # it as being from the correct saltenv. The setUp function
  309. # creates the file with the name of the saltenv mentioned in
  310. # the file, so a simple 'in' check is sufficient here. If
  311. # opening the file raises an exception, this is a problem, so
  312. # we are not catching the exception and letting it be raised so
  313. # that the test fails.
  314. with salt.utils.files.fopen(cache_loc) as fp_:
  315. content = fp_.read()
  316. log.debug("cache_loc = %s", cache_loc)
  317. log.debug("content = %s", content)
  318. self.assertTrue(saltenv in content)
  319. def test_cache_file_with_alternate_cachedir_and_absolute_path(self):
  320. """
  321. Ensure file is cached to correct location when an alternate cachedir is
  322. specified and that cachedir is an absolute path
  323. """
  324. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  325. patched_opts.update(self.MOCKED_OPTS)
  326. alt_cachedir = os.path.join(RUNTIME_VARS.TMP, "abs_cachedir")
  327. with patch.dict(fileclient.__opts__, patched_opts):
  328. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  329. for saltenv in SALTENVS:
  330. self.assertTrue(
  331. client.cache_file("salt://foo.txt", saltenv, cachedir=alt_cachedir)
  332. )
  333. cache_loc = os.path.join(alt_cachedir, "files", saltenv, "foo.txt")
  334. # Double check that the content of the cached file identifies
  335. # it as being from the correct saltenv. The setUp function
  336. # creates the file with the name of the saltenv mentioned in
  337. # the file, so a simple 'in' check is sufficient here. If
  338. # opening the file raises an exception, this is a problem, so
  339. # we are not catching the exception and letting it be raised so
  340. # that the test fails.
  341. with salt.utils.files.fopen(cache_loc) as fp_:
  342. content = fp_.read()
  343. log.debug("cache_loc = %s", cache_loc)
  344. log.debug("content = %s", content)
  345. self.assertTrue(saltenv in content)
  346. def test_cache_file_with_alternate_cachedir_and_relative_path(self):
  347. """
  348. Ensure file is cached to correct location when an alternate cachedir is
  349. specified and that cachedir is a relative path
  350. """
  351. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  352. patched_opts.update(self.MOCKED_OPTS)
  353. alt_cachedir = "foo"
  354. with patch.dict(fileclient.__opts__, patched_opts):
  355. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  356. for saltenv in SALTENVS:
  357. self.assertTrue(
  358. client.cache_file("salt://foo.txt", saltenv, cachedir=alt_cachedir)
  359. )
  360. cache_loc = os.path.join(
  361. fileclient.__opts__["cachedir"],
  362. alt_cachedir,
  363. "files",
  364. saltenv,
  365. "foo.txt",
  366. )
  367. # Double check that the content of the cached file identifies
  368. # it as being from the correct saltenv. The setUp function
  369. # creates the file with the name of the saltenv mentioned in
  370. # the file, so a simple 'in' check is sufficient here. If
  371. # opening the file raises an exception, this is a problem, so
  372. # we are not catching the exception and letting it be raised so
  373. # that the test fails.
  374. with salt.utils.files.fopen(cache_loc) as fp_:
  375. content = fp_.read()
  376. log.debug("cache_loc = %s", cache_loc)
  377. log.debug("content = %s", content)
  378. self.assertTrue(saltenv in content)
  379. def test_cache_dest(self):
  380. """
  381. Tests functionality for cache_dest
  382. """
  383. patched_opts = dict((x, y) for x, y in six.iteritems(self.minion_opts))
  384. patched_opts.update(self.MOCKED_OPTS)
  385. relpath = "foo.com/bar.txt"
  386. cachedir = self.minion_opts["cachedir"]
  387. def _external(saltenv="base"):
  388. return salt.utils.path.join(
  389. patched_opts["cachedir"], "extrn_files", saltenv, relpath
  390. )
  391. def _salt(saltenv="base"):
  392. return salt.utils.path.join(
  393. patched_opts["cachedir"], "files", saltenv, relpath
  394. )
  395. def _check(ret, expected):
  396. assert ret == expected, "{0} != {1}".format(ret, expected)
  397. with patch.dict(fileclient.__opts__, patched_opts):
  398. client = fileclient.get_file_client(fileclient.__opts__, pillar=False)
  399. _check(client.cache_dest("https://" + relpath), _external())
  400. _check(client.cache_dest("https://" + relpath, "dev"), _external("dev"))
  401. _check(client.cache_dest("salt://" + relpath), _salt())
  402. _check(client.cache_dest("salt://" + relpath, "dev"), _salt("dev"))
  403. _check(
  404. client.cache_dest("salt://" + relpath + "?saltenv=dev"), _salt("dev")
  405. )
  406. _check("/foo/bar", "/foo/bar")