test_schedule.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Nicole Thomas <nicole@saltstack.com>
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import copy
  7. import datetime
  8. import logging
  9. import salt.config
  10. import salt.utils.schedule
  11. from salt.utils.schedule import Schedule
  12. from tests.support.helpers import slowTest
  13. from tests.support.mock import MagicMock, patch
  14. from tests.support.unit import skipIf
  15. from tests.unit.utils.scheduler.base import SchedulerTestsBase
  16. # pylint: disable=import-error,unused-import
  17. try:
  18. import croniter
  19. _CRON_SUPPORTED = True
  20. except ImportError:
  21. _CRON_SUPPORTED = False
  22. # pylint: enable=import-error
  23. log = logging.getLogger(__name__)
  24. # pylint: disable=too-many-public-methods,invalid-name
  25. class ScheduleTestCase(SchedulerTestsBase):
  26. """
  27. Unit tests for salt.utils.schedule module
  28. """
  29. # delete_job tests
  30. @slowTest
  31. def test_delete_job_exists(self):
  32. """
  33. Tests ensuring the job exists and deleting it
  34. """
  35. self.schedule.opts.update({"schedule": {"foo": "bar"}, "pillar": {}})
  36. self.assertIn("foo", self.schedule.opts["schedule"])
  37. self.schedule.delete_job("foo")
  38. self.assertNotIn("foo", self.schedule.opts["schedule"])
  39. @slowTest
  40. def test_delete_job_in_pillar(self):
  41. """
  42. Tests ignoring deletion job from pillar
  43. """
  44. self.schedule.opts.update(
  45. {"pillar": {"schedule": {"foo": "bar"}}, "schedule": {}}
  46. )
  47. self.assertIn("foo", self.schedule.opts["pillar"]["schedule"])
  48. self.schedule.delete_job("foo")
  49. self.assertIn("foo", self.schedule.opts["pillar"]["schedule"])
  50. @slowTest
  51. def test_delete_job_intervals(self):
  52. """
  53. Tests removing job from intervals
  54. """
  55. self.schedule.opts.update({"pillar": {}, "schedule": {}})
  56. self.schedule.intervals = {"foo": "bar"}
  57. self.schedule.delete_job("foo")
  58. self.assertNotIn("foo", self.schedule.intervals)
  59. @slowTest
  60. def test_delete_job_prefix(self):
  61. """
  62. Tests ensuring jobs exists and deleting them by prefix
  63. """
  64. self.schedule.opts.update(
  65. {
  66. "schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"},
  67. "pillar": {},
  68. }
  69. )
  70. ret = copy.deepcopy(self.schedule.opts)
  71. del ret["schedule"]["foobar"]
  72. del ret["schedule"]["foobaz"]
  73. self.schedule.delete_job_prefix("fooba")
  74. self.assertEqual(self.schedule.opts, ret)
  75. @slowTest
  76. def test_delete_job_prefix_in_pillar(self):
  77. """
  78. Tests ignoring deletion jobs by prefix from pillar
  79. """
  80. self.schedule.opts.update(
  81. {
  82. "pillar": {
  83. "schedule": {"foobar": "bar", "foobaz": "baz", "fooboo": "boo"}
  84. },
  85. "schedule": {},
  86. }
  87. )
  88. ret = copy.deepcopy(self.schedule.opts)
  89. self.schedule.delete_job_prefix("fooba")
  90. self.assertEqual(self.schedule.opts, ret)
  91. # add_job tests
  92. def test_add_job_data_not_dict(self):
  93. """
  94. Tests if data is a dictionary
  95. """
  96. data = "foo"
  97. self.assertRaises(ValueError, Schedule.add_job, self.schedule, data)
  98. def test_add_job_multiple_jobs(self):
  99. """
  100. Tests if more than one job is scheduled at a time
  101. """
  102. data = {"key1": "value1", "key2": "value2"}
  103. self.assertRaises(ValueError, Schedule.add_job, self.schedule, data)
  104. @slowTest
  105. def test_add_job(self):
  106. """
  107. Tests adding a job to the schedule
  108. """
  109. data = {"foo": {"bar": "baz"}}
  110. ret = copy.deepcopy(self.schedule.opts)
  111. ret.update(
  112. {
  113. "schedule": {
  114. "foo": {"bar": "baz", "enabled": True},
  115. "hello": {"world": "peace", "enabled": True},
  116. },
  117. "pillar": {},
  118. }
  119. )
  120. self.schedule.opts.update(
  121. {"schedule": {"hello": {"world": "peace", "enabled": True}}, "pillar": {}}
  122. )
  123. Schedule.add_job(self.schedule, data)
  124. self.assertEqual(self.schedule.opts, ret)
  125. # enable_job tests
  126. @slowTest
  127. def test_enable_job(self):
  128. """
  129. Tests enabling a job
  130. """
  131. self.schedule.opts.update({"schedule": {"name": {"enabled": "foo"}}})
  132. Schedule.enable_job(self.schedule, "name")
  133. self.assertTrue(self.schedule.opts["schedule"]["name"]["enabled"])
  134. @slowTest
  135. def test_enable_job_pillar(self):
  136. """
  137. Tests ignoring enable a job from pillar
  138. """
  139. self.schedule.opts.update(
  140. {"pillar": {"schedule": {"name": {"enabled": False}}}}
  141. )
  142. Schedule.enable_job(self.schedule, "name", persist=False)
  143. self.assertFalse(self.schedule.opts["pillar"]["schedule"]["name"]["enabled"])
  144. # disable_job tests
  145. @slowTest
  146. def test_disable_job(self):
  147. """
  148. Tests disabling a job
  149. """
  150. self.schedule.opts.update(
  151. {"schedule": {"name": {"enabled": "foo"}}, "pillar": {}}
  152. )
  153. Schedule.disable_job(self.schedule, "name")
  154. self.assertFalse(self.schedule.opts["schedule"]["name"]["enabled"])
  155. @slowTest
  156. def test_disable_job_pillar(self):
  157. """
  158. Tests ignoring disable a job in pillar
  159. """
  160. self.schedule.opts.update(
  161. {"pillar": {"schedule": {"name": {"enabled": True}}}, "schedule": {}}
  162. )
  163. Schedule.disable_job(self.schedule, "name", persist=False)
  164. self.assertTrue(self.schedule.opts["pillar"]["schedule"]["name"]["enabled"])
  165. # modify_job tests
  166. @slowTest
  167. def test_modify_job(self):
  168. """
  169. Tests modifying a job in the scheduler
  170. """
  171. schedule = {"foo": "bar"}
  172. self.schedule.opts.update({"schedule": {"name": "baz"}, "pillar": {}})
  173. ret = copy.deepcopy(self.schedule.opts)
  174. ret.update({"schedule": {"name": {"foo": "bar"}}})
  175. Schedule.modify_job(self.schedule, "name", schedule)
  176. self.assertEqual(self.schedule.opts, ret)
  177. def test_modify_job_not_exists(self):
  178. """
  179. Tests modifying a job in the scheduler if jobs not exists
  180. """
  181. schedule = {"foo": "bar"}
  182. self.schedule.opts.update({"schedule": {}, "pillar": {}})
  183. ret = copy.deepcopy(self.schedule.opts)
  184. ret.update({"schedule": {"name": {"foo": "bar"}}})
  185. Schedule.modify_job(self.schedule, "name", schedule)
  186. self.assertEqual(self.schedule.opts, ret)
  187. def test_modify_job_pillar(self):
  188. """
  189. Tests ignoring modification of job from pillar
  190. """
  191. schedule = {"foo": "bar"}
  192. self.schedule.opts.update(
  193. {"schedule": {}, "pillar": {"schedule": {"name": "baz"}}}
  194. )
  195. ret = copy.deepcopy(self.schedule.opts)
  196. Schedule.modify_job(self.schedule, "name", schedule, persist=False)
  197. self.assertEqual(self.schedule.opts, ret)
  198. maxDiff = None
  199. # enable_schedule tests
  200. @slowTest
  201. def test_enable_schedule(self):
  202. """
  203. Tests enabling the scheduler
  204. """
  205. with patch(
  206. "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
  207. ) as persist_mock:
  208. self.schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
  209. Schedule.enable_schedule(self.schedule)
  210. self.assertTrue(self.schedule.opts["schedule"]["enabled"])
  211. persist_mock.assert_called()
  212. # disable_schedule tests
  213. @slowTest
  214. def test_disable_schedule(self):
  215. """
  216. Tests disabling the scheduler
  217. """
  218. with patch(
  219. "salt.utils.schedule.Schedule.persist", MagicMock(return_value=None)
  220. ) as persist_mock:
  221. self.schedule.opts.update({"schedule": {"enabled": "foo"}, "pillar": {}})
  222. Schedule.disable_schedule(self.schedule)
  223. self.assertFalse(self.schedule.opts["schedule"]["enabled"])
  224. persist_mock.assert_called()
  225. # reload tests
  226. def test_reload_update_schedule_key(self):
  227. """
  228. Tests reloading the schedule from saved schedule where both the
  229. saved schedule and self.schedule.opts contain a schedule key
  230. """
  231. saved = {"schedule": {"foo": "bar"}}
  232. ret = copy.deepcopy(self.schedule.opts)
  233. ret.update({"schedule": {"foo": "bar", "hello": "world"}})
  234. self.schedule.opts.update({"schedule": {"hello": "world"}})
  235. Schedule.reload(self.schedule, saved)
  236. self.assertEqual(self.schedule.opts, ret)
  237. def test_reload_update_schedule_no_key(self):
  238. """
  239. Tests reloading the schedule from saved schedule that does not
  240. contain a schedule key but self.schedule.opts does
  241. """
  242. saved = {"foo": "bar"}
  243. ret = copy.deepcopy(self.schedule.opts)
  244. ret.update({"schedule": {"foo": "bar", "hello": "world"}})
  245. self.schedule.opts.update({"schedule": {"hello": "world"}})
  246. Schedule.reload(self.schedule, saved)
  247. self.assertEqual(self.schedule.opts, ret)
  248. def test_reload_no_schedule_in_opts(self):
  249. """
  250. Tests reloading the schedule from saved schedule that does not
  251. contain a schedule key and neither does self.schedule.opts
  252. """
  253. saved = {"foo": "bar"}
  254. ret = copy.deepcopy(self.schedule.opts)
  255. ret["schedule"] = {"foo": "bar"}
  256. self.schedule.opts.pop("schedule", None)
  257. Schedule.reload(self.schedule, saved)
  258. self.assertEqual(self.schedule.opts, ret)
  259. def test_reload_schedule_in_saved_but_not_opts(self):
  260. """
  261. Tests reloading the schedule from saved schedule that contains
  262. a schedule key, but self.schedule.opts does not
  263. """
  264. saved = {"schedule": {"foo": "bar"}}
  265. ret = copy.deepcopy(self.schedule.opts)
  266. ret["schedule"] = {"foo": "bar"}
  267. self.schedule.opts.pop("schedule", None)
  268. Schedule.reload(self.schedule, saved)
  269. self.assertEqual(self.schedule.opts, ret)
  270. # eval tests
  271. def test_eval_schedule_is_not_dict(self):
  272. """
  273. Tests eval if the schedule is not a dictionary
  274. """
  275. self.schedule.opts.update({"schedule": "", "pillar": {"schedule": {}}})
  276. self.assertRaises(ValueError, Schedule.eval, self.schedule)
  277. def test_eval_schedule_is_not_dict_in_pillar(self):
  278. """
  279. Tests eval if the schedule from pillar is not a dictionary
  280. """
  281. self.schedule.opts.update({"schedule": {}, "pillar": {"schedule": ""}})
  282. self.assertRaises(ValueError, Schedule.eval, self.schedule)
  283. def test_eval_schedule_time(self):
  284. """
  285. Tests eval if the schedule setting time is in the future
  286. """
  287. self.schedule.opts.update({"pillar": {"schedule": {}}})
  288. self.schedule.opts.update(
  289. {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
  290. )
  291. now = datetime.datetime.now()
  292. self.schedule.eval()
  293. self.assertTrue(
  294. self.schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
  295. )
  296. def test_eval_schedule_time_eval(self):
  297. """
  298. Tests eval if the schedule setting time is in the future plus splay
  299. """
  300. self.schedule.opts.update({"pillar": {"schedule": {}}})
  301. self.schedule.opts.update(
  302. {
  303. "schedule": {
  304. "testjob": {"function": "test.true", "seconds": 60, "splay": 5}
  305. }
  306. }
  307. )
  308. now = datetime.datetime.now()
  309. self.schedule.eval()
  310. self.assertTrue(
  311. self.schedule.opts["schedule"]["testjob"]["_splay"] - now
  312. > datetime.timedelta(seconds=60)
  313. )
  314. @skipIf(not _CRON_SUPPORTED, "croniter module not installed")
  315. def test_eval_schedule_cron(self):
  316. """
  317. Tests eval if the schedule is defined with cron expression
  318. """
  319. self.schedule.opts.update({"pillar": {"schedule": {}}})
  320. self.schedule.opts.update(
  321. {"schedule": {"testjob": {"function": "test.true", "cron": "* * * * *"}}}
  322. )
  323. now = datetime.datetime.now()
  324. self.schedule.eval()
  325. self.assertTrue(
  326. self.schedule.opts["schedule"]["testjob"]["_next_fire_time"] > now
  327. )
  328. @skipIf(not _CRON_SUPPORTED, "croniter module not installed")
  329. def test_eval_schedule_cron_splay(self):
  330. """
  331. Tests eval if the schedule is defined with cron expression plus splay
  332. """
  333. self.schedule.opts.update({"pillar": {"schedule": {}}})
  334. self.schedule.opts.update(
  335. {
  336. "schedule": {
  337. "testjob": {
  338. "function": "test.true",
  339. "cron": "* * * * *",
  340. "splay": 5,
  341. }
  342. }
  343. }
  344. )
  345. self.schedule.eval()
  346. self.assertTrue(
  347. self.schedule.opts["schedule"]["testjob"]["_splay"]
  348. > self.schedule.opts["schedule"]["testjob"]["_next_fire_time"]
  349. )
  350. @slowTest
  351. def test_handle_func_schedule_minion_blackout(self):
  352. """
  353. Tests eval if the schedule from pillar is not a dictionary
  354. """
  355. self.schedule.opts.update({"pillar": {"schedule": {}}})
  356. self.schedule.opts.update({"grains": {"minion_blackout": True}})
  357. self.schedule.opts.update(
  358. {"schedule": {"testjob": {"function": "test.true", "seconds": 60}}}
  359. )
  360. data = {
  361. "function": "test.true",
  362. "_next_scheduled_fire_time": datetime.datetime(
  363. 2018, 11, 21, 14, 9, 53, 903438
  364. ),
  365. "run": True,
  366. "name": "testjob",
  367. "seconds": 60,
  368. "_splay": None,
  369. "_seconds": 60,
  370. "jid_include": True,
  371. "maxrunning": 1,
  372. "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
  373. }
  374. with patch.object(salt.utils.schedule, "log") as log_mock:
  375. with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
  376. self.schedule.handle_func(False, "test.ping", data)
  377. self.assertTrue(log_mock.exception.called)
  378. def test_handle_func_check_data(self):
  379. """
  380. Tests handle_func to ensure that __pub_fun_args is not
  381. being duplicated in the value of kwargs in data.
  382. """
  383. self.schedule.standalone = True
  384. data = {
  385. "function": "test.arg",
  386. "_next_scheduled_fire_time": datetime.datetime(
  387. 2018, 11, 21, 14, 9, 53, 903438
  388. ),
  389. "run": True,
  390. "args": ["arg1", "arg2"],
  391. "kwargs": {"key1": "value1", "key2": "value2"},
  392. "name": "testjob",
  393. "seconds": 60,
  394. "_splay": None,
  395. "_seconds": 60,
  396. "jid_include": True,
  397. "maxrunning": 1,
  398. "_next_fire_time": datetime.datetime(2018, 11, 21, 14, 8, 53, 903438),
  399. }
  400. with patch("salt.utils.process.daemonize"), patch("sys.platform", "linux2"):
  401. # run handle_func once
  402. self.schedule.handle_func(False, "test.arg", data)
  403. # run handle_func and ensure __pub_fun_args
  404. # is not in kwargs
  405. self.schedule.handle_func(False, "test.arg", data)
  406. self.assertIn("kwargs", data)
  407. self.assertNotIn("__pub_fun_args", data["kwargs"])