test_gitfs.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Erik Johnson <erik@saltstack.com>
  4. '''
  5. # Import Python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import copy
  8. import errno
  9. import os
  10. import shutil
  11. import tempfile
  12. import textwrap
  13. import tornado.ioloop
  14. import logging
  15. import stat
  16. try:
  17. import pwd # pylint: disable=unused-import
  18. except ImportError:
  19. pass
  20. # Import Salt Testing Libs
  21. from tests.support.mixins import LoaderModuleMockMixin
  22. from tests.support.unit import TestCase, skipIf
  23. from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch
  24. from tests.support.paths import TMP, FILES
  25. # Import salt libs
  26. import salt.fileserver.gitfs as gitfs
  27. import salt.utils.files
  28. import salt.utils.platform
  29. import salt.utils.win_functions
  30. import salt.utils.yaml
  31. import salt.ext.six
  32. import salt.utils.gitfs
  33. from salt.utils.gitfs import (
  34. GITPYTHON_VERSION,
  35. GITPYTHON_MINVER,
  36. PYGIT2_VERSION,
  37. PYGIT2_MINVER,
  38. LIBGIT2_VERSION,
  39. LIBGIT2_MINVER
  40. )
  41. try:
  42. import git
  43. # We still need to use GitPython here for temp repo setup, so we do need to
  44. # actually import it. But we don't need import pygit2 in this module, we
  45. # can just use the LooseVersion instances imported along with
  46. # salt.utils.gitfs to check if we have a compatible version.
  47. HAS_GITPYTHON = GITPYTHON_VERSION >= GITPYTHON_MINVER
  48. except (ImportError, AttributeError):
  49. HAS_GITPYTHON = False
  50. try:
  51. HAS_PYGIT2 = PYGIT2_VERSION >= PYGIT2_MINVER \
  52. and LIBGIT2_VERSION >= LIBGIT2_MINVER
  53. except AttributeError:
  54. HAS_PYGIT2 = False
  55. log = logging.getLogger(__name__)
  56. TMP_SOCK_DIR = tempfile.mkdtemp(dir=TMP)
  57. TMP_REPO_DIR = os.path.join(TMP, 'gitfs_root')
  58. TMP_FILE_URI = 'file://'
  59. if salt.utils.platform.is_windows():
  60. TMP_REPO_DIR = TMP_REPO_DIR.replace('\\', '/')
  61. TMP_FILE_URI = TMP_FILE_URI + '/'
  62. INTEGRATION_BASE_FILES = os.path.join(FILES, 'file', 'base')
  63. UNICODE_FILENAME = 'питон.txt'
  64. UNICODE_DIRNAME = UNICODE_ENVNAME = 'соль'
  65. TAG_NAME = 'mytag'
  66. OPTS = {
  67. 'sock_dir': TMP_SOCK_DIR,
  68. 'gitfs_remotes': [TMP_FILE_URI + TMP_REPO_DIR],
  69. 'gitfs_root': '',
  70. 'fileserver_backend': ['gitfs'],
  71. 'gitfs_base': 'master',
  72. 'fileserver_events': True,
  73. 'transport': 'zeromq',
  74. 'gitfs_mountpoint': '',
  75. 'gitfs_saltenv': [],
  76. 'gitfs_env_whitelist': [],
  77. 'gitfs_env_blacklist': [],
  78. 'gitfs_saltenv_whitelist': [],
  79. 'gitfs_saltenv_blacklist': [],
  80. 'gitfs_user': '',
  81. 'gitfs_password': '',
  82. 'gitfs_insecure_auth': False,
  83. 'gitfs_privkey': '',
  84. 'gitfs_pubkey': '',
  85. 'gitfs_passphrase': '',
  86. 'gitfs_refspecs': [
  87. '+refs/heads/*:refs/remotes/origin/*',
  88. '+refs/tags/*:refs/tags/*'
  89. ],
  90. 'gitfs_ssl_verify': True,
  91. 'gitfs_disable_saltenv_mapping': False,
  92. 'gitfs_ref_types': ['branch', 'tag', 'sha'],
  93. 'gitfs_update_interval': 60,
  94. '__role': 'master',
  95. }
  96. def _rmtree_error(func, path, excinfo):
  97. os.chmod(path, stat.S_IWRITE)
  98. func(path)
  99. def _clear_instance_map():
  100. try:
  101. del salt.utils.gitfs.GitFS.instance_map[tornado.ioloop.IOLoop.current()]
  102. except KeyError:
  103. pass
  104. @skipIf(not HAS_GITPYTHON, 'GitPython >= {0} required'.format(GITPYTHON_MINVER))
  105. class GitfsConfigTestCase(TestCase, LoaderModuleMockMixin):
  106. def setup_loader_modules(self):
  107. opts = copy.deepcopy(OPTS)
  108. opts['cachedir'] = self.tmp_cachedir
  109. opts['sock_dir'] = self.tmp_sock_dir
  110. return {
  111. gitfs: {
  112. '__opts__': opts,
  113. }
  114. }
  115. @classmethod
  116. def setUpClass(cls):
  117. # Clear the instance map so that we make sure to create a new instance
  118. # for this test class.
  119. _clear_instance_map()
  120. cls.tmp_cachedir = tempfile.mkdtemp(dir=TMP)
  121. cls.tmp_sock_dir = tempfile.mkdtemp(dir=TMP)
  122. @classmethod
  123. def tearDownClass(cls):
  124. '''
  125. Remove the temporary git repository and gitfs cache directory to ensure
  126. a clean environment for the other test class(es).
  127. '''
  128. for path in (cls.tmp_cachedir, cls.tmp_sock_dir):
  129. try:
  130. shutil.rmtree(path, onerror=_rmtree_error)
  131. except OSError as exc:
  132. if exc.errno == errno.EACCES:
  133. log.error("Access error removeing file %s", path)
  134. continue
  135. if exc.errno != errno.EEXIST:
  136. raise
  137. def test_per_saltenv_config(self):
  138. opts_override = textwrap.dedent('''
  139. gitfs_root: salt
  140. gitfs_saltenv:
  141. - baz:
  142. # when loaded, the "salt://" prefix will be removed
  143. - mountpoint: salt://baz_mountpoint
  144. - ref: baz_branch
  145. - root: baz_root
  146. gitfs_remotes:
  147. - {0}tmp/repo1:
  148. - saltenv:
  149. - foo:
  150. - ref: foo_branch
  151. - root: foo_root
  152. - {0}tmp/repo2:
  153. - mountpoint: repo2
  154. - saltenv:
  155. - baz:
  156. - mountpoint: abc
  157. '''.format(TMP_FILE_URI))
  158. with patch.dict(gitfs.__opts__, salt.utils.yaml.safe_load(opts_override)):
  159. git_fs = salt.utils.gitfs.GitFS(
  160. gitfs.__opts__,
  161. gitfs.__opts__['gitfs_remotes'],
  162. per_remote_overrides=gitfs.PER_REMOTE_OVERRIDES,
  163. per_remote_only=gitfs.PER_REMOTE_ONLY)
  164. # repo1 (branch: foo)
  165. # The mountpoint should take the default (from gitfs_mountpoint), while
  166. # ref and root should take the per-saltenv params.
  167. self.assertEqual(git_fs.remotes[0].mountpoint('foo'), '')
  168. self.assertEqual(git_fs.remotes[0].ref('foo'), 'foo_branch')
  169. self.assertEqual(git_fs.remotes[0].root('foo'), 'foo_root')
  170. # repo1 (branch: bar)
  171. # The 'bar' branch does not have a per-saltenv configuration set, so
  172. # each of the below values should fall back to global values.
  173. self.assertEqual(git_fs.remotes[0].mountpoint('bar'), '')
  174. self.assertEqual(git_fs.remotes[0].ref('bar'), 'bar')
  175. self.assertEqual(git_fs.remotes[0].root('bar'), 'salt')
  176. # repo1 (branch: baz)
  177. # The 'baz' branch does not have a per-saltenv configuration set, but
  178. # it is defined in the gitfs_saltenv parameter, so the values
  179. # from that parameter should be returned.
  180. self.assertEqual(git_fs.remotes[0].mountpoint('baz'), 'baz_mountpoint')
  181. self.assertEqual(git_fs.remotes[0].ref('baz'), 'baz_branch')
  182. self.assertEqual(git_fs.remotes[0].root('baz'), 'baz_root')
  183. # repo2 (branch: foo)
  184. # The mountpoint should take the per-remote mountpoint value of
  185. # 'repo2', while ref and root should fall back to global values.
  186. self.assertEqual(git_fs.remotes[1].mountpoint('foo'), 'repo2')
  187. self.assertEqual(git_fs.remotes[1].ref('foo'), 'foo')
  188. self.assertEqual(git_fs.remotes[1].root('foo'), 'salt')
  189. # repo2 (branch: bar)
  190. # The 'bar' branch does not have a per-saltenv configuration set, so
  191. # the mountpoint should take the per-remote mountpoint value of
  192. # 'repo2', while ref and root should fall back to global values.
  193. self.assertEqual(git_fs.remotes[1].mountpoint('bar'), 'repo2')
  194. self.assertEqual(git_fs.remotes[1].ref('bar'), 'bar')
  195. self.assertEqual(git_fs.remotes[1].root('bar'), 'salt')
  196. # repo2 (branch: baz)
  197. # The 'baz' branch has the mountpoint configured as a per-saltenv
  198. # parameter. The other two should take the values defined in
  199. # gitfs_saltenv.
  200. self.assertEqual(git_fs.remotes[1].mountpoint('baz'), 'abc')
  201. self.assertEqual(git_fs.remotes[1].ref('baz'), 'baz_branch')
  202. self.assertEqual(git_fs.remotes[1].root('baz'), 'baz_root')
  203. LOAD = {'saltenv': 'base'}
  204. class GitFSTestFuncs(object):
  205. '''
  206. These are where the tests go, so that they can be run using both GitPython
  207. and pygit2.
  208. NOTE: The gitfs.update() has to happen AFTER the setUp is called. This is
  209. because running it inside the setUp will spawn a new singleton, which means
  210. that tests which need to mock the __opts__ will be too late; the setUp will
  211. have created a new singleton that will bypass our mocking. To ensure that
  212. our tests are reliable and correct, we want to make sure that each test
  213. uses a new gitfs object, allowing different manipulations of the opts to be
  214. tested.
  215. Therefore, keep the following in mind:
  216. 1. Each test needs to call gitfs.update() *after* any patching, and
  217. *before* calling the function being tested.
  218. 2. Do *NOT* move the gitfs.update() into the setUp.
  219. '''
  220. def test_file_list(self):
  221. gitfs.update()
  222. ret = gitfs.file_list(LOAD)
  223. self.assertIn('testfile', ret)
  224. self.assertIn(UNICODE_FILENAME, ret)
  225. # This function does not use os.sep, the Salt fileserver uses the
  226. # forward slash, hence it being explicitly used to join here.
  227. self.assertIn('/'.join((UNICODE_DIRNAME, 'foo.txt')), ret)
  228. def test_dir_list(self):
  229. gitfs.update()
  230. ret = gitfs.dir_list(LOAD)
  231. self.assertIn('grail', ret)
  232. self.assertIn(UNICODE_DIRNAME, ret)
  233. def test_envs(self):
  234. gitfs.update()
  235. ret = gitfs.envs(ignore_cache=True)
  236. self.assertIn('base', ret)
  237. self.assertIn(UNICODE_ENVNAME, ret)
  238. self.assertIn(TAG_NAME, ret)
  239. def test_ref_types_global(self):
  240. '''
  241. Test the global gitfs_ref_types config option
  242. '''
  243. with patch.dict(gitfs.__opts__, {'gitfs_ref_types': ['branch']}):
  244. gitfs.update()
  245. ret = gitfs.envs(ignore_cache=True)
  246. # Since we are restricting to branches only, the tag should not
  247. # appear in the envs list.
  248. self.assertIn('base', ret)
  249. self.assertIn(UNICODE_ENVNAME, ret)
  250. self.assertNotIn(TAG_NAME, ret)
  251. def test_ref_types_per_remote(self):
  252. '''
  253. Test the per_remote ref_types config option, using a different
  254. ref_types setting than the global test.
  255. '''
  256. remotes = [{TMP_FILE_URI + TMP_REPO_DIR: [{'ref_types': ['tag']}]}]
  257. with patch.dict(gitfs.__opts__, {'gitfs_remotes': remotes}):
  258. gitfs.update()
  259. ret = gitfs.envs(ignore_cache=True)
  260. # Since we are restricting to tags only, the tag should appear in
  261. # the envs list, but the branches should not.
  262. self.assertNotIn('base', ret)
  263. self.assertNotIn(UNICODE_ENVNAME, ret)
  264. self.assertIn(TAG_NAME, ret)
  265. def test_disable_saltenv_mapping_global_with_mapping_defined_globally(self):
  266. '''
  267. Test the global gitfs_disable_saltenv_mapping config option, combined
  268. with the per-saltenv mapping being defined in the global gitfs_saltenv
  269. option.
  270. '''
  271. opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
  272. gitfs_disable_saltenv_mapping: True
  273. gitfs_saltenv:
  274. - foo:
  275. - ref: somebranch
  276. '''))
  277. with patch.dict(gitfs.__opts__, opts):
  278. gitfs.update()
  279. ret = gitfs.envs(ignore_cache=True)
  280. # Since we are restricting to tags only, the tag should appear in
  281. # the envs list, but the branches should not.
  282. self.assertEqual(ret, ['base', 'foo'])
  283. def test_disable_saltenv_mapping_global_with_mapping_defined_per_remote(self):
  284. '''
  285. Test the global gitfs_disable_saltenv_mapping config option, combined
  286. with the per-saltenv mapping being defined in the remote itself via the
  287. "saltenv" per-remote option.
  288. '''
  289. opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
  290. gitfs_disable_saltenv_mapping: True
  291. gitfs_remotes:
  292. - {0}:
  293. - saltenv:
  294. - bar:
  295. - ref: somebranch
  296. '''.format(TMP_FILE_URI + TMP_REPO_DIR)))
  297. with patch.dict(gitfs.__opts__, opts):
  298. gitfs.update()
  299. ret = gitfs.envs(ignore_cache=True)
  300. # Since we are restricting to tags only, the tag should appear in
  301. # the envs list, but the branches should not.
  302. self.assertEqual(ret, ['bar', 'base'])
  303. def test_disable_saltenv_mapping_per_remote_with_mapping_defined_globally(self):
  304. '''
  305. Test the per-remote disable_saltenv_mapping config option, combined
  306. with the per-saltenv mapping being defined in the global gitfs_saltenv
  307. option.
  308. '''
  309. opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
  310. gitfs_remotes:
  311. - {0}:
  312. - disable_saltenv_mapping: True
  313. gitfs_saltenv:
  314. - hello:
  315. - ref: somebranch
  316. '''.format(TMP_FILE_URI + TMP_REPO_DIR)))
  317. with patch.dict(gitfs.__opts__, opts):
  318. gitfs.update()
  319. ret = gitfs.envs(ignore_cache=True)
  320. # Since we are restricting to tags only, the tag should appear in
  321. # the envs list, but the branches should not.
  322. self.assertEqual(ret, ['base', 'hello'])
  323. def test_disable_saltenv_mapping_per_remote_with_mapping_defined_per_remote(self):
  324. '''
  325. Test the per-remote disable_saltenv_mapping config option, combined
  326. with the per-saltenv mapping being defined in the remote itself via the
  327. "saltenv" per-remote option.
  328. '''
  329. opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
  330. gitfs_remotes:
  331. - {0}:
  332. - disable_saltenv_mapping: True
  333. - saltenv:
  334. - world:
  335. - ref: somebranch
  336. '''.format(TMP_FILE_URI + TMP_REPO_DIR)))
  337. with patch.dict(gitfs.__opts__, opts):
  338. gitfs.update()
  339. ret = gitfs.envs(ignore_cache=True)
  340. # Since we are restricting to tags only, the tag should appear in
  341. # the envs list, but the branches should not.
  342. self.assertEqual(ret, ['base', 'world'])
  343. class GitFSTestBase(object):
  344. @classmethod
  345. def setUpClass(cls):
  346. cls.tmp_cachedir = tempfile.mkdtemp(dir=TMP)
  347. cls.tmp_sock_dir = tempfile.mkdtemp(dir=TMP)
  348. try:
  349. shutil.rmtree(TMP_REPO_DIR)
  350. except OSError as exc:
  351. if exc.errno == errno.EACCES:
  352. log.error("Access error removeing file %s", TMP_REPO_DIR)
  353. elif exc.errno != errno.ENOENT:
  354. raise
  355. shutil.copytree(INTEGRATION_BASE_FILES, TMP_REPO_DIR + '/')
  356. repo = git.Repo.init(TMP_REPO_DIR)
  357. username_key = str('USERNAME')
  358. orig_username = os.environ.get(username_key)
  359. environ_copy = os.environ.copy()
  360. try:
  361. if username_key not in os.environ:
  362. try:
  363. if salt.utils.platform.is_windows():
  364. os.environ[username_key] = \
  365. salt.utils.win_functions.get_current_user()
  366. else:
  367. os.environ[username_key] = \
  368. pwd.getpwuid(os.geteuid()).pw_name
  369. except AttributeError:
  370. log.error(
  371. 'Unable to get effective username, falling back to '
  372. '\'root\'.'
  373. )
  374. os.environ[username_key] = str('root')
  375. repo.index.add([x for x in os.listdir(TMP_REPO_DIR)
  376. if x != '.git'])
  377. repo.index.commit('Test')
  378. # Add another branch with unicode characters in the name
  379. repo.create_head(UNICODE_ENVNAME, 'HEAD')
  380. # Add a tag
  381. repo.create_tag(TAG_NAME, 'HEAD')
  382. # Older GitPython versions do not have a close method.
  383. if hasattr(repo, 'close'):
  384. repo.close()
  385. finally:
  386. os.environ.clear()
  387. os.environ.update(environ_copy)
  388. @classmethod
  389. def tearDownClass(cls):
  390. '''
  391. Remove the temporary git repository and gitfs cache directory to ensure
  392. a clean environment for the other test class(es).
  393. '''
  394. for path in (cls.tmp_cachedir, cls.tmp_sock_dir, TMP_REPO_DIR):
  395. try:
  396. salt.utils.files.rm_rf(path)
  397. except OSError as exc:
  398. if exc.errno == errno.EACCES:
  399. log.error("Access error removeing file %s", path)
  400. elif exc.errno != errno.EEXIST:
  401. raise
  402. def setUp(self):
  403. '''
  404. We don't want to check in another .git dir into GH because that just
  405. gets messy. Instead, we'll create a temporary repo on the fly for the
  406. tests to examine.
  407. Also ensure we A) don't re-use the singleton, and B) that the cachedirs
  408. are cleared. This keeps these performance enhancements from affecting
  409. the results of subsequent tests.
  410. '''
  411. if not gitfs.__virtual__():
  412. self.skipTest("GitFS could not be loaded. Skipping GitFS tests!")
  413. _clear_instance_map()
  414. for subdir in ('gitfs', 'file_lists'):
  415. try:
  416. salt.utils.files.rm_rf(os.path.join(self.tmp_cachedir, subdir))
  417. except OSError as exc:
  418. if exc.errno == errno.EACCES:
  419. log.warning("Access error removeing file %s", os.path.join(self.tmp_cachedir, subdir))
  420. continue
  421. if exc.errno != errno.ENOENT:
  422. raise
  423. if salt.ext.six.PY3 and salt.utils.platform.is_windows():
  424. self.setUpClass()
  425. self.setup_loader_modules()
  426. @skipIf(not HAS_GITPYTHON, 'GitPython >= {0} required'.format(GITPYTHON_MINVER))
  427. @skipIf(NO_MOCK, NO_MOCK_REASON)
  428. class GitPythonTest(GitFSTestBase, GitFSTestFuncs, TestCase, LoaderModuleMockMixin):
  429. def setup_loader_modules(self):
  430. opts = copy.deepcopy(OPTS)
  431. opts['cachedir'] = self.tmp_cachedir
  432. opts['sock_dir'] = self.tmp_sock_dir
  433. opts['gitfs_provider'] = 'gitpython'
  434. return {
  435. gitfs: {
  436. '__opts__': opts,
  437. }
  438. }
  439. @skipIf(not HAS_GITPYTHON, 'GitPython >= {0} required for temp repo setup'.format(GITPYTHON_MINVER))
  440. @skipIf(not HAS_PYGIT2, 'pygit2 >= {0} and libgit2 >= {1} required'.format(PYGIT2_MINVER, LIBGIT2_MINVER))
  441. @skipIf(salt.utils.platform.is_windows(), 'Skip Pygit2 on windows, due to pygit2 access error on windows')
  442. @skipIf(NO_MOCK, NO_MOCK_REASON)
  443. class Pygit2Test(GitFSTestBase, GitFSTestFuncs, TestCase, LoaderModuleMockMixin):
  444. def setup_loader_modules(self):
  445. opts = copy.deepcopy(OPTS)
  446. opts['cachedir'] = self.tmp_cachedir
  447. opts['sock_dir'] = self.tmp_sock_dir
  448. opts['gitfs_provider'] = 'pygit2'
  449. return {
  450. gitfs: {
  451. '__opts__': opts,
  452. }
  453. }