1""" 2 :codeauthor: Jayesh Kariya <jayeshk@saltstack.com> 3""" 4 5import logging 6 7import pytest 8import salt.modules.schedule as schedule 9import salt.utils.odict 10from salt.utils.event import SaltEvent 11from tests.support.mock import MagicMock, patch 12 13log = logging.getLogger(__name__) 14 15 16@pytest.fixture 17def job1(): 18 return { 19 "function": "test.ping", 20 "maxrunning": 1, 21 "name": "job1", 22 "jid_include": True, 23 "enabled": True, 24 } 25 26 27@pytest.fixture 28def sock_dir(tmp_path): 29 return str(tmp_path / "test-socks") 30 31 32@pytest.fixture 33def configure_loader_modules(): 34 return {schedule: {}} 35 36 37# 'purge' function tests: 1 38@pytest.mark.slow_test 39def test_purge(sock_dir): 40 """ 41 Test if it purge all the jobs currently scheduled on the minion. 42 """ 43 with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}): 44 mock = MagicMock(return_value=True) 45 with patch.dict(schedule.__salt__, {"event.fire": mock}): 46 _ret_value = {"complete": True, "schedule": {}} 47 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 48 assert schedule.purge() == { 49 "comment": ["Deleted job: schedule from schedule."], 50 "result": True, 51 } 52 53 54# 'delete' function tests: 1 55@pytest.mark.slow_test 56def test_delete(sock_dir): 57 """ 58 Test if it delete a job from the minion's schedule. 59 """ 60 with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}): 61 mock = MagicMock(return_value=True) 62 with patch.dict(schedule.__salt__, {"event.fire": mock}): 63 _ret_value = {"complete": True, "schedule": {}} 64 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 65 assert schedule.delete("job1") == { 66 "comment": "Job job1 does not exist.", 67 "changes": {}, 68 "result": False, 69 } 70 71 72# 'build_schedule_item' function tests: 1 73def test_build_schedule_item(sock_dir): 74 """ 75 Test if it build a schedule job. 76 """ 77 comment = ( 78 'Unable to use "seconds", "minutes", "hours", ' 79 'or "days" with "when" or "cron" options.' 80 ) 81 comment1 = 'Unable to use "when" and "cron" options together. Ignoring.' 82 with patch.dict(schedule.__opts__, {"job1": {}}): 83 assert schedule.build_schedule_item("") == { 84 "comment": "Job name is required.", 85 "result": False, 86 } 87 88 assert schedule.build_schedule_item("job1", function="test.ping") == { 89 "function": "test.ping", 90 "maxrunning": 1, 91 "name": "job1", 92 "jid_include": True, 93 "enabled": True, 94 } 95 96 assert schedule.build_schedule_item( 97 "job1", function="test.ping", seconds=3600, when="2400" 98 ) == {"comment": comment, "result": False} 99 100 assert schedule.build_schedule_item( 101 "job1", function="test.ping", when="2400", cron="2" 102 ) == {"comment": comment1, "result": False} 103 104 105# 'build_schedule_item_invalid_when' function tests: 1 106 107 108def test_build_schedule_item_invalid_when(sock_dir): 109 """ 110 Test if it build a schedule job. 111 """ 112 comment = 'Schedule item garbage for "when" in invalid.' 113 with patch.dict(schedule.__opts__, {"job1": {}}): 114 assert schedule.build_schedule_item( 115 "job1", function="test.ping", when="garbage" 116 ) == {"comment": comment, "result": False} 117 118 119# 'add' function tests: 1 120 121 122@pytest.mark.slow_test 123def test_add(sock_dir): 124 """ 125 Test if it add a job to the schedule. 126 """ 127 comm1 = "Job job1 already exists in schedule." 128 comm2 = ( 129 'Error: Unable to use "seconds", "minutes", "hours", ' 130 'or "days" with "when" or "cron" options.' 131 ) 132 comm3 = 'Unable to use "when" and "cron" options together. Ignoring.' 133 comm4 = "Job: job2 would be added to schedule." 134 with patch.dict( 135 schedule.__opts__, {"schedule": {"job1": "salt"}, "sock_dir": sock_dir} 136 ): 137 mock = MagicMock(return_value=True) 138 with patch.dict(schedule.__salt__, {"event.fire": mock}): 139 _ret_value = {"complete": True, "schedule": {"job1": {"salt": "salt"}}} 140 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 141 assert schedule.add("job1") == { 142 "comment": comm1, 143 "changes": {}, 144 "result": False, 145 } 146 147 _ret_value = {"complete": True, "schedule": {}} 148 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 149 assert schedule.add( 150 "job2", function="test.ping", seconds=3600, when="2400" 151 ) == {"comment": comm2, "changes": {}, "result": False} 152 153 _ret_value = {"complete": True, "schedule": {}} 154 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 155 assert schedule.add( 156 "job2", function="test.ping", when="2400", cron="2" 157 ) == {"comment": comm3, "changes": {}, "result": False} 158 _ret_value = {"complete": True, "schedule": {}} 159 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 160 assert schedule.add("job2", function="test.ping", test=True) == { 161 "comment": comm4, 162 "changes": {}, 163 "result": True, 164 } 165 166 167# 'run_job' function tests: 1 168 169 170@pytest.mark.slow_test 171def test_run_job(sock_dir, job1): 172 """ 173 Test if it run a scheduled job on the minion immediately. 174 """ 175 with patch.dict( 176 schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir} 177 ): 178 mock = MagicMock(return_value=True) 179 with patch.dict(schedule.__salt__, {"event.fire": mock}): 180 _ret_value = {"complete": True, "schedule": {"job1": job1}} 181 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 182 assert schedule.run_job("job1") == { 183 "comment": "Scheduling Job job1 on minion.", 184 "result": True, 185 } 186 187 188# 'enable_job' function tests: 1 189 190 191@pytest.mark.slow_test 192def test_enable_job(sock_dir): 193 """ 194 Test if it enable a job in the minion's schedule. 195 """ 196 with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}): 197 mock = MagicMock(return_value=True) 198 with patch.dict(schedule.__salt__, {"event.fire": mock}): 199 _ret_value = {"complete": True, "schedule": {}} 200 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 201 assert schedule.enable_job("job1") == { 202 "comment": "Job job1 does not exist.", 203 "changes": {}, 204 "result": False, 205 } 206 207 208# 'disable_job' function tests: 1 209 210 211@pytest.mark.slow_test 212def test_disable_job(sock_dir): 213 """ 214 Test if it disable a job in the minion's schedule. 215 """ 216 with patch.dict(schedule.__opts__, {"schedule": {}, "sock_dir": sock_dir}): 217 mock = MagicMock(return_value=True) 218 with patch.dict(schedule.__salt__, {"event.fire": mock}): 219 _ret_value = {"complete": True, "schedule": {}} 220 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 221 assert schedule.disable_job("job1") == { 222 "comment": "Job job1 does not exist.", 223 "changes": {}, 224 "result": False, 225 } 226 227 228# 'save' function tests: 1 229 230 231@pytest.mark.slow_test 232def test_save(sock_dir): 233 """ 234 Test if it save all scheduled jobs on the minion. 235 """ 236 comm1 = "Schedule (non-pillar items) saved." 237 with patch.dict( 238 schedule.__opts__, 239 {"schedule": {}, "default_include": "/tmp", "sock_dir": sock_dir}, 240 ): 241 242 mock = MagicMock(return_value=True) 243 with patch.dict(schedule.__salt__, {"event.fire": mock}): 244 _ret_value = {"complete": True, "schedule": {}} 245 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 246 assert schedule.save() == {"comment": comm1, "result": True} 247 248 249# 'enable' function tests: 1 250 251 252def test_enable(sock_dir): 253 """ 254 Test if it enable all scheduled jobs on the minion. 255 """ 256 assert schedule.enable(test=True) == { 257 "comment": "Schedule would be enabled.", 258 "changes": {}, 259 "result": True, 260 } 261 262 263# 'disable' function tests: 1 264 265 266def test_disable(sock_dir): 267 """ 268 Test if it disable all scheduled jobs on the minion. 269 """ 270 assert schedule.disable(test=True) == { 271 "comment": "Schedule would be disabled.", 272 "changes": {}, 273 "result": True, 274 } 275 276 277# 'move' function tests: 1 278 279 280@pytest.mark.slow_test 281def test_move(sock_dir, job1): 282 """ 283 Test if it move scheduled job to another minion or minions. 284 """ 285 comm1 = "no servers answered the published schedule.add command" 286 comm2 = "the following minions return False" 287 comm3 = "Moved Job job1 from schedule." 288 with patch.dict( 289 schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir} 290 ): 291 mock = MagicMock(return_value=True) 292 with patch.dict(schedule.__salt__, {"event.fire": mock}): 293 _ret_value = {"complete": True, "schedule": {"job1": job1}} 294 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 295 mock = MagicMock(return_value={}) 296 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 297 assert schedule.move("job1", "minion1") == { 298 "comment": comm1, 299 "result": True, 300 } 301 302 mock = MagicMock(return_value={"minion1": ""}) 303 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 304 assert schedule.move("job1", "minion1") == { 305 "comment": comm2, 306 "minions": ["minion1"], 307 "result": True, 308 } 309 310 mock = MagicMock(return_value={"minion1": "job1"}) 311 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 312 mock = MagicMock(return_value=True) 313 with patch.dict(schedule.__salt__, {"event.fire": mock}): 314 assert schedule.move("job1", "minion1") == { 315 "comment": comm3, 316 "minions": ["minion1"], 317 "result": True, 318 } 319 320 assert schedule.move("job3", "minion1") == { 321 "comment": "Job job3 does not exist.", 322 "result": False, 323 } 324 325 mock = MagicMock(side_effect=[{}, {"job1": {}}]) 326 with patch.dict(schedule.__opts__, {"schedule": mock, "sock_dir": sock_dir}): 327 mock = MagicMock(return_value=True) 328 with patch.dict(schedule.__salt__, {"event.fire": mock}): 329 _ret_value = {"complete": True, "schedule": {"job1": job1}} 330 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 331 with patch.dict(schedule.__pillar__, {"schedule": {"job1": job1}}): 332 mock = MagicMock(return_value={}) 333 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 334 assert schedule.move("job1", "minion1") == { 335 "comment": comm1, 336 "result": True, 337 } 338 339 mock = MagicMock(return_value={"minion1": ""}) 340 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 341 assert schedule.move("job1", "minion1") == { 342 "comment": comm2, 343 "minions": ["minion1"], 344 "result": True, 345 } 346 347 mock = MagicMock(return_value={"minion1": "job1"}) 348 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 349 mock = MagicMock(return_value=True) 350 with patch.dict(schedule.__salt__, {"event.fire": mock}): 351 assert schedule.move("job1", "minion1") == { 352 "comment": comm3, 353 "minions": ["minion1"], 354 "result": True, 355 } 356 357 358# 'copy' function tests: 1 359 360 361@pytest.mark.slow_test 362def test_copy(sock_dir, job1): 363 """ 364 Test if it copy scheduled job to another minion or minions. 365 """ 366 comm1 = "no servers answered the published schedule.add command" 367 comm2 = "the following minions return False" 368 comm3 = "Copied Job job1 from schedule to minion(s)." 369 with patch.dict( 370 schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir} 371 ): 372 mock = MagicMock(return_value=True) 373 with patch.dict(schedule.__salt__, {"event.fire": mock}): 374 _ret_value = {"complete": True, "schedule": {"job1": {"job1": job1}}} 375 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 376 mock = MagicMock(return_value={}) 377 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 378 assert schedule.copy("job1", "minion1") == { 379 "comment": comm1, 380 "result": True, 381 } 382 383 mock = MagicMock(return_value={"minion1": ""}) 384 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 385 assert schedule.copy("job1", "minion1") == { 386 "comment": comm2, 387 "minions": ["minion1"], 388 "result": True, 389 } 390 391 mock = MagicMock(return_value={"minion1": "job1"}) 392 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 393 mock = MagicMock(return_value=True) 394 with patch.dict(schedule.__salt__, {"event.fire": mock}): 395 assert schedule.copy("job1", "minion1") == { 396 "comment": comm3, 397 "minions": ["minion1"], 398 "result": True, 399 } 400 401 assert schedule.copy("job3", "minion1") == { 402 "comment": "Job job3 does not exist.", 403 "result": False, 404 } 405 406 mock = MagicMock(side_effect=[{}, {"job1": {}}]) 407 with patch.dict(schedule.__opts__, {"schedule": mock, "sock_dir": sock_dir}): 408 with patch.dict(schedule.__pillar__, {"schedule": {"job1": job1}}): 409 mock = MagicMock(return_value=True) 410 with patch.dict(schedule.__salt__, {"event.fire": mock}): 411 _ret_value = { 412 "complete": True, 413 "schedule": {"job1": {"job1": job1}}, 414 } 415 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 416 417 mock = MagicMock(return_value={}) 418 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 419 assert schedule.copy("job1", "minion1") == { 420 "comment": comm1, 421 "result": True, 422 } 423 424 mock = MagicMock(return_value={"minion1": ""}) 425 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 426 assert schedule.copy("job1", "minion1") == { 427 "comment": comm2, 428 "minions": ["minion1"], 429 "result": True, 430 } 431 432 mock = MagicMock(return_value={"minion1": "job1"}) 433 with patch.dict(schedule.__salt__, {"publish.publish": mock}): 434 mock = MagicMock(return_value=True) 435 with patch.dict(schedule.__salt__, {"event.fire": mock}): 436 assert schedule.copy("job1", "minion1") == { 437 "comment": comm3, 438 "minions": ["minion1"], 439 "result": True, 440 } 441 442 443# 'modify' function tests: 1 444 445 446@pytest.mark.slow_test 447def test_modify(sock_dir): 448 """ 449 Test if modifying job to the schedule. 450 """ 451 current_job1 = { 452 "function": "salt", 453 "seconds": "3600", 454 "maxrunning": 1, 455 "name": "job1", 456 "enabled": True, 457 "jid_include": True, 458 } 459 460 new_job1 = { 461 "function": "salt", 462 "seconds": "60", 463 "maxrunning": 1, 464 "name": "job1", 465 "enabled": True, 466 "jid_include": True, 467 } 468 469 comm1 = "Modified job: job1 in schedule." 470 changes1 = { 471 "job1": { 472 "new": salt.utils.odict.OrderedDict(new_job1), 473 "old": salt.utils.odict.OrderedDict(current_job1), 474 } 475 } 476 477 new_job4 = { 478 "function": "test.version", 479 "seconds": "3600", 480 "maxrunning": 1, 481 "name": "job1", 482 "enabled": True, 483 "jid_include": True, 484 } 485 486 changes4 = { 487 "job1": { 488 "new": salt.utils.odict.OrderedDict(new_job4), 489 "old": salt.utils.odict.OrderedDict(current_job1), 490 } 491 } 492 493 expected1 = {"comment": comm1, "changes": changes1, "result": True} 494 495 comm2 = ( 496 'Error: Unable to use "seconds", "minutes", "hours", ' 497 'or "days" with "when" option.' 498 ) 499 expected2 = {"comment": comm2, "changes": {}, "result": False} 500 501 comm3 = 'Unable to use "when" and "cron" options together. Ignoring.' 502 expected3 = {"comment": comm3, "changes": {}, "result": False} 503 504 comm4 = "Job: job1 would be modified in schedule." 505 expected4 = {"comment": comm4, "changes": changes4, "result": True} 506 507 comm5 = "Job job2 does not exist in schedule." 508 expected5 = {"comment": comm5, "changes": {}, "result": False} 509 510 with patch.dict( 511 schedule.__opts__, {"schedule": {"job1": current_job1}, "sock_dir": sock_dir} 512 ): 513 mock = MagicMock(return_value=True) 514 with patch.dict(schedule.__salt__, {"event.fire": mock}): 515 _ret_value = {"complete": True, "schedule": {"job1": current_job1}} 516 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 517 ret = schedule.modify("job1", seconds="60") 518 assert "job1" in ret["changes"] 519 assert "new" in ret["changes"]["job1"] 520 assert "old" in ret["changes"]["job1"] 521 522 for key in [ 523 "maxrunning", 524 "function", 525 "seconds", 526 "jid_include", 527 "name", 528 "enabled", 529 ]: 530 assert ( 531 ret["changes"]["job1"]["new"][key] 532 == expected1["changes"]["job1"]["new"][key] 533 ) 534 assert ( 535 ret["changes"]["job1"]["old"][key] 536 == expected1["changes"]["job1"]["old"][key] 537 ) 538 539 assert ret["comment"] == expected1["comment"] 540 assert ret["result"] == expected1["result"] 541 542 _ret_value = {"complete": True, "schedule": {"job1": current_job1}} 543 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 544 ret = schedule.modify( 545 "job1", function="test.ping", seconds=3600, when="2400" 546 ) 547 assert ret == expected2 548 549 _ret_value = {"complete": True, "schedule": {"job1": current_job1}} 550 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 551 ret = schedule.modify( 552 "job1", function="test.ping", when="2400", cron="2" 553 ) 554 assert ret == expected3 555 556 _ret_value = {"complete": True, "schedule": {"job1": current_job1}} 557 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 558 ret = schedule.modify("job1", function="test.version", test=True) 559 560 assert "job1" in ret["changes"] 561 assert "new" in ret["changes"]["job1"] 562 assert "old" in ret["changes"]["job1"] 563 564 for key in [ 565 "maxrunning", 566 "function", 567 "seconds", 568 "jid_include", 569 "name", 570 "enabled", 571 ]: 572 assert ( 573 ret["changes"]["job1"]["new"][key] 574 == expected4["changes"]["job1"]["new"][key] 575 ) 576 assert ( 577 ret["changes"]["job1"]["old"][key] 578 == expected4["changes"]["job1"]["old"][key] 579 ) 580 581 assert ret["comment"] == expected4["comment"] 582 assert ret["result"] == expected4["result"] 583 584 _ret_value = {"complete": True, "schedule": {}} 585 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 586 ret = schedule.modify("job2", function="test.version", test=True) 587 assert ret == expected5 588 589 590# 'is_enabled' function tests: 1 591 592 593def test_is_enabled(sock_dir): 594 """ 595 Test is_enabled 596 """ 597 job1 = {"function": "salt", "seconds": 3600} 598 599 comm1 = "Modified job: job1 in schedule." 600 601 mock_schedule = {"enabled": True, "job1": job1} 602 603 mock_lst = MagicMock(return_value=mock_schedule) 604 605 with patch.dict( 606 schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir} 607 ): 608 mock = MagicMock(return_value=True) 609 with patch.dict( 610 schedule.__salt__, {"event.fire": mock, "schedule.list": mock_lst} 611 ): 612 _ret_value = {"complete": True, "schedule": {"job1": job1}} 613 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 614 ret = schedule.is_enabled("job1") 615 assert ret == job1 616 617 ret = schedule.is_enabled() 618 assert ret 619 620 621# 'job_status' function tests: 1 622 623 624def test_job_status(sock_dir): 625 """ 626 Test is_enabled 627 """ 628 job1 = {"function": "salt", "seconds": 3600} 629 630 comm1 = "Modified job: job1 in schedule." 631 632 mock_schedule = {"enabled": True, "job1": job1} 633 634 mock_lst = MagicMock(return_value=mock_schedule) 635 636 with patch.dict( 637 schedule.__opts__, {"schedule": {"job1": job1}, "sock_dir": sock_dir} 638 ): 639 mock = MagicMock(return_value=True) 640 with patch.dict(schedule.__salt__, {"event.fire": mock}): 641 _ret_value = {"complete": True, "data": job1} 642 with patch.object(SaltEvent, "get_event", return_value=_ret_value): 643 ret = schedule.job_status("job1") 644 assert ret == job1 645