test_gitfs.py 19 KB

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