Skip to content

Update ibgp mesh

Update iBGP mesh workflow. Adds a new P router to the mesh of PE routers in the network.

Once a new router is added to the network, it must become reachable by all other devices. To achieve this, the update_ibgp_mesh workflow must be executed. This workflow will add the new P router to all PE routers in the network, and add all existing PE routers to the new P router. The only input this workflow takes, is a trouble ticket number. All other required information is already in the service database.

The workflow will run 5 Ansible playbooks:

  1. Check: add P router to all PE routers
  2. Deploy: add P router to all PE routers
  3. Check: add all PE routers to P router
  4. Deploy: add all PE routers to P router
  5. Verify: check that the iBGP has come up

Once these playbooks have been run successfully, the new P router is added to LibreNMS. Finally, the subscription model of the router is updated such that router_access_via_ts is set to False. This is because the router is now reachable by other machines by its loopback address. Using out of band access is therefore not needed anymore.

initial_input_form_generator(subscription_id)

Show a confirmation window before running the workflow.

Does not allow for user input, but does run a validation check. The workflow is only allowed to run, if the router already is connected by at least one IP trunk.

Source code in gso/workflows/router/update_ibgp_mesh.py
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
    """Show a confirmation window before running the workflow.

    Does not allow for user input, but does run a validation check. The workflow is only allowed to run, if the router
    already is connected by at least one IP trunk.
    """
    subscription = Router.from_subscription(subscription_id)

    class AddBGPSessionForm(SubmitFormPage):
        model_config = ConfigDict(title=f"Add {subscription.router.router_fqdn} to the iBGP mesh?")

        tt_number: TTNumber

        @model_validator(mode="before")
        def router_has_a_trunk(cls, data: Any) -> Any:
            terminating_trunks = get_trunks_that_terminate_on_router(
                subscription_id, SubscriptionLifecycle.PROVISIONING
            ) + get_trunks_that_terminate_on_router(subscription_id, SubscriptionLifecycle.ACTIVE)
            if len(terminating_trunks) == 0:
                msg = "Selected router does not terminate any available IP trunks."
                raise ValueError(msg)

            return data

    user_input = yield AddBGPSessionForm

    return user_input.model_dump()

add_p_to_mesh_dry(subscription, tt_number, process_id)

Perform a dry run of adding the new P router to the PE router mesh.

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("[DRY RUN] Add P router to iBGP mesh")
def add_p_to_mesh_dry(subscription: dict[str, Any], tt_number: str, process_id: UUIDstr) -> LSOState:
    """Perform a dry run of adding the new P router to the PE router mesh."""
    extra_vars = {
        "dry_run": True,
        "subscription": subscription,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update iBGP mesh",
        "verb": "add_p_to_pe",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml",
        "inventory": generate_inventory_for_routers(RouterRole.PE),
        "extra_vars": extra_vars,
    }

add_p_to_mesh_real(subscription, tt_number, process_id)

Add the P router to the mesh of PE routers.

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("[FOR REAL] Add P router to iBGP mesh")
def add_p_to_mesh_real(subscription: dict[str, Any], tt_number: str, process_id: UUIDstr) -> LSOState:
    """Add the P router to the mesh of PE routers."""
    extra_vars = {
        "dry_run": False,
        "subscription": subscription,
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update iBGP mesh",
        "verb": "add_p_to_pe",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml",
        "inventory": generate_inventory_for_routers(RouterRole.PE),
        "extra_vars": extra_vars,
    }

add_all_pe_to_p_dry(subscription)

Perform a dry run of adding the list of all PE routers to the new P router.

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("[DRY RUN] Add all PE routers to P router iBGP group")
def add_all_pe_to_p_dry(subscription: dict[str, Any]) -> LSOState:
    """Perform a dry run of adding the list of all PE routers to the new P router."""
    extra_vars = {
        "dry_run": True,
        "subscription": subscription,
        "pe_router_list": generate_inventory_for_routers(RouterRole.PE)["all"]["hosts"],
        "verb": "add_pe_to_p",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml",
        "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}},
        "extra_vars": extra_vars,
    }

add_all_pe_to_p_real(subscription, tt_number, process_id)

Add the list of all PE routers to the new P router.

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("[FOR REAL] Add all PE routers to P router iBGP group")
def add_all_pe_to_p_real(subscription: dict[str, Any], tt_number: str, process_id: UUIDstr) -> LSOState:
    """Add the list of all PE routers to the new P router."""
    extra_vars = {
        "dry_run": False,
        "subscription": subscription,
        "pe_router_list": generate_inventory_for_routers(RouterRole.PE)["all"]["hosts"],
        "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Update iBGP mesh",
        "verb": "add_pe_to_p",
    }

    return {
        "playbook_name": "gap_ansible/playbooks/update_ibgp_mesh.yaml",
        "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}},
        "extra_vars": extra_vars,
    }

check_ibgp_session(subscription)

Run a playbook using the provisioning proxy, to check the health of the new iBGP session.

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("Verify iBGP session health")
def check_ibgp_session(subscription: dict[str, Any]) -> LSOState:
    """Run a playbook using the provisioning proxy, to check the health of the new iBGP session."""
    return {
        "playbook_name": "gap_ansible/playbooks/check_ibgp.yaml",
        "inventory": {"all": {"hosts": {subscription["router"]["router_fqdn"]: None}}},
        "extra_vars": {"subscription": subscription},
    }

add_device_to_librenms(subscription)

Add the router as a device to LibreNMS.

If the device already exists, retrieve the device information instead of adding it again

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("Add the router to LibreNMS")
def add_device_to_librenms(subscription: Router) -> State:
    """Add the router as a device to LibreNMS.

    If the device already exists, retrieve the device information instead of adding it again
    """
    client = librenms_client.LibreNMSClient()
    if not client.device_exists(subscription.router.router_fqdn):
        librenms_result = client.add_device(subscription.router.router_fqdn, SNMPVersion.V2C)
    else:
        librenms_result = client.get_device(subscription.router.router_fqdn)
    return {"librenms_device": librenms_result}

prompt_insert_in_radius()

Wait for confirmation from an operator that the router has been inserted in RADIUS.

Source code in gso/workflows/router/update_ibgp_mesh.py
@inputstep("Prompt RADIUS insertion", assignee=Assignee.SYSTEM)
def prompt_insert_in_radius() -> FormGenerator:
    """Wait for confirmation from an operator that the router has been inserted in RADIUS."""

    class RADIUSPrompt(SubmitFormPage):
        model_config = ConfigDict(title="Please update RADIUS before continuing")

        info_label: Label = "Insert the router into RADIUS, and continue the workflow once this has been completed."

    yield RADIUSPrompt

    return {}

prompt_radius_login()

Wait for confirmation from an operator that the router can be logged into using RADIUS.

Source code in gso/workflows/router/update_ibgp_mesh.py
@inputstep("Prompt RADIUS login", assignee=Assignee.SYSTEM)
def prompt_radius_login() -> FormGenerator:
    """Wait for confirmation from an operator that the router can be logged into using RADIUS."""

    class RADIUSPrompt(SubmitFormPage):
        model_config = ConfigDict(title="Please check RADIUS before continuing")

        info_label: Label = "Log in to the router using RADIUS, and continue the workflow when this was successful."

    yield RADIUSPrompt

    return {}

update_subscription_model(subscription)

Update the database model, such that it should not be reached via OOB access anymore.

Source code in gso/workflows/router/update_ibgp_mesh.py
@step("Update subscription model")
def update_subscription_model(subscription: Router) -> State:
    """Update the database model, such that it should not be reached via OOB access anymore."""
    subscription.router.router_access_via_ts = False

    return {"subscription": subscription}

update_ibgp_mesh()

Update the iBGP mesh with a new P router.

  • Add the new P-router to all other PE-routers in the network, including a dry run.
  • Add all PE-routers to the P-router, including a dry run.
  • Verify that the iBGP session is up.
  • Add the new P-router to LibreNMS.
  • Update the subscription model.
Source code in gso/workflows/router/update_ibgp_mesh.py
@workflow(
    "Update iBGP mesh",
    initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator),
    target=Target.MODIFY,
)
def update_ibgp_mesh() -> StepList:
    """Update the iBGP mesh with a new P router.

    * Add the new P-router to all other PE-routers in the network, including a dry run.
    * Add all PE-routers to the P-router, including a dry run.
    * Verify that the iBGP session is up.
    * Add the new P-router to LibreNMS.
    * Update the subscription model.
    """
    router_is_pe = conditional(lambda state: state["subscription"]["router"]["router_role"] == RouterRole.PE)
    router_is_p = conditional(lambda state: state["subscription"]["router"]["router_role"] == RouterRole.P)

    return (
        begin
        >> store_process_subscription(Target.MODIFY)
        >> unsync
        >> router_is_p(lso_interaction(add_p_to_mesh_dry))
        >> router_is_p(lso_interaction(add_p_to_mesh_real))
        >> router_is_p(lso_interaction(add_all_pe_to_p_dry))
        >> router_is_p(lso_interaction(add_all_pe_to_p_real))
        >> router_is_p(lso_interaction(check_ibgp_session))
        >> router_is_pe(lso_interaction(add_pe_mesh_to_pe_dry))
        >> router_is_pe(lso_interaction(add_pe_mesh_to_pe_real))
        >> router_is_pe(lso_interaction(add_pe_to_pe_mesh_dry))
        >> router_is_pe(lso_interaction(add_pe_to_pe_mesh_real))
        >> router_is_pe(lso_interaction(add_all_p_to_pe_dry))
        >> router_is_pe(lso_interaction(add_all_p_to_pe_real))
        >> router_is_pe(lso_interaction(add_pe_to_all_p_dry))
        >> router_is_pe(lso_interaction(add_pe_to_all_p_real))
        >> router_is_pe(lso_interaction(update_sdp_single_pe_dry))
        >> router_is_pe(lso_interaction(update_sdp_single_pe_real))
        >> router_is_pe(lso_interaction(update_sdp_mesh_dry))
        >> router_is_pe(lso_interaction(update_sdp_mesh_real))
        >> router_is_pe(lso_interaction(check_pe_ibgp))
        >> router_is_pe(lso_interaction(check_l3_services))
        >> add_device_to_librenms
        >> prompt_insert_in_radius
        >> prompt_radius_login
        >> update_subscription_model
        >> resync
        >> done
    )