diff --git a/gso/migrations/versions/2023-06-26_e57ebfade80b_add_iptrunk_modify_workflow.py b/gso/migrations/versions/2023-06-26_e57ebfade80b_add_iptrunk_modify_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..cad31c4d8c9436c98513757b14b8b3fa6d18f394 --- /dev/null +++ b/gso/migrations/versions/2023-06-26_e57ebfade80b_add_iptrunk_modify_workflow.py @@ -0,0 +1,39 @@ +"""add Iptrunk modify workflow. + +Revision ID: e57ebfade80b +Revises: 7694b98571f8 +Create Date: 2023-06-26 12:20:16.828055 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'e57ebfade80b' +down_revision = '7694b98571f8' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "modify_generic", + "target": "MODIFY", + "description": "Modify IP trunk", + "product_type": "Iptrunk" + } +] + + +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/products/product_blocks/__init__.py b/gso/products/product_blocks/__init__.py index d1f718d83ec9df7d2b68374f4a7149168dac76b2..221e9d44891e867eb511b23f3347615b8e36bd89 100644 --- a/gso/products/product_blocks/__init__.py +++ b/gso/products/product_blocks/__init__.py @@ -1,8 +1,8 @@ -from enum import IntEnum +from enum import Enum -class PhyPortCapacity(IntEnum): - ONE = 1 - TEN = 10 - HUNDRED = 100 - FOUR_HUNDRED = 400 +class PhyPortCapacity(Enum): + ONE = "1g" + TEN = "10g" + HUNDRED = "100g" + FOUR_HUNDRED = "400g" diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index c340082fc6574fd66ec32249296eeb4cda445dd4..418e7e852d144fe0f98d0bd9417a7b90cef63bfe 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -32,6 +32,8 @@ } }, "workflow": { - "modify_isis_metric": "Modify the ISIS metric." - } + "modify_isis_metric": "Modify the ISIS metric", + "modify_generic": "Modify Trunk interface", + "confirm_info": "Please verify this form looks correct." + } } diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index 77366536d0b024d4ae7837ad7a11e74bf3816612..61f560b81b02bf2e5c4ed7df0d89a92ff4171c86 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -4,12 +4,16 @@ init class that imports all workflows into GSO. from orchestrator.workflows import LazyWorkflowInstance LazyWorkflowInstance("gso.workflows.device.create_device", "create_device") -LazyWorkflowInstance("gso.workflows.device.terminate_device", - "terminate_device") +LazyWorkflowInstance( + "gso.workflows.device.terminate_device", "terminate_device" +) LazyWorkflowInstance("gso.workflows.device.get_facts", "get_facts") LazyWorkflowInstance("gso.workflows.iptrunk.create_iptrunk", "create_iptrunk") +LazyWorkflowInstance("gso.workflows.iptrunk.modify_generic", "modify_generic") LazyWorkflowInstance("gso.workflows.iptrunk.terminate_iptrunk", "terminate_iptrunk") LazyWorkflowInstance("gso.workflows.iptrunk.modify_isis_metric", "modify_isis_metric") +LazyWorkflowInstance("gso.workflows.iptrunk.modify_generic", + "modify_generic") LazyWorkflowInstance("gso.workflows.site.create_site", "create_site") diff --git a/gso/workflows/device/get_facts.py b/gso/workflows/device/get_facts.py index 024631740dfbfacd229a1d442d26c0d274af5e8e..a50caee3a8fe3eba6811107615d1eca0f2ea58c4 100644 --- a/gso/workflows/device/get_facts.py +++ b/gso/workflows/device/get_facts.py @@ -1,9 +1,11 @@ from orchestrator.forms import FormPage from orchestrator.forms.validators import Label from orchestrator.targets import Target + # from orchestrator.types import SubscriptionLifecycle from orchestrator.types import InputForm, UUIDstr from orchestrator.workflow import done, init, step, workflow + # from orchestrator.workflows.steps import ( # resync, # set_status, @@ -21,10 +23,8 @@ def initial_input_form_generator( subscription = Device.from_subscription(subscription_id) class TerminateForm(FormPage): - are_you_sure: Label = ( - f"Are you sure you want to get facts from \ + are_you_sure: Label = f"Are you sure you want to get facts from \ {subscription.description}?" - ) return TerminateForm @@ -48,7 +48,8 @@ def get_facts(subscription_id) -> None: @workflow( "Get Facts from Device", initial_input_form=wrap_modify_initial_input_form( - initial_input_form_generator), + initial_input_form_generator + ), target=Target.SYSTEM, ) def get_facts_from_device(): diff --git a/gso/workflows/iptrunk/modify_generic.py b/gso/workflows/iptrunk/modify_generic.py new file mode 100644 index 0000000000000000000000000000000000000000..9c3c633c352be9c555ca6afa39d92b58bb3ffcfe --- /dev/null +++ b/gso/workflows/iptrunk/modify_generic.py @@ -0,0 +1,240 @@ +import ipaddress +from orchestrator.forms import FormPage, ReadOnlyField +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State +from orchestrator.types import UUIDstr +from orchestrator.workflow import done, init, step, workflow +from orchestrator.workflows.steps import resync +from orchestrator.workflows.steps import store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from orchestrator.forms.validators import Choice, UniqueConstrainedList +from gso.products.product_blocks.iptrunk import IptrunkType +from gso.products.product_blocks import PhyPortCapacity + +from gso.products.product_types.iptrunk import ( + Iptrunk, +) +from gso.services import provisioning_proxy +from gso.services.provisioning_proxy import ( + confirm_pp_results, + await_pp_results, +) + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + subscription = Iptrunk.from_subscription(subscription_id) + + class ModifyIptrunkForm(FormPage): + geant_s_sid: str = subscription.iptrunk.geant_s_sid + iptrunk_description: str = subscription.iptrunk.iptrunk_description + iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type + iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed + iptrunk_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links + iptrunk_isis_metric: int = ReadOnlyField( + subscription.iptrunk.iptrunk_isis_metric + ) + iptrunk_ipv4_network: ipaddress.IPv4Network = ReadOnlyField( + subscription.iptrunk.iptrunk_ipv4_network + ) + iptrunk_ipv6_network: ipaddress.IPv6Network = ReadOnlyField( + subscription.iptrunk.iptrunk_ipv6_network + ) + + initial_user_input = yield ModifyIptrunkForm + + class AeMembersListA(UniqueConstrainedList[str]): + min_items = initial_user_input.iptrunk_minimum_links + + class ModifyIptrunkSideAForm(FormPage): + class Config: + title = "Provide subscription details for side A of the trunk." + + iptrunk_sideA_node: str = ReadOnlyField( + subscription.iptrunk.iptrunk_sideA_node.device_fqdn + ) + iptrunk_sideA_ae_iface: str = ReadOnlyField( + subscription.iptrunk.iptrunk_sideA_ae_iface + ) + iptrunk_sideA_ae_geant_a_sid: str = ( + subscription.iptrunk.iptrunk_sideA_ae_geant_a_sid + ) + iptrunk_sideA_ae_members: AeMembersListA = ( + subscription.iptrunk.iptrunk_sideA_ae_members + ) + iptrunk_sideA_ae_members_descriptions: AeMembersListA = ( + subscription.iptrunk.iptrunk_sideA_ae_members_description + ) + + user_input_side_a = yield ModifyIptrunkSideAForm + + class AeMembersListB(UniqueConstrainedList[str]): + min_items = len(user_input_side_a.iptrunk_sideA_ae_members) + max_items = len(user_input_side_a.iptrunk_sideA_ae_members) + + class ModifyIptrunkSideBForm(FormPage): + class Config: + title = "Provide subscription details for side B of the trunk." + + iptrunk_sideB_node: str = ReadOnlyField( + subscription.iptrunk.iptrunk_sideB_node.device_fqdn + ) + iptrunk_sideB_ae_iface: str = ReadOnlyField( + subscription.iptrunk.iptrunk_sideB_ae_iface + ) + iptrunk_sideB_ae_geant_a_sid: str = ( + subscription.iptrunk.iptrunk_sideB_ae_geant_a_sid + ) + iptrunk_sideB_ae_members: AeMembersListB = ( + subscription.iptrunk.iptrunk_sideB_ae_members + ) + iptrunk_sideB_ae_members_descriptions: AeMembersListB = ( + subscription.iptrunk.iptrunk_sideB_ae_members_description + ) + + user_input_side_b = yield ModifyIptrunkSideBForm + + return ( + initial_user_input.dict() + | user_input_side_a.dict() + | user_input_side_b.dict() + ) + + +@step("Update subscription") +def modify_iptrunk_subscription( + subscription: Iptrunk, + geant_s_sid: str, + iptrunk_type: IptrunkType, + iptrunk_description: str, + iptrunk_speed: str, + iptrunk_minimum_links: int, + iptrunk_sideA_ae_geant_a_sid: str, + iptrunk_sideA_ae_members: list[str], + iptrunk_sideA_ae_members_descriptions: list[str], + iptrunk_sideB_ae_geant_a_sid: str, + iptrunk_sideB_ae_members: list[str], + iptrunk_sideB_ae_members_descriptions: list[str], +) -> State: + subscription.iptrunk.geant_s_sid = geant_s_sid + subscription.iptrunk.iptrunk_description = iptrunk_description + subscription.iptrunk.iptrunk_type = iptrunk_type + subscription.iptrunk.iptrunk_speed = iptrunk_speed + subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links + + subscription.iptrunk.iptrunk_sideA_ae_geant_a_sid = ( + iptrunk_sideA_ae_geant_a_sid + ) + subscription.iptrunk.iptrunk_sideA_ae_members = iptrunk_sideA_ae_members + subscription.iptrunk.iptrunk_sideA_ae_members_description = ( + iptrunk_sideA_ae_members_descriptions + ) + + subscription.iptrunk.iptrunk_sideB_ae_geant_a_sid = ( + iptrunk_sideB_ae_geant_a_sid + ) + subscription.iptrunk.iptrunk_sideB_ae_members = iptrunk_sideB_ae_members + subscription.iptrunk.iptrunk_sideB_ae_members_description = ( + iptrunk_sideB_ae_members_descriptions + ) + + subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}" + + return {"subscription": subscription} + + +@step("Provision IP trunk interface [DRY RUN]") +def provision_ip_trunk_iface_dry( + subscription: Iptrunk, process_id: UUIDstr +) -> State: + provisioning_proxy.provision_ip_trunk( + subscription, process_id, "trunk_interface" + ) + + return { + "subscription": subscription, + "label_text": ( + "Provision of the Trunk interface [DRY] " + "Please refresh to get the results of the playbook" + ), + } + + +@step("Provision IP trunk interface [FOR REAL]") +def provision_ip_trunk_iface_real( + subscription: Iptrunk, process_id: UUIDstr +) -> State: + provisioning_proxy.provision_ip_trunk( + subscription, process_id, "trunk_interface", False + ) + + return { + "subscription": subscription, + "label_text": ( + "Provision of the Trunk interface [REAL] " + "Please refresh to get the results of the playbook" + ), + } + + +@step("Provision IP trunk LLDP interface [DRY RUN]") +def provision_ip_trunk_lldp_iface_dry( + subscription: Iptrunk, process_id: UUIDstr +) -> State: + provisioning_proxy.provision_ip_trunk( + subscription, process_id, "lldp_interface" + ) + + return { + "subscription": subscription, + "label_text": ( + "Provision of the LLDP interface [DRY]" + "Please refresh to get the results of the playbook" + ), + } + + +@step("Provision IP trunk LLDP interface [FOR REAL]") +def provision_ip_trunk_lldp_iface_real( + subscription: Iptrunk, process_id: UUIDstr +) -> State: + provisioning_proxy.provision_ip_trunk( + subscription, process_id, "lldp_interface", False + ) + + return { + "subscription": subscription, + "label_text": ( + "Provision of the LLDP interface [REAL]" + "Please refresh to get the results of the playbook" + ), + } + + +@workflow( + "Modify IP trunk", + initial_input_form=wrap_modify_initial_input_form( + initial_input_form_generator + ), + target=Target.MODIFY, +) +def modify_generic(): + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> modify_iptrunk_subscription + >> provision_ip_trunk_iface_dry + >> await_pp_results + >> confirm_pp_results + >> provision_ip_trunk_iface_real + >> await_pp_results + >> confirm_pp_results + >> provision_ip_trunk_lldp_iface_dry + >> await_pp_results + >> confirm_pp_results + >> provision_ip_trunk_lldp_iface_real + >> await_pp_results + >> confirm_pp_results + >> resync + >> done + ) diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index aa92be43b8c11bdddf0aa99e819f45916447ffb1..0773d71c3ccdea5103211dabd94358d73201658b 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -45,16 +45,16 @@ def create_subscription(product: UUIDstr) -> State: @step("Initialize subscription") def initialize_subscription( - subscription: site.SiteInactive, - site_name: str, - site_city: str, - site_country: str, - site_country_code: str, - site_latitude: float, - site_longitude: float, - site_bgp_community_id: int, - site_internal_id: int, - site_tier: site_pb.SiteTier + subscription: site.SiteInactive, + site_name: str, + site_city: str, + site_country: str, + site_country_code: str, + site_latitude: float, + site_longitude: float, + site_bgp_community_id: int, + site_internal_id: int, + site_tier: site_pb.SiteTier, ) -> State: subscription.site.site_name = site_name subscription.site.site_city = site_city @@ -78,16 +78,17 @@ def initialize_subscription( @workflow( "Create Site", initial_input_form=wrap_create_initial_input_form( - initial_input_form_generator), + initial_input_form_generator + ), target=Target.CREATE, ) def create_site(): return ( - init - >> create_subscription - >> store_process_subscription(Target.CREATE) - >> initialize_subscription - >> set_status(SubscriptionLifecycle.ACTIVE) - >> resync - >> done + init + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done )