test_fileclient.py 18 KB

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