123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- # -*- coding: utf-8 -*-
- '''
- :codeauthor: Erik Johnson <erik@saltstack.com>
- '''
- # Import Python libs
- from __future__ import absolute_import, print_function, unicode_literals
- import copy
- import errno
- import os
- import shutil
- import tempfile
- import textwrap
- import tornado.ioloop
- import logging
- import stat
- try:
- import pwd # pylint: disable=unused-import
- except ImportError:
- pass
- # Import Salt Testing Libs
- from tests.support.mixins import LoaderModuleMockMixin
- from tests.support.unit import TestCase, skipIf
- from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch
- from tests.support.paths import TMP, FILES
- # Import salt libs
- import salt.fileserver.gitfs as gitfs
- import salt.utils.files
- import salt.utils.platform
- import salt.utils.win_functions
- import salt.utils.yaml
- import salt.ext.six
- import salt.utils.gitfs
- from salt.utils.gitfs import (
- GITPYTHON_VERSION,
- GITPYTHON_MINVER,
- PYGIT2_VERSION,
- PYGIT2_MINVER,
- LIBGIT2_VERSION,
- LIBGIT2_MINVER
- )
- try:
- import git
- # We still need to use GitPython here for temp repo setup, so we do need to
- # actually import it. But we don't need import pygit2 in this module, we
- # can just use the LooseVersion instances imported along with
- # salt.utils.gitfs to check if we have a compatible version.
- HAS_GITPYTHON = GITPYTHON_VERSION >= GITPYTHON_MINVER
- except (ImportError, AttributeError):
- HAS_GITPYTHON = False
- try:
- HAS_PYGIT2 = PYGIT2_VERSION >= PYGIT2_MINVER \
- and LIBGIT2_VERSION >= LIBGIT2_MINVER
- except AttributeError:
- HAS_PYGIT2 = False
- log = logging.getLogger(__name__)
- TMP_SOCK_DIR = tempfile.mkdtemp(dir=TMP)
- TMP_REPO_DIR = os.path.join(TMP, 'gitfs_root')
- if salt.utils.platform.is_windows():
- TMP_REPO_DIR = TMP_REPO_DIR.replace('\\', '/')
- INTEGRATION_BASE_FILES = os.path.join(FILES, 'file', 'base')
- UNICODE_FILENAME = 'питон.txt'
- UNICODE_DIRNAME = UNICODE_ENVNAME = 'соль'
- TAG_NAME = 'mytag'
- OPTS = {
- 'sock_dir': TMP_SOCK_DIR,
- 'gitfs_remotes': ['file://' + TMP_REPO_DIR],
- 'gitfs_root': '',
- 'fileserver_backend': ['gitfs'],
- 'gitfs_base': 'master',
- 'fileserver_events': True,
- 'transport': 'zeromq',
- 'gitfs_mountpoint': '',
- 'gitfs_saltenv': [],
- 'gitfs_env_whitelist': [],
- 'gitfs_env_blacklist': [],
- 'gitfs_saltenv_whitelist': [],
- 'gitfs_saltenv_blacklist': [],
- 'gitfs_user': '',
- 'gitfs_password': '',
- 'gitfs_insecure_auth': False,
- 'gitfs_privkey': '',
- 'gitfs_pubkey': '',
- 'gitfs_passphrase': '',
- 'gitfs_refspecs': [
- '+refs/heads/*:refs/remotes/origin/*',
- '+refs/tags/*:refs/tags/*'
- ],
- 'gitfs_ssl_verify': True,
- 'gitfs_disable_saltenv_mapping': False,
- 'gitfs_ref_types': ['branch', 'tag', 'sha'],
- 'gitfs_update_interval': 60,
- '__role': 'master',
- }
- def _rmtree_error(func, path, excinfo):
- os.chmod(path, stat.S_IWRITE)
- func(path)
- def _clear_instance_map():
- try:
- del salt.utils.gitfs.GitFS.instance_map[tornado.ioloop.IOLoop.current()]
- except KeyError:
- pass
- @skipIf(not HAS_GITPYTHON, 'GitPython >= {0} required'.format(GITPYTHON_MINVER))
- class GitfsConfigTestCase(TestCase, LoaderModuleMockMixin):
- def setup_loader_modules(self):
- opts = copy.deepcopy(OPTS)
- opts['cachedir'] = self.tmp_cachedir
- opts['sock_dir'] = self.tmp_sock_dir
- return {
- gitfs: {
- '__opts__': opts,
- }
- }
- @classmethod
- def setUpClass(cls):
- # Clear the instance map so that we make sure to create a new instance
- # for this test class.
- _clear_instance_map()
- cls.tmp_cachedir = tempfile.mkdtemp(dir=TMP)
- cls.tmp_sock_dir = tempfile.mkdtemp(dir=TMP)
- @classmethod
- def tearDownClass(cls):
- '''
- Remove the temporary git repository and gitfs cache directory to ensure
- a clean environment for the other test class(es).
- '''
- for path in (cls.tmp_cachedir, cls.tmp_sock_dir):
- try:
- shutil.rmtree(path, onerror=_rmtree_error)
- except OSError as exc:
- if exc.errno == errno.EACCES:
- log.error("Access error removeing file %s", path)
- continue
- if exc.errno != errno.EEXIST:
- raise
- def test_per_saltenv_config(self):
- opts_override = textwrap.dedent('''
- gitfs_root: salt
- gitfs_saltenv:
- - baz:
- # when loaded, the "salt://" prefix will be removed
- - mountpoint: salt://baz_mountpoint
- - ref: baz_branch
- - root: baz_root
- gitfs_remotes:
- - file://tmp/repo1:
- - saltenv:
- - foo:
- - ref: foo_branch
- - root: foo_root
- - file://tmp/repo2:
- - mountpoint: repo2
- - saltenv:
- - baz:
- - mountpoint: abc
- ''')
- with patch.dict(gitfs.__opts__, salt.utils.yaml.safe_load(opts_override)):
- git_fs = salt.utils.gitfs.GitFS(
- gitfs.__opts__,
- gitfs.__opts__['gitfs_remotes'],
- per_remote_overrides=gitfs.PER_REMOTE_OVERRIDES,
- per_remote_only=gitfs.PER_REMOTE_ONLY)
- # repo1 (branch: foo)
- # The mountpoint should take the default (from gitfs_mountpoint), while
- # ref and root should take the per-saltenv params.
- self.assertEqual(git_fs.remotes[0].mountpoint('foo'), '')
- self.assertEqual(git_fs.remotes[0].ref('foo'), 'foo_branch')
- self.assertEqual(git_fs.remotes[0].root('foo'), 'foo_root')
- # repo1 (branch: bar)
- # The 'bar' branch does not have a per-saltenv configuration set, so
- # each of the below values should fall back to global values.
- self.assertEqual(git_fs.remotes[0].mountpoint('bar'), '')
- self.assertEqual(git_fs.remotes[0].ref('bar'), 'bar')
- self.assertEqual(git_fs.remotes[0].root('bar'), 'salt')
- # repo1 (branch: baz)
- # The 'baz' branch does not have a per-saltenv configuration set, but
- # it is defined in the gitfs_saltenv parameter, so the values
- # from that parameter should be returned.
- self.assertEqual(git_fs.remotes[0].mountpoint('baz'), 'baz_mountpoint')
- self.assertEqual(git_fs.remotes[0].ref('baz'), 'baz_branch')
- self.assertEqual(git_fs.remotes[0].root('baz'), 'baz_root')
- # repo2 (branch: foo)
- # The mountpoint should take the per-remote mountpoint value of
- # 'repo2', while ref and root should fall back to global values.
- self.assertEqual(git_fs.remotes[1].mountpoint('foo'), 'repo2')
- self.assertEqual(git_fs.remotes[1].ref('foo'), 'foo')
- self.assertEqual(git_fs.remotes[1].root('foo'), 'salt')
- # repo2 (branch: bar)
- # The 'bar' branch does not have a per-saltenv configuration set, so
- # the mountpoint should take the per-remote mountpoint value of
- # 'repo2', while ref and root should fall back to global values.
- self.assertEqual(git_fs.remotes[1].mountpoint('bar'), 'repo2')
- self.assertEqual(git_fs.remotes[1].ref('bar'), 'bar')
- self.assertEqual(git_fs.remotes[1].root('bar'), 'salt')
- # repo2 (branch: baz)
- # The 'baz' branch has the mountpoint configured as a per-saltenv
- # parameter. The other two should take the values defined in
- # gitfs_saltenv.
- self.assertEqual(git_fs.remotes[1].mountpoint('baz'), 'abc')
- self.assertEqual(git_fs.remotes[1].ref('baz'), 'baz_branch')
- self.assertEqual(git_fs.remotes[1].root('baz'), 'baz_root')
- LOAD = {'saltenv': 'base'}
- class GitFSTestFuncs(object):
- '''
- These are where the tests go, so that they can be run using both GitPython
- and pygit2.
- NOTE: The gitfs.update() has to happen AFTER the setUp is called. This is
- because running it inside the setUp will spawn a new singleton, which means
- that tests which need to mock the __opts__ will be too late; the setUp will
- have created a new singleton that will bypass our mocking. To ensure that
- our tests are reliable and correct, we want to make sure that each test
- uses a new gitfs object, allowing different manipulations of the opts to be
- tested.
- Therefore, keep the following in mind:
- 1. Each test needs to call gitfs.update() *after* any patching, and
- *before* calling the function being tested.
- 2. Do *NOT* move the gitfs.update() into the setUp.
- '''
- def test_file_list(self):
- gitfs.update()
- ret = gitfs.file_list(LOAD)
- self.assertIn('testfile', ret)
- self.assertIn(UNICODE_FILENAME, ret)
- # This function does not use os.sep, the Salt fileserver uses the
- # forward slash, hence it being explicitly used to join here.
- self.assertIn('/'.join((UNICODE_DIRNAME, 'foo.txt')), ret)
- def test_dir_list(self):
- gitfs.update()
- ret = gitfs.dir_list(LOAD)
- self.assertIn('grail', ret)
- self.assertIn(UNICODE_DIRNAME, ret)
- def test_envs(self):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- self.assertIn('base', ret)
- self.assertIn(UNICODE_ENVNAME, ret)
- self.assertIn(TAG_NAME, ret)
- def test_ref_types_global(self):
- '''
- Test the global gitfs_ref_types config option
- '''
- with patch.dict(gitfs.__opts__, {'gitfs_ref_types': ['branch']}):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- # Since we are restricting to branches only, the tag should not
- # appear in the envs list.
- self.assertIn('base', ret)
- self.assertIn(UNICODE_ENVNAME, ret)
- self.assertNotIn(TAG_NAME, ret)
- def test_ref_types_per_remote(self):
- '''
- Test the per_remote ref_types config option, using a different
- ref_types setting than the global test.
- '''
- remotes = [{'file://' + TMP_REPO_DIR: [{'ref_types': ['tag']}]}]
- with patch.dict(gitfs.__opts__, {'gitfs_remotes': remotes}):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- # Since we are restricting to tags only, the tag should appear in
- # the envs list, but the branches should not.
- self.assertNotIn('base', ret)
- self.assertNotIn(UNICODE_ENVNAME, ret)
- self.assertIn(TAG_NAME, ret)
- def test_disable_saltenv_mapping_global_with_mapping_defined_globally(self):
- '''
- Test the global gitfs_disable_saltenv_mapping config option, combined
- with the per-saltenv mapping being defined in the global gitfs_saltenv
- option.
- '''
- opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
- gitfs_disable_saltenv_mapping: True
- gitfs_saltenv:
- - foo:
- - ref: somebranch
- '''))
- with patch.dict(gitfs.__opts__, opts):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- # Since we are restricting to tags only, the tag should appear in
- # the envs list, but the branches should not.
- self.assertEqual(ret, ['base', 'foo'])
- def test_disable_saltenv_mapping_global_with_mapping_defined_per_remote(self):
- '''
- Test the global gitfs_disable_saltenv_mapping config option, combined
- with the per-saltenv mapping being defined in the remote itself via the
- "saltenv" per-remote option.
- '''
- opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
- gitfs_disable_saltenv_mapping: True
- gitfs_remotes:
- - file://{0}:
- - saltenv:
- - bar:
- - ref: somebranch
- '''.format(TMP_REPO_DIR)))
- with patch.dict(gitfs.__opts__, opts):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- # Since we are restricting to tags only, the tag should appear in
- # the envs list, but the branches should not.
- self.assertEqual(ret, ['bar', 'base'])
- def test_disable_saltenv_mapping_per_remote_with_mapping_defined_globally(self):
- '''
- Test the per-remote disable_saltenv_mapping config option, combined
- with the per-saltenv mapping being defined in the global gitfs_saltenv
- option.
- '''
- opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
- gitfs_remotes:
- - file://{0}:
- - disable_saltenv_mapping: True
- gitfs_saltenv:
- - hello:
- - ref: somebranch
- '''.format(TMP_REPO_DIR)))
- with patch.dict(gitfs.__opts__, opts):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- # Since we are restricting to tags only, the tag should appear in
- # the envs list, but the branches should not.
- self.assertEqual(ret, ['base', 'hello'])
- def test_disable_saltenv_mapping_per_remote_with_mapping_defined_per_remote(self):
- '''
- Test the per-remote disable_saltenv_mapping config option, combined
- with the per-saltenv mapping being defined in the remote itself via the
- "saltenv" per-remote option.
- '''
- opts = salt.utils.yaml.safe_load(textwrap.dedent('''\
- gitfs_remotes:
- - file://{0}:
- - disable_saltenv_mapping: True
- - saltenv:
- - world:
- - ref: somebranch
- '''.format(TMP_REPO_DIR)))
- with patch.dict(gitfs.__opts__, opts):
- gitfs.update()
- ret = gitfs.envs(ignore_cache=True)
- # Since we are restricting to tags only, the tag should appear in
- # the envs list, but the branches should not.
- self.assertEqual(ret, ['base', 'world'])
- class GitFSTestBase(object):
- @classmethod
- def setUpClass(cls):
- cls.tmp_cachedir = tempfile.mkdtemp(dir=TMP)
- cls.tmp_sock_dir = tempfile.mkdtemp(dir=TMP)
- try:
- shutil.rmtree(TMP_REPO_DIR)
- except OSError as exc:
- if exc.errno == errno.EACCES:
- log.error("Access error removeing file %s", TMP_REPO_DIR)
- elif exc.errno != errno.ENOENT:
- raise
- shutil.copytree(INTEGRATION_BASE_FILES, TMP_REPO_DIR + '/')
- repo = git.Repo.init(TMP_REPO_DIR)
- username_key = str('USERNAME')
- orig_username = os.environ.get(username_key)
- try:
- if username_key not in os.environ:
- try:
- if salt.utils.platform.is_windows():
- os.environ[username_key] = \
- salt.utils.win_functions.get_current_user()
- else:
- os.environ[username_key] = \
- pwd.getpwuid(os.geteuid()).pw_name
- except AttributeError:
- log.error(
- 'Unable to get effective username, falling back to '
- '\'root\'.'
- )
- os.environ[username_key] = str('root')
- repo.index.add([x for x in os.listdir(TMP_REPO_DIR)
- if x != '.git'])
- repo.index.commit('Test')
- # Add another branch with unicode characters in the name
- repo.create_head(UNICODE_ENVNAME, 'HEAD')
- # Add a tag
- repo.create_tag(TAG_NAME, 'HEAD')
- # Older GitPython versions do not have a close method.
- if hasattr(repo, 'close'):
- repo.close()
- finally:
- if orig_username is not None:
- os.environ[username_key] = orig_username
- else:
- os.environ.pop(username_key, None)
- @classmethod
- def tearDownClass(cls):
- '''
- Remove the temporary git repository and gitfs cache directory to ensure
- a clean environment for the other test class(es).
- '''
- for path in (cls.tmp_cachedir, cls.tmp_sock_dir, TMP_REPO_DIR):
- try:
- salt.utils.files.rm_rf(path)
- except OSError as exc:
- if exc.errno == errno.EACCES:
- log.error("Access error removeing file %s", path)
- elif exc.errno != errno.EEXIST:
- raise
- def setUp(self):
- '''
- We don't want to check in another .git dir into GH because that just
- gets messy. Instead, we'll create a temporary repo on the fly for the
- tests to examine.
- Also ensure we A) don't re-use the singleton, and B) that the cachedirs
- are cleared. This keeps these performance enhancements from affecting
- the results of subsequent tests.
- '''
- if not gitfs.__virtual__():
- self.skipTest("GitFS could not be loaded. Skipping GitFS tests!")
- _clear_instance_map()
- for subdir in ('gitfs', 'file_lists'):
- try:
- salt.utils.files.rm_rf(os.path.join(self.tmp_cachedir, subdir))
- except OSError as exc:
- if exc.errno == errno.EACCES:
- log.warning("Access error removeing file %s", os.path.join(self.tmp_cachedir, subdir))
- continue
- if exc.errno != errno.ENOENT:
- raise
- if salt.ext.six.PY3 and salt.utils.platform.is_windows():
- self.setUpClass()
- self.setup_loader_modules()
- @skipIf(not HAS_GITPYTHON, 'GitPython >= {0} required'.format(GITPYTHON_MINVER))
- @skipIf(NO_MOCK, NO_MOCK_REASON)
- class GitPythonTest(GitFSTestBase, GitFSTestFuncs, TestCase, LoaderModuleMockMixin):
- def setup_loader_modules(self):
- opts = copy.deepcopy(OPTS)
- opts['cachedir'] = self.tmp_cachedir
- opts['sock_dir'] = self.tmp_sock_dir
- opts['gitfs_provider'] = 'gitpython'
- return {
- gitfs: {
- '__opts__': opts,
- }
- }
- @skipIf(not HAS_GITPYTHON, 'GitPython >= {0} required for temp repo setup'.format(GITPYTHON_MINVER))
- @skipIf(not HAS_PYGIT2, 'pygit2 >= {0} and libgit2 >= {1} required'.format(PYGIT2_MINVER, LIBGIT2_MINVER))
- @skipIf(NO_MOCK, NO_MOCK_REASON)
- class Pygit2Test(GitFSTestBase, GitFSTestFuncs, TestCase, LoaderModuleMockMixin):
- def setup_loader_modules(self):
- opts = copy.deepcopy(OPTS)
- opts['cachedir'] = self.tmp_cachedir
- opts['sock_dir'] = self.tmp_sock_dir
- opts['gitfs_provider'] = 'pygit2'
- return {
- gitfs: {
- '__opts__': opts,
- }
- }
|