diff --git a/Changelog.md b/Changelog.md index 9d26fb9a9718203cfaf76933c772e035d04b8506..7300e570c30194f361316b08700133eefa8354fd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,7 +1,10 @@ # Changelog +# [3.4] - 2025-05-08 +- Fix some bugs in the nightly validation schedule. + # [3.3] - 2025-05-07 -- Fix import L3 core services bug +- Fix CLI bug for importing L3 core services. # [3.2] - 2025-05-02 - Allow running the Edge Port modification workflow on Juniper routers. diff --git a/gso/schedules/validate_products.py b/gso/schedules/validate_products.py index f51f5c22b6c3fb33bafb9065e34d494215834276..efc441ad21f3a4894b82a030e99fd7f0653e0a99 100644 --- a/gso/schedules/validate_products.py +++ b/gso/schedules/validate_products.py @@ -11,5 +11,5 @@ from gso.services.processes import count_incomplete_validate_products @scheduler(CronScheduleConfig(name="Validate Products and inactive subscriptions", minute="30", hour="2")) def validate_products() -> None: """Validate all products.""" - if count_incomplete_validate_products() > 0: + if count_incomplete_validate_products() == 0: start_process("task_validate_geant_products") diff --git a/gso/schedules/validate_subscriptions.py b/gso/schedules/validate_subscriptions.py index 0ac4e36f655fda58f108967095123ef17f4f90dc..7323b6086b948201da4a0f5b0ee1574030646076 100644 --- a/gso/schedules/validate_subscriptions.py +++ b/gso/schedules/validate_subscriptions.py @@ -10,11 +10,11 @@ From this list, each workflow is selected that meets the following: import structlog from celery import shared_task from orchestrator.services.processes import get_execution_context -from orchestrator.services.subscriptions import TARGET_DEFAULT_USABLE_MAP +from orchestrator.services.subscriptions import TARGET_DEFAULT_USABLE_MAP, WF_USABLE_WHILE_OUT_OF_SYNC from orchestrator.targets import Target from gso.schedules.scheduling import CronScheduleConfig, scheduler -from gso.services.subscriptions import get_active_insync_subscriptions +from gso.services.subscriptions import get_active_subscriptions logger = structlog.get_logger(__name__) @@ -22,8 +22,16 @@ logger = structlog.get_logger(__name__) @shared_task @scheduler(CronScheduleConfig(name="Subscriptions Validator", minute="10", hour="3")) def validate_subscriptions() -> None: - """Validate all subscriptions using their corresponding validation workflow.""" - subscriptions = get_active_insync_subscriptions() + """Validate all subscriptions using their corresponding validation workflow. + + Validation workflows only run on subscriptions that are active, even when they could be run on provisioning + subscriptions. E.g. for routers, they can manually be validated when provisioning, but are not included in this + schedule. + + Validation workflows will, in principle, only run on subscriptions that are in sync. Except when a specific + validation workflow is marked as usable while out of sync in the list `WF_USABLE_WHILE_OUT_OF_SYNC`. + """ + subscriptions = get_active_subscriptions() if not subscriptions: logger.info("No subscriptions to validate") return @@ -35,18 +43,18 @@ def validate_subscriptions() -> None: if workflow.target == Target.SYSTEM and workflow.name.startswith("validate_"): validation_workflow = workflow.name - if validation_workflow: - # Validation workflows only run on subscriptions that are active, even when they could be run on - # provisioning subscriptions. E.g. for routers, they can manually be validated when provisioning, but are - # not included in this schedule. - usable_when = TARGET_DEFAULT_USABLE_MAP[Target.SYSTEM] + if validation_workflow: + validation_workflow_usable = (subscription.status in TARGET_DEFAULT_USABLE_MAP[Target.SYSTEM]) and ( + subscription.insync or (workflow in WF_USABLE_WHILE_OUT_OF_SYNC) + ) + + if validation_workflow_usable: + json = [{"subscription_id": str(subscription.subscription_id)}] - if subscription.status in usable_when: - json = [{"subscription_id": str(subscription.subscription_id)}] + validate_func = get_execution_context()["validate"] + validate_func(validation_workflow, json=json) - validate_func = get_execution_context()["validate"] - validate_func(validation_workflow, json=json) - else: + if not validation_workflow: logger.warning( "SubscriptionTable has no validation workflow", subscription=subscription, diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index 6ae731410e60b1a60e0a80b3b5cd11508e101aab..93e849d892144299f932467501e8d82b1805ef89 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -343,11 +343,11 @@ def get_subscription_by_process_id(process_id: str) -> SubscriptionModel | None: return SubscriptionModel.from_subscription(subscription_table.subscription_id) if subscription_table else None -def get_active_insync_subscriptions() -> list[SubscriptionTable]: - """Retrieve all subscriptions that are currently active and in sync.""" +def get_active_subscriptions() -> list[SubscriptionTable]: + """Retrieve all subscriptions that are currently active.""" return ( SubscriptionTable.query.join(ProductTable) - .filter(SubscriptionTable.insync.is_(True), SubscriptionTable.status == SubscriptionLifecycle.ACTIVE.value) + .filter(SubscriptionTable.status == SubscriptionLifecycle.ACTIVE) .all() ) diff --git a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py index ec335abd7aa3f70ac054a5d6d518893c97a4a548..8dc838d20f7c29db62359d8cda6bb5a2538ef320 100644 --- a/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py +++ b/gso/workflows/l3_core_service/geant_ip/validate_prefix_list.py @@ -7,7 +7,7 @@ from orchestrator.domain import SubscriptionModel from orchestrator.forms import SubmitFormPage from orchestrator.targets import Target from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step, workflow -from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.steps import resync, store_process_subscription, unsync_unchecked from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic import Field from pydantic_forms.types import FormGenerator, State, UUIDstr @@ -26,7 +26,7 @@ def build_fqdn_list(subscription_id: UUIDstr) -> State: ap_fqdn_list = [ ap.sbp.edge_port.node.router_fqdn for ap in ap_list if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER ] - return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription} + return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription, "subscription_was_in_sync": subscription.insync} @step("[DRY RUN] Validate Prefix-Lists") @@ -115,14 +115,15 @@ def validate_geant_ip_prefix_list() -> StepList: """Validate prefix-lists for an existing GÉANT IP subscription.""" fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == []) prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"])) + subscription_was_in_sync = conditional(lambda state: bool(state["subscription_was_in_sync"])) redeploy_prefix_list_steps = ( begin - >> unsync + >> unsync_unchecked >> await_operator >> lso_interaction(deploy_prefix_lists_dry) >> lso_interaction(deploy_prefix_lists_real) - >> resync + >> subscription_was_in_sync(resync) ) prefix_list_validation_steps = ( begin diff --git a/gso/workflows/tasks/send_email_notifications.py b/gso/workflows/tasks/send_email_notifications.py index cf87864bb51d724ead84e31663fb754c1a3946b6..6db502a7932c381d273ef8c863a22e71165edd4f 100644 --- a/gso/workflows/tasks/send_email_notifications.py +++ b/gso/workflows/tasks/send_email_notifications.py @@ -18,7 +18,7 @@ from gso.settings import load_oss_params @step("Gather all tasks that have failed") def gather_failed_tasks() -> State: """Gather all tasks that have failed.""" - failed_prefix_list_tasks = get_suspended_tasks_by_workflow_name("validate_prefix_list") + failed_prefix_list_tasks = get_suspended_tasks_by_workflow_name("validate_geant_ip_prefix_list") all_other_tasks = list(set(get_failed_tasks()) - set(failed_prefix_list_tasks)) return { diff --git a/setup.py b/setup.py index 8e83d4c1e7cbab699f2fc7faf9cc0223a93d8b4c..58344ee060832df1d14ea56f7d86e56d1ab824a0 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="3.3", + version="3.4", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator", diff --git a/test/schedules/test_scheduling.py b/test/schedules/test_scheduling.py index 93f1b5d3326719c4faecc3485483402a848a0875..a1eb56b48a4d0023c527635701ba85dbfa5a4bab 100644 --- a/test/schedules/test_scheduling.py +++ b/test/schedules/test_scheduling.py @@ -14,8 +14,8 @@ def validate_subscriptions(): @pytest.fixture() -def mock_get_active_insync_subscriptions(): - with patch("gso.schedules.validate_subscriptions.get_active_insync_subscriptions") as mock: +def mock_get_active_subscriptions(): + with patch("gso.schedules.validate_subscriptions.get_active_subscriptions") as mock: yield mock @@ -82,24 +82,24 @@ def test_scheduled_task_still_works(): assert result == "task result" -def test_no_subscriptions(mock_get_active_insync_subscriptions, mock_logger, validate_subscriptions): - mock_get_active_insync_subscriptions.return_value = [] +def test_no_subscriptions(mock_get_active_subscriptions, mock_logger, validate_subscriptions): + mock_get_active_subscriptions.return_value = [] validate_subscriptions() mock_logger.info.assert_called_once_with("No subscriptions to validate") def test_subscriptions_without_system_target_workflow( - mock_get_active_insync_subscriptions, + mock_get_active_subscriptions, mock_logger, validate_subscriptions, ): - mock_get_active_insync_subscriptions.return_value = [MagicMock(product=MagicMock(workflows=[]))] + mock_get_active_subscriptions.return_value = [MagicMock(product=MagicMock(workflows=[]))] validate_subscriptions() mock_logger.warning.assert_called_once() def test_subscription_status_not_usable( - mock_get_active_insync_subscriptions, + mock_get_active_subscriptions, mock_get_execution_context, validate_subscriptions, ): @@ -107,7 +107,7 @@ def test_subscription_status_not_usable( subscription_mock.product.workflows = [MagicMock(target=Target.SYSTEM, name="workflow_name")] subscription_mock.status = "Not Usable Status" - mock_get_active_insync_subscriptions.return_value = [subscription_mock] + mock_get_active_subscriptions.return_value = [subscription_mock] validate_subscriptions() validate_func = mock_get_execution_context()["validate"] @@ -115,7 +115,7 @@ def test_subscription_status_not_usable( def test_valid_subscriptions_for_validation( - mock_get_active_insync_subscriptions, + mock_get_active_subscriptions, mock_get_execution_context, validate_subscriptions, ): @@ -123,7 +123,7 @@ def test_valid_subscriptions_for_validation( mocked_workflow = MagicMock(target=Target.SYSTEM, name="workflow_name") subscription_mock.product.workflows = [mocked_workflow] subscription_mock.status = "active" - mock_get_active_insync_subscriptions.return_value = [subscription_mock] + mock_get_active_subscriptions.return_value = [subscription_mock] validate_subscriptions() validate_func = mock_get_execution_context()["validate"] validate_func.assert_called_once_with(