testprogram.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  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.runtime import RUNTIME_VARS
  31. from tests.support.processes import terminate_process, terminate_process_list
  32. from tests.support.cli_scripts import ScriptPathMixin
  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 RUNTIME_VARS.CODE_DIR != env_pypath[0]:
  321. env_pypath.insert(0, RUNTIME_VARS.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. if salt.utils.platform.is_windows() and six.PY2:
  329. for k, v in cmd_env.items():
  330. if isinstance(k, six.text_type) or isinstance(v, six.text_type):
  331. cmd_env[k.encode('ascii')] = v.encode('ascii')
  332. popen_kwargs = {
  333. 'shell': self.shell,
  334. 'stdout': subprocess.PIPE,
  335. 'env': cmd_env,
  336. }
  337. if catch_stderr is True:
  338. popen_kwargs['stderr'] = subprocess.PIPE
  339. if not sys.platform.lower().startswith('win'):
  340. popen_kwargs['close_fds'] = True
  341. def detach_from_parent_group():
  342. '''
  343. A utility function that prevents child process from getting parent signals.
  344. '''
  345. os.setpgrp()
  346. popen_kwargs['preexec_fn'] = detach_from_parent_group
  347. if salt.utils.platform.is_windows():
  348. self.argv = ['python.exe', self.program]
  349. else:
  350. self.argv = [self.program]
  351. self.argv.extend(args)
  352. log.debug('TestProgram.run: %s Environment %s', self.argv, env_delta)
  353. process = subprocess.Popen(self.argv, **popen_kwargs)
  354. self.process = process
  355. if timeout is not None:
  356. stop_at = datetime.now() + timedelta(seconds=timeout)
  357. term_sent = False
  358. while True:
  359. process.poll()
  360. if datetime.now() > stop_at:
  361. try:
  362. terminate_process(pid=process.pid, kill_children=True)
  363. process.wait()
  364. except OSError as exc:
  365. if exc.errno != errno.ESRCH:
  366. raise
  367. out = process.stdout.read().splitlines()
  368. out.extend([
  369. 'Process took more than {0} seconds to complete. '
  370. 'Process Killed!'.format(timeout)
  371. ])
  372. if catch_stderr:
  373. err = process.stderr.read().splitlines()
  374. if with_retcode:
  375. return out, err, process.returncode
  376. else:
  377. return out, err
  378. if with_retcode:
  379. return out, process.returncode
  380. else:
  381. return out
  382. if process.returncode is not None:
  383. break
  384. if catch_stderr:
  385. if sys.version_info < (2, 7):
  386. # On python 2.6, the subprocess'es communicate() method uses
  387. # select which, is limited by the OS to 1024 file descriptors
  388. # We need more available descriptors to run the tests which
  389. # need the stderr output.
  390. # So instead of .communicate() we wait for the process to
  391. # finish, but, as the python docs state "This will deadlock
  392. # when using stdout=PIPE and/or stderr=PIPE and the child
  393. # process generates enough output to a pipe such that it
  394. # blocks waiting for the OS pipe buffer to accept more data.
  395. # Use communicate() to avoid that." <- a catch, catch situation
  396. #
  397. # Use this work around were it's needed only, python 2.6
  398. process.wait()
  399. out = process.stdout.read()
  400. err = process.stderr.read()
  401. else:
  402. out, err = process.communicate()
  403. # Force closing stderr/stdout to release file descriptors
  404. if process.stdout is not None:
  405. process.stdout.close()
  406. if process.stderr is not None:
  407. process.stderr.close()
  408. # pylint: disable=maybe-no-member
  409. try:
  410. if with_retcode:
  411. if out is not None and err is not None:
  412. if not raw:
  413. return out.splitlines(), err.splitlines(), process.returncode
  414. else:
  415. return out, err, process.returncode
  416. return out.splitlines(), [], process.returncode
  417. else:
  418. if out is not None and err is not None:
  419. if not raw:
  420. return out.splitlines(), err.splitlines()
  421. else:
  422. return out, err
  423. if not raw:
  424. return out.splitlines(), []
  425. else:
  426. return out, []
  427. finally:
  428. try:
  429. process.terminate()
  430. except OSError as err:
  431. # process already terminated
  432. pass
  433. # pylint: enable=maybe-no-member
  434. data = process.communicate()
  435. process.stdout.close()
  436. try:
  437. if with_retcode:
  438. if not raw:
  439. return data[0].splitlines(), process.returncode
  440. else:
  441. return data[0], process.returncode
  442. else:
  443. if not raw:
  444. return data[0].splitlines()
  445. else:
  446. return data[0]
  447. finally:
  448. try:
  449. process.terminate()
  450. except OSError as err:
  451. # process already terminated
  452. pass
  453. class TestSaltProgramMeta(TestProgramMeta):
  454. '''
  455. A Meta-class to set self.script from the class name when it is
  456. not specifically set by a "script" argument.
  457. '''
  458. def __new__(mcs, name, bases, attrs):
  459. if attrs.get('script') is None:
  460. if 'Salt' in name:
  461. script = 'salt-{0}'.format(name.rsplit('Salt', 1)[-1].lower())
  462. if script is None:
  463. raise AttributeError(
  464. 'Class {0}: Unable to set "script" attribute: class name'
  465. ' must include "Salt" or "script" must be explicitly set.'.format(name)
  466. )
  467. attrs['script'] = script
  468. config_base = {}
  469. configs = {}
  470. for base in bases:
  471. if 'Salt' not in base.__name__:
  472. continue
  473. config_base.update(getattr(base, 'config_base', {}))
  474. configs.update(getattr(base, 'configs', {}))
  475. config_base.update(attrs.get('config_base', {}))
  476. attrs['config_base'] = config_base
  477. configs.update(attrs.get('configs', {}))
  478. attrs['configs'] = configs
  479. return super(TestSaltProgramMeta, mcs).__new__(mcs, name, bases, attrs)
  480. class TestSaltProgram(six.with_metaclass(TestSaltProgramMeta, TestProgram, ScriptPathMixin)):
  481. '''
  482. This is like TestProgram but with some functions to run a salt-specific
  483. auxiliary program.
  484. '''
  485. config_types = (dict,)
  486. config_attrs = set([
  487. 'log_dir',
  488. 'script_dir',
  489. ])
  490. pub_port = 4505
  491. ret_port = 4506
  492. for port in [pub_port, ret_port]:
  493. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  494. try:
  495. connect = sock.bind(('localhost', port))
  496. except (socket.error, OSError):
  497. # these ports are already in use, use different ones
  498. pub_port = 4606
  499. ret_port = 4607
  500. break
  501. sock.close()
  502. config_base = {
  503. 'root_dir': '{test_dir}',
  504. 'publish_port': pub_port,
  505. 'ret_port': ret_port,
  506. }
  507. configs = {}
  508. config_dir = os.path.join('etc', 'salt')
  509. log_dir = os.path.join('var', 'log', 'salt')
  510. dirtree = [
  511. '&log_dir',
  512. '&script_dir',
  513. ]
  514. script = ''
  515. script_dir = 'bin'
  516. @staticmethod
  517. def config_caster(cfg):
  518. return salt.utils.yaml.safe_load(cfg)
  519. def __init__(self, *args, **kwargs):
  520. if len(args) < 2 and 'program' not in kwargs:
  521. # This is effectively a place-holder - it gets set correctly after super()
  522. kwargs['program'] = self.script
  523. super(TestSaltProgram, self).__init__(*args, **kwargs)
  524. self.program = self.get_script_path(self.script)
  525. def config_merge(self, base, overrides):
  526. _base = self.config_cast(copy.deepcopy(base))
  527. _overrides = self.config_cast(overrides)
  528. # NOTE: this simple update will not work for deep dictionaries
  529. _base.update(copy.deepcopy(_overrides))
  530. return _base
  531. def config_get(self, config):
  532. cfg_base = {}
  533. for key, val in self.config_base.items():
  534. _val = val
  535. if val and isinstance(val, six.string_types) and val[0] == '&':
  536. _val = getattr(self, val[1:], None)
  537. if _val is None:
  538. continue
  539. cfg_base[key] = _val
  540. if config in self.configs:
  541. cfg = {}
  542. for key, val in self.configs.get(config, {}).get('map', {}).items():
  543. _val = val
  544. if val and isinstance(val, six.string_types) and val[0] == '&':
  545. _val = getattr(self, val[1:], None)
  546. if _val is None:
  547. continue
  548. cfg[key] = _val
  549. cfg = self.config_merge(cfg_base, cfg)
  550. log.debug('Generated config => {0}'.format(cfg))
  551. return cfg
  552. def config_stringify(self, config):
  553. '''Transform the configuration data into a string (suitable to write to a file)'''
  554. subs = self.config_subs()
  555. cfg = {}
  556. for key, val in self.config_get(config).items():
  557. if isinstance(val, six.string_types):
  558. cfg[key] = val.format(**subs)
  559. else:
  560. cfg[key] = val
  561. return salt.utils.yaml.safe_dump(cfg, default_flow_style=False)
  562. def run(self, **kwargs):
  563. if not kwargs.get('verbatim_args'):
  564. args = kwargs.setdefault('args', [])
  565. if '-c' not in args and '--config-dir' not in args:
  566. args.extend(['--config-dir', self.abs_path(self.config_dir)])
  567. return super(TestSaltProgram, self).run(**kwargs)
  568. class TestProgramSalt(TestSaltProgram):
  569. '''Class to manage salt'''
  570. configs = {'master': {}}
  571. script = 'salt'
  572. class TestProgramSaltCall(TestSaltProgram):
  573. '''Class to manage salt-call'''
  574. configs = {'minion': {'map': {'id': '{name}'}}}
  575. class TestProgramSaltRun(TestSaltProgram):
  576. '''Class to manage salt-run'''
  577. configs = {'master': {}}
  578. def __init__(self, *args, **kwargs):
  579. cfgb = kwargs.setdefault('config_base', {})
  580. _ = cfgb.setdefault('user', getpass.getuser())
  581. super(TestProgramSaltRun, self).__init__(*args, **kwargs)
  582. class TestDaemon(TestProgram):
  583. '''
  584. Run one of the standard daemons
  585. '''
  586. script = None
  587. pid_file = None
  588. pid_dir = os.path.join('var', 'run')
  589. dirtree = [
  590. '&pid_dir',
  591. ]
  592. def __init__(self, *args, **kwargs):
  593. self.script = kwargs.pop('script', self.script)
  594. self.pid_file = kwargs.pop('pid_file', self.pid_file if self.pid_file else '{0}.pid'.format(self.script))
  595. self.pid_dir = kwargs.pop('pid_dir', self.pid_dir)
  596. self._shutdown = False
  597. if not args and 'program' not in kwargs:
  598. # This is effectively a place-holder - it gets set correctly after super()
  599. kwargs['program'] = self.script
  600. super(TestDaemon, self).__init__(*args, **kwargs)
  601. @property
  602. def pid_path(self):
  603. '''Path to the pid file created by the daemon'''
  604. return os.path.join(self.pid_dir, self.pid_file) if os.path.sep not in self.pid_file else self.pid_file
  605. @property
  606. def daemon_pid(self):
  607. '''Return the daemon PID'''
  608. daemon_pid = None
  609. pid_path = self.abs_path(self.pid_path)
  610. if salt.utils.process.check_pidfile(pid_path):
  611. daemon_pid = salt.utils.process.get_pidfile(pid_path)
  612. return daemon_pid
  613. def wait_for_daemon_pid(self, timeout=10):
  614. '''Wait up to timeout seconds for the PID file to appear and return the PID'''
  615. endtime = time.time() + timeout
  616. while True:
  617. pid = self.daemon_pid
  618. if pid:
  619. return pid
  620. if endtime < time.time():
  621. raise TimeoutError('Timeout waiting for "{0}" pid in "{1}"'.format(
  622. self.name, self.abs_path(self.pid_path)
  623. ))
  624. time.sleep(0.2)
  625. def is_running(self):
  626. '''Is the daemon running?'''
  627. ret = False
  628. if not self._shutdown:
  629. try:
  630. pid = self.wait_for_daemon_pid()
  631. ret = psutils.pid_exists(pid)
  632. except TimeoutError:
  633. pass
  634. return ret
  635. def find_orphans(self, cmdline):
  636. '''Find orphaned processes matching the specified cmdline'''
  637. ret = []
  638. if six.PY3:
  639. cmdline = ' '.join(cmdline)
  640. for proc in psutils.process_iter():
  641. try:
  642. for item in proc.cmdline():
  643. if cmdline in item:
  644. ret.append(proc)
  645. except psutils.NoSuchProcess:
  646. # Process exited between when process_iter was invoked and
  647. # when we tried to invoke this instance's cmdline() func.
  648. continue
  649. except psutils.AccessDenied:
  650. # We might get access denied if not running as root
  651. if not salt.utils.platform.is_windows():
  652. pinfo = proc.as_dict(attrs=['pid', 'name', 'username'])
  653. log.error('Unable to access process %s, '
  654. 'running command %s as user %s',
  655. pinfo['pid'], pinfo['name'], pinfo['username'])
  656. continue
  657. else:
  658. cmd_len = len(cmdline)
  659. for proc in psutils.process_iter():
  660. try:
  661. proc_cmdline = proc.cmdline()
  662. except psutils.NoSuchProcess:
  663. # Process exited between when process_iter was invoked and
  664. # when we tried to invoke this instance's cmdline() func.
  665. continue
  666. except psutils.AccessDenied:
  667. # We might get access denied if not running as root
  668. if not salt.utils.platform.is_windows():
  669. pinfo = proc.as_dict(attrs=['pid', 'name', 'username'])
  670. log.error('Unable to access process %s, '
  671. 'running command %s as user %s',
  672. pinfo['pid'], pinfo['name'], pinfo['username'])
  673. continue
  674. if any((cmdline == proc_cmdline[n:n + cmd_len])
  675. for n in range(len(proc_cmdline) - cmd_len + 1)):
  676. ret.append(proc)
  677. return ret
  678. def shutdown(self, signum=signal.SIGTERM, timeout=10, wait_for_orphans=0):
  679. '''Shutdown a running daemon'''
  680. if not self._shutdown:
  681. try:
  682. pid = self.wait_for_daemon_pid(timeout)
  683. terminate_process(pid=pid, kill_children=True)
  684. except TimeoutError:
  685. pass
  686. if self.process:
  687. terminate_process(pid=self.process.pid, kill_children=True)
  688. self.process.wait()
  689. if wait_for_orphans:
  690. # NOTE: The process for finding orphans is greedy, it just
  691. # looks for processes with the same cmdline which are owned by
  692. # PID 1.
  693. orphans = self.find_orphans(self.argv)
  694. last = time.time()
  695. while True:
  696. if orphans:
  697. log.debug(
  698. 'Terminating orphaned child processes: %s',
  699. orphans
  700. )
  701. terminate_process_list(orphans)
  702. last = time.time()
  703. if (time.time() - last) >= wait_for_orphans:
  704. break
  705. time.sleep(0.25)
  706. orphans = self.find_orphans(self.argv)
  707. self.process = None
  708. self._shutdown = True
  709. def cleanup(self, *args, **kwargs):
  710. '''Remove left-over scaffolding - antithesis of setup()'''
  711. # Shutdown if not alreadt shutdown
  712. self.shutdown()
  713. super(TestDaemon, self).cleanup(*args, **kwargs)
  714. class TestSaltDaemon(six.with_metaclass(TestSaltProgramMeta, TestDaemon, TestSaltProgram)):
  715. '''
  716. A class to run arbitrary salt daemons (master, minion, syndic, etc.)
  717. '''
  718. pass
  719. class TestDaemonSaltMaster(TestSaltDaemon):
  720. '''
  721. Manager for salt-master daemon.
  722. '''
  723. configs = {'master': {}}
  724. def __init__(self, *args, **kwargs):
  725. cfgb = kwargs.setdefault('config_base', {})
  726. _ = cfgb.setdefault('user', getpass.getuser())
  727. super(TestDaemonSaltMaster, self).__init__(*args, **kwargs)
  728. class TestDaemonSaltMinion(TestSaltDaemon):
  729. '''
  730. Manager for salt-minion daemon.
  731. '''
  732. configs = {'minion': {'map': {'id': '{name}'}}}
  733. def __init__(self, *args, **kwargs):
  734. cfgb = kwargs.setdefault('config_base', {})
  735. _ = cfgb.setdefault('user', getpass.getuser())
  736. super(TestDaemonSaltMinion, self).__init__(*args, **kwargs)
  737. class TestDaemonSaltApi(TestSaltDaemon):
  738. '''
  739. Manager for salt-api daemon.
  740. '''
  741. pass
  742. class TestDaemonSaltSyndic(TestSaltDaemon):
  743. '''
  744. Manager for salt-syndic daemon.
  745. '''
  746. configs = {
  747. 'master': {'map': {'syndic_master': 'localhost'}},
  748. 'minion': {'map': {'id': '{name}'}},
  749. }
  750. def __init__(self, *args, **kwargs):
  751. cfgb = kwargs.setdefault('config_base', {})
  752. _ = cfgb.setdefault('user', getpass.getuser())
  753. super(TestDaemonSaltSyndic, self).__init__(*args, **kwargs)
  754. class TestDaemonSaltProxy(TestSaltDaemon):
  755. '''
  756. Manager for salt-proxy daemon.
  757. '''
  758. pid_file = 'salt-minion.pid'
  759. configs = {'proxy': {}}
  760. def __init__(self, *args, **kwargs):
  761. cfgb = kwargs.setdefault('config_base', {})
  762. _ = cfgb.setdefault('user', getpass.getuser())
  763. super(TestDaemonSaltProxy, self).__init__(*args, **kwargs)
  764. def run(self, **kwargs):
  765. if not kwargs.get('verbatim_args'):
  766. args = kwargs.setdefault('args', [])
  767. if '--proxyid' not in args:
  768. args.extend(['--proxyid', self.name])
  769. return super(TestDaemonSaltProxy, self).run(**kwargs)
  770. class TestProgramCase(TestCase):
  771. '''
  772. Utilities for unit tests that use TestProgram()
  773. '''
  774. def setUp(self):
  775. # Setup for scripts
  776. if not getattr(self, '_test_dir', None):
  777. self._test_dir = tempfile.mkdtemp(prefix='salt-testdaemon-')
  778. super(TestProgramCase, self).setUp()
  779. def tearDown(self):
  780. # shutdown for scripts
  781. if self._test_dir and os.path.sep == self._test_dir[0]:
  782. shutil.rmtree(self._test_dir)
  783. self._test_dir = None
  784. super(TestProgramCase, self).tearDown()
  785. def assert_exit_status(self, status, ex_status, message=None, stdout=None, stderr=None):
  786. '''
  787. Helper function to verify exit status and emit failure information.
  788. '''
  789. ex_val = getattr(exitcodes, ex_status)
  790. _message = '' if not message else ' ({0})'.format(message)
  791. _stdout = '' if not stdout else '\nstdout: {0}'.format(stdout)
  792. _stderr = '' if not stderr else '\nstderr: {0}'.format(stderr)
  793. self.assertEqual(
  794. status,
  795. ex_val,
  796. 'Exit status was {0}, must be {1} (salt.default.exitcodes.{2}){3}{4}{5}'.format(
  797. status,
  798. ex_val,
  799. ex_status,
  800. _message,
  801. _stdout,
  802. _stderr,
  803. )
  804. )