123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- # -*- coding: utf-8 -*-
- '''
- Base classes for gitfs/git_pillar integration tests
- '''
- # Import python libs
- from __future__ import absolute_import, print_function, unicode_literals
- import copy
- import errno
- import logging
- import os
- import psutil
- import shutil
- import signal
- import tempfile
- import textwrap
- import time
- # Import Salt libs
- import salt.utils.files
- import salt.utils.path
- import salt.utils.yaml
- from salt.fileserver import gitfs
- from salt.pillar import git_pillar
- from salt.ext.six.moves import range # pylint: disable=redefined-builtin
- # Import Salt Testing libs
- from tests.support.case import ModuleCase
- from tests.support.mixins import LoaderModuleMockMixin, SaltReturnAssertsMixin
- from tests.support.paths import TMP
- from tests.support.helpers import (
- get_unused_localhost_port,
- requires_system_grains,
- )
- from tests.support.mock import patch
- log = logging.getLogger(__name__)
- USERNAME = 'gitpillaruser'
- PASSWORD = 'saltrules'
- _OPTS = {
- '__role': 'minion',
- 'environment': None,
- 'pillarenv': None,
- 'hash_type': 'sha256',
- 'file_roots': {},
- 'state_top': 'top.sls',
- 'state_top_saltenv': None,
- 'renderer': 'yaml_jinja',
- 'renderer_whitelist': [],
- 'renderer_blacklist': [],
- 'pillar_merge_lists': False,
- 'git_pillar_base': 'master',
- 'git_pillar_branch': 'master',
- 'git_pillar_env': '',
- 'git_pillar_root': '',
- 'git_pillar_ssl_verify': True,
- 'git_pillar_global_lock': True,
- 'git_pillar_user': '',
- 'git_pillar_password': '',
- 'git_pillar_insecure_auth': False,
- 'git_pillar_privkey': '',
- 'git_pillar_pubkey': '',
- 'git_pillar_passphrase': '',
- 'git_pillar_refspecs': [
- '+refs/heads/*:refs/remotes/origin/*',
- '+refs/tags/*:refs/tags/*',
- ],
- 'git_pillar_includes': True,
- }
- PROC_TIMEOUT = 10
- class ProcessManager(object):
- '''
- Functions used both to set up self-contained SSH/HTTP servers for testing
- '''
- wait = 10
- def find_proc(self, name=None, search=None):
- def _search(proc):
- return any([search in x for x in proc.cmdline()])
- if name is None and search is None:
- raise ValueError('one of name or search is required')
- for proc in psutil.process_iter():
- if name is not None:
- try:
- if search is None:
- if name in proc.name():
- return proc
- elif name in proc.name() and _search(proc):
- return proc
- except psutil.NoSuchProcess:
- # Whichever process we are interrogating is no longer alive.
- # Skip it and keep searching.
- continue
- else:
- if _search(proc):
- return proc
- return None
- def wait_proc(self, name=None, search=None, timeout=PROC_TIMEOUT):
- for idx in range(1, self.wait + 1):
- proc = self.find_proc(name=name, search=search)
- if proc is not None:
- return proc
- else:
- if idx != self.wait:
- log.debug(
- 'Waiting for %s process (%d of %d)',
- name, idx, self.wait
- )
- time.sleep(1)
- else:
- log.debug(
- 'Failed fo find %s process after %d seconds',
- name, self.wait
- )
- raise Exception(
- 'Unable to find {0} process running from temp config file {1} '
- 'using psutil'.format(name, search)
- )
- class SSHDMixin(ModuleCase, ProcessManager, SaltReturnAssertsMixin):
- '''
- Functions to stand up an SSHD server to serve up git repos for tests.
- '''
- sshd_proc = None
- @classmethod
- def prep_server(cls):
- cls.sshd_config_dir = tempfile.mkdtemp(dir=TMP)
- cls.sshd_config = os.path.join(cls.sshd_config_dir, 'sshd_config')
- cls.sshd_port = get_unused_localhost_port()
- cls.url = 'ssh://{username}@127.0.0.1:{port}/~/repo.git'.format(
- username=cls.username,
- port=cls.sshd_port)
- cls.url_extra_repo = 'ssh://{username}@127.0.0.1:{port}/~/extra_repo.git'.format(
- username=cls.username,
- port=cls.sshd_port)
- home = '/root/.ssh'
- cls.ext_opts = {
- 'url': cls.url,
- 'url_extra_repo': cls.url_extra_repo,
- 'privkey_nopass': os.path.join(home, cls.id_rsa_nopass),
- 'pubkey_nopass': os.path.join(home, cls.id_rsa_nopass + '.pub'),
- 'privkey_withpass': os.path.join(home, cls.id_rsa_withpass),
- 'pubkey_withpass': os.path.join(home, cls.id_rsa_withpass + '.pub'),
- 'passphrase': cls.passphrase}
- def spawn_server(self):
- ret = self.run_function(
- 'state.apply',
- mods='git_pillar.ssh',
- pillar={'git_pillar': {'git_ssh': self.git_ssh,
- 'id_rsa_nopass': self.id_rsa_nopass,
- 'id_rsa_withpass': self.id_rsa_withpass,
- 'sshd_bin': self.sshd_bin,
- 'sshd_port': self.sshd_port,
- 'sshd_config_dir': self.sshd_config_dir,
- 'master_user': self.master_opts['user'],
- 'user': self.username}}
- )
- try:
- self.sshd_proc = self.wait_proc(name='sshd',
- search=self.sshd_config)
- finally:
- # Do the assert after we check for the PID so that we can track
- # it regardless of whether or not something else in the SLS
- # failed (but the SSH server still started).
- self.assertSaltTrueReturn(ret)
- class WebserverMixin(ModuleCase, ProcessManager, SaltReturnAssertsMixin):
- '''
- Functions to stand up an nginx + uWSGI + git-http-backend webserver to
- serve up git repos for tests.
- '''
- nginx_proc = uwsgi_proc = None
- @classmethod
- def prep_server(cls):
- '''
- Set up all the webserver paths. Designed to be run once in a
- setUpClass function.
- '''
- cls.root_dir = tempfile.mkdtemp(dir=TMP)
- cls.config_dir = os.path.join(cls.root_dir, 'config')
- cls.nginx_conf = os.path.join(cls.config_dir, 'nginx.conf')
- cls.uwsgi_conf = os.path.join(cls.config_dir, 'uwsgi.yml')
- cls.git_dir = os.path.join(cls.root_dir, 'git')
- cls.repo_dir = os.path.join(cls.git_dir, 'repos')
- cls.venv_dir = os.path.join(cls.root_dir, 'venv')
- cls.uwsgi_bin = os.path.join(cls.venv_dir, 'bin', 'uwsgi')
- cls.nginx_port = cls.uwsgi_port = get_unused_localhost_port()
- while cls.uwsgi_port == cls.nginx_port:
- # Ensure we don't hit a corner case in which two sucessive calls to
- # get_unused_localhost_port() return identical port numbers.
- cls.uwsgi_port = get_unused_localhost_port()
- cls.url = 'http://127.0.0.1:{port}/repo.git'.format(port=cls.nginx_port)
- cls.url_extra_repo = 'http://127.0.0.1:{port}/extra_repo.git'.format(port=cls.nginx_port)
- cls.ext_opts = {'url': cls.url, 'url_extra_repo': cls.url_extra_repo}
- # Add auth params if present (if so this will trigger the spawned
- # server to turn on HTTP basic auth).
- for credential_param in ('user', 'password'):
- if hasattr(cls, credential_param):
- cls.ext_opts[credential_param] = getattr(cls, credential_param)
- @requires_system_grains
- def spawn_server(self, grains):
- auth_enabled = hasattr(self, 'username') and hasattr(self, 'password')
- pillar = {'git_pillar': {'config_dir': self.config_dir,
- 'git_dir': self.git_dir,
- 'venv_dir': self.venv_dir,
- 'root_dir': self.root_dir,
- 'nginx_port': self.nginx_port,
- 'uwsgi_port': self.uwsgi_port,
- 'auth_enabled': auth_enabled}}
- # Different libexec dir for git backend on Debian-based systems
- git_core = '/usr/libexec/git-core' \
- if grains['os_family'] in ('RedHat') \
- else '/usr/lib/git-core'
- pillar['git_pillar']['git-http-backend'] = os.path.join(
- git_core,
- 'git-http-backend')
- ret = self.run_function(
- 'state.apply',
- mods='git_pillar.http',
- pillar=pillar)
- if not os.path.exists(pillar['git_pillar']['git-http-backend']):
- self.fail(
- '{0} not found. Either git is not installed, or the test '
- 'class needs to be updated.'.format(
- pillar['git_pillar']['git-http-backend']
- )
- )
- try:
- self.nginx_proc = self.wait_proc(name='nginx',
- search=self.nginx_conf)
- self.uwsgi_proc = self.wait_proc(name='uwsgi',
- search=self.uwsgi_conf)
- finally:
- # Do the assert after we check for the PID so that we can track
- # it regardless of whether or not something else in the SLS
- # failed (but the webserver still started).
- self.assertSaltTrueReturn(ret)
- class GitTestBase(ModuleCase):
- '''
- Base class for all gitfs/git_pillar tests. Must be subclassed and paired
- with either SSHDMixin or WebserverMixin to provide the server.
- '''
- case = port = bare_repo = base_extra_repo = admin_repo = admin_extra_repo = None
- maxDiff = None
- git_opts = '-c user.name="Foo Bar" -c user.email=foo@bar.com'
- ext_opts = {}
- # We need to temporarily skip pygit2 tests on EL7 until the EPEL packager
- # updates pygit2 to bring it up-to-date with libgit2.
- @requires_system_grains
- def is_el7(self, grains):
- return grains['os_family'] == 'RedHat' and grains['osmajorrelease'] == 7
- # Cent OS 6 has too old a version of git to handle the make_repo code, as
- # it lacks the -c option for git itself.
- @requires_system_grains
- def is_pre_el7(self, grains):
- return grains['os_family'] == 'RedHat' and grains['osmajorrelease'] < 7
- @classmethod
- def setUpClass(cls):
- cls.prep_server()
- def setUp(self):
- # Make the test class available to the tearDownClass so we can clean up
- # after ourselves. This (and the gated block below) prevent us from
- # needing to spend the extra time creating an ssh server and user and
- # then tear them down separately for each test.
- self.update_class(self)
- if self.is_pre_el7(): # pylint: disable=E1120
- self.skipTest(
- 'RHEL < 7 has too old a version of git to run these tests')
- @classmethod
- def update_class(cls, case):
- '''
- Make the test class available to the tearDownClass. Note that this
- cannot be defined in a parent class and inherited, as this will cause
- the parent class to be modified.
- '''
- if getattr(cls, 'case') is None:
- setattr(cls, 'case', case)
- def make_repo(self, root_dir, user='root'):
- raise NotImplementedError()
- class GitFSTestBase(GitTestBase, LoaderModuleMockMixin):
- '''
- Base class for all gitfs tests
- '''
- @requires_system_grains
- def setup_loader_modules(self, grains): # pylint: disable=W0221
- return {
- gitfs: {
- '__opts__': copy.copy(_OPTS),
- '__grains__': grains,
- }
- }
- def make_repo(self, root_dir, user='root'):
- raise NotImplementedError()
- class GitPillarTestBase(GitTestBase, LoaderModuleMockMixin):
- '''
- Base class for all git_pillar tests
- '''
- @requires_system_grains
- def setup_loader_modules(self, grains): # pylint: disable=W0221
- return {
- git_pillar: {
- '__opts__': copy.copy(_OPTS),
- '__grains__': grains,
- }
- }
- def get_pillar(self, ext_pillar_conf):
- '''
- Run git_pillar with the specified configuration
- '''
- cachedir = tempfile.mkdtemp(dir=TMP)
- self.addCleanup(shutil.rmtree, cachedir, ignore_errors=True)
- ext_pillar_opts = {'optimization_order': [0, 1, 2]}
- ext_pillar_opts.update(
- salt.utils.yaml.safe_load(
- ext_pillar_conf.format(
- cachedir=cachedir,
- extmods=os.path.join(cachedir, 'extmods'),
- **self.ext_opts
- )
- )
- )
- with patch.dict(git_pillar.__opts__, ext_pillar_opts):
- return git_pillar.ext_pillar(
- 'minion',
- {},
- *ext_pillar_opts['ext_pillar'][0]['git']
- )
- def make_repo(self, root_dir, user='root'):
- self.bare_repo = os.path.join(root_dir, 'repo.git')
- self.admin_repo = os.path.join(root_dir, 'admin')
- for dirname in (self.bare_repo, self.admin_repo):
- shutil.rmtree(dirname, ignore_errors=True)
- # Create bare repo
- self.run_function(
- 'git.init',
- [self.bare_repo],
- user=user,
- bare=True)
- # Clone bare repo
- self.run_function(
- 'git.clone',
- [self.admin_repo],
- url=self.bare_repo,
- user=user)
- def _push(branch, message):
- self.run_function(
- 'git.add',
- [self.admin_repo, '.'],
- user=user)
- self.run_function(
- 'git.commit',
- [self.admin_repo, message],
- user=user,
- git_opts=self.git_opts,
- )
- self.run_function(
- 'git.push',
- [self.admin_repo],
- remote='origin',
- ref=branch,
- user=user,
- )
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- base:
- '*':
- - foo
- '''))
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'foo.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- branch: master
- mylist:
- - master
- mydict:
- master: True
- nested_list:
- - master
- nested_dict:
- master: True
- '''))
- # Add another file to be referenced using git_pillar_includes
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'bar.sls'), 'w') as fp_:
- fp_.write('included_pillar: True\n')
- # Add another file in subdir
- os.mkdir(os.path.join(self.admin_repo, 'subdir'))
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'subdir', 'bar.sls'), 'w') as fp_:
- fp_.write('from_subdir: True\n')
- _push('master', 'initial commit')
- # Do the same with different values for "dev" branch
- self.run_function(
- 'git.checkout',
- [self.admin_repo],
- user=user,
- opts='-b dev')
- # The bar.sls shouldn't be in any branch but master
- self.run_function(
- 'git.rm',
- [self.admin_repo, 'bar.sls'],
- user=user)
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- dev:
- '*':
- - foo
- '''))
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'foo.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- branch: dev
- mylist:
- - dev
- mydict:
- dev: True
- nested_list:
- - dev
- nested_dict:
- dev: True
- '''))
- _push('dev', 'add dev branch')
- # Create just a top file in a separate repo, to be mapped to the base
- # env and referenced using git_pillar_includes
- self.run_function(
- 'git.checkout',
- [self.admin_repo],
- user=user,
- opts='-b top_only')
- # The top.sls should be the only file in this branch
- self.run_function(
- 'git.rm',
- [self.admin_repo, 'foo.sls', os.path.join('subdir', 'bar.sls')],
- user=user)
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- base:
- '*':
- - bar
- '''))
- _push('top_only', 'add top_only branch')
- # Create just another top file in a separate repo, to be mapped to the base
- # env and including mounted.bar
- self.run_function(
- 'git.checkout',
- [self.admin_repo],
- user=user,
- opts='-b top_mounted')
- # The top.sls should be the only file in this branch
- with salt.utils.files.fopen(
- os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- base:
- '*':
- - mounted.bar
- '''))
- _push('top_mounted', 'add top_mounted branch')
- def make_extra_repo(self, root_dir, user='root'):
- self.bare_extra_repo = os.path.join(root_dir, 'extra_repo.git')
- self.admin_extra_repo = os.path.join(root_dir, 'admin_extra')
- for dirname in (self.bare_extra_repo, self.admin_extra_repo):
- shutil.rmtree(dirname, ignore_errors=True)
- # Create bare extra repo
- self.run_function(
- 'git.init',
- [self.bare_extra_repo],
- user=user,
- bare=True)
- # Clone bare repo
- self.run_function(
- 'git.clone',
- [self.admin_extra_repo],
- url=self.bare_extra_repo,
- user=user)
- def _push(branch, message):
- self.run_function(
- 'git.add',
- [self.admin_extra_repo, '.'],
- user=user)
- self.run_function(
- 'git.commit',
- [self.admin_extra_repo, message],
- user=user,
- git_opts=self.git_opts,
- )
- self.run_function(
- 'git.push',
- [self.admin_extra_repo],
- remote='origin',
- ref=branch,
- user=user,
- )
- with salt.utils.files.fopen(
- os.path.join(self.admin_extra_repo, 'top.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- "{{saltenv}}":
- '*':
- - motd
- - nowhere.foo
- '''))
- with salt.utils.files.fopen(
- os.path.join(self.admin_extra_repo, 'motd.sls'), 'w') as fp_:
- fp_.write(textwrap.dedent('''\
- motd: The force will be with you. Always.
- '''))
- _push('master', 'initial commit')
- class GitPillarSSHTestBase(GitPillarTestBase, SSHDMixin):
- '''
- Base class for GitPython and Pygit2 SSH tests
- '''
- id_rsa_nopass = id_rsa_withpass = None
- git_ssh = '/tmp/git_ssh'
- @classmethod
- def tearDownClass(cls):
- if cls.case is None:
- return
- if cls.case.sshd_proc is not None:
- cls.case.sshd_proc.send_signal(signal.SIGTERM)
- cls.case.run_state('user.absent', name=cls.username, purge=True)
- for dirname in (cls.sshd_config_dir, cls.case.admin_repo,
- cls.case.bare_repo):
- if dirname is not None:
- shutil.rmtree(dirname, ignore_errors=True)
- ssh_dir = os.path.expanduser('~/.ssh')
- for filename in (cls.id_rsa_nopass,
- cls.id_rsa_nopass + '.pub',
- cls.id_rsa_withpass,
- cls.id_rsa_withpass + '.pub',
- cls.git_ssh):
- try:
- os.remove(os.path.join(ssh_dir, filename))
- except OSError as exc:
- if exc.errno != errno.ENOENT:
- raise
- def setUp(self):
- '''
- Create the SSH server and user, and create the git repo
- '''
- super(GitPillarSSHTestBase, self).setUp()
- self.sshd_proc = self.find_proc(name='sshd',
- search=self.sshd_config)
- self.sshd_bin = salt.utils.path.which('sshd')
- if self.sshd_proc is None:
- self.spawn_server()
- known_hosts_ret = self.run_function(
- 'ssh.set_known_host',
- user=self.master_opts['user'],
- hostname='127.0.0.1',
- port=self.sshd_port,
- enc='ssh-rsa',
- fingerprint='fd:6f:7f:5d:06:6b:f2:06:0d:26:93:9e:5a:b5:19:46',
- hash_known_hosts=False,
- fingerprint_hash_type='md5',
- )
- if 'error' in known_hosts_ret:
- raise Exception(
- 'Failed to add key to {0} user\'s known_hosts '
- 'file: {1}'.format(
- self.master_opts['user'],
- known_hosts_ret['error']
- )
- )
- root_dir = os.path.expanduser('~{0}'.format(self.username))
- if root_dir.startswith('~'):
- self.fail(
- 'Unable to resolve homedir for user \'{0}\''.format(
- self.username
- )
- )
- self.make_repo(root_dir, user=self.username)
- self.make_extra_repo(root_dir, user=self.username)
- def get_pillar(self, ext_pillar_conf):
- '''
- Wrap the parent class' get_pillar() func in logic that temporarily
- changes the GIT_SSH to use our custom script, ensuring that the
- passphraselsess key is used to auth without needing to modify the root
- user's ssh config file.
- '''
- def cleanup_environ(environ):
- os.environ.clear()
- os.environ.update(environ)
- self.addCleanup(cleanup_environ, os.environ.copy())
- os.environ['GIT_SSH'] = self.git_ssh
- return super(GitPillarSSHTestBase, self).get_pillar(ext_pillar_conf)
- class GitPillarHTTPTestBase(GitPillarTestBase, WebserverMixin):
- '''
- Base class for GitPython and Pygit2 HTTP tests
- '''
- @classmethod
- def tearDownClass(cls):
- for proc in (cls.case.nginx_proc, cls.case.uwsgi_proc):
- if proc is not None:
- try:
- proc.send_signal(signal.SIGQUIT)
- except psutil.NoSuchProcess:
- pass
- shutil.rmtree(cls.root_dir, ignore_errors=True)
- def setUp(self):
- '''
- Create and start the webserver, and create the git repo
- '''
- super(GitPillarHTTPTestBase, self).setUp()
- self.nginx_proc = self.find_proc(name='nginx',
- search=self.nginx_conf)
- self.uwsgi_proc = self.find_proc(name='uwsgi',
- search=self.uwsgi_conf)
- if self.nginx_proc is None and self.uwsgi_proc is None:
- self.spawn_server() # pylint: disable=E1120
- self.make_repo(self.repo_dir)
- self.make_extra_repo(self.repo_dir)
|