Skip to content
Snippets Groups Projects
Commit 459af11f authored by Karel van Klink's avatar Karel van Klink :smiley_cat: Committed by Neda Moeini
Browse files

update IPtrunk product block where the list of LAG members is a separate product block

parent 3a90b9ce
No related branches found
No related tags found
1 merge request!83Clean up the repo a bit, and add some unit tests
Showing
with 279 additions and 231 deletions
import logging
import os
from alembic import context
from sqlalchemy import engine_from_config, pool
import orchestrator
from alembic import context
from orchestrator.db.database import BaseModel
from orchestrator.settings import app_settings
from sqlalchemy import engine_from_config, pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
......@@ -61,7 +61,7 @@ def run_migrations_online() -> None:
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives): # type: ignore
def process_revision_directives(context, revision, directives): # type: ignore[no-untyped-def]
if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
if script.upgrade_ops.is_empty():
......
"""Modify IP trunk model.
Revision ID: 394dc60d5c02
Revises: 01e42c100448
Create Date: 2023-10-11 17:55:38.289125
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '394dc60d5c02'
down_revision = '01e42c100448'
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description'))
"""))
conn.execute(sa.text("""
DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description', 'iptrunk_side_ae_members'))
"""))
conn.execute(sa.text("""
DELETE FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description', 'iptrunk_side_ae_members')
"""))
conn.execute(sa.text("""
INSERT INTO product_blocks (name, description, tag, status) VALUES ('IptrunkInterfaceBlock', 'Interface in a LAG as part of an IP trunk', 'IPTINT', 'active') RETURNING product_blocks.product_block_id
"""))
conn.execute(sa.text("""
INSERT INTO resource_types (resource_type, description) VALUES ('interface_description', 'Description of a LAG interface') RETURNING resource_types.resource_type_id
"""))
conn.execute(sa.text("""
INSERT INTO resource_types (resource_type, description) VALUES ('interface_name', 'Interface name of a LAG member') RETURNING resource_types.resource_type_id
"""))
conn.execute(sa.text("""
INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')))
"""))
conn.execute(sa.text("""
INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description')))
"""))
conn.execute(sa.text("""
INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name')))
"""))
def downgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))
"""))
conn.execute(sa.text("""
DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description', 'interface_name'))
"""))
conn.execute(sa.text("""
DELETE FROM resource_types WHERE resource_types.resource_type IN ('interface_description', 'interface_name')
"""))
conn.execute(sa.text("""
DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))
"""))
conn.execute(sa.text("""
DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))
"""))
conn.execute(sa.text("""
DELETE FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')
"""))
......@@ -6,7 +6,6 @@ from typing import TypeVar
from orchestrator.domain.base import ProductBlockModel
from orchestrator.forms.validators import UniqueConstrainedList
from orchestrator.types import SubscriptionLifecycle, strEnum
from pydantic import Field
from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning
from gso.utils.types.phy_port import PhyPortCapacity
......@@ -20,6 +19,10 @@ class IptrunkType(strEnum):
T = TypeVar("T", covariant=True)
class LAGMemberList(UniqueConstrainedList[T]): # type: ignore[type-var]
pass
class IptrunkInterfaceBlockInactive(
ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkInterfaceBlock"
):
......@@ -49,21 +52,21 @@ class IptrunkSideBlockInactive(
iptrunk_side_node: RouterBlockInactive
iptrunk_side_ae_iface: str | None = None
iptrunk_side_ae_geant_a_sid: str | None = None
iptrunk_side_ae_members: list[IptrunkInterfaceBlockInactive] = Field(default_factory=list)
iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlockInactive]
class IptrunkSideBlockProvisioning(IptrunkSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
iptrunk_side_node: RouterBlockProvisioning
iptrunk_side_ae_iface: str | None = None
iptrunk_side_ae_geant_a_sid: str | None = None
iptrunk_side_ae_members: list[IptrunkInterfaceBlockProvisioning] = Field(default_factory=list)
iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlockProvisioning]
class IptrunkSideBlock(IptrunkSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
iptrunk_side_node: RouterBlock
iptrunk_side_ae_iface: str | None = None
iptrunk_side_ae_geant_a_sid: str | None = None
iptrunk_side_ae_members: list[IptrunkInterfaceBlock] = Field(default_factory=list)
iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlock]
class IptrunkBlockInactive(
......@@ -79,7 +82,6 @@ class IptrunkBlockInactive(
iptrunk_isis_metric: int | None = None
iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
#
iptrunk_sides: IptrunkSides[IptrunkSideBlockInactive]
......@@ -94,7 +96,6 @@ class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLife
iptrunk_isis_metric: int | None = None
iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
#
iptrunk_sides: IptrunkSides[IptrunkSideBlockProvisioning]
......
......@@ -101,7 +101,9 @@ class NetboxClient:
# First get manufacturer id
manufacturer_id = int(self.netbox.dcim.manufacturers.get(name=manufacturer).id)
device_type = DeviceType(**{"manufacturer": manufacturer_id, "model": model, "slug": slug}) # type: ignore
device_type = DeviceType(
**{"manufacturer": manufacturer_id, "model": model, "slug": slug} # type: ignore[arg-type]
)
return self.netbox.dcim.device_types.create(dict(device_type))
def create_device_role(self, name: str, slug: str) -> DeviceRole:
......
......@@ -10,6 +10,7 @@ from gso.products.product_blocks.site import SiteTier
from gso.services import subscriptions
from gso.services.crm import CustomerNotFoundError, get_customer_by_name
from gso.utils.types.phy_port import PhyPortCapacity
from gso.workflows.iptrunk.utils import LAGMember
class ImportResponseModel(BaseModel):
......@@ -54,16 +55,14 @@ class IptrunkImportModel(BaseModel):
iptrunk_description: str
iptrunk_speed: PhyPortCapacity
iptrunk_minimum_links: int
iptrunk_sideA_node_id: str
iptrunk_sideA_ae_iface: str
iptrunk_sideA_ae_geant_a_sid: str
iptrunk_sideA_ae_members: list[str]
iptrunk_sideA_ae_members_descriptions: list[str]
iptrunk_sideB_node_id: str
iptrunk_sideB_ae_iface: str
iptrunk_sideB_ae_geant_a_sid: str
iptrunk_sideB_ae_members: list[str]
iptrunk_sideB_ae_members_descriptions: list[str]
side_a_node_id: str
side_a_ae_iface: str
side_a_ae_geant_a_sid: str
side_a_ae_members: list[LAGMember]
side_b_node_id: str
side_b_ae_iface: str
side_b_ae_geant_a_sid: str
side_b_ae_members: list[LAGMember]
iptrunk_ipv4_network: ipaddress.IPv4Network
iptrunk_ipv6_network: ipaddress.IPv6Network
......@@ -83,14 +82,14 @@ class IptrunkImportModel(BaseModel):
return value
@validator("iptrunk_sideA_node_id", "iptrunk_sideB_node_id")
@validator("side_a_node_id", "side_b_node_id")
def check_if_router_side_is_available(cls, value: str) -> str:
if value not in cls._get_active_routers():
raise ValueError("Router not found")
return value
@validator("iptrunk_sideA_ae_members", "iptrunk_sideB_ae_members")
@validator("side_a_ae_members", "side_b_ae_members")
def check_side_uniqueness(cls, value: list[str]) -> list[str]:
if len(value) != len(set(value)):
raise ValueError("Items must be unique")
......@@ -100,26 +99,16 @@ class IptrunkImportModel(BaseModel):
@root_validator
def check_members(cls, values: dict[str, Any]) -> dict[str, Any]:
min_links = values["iptrunk_minimum_links"]
side_a_members = values.get("iptrunk_sideA_ae_members", [])
side_a_descriptions = values.get("iptrunk_sideA_ae_members_descriptions", [])
side_b_members = values.get("iptrunk_sideB_ae_members", [])
side_b_descriptions = values.get("iptrunk_sideB_ae_members_descriptions", [])
side_a_members = values.get("side_a_ae_members", {})
side_b_members = values.get("side_b_ae_members", {})
len_a = len(side_a_members)
len_a_desc = len(side_a_descriptions)
len_b = len(side_b_members)
len_b_desc = len(side_b_descriptions)
if len_a < min_links:
raise ValueError(f"Side A members should be at least {min_links} (iptrunk_minimum_links)")
if len_a != len_a_desc:
raise ValueError("Mismatch in Side A members and their descriptions")
if len_a != len_b:
raise ValueError("Mismatch between Side A and B members")
if len_a != len_b_desc:
raise ValueError("Mismatch in Side B members and their descriptions")
return values
from uuid import uuid4
from orchestrator.forms import FormPage
from orchestrator.forms.validators import Choice, ChoiceList, UniqueConstrainedList
from orchestrator.targets import Target
......@@ -8,7 +10,7 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form
from pydantic import validator
from pynetbox.models.dcim import Interfaces
from gso.products.product_blocks.iptrunk import IptrunkType
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType
from gso.products.product_blocks.router import RouterVendor
from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
from gso.products.product_types.router import Router
......@@ -24,6 +26,7 @@ from gso.workflows.utils import (
validate_router_in_netbox,
)
from gso.utils.types.phy_port import PhyPortCapacity
from gso.workflows.iptrunk.utils import LAGMember
def initial_input_form_generator(product_name: str) -> FormGenerator:
......@@ -71,7 +74,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
item_type = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed) # type: ignore
unique_items = True
class JuniperAeMembers(UniqueConstrainedList[str]):
class JuniperAeMembers(UniqueConstrainedList[LAGMember]):
min_items = initial_user_input.iptrunk_minimum_links
unique_items = True
......@@ -84,15 +87,15 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
class Config:
title = "Provide subscription details for side A of the trunk."
iptrunk_sideA_ae_iface: side_a_ae_iface # type: ignore[valid-type]
iptrunk_sideA_ae_geant_a_sid: str
iptrunk_sideA_ae_members: ae_members_side_a # type: ignore[valid-type]
iptrunk_sideA_ae_members_descriptions: AeMembersDescriptionListA
side_a_ae_iface: side_a_ae_iface # type: ignore[valid-type]
side_a_ae_geant_a_sid: str
side_a_ae_members: ae_members_side_a # type: ignore[valid-type]
side_a_ae_members_descriptions: AeMembersDescriptionListA
user_input_side_a = yield CreateIptrunkSideAForm
# Remove the selected router for side A, to prevent any loops
routers.pop(str(user_input_router_side_a.iptrunk_sideA_node_id.name))
router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore
routers.pop(str(user_input_side_a.side_a_node_id.name))
router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items())) # type: ignore[arg-type]
class SelectRouterSideB(FormPage):
class Config:
......@@ -109,25 +112,25 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
side_b_ae_iface = available_lags_choices(router_b) or str
class AeMembersListB(ChoiceList):
min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
min_items = len(user_input_side_a.side_a_ae_members)
max_items = len(user_input_side_a.side_a_ae_members)
item_type = available_interfaces_choices(router_b, initial_user_input.iptrunk_speed) # type: ignore
unique_items = True
ae_members_side_b = AeMembersListB if get_router_vendor(router_b) == RouterVendor.NOKIA else JuniperAeMembers
class AeMembersDescriptionListB(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 AeMembersDescriptionListB(UniqueConstrainedList[LAGMember]):
min_items = len(user_input_side_a.side_a_ae_members)
max_items = len(user_input_side_a.side_a_ae_members)
class CreateIptrunkSideBForm(FormPage):
class Config:
title = "Provide subscription details for side B of the trunk."
iptrunk_sideB_ae_iface: side_b_ae_iface # type: ignore[valid-type]
iptrunk_sideB_ae_geant_a_sid: str
iptrunk_sideB_ae_members: ae_members_side_b # type: ignore[valid-type]
iptrunk_sideB_ae_members_descriptions: AeMembersDescriptionListB
side_b_ae_iface: side_b_ae_iface # type: ignore[valid-type]
side_b_ae_geant_a_sid: str
side_b_ae_members: ae_members_side_b # type: ignore[valid-type]
side_b_ae_members_descriptions: AeMembersDescriptionListB
user_input_side_b = yield CreateIptrunkSideBForm
......@@ -170,35 +173,37 @@ def initialize_subscription(
iptrunk_description: str,
iptrunk_speed: PhyPortCapacity,
iptrunk_minimum_links: int,
iptrunk_sideA_node_id: str,
iptrunk_sideA_ae_iface: str,
iptrunk_sideA_ae_geant_a_sid: str,
iptrunk_sideA_ae_members: list[str],
iptrunk_sideA_ae_members_descriptions: list[str],
iptrunk_sideB_node_id: str,
iptrunk_sideB_ae_iface: str,
iptrunk_sideB_ae_geant_a_sid: str,
iptrunk_sideB_ae_members: list[str],
iptrunk_sideB_ae_members_descriptions: list[str],
side_a_node_id: str,
side_a_ae_iface: str,
side_a_ae_geant_a_sid: str,
side_a_ae_members: list[dict],
side_b_node_id: str,
side_b_ae_iface: str,
side_b_ae_geant_a_sid: str,
side_b_ae_members: list[dict],
) -> 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_isis_metric = 9000
subscription.iptrunk.iptrunk_isis_metric = 90000
subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = Router.from_subscription(iptrunk_sideA_node_id).router
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface = iptrunk_sideA_ae_iface
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = iptrunk_sideA_ae_geant_a_sid
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members = iptrunk_sideA_ae_members
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members_description = iptrunk_sideA_ae_members_descriptions
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node = Router.from_subscription(iptrunk_sideB_node_id).router
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface = iptrunk_sideB_ae_iface
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = iptrunk_sideB_ae_geant_a_sid
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members = iptrunk_sideB_ae_members
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members_description = iptrunk_sideB_ae_members_descriptions
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = Router.from_subscription(side_a_node_id).router
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface = side_a_ae_iface
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = side_a_ae_geant_a_sid
for member in side_a_ae_members:
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member)
)
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node = Router.from_subscription(side_b_node_id).router
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface = side_b_ae_iface
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = side_b_ae_geant_a_sid
for member in side_b_ae_members:
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member)
)
subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
subscription = IptrunkProvisioning.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING)
......
import ipaddress
from uuid import uuid4
from orchestrator.forms import FormPage, ReadOnlyField
from orchestrator.forms.validators import UniqueConstrainedList
......@@ -8,11 +9,12 @@ from orchestrator.workflow import StepList, done, init, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form
from gso.products.product_blocks.iptrunk import IptrunkType
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType
from gso.products.product_types.iptrunk import Iptrunk
from gso.services import provisioning_proxy
from gso.services.provisioning_proxy import pp_interaction
from gso.utils.types.phy_port import PhyPortCapacity
from gso.workflows.iptrunk.utils import LAGMember
def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
......@@ -31,38 +33,32 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
initial_user_input = yield ModifyIptrunkForm
class AeMembersListA(UniqueConstrainedList[str]):
class AeMembersListA(UniqueConstrainedList[LAGMember]):
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_sides[0].iptrunk_side_node.router_fqdn)
iptrunk_sideA_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
iptrunk_sideA_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
iptrunk_sideA_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
iptrunk_sideA_ae_members_descriptions: AeMembersListA = subscription.iptrunk.iptrunk_sides[
0
].iptrunk_side_ae_members_description
side_a_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn)
side_a_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
side_a_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
side_a_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
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 AeMembersListB(UniqueConstrainedList[LAGMember]):
min_items = len(user_input_side_a.side_a_ae_members)
max_items = len(user_input_side_a.side_a_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_sides[1].iptrunk_side_node.router_fqdn)
iptrunk_sideB_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
iptrunk_sideB_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
iptrunk_sideB_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
iptrunk_sideB_ae_members_descriptions: AeMembersListB = subscription.iptrunk.iptrunk_sides[
1
].iptrunk_side_ae_members_description
side_b_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn)
side_b_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
side_b_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
side_b_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
user_input_side_b = yield ModifyIptrunkSideBForm
......@@ -77,12 +73,10 @@ def modify_iptrunk_subscription(
iptrunk_description: str,
iptrunk_speed: PhyPortCapacity,
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],
side_a_ae_geant_a_sid: str,
side_a_ae_members: list[dict],
side_b_ae_geant_a_sid: str,
side_b_ae_members: list[dict],
) -> State:
subscription.iptrunk.geant_s_sid = geant_s_sid
subscription.iptrunk.iptrunk_description = iptrunk_description
......@@ -90,13 +84,21 @@ def modify_iptrunk_subscription(
subscription.iptrunk.iptrunk_speed = iptrunk_speed
subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = iptrunk_sideA_ae_geant_a_sid
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members = iptrunk_sideA_ae_members
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members_description = iptrunk_sideA_ae_members_descriptions
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = iptrunk_sideB_ae_geant_a_sid
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members = iptrunk_sideB_ae_members
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members_description = iptrunk_sideB_ae_members_descriptions
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = side_a_ae_geant_a_sid
# Flush the old list of member interfaces
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.clear()
# And update the list to only include the new member interfaces
for member in side_a_ae_members:
subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
)
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = side_b_ae_geant_a_sid
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.clear()
for member in side_b_ae_members:
subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
)
subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
......
from orchestrator import step
from orchestrator.types import State, UUIDstr
from pydantic import BaseModel
from gso.products.product_types.iptrunk import Iptrunk
from gso.services import provisioning_proxy
class LAGMember(BaseModel):
# TODO: validate interface name
interface_name: str
interface_description: str
def __hash__(self) -> int:
return hash((self.interface_name, self.interface_description))
@step("[COMMIT] Set ISIS metric to 90000")
def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State:
old_isis_metric = subscription.iptrunk.iptrunk_isis_metric
......
......@@ -160,7 +160,7 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr,
def create_netbox_device(subscription: RouterProvisioning) -> State:
if subscription.router.router_vendor == RouterVendor.NOKIA:
NetboxClient().create_device(
subscription.router.router_fqdn, subscription.router.router_site.site_tier # type: ignore
subscription.router.router_fqdn, subscription.router.router_site.site_tier # type: ignore[arg-type, union-attr]
)
return {"subscription": subscription, "label_text": "Creating NetBox device"}
return {"subscription": subscription, "label_text": "Skipping NetBox device creation for Juniper router."}
......
......@@ -15,6 +15,7 @@ from gso.services import subscriptions
from gso.services.crm import get_customer_by_name
from gso.utils.types.phy_port import PhyPortCapacity
from gso.workflows.iptrunk.create_iptrunk import initialize_subscription
from gso.workflows.iptrunk.utils import LAGMember
def _generate_routers() -> dict[str, str]:
......@@ -42,17 +43,15 @@ def initial_input_form_generator() -> FormGenerator:
iptrunk_speed: PhyPortCapacity
iptrunk_minimum_links: int
iptrunk_sideA_node_id: RouterEnum # type: ignore[valid-type]
iptrunk_sideA_ae_iface: str
iptrunk_sideA_ae_geant_a_sid: str
iptrunk_sideA_ae_members: UniqueConstrainedList[str]
iptrunk_sideA_ae_members_descriptions: UniqueConstrainedList[str]
side_a_node_id: RouterEnum # type: ignore[valid-type]
side_a_ae_iface: str
side_a_ae_geant_a_sid: str
side_a_ae_members: UniqueConstrainedList[LAGMember]
iptrunk_sideB_node_id: RouterEnum # type: ignore[valid-type]
iptrunk_sideB_ae_iface: str
iptrunk_sideB_ae_geant_a_sid: str
iptrunk_sideB_ae_members: UniqueConstrainedList[str]
iptrunk_sideB_ae_members_descriptions: UniqueConstrainedList[str]
side_b_node_id: RouterEnum # type: ignore[valid-type]
side_b_ae_iface: str
side_b_ae_geant_a_sid: str
side_b_ae_members: UniqueConstrainedList[LAGMember]
iptrunk_ipv4_network: ipaddress.IPv4Network
iptrunk_ipv6_network: ipaddress.IPv6Network
......@@ -82,6 +81,7 @@ def update_ipam_stub_for_subscription(
) -> State:
subscription.iptrunk.iptrunk_ipv4_network = iptrunk_ipv4_network
subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
return {"subscription": subscription}
......
......@@ -46,6 +46,8 @@ show_error_codes = true
show_column_numbers = true
# Suppress "note: By default the bodies of untyped functions are not checked"
disable_error_code = "annotation-unchecked"
# Forbid the use of a generic "type: ignore" without specifying the exact error that is ignored
enable_error_code = "ignore-without-code"
[tool.ruff]
exclude = [
......
......@@ -6,7 +6,7 @@ from orchestrator.domain import SubscriptionModel
from orchestrator.types import SubscriptionLifecycle, UUIDstr
from gso.products import ProductType
from gso.products.product_blocks.iptrunk import IptrunkSideBlock, IptrunkType
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType
from gso.products.product_blocks.router import RouterRole, RouterVendor
from gso.products.product_blocks.site import SiteTier
from gso.products.product_types.iptrunk import IptrunkInactive
......@@ -140,10 +140,13 @@ def iptrunk_side_subscription_factory(router_subscription_factory, faker):
iptrunk_side_node = Router.from_subscription(iptrunk_side_node_id).router
iptrunk_side_ae_iface = iptrunk_side_ae_iface or faker.pystr()
iptrunk_side_ae_geant_a_sid = iptrunk_side_ae_geant_a_sid or faker.geant_sid()
iptrunk_side_ae_members = iptrunk_side_ae_members or [faker.network_interface(), faker.network_interface()]
iptrunk_side_ae_members_description = iptrunk_side_ae_members_description or [
faker.sentence(),
faker.sentence(),
iptrunk_side_ae_members = iptrunk_side_ae_members or [
IptrunkInterfaceBlock.new(
faker.uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence()
),
IptrunkInterfaceBlock.new(
faker.uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence()
),
]
return IptrunkSideBlock.new(
......
......@@ -26,16 +26,18 @@ def iptrunk_data(router_subscription_factory, faker):
"iptrunk_description": faker.sentence(),
"iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
"iptrunk_minimum_links": 5,
"iptrunk_sideA_node_id": router_side_a,
"iptrunk_sideA_ae_iface": faker.pystr(),
"iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
"iptrunk_sideA_ae_members": [faker.pystr() for _ in range(5)],
"iptrunk_sideA_ae_members_descriptions": [faker.sentence() for _ in range(5)],
"iptrunk_sideB_node_id": router_side_b,
"iptrunk_sideB_ae_iface": faker.pystr(),
"iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
"iptrunk_sideB_ae_members": [faker.pystr() for _ in range(5)],
"iptrunk_sideB_ae_members_descriptions": [faker.sentence() for _ in range(5)],
"side_a_node_id": router_side_a,
"side_a_ae_iface": faker.pystr(),
"side_a_ae_geant_a_sid": faker.pystr(),
"side_a_ae_members": [
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
],
"side_b_node_id": router_side_b,
"side_b_ae_iface": faker.pystr(),
"side_b_ae_geant_a_sid": faker.pystr(),
"side_b_ae_members": [
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
],
"iptrunk_ipv4_network": str(faker.ipv4_network()),
"iptrunk_ipv6_network": str(faker.ipv6_network()),
}
......@@ -43,13 +45,13 @@ def iptrunk_data(router_subscription_factory, faker):
@pytest.fixture
def mock_routers(iptrunk_data):
first_call = [iptrunk_data["iptrunk_sideA_node_id"], iptrunk_data["iptrunk_sideB_node_id"], str(uuid4())]
first_call = [iptrunk_data["side_a_node_id"], iptrunk_data["side_b_node_id"], str(uuid4())]
side_effects = [
first_call,
first_call,
[
(iptrunk_data["iptrunk_sideA_node_id"], "iptrunk_sideA_node_id description"),
(iptrunk_data["iptrunk_sideB_node_id"], "iptrunk_sideB_node_id description"),
(iptrunk_data["side_a_node_id"], "side_a_node_id description"),
(iptrunk_data["side_b_node_id"], "side_b_node_id description"),
(str(uuid4()), "random description"),
],
]
......@@ -203,26 +205,40 @@ def test_import_iptrunk_invalid_router_id_side_a_and_b(mock_start_process, test_
assert response.status_code == 422
assert response.json() == {
"detail": [
{"loc": ["body", "iptrunk_sideA_node_id"], "msg": "Router not found", "type": "value_error"},
{"loc": ["body", "iptrunk_sideB_node_id"], "msg": "Router not found", "type": "value_error"},
{"loc": ["body", "side_a_node_id"], "msg": "Router not found", "type": "value_error"},
{"loc": ["body", "side_b_node_id"], "msg": "Router not found", "type": "value_error"},
]
}
@patch("gso.api.v1.imports._start_process")
def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, iptrunk_data, mock_routers):
def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, iptrunk_data, mock_routers, faker):
mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
iptrunk_data["iptrunk_sideA_ae_members"] = [5, 5, 5, 5, 5]
iptrunk_data["iptrunk_sideB_ae_members"] = [4, 4, 4, 5, 5]
repeat_interface_a = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()}
repeat_interface_b = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()}
iptrunk_data["side_a_ae_members"] = [
repeat_interface_a,
repeat_interface_a,
repeat_interface_a,
repeat_interface_a,
repeat_interface_a,
]
iptrunk_data["side_b_ae_members"] = [
repeat_interface_b,
repeat_interface_a,
repeat_interface_a,
repeat_interface_b,
repeat_interface_b,
]
response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
assert response.status_code == 422
assert response.json() == {
"detail": [
{"loc": ["body", "iptrunk_sideA_ae_members"], "msg": "Items must be unique", "type": "value_error"},
{"loc": ["body", "iptrunk_sideB_ae_members"], "msg": "Items must be unique", "type": "value_error"},
{"loc": ["body", "side_a_ae_members"], "msg": "Items must be unique", "type": "value_error"},
{"loc": ["body", "side_b_ae_members"], "msg": "Items must be unique", "type": "value_error"},
{
"loc": ["body", "__root__"],
"msg": "Side A members should be at least 5 (iptrunk_minimum_links)",
......@@ -233,12 +249,12 @@ def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_clien
@patch("gso.api.v1.imports._start_process")
def test_iptrunk_import_fails_on_side_a_member_count_mismatch(
def test_import_iptrunk_fails_on_side_a_member_count_mismatch(
mock_start_process, test_client, iptrunk_data, mock_routers
):
mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
iptrunk_data["iptrunk_sideA_ae_members"].remove(iptrunk_data["iptrunk_sideA_ae_members"][0])
iptrunk_data["side_a_ae_members"].remove(iptrunk_data["side_a_ae_members"][0])
response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
......@@ -255,36 +271,12 @@ def test_iptrunk_import_fails_on_side_a_member_count_mismatch(
@patch("gso.api.v1.imports._start_process")
def test_iptrunk_import_fails_on_side_a_member_description_mismatch(
mock_start_process, test_client, iptrunk_data, mock_routers
):
mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
iptrunk_data["iptrunk_sideA_ae_members_descriptions"].remove(
iptrunk_data["iptrunk_sideA_ae_members_descriptions"][0]
)
response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"loc": ["body", "__root__"],
"msg": "Mismatch in Side A members and their descriptions",
"type": "value_error",
}
]
}
@patch("gso.api.v1.imports._start_process")
def test_iptrunk_import_fails_on_side_a_and_b_members_mismatch(
def test_import_iptrunk_fails_on_side_a_and_b_members_mismatch(
mock_start_process, test_client, iptrunk_data, mock_routers
):
mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
iptrunk_data["iptrunk_sideB_ae_members"].remove(iptrunk_data["iptrunk_sideB_ae_members"][0])
iptrunk_data["side_b_ae_members"].remove(iptrunk_data["side_b_ae_members"][0])
response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
......@@ -292,27 +284,3 @@ def test_iptrunk_import_fails_on_side_a_and_b_members_mismatch(
assert response.json() == {
"detail": [{"loc": ["body", "__root__"], "msg": "Mismatch between Side A and B members", "type": "value_error"}]
}
@patch("gso.api.v1.imports._start_process")
def test_iptrunk_import_fails_on_side_b_member_description_mismatch(
mock_start_process, test_client, iptrunk_data, mock_routers
):
mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
iptrunk_data["iptrunk_sideB_ae_members_descriptions"].remove(
iptrunk_data["iptrunk_sideB_ae_members_descriptions"][0]
)
response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"loc": ["body", "__root__"],
"msg": "Mismatch in Side B members and their descriptions",
"type": "value_error",
}
]
}
......@@ -94,18 +94,22 @@ def input_form_wizard_data(router_subscription_factory, faker):
}
create_ip_trunk_side_a_router_name = {"iptrunk_sideA_node_id": router_side_a}
create_ip_trunk_side_a_step = {
"iptrunk_sideA_ae_iface": "LAG1",
"iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
"iptrunk_sideA_ae_members": ["Interface1", "Interface2"],
"iptrunk_sideA_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
"side_a_node_id": router_side_a,
"side_a_ae_iface": faker.pystr(),
"side_a_ae_geant_a_sid": faker.pystr(),
"side_a_ae_members": [
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
],
}
create_ip_trunk_side_b_router_name = {"iptrunk_sideB_node_id": router_side_b}
create_ip_trunk_side_b_step = {
"iptrunk_sideB_ae_iface": "LAG1",
"iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
"iptrunk_sideB_ae_members": ["Interface1", "Interface2"],
"iptrunk_sideB_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
"side_b_node_id": router_side_b,
"side_b_ae_iface": faker.pystr(),
"side_b_ae_geant_a_sid": faker.pystr(),
"side_b_ae_members": [
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
],
}
return [
......
......@@ -32,34 +32,12 @@ def test_iptrunk_modify_trunk_interface_success(
new_side_a_sid = faker.geant_sid()
new_side_a_ae_members = [
faker.network_interface(),
faker.network_interface(),
faker.network_interface(),
faker.network_interface(),
faker.network_interface(),
]
new_side_a_ae_descriptions = [
faker.sentence(),
faker.sentence(),
faker.sentence(),
faker.sentence(),
faker.sentence(),
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
]
new_side_b_sid = faker.geant_sid()
new_side_b_ae_members = [
faker.network_interface(),
faker.network_interface(),
faker.network_interface(),
faker.network_interface(),
faker.network_interface(),
]
new_side_b_ae_descriptions = [
faker.sentence(),
faker.sentence(),
faker.sentence(),
faker.sentence(),
faker.sentence(),
{"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
]
# Run workflow
......@@ -74,14 +52,12 @@ def test_iptrunk_modify_trunk_interface_success(
"iptrunk_minimum_links": new_link_count,
},
{
"iptrunk_sideA_ae_geant_a_sid": new_side_a_sid,
"iptrunk_sideA_ae_members": new_side_a_ae_members,
"iptrunk_sideA_ae_members_descriptions": new_side_a_ae_descriptions,
"side_a_ae_geant_a_sid": new_side_a_sid,
"side_a_ae_members": new_side_a_ae_members,
},
{
"iptrunk_sideB_ae_geant_a_sid": new_side_b_sid,
"iptrunk_sideB_ae_members": new_side_b_ae_members,
"iptrunk_sideB_ae_members_descriptions": new_side_b_ae_descriptions,
"side_b_ae_geant_a_sid": new_side_b_sid,
"side_b_ae_members": new_side_b_ae_members,
},
]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment