testprogram.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Classes for starting/stopping/status salt daemons, auxiliary
  4. scripts, generic commands.
  5. '''
  6. from __future__ import absolute_import
  7. import atexit
  8. import copy
  9. from datetime import datetime, timedelta
  10. import errno
  11. import getpass
  12. import logging
  13. import os
  14. import shutil
  15. import signal
  16. import socket
  17. import subprocess
  18. import sys
  19. import tempfile
  20. import time
  21. import salt.utils.files
  22. import salt.utils.platform
  23. import salt.utils.process
  24. import salt.utils.psutil_compat as psutils
  25. import salt.utils.yaml
  26. import salt.defaults.exitcodes as exitcodes
  27. from salt.ext import six
  28. from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
  29. from tests.support.unit import TestCase
  30. from tests.support.helpers import win32_kill_process_tree
  31. from tests.support.paths import CODE_DIR
  32. from tests.support.processes import terminate_process, terminate_process_list
  33. log = logging.getLogger(__name__)
  34. if 'TimeoutError' not in __builtins__:
  35. class TimeoutError(OSError):
  36. '''Compatibility exception with python3'''
  37. pass
  38. __builtins__['TimeoutError'] = TimeoutError
  39. class TestProgramMeta(type):
  40. '''
  41. Stack all inherited config_attrs and dirtree dirs from the base classes.
  42. '''
  43. def __new__(mcs, name, bases, attrs):
  44. config_vals = {}
  45. config_attrs = set()
  46. dirtree = set()
  47. for base in bases:
  48. config_vals.update(getattr(base, 'config_vals', {}))
  49. config_attrs.update(getattr(base, 'config_attrs', {}))
  50. dirtree.update(getattr(base, 'dirtree', []))
  51. config_vals.update(attrs.get('config_vals', {}))
  52. attrs['config_vals'] = config_vals
  53. config_attrs.update(attrs.get('config_attrs', {}))
  54. attrs['config_attrs'] = config_attrs
  55. dirtree.update(attrs.get('dirtree', []))
  56. attrs['dirtree'] = dirtree
  57. return super(TestProgramMeta, mcs).__new__(mcs, name, bases, attrs)
  58. # pylint: disable=too-many-instance-attributes
  59. class TestProgram(six.with_metaclass(TestProgramMeta, object)):
  60. '''
  61. Set up an arbitrary executable to run.
  62. :attribute dirtree: An iterable of directories to be created
  63. '''
  64. empty_config = ''
  65. config_file = ''
  66. config_attrs = set([
  67. 'name',
  68. 'test_dir',
  69. 'config_dirs',
  70. ])
  71. config_vals = {
  72. }
  73. config_base = ''
  74. config_dir = os.path.join('etc')
  75. configs = {}
  76. config_types = (str, six.string_types,)
  77. dirtree = [
  78. '&config_dirs',
  79. ]
  80. @staticmethod
  81. def config_caster(cfg):
  82. return str(cfg)
  83. def __init__(self, program=None, name=None, env=None, shell=False, parent_dir=None, clean_on_exit=True, **kwargs):
  84. self.program = program or getattr(self, 'program', None)
  85. self.name = name or getattr(self, 'name', '')
  86. self.env = env or {}
  87. self.shell = shell
  88. self._parent_dir = parent_dir or None
  89. self.clean_on_exit = clean_on_exit
  90. self._root_dir = kwargs.pop('root_dir', self.name)
  91. self.config_dir = kwargs.pop('config_dir', copy.copy(self.config_dir))
  92. config_attrs = copy.copy(self.config_attrs)
  93. config_attrs.update(kwargs.pop('config_attrs', set()))
  94. self.config_attrs = config_attrs
  95. config_vals = copy.copy(self.config_vals)
  96. config_vals.update(kwargs.pop('config_vals', {}))
  97. self.config_vals = config_vals
  98. config_base = copy.deepcopy(self.config_base)
  99. config_base = self.config_merge(config_base, kwargs.pop('config_base', self.config_types[0]()))
  100. self.config_base = config_base
  101. configs = copy.deepcopy(self.configs)
  102. for cname, cinfo in kwargs.pop('configs', {}).items():
  103. target = configs.setdefault(cname, {})
  104. if 'path' in cinfo:
  105. target['path'] = cinfo['path']
  106. if 'map' in cinfo:
  107. target_map = target.setdefault('map', self.config_types[0]())
  108. target_map = self.config_merge(target_map, cinfo['map'])
  109. target['map'] = target_map
  110. self.configs = configs
  111. if not self.name:
  112. if not self.program:
  113. raise ValueError('"{0}" object must specify "program" parameter'.format(self.__class__.__name__))
  114. self.name = os.path.basename(self.program)
  115. self.process = None
  116. self.created_parent_dir = False
  117. self._setup_done = False
  118. dirtree = set(self.dirtree)
  119. dirtree.update(kwargs.pop('dirtree', []))
  120. self.dirtree = dirtree
  121. # Register the exit clean-up before making anything needing clean-up
  122. atexit.register(self.cleanup)
  123. def __enter__(self):
  124. pass
  125. def __exit__(self, typ, value, traceback):
  126. pass
  127. @property
  128. def test_dir(self):
  129. '''Directory that will contains all of the static and dynamic files for the daemon'''
  130. return os.path.join(self.parent_dir, self._root_dir)
  131. def config_file_get(self, config):
  132. '''Get the filename (viz. path) to the configuration file'''
  133. cfgf = self.configs[config].get('path')
  134. if cfgf:
  135. cfgf.format(**self.config_subs())
  136. else:
  137. cfgf = os.path.join(self.config_dir, config)
  138. return cfgf
  139. def config_dir_get(self, config):
  140. '''Get the parent directory for the configuration file'''
  141. return os.path.dirname(self.config_file_get(config))
  142. @property
  143. def config_dirs(self):
  144. '''Return a list of configuration directories'''
  145. cdirs = [self.config_dir_get(config) for config in self.configs.keys()]
  146. return cdirs
  147. def abs_path(self, path):
  148. '''Absolute path of file including the test_dir'''
  149. return os.path.join(self.test_dir, path)
  150. @property
  151. def start_pid(self):
  152. '''PID of the called script prior to daemonizing.'''
  153. return self.process.pid if self.process else None
  154. @property
  155. def parent_dir(self):
  156. '''
  157. Directory that contains everything generated for running scripts - possibly
  158. for multiple scripts.
  159. '''
  160. if self._parent_dir is None:
  161. self.created_parent_dir = True
  162. self._parent_dir = tempfile.mkdtemp(prefix='salt-testdaemon-')
  163. else:
  164. self._parent_dir = os.path.abspath(os.path.normpath(self._parent_dir))
  165. if not os.path.exists(self._parent_dir):
  166. self.created_parent_dir = True
  167. os.makedirs(self._parent_dir)
  168. elif not os.path.isdir(self._parent_dir):
  169. raise ValueError('Parent path "{0}" exists but is not a directory'.format(self._parent_dir))
  170. return self._parent_dir
  171. def config_write(self, config):
  172. '''Write out the config to a file'''
  173. if not config:
  174. return
  175. cpath = self.abs_path(self.config_file_get(config))
  176. with salt.utils.files.fopen(cpath, 'w') as cfo:
  177. cfg = self.config_stringify(config)
  178. log.debug('Writing configuration for {0} to {1}:\n{2}'.format(self.name, cpath, cfg))
  179. cfo.write(cfg)
  180. cfo.flush()
  181. def configs_write(self):
  182. '''Write all configuration files'''
  183. for config in self.configs:
  184. self.config_write(config)
  185. def config_type(self, config):
  186. '''Check if a configuration is an acceptable type.'''
  187. return isinstance(config, self.config_types)
  188. def config_cast(self, config):
  189. '''Cast a configuration to the internal expected type.'''
  190. if not self.config_type(config):
  191. config = self.config_caster(config)
  192. return config
  193. def config_subs(self):
  194. '''Get the substitution values for use to generate the config'''
  195. subs = dict([(attr, getattr(self, attr, None)) for attr in self.config_attrs])
  196. for key, val in self.config_vals.items():
  197. subs[key] = val.format(**subs)
  198. return subs
  199. def config_stringify(self, config):
  200. '''Get the configuration as a string'''
  201. cfg = self.config_get(config)
  202. cfg.format(**self.config_subs())
  203. return cfg
  204. def config_merge(self, base, overrides):
  205. '''Merge two configuration hunks'''
  206. base = self.config_cast(base)
  207. overrides = self.config_cast(overrides)
  208. return ''.join([base, overrides])
  209. def config_get(self, config):
  210. '''Get the configuration data'''
  211. return self.configs[config]
  212. def config_set(self, config, val):
  213. '''Set the configuration data'''
  214. self.configs[config] = val
  215. def make_dirtree(self):
  216. '''Create directory structure.'''
  217. subdirs = []
  218. for branch in self.dirtree:
  219. log.debug('checking dirtree: {0}'.format(branch))
  220. if not branch:
  221. continue
  222. if isinstance(branch, six.string_types) and branch[0] == '&':
  223. log.debug('Looking up dirtree branch "{0}"'.format(branch))
  224. try:
  225. dirattr = getattr(self, branch[1:], None)
  226. log.debug('dirtree "{0}" => "{1}"'.format(branch, dirattr))
  227. except AttributeError:
  228. raise ValueError(
  229. 'Unable to find dirtree attribute "{0}" on object "{1}.name = {2}: {3}"'.format(
  230. branch, self.__class__.__name__, self.name, dir(self),
  231. )
  232. )
  233. if not dirattr:
  234. continue
  235. if isinstance(dirattr, six.string_types):
  236. subdirs.append(dirattr)
  237. elif hasattr(dirattr, '__iter__'):
  238. subdirs.extend(dirattr)
  239. else:
  240. raise TypeError("Branch type of {0} in dirtree is unhandled".format(branch))
  241. elif isinstance(branch, six.string_types):
  242. subdirs.append(branch)
  243. else:
  244. raise TypeError("Branch type of {0} in dirtree is unhandled".format(branch))
  245. for subdir in subdirs:
  246. path = self.abs_path(subdir)
  247. if not os.path.exists(path):
  248. log.debug('make_dirtree: {0}'.format(path))
  249. os.makedirs(path)
  250. def setup(self, *args, **kwargs):
  251. '''Create any scaffolding for run-time'''
  252. # unused
  253. _ = args, kwargs
  254. if not self._setup_done:
  255. self.make_dirtree()
  256. self.configs_write()
  257. self._setup_done = True
  258. def cleanup(self, *args, **kwargs):
  259. ''' Clean out scaffolding of setup() and any run-time generated files.'''
  260. # Unused for now
  261. _ = (args, kwargs)
  262. if self.process:
  263. try:
  264. self.process.kill()
  265. self.process.wait()
  266. except OSError:
  267. pass
  268. if os.path.exists(self.test_dir):
  269. shutil.rmtree(self.test_dir)
  270. if self.created_parent_dir and os.path.exists(self.parent_dir):
  271. shutil.rmtree(self.parent_dir)
  272. def run(
  273. self,
  274. args=None,
  275. catch_stderr=False,
  276. with_retcode=False,
  277. timeout=None,
  278. raw=False,
  279. env=None,
  280. verbatim_args=False,
  281. verbatim_env=False,
  282. ):
  283. '''
  284. Execute a command possibly using a supplied environment.
  285. :param args:
  286. A command string or a command sequence of arguments for the program.
  287. :param catch_stderr: A boolean whether to capture and return stderr.
  288. :param with_retcode: A boolean whether to return the exit code.
  289. :param timeout: A float of how long to wait for the process to
  290. complete before it is killed.
  291. :param raw: A boolean whether to return buffer strings for stdout and
  292. stderr or sequences of output lines.
  293. :param env: A dictionary of environment key/value settings for the
  294. command.
  295. :param verbatim_args: A boolean whether to automatically add inferred arguments.
  296. :param verbatim_env: A boolean whether to automatically add inferred
  297. environment values.
  298. :return list: (stdout [,stderr] [,retcode])
  299. '''
  300. # unused for now
  301. _ = verbatim_args
  302. self.setup()
  303. if args is None:
  304. args = []
  305. if env is None:
  306. env = {}
  307. env_delta = {}
  308. env_delta.update(self.env)
  309. env_delta.update(env)
  310. if not verbatim_env:
  311. env_pypath = env_delta.get('PYTHONPATH', os.environ.get('PYTHONPATH'))
  312. if not env_pypath:
  313. env_pypath = sys.path
  314. else:
  315. env_pypath = env_pypath.split(':')
  316. for path in sys.path:
  317. if path not in env_pypath:
  318. env_pypath.append(path)
  319. # Always ensure that the test tree is searched first for python modules
  320. if CODE_DIR != env_pypath[0]:
  321. env_pypath.insert(0, CODE_DIR)
  322. if salt.utils.platform.is_windows():
  323. env_delta['PYTHONPATH'] = ';'.join(env_pypath)
  324. else:
  325. env_delta['PYTHONPATH'] = ':'.join(env_pypath)
  326. cmd_env = dict(os.environ)
  327. cmd_env.update(env_delta)
  328. popen_kwargs = {
  329. 'shell': self.shell,
  330. 'stdout': subprocess.PIPE,
  331. 'env': cmd_env,
  332. }
  333. if catch_stderr is True:
  334. popen_kwargs['stderr'] = subprocess.PIPE
  335. if not sys.platform.lower().startswith('win'):
  336. popen_kwargs['close_fds'] = True
  337. def detach_from_parent_group():
  338. '''
  339. A utility function that prevents child process from getting parent signals.
  340. '''
  341. os.setpgrp()
  342. popen_kwargs['preexec_fn'] = detach_from_parent_group
  343. if salt.utils.platform.is_windows():
  344. self.argv = ['python.exe', self.program]
  345. else:
  346. self.argv = [self.program]
  347. self.argv.extend(args)
  348. log.debug('TestProgram.run: %s Environment %s', self.argv, env_delta)
  349. process = subprocess.Popen(self.argv, **popen_kwargs)
  350. self.process = process
  351. if timeout is not None:
  352. stop_at = datetime.now() + timedelta(seconds=timeout)
  353. term_sent = False
  354. while True:
  355. process.poll()
  356. if datetime.now() > stop_at:
  357. if term_sent is False:
  358. if salt.utils.platform.is_windows():
  359. _, alive = win32_kill_process_tree(process.pid)
  360. if alive:
  361. log.error("Child processes still alive: %s", alive)
  362. else:
  363. # Kill the process group since sending the term signal
  364. # would only terminate the shell, not the command
  365. # executed in the shell
  366. os.killpg(os.getpgid(process.pid), signal.SIGINT)
  367. term_sent = True
  368. continue
  369. try:
  370. if salt.utils.platform.is_windows():
  371. _, alive = win32_kill_process_tree(process.pid)
  372. if alive:
  373. log.error("Child processes still alive: %s", alive)
  374. else:
  375. # As a last resort, kill the process group
  376. os.killpg(os.getpgid(process.pid), signal.SIGKILL)
  377. process.wait()
  378. except OSError as exc:
  379. if exc.errno != errno.ESRCH:
  380. raise
  381. out = process.stdout.read().splitlines()
  382. out.extend([
  383. 'Process took more than {0} seconds to complete. '
  384. 'Process Killed!'.format(timeout)
  385. ])
  386. if catch_stderr:
  387. err = process.stderr.read().splitlines()
  388. if with_retcode:
  389. return out, err, process.returncode
  390. else:
  391. return out, err
  392. if with_retcode:
  393. return out, process.returncode
  394. else:
  395. return out
  396. if process.returncode is not None:
  397. break
  398. if catch_stderr:
  399. if sys.version_info < (2, 7):
  400. # On python 2.6, the subprocess'es communicate() method uses
  401. # select which, is limited by the OS to 1024 file descriptors
  402. # We need more available descriptors to run the tests which
  403. # need the stderr output.
  404. # So instead of .communicate() we wait for the process to
  405. # finish, but, as the python docs state "This will deadlock
  406. # when using stdout=PIPE and/or stderr=PIPE and the child
  407. # process generates enough output to a pipe such that it
  408. # blocks waiting for the OS pipe buffer to accept more data.
  409. # Use communicate() to avoid that." <- a catch, catch situation
  410. #
  411. # Use this work around were it's needed only, python 2.6
  412. process.wait()
  413. out = process.stdout.read()
  414. err = process.stderr.read()
  415. else:
  416. out, err = process.communicate()
  417. # Force closing stderr/stdout to release file descriptors
  418. if process.stdout is not None:
  419. process.stdout.close()
  420. if process.stderr is not None:
  421. process.stderr.close()
  422. # pylint: disable=maybe-no-member
  423. try:
  424. if with_retcode:
  425. if out is not None and err is not None:
  426. if not raw:
  427. return out.splitlines(), err.splitlines(), process.returncode
  428. else:
  429. return out, err, process.returncode
  430. return out.splitlines(), [], process.returncode
  431. else:
  432. if out is not None and err is not None:
  433. if not raw:
  434. return out.splitlines(), err.splitlines()
  435. else:
  436. return out, err
  437. if not raw:
  438. return out.splitlines(), []
  439. else:
  440. return out, []
  441. finally:
  442. try:
  443. process.terminate()
  444. except OSError as err:
  445. # process already terminated
  446. pass
  447. # pylint: enable=maybe-no-member
  448. data = process.communicate()
  449. process.stdout.close()
  450. try:
  451. if with_retcode:
  452. if not raw:
  453. return data[0].splitlines(), process.returncode
  454. else:
  455. return data[0], process.returncode
  456. else:
  457. if not raw:
  458. return data[0].splitlines()
  459. else:
  460. return data[0]
  461. finally:
  462. try:
  463. process.terminate()
  464. except OSError as err:
  465. # process already terminated
  466. pass
  467. class TestSaltProgramMeta(TestProgramMeta):
  468. '''
  469. A Meta-class to set self.script from the class name when it is
  470. not specifically set by a "script" argument.
  471. '''
  472. def __new__(mcs, name, bases, attrs):
  473. if attrs.get('script') is None:
  474. if 'Salt' in name:
  475. script = 'salt-{0}'.format(name.rsplit('Salt', 1)[-1].lower())
  476. if script is None:
  477. raise AttributeError(
  478. 'Class {0}: Unable to set "script" attribute: class name'
  479. ' must include "Salt" or "script" must be explicitly set.'.format(name)
  480. )
  481. attrs['script'] = script
  482. config_base = {}
  483. configs = {}
  484. for base in bases:
  485. if 'Salt' not in base.__name__:
  486. continue
  487. config_base.update(getattr(base, 'config_base', {}))
  488. configs.update(getattr(base, 'configs', {}))
  489. config_base.update(attrs.get('config_base', {}))
  490. attrs['config_base'] = config_base
  491. configs.update(attrs.get('configs', {}))
  492. attrs['configs'] = configs
  493. return super(TestSaltProgramMeta, mcs).__new__(mcs, name, bases, attrs)
  494. class TestSaltProgram(six.with_metaclass(TestSaltProgramMeta, TestProgram)):
  495. '''
  496. This is like TestProgram but with some functions to run a salt-specific
  497. auxiliary program.
  498. '''
  499. config_types = (dict,)
  500. config_attrs = set([
  501. 'log_dir',
  502. 'script_dir',
  503. ])
  504. pub_port = 4505
  505. ret_port = 4506
  506. for port in [pub_port, ret_port]:
  507. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  508. try:
  509. connect = sock.bind(('localhost', port))
  510. except (socket.error, OSError):
  511. # these ports are already in use, use different ones
  512. pub_port = 4606
  513. ret_port = 4607
  514. break
  515. sock.close()
  516. config_base = {
  517. 'root_dir': '{test_dir}',
  518. 'publish_port': pub_port,
  519. 'ret_port': ret_port,
  520. }
  521. configs = {}
  522. config_dir = os.path.join('etc', 'salt')
  523. log_dir = os.path.join('var', 'log', 'salt')
  524. dirtree = [
  525. '&log_dir',
  526. '&script_dir',
  527. ]
  528. script = ''
  529. script_dir = 'bin'
  530. @staticmethod
  531. def config_caster(cfg):
  532. return salt.utils.yaml.safe_load(cfg)
  533. def __init__(self, *args, **kwargs):
  534. if len(args) < 2 and 'program' not in kwargs:
  535. # This is effectively a place-holder - it gets set correctly after super()
  536. kwargs['program'] = self.script
  537. super(TestSaltProgram, self).__init__(*args, **kwargs)
  538. self.program = self.abs_path(os.path.join(self.script_dir, self.script))
  539. path = self.env.get('PATH', os.getenv('PATH'))
  540. self.env['PATH'] = ':'.join([self.abs_path(self.script_dir), path])
  541. def config_merge(self, base, overrides):
  542. _base = self.config_cast(copy.deepcopy(base))
  543. _overrides = self.config_cast(overrides)
  544. # NOTE: this simple update will not work for deep dictionaries
  545. _base.update(copy.deepcopy(_overrides))
  546. return _base
  547. def config_get(self, config):
  548. cfg_base = {}
  549. for key, val in self.config_base.items():
  550. _val = val
  551. if val and isinstance(val, six.string_types) and val[0] == '&':
  552. _val = getattr(self, val[1:], None)
  553. if _val is None:
  554. continue
  555. cfg_base[key] = _val
  556. if config in self.configs:
  557. cfg = {}
  558. for key, val in self.configs.get(config, {}).get('map', {}).items():
  559. _val = val
  560. if val and isinstance(val, six.string_types) and val[0] == '&':
  561. _val = getattr(self, val[1:], None)
  562. if _val is None:
  563. continue
  564. cfg[key] = _val
  565. cfg = self.config_merge(cfg_base, cfg)
  566. log.debug('Generated config => {0}'.format(cfg))
  567. return cfg
  568. def config_stringify(self, config):
  569. '''Transform the configuration data into a string (suitable to write to a file)'''
  570. subs = self.config_subs()
  571. cfg = {}
  572. for key, val in self.config_get(config).items():
  573. if isinstance(val, six.string_types):
  574. cfg[key] = val.format(**subs)
  575. else:
  576. cfg[key] = val
  577. return salt.utils.yaml.safe_dump(cfg, default_flow_style=False)
  578. def setup(self, *args, **kwargs):
  579. super(TestSaltProgram, self).setup(*args, **kwargs)
  580. self.install_script()
  581. def install_script(self):
  582. '''Generate the script file that calls python objects and libraries.'''
  583. lines = []
  584. script_source = os.path.join(CODE_DIR, 'scripts', self.script)
  585. with salt.utils.files.fopen(script_source, 'r') as sso:
  586. lines.extend(sso.readlines())
  587. if lines[0].startswith('#!'):
  588. lines.pop(0)
  589. lines.insert(0, '#!{0}\n'.format(sys.executable))
  590. script_path = self.abs_path(os.path.join(self.script_dir, self.script))
  591. log.debug('Installing "{0}" to "{1}"'.format(script_source, script_path))
  592. with salt.utils.files.fopen(script_path, 'w') as sdo:
  593. sdo.write(''.join(lines))
  594. sdo.flush()
  595. os.chmod(script_path, 0o755)
  596. def run(self, **kwargs):
  597. if not kwargs.get('verbatim_args'):
  598. args = kwargs.setdefault('args', [])
  599. if '-c' not in args and '--config-dir' not in args:
  600. args.extend(['--config-dir', self.abs_path(self.config_dir)])
  601. return super(TestSaltProgram, self).run(**kwargs)
  602. class TestProgramSalt(TestSaltProgram):
  603. '''Class to manage salt'''
  604. configs = {'master': {}}
  605. script = 'salt'
  606. class TestProgramSaltCall(TestSaltProgram):
  607. '''Class to manage salt-call'''
  608. configs = {'minion': {'map': {'id': '{name}'}}}
  609. class TestProgramSaltRun(TestSaltProgram):
  610. '''Class to manage salt-run'''
  611. configs = {'master': {}}
  612. def __init__(self, *args, **kwargs):
  613. cfgb = kwargs.setdefault('config_base', {})
  614. _ = cfgb.setdefault('user', getpass.getuser())
  615. super(TestProgramSaltRun, self).__init__(*args, **kwargs)
  616. class TestDaemon(TestProgram):
  617. '''
  618. Run one of the standard daemons
  619. '''
  620. script = None
  621. pid_file = None
  622. pid_dir = os.path.join('var', 'run')
  623. dirtree = [
  624. '&pid_dir',
  625. ]
  626. def __init__(self, *args, **kwargs):
  627. self.script = kwargs.pop('script', self.script)
  628. self.pid_file = kwargs.pop('pid_file', self.pid_file if self.pid_file else '{0}.pid'.format(self.script))
  629. self.pid_dir = kwargs.pop('pid_dir', self.pid_dir)
  630. self._shutdown = False
  631. if not args and 'program' not in kwargs:
  632. # This is effectively a place-holder - it gets set correctly after super()
  633. kwargs['program'] = self.script
  634. super(TestDaemon, self).__init__(*args, **kwargs)
  635. @property
  636. def pid_path(self):
  637. '''Path to the pid file created by the daemon'''
  638. return os.path.join(self.pid_dir, self.pid_file) if os.path.sep not in self.pid_file else self.pid_file
  639. @property
  640. def daemon_pid(self):
  641. '''Return the daemon PID'''
  642. daemon_pid = None
  643. pid_path = self.abs_path(self.pid_path)
  644. if salt.utils.process.check_pidfile(pid_path):
  645. daemon_pid = salt.utils.process.get_pidfile(pid_path)
  646. return daemon_pid
  647. def wait_for_daemon_pid(self, timeout=10):
  648. '''Wait up to timeout seconds for the PID file to appear and return the PID'''
  649. endtime = time.time() + timeout
  650. while True:
  651. pid = self.daemon_pid
  652. if pid:
  653. return pid
  654. if endtime < time.time():
  655. raise TimeoutError('Timeout waiting for "{0}" pid in "{1}"'.format(
  656. self.name, self.abs_path(self.pid_path)
  657. ))
  658. time.sleep(0.2)
  659. def is_running(self):
  660. '''Is the daemon running?'''
  661. ret = False
  662. if not self._shutdown:
  663. try:
  664. pid = self.wait_for_daemon_pid()
  665. ret = psutils.pid_exists(pid)
  666. except TimeoutError:
  667. pass
  668. return ret
  669. def find_orphans(self, cmdline):
  670. '''Find orphaned processes matching the specified cmdline'''
  671. ret = []
  672. if six.PY3:
  673. cmdline = ' '.join(cmdline)
  674. for proc in psutils.process_iter():
  675. try:
  676. for item in proc.cmdline():
  677. if cmdline in item:
  678. ret.append(proc)
  679. except psutils.NoSuchProcess:
  680. # Process exited between when process_iter was invoked and
  681. # when we tried to invoke this instance's cmdline() func.
  682. continue
  683. else:
  684. cmd_len = len(cmdline)
  685. for proc in psutils.process_iter():
  686. try:
  687. proc_cmdline = proc.cmdline()
  688. except psutils.NoSuchProcess:
  689. # Process exited between when process_iter was invoked and
  690. # when we tried to invoke this instance's cmdline() func.
  691. continue
  692. if any((cmdline == proc_cmdline[n:n + cmd_len])
  693. for n in range(len(proc_cmdline) - cmd_len + 1)):
  694. ret.append(proc)
  695. return ret
  696. def shutdown(self, signum=signal.SIGTERM, timeout=10, wait_for_orphans=0):
  697. '''Shutdown a running daemon'''
  698. if not self._shutdown:
  699. try:
  700. pid = self.wait_for_daemon_pid(timeout)
  701. terminate_process(pid=pid, kill_children=True)
  702. except TimeoutError:
  703. pass
  704. if self.process:
  705. terminate_process(pid=self.process.pid, kill_children=True)
  706. self.process.wait()
  707. if wait_for_orphans:
  708. # NOTE: The process for finding orphans is greedy, it just
  709. # looks for processes with the same cmdline which are owned by
  710. # PID 1.
  711. orphans = self.find_orphans(self.argv)
  712. last = time.time()
  713. while True:
  714. if orphans:
  715. log.debug(
  716. 'Terminating orphaned child processes: %s',
  717. orphans
  718. )
  719. terminate_process_list(orphans)
  720. last = time.time()
  721. if (time.time() - last) >= wait_for_orphans:
  722. break
  723. time.sleep(0.25)
  724. orphans = self.find_orphans(self.argv)
  725. self.process = None
  726. self._shutdown = True
  727. def cleanup(self, *args, **kwargs):
  728. '''Remove left-over scaffolding - antithesis of setup()'''
  729. # Shutdown if not alreadt shutdown
  730. self.shutdown()
  731. super(TestDaemon, self).cleanup(*args, **kwargs)
  732. class TestSaltDaemon(six.with_metaclass(TestSaltProgramMeta, TestDaemon, TestSaltProgram)):
  733. '''
  734. A class to run arbitrary salt daemons (master, minion, syndic, etc.)
  735. '''
  736. pass
  737. class TestDaemonSaltMaster(TestSaltDaemon):
  738. '''
  739. Manager for salt-master daemon.
  740. '''
  741. configs = {'master': {}}
  742. def __init__(self, *args, **kwargs):
  743. cfgb = kwargs.setdefault('config_base', {})
  744. _ = cfgb.setdefault('user', getpass.getuser())
  745. super(TestDaemonSaltMaster, self).__init__(*args, **kwargs)
  746. class TestDaemonSaltMinion(TestSaltDaemon):
  747. '''
  748. Manager for salt-minion daemon.
  749. '''
  750. configs = {'minion': {'map': {'id': '{name}'}}}
  751. def __init__(self, *args, **kwargs):
  752. cfgb = kwargs.setdefault('config_base', {})
  753. _ = cfgb.setdefault('user', getpass.getuser())
  754. super(TestDaemonSaltMinion, self).__init__(*args, **kwargs)
  755. class TestDaemonSaltApi(TestSaltDaemon):
  756. '''
  757. Manager for salt-api daemon.
  758. '''
  759. pass
  760. class TestDaemonSaltSyndic(TestSaltDaemon):
  761. '''
  762. Manager for salt-syndic daemon.
  763. '''
  764. configs = {
  765. 'master': {'map': {'syndic_master': 'localhost'}},
  766. 'minion': {'map': {'id': '{name}'}},
  767. }
  768. def __init__(self, *args, **kwargs):
  769. cfgb = kwargs.setdefault('config_base', {})
  770. _ = cfgb.setdefault('user', getpass.getuser())
  771. super(TestDaemonSaltSyndic, self).__init__(*args, **kwargs)
  772. class TestDaemonSaltProxy(TestSaltDaemon):
  773. '''
  774. Manager for salt-proxy daemon.
  775. '''
  776. pid_file = 'salt-minion.pid'
  777. configs = {'proxy': {}}
  778. def __init__(self, *args, **kwargs):
  779. cfgb = kwargs.setdefault('config_base', {})
  780. _ = cfgb.setdefault('user', getpass.getuser())
  781. super(TestDaemonSaltProxy, self).__init__(*args, **kwargs)
  782. def run(self, **kwargs):
  783. if not kwargs.get('verbatim_args'):
  784. args = kwargs.setdefault('args', [])
  785. if '--proxyid' not in args:
  786. args.extend(['--proxyid', self.name])
  787. return super(TestDaemonSaltProxy, self).run(**kwargs)
  788. class TestProgramCase(TestCase):
  789. '''
  790. Utilities for unit tests that use TestProgram()
  791. '''
  792. def setUp(self):
  793. # Setup for scripts
  794. if not getattr(self, '_test_dir', None):
  795. self._test_dir = tempfile.mkdtemp(prefix='salt-testdaemon-')
  796. super(TestProgramCase, self).setUp()
  797. def tearDown(self):
  798. # shutdown for scripts
  799. if self._test_dir and os.path.sep == self._test_dir[0]:
  800. shutil.rmtree(self._test_dir)
  801. self._test_dir = None
  802. super(TestProgramCase, self).tearDown()
  803. def assert_exit_status(self, status, ex_status, message=None, stdout=None, stderr=None):
  804. '''
  805. Helper function to verify exit status and emit failure information.
  806. '''
  807. ex_val = getattr(exitcodes, ex_status)
  808. _message = '' if not message else ' ({0})'.format(message)
  809. _stdout = '' if not stdout else '\nstdout: {0}'.format(stdout)
  810. _stderr = '' if not stderr else '\nstderr: {0}'.format(stderr)
  811. self.assertEqual(
  812. status,
  813. ex_val,
  814. 'Exit status was {0}, must be {1} (salt.default.exitcodes.{2}){3}{4}{5}'.format(
  815. status,
  816. ex_val,
  817. ex_status,
  818. _message,
  819. _stdout,
  820. _stderr,
  821. )
  822. )