diff --git a/changelog.d/3229.added.md b/changelog.d/3229.added.md new file mode 100644 index 0000000000..3c65d655f1 --- /dev/null +++ b/changelog.d/3229.added.md @@ -0,0 +1 @@ +Active maintenance tasks that only reference deleted components will be automatically cancelled diff --git a/python/nav/maintengine.py b/python/nav/maintengine.py index 55204da143..5176d043c7 100644 --- a/python/nav/maintengine.py +++ b/python/nav/maintengine.py @@ -86,10 +86,8 @@ def check_tasks_without_end(): @transaction.atomic() def do_state_transitions(): - """ - Finds active or scheduled tasks that have run out and sets them as passed, - and finds scheduled scheduled tasks in the current window and sets them - as active. + """Finds active or scheduled tasks that have run out and sets them as passed, + and finds scheduled tasks in the current window and sets them as active. """ tasks = MaintenanceTask.objects.past().filter( state__in=[MaintenanceTask.STATE_ACTIVE, MaintenanceTask.STATE_SCHEDULED] @@ -105,6 +103,20 @@ def do_state_transitions(): _logger.debug("Tasks transitioned to active state: %r", tasks) + cancel_tasks_without_components() + + +def cancel_tasks_without_components(): + """Cancels active tasks where all components are missing""" + tasks = MaintenanceTask.objects.filter( + state=MaintenanceTask.STATE_ACTIVE + ).prefetch_related('maintenance_components') + for task in tasks: + if not any(task.get_components()): + task.state = MaintenanceTask.STATE_CANCELED + task.save() + _logger.debug("Task %r canceled because all components were missing", task) + def check_state_differences(): """ diff --git a/tests/integration/maintengine_test.py b/tests/integration/maintengine_test.py new file mode 100644 index 0000000000..ba6cbcf789 --- /dev/null +++ b/tests/integration/maintengine_test.py @@ -0,0 +1,61 @@ +from datetime import datetime, timedelta + +import pytest + +from nav import maintengine +from nav.models.msgmaint import MaintenanceTask, MaintenanceComponent + + +class TestCancelTasksWithoutComponents: + def test_it_should_cancel_active_empty_tasks(self, empty_task): + assert empty_task.state == MaintenanceTask.STATE_ACTIVE + maintengine.cancel_tasks_without_components() + empty_task.refresh_from_db() + assert empty_task.state == MaintenanceTask.STATE_CANCELED + + def test_it_should_not_cancel_scheduled_empty_tasks(self, scheduled_empty_task): + assert scheduled_empty_task.state == MaintenanceTask.STATE_SCHEDULED + maintengine.cancel_tasks_without_components() + scheduled_empty_task.refresh_from_db() + assert scheduled_empty_task.state == MaintenanceTask.STATE_SCHEDULED + + def test_it_should_not_cancel_nonempty_tasks(self, half_empty_task): + assert half_empty_task.state == MaintenanceTask.STATE_ACTIVE + maintengine.cancel_tasks_without_components() + half_empty_task.refresh_from_db() + assert half_empty_task.state == MaintenanceTask.STATE_ACTIVE + + +@pytest.fixture +def empty_task(db): + task = MaintenanceTask( + start_time=datetime.now() - timedelta(minutes=30), + end_time=datetime.now() + timedelta(minutes=30), + description="Test task", + state=MaintenanceTask.STATE_ACTIVE, + ) + task.save() + component = MaintenanceComponent( + maintenance_task=task, + key="netbox", + value=99999, + ) + component.save() + + yield task + + +@pytest.fixture +def scheduled_empty_task(empty_task): + empty_task.state = MaintenanceTask.STATE_SCHEDULED + empty_task.start_time = datetime.now() + timedelta(minutes=30) + empty_task.end_time = datetime.now() + timedelta(minutes=60) + empty_task.save() + yield empty_task + + +@pytest.fixture +def half_empty_task(empty_task, localhost): + component = MaintenanceComponent(maintenance_task=empty_task, component=localhost) + component.save() + yield empty_task