1
0

testprogram.py 35 KB

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