diff --git "a/gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py" "b/gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py" new file mode 100644 index 0000000000000000000000000000000000000000..0e64bb46fb134fe59c5bfedd85ff06f8511ac0cc --- /dev/null +++ "b/gso/migrations/versions/2024-09-26_289e5334848f_add_g\303\251ant_ip_modification_workflow.py" @@ -0,0 +1,39 @@ +"""Add GÉANT IP modification workflow. + +Revision ID: 289e5334848f +Revises: f4239c9361b4 +Create Date: 2024-09-26 15:27:51.471877 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '289e5334848f' +down_revision = 'f4239c9361b4' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "modify_geant_ip", + "target": "MODIFY", + "description": "Modify G\u00c9ANT IP", + "product_type": "GeantIP" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + create_workflow(conn, workflow) + + +def downgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + delete_workflow(conn, workflow["name"]) diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index 900b1af0e02052579fc3ef9438c9d374c06a7c57..22e58fe62499b5456091a8a2e2f04783a24f83fa 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -54,6 +54,7 @@ "modify_connection_strategy": "Modify connection strategy", "modify_router_kentik_license": "Modify device license in Kentik", "modify_edge_port": "Modify Edge Port", + "modify_geant_ip": "Modify GÉANT IP", "terminate_iptrunk": "Terminate IP Trunk", "terminate_router": "Terminate Router", "terminate_site": "Terminate Site", diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 29d180c1dd139a3b4803eaed432b391a611a30d5..4bf546c4fc906c409a7d43b3f2efc9db8ff222fb 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -89,3 +89,4 @@ LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_po # GÉANT IP workflows LazyWorkflowInstance("gso.workflows.geant_ip.create_geant_ip", "create_geant_ip") +LazyWorkflowInstance("gso.workflows.geant_ip.modify_geant_ip", "modify_geant_ip") diff --git a/gso/workflows/geant_ip/create_geant_ip.py b/gso/workflows/geant_ip/create_geant_ip.py index a55ebd2eee372e667476b5f7e4751c6dfeee45f6..a194abcb5505a3039047aea936a796342eed8957 100644 --- a/gso/workflows/geant_ip/create_geant_ip.py +++ b/gso/workflows/geant_ip/create_geant_ip.py @@ -33,7 +33,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: """Gather input from the operator to build a new subscription object.""" class CreateGeantIPForm(FormPage): - model_config = ConfigDict(title=f"{product_name} - Select partner") + model_config = ConfigDict(title="GÉANT IP - Select partner") tt_number: TTNumber partner: partner_choice() # type: ignore[valid-type] @@ -53,7 +53,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: return edge_ports class EdgePortSelectionForm(FormPage): - model_config = ConfigDict(title=f"{product_name} - Select Edge Ports") + model_config = ConfigDict(title="GÉANT IP - Select Edge Ports") info_label: Label = Field( "Please select the Edge Ports where this GÉANT IP service will terminate", exclude=True ) @@ -62,19 +62,15 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: selected_edge_ports = yield EdgePortSelectionForm ep_list = selected_edge_ports.edge_ports - total_ep_count = len(ep_list) - current_ep_index = 0 class BaseBGPPeer(BaseModel): bfd_enabled: bool = False bfd_interval: int | None = None bfd_multiplier: int | None = None - rtbh_enabled: bool = False has_custom_policies: bool = False authentication_key: str multipath_enabled: bool = False send_default_route: bool = False - is_multi_hop: bool = False is_passive: bool = False class IPv4BGPPeer(BaseBGPPeer): @@ -95,18 +91,16 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: def families(self) -> list[IPFamily]: return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST] + bgp_peer_defaults = {"rtbh_enabled": True, "is_multi_hop": True} binding_port_inputs = [] - while current_ep_index < total_ep_count: - current_edge_port = EdgePort.from_subscription(ep_list[current_ep_index].edge_port) + for ep_index, edge_port in enumerate(ep_list): class BindingPortsInputForm(FormPage): - model_config = ConfigDict( - title=f"{product_name} - Configure Service Binding Ports ({current_ep_index + 1}/{total_ep_count})" - ) + model_config = ConfigDict(title=f"GÉANT IP - Configure Edge Ports ({ep_index + 1}/{len(ep_list)})") info_label: Label = Field("Please configure the Service Binding Ports for each Edge Port.", exclude=True) current_ep_label: Label = Field( - f"Currently configuring on {current_edge_port.description} " - f"(Access Port type: {ep_list[current_ep_index].ap_type})", + f"Currently configuring on {EdgePort.from_subscription(edge_port.edge_port).description} " + f"(Access Port type: {edge_port.ap_type})", exclude=True, ) @@ -125,11 +119,10 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: binding_port_input["sbp_type"] = SBPType.L3 binding_port_input["bgp_peers"] = [ - binding_port_input_form.v4_bgp_peer.model_dump(), - binding_port_input_form.v6_bgp_peer.model_dump(), + binding_port_input_form.v4_bgp_peer.model_dump() | bgp_peer_defaults, + binding_port_input_form.v6_bgp_peer.model_dump() | bgp_peer_defaults, ] binding_port_inputs.append(binding_port_input) - current_ep_index += 1 return ( initial_user_input.model_dump() diff --git a/gso/workflows/geant_ip/modify_geant_ip.py b/gso/workflows/geant_ip/modify_geant_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..d70db66308a5cfd8f9b5f7398bd455f3d575381d --- /dev/null +++ b/gso/workflows/geant_ip/modify_geant_ip.py @@ -0,0 +1,73 @@ +"""A modification workflow for a GÉANT IP subscription.""" + +from orchestrator import begin, done, step, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, UUIDstr +from orchestrator.workflows.steps import State, resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import BaseModel, ConfigDict + +from gso.products.product_types.geant_ip import GeantIP +from gso.utils.helpers import active_edge_port_selector +from gso.utils.shared_enums import APType + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + subscription = GeantIP.from_subscription(subscription_id) + + class AccessPortSelection(BaseModel): + geant_ip_ep: active_edge_port_selector(partner_id=subscription.customer_id) # type: ignore[valid-type] + nren_ap_type: APType + + def validate_edge_ports_are_unique(edge_ports: list[AccessPortSelection]) -> list[AccessPortSelection]: + """Verify if interfaces are unique.""" + port_names = [port.geant_ip_ep for port in edge_ports] + if len(port_names) != len(set(port_names)): + msg = "Edge Ports must be unique." + raise ValueError(msg) + return edge_ports + + class ModifyGeantIPAccessPortsForm(FormPage): + model_config = ConfigDict(title="Modify GÉANT IP") + access_ports: list[AccessPortSelection] = [ + AccessPortSelection( + geant_ip_ep=str(access_port.geant_ip_ep.owner_subscription_id), nren_ap_type=access_port.nren_ap_type + ) + for access_port in subscription.geant_ip.geant_ip_ap_list + ] + + access_port_input = yield ModifyGeantIPAccessPortsForm + ap_list = access_port_input.access_ports + total_ap_count = len(ap_list) + + access_port_inputs = [] + for access_port_index, access_port in enumerate(ap_list): + access_port_input.append(access_port) + + return access_port_input.model_dump() + + +@step("Update subscription model") +def modify_geant_ip_subscription(subscription: GeantIP) -> State: + """Update the subscription model.""" + return {"subscription": subscription} + + +@workflow( + "Modify GÉANT IP", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_geant_ip(): + """Modify a GÉANT IP subscription.""" + return ( + begin >> store_process_subscription(Target.MODIFY) >> unsync >> modify_geant_ip_subscription >> resync >> done + ) + + +# we can change the list of edge ports, and reflect this in new SBPs +# we can change BFD +# we can change firewall filters and policies +# for BGP peers: +# is_passive chan change