Skip to content
Snippets Groups Projects
Commit ffd29fda authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 3.4.

parents 93a5d129 b02416b4
Branches
Tags
No related merge requests found
Pipeline #93987 failed
# Changelog # Changelog
# [3.4] - 2025-05-08
- Fix some bugs in the nightly validation schedule.
# [3.3] - 2025-05-07 # [3.3] - 2025-05-07
- Fix import L3 core services bug - Fix CLI bug for importing L3 core services.
# [3.2] - 2025-05-02 # [3.2] - 2025-05-02
- Allow running the Edge Port modification workflow on Juniper routers. - Allow running the Edge Port modification workflow on Juniper routers.
......
...@@ -11,5 +11,5 @@ from gso.services.processes import count_incomplete_validate_products ...@@ -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")) @scheduler(CronScheduleConfig(name="Validate Products and inactive subscriptions", minute="30", hour="2"))
def validate_products() -> None: def validate_products() -> None:
"""Validate all products.""" """Validate all products."""
if count_incomplete_validate_products() > 0: if count_incomplete_validate_products() == 0:
start_process("task_validate_geant_products") start_process("task_validate_geant_products")
...@@ -10,11 +10,11 @@ From this list, each workflow is selected that meets the following: ...@@ -10,11 +10,11 @@ From this list, each workflow is selected that meets the following:
import structlog import structlog
from celery import shared_task from celery import shared_task
from orchestrator.services.processes import get_execution_context 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 orchestrator.targets import Target
from gso.schedules.scheduling import CronScheduleConfig, scheduler 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__) logger = structlog.get_logger(__name__)
...@@ -22,8 +22,16 @@ logger = structlog.get_logger(__name__) ...@@ -22,8 +22,16 @@ logger = structlog.get_logger(__name__)
@shared_task @shared_task
@scheduler(CronScheduleConfig(name="Subscriptions Validator", minute="10", hour="3")) @scheduler(CronScheduleConfig(name="Subscriptions Validator", minute="10", hour="3"))
def validate_subscriptions() -> None: def validate_subscriptions() -> None:
"""Validate all subscriptions using their corresponding validation workflow.""" """Validate all subscriptions using their corresponding validation workflow.
subscriptions = get_active_insync_subscriptions()
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: if not subscriptions:
logger.info("No subscriptions to validate") logger.info("No subscriptions to validate")
return return
...@@ -35,18 +43,18 @@ def validate_subscriptions() -> None: ...@@ -35,18 +43,18 @@ def validate_subscriptions() -> None:
if workflow.target == Target.SYSTEM and workflow.name.startswith("validate_"): if workflow.target == Target.SYSTEM and workflow.name.startswith("validate_"):
validation_workflow = workflow.name validation_workflow = workflow.name
if validation_workflow: if validation_workflow:
# Validation workflows only run on subscriptions that are active, even when they could be run on validation_workflow_usable = (subscription.status in TARGET_DEFAULT_USABLE_MAP[Target.SYSTEM]) and (
# provisioning subscriptions. E.g. for routers, they can manually be validated when provisioning, but are subscription.insync or (workflow in WF_USABLE_WHILE_OUT_OF_SYNC)
# not included in this schedule. )
usable_when = TARGET_DEFAULT_USABLE_MAP[Target.SYSTEM]
if validation_workflow_usable:
json = [{"subscription_id": str(subscription.subscription_id)}]
if subscription.status in usable_when: validate_func = get_execution_context()["validate"]
json = [{"subscription_id": str(subscription.subscription_id)}] validate_func(validation_workflow, json=json)
validate_func = get_execution_context()["validate"] if not validation_workflow:
validate_func(validation_workflow, json=json)
else:
logger.warning( logger.warning(
"SubscriptionTable has no validation workflow", "SubscriptionTable has no validation workflow",
subscription=subscription, subscription=subscription,
......
...@@ -343,11 +343,11 @@ def get_subscription_by_process_id(process_id: str) -> SubscriptionModel | None: ...@@ -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 return SubscriptionModel.from_subscription(subscription_table.subscription_id) if subscription_table else None
def get_active_insync_subscriptions() -> list[SubscriptionTable]: def get_active_subscriptions() -> list[SubscriptionTable]:
"""Retrieve all subscriptions that are currently active and in sync.""" """Retrieve all subscriptions that are currently active."""
return ( return (
SubscriptionTable.query.join(ProductTable) SubscriptionTable.query.join(ProductTable)
.filter(SubscriptionTable.insync.is_(True), SubscriptionTable.status == SubscriptionLifecycle.ACTIVE.value) .filter(SubscriptionTable.status == SubscriptionLifecycle.ACTIVE)
.all() .all()
) )
......
...@@ -7,7 +7,7 @@ from orchestrator.domain import SubscriptionModel ...@@ -7,7 +7,7 @@ from orchestrator.domain import SubscriptionModel
from orchestrator.forms import SubmitFormPage from orchestrator.forms import SubmitFormPage
from orchestrator.targets import Target from orchestrator.targets import Target
from orchestrator.workflow import StepList, begin, conditional, done, inputstep, step, workflow 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 orchestrator.workflows.utils import wrap_modify_initial_input_form
from pydantic import Field from pydantic import Field
from pydantic_forms.types import FormGenerator, State, UUIDstr from pydantic_forms.types import FormGenerator, State, UUIDstr
...@@ -26,7 +26,7 @@ def build_fqdn_list(subscription_id: UUIDstr) -> State: ...@@ -26,7 +26,7 @@ def build_fqdn_list(subscription_id: UUIDstr) -> State:
ap_fqdn_list = [ ap_fqdn_list = [
ap.sbp.edge_port.node.router_fqdn for ap in ap_list if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER 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") @step("[DRY RUN] Validate Prefix-Lists")
...@@ -115,14 +115,15 @@ def validate_geant_ip_prefix_list() -> StepList: ...@@ -115,14 +115,15 @@ def validate_geant_ip_prefix_list() -> StepList:
"""Validate prefix-lists for an existing GÉANT IP subscription.""" """Validate prefix-lists for an existing GÉANT IP subscription."""
fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == []) fqdn_list_is_empty = conditional(lambda state: state["ap_fqdn_list"] == [])
prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"])) 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 = ( redeploy_prefix_list_steps = (
begin begin
>> unsync >> unsync_unchecked
>> await_operator >> await_operator
>> lso_interaction(deploy_prefix_lists_dry) >> lso_interaction(deploy_prefix_lists_dry)
>> lso_interaction(deploy_prefix_lists_real) >> lso_interaction(deploy_prefix_lists_real)
>> resync >> subscription_was_in_sync(resync)
) )
prefix_list_validation_steps = ( prefix_list_validation_steps = (
begin begin
......
...@@ -18,7 +18,7 @@ from gso.settings import load_oss_params ...@@ -18,7 +18,7 @@ from gso.settings import load_oss_params
@step("Gather all tasks that have failed") @step("Gather all tasks that have failed")
def gather_failed_tasks() -> State: def gather_failed_tasks() -> State:
"""Gather all tasks that have failed.""" """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)) all_other_tasks = list(set(get_failed_tasks()) - set(failed_prefix_list_tasks))
return { return {
......
...@@ -4,7 +4,7 @@ from setuptools import find_packages, setup ...@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup( setup(
name="geant-service-orchestrator", name="geant-service-orchestrator",
version="3.3", version="3.4",
author="GÉANT Orchestration and Automation Team", author="GÉANT Orchestration and Automation Team",
author_email="goat@geant.org", author_email="goat@geant.org",
description="GÉANT Service Orchestrator", description="GÉANT Service Orchestrator",
......
...@@ -14,8 +14,8 @@ def validate_subscriptions(): ...@@ -14,8 +14,8 @@ def validate_subscriptions():
@pytest.fixture() @pytest.fixture()
def mock_get_active_insync_subscriptions(): def mock_get_active_subscriptions():
with patch("gso.schedules.validate_subscriptions.get_active_insync_subscriptions") as mock: with patch("gso.schedules.validate_subscriptions.get_active_subscriptions") as mock:
yield mock yield mock
...@@ -82,24 +82,24 @@ def test_scheduled_task_still_works(): ...@@ -82,24 +82,24 @@ def test_scheduled_task_still_works():
assert result == "task result" assert result == "task result"
def test_no_subscriptions(mock_get_active_insync_subscriptions, mock_logger, validate_subscriptions): def test_no_subscriptions(mock_get_active_subscriptions, mock_logger, validate_subscriptions):
mock_get_active_insync_subscriptions.return_value = [] mock_get_active_subscriptions.return_value = []
validate_subscriptions() validate_subscriptions()
mock_logger.info.assert_called_once_with("No subscriptions to validate") mock_logger.info.assert_called_once_with("No subscriptions to validate")
def test_subscriptions_without_system_target_workflow( def test_subscriptions_without_system_target_workflow(
mock_get_active_insync_subscriptions, mock_get_active_subscriptions,
mock_logger, mock_logger,
validate_subscriptions, 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() validate_subscriptions()
mock_logger.warning.assert_called_once() mock_logger.warning.assert_called_once()
def test_subscription_status_not_usable( def test_subscription_status_not_usable(
mock_get_active_insync_subscriptions, mock_get_active_subscriptions,
mock_get_execution_context, mock_get_execution_context,
validate_subscriptions, validate_subscriptions,
): ):
...@@ -107,7 +107,7 @@ def test_subscription_status_not_usable( ...@@ -107,7 +107,7 @@ def test_subscription_status_not_usable(
subscription_mock.product.workflows = [MagicMock(target=Target.SYSTEM, name="workflow_name")] subscription_mock.product.workflows = [MagicMock(target=Target.SYSTEM, name="workflow_name")]
subscription_mock.status = "Not Usable Status" 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_subscriptions()
validate_func = mock_get_execution_context()["validate"] validate_func = mock_get_execution_context()["validate"]
...@@ -115,7 +115,7 @@ def test_subscription_status_not_usable( ...@@ -115,7 +115,7 @@ def test_subscription_status_not_usable(
def test_valid_subscriptions_for_validation( def test_valid_subscriptions_for_validation(
mock_get_active_insync_subscriptions, mock_get_active_subscriptions,
mock_get_execution_context, mock_get_execution_context,
validate_subscriptions, validate_subscriptions,
): ):
...@@ -123,7 +123,7 @@ def test_valid_subscriptions_for_validation( ...@@ -123,7 +123,7 @@ def test_valid_subscriptions_for_validation(
mocked_workflow = MagicMock(target=Target.SYSTEM, name="workflow_name") mocked_workflow = MagicMock(target=Target.SYSTEM, name="workflow_name")
subscription_mock.product.workflows = [mocked_workflow] subscription_mock.product.workflows = [mocked_workflow]
subscription_mock.status = "active" subscription_mock.status = "active"
mock_get_active_insync_subscriptions.return_value = [subscription_mock] mock_get_active_subscriptions.return_value = [subscription_mock]
validate_subscriptions() validate_subscriptions()
validate_func = mock_get_execution_context()["validate"] validate_func = mock_get_execution_context()["validate"]
validate_func.assert_called_once_with( validate_func.assert_called_once_with(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment