gitfs.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Base classes for gitfs/git_pillar integration tests
  4. '''
  5. # Import python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import sys
  8. import copy
  9. import errno
  10. import logging
  11. import os
  12. import pprint
  13. import shutil
  14. import tempfile
  15. import textwrap
  16. import threading
  17. import subprocess
  18. import time
  19. # Import 3rd-party libs
  20. import psutil
  21. # Import Salt libs
  22. import salt.utils.files
  23. import salt.utils.path
  24. import salt.utils.yaml
  25. import salt.ext.six as six
  26. from salt.fileserver import gitfs
  27. from salt.pillar import git_pillar
  28. # Import Salt Testing libs
  29. from tests.support.case import ModuleCase
  30. from tests.support.unit import SkipTest
  31. from tests.support.mixins import AdaptedConfigurationTestCaseMixin, LoaderModuleMockMixin, SaltReturnAssertsMixin
  32. from tests.support.helpers import get_unused_localhost_port, requires_system_grains
  33. from tests.support.runtests import RUNTIME_VARS
  34. from tests.support.mock import patch
  35. from pytestsalt.utils import SaltDaemonScriptBase as _SaltDaemonScriptBase, terminate_process
  36. log = logging.getLogger(__name__)
  37. USERNAME = 'gitpillaruser'
  38. PASSWORD = 'saltrules'
  39. _OPTS = {
  40. '__role': 'minion',
  41. 'environment': None,
  42. 'pillarenv': None,
  43. 'hash_type': 'sha256',
  44. 'file_roots': {},
  45. 'state_top': 'top.sls',
  46. 'state_top_saltenv': None,
  47. 'renderer': 'yaml_jinja',
  48. 'renderer_whitelist': [],
  49. 'renderer_blacklist': [],
  50. 'pillar_merge_lists': False,
  51. 'git_pillar_base': 'master',
  52. 'git_pillar_branch': 'master',
  53. 'git_pillar_env': '',
  54. 'git_pillar_root': '',
  55. 'git_pillar_ssl_verify': True,
  56. 'git_pillar_global_lock': True,
  57. 'git_pillar_user': '',
  58. 'git_pillar_password': '',
  59. 'git_pillar_insecure_auth': False,
  60. 'git_pillar_privkey': '',
  61. 'git_pillar_pubkey': '',
  62. 'git_pillar_passphrase': '',
  63. 'git_pillar_refspecs': [
  64. '+refs/heads/*:refs/remotes/origin/*',
  65. '+refs/tags/*:refs/tags/*',
  66. ],
  67. 'git_pillar_includes': True,
  68. }
  69. PROC_TIMEOUT = 10
  70. def start_daemon(daemon_cli_script_name,
  71. daemon_config_dir,
  72. daemon_check_port,
  73. daemon_class,
  74. fail_hard=False,
  75. start_timeout=10,
  76. slow_stop=True,
  77. environ=None,
  78. cwd=None,
  79. max_attempts=3,
  80. **kwargs):
  81. '''
  82. Returns a running process daemon
  83. '''
  84. log.info('[%s] Starting %s', daemon_class.log_prefix, daemon_class.__name__)
  85. attempts = 0
  86. process = None
  87. while attempts <= max_attempts: # pylint: disable=too-many-nested-blocks
  88. attempts += 1
  89. process = daemon_class(str(daemon_config_dir),
  90. daemon_check_port,
  91. cli_script_name=daemon_cli_script_name,
  92. slow_stop=slow_stop,
  93. environ=environ,
  94. cwd=cwd,
  95. **kwargs)
  96. process.start()
  97. if process.is_alive():
  98. try:
  99. connectable = process.wait_until_running(timeout=start_timeout)
  100. if connectable is False:
  101. connectable = process.wait_until_running(timeout=start_timeout/2)
  102. if connectable is False:
  103. process.terminate()
  104. if attempts >= max_attempts:
  105. raise AssertionError(
  106. 'The {} has failed to confirm running status '
  107. 'after {} attempts'.format(daemon_class.__name__, attempts))
  108. continue
  109. except Exception as exc: # pylint: disable=broad-except
  110. log.exception('[%s] %s', daemon_class.log_prefix, exc, exc_info=True)
  111. terminate_process(process.pid, kill_children=True, slow_stop=slow_stop)
  112. if attempts >= max_attempts:
  113. raise AssertionError(str(exc))
  114. continue
  115. # A little breathing before returning the process
  116. time.sleep(0.5)
  117. log.info(
  118. '[%s] The %s is running after %d attempts',
  119. daemon_class.log_prefix,
  120. daemon_class.__name__,
  121. attempts
  122. )
  123. break
  124. else:
  125. terminate_process(process.pid, kill_children=True, slow_stop=slow_stop)
  126. time.sleep(1)
  127. continue
  128. else:
  129. if process is not None:
  130. terminate_process(process.pid, kill_children=True, slow_stop=slow_stop)
  131. raise AssertionError(
  132. 'The {} has failed to start after {} attempts'.format(
  133. daemon_class.__name__,
  134. attempts-1
  135. )
  136. )
  137. return process
  138. class SaltDaemonScriptBase(_SaltDaemonScriptBase):
  139. def start(self):
  140. '''
  141. Start the daemon subprocess
  142. '''
  143. # Late import
  144. log.info('[%s][%s] Starting DAEMON in CWD: %s', self.log_prefix, self.cli_display_name, self.cwd)
  145. proc_args = [
  146. self.get_script_path(self.cli_script_name)
  147. ] + self.get_base_script_args() + self.get_script_args()
  148. if sys.platform.startswith('win'):
  149. # Windows needs the python executable to come first
  150. proc_args.insert(0, sys.executable)
  151. log.info('[%s][%s] Running \'%s\'...', self.log_prefix, self.cli_display_name, ' '.join(proc_args))
  152. self.init_terminal(proc_args,
  153. env=self.environ,
  154. cwd=self.cwd,
  155. stdout=subprocess.PIPE,
  156. stderr=subprocess.PIPE)
  157. self._running.set()
  158. if self._process_cli_output_in_thread:
  159. process_output_thread = threading.Thread(target=self._process_output_in_thread)
  160. process_output_thread.daemon = True
  161. process_output_thread.start()
  162. return True
  163. class UwsgiDaemon(SaltDaemonScriptBase):
  164. log_prefix = 'uWSGI'
  165. def __init__(self,
  166. config_dir,
  167. uwsgi_port,
  168. cli_script_name='uwsgi',
  169. **kwargs):
  170. super(UwsgiDaemon, self).__init__(None, # request
  171. {'check_port': uwsgi_port}, # config
  172. config_dir, # config_dir
  173. None, # bin_dir_path
  174. self.__class__.log_prefix, # log_prefix
  175. cli_script_name=cli_script_name,
  176. **kwargs)
  177. def get_script_path(self, script_name):
  178. '''
  179. Returns the path to the script to run
  180. '''
  181. return script_name
  182. def get_base_script_args(self):
  183. '''
  184. Returns any additional arguments to pass to the CLI script
  185. '''
  186. return ['--yaml', os.path.join(self.config_dir, 'uwsgi.yml')]
  187. def get_check_ports(self):
  188. '''
  189. Return a list of ports to check against to ensure the daemon is running
  190. '''
  191. return [self.config['check_port']]
  192. def get_salt_run_event_listener(self):
  193. # Remove this method once pytest-salt get's past 2019.7.20
  194. # Just return a class with a terminate method
  195. class EV(object):
  196. def terminate(self):
  197. pass
  198. return EV()
  199. class NginxDaemon(SaltDaemonScriptBase):
  200. log_prefix = 'Nginx'
  201. def __init__(self,
  202. config_dir,
  203. nginx_port,
  204. cli_script_name='nginx',
  205. **kwargs):
  206. super(NginxDaemon, self).__init__(None, # request
  207. {'check_port': nginx_port}, # config
  208. config_dir, # config_dir
  209. None, # bin_dir_path
  210. self.__class__.log_prefix, # log_prefix
  211. cli_script_name=cli_script_name,
  212. **kwargs)
  213. def get_script_path(self, script_name):
  214. '''
  215. Returns the path to the script to run
  216. '''
  217. return script_name
  218. def get_base_script_args(self):
  219. '''
  220. Returns any additional arguments to pass to the CLI script
  221. '''
  222. return ['-c', os.path.join(self.config_dir, 'nginx.conf')]
  223. def get_check_ports(self):
  224. '''
  225. Return a list of ports to check against to ensure the daemon is running
  226. '''
  227. return [self.config['check_port']]
  228. def get_salt_run_event_listener(self):
  229. # Remove this method once pytest-salt get's past 2019.7.20
  230. # Just return a class with a terminate method
  231. class EV(object):
  232. def terminate(self):
  233. pass
  234. return EV()
  235. class SshdDaemon(SaltDaemonScriptBase):
  236. log_prefix = 'SSHD'
  237. def __init__(self,
  238. config_dir,
  239. sshd_port,
  240. cli_script_name='sshd',
  241. **kwargs):
  242. super(SshdDaemon, self).__init__(None, # request
  243. {'check_port': sshd_port}, # config
  244. config_dir, # config_dir
  245. None, # bin_dir_path
  246. self.__class__.log_prefix, # log_prefix
  247. cli_script_name=cli_script_name,
  248. **kwargs)
  249. def get_script_path(self, script_name):
  250. '''
  251. Returns the path to the script to run
  252. '''
  253. return script_name
  254. def get_base_script_args(self):
  255. '''
  256. Returns any additional arguments to pass to the CLI script
  257. '''
  258. return ['-D', '-e', '-f', os.path.join(self.config_dir, 'sshd_config')]
  259. def get_check_ports(self):
  260. '''
  261. Return a list of ports to check against to ensure the daemon is running
  262. '''
  263. return [self.config['check_port']]
  264. def get_salt_run_event_listener(self):
  265. # Remove this method once pytest-salt get's past 2019.7.20
  266. # Just return a class with a terminate method
  267. class EV(object):
  268. def terminate(self):
  269. pass
  270. return EV()
  271. class SaltClientMixin(ModuleCase):
  272. client = None
  273. @classmethod
  274. @requires_system_grains
  275. def setUpClass(cls, grains=None):
  276. # Cent OS 6 has too old a version of git to handle the make_repo code, as
  277. # it lacks the -c option for git itself.
  278. make_repo = getattr(cls, 'make_repo', None)
  279. if callable(make_repo) and grains['os_family'] == 'RedHat' and grains['osmajorrelease'] < 7:
  280. raise SkipTest('RHEL < 7 has too old a version of git to run these tests')
  281. # Late import
  282. import salt.client
  283. mopts = AdaptedConfigurationTestCaseMixin.get_config('master', from_scratch=True)
  284. cls.user = mopts['user']
  285. cls.client = salt.client.get_local_client(mopts=mopts)
  286. @classmethod
  287. def tearDownClass(cls):
  288. cls.client = None
  289. @classmethod
  290. def cls_run_function(cls, function, *args, **kwargs):
  291. orig = cls.client.cmd('minion',
  292. function,
  293. arg=args,
  294. timeout=300,
  295. kwarg=kwargs)
  296. return orig['minion']
  297. class SSHDMixin(SaltClientMixin, SaltReturnAssertsMixin):
  298. '''
  299. Functions to stand up an SSHD server to serve up git repos for tests.
  300. '''
  301. sshd_proc = None
  302. prep_states_ran = False
  303. known_hosts_setup = False
  304. @classmethod
  305. def setUpClass(cls): # pylint: disable=arguments-differ
  306. super(SSHDMixin, cls).setUpClass()
  307. try:
  308. log.info('%s: prep_server()', cls.__name__)
  309. cls.sshd_bin = salt.utils.path.which('sshd')
  310. cls.sshd_config_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  311. cls.sshd_config = os.path.join(cls.sshd_config_dir, 'sshd_config')
  312. cls.sshd_port = get_unused_localhost_port()
  313. cls.url = 'ssh://{username}@127.0.0.1:{port}/~/repo.git'.format(
  314. username=cls.username,
  315. port=cls.sshd_port)
  316. cls.url_extra_repo = 'ssh://{username}@127.0.0.1:{port}/~/extra_repo.git'.format(
  317. username=cls.username,
  318. port=cls.sshd_port)
  319. home = '/root/.ssh'
  320. cls.ext_opts = {
  321. 'url': cls.url,
  322. 'url_extra_repo': cls.url_extra_repo,
  323. 'privkey_nopass': os.path.join(home, cls.id_rsa_nopass),
  324. 'pubkey_nopass': os.path.join(home, cls.id_rsa_nopass + '.pub'),
  325. 'privkey_withpass': os.path.join(home, cls.id_rsa_withpass),
  326. 'pubkey_withpass': os.path.join(home, cls.id_rsa_withpass + '.pub'),
  327. 'passphrase': cls.passphrase}
  328. if cls.prep_states_ran is False:
  329. ret = cls.cls_run_function(
  330. 'state.apply',
  331. mods='git_pillar.ssh',
  332. pillar={'git_pillar': {'git_ssh': cls.git_ssh,
  333. 'id_rsa_nopass': cls.id_rsa_nopass,
  334. 'id_rsa_withpass': cls.id_rsa_withpass,
  335. 'sshd_bin': cls.sshd_bin,
  336. 'sshd_port': cls.sshd_port,
  337. 'sshd_config_dir': cls.sshd_config_dir,
  338. 'master_user': cls.user,
  339. 'user': cls.username}}
  340. )
  341. assert next(six.itervalues(ret))['result'] is True
  342. cls.prep_states_ran = True
  343. log.info('%s: States applied', cls.__name__)
  344. if cls.sshd_proc is not None:
  345. if not psutil.pid_exists(cls.sshd_proc.pid):
  346. log.info('%s: sshd started but appears to be dead now. Will try to restart it.',
  347. cls.__name__)
  348. cls.sshd_proc = None
  349. if cls.sshd_proc is None:
  350. cls.sshd_proc = start_daemon(cls.sshd_bin, cls.sshd_config_dir, cls.sshd_port, SshdDaemon)
  351. log.info('%s: sshd started', cls.__name__)
  352. except AssertionError:
  353. cls.tearDownClass()
  354. six.reraise(*sys.exc_info())
  355. if cls.known_hosts_setup is False:
  356. known_hosts_ret = cls.cls_run_function(
  357. 'ssh.set_known_host',
  358. user=cls.user,
  359. hostname='127.0.0.1',
  360. port=cls.sshd_port,
  361. enc='ssh-rsa',
  362. fingerprint='fd:6f:7f:5d:06:6b:f2:06:0d:26:93:9e:5a:b5:19:46',
  363. hash_known_hosts=False,
  364. fingerprint_hash_type='md5',
  365. )
  366. if 'error' in known_hosts_ret:
  367. cls.tearDownClass()
  368. raise AssertionError(
  369. 'Failed to add key to {0} user\'s known_hosts '
  370. 'file: {1}'.format(
  371. cls.master_opts['user'],
  372. known_hosts_ret['error']
  373. )
  374. )
  375. cls.known_hosts_setup = True
  376. @classmethod
  377. def tearDownClass(cls):
  378. if cls.sshd_proc is not None:
  379. log.info('[%s] Stopping %s', cls.sshd_proc.log_prefix, cls.sshd_proc.__class__.__name__)
  380. terminate_process(cls.sshd_proc.pid, kill_children=True, slow_stop=True)
  381. log.info('[%s] %s stopped', cls.sshd_proc.log_prefix, cls.sshd_proc.__class__.__name__)
  382. cls.sshd_proc = None
  383. if cls.prep_states_ran:
  384. ret = cls.cls_run_function('state.single', 'user.absent', name=cls.username, purge=True)
  385. try:
  386. if ret and 'minion' in ret:
  387. ret_data = next(six.itervalues(ret['minion']))
  388. if not ret_data['result']:
  389. log.warning('Failed to delete test account %s', cls.username)
  390. except KeyError:
  391. log.warning('Failed to delete test account. Salt return:\n%s',
  392. pprint.pformat(ret))
  393. shutil.rmtree(cls.sshd_config_dir, ignore_errors=True)
  394. ssh_dir = os.path.expanduser('~/.ssh')
  395. for filename in (cls.id_rsa_nopass,
  396. cls.id_rsa_nopass + '.pub',
  397. cls.id_rsa_withpass,
  398. cls.id_rsa_withpass + '.pub',
  399. cls.git_ssh):
  400. try:
  401. os.remove(os.path.join(ssh_dir, filename))
  402. except OSError as exc:
  403. if exc.errno != errno.ENOENT:
  404. raise
  405. super(SSHDMixin, cls).tearDownClass()
  406. class WebserverMixin(SaltClientMixin, SaltReturnAssertsMixin):
  407. '''
  408. Functions to stand up an nginx + uWSGI + git-http-backend webserver to
  409. serve up git repos for tests.
  410. '''
  411. nginx_proc = uwsgi_proc = None
  412. prep_states_ran = False
  413. @classmethod
  414. def setUpClass(cls): # pylint: disable=arguments-differ
  415. '''
  416. Set up all the webserver paths. Designed to be run once in a
  417. setUpClass function.
  418. '''
  419. super(WebserverMixin, cls).setUpClass()
  420. cls.root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  421. cls.config_dir = os.path.join(cls.root_dir, 'config')
  422. cls.nginx_conf = os.path.join(cls.config_dir, 'nginx.conf')
  423. cls.uwsgi_conf = os.path.join(cls.config_dir, 'uwsgi.yml')
  424. cls.git_dir = os.path.join(cls.root_dir, 'git')
  425. cls.repo_dir = os.path.join(cls.git_dir, 'repos')
  426. cls.venv_dir = os.path.join(cls.root_dir, 'venv')
  427. cls.uwsgi_bin = os.path.join(cls.venv_dir, 'bin', 'uwsgi')
  428. cls.nginx_port = cls.uwsgi_port = get_unused_localhost_port()
  429. while cls.uwsgi_port == cls.nginx_port:
  430. # Ensure we don't hit a corner case in which two sucessive calls to
  431. # get_unused_localhost_port() return identical port numbers.
  432. cls.uwsgi_port = get_unused_localhost_port()
  433. cls.url = 'http://127.0.0.1:{port}/repo.git'.format(port=cls.nginx_port)
  434. cls.url_extra_repo = 'http://127.0.0.1:{port}/extra_repo.git'.format(port=cls.nginx_port)
  435. cls.ext_opts = {'url': cls.url, 'url_extra_repo': cls.url_extra_repo}
  436. # Add auth params if present (if so this will trigger the spawned
  437. # server to turn on HTTP basic auth).
  438. for credential_param in ('user', 'password'):
  439. if hasattr(cls, credential_param):
  440. cls.ext_opts[credential_param] = getattr(cls, credential_param)
  441. auth_enabled = hasattr(cls, 'username') and hasattr(cls, 'password')
  442. pillar = {'git_pillar': {'config_dir': cls.config_dir,
  443. 'git_dir': cls.git_dir,
  444. 'venv_dir': cls.venv_dir,
  445. 'root_dir': cls.root_dir,
  446. 'nginx_port': cls.nginx_port,
  447. 'uwsgi_port': cls.uwsgi_port,
  448. 'auth_enabled': auth_enabled}}
  449. # Different libexec dir for git backend on Debian-based systems
  450. git_core = '/usr/libexec/git-core'
  451. if not os.path.exists(git_core):
  452. git_core = '/usr/lib/git-core'
  453. if not os.path.exists(git_core):
  454. cls.tearDownClass()
  455. raise AssertionError(
  456. '{} not found. Either git is not installed, or the test '
  457. 'class needs to be updated.'.format(git_core)
  458. )
  459. pillar['git_pillar']['git-http-backend'] = os.path.join(git_core, 'git-http-backend')
  460. try:
  461. if cls.prep_states_ran is False:
  462. ret = cls.cls_run_function('state.apply', mods='git_pillar.http', pillar=pillar)
  463. assert next(six.itervalues(ret))['result'] is True
  464. cls.prep_states_ran = True
  465. log.info('%s: States applied', cls.__name__)
  466. if cls.uwsgi_proc is not None:
  467. if not psutil.pid_exists(cls.uwsgi_proc.pid):
  468. log.warning('%s: uWsgi started but appears to be dead now. Will try to restart it.',
  469. cls.__name__)
  470. cls.uwsgi_proc = None
  471. if cls.uwsgi_proc is None:
  472. cls.uwsgi_proc = start_daemon(cls.uwsgi_bin, cls.config_dir, cls.uwsgi_port, UwsgiDaemon)
  473. log.info('%s: %s started', cls.__name__, cls.uwsgi_bin)
  474. if cls.nginx_proc is not None:
  475. if not psutil.pid_exists(cls.nginx_proc.pid):
  476. log.warning('%s: nginx started but appears to be dead now. Will try to restart it.',
  477. cls.__name__)
  478. cls.nginx_proc = None
  479. if cls.nginx_proc is None:
  480. cls.nginx_proc = start_daemon('nginx', cls.config_dir, cls.nginx_port, NginxDaemon)
  481. log.info('%s: nginx started', cls.__name__)
  482. except AssertionError:
  483. cls.tearDownClass()
  484. six.reraise(*sys.exc_info())
  485. @classmethod
  486. def tearDownClass(cls):
  487. if cls.nginx_proc is not None:
  488. log.info('[%s] Stopping %s', cls.nginx_proc.log_prefix, cls.nginx_proc.__class__.__name__)
  489. terminate_process(cls.nginx_proc.pid, kill_children=True, slow_stop=True)
  490. log.info('[%s] %s stopped', cls.nginx_proc.log_prefix, cls.nginx_proc.__class__.__name__)
  491. cls.nginx_proc = None
  492. if cls.uwsgi_proc is not None:
  493. log.info('[%s] Stopping %s', cls.uwsgi_proc.log_prefix, cls.uwsgi_proc.__class__.__name__)
  494. terminate_process(cls.uwsgi_proc.pid, kill_children=True, slow_stop=True)
  495. log.info('[%s] %s stopped', cls.uwsgi_proc.log_prefix, cls.uwsgi_proc.__class__.__name__)
  496. cls.uwsgi_proc = None
  497. shutil.rmtree(cls.root_dir, ignore_errors=True)
  498. super(WebserverMixin, cls).tearDownClass()
  499. class GitTestBase(ModuleCase):
  500. '''
  501. Base class for all gitfs/git_pillar tests. Must be subclassed and paired
  502. with either SSHDMixin or WebserverMixin to provide the server.
  503. '''
  504. maxDiff = None
  505. git_opts = '-c user.name="Foo Bar" -c user.email=foo@bar.com'
  506. ext_opts = {}
  507. def make_repo(self, root_dir, user='root'):
  508. raise NotImplementedError()
  509. class GitFSTestBase(GitTestBase, LoaderModuleMockMixin):
  510. '''
  511. Base class for all gitfs tests
  512. '''
  513. @requires_system_grains
  514. def setup_loader_modules(self, grains): # pylint: disable=W0221
  515. return {
  516. gitfs: {
  517. '__opts__': copy.copy(_OPTS),
  518. '__grains__': grains,
  519. }
  520. }
  521. def make_repo(self, root_dir, user='root'):
  522. raise NotImplementedError()
  523. class GitPillarTestBase(GitTestBase, LoaderModuleMockMixin):
  524. '''
  525. Base class for all git_pillar tests
  526. '''
  527. bare_repo = bare_repo_backup = bare_extra_repo = bare_extra_repo_backup = None
  528. admin_repo = admin_repo_backup = admin_extra_repo = admin_extra_repo_backup = None
  529. @requires_system_grains
  530. def setup_loader_modules(self, grains): # pylint: disable=W0221
  531. return {
  532. git_pillar: {
  533. '__opts__': copy.copy(_OPTS),
  534. '__grains__': grains,
  535. }
  536. }
  537. def get_pillar(self, ext_pillar_conf):
  538. '''
  539. Run git_pillar with the specified configuration
  540. '''
  541. cachedir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  542. self.addCleanup(shutil.rmtree, cachedir, ignore_errors=True)
  543. ext_pillar_opts = {'optimization_order': [0, 1, 2]}
  544. ext_pillar_opts.update(
  545. salt.utils.yaml.safe_load(
  546. ext_pillar_conf.format(
  547. cachedir=cachedir,
  548. extmods=os.path.join(cachedir, 'extmods'),
  549. **self.ext_opts
  550. )
  551. )
  552. )
  553. with patch.dict(git_pillar.__opts__, ext_pillar_opts):
  554. return git_pillar.ext_pillar(
  555. 'minion',
  556. {},
  557. *ext_pillar_opts['ext_pillar'][0]['git']
  558. )
  559. def make_repo(self, root_dir, user='root'):
  560. log.info('Creating test Git repo....')
  561. self.bare_repo = os.path.join(root_dir, 'repo.git')
  562. self.bare_repo_backup = '{}.backup'.format(self.bare_repo)
  563. self.admin_repo = os.path.join(root_dir, 'admin')
  564. self.admin_repo_backup = '{}.backup'.format(self.admin_repo)
  565. for dirname in (self.bare_repo, self.admin_repo):
  566. shutil.rmtree(dirname, ignore_errors=True)
  567. if os.path.exists(self.bare_repo_backup) and os.path.exists(self.admin_repo_backup):
  568. shutil.copytree(self.bare_repo_backup, self.bare_repo)
  569. shutil.copytree(self.admin_repo_backup, self.admin_repo)
  570. return
  571. # Create bare repo
  572. self.run_function(
  573. 'git.init',
  574. [self.bare_repo],
  575. user=user,
  576. bare=True)
  577. # Clone bare repo
  578. self.run_function(
  579. 'git.clone',
  580. [self.admin_repo],
  581. url=self.bare_repo,
  582. user=user)
  583. def _push(branch, message):
  584. self.run_function(
  585. 'git.add',
  586. [self.admin_repo, '.'],
  587. user=user)
  588. self.run_function(
  589. 'git.commit',
  590. [self.admin_repo, message],
  591. user=user,
  592. git_opts=self.git_opts,
  593. )
  594. self.run_function(
  595. 'git.push',
  596. [self.admin_repo],
  597. remote='origin',
  598. ref=branch,
  599. user=user,
  600. )
  601. with salt.utils.files.fopen(
  602. os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
  603. fp_.write(textwrap.dedent('''\
  604. base:
  605. '*':
  606. - foo
  607. '''))
  608. with salt.utils.files.fopen(
  609. os.path.join(self.admin_repo, 'foo.sls'), 'w') as fp_:
  610. fp_.write(textwrap.dedent('''\
  611. branch: master
  612. mylist:
  613. - master
  614. mydict:
  615. master: True
  616. nested_list:
  617. - master
  618. nested_dict:
  619. master: True
  620. '''))
  621. # Add another file to be referenced using git_pillar_includes
  622. with salt.utils.files.fopen(
  623. os.path.join(self.admin_repo, 'bar.sls'), 'w') as fp_:
  624. fp_.write('included_pillar: True\n')
  625. # Add another file in subdir
  626. os.mkdir(os.path.join(self.admin_repo, 'subdir'))
  627. with salt.utils.files.fopen(
  628. os.path.join(self.admin_repo, 'subdir', 'bar.sls'), 'w') as fp_:
  629. fp_.write('from_subdir: True\n')
  630. _push('master', 'initial commit')
  631. # Do the same with different values for "dev" branch
  632. self.run_function(
  633. 'git.checkout',
  634. [self.admin_repo],
  635. user=user,
  636. opts='-b dev')
  637. # The bar.sls shouldn't be in any branch but master
  638. self.run_function(
  639. 'git.rm',
  640. [self.admin_repo, 'bar.sls'],
  641. user=user)
  642. with salt.utils.files.fopen(
  643. os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
  644. fp_.write(textwrap.dedent('''\
  645. dev:
  646. '*':
  647. - foo
  648. '''))
  649. with salt.utils.files.fopen(
  650. os.path.join(self.admin_repo, 'foo.sls'), 'w') as fp_:
  651. fp_.write(textwrap.dedent('''\
  652. branch: dev
  653. mylist:
  654. - dev
  655. mydict:
  656. dev: True
  657. nested_list:
  658. - dev
  659. nested_dict:
  660. dev: True
  661. '''))
  662. _push('dev', 'add dev branch')
  663. # Create just a top file in a separate repo, to be mapped to the base
  664. # env and referenced using git_pillar_includes
  665. self.run_function(
  666. 'git.checkout',
  667. [self.admin_repo],
  668. user=user,
  669. opts='-b top_only')
  670. # The top.sls should be the only file in this branch
  671. self.run_function(
  672. 'git.rm',
  673. [self.admin_repo, 'foo.sls', os.path.join('subdir', 'bar.sls')],
  674. user=user)
  675. with salt.utils.files.fopen(
  676. os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
  677. fp_.write(textwrap.dedent('''\
  678. base:
  679. '*':
  680. - bar
  681. '''))
  682. _push('top_only', 'add top_only branch')
  683. # Create just another top file in a separate repo, to be mapped to the base
  684. # env and including mounted.bar
  685. self.run_function(
  686. 'git.checkout',
  687. [self.admin_repo],
  688. user=user,
  689. opts='-b top_mounted')
  690. # The top.sls should be the only file in this branch
  691. with salt.utils.files.fopen(
  692. os.path.join(self.admin_repo, 'top.sls'), 'w') as fp_:
  693. fp_.write(textwrap.dedent('''\
  694. base:
  695. '*':
  696. - mounted.bar
  697. '''))
  698. _push('top_mounted', 'add top_mounted branch')
  699. shutil.copytree(self.bare_repo, self.bare_repo_backup)
  700. shutil.copytree(self.admin_repo, self.admin_repo_backup)
  701. log.info('Test Git repo created.')
  702. def make_extra_repo(self, root_dir, user='root'):
  703. log.info('Creating extra test Git repo....')
  704. self.bare_extra_repo = os.path.join(root_dir, 'extra_repo.git')
  705. self.bare_extra_repo_backup = '{}.backup'.format(self.bare_extra_repo)
  706. self.admin_extra_repo = os.path.join(root_dir, 'admin_extra')
  707. self.admin_extra_repo_backup = '{}.backup'.format(self.admin_extra_repo)
  708. for dirname in (self.bare_extra_repo, self.admin_extra_repo):
  709. shutil.rmtree(dirname, ignore_errors=True)
  710. if os.path.exists(self.bare_extra_repo_backup) and os.path.exists(self.admin_extra_repo_backup):
  711. shutil.copytree(self.bare_extra_repo_backup, self.bare_extra_repo)
  712. shutil.copytree(self.admin_extra_repo_backup, self.admin_extra_repo)
  713. return
  714. # Create bare extra repo
  715. self.run_function(
  716. 'git.init',
  717. [self.bare_extra_repo],
  718. user=user,
  719. bare=True)
  720. # Clone bare repo
  721. self.run_function(
  722. 'git.clone',
  723. [self.admin_extra_repo],
  724. url=self.bare_extra_repo,
  725. user=user)
  726. def _push(branch, message):
  727. self.run_function(
  728. 'git.add',
  729. [self.admin_extra_repo, '.'],
  730. user=user)
  731. self.run_function(
  732. 'git.commit',
  733. [self.admin_extra_repo, message],
  734. user=user,
  735. git_opts=self.git_opts,
  736. )
  737. self.run_function(
  738. 'git.push',
  739. [self.admin_extra_repo],
  740. remote='origin',
  741. ref=branch,
  742. user=user,
  743. )
  744. with salt.utils.files.fopen(
  745. os.path.join(self.admin_extra_repo, 'top.sls'), 'w') as fp_:
  746. fp_.write(textwrap.dedent('''\
  747. "{{saltenv}}":
  748. '*':
  749. - motd
  750. - nowhere.foo
  751. '''))
  752. with salt.utils.files.fopen(
  753. os.path.join(self.admin_extra_repo, 'motd.sls'), 'w') as fp_:
  754. fp_.write(textwrap.dedent('''\
  755. motd: The force will be with you. Always.
  756. '''))
  757. _push('master', 'initial commit')
  758. shutil.copytree(self.bare_extra_repo, self.bare_extra_repo_backup)
  759. shutil.copytree(self.admin_extra_repo, self.admin_extra_repo_backup)
  760. log.info('Extra test Git repo created.')
  761. @classmethod
  762. def tearDownClass(cls):
  763. super(GitPillarTestBase, cls).tearDownClass()
  764. for dirname in (cls.admin_repo,
  765. cls.admin_repo_backup,
  766. cls.admin_extra_repo,
  767. cls.admin_extra_repo_backup,
  768. cls.bare_repo,
  769. cls.bare_repo_backup,
  770. cls.bare_extra_repo,
  771. cls.bare_extra_repo_backup):
  772. if dirname is not None:
  773. shutil.rmtree(dirname, ignore_errors=True)
  774. class GitPillarSSHTestBase(GitPillarTestBase, SSHDMixin):
  775. '''
  776. Base class for GitPython and Pygit2 SSH tests
  777. '''
  778. id_rsa_nopass = id_rsa_withpass = None
  779. git_ssh = '/tmp/git_ssh'
  780. def setUp(self):
  781. '''
  782. Create the SSH server and user, and create the git repo
  783. '''
  784. log.info('%s.setUp() started...', self.__class__.__name__)
  785. super(GitPillarSSHTestBase, self).setUp()
  786. root_dir = os.path.expanduser('~{0}'.format(self.username))
  787. if root_dir.startswith('~'):
  788. raise AssertionError(
  789. 'Unable to resolve homedir for user \'{0}\''.format(
  790. self.username
  791. )
  792. )
  793. self.make_repo(root_dir, user=self.username)
  794. self.make_extra_repo(root_dir, user=self.username)
  795. log.info('%s.setUp() complete.', self.__class__.__name__)
  796. def get_pillar(self, ext_pillar_conf):
  797. '''
  798. Wrap the parent class' get_pillar() func in logic that temporarily
  799. changes the GIT_SSH to use our custom script, ensuring that the
  800. passphraselsess key is used to auth without needing to modify the root
  801. user's ssh config file.
  802. '''
  803. def cleanup_environ(environ):
  804. os.environ.clear()
  805. os.environ.update(environ)
  806. self.addCleanup(cleanup_environ, os.environ.copy())
  807. os.environ['GIT_SSH'] = self.git_ssh
  808. return super(GitPillarSSHTestBase, self).get_pillar(ext_pillar_conf)
  809. class GitPillarHTTPTestBase(GitPillarTestBase, WebserverMixin):
  810. '''
  811. Base class for GitPython and Pygit2 HTTP tests
  812. '''
  813. def setUp(self):
  814. '''
  815. Create and start the webserver, and create the git repo
  816. '''
  817. log.info('%s.setUp() started...', self.__class__.__name__)
  818. super(GitPillarHTTPTestBase, self).setUp()
  819. self.make_repo(self.repo_dir)
  820. self.make_extra_repo(self.repo_dir)
  821. log.info('%s.setUp() complete', self.__class__.__name__)