testprogram.py 34 KB

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