Skip to content

Base modify l3 core service

Base functionality for modifying an L3 Core Service subscription.

Operation

Bases: strEnum

The three operations that can be performed to modify an L3 Core Service.

Source code in gso/workflows/l3_core_service/base_modify_l3_core_service.py
class Operation(strEnum):
    """The three operations that can be performed to modify an L3 Core Service."""

    ADD = "Add an Access Port"
    REMOVE = "Remove an existing Access Port"
    EDIT = "Edit an existing Access Port"

initial_input_form_generator(subscription_id)

Get input about added, removed, and modified Access Ports.

Source code in gso/workflows/l3_core_service/base_modify_l3_core_service.py
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
    """Get input about added, removed, and modified Access Ports."""
    subscription = SubscriptionModel.from_subscription(subscription_id)
    product_name = subscription.product.name

    class OperationSelectionForm(FormPage):
        model_config = ConfigDict(title="Modify Edge Port")

        tt_number: TTNumber
        operation: Operation
        include_ansible_playbook_sbp_config: bool = True
        include_ansible_playbook_bgp_config: bool = True

        @model_validator(mode="after")
        def validate_no_duplicate_subscriptions(self) -> "OperationSelectionForm":
            """Warn if duplicate subscriptions exist for this partner + product.

            For modify workflows, we allow the operation to proceed but warn the operator
            that duplicate subscriptions exist and they should use the merge workflow.
            """
            existing_subscriptions = get_active_l3_subscriptions_by_partner_and_product(
                subscription.customer_id,
                ProductName(product_name),
            )

            # Exclude current subscription from count
            other_subscriptions = [
                s for s in existing_subscriptions if s.subscription_id != subscription.subscription_id
            ]

            if len(other_subscriptions) >= 1:
                partner_name = get_partner_by_id(subscription.customer_id).name
                subscription_ids = [str(s.subscription_id) for s in other_subscriptions]
                msg = (
                    f"Warning: {len(other_subscriptions)} additional active {product_name} subscription(s) found "
                    f"for partner {partner_name} ({subscription.customer_id}): {', '.join(subscription_ids)}. "
                    "\n\nThe target model requires ONE subscription per partner with multiple Access Ports. "
                    "Consider using the 'Merge L3 Core Subscriptions' workflow to consolidate these subscriptions."
                )
                raise ValueError(msg)

            return self

    def access_port_selector() -> TypeAlias:
        """Generate a dropdown selector for choosing an Access Port in an input form."""
        access_ports = subscription.l3_core.ap_list  # type: ignore[attr-defined]
        options = {
            str(access_port.subscription_instance_id): (
                f"{access_port.sbp.gs_id} on "
                f"{EdgePort.from_subscription(access_port.sbp.edge_port.owner_subscription_id).description} "
                f"({access_port.ap_type})"
            )
            for access_port in access_ports
        }

        return cast(
            type[Choice],
            Choice.__call__(
                "Select an Access Port",
                zip(options.keys(), options.items(), strict=True),
            ),
        )

    class BFDInputModel(BaseModel):
        bfd_enabled: bool = False
        bfd_interval_rx: int | None = None
        bfd_interval_tx: int | None = None
        bfd_multiplier: int | None = None

    initial_input = yield OperationSelectionForm
    match initial_input.operation:
        case Operation.ADD:

            class PartnerSelectionForm(FormPage):
                model_config = ConfigDict(title=f"Add an Edge Port to a {product_name}")
                label: Label = Field(
                    "Please select the partner who owns the Edge Port which is to be added.", exclude=True
                )
                edge_port_partner: partner_choice() = subscription.customer_id  # type: ignore[valid-type]

            partner_input = yield PartnerSelectionForm

            class AccessPortListItem(BaseModel):
                edge_port: str
                ap_type: str
                custom_service_name: str

            def available_new_edge_port_selector() -> TypeAlias:
                """Generate a dropdown selector for choosing an active Edge Port in an input form."""
                edge_ports = get_active_edge_port_subscriptions(partner_id=partner_input.edge_port_partner)

                options = {
                    str(edge_port.subscription_id): edge_port.description
                    for edge_port in edge_ports
                    if edge_port.subscription_id
                    not in [ap.sbp.edge_port.owner_subscription_id for ap in subscription.l3_core.ap_list]  # type: ignore[attr-defined]
                }

                return cast(
                    type[Choice],
                    Choice.__call__(
                        "Select an Edge Port",
                        zip(options.keys(), options.items(), strict=True),
                    ),
                )

            def existing_ap_list() -> type[list]:
                return cast(
                    type[list],
                    read_only_list([
                        AccessPortListItem(
                            edge_port=EdgePort.from_subscription(
                                access_port.sbp.edge_port.owner_subscription_id
                            ).description,
                            ap_type=access_port.ap_type.value,
                            custom_service_name=access_port.custom_service_name or "",
                        )
                        for access_port in subscription.l3_core.ap_list  # type: ignore[attr-defined]
                    ]),
                )

            class AddAccessPortForm(FormPage):
                model_config = ConfigDict(title=f"Add an Edge Port to a {product_name}")
                existing_access_ports: existing_ap_list()  # type: ignore[valid-type]

                divider_a: Divider = Field(exclude=True)
                label_a: Label = Field(
                    "Please use the fields below to configure a new Access Port, in addition to the existing ones "
                    "listed above.",
                    exclude=True,
                )
                edge_port: available_new_edge_port_selector()  # type: ignore[valid-type]
                ap_type: APType
                generate_gs_id: bool = True
                gs_id: IMPORTED_GS_ID | None = None
                custom_service_name: str | None = None
                is_tagged: bool = False
                vlan_id: VLAN_ID
                ipv4_address: IPv4AddressType
                ipv4_mask: IPv4Netmask
                ipv6_address: IPv6AddressType
                ipv6_mask: IPv6Netmask
                custom_firewall_filters: bool = False

                divider_b: Divider = Field(None, exclude=True)
                label_b: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
                v4_bfd_settings: BFDInputModel
                v4_bgp_peer: IPv4BGPPeer

                divider_c: Divider = Field(None, exclude=True)
                label_c: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
                v6_bfd_settings: BFDInputModel
                v6_bgp_peer: IPv6BGPPeer

                @model_validator(mode="before")
                def validate_gs_id(cls, input_data: dict[str, Any]) -> dict[str, Any]:
                    gs_id = input_data.get("gs_id")
                    generate_gs_id = input_data.get("generate_gs_id", True)

                    if generate_gs_id and gs_id:
                        error_message = (
                            "You cannot provide a GS ID manually while the 'Auto-generate GS ID' option is enabled."
                            "Please either uncheck 'Auto-generate GS ID' or remove the manual GS ID."
                        )
                        raise ValueError(error_message)
                    return input_data

                @field_validator("edge_port")
                def selected_edge_port_is_new(cls, value: UUIDstr) -> UUIDstr:
                    if value in [str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.l3_core.ap_list]:  # type: ignore[attr-defined]
                        error_message = (
                            f"This {product_name} service is already deployed on "
                            f"{EdgePort.from_subscription(value).description}."
                        )
                        raise ValueError(error_message)
                    return value

            user_input = yield AddAccessPortForm
            return {
                "operation": initial_input.operation,
                "verb": "deploy",
                "tt_number": initial_input.tt_number,
                "added_access_port": user_input.model_dump(),
                "include_ansible_playbook_sbp_config": initial_input.include_ansible_playbook_sbp_config,
                "include_ansible_playbook_bgp_config": initial_input.include_ansible_playbook_bgp_config,
            }

        case Operation.REMOVE:

            class RemoveAccessPortForm(FormPage):
                model_config = ConfigDict(title=f"Remove an Edge Port from a {product_name}")
                label: Label = Field(
                    f"Please select one of the Access Ports associated with this {product_name} that should get "
                    f"removed.",
                    exclude=True,
                )
                access_port: access_port_selector()  # type: ignore[valid-type]

            user_input = yield RemoveAccessPortForm

            return {
                "operation": initial_input.operation,
                "verb": "delete",
                "tt_number": initial_input.tt_number,
                "active_ap": user_input.access_port,
                "edge_port_fqdn_list": [AccessPort.from_db(user_input.access_port).sbp.edge_port.node.router_fqdn],
                "include_ansible_playbook_sbp_config": initial_input.include_ansible_playbook_sbp_config,
                "include_ansible_playbook_bgp_config": initial_input.include_ansible_playbook_bgp_config,
            }

        case Operation.EDIT:

            class SelectModifyAccessPortForm(FormPage):
                model_config = ConfigDict(title=f"Modify {product_name}")
                label: Label = Field(
                    f"Please select one of the Access Ports associated with this {product_name} to be modified.",
                    exclude=True,
                )
                access_port: access_port_selector()  # type: ignore[valid-type]

            user_input = yield SelectModifyAccessPortForm
            current_ap = AccessPort.from_db(user_input.access_port)
            v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
            v6_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families)

            class BindingPortModificationForm(FormPage):
                model_config = ConfigDict(title=f"{product_name} - Modify Edge Port configuration")
                current_ep_label: Label = Field(
                    f'Currently configuring on Edge Port "{current_ap.sbp.edge_port.edge_port_description}"',
                    exclude=True,
                )

                gs_id: str = current_ap.sbp.gs_id
                custom_service_name: str | None = current_ap.custom_service_name
                is_tagged: bool = current_ap.sbp.is_tagged
                ap_type: APType = current_ap.ap_type
                # The SBP model does not require these five fields, but in the case of L3 Core Services this will never
                # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease.
                vlan_id: VLAN_ID = current_ap.sbp.vlan_id  # type: ignore[assignment]
                ipv4_address: IPv4AddressType = current_ap.sbp.ipv4_address  # type: ignore[assignment]
                ipv4_mask: IPv4Netmask = current_ap.sbp.ipv4_mask  # type: ignore[assignment]
                ipv6_address: IPv6AddressType = current_ap.sbp.ipv6_address  # type: ignore[assignment]
                ipv6_mask: IPv6Netmask = current_ap.sbp.ipv6_mask  # type: ignore[assignment]
                custom_firewall_filters: bool = current_ap.sbp.custom_firewall_filters

                divider_a: Divider = Field(None, exclude=True)
                label_a: Label = Field("IPv4 settings for BFD and BGP", exclude=True)
                v4_bfd_enabled: bool = Field(current_ap.sbp.v4_bfd_settings.bfd_enabled, exclude=True)
                v4_bfd_multiplier: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_multiplier, exclude=True)
                v4_bfd_interval_rx: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_interval_rx, exclude=True)
                v4_bfd_interval_tx: int | None = Field(current_ap.sbp.v4_bfd_settings.bfd_interval_tx, exclude=True)

                v4_bgp_peer_address: IPv4AddressType = Field(IPv4AddressType(v4_peer.peer_address), exclude=True)
                v4_bgp_authentication_key: str | None = Field(v4_peer.authentication_key, exclude=True)
                v4_bgp_has_custom_policies: bool = Field(v4_peer.has_custom_policies, exclude=True)
                v4_bgp_bfd_enabled: bool = Field(v4_peer.bfd_enabled, exclude=True)
                v4_bgp_multipath_enabled: bool = Field(v4_peer.multipath_enabled, exclude=True)
                v4_bgp_prefix_limit: NonNegativeInt | None = Field(v4_peer.prefix_limit, exclude=True)
                v4_bgp_ttl_security: TTL | None = Field(v4_peer.ttl_security, exclude=True)
                v4_bgp_is_passive: bool = Field(v4_peer.is_passive, exclude=True)
                v4_bgp_send_default_route: bool = Field(v4_peer.send_default_route, exclude=True)
                v4_bgp_add_v4_multicast: bool = Field(bool(IPFamily.V4MULTICAST in v4_peer.families), exclude=True)

                divider_b: Divider = Field(None, exclude=True)
                label_b: Label = Field("IPv6 settings for BFD and BGP", exclude=True)
                v6_bfd_enabled: bool = Field(current_ap.sbp.v6_bfd_settings.bfd_enabled, exclude=True)
                v6_bfd_multiplier: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_multiplier, exclude=True)
                v6_bfd_interval_rx: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_interval_rx, exclude=True)
                v6_bfd_interval_tx: int | None = Field(current_ap.sbp.v6_bfd_settings.bfd_interval_tx, exclude=True)

                v6_bgp_peer_address: IPv6AddressType = Field(IPv6AddressType(v6_peer.peer_address), exclude=True)
                v6_bgp_authentication_key: str | None = Field(v6_peer.authentication_key, exclude=True)
                v6_bgp_has_custom_policies: bool = Field(v6_peer.has_custom_policies, exclude=True)
                v6_bgp_bfd_enabled: bool = Field(v6_peer.bfd_enabled, exclude=True)
                v6_bgp_multipath_enabled: bool = Field(v6_peer.multipath_enabled, exclude=True)
                v6_bgp_prefix_limit: NonNegativeInt | None = Field(v6_peer.prefix_limit, exclude=True)
                v6_bgp_ttl_security: TTL | None = Field(v6_peer.ttl_security, exclude=True)
                v6_bgp_is_passive: bool = Field(v6_peer.is_passive, exclude=True)
                v6_bgp_send_default_route: bool = Field(v6_peer.send_default_route, exclude=True)
                v6_bgp_add_v6_multicast: bool = Field(bool(IPFamily.V6MULTICAST in v6_peer.families), exclude=True)

                @computed_field  # type: ignore[prop-decorator]
                @property
                def v4_bfd_settings(self) -> BFDInputModel:
                    return BFDInputModel(
                        bfd_enabled=self.v4_bfd_enabled,
                        bfd_multiplier=self.v4_bfd_multiplier,
                        bfd_interval_rx=self.v4_bfd_interval_rx,
                        bfd_interval_tx=self.v4_bfd_interval_tx,
                    )

                @computed_field  # type: ignore[prop-decorator]
                @property
                def v4_bgp_peer(self) -> IPv4BGPPeer:
                    return IPv4BGPPeer(
                        peer_address=self.v4_bgp_peer_address,
                        authentication_key=self.v4_bgp_authentication_key,
                        has_custom_policies=self.v4_bgp_has_custom_policies,
                        bfd_enabled=self.v4_bgp_bfd_enabled,
                        multipath_enabled=self.v4_bgp_multipath_enabled,
                        prefix_limit=self.v4_bgp_prefix_limit,
                        ttl_security=self.v4_bgp_ttl_security,
                        is_passive=self.v4_bgp_is_passive,
                        send_default_route=self.v4_bgp_send_default_route,
                        add_v4_multicast=self.v4_bgp_add_v4_multicast,
                    )

                @computed_field  # type: ignore[prop-decorator]
                @property
                def v6_bfd_settings(self) -> BFDInputModel:
                    return BFDInputModel(
                        bfd_enabled=self.v6_bfd_enabled,
                        bfd_multiplier=self.v6_bfd_multiplier,
                        bfd_interval_rx=self.v6_bfd_interval_rx,
                        bfd_interval_tx=self.v6_bfd_interval_tx,
                    )

                @computed_field  # type: ignore[prop-decorator]
                @property
                def v6_bgp_peer(self) -> IPv6BGPPeer:
                    return IPv6BGPPeer(
                        peer_address=self.v6_bgp_peer_address,
                        authentication_key=self.v6_bgp_authentication_key,
                        has_custom_policies=self.v6_bgp_has_custom_policies,
                        bfd_enabled=self.v6_bgp_bfd_enabled,
                        multipath_enabled=self.v6_bgp_multipath_enabled,
                        prefix_limit=self.v6_bgp_prefix_limit,
                        ttl_security=self.v6_bgp_ttl_security,
                        is_passive=self.v6_bgp_is_passive,
                        send_default_route=self.v6_bgp_send_default_route,
                        add_v6_multicast=self.v6_bgp_add_v6_multicast,
                    )

            binding_port_input_form = yield BindingPortModificationForm
            return {
                "operation": initial_input.operation,
                "verb": "deploy",
                "active_ap": user_input.access_port,
                "tt_number": initial_input.tt_number,
                "modified_sbp": binding_port_input_form.model_dump(),
                "include_ansible_playbook_sbp_config": initial_input.include_ansible_playbook_sbp_config,
                "include_ansible_playbook_bgp_config": initial_input.include_ansible_playbook_bgp_config,
            }

        case _:
            msg = f"Invalid operation selected: {initial_input.operation}"
            raise ValueError(msg)

create_new_sbp(subscription, added_access_port)

Add new SBP to the L3 Core Service subscription.

Source code in gso/workflows/l3_core_service/base_modify_l3_core_service.py
@step("Instantiate new Service Binding Ports")
def create_new_sbp(subscription: SubscriptionModel, added_access_port: dict[str, Any]) -> State:
    """Add new SBP to the L3 Core Service subscription."""
    edge_port = EdgePort.from_subscription(added_access_port.pop("edge_port"))
    bgp_session_list = [
        BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True)
        for session in [added_access_port["v4_bgp_peer"], added_access_port["v6_bgp_peer"]]
    ]
    v4_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **added_access_port.pop("v4_bfd_settings"))
    v6_bfd_settings = BFDSettings.new(subscription_id=uuid4(), **added_access_port.pop("v6_bfd_settings"))
    sbp_gs_id = (
        generate_unique_id(prefix="GS")
        if added_access_port.pop("generate_gs_id", False)
        else added_access_port.pop("gs_id")
    )
    added_access_port.pop("gs_id", None)
    service_binding_port = ServiceBindingPort.new(
        subscription_id=uuid4(),
        **added_access_port,
        v4_bfd_settings=v4_bfd_settings,
        v6_bfd_settings=v6_bfd_settings,
        bgp_session_list=bgp_session_list,
        sbp_type=SBPType.L3,
        edge_port=edge_port.edge_port,
        gs_id=sbp_gs_id,
    )
    new_ap = AccessPort.new(
        subscription_id=uuid4(),
        ap_type=added_access_port["ap_type"],
        sbp=service_binding_port,
        custom_service_name=added_access_port.get("custom_service_name"),
    )
    subscription.l3_core.ap_list.append(new_ap)  # type: ignore[attr-defined]

    return {
        "subscription": subscription,
        "active_ap": new_ap.subscription_instance_id,
        "edge_port_fqdn_list": [new_ap.sbp.edge_port.node.router_fqdn],
    }

remove_old_sbp(subscription, active_ap)

Remove old SBP product blocks from the specific L3 core service subscription.

Source code in gso/workflows/l3_core_service/base_modify_l3_core_service.py
@step("Clean up removed Edge Ports")
def remove_old_sbp(subscription: SubscriptionModel, active_ap: UUID) -> State:
    """Remove old SBP product blocks from the specific L3 core service subscription."""
    subscription.l3_core.ap_list.remove(AccessPort.from_db(active_ap))  # type: ignore[attr-defined]

    return {"subscription": subscription}

modify_existing_sbp(subscription, active_ap, modified_sbp)

Update the subscription model.

Source code in gso/workflows/l3_core_service/base_modify_l3_core_service.py
@step("Modify existing Service Binding Ports")
def modify_existing_sbp(
    subscription: SubscriptionModel,
    active_ap: UUID,
    modified_sbp: dict[str, Any],
) -> State:
    """Update the subscription model."""
    current_ap = next(
        ap
        for ap in subscription.l3_core.ap_list  # type: ignore[attr-defined]
        if ap.subscription_instance_id == active_ap
    )
    v4_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families)
    for attribute in modified_sbp["v4_bgp_peer"]:
        setattr(v4_peer, attribute, modified_sbp["v4_bgp_peer"][attribute])
    for attribute in modified_sbp["v4_bfd_settings"]:
        setattr(current_ap.sbp.v4_bfd_settings, attribute, modified_sbp["v4_bfd_settings"][attribute])

    v6_peer = next(peer for peer in current_ap.sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families)
    for attribute in modified_sbp["v6_bgp_peer"]:
        setattr(v6_peer, attribute, modified_sbp["v6_bgp_peer"][attribute])
    for attribute in modified_sbp["v6_bfd_settings"]:
        setattr(current_ap.sbp.v6_bfd_settings, attribute, modified_sbp["v6_bfd_settings"][attribute])

    current_ap.sbp.bgp_session_list = [v4_peer, v6_peer]
    current_ap.sbp.vlan_id = modified_sbp["vlan_id"]
    current_ap.sbp.gs_id = modified_sbp["gs_id"]
    current_ap.sbp.is_tagged = modified_sbp["is_tagged"]
    current_ap.sbp.ipv4_address = modified_sbp["ipv4_address"]
    current_ap.sbp.ipv4_mask = modified_sbp["ipv4_mask"]
    current_ap.sbp.ipv6_address = modified_sbp["ipv6_address"]
    current_ap.sbp.ipv6_mask = modified_sbp["ipv6_mask"]
    current_ap.sbp.custom_firewall_filters = modified_sbp["custom_firewall_filters"]
    current_ap.ap_type = modified_sbp["ap_type"]
    current_ap.custom_service_name = modified_sbp["custom_service_name"]

    return {"subscription": subscription, "edge_port_fqdn_list": [current_ap.sbp.edge_port.node.router_fqdn]}

populate_partner_and_scope_subscription(subscription, active_ap)

Populate the partner name and build the scoped subscription.

Source code in gso/workflows/l3_core_service/base_modify_l3_core_service.py
@step("Populate required fields for LSO interactions")
def populate_partner_and_scope_subscription(subscription: SubscriptionModel, active_ap: UUID) -> State:
    """Populate the partner name and build the scoped subscription."""
    partner_name = get_partner_by_id(subscription.customer_id).name
    filtered_ap: list[AccessPort] = list(
        filter(lambda ap: ap.subscription_instance_id == active_ap, subscription.l3_core.ap_list)  # type: ignore[attr-defined, arg-type]
    )

    #  An L3 subscription stores the l3_core block in two places, we scope both.
    scoped_subscription = json.loads(json_dumps(subscription))
    scoped_subscription[subscription.service_name_attribute]["l3_core"]["ap_list"] = filtered_ap  # type: ignore[attr-defined]
    scoped_subscription["l3_core"] = {"ap_list": filtered_ap}

    return {"partner_name": partner_name, "scoped_subscription": scoped_subscription}