testprogram.py 33 KB

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