test_schedule.py 15 KB

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