diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 4c973ace3bd5653e751292c7a078576fb79265ce..7bfcbd2afccdd9c13623a4817fc3567c2bfdfe03 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -43,13 +43,11 @@ from gso.utils.types.base_site import BaseSiteValidatorModel from gso.utils.types.geant_ids import IMPORTED_GA_ID, IMPORTED_GS_ID from gso.utils.types.interfaces import BandwidthString, LAGMember, LAGMemberList, PhysicalPortCapacity from gso.utils.types.ip_address import ( - AddressSpace, IPAddress, IPv4AddressType, - IPV4Netmask, - IPv4NetworkType, + IPv4Netmask, IPv6AddressType, - IPV6Netmask, + IPv6Netmask, PortNumber, ) from gso.utils.types.virtual_identifiers import VC_ID, VLAN_ID @@ -276,9 +274,9 @@ class L3CoreServiceImportModel(BaseModel): vlan_id: VLAN_ID custom_firewall_filters: bool = False ipv4_address: IPv4AddressType - ipv4_mask: IPV4Netmask + ipv4_mask: IPv4Netmask ipv6_address: IPv6AddressType - ipv6_mask: IPV6Netmask + ipv6_mask: IPv6Netmask is_multi_hop: bool = True bgp_peers: list["L3CoreServiceImportModel.BaseBGPPeer"] v4_bfd_settings: "L3CoreServiceImportModel.BFDSettingsModel" @@ -316,7 +314,6 @@ class LanSwitchInterconnectRouterSideImportModel(BaseModel): node: UUIDstr ae_iface: str ae_members: LAGMemberList[LAGMember] - ipv4_address: IPv4AddressType class LanSwitchInterconnectSwitchSideImportModel(BaseModel): @@ -325,15 +322,12 @@ class LanSwitchInterconnectSwitchSideImportModel(BaseModel): switch: UUIDstr ae_iface: str ae_members: LAGMemberList[LAGMember] - ipv4_address: IPv4AddressType class LanSwitchInterconnectImportModel(BaseModel): """Import LAN Switch Interconnect model.""" lan_switch_interconnect_description: str - lan_switch_interconnect_ip_network: IPv4NetworkType | None - address_space: AddressSpace minimum_links: int router_side: LanSwitchInterconnectRouterSideImportModel switch_side: LanSwitchInterconnectSwitchSideImportModel diff --git a/gso/migrations/versions/2024-11-04_e854e0c35e20_update_lan_switch_interconnect.py b/gso/migrations/versions/2024-11-04_e854e0c35e20_update_lan_switch_interconnect.py new file mode 100644 index 0000000000000000000000000000000000000000..f041528c680db07517f94e05c7c6b7f767d0919a --- /dev/null +++ b/gso/migrations/versions/2024-11-04_e854e0c35e20_update_lan_switch_interconnect.py @@ -0,0 +1,48 @@ +"""Update LAN Switch Interconnect. + +Revision ID: e854e0c35e20 +Revises: 0e7e7d749617 +Create Date: 2024-11-04 17:21:14.612740 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'e854e0c35e20' +down_revision = '0e7e7d749617' +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 ('LanSwitchInterconnectSwitchSideBlock', 'LanSwitchInterconnectRouterSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address')) + """)) + 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 ('LanSwitchInterconnectSwitchSideBlock', 'LanSwitchInterconnectRouterSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address')) + """)) + 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 ('LanSwitchInterconnectBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('address_space')) + """)) + 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 ('LanSwitchInterconnectBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('address_space')) + """)) + 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 ('LanSwitchInterconnectBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('lan_switch_interconnect_ip_network')) + """)) + 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 ('LanSwitchInterconnectBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('lan_switch_interconnect_ip_network')) + """)) + 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 ('address_space', 'lan_switch_interconnect_ip_network')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('address_space', 'lan_switch_interconnect_ip_network') + """)) + + +def downgrade() -> None: + conn = op.get_bind() + diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 63de6f892954672074a3327987572626ed724c16..f6e85ed8706fb385f1776eca30ad201603ed7bf7 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -51,6 +51,13 @@ "domain_name": ".geantip", "dns_view": "default", "network_view": "default" + }, + "LAN_SWITCH_INTERCONNECT": { + "V4": {"containers": ["10.2.0.0/16"], "networks": [], "mask": 24}, + "V6": {"containers": [], "networks": [], "mask": 126}, + "domain_name": ".geant.net", + "dns_view": "default", + "network_view": "default" } }, "MONITORING": { diff --git a/gso/products/product_blocks/lan_switch_interconnect.py b/gso/products/product_blocks/lan_switch_interconnect.py index a3128a72347e90d8f5eb86d00509e441b97f2b01..30c2fb93672c66a110d0dee5cdb1171113b84a0f 100644 --- a/gso/products/product_blocks/lan_switch_interconnect.py +++ b/gso/products/product_blocks/lan_switch_interconnect.py @@ -6,7 +6,6 @@ from orchestrator.types import SubscriptionLifecycle from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning from gso.products.product_blocks.switch import SwitchBlock, SwitchBlockInactive, SwitchBlockProvisioning from gso.utils.types.interfaces import LAGMemberList -from gso.utils.types.ip_address import AddressSpace, IPv4AddressType, IPv4NetworkType class LanSwitchInterconnectInterfaceBlockInactive( @@ -48,7 +47,6 @@ class LanSwitchInterconnectRouterSideBlockInactive( node: RouterBlockInactive ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockInactive] - ipv4_address: IPv4AddressType | None = None class LanSwitchInterconnectRouterSideBlockProvisioning( @@ -59,7 +57,6 @@ class LanSwitchInterconnectRouterSideBlockProvisioning( node: RouterBlockProvisioning ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockProvisioning] # type: ignore[assignment] - ipv4_address: IPv4AddressType | None class LanSwitchInterconnectRouterSideBlock( @@ -70,7 +67,6 @@ class LanSwitchInterconnectRouterSideBlock( node: RouterBlock ae_iface: str ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlock] # type: ignore[assignment] - ipv4_address: IPv4AddressType | None class LanSwitchInterconnectSwitchSideBlockInactive( @@ -83,7 +79,6 @@ class LanSwitchInterconnectSwitchSideBlockInactive( switch: SwitchBlockInactive ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockInactive] - ipv4_address: IPv4AddressType | None = None class LanSwitchInterconnectSwitchSideBlockProvisioning( @@ -94,7 +89,6 @@ class LanSwitchInterconnectSwitchSideBlockProvisioning( switch: SwitchBlockProvisioning ae_iface: str | None = None ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlockProvisioning] # type: ignore[assignment] - ipv4_address: IPv4AddressType | None class LanSwitchInterconnectSwitchSideBlock( @@ -105,7 +99,6 @@ class LanSwitchInterconnectSwitchSideBlock( switch: SwitchBlock ae_iface: str ae_members: LAGMemberList[LanSwitchInterconnectInterfaceBlock] # type: ignore[assignment] - ipv4_address: IPv4AddressType | None class LanSwitchInterconnectBlockInactive( @@ -116,8 +109,6 @@ class LanSwitchInterconnectBlockInactive( """A LAN Switch Interconnect that's currently inactive, see ``LanSwitchInterconnectBlock``.""" lan_switch_interconnect_description: str | None = None - lan_switch_interconnect_ip_network: IPv4NetworkType | None = None - address_space: AddressSpace | None = None minimum_links: int | None = None router_side: LanSwitchInterconnectRouterSideBlockInactive switch_side: LanSwitchInterconnectSwitchSideBlockInactive @@ -129,8 +120,6 @@ class LanSwitchInterconnectBlockProvisioning( """A LAN Switch Interconnect that's currently being provisioned, see ``LanSwitchInterconnectBlock``.""" lan_switch_interconnect_description: str | None = None - lan_switch_interconnect_ip_network: IPv4NetworkType | None - address_space: AddressSpace | None = None minimum_links: int | None = None router_side: LanSwitchInterconnectRouterSideBlockProvisioning switch_side: LanSwitchInterconnectSwitchSideBlockProvisioning @@ -141,10 +130,6 @@ class LanSwitchInterconnectBlock(LanSwitchInterconnectBlockProvisioning, lifecyc #: A human-readable description of this LAN Switch Interconnect. lan_switch_interconnect_description: str - #: The IP resources for this VLAN Switch Interconnect. - lan_switch_interconnect_ip_network: IPv4NetworkType | None - #: The address space of the VLAN Switch Interconnect. It can be private or public. - address_space: AddressSpace #: The minimum amount of links the LAN Switch Interconnect should consist of. minimum_links: int #: The router side of the LAN Switch Interconnect. diff --git a/gso/products/product_blocks/service_binding_port.py b/gso/products/product_blocks/service_binding_port.py index 5f10b33c54c7ffa373fd555effef6014f5e59a4a..6d736f56b29919190122f67155823d0b3bfdad0a 100644 --- a/gso/products/product_blocks/service_binding_port.py +++ b/gso/products/product_blocks/service_binding_port.py @@ -14,7 +14,7 @@ from gso.products.product_blocks.bgp_session import ( ) from gso.products.product_blocks.edge_port import EdgePortBlock, EdgePortBlockInactive, EdgePortBlockProvisioning from gso.utils.shared_enums import SBPType -from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask from gso.utils.types.virtual_identifiers import VLAN_ID @@ -60,9 +60,9 @@ class ServiceBindingPortInactive( vlan_id: VLAN_ID | None = None sbp_type: SBPType | None = None ipv4_address: IPv4AddressType | None = None - ipv4_mask: IPV4Netmask | None = None + ipv4_mask: IPv4Netmask | None = None ipv6_address: IPv6AddressType | None = None - ipv6_mask: IPV6Netmask | None = None + ipv6_mask: IPv6Netmask | None = None custom_firewall_filters: bool | None = None gs_id: str | None = None bgp_session_list: list[BGPSessionInactive] = Field(default_factory=list) @@ -78,9 +78,9 @@ class ServiceBindingPortProvisioning(ServiceBindingPortInactive, lifecycle=[Subs vlan_id: VLAN_ID | None = None sbp_type: SBPType ipv4_address: IPv4AddressType | None = None - ipv4_mask: IPV4Netmask | None = None + ipv4_mask: IPv4Netmask | None = None ipv6_address: IPv6AddressType | None = None - ipv6_mask: IPV6Netmask | None = None + ipv6_mask: IPv6Netmask | None = None custom_firewall_filters: bool gs_id: str bgp_session_list: list[BGPSessionProvisioning] # type: ignore[assignment] @@ -101,11 +101,11 @@ class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[Subscription #: If layer 3, IPv4 resources. ipv4_address: IPv4AddressType | None = None #: IPV4 subnet mask. - ipv4_mask: IPV4Netmask | None = None + ipv4_mask: IPv4Netmask | None = None #: If layer 3, IPv6 resources. ipv6_address: IPv6AddressType | None = None #: IPV6 subnet mask. - ipv6_mask: IPV6Netmask | None = None + ipv6_mask: IPv6Netmask | None = None #: Any custom firewall filters that the partner may require. custom_firewall_filters: bool #: The GÉANT service ID of this binding port. diff --git a/gso/services/infoblox.py b/gso/services/infoblox.py index cad3fb436961519534cf403db7daa386f8764bbb..1068be9750a15e14b6f39a4356375c407b9f8985 100644 --- a/gso/services/infoblox.py +++ b/gso/services/infoblox.py @@ -10,7 +10,7 @@ from infoblox_client.exceptions import ( ) from gso.settings import IPAMParams, load_oss_params -from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType +from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv4NetworkType, IPv6AddressType, IPv6Netmask logger = getLogger(__name__) NULL_MAC = "00:00:00:00:00:00" @@ -41,7 +41,7 @@ def _allocate_network( # noqa: PLR0917 conn: connector.Connector, dns_view: str, network_view: str, - netmask: int, + netmask: IPv4Netmask | IPv6Netmask, containers: list[str], comment: str | None = "", ) -> ipaddress.IPv4Network | ipaddress.IPv6Network: @@ -73,6 +73,21 @@ def _allocate_network( # noqa: PLR0917 raise AllocationError(msg) +def create_v4_network_by_ip( + dns_view: str, network_view: str, network: IPv4NetworkType, comment: str | None = "" +) -> None: + """Register an IPv4 network at the given location. Raises an :class:`AllocationError` on failure.""" + conn, _ = _setup_connection() + created_net = objects.NetworkV4.create( + conn, network=network, view=dns_view, network_view=network_view, comment=comment + ) + if created_net.response != "Infoblox Object created": + msg = f"Failed to allocate network at {network}. Response from Netbox: {created_net.response}" + raise AllocationError(msg) + msg = f"Successfully registered new network at {network}" + logger.debug(msg) + + def hostname_available(hostname: str) -> bool: """Check whether a hostname is still available **in Infoblox**. @@ -229,38 +244,48 @@ def allocate_host( def create_host_by_ip( hostname: str, - ipv4_address: IPv4AddressType, - ipv6_address: IPv6AddressType, service_type: str, comment: str, + ipv4_address: IPv4AddressType | None = None, + ipv6_address: IPv6AddressType | None = None, ) -> None: """Create a new host record with a given IPv4 and IPv6 address. Args: hostname: The FQDN of the new host. - ipv4_address: The IPv4 address of the new host. - ipv6_address: The IPv6 address of the new host. service_type: The relevant service type, used to deduce the correct ``dns_view`` and ``network_view`` in Infoblox. comment: The comment stored in this Infoblox record, most likely the relevant ``subscription_id`` in GSO. + ipv4_address: The IPv4 address of the new host. + ipv6_address: The IPv6 address of the new host. """ if not hostname_available(hostname): msg = f"FQDN '{hostname}' is already in use, allocation aborted." raise AllocationError(msg) + if not (ipv4_address or ipv6_address): + msg = "At least one IP address must be given to create a record for. Received None." + raise AllocationError(msg) conn, oss = _setup_connection() - ipv6_object = objects.IP.create(ip=str(ipv6_address), mac=NULL_MAC, configure_for_dhcp=False) - ipv4_object = objects.IP.create(ip=str(ipv4_address), mac=NULL_MAC, configure_for_dhcp=False) dns_view = getattr(oss, service_type).dns_view network_view = getattr(oss, service_type).network_view # This needs to be done in two steps, otherwise only one of the IP addresses is stored. - objects.HostRecord.create( - conn, ip=ipv6_object, name=hostname, comment=comment, view=dns_view, network_view=network_view - ) - new_host = find_host_by_fqdn(hostname) - new_host.ipv4addrs = [ipv4_object] - new_host.update() + if ipv6_address: # We first register the IPv6 record. + ipv6_object = objects.IP.create(ip=str(ipv6_address), mac=NULL_MAC, configure_for_dhcp=False) + objects.HostRecord.create( + conn, ip=ipv6_object, name=hostname, comment=comment, view=dns_view, network_view=network_view + ) + if ipv4_address: # If set, we also register the IPv4 record. + new_host = find_host_by_fqdn(hostname) + ipv4_object = objects.IP.create(ip=str(ipv4_address), mac=NULL_MAC, configure_for_dhcp=False) + new_host.ipv4addrs = [ipv4_object] + new_host.update() + else: # IPv6 is not set, it must therefore be IPv4 only. + ipv4_object = objects.IP.create(ip=str(ipv4_address), mac=NULL_MAC, configure_for_dhcp=False) + objects.HostRecord.create( + conn, ip=ipv4_object, name=hostname, comment=comment, view=dns_view, network_view=network_view + ) def find_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> objects.HostRecord | None: @@ -290,11 +315,7 @@ def find_host_by_fqdn(fqdn: str) -> objects.HostRecord: fqdn: The FQDN of a host that is searched for. """ conn, _ = _setup_connection() - return objects.HostRecord.search( - conn, - name=fqdn, - return_fields=["ipv4addrs", "name", "view", "aliases", "comment"], - ) + return objects.HostRecord.search(conn, name=fqdn, return_fields=["ipv4addrs", "name", "view", "aliases", "comment"]) def find_v6_host_by_fqdn(fqdn: str) -> objects.HostRecordV6: diff --git a/gso/settings.py b/gso/settings.py index 7f71a5605d52ec1e0df7184dce7880fe00f85f5f..5b3beefd963591541845255ddef67a6a1e0234ab 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -15,7 +15,7 @@ from pydantic import EmailStr from pydantic_forms.types import strEnum from pydantic_settings import BaseSettings -from gso.utils.types.ip_address import IPV4Netmask, IPV6Netmask, PortNumber +from gso.utils.types.ip_address import IPv4Netmask, IPv6Netmask, PortNumber logger = logging.getLogger(__name__) @@ -65,7 +65,7 @@ class V4NetworkParams(BaseSettings): containers: list[ipaddress.IPv4Network] networks: list[ipaddress.IPv4Network] - mask: IPV4Netmask + mask: IPv4Netmask class V6NetworkParams(BaseSettings): @@ -73,7 +73,7 @@ class V6NetworkParams(BaseSettings): containers: list[ipaddress.IPv6Network] networks: list[ipaddress.IPv6Network] - mask: IPV6Netmask + mask: IPv6Netmask class ServiceNetworkParams(BaseSettings): @@ -98,6 +98,7 @@ class IPAMParams(BaseSettings): GEANT_IP: ServiceNetworkParams SI: ServiceNetworkParams LT_IAS: ServiceNetworkParams + LAN_SWITCH_INTERCONNECT: ServiceNetworkParams class MonitoringSNMPV2Params(BaseSettings): diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index 852e152f21f00605e55b2283ad1ca06c6f37e8c3..776d46298a1e360490b0e97ef1f68822dbc1e154 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -2,6 +2,7 @@ import random import re +from ipaddress import IPv4Network from typing import TYPE_CHECKING from uuid import UUID @@ -18,7 +19,7 @@ from gso.services.partners import get_all_partners from gso.services.subscriptions import is_virtual_circuit_id_available from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity -from gso.utils.types.ip_address import IPv4AddressType +from gso.utils.types.ip_address import IPv4AddressType, IPv4NetworkType from gso.utils.types.virtual_identifiers import VC_ID if TYPE_CHECKING: @@ -126,6 +127,17 @@ def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str: return f"{hostname}.{site_name.lower()}.{country_code.lower()}{oss.IPAM.LO.domain_name}" +def generate_lan_switch_interconnect_subnet(site_internal_id: int) -> IPv4NetworkType: + """Generate an IPv4 network in which a :term:`LAN` Switch Interconnect resides, given a Site internal ID.""" + ipam_oss = settings.load_oss_params().IPAM.LAN_SWITCH_INTERCONNECT + + result = str(ipam_oss.V4.containers[0]).split(".")[:2] # Take the first two octets from the IPv4 network. + result.append(str(site_internal_id)) # Append the side ID as the third octet. + result.append(f"0/{ipam_oss.V4.mask}") # Append the fourth octet, together with the netmask. + + return IPv4Network(".".join(result)) + + def generate_inventory_for_routers( router_role: RouterRole, exclude_routers: list[str] | None = None, diff --git a/gso/utils/types/ip_address.py b/gso/utils/types/ip_address.py index 3b91167fe978a863a133abe75c68b275a1b028fb..83710c3ce4bd07d8591fe95eb9a5e373513d66dc 100644 --- a/gso/utils/types/ip_address.py +++ b/gso/utils/types/ip_address.py @@ -4,7 +4,6 @@ import ipaddress from typing import Annotated, Any from pydantic import AfterValidator, Field, PlainSerializer -from pydantic_forms.types import strEnum from typing_extensions import Doc @@ -40,8 +39,8 @@ IPv6AddressType = Annotated[ipaddress.IPv6Address, PlainSerializer(_str, return_ IPv6NetworkType = Annotated[ipaddress.IPv6Network, PlainSerializer(_str, return_type=str, when_used="always")] IPAddress = Annotated[str, AfterValidator(validate_ipv4_or_ipv6)] IPNetwork = Annotated[str, AfterValidator(validate_ipv4_or_ipv6_network)] -IPV4Netmask = Annotated[int, Field(ge=0, le=32), Doc("A valid netmask for an IPv4 network or address.")] -IPV6Netmask = Annotated[int, Field(ge=0, le=128), Doc("A valid netmask for an IPv6 network or address.")] +IPv4Netmask = Annotated[int, Field(ge=0, le=32), Doc("A valid netmask for an IPv4 network or address.")] +IPv6Netmask = Annotated[int, Field(ge=0, le=128), Doc("A valid netmask for an IPv6 network or address.")] PortNumber = Annotated[ int, Field( @@ -53,10 +52,3 @@ PortNumber = Annotated[ "and can therefore not be selected for permanent allocation." ), ] - - -class AddressSpace(strEnum): - """Types of address space. Can be private or public.""" - - PRIVATE = "PRIVATE" - PUBLIC = "PUBLIC" diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index 962cc5d01ec50c76ef03f82ee0d7b5fdc6c7353c..909da8b2852cdb7f385d8f1cc4482a89c930a487 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -505,7 +505,7 @@ def register_dns_records(subscription: IptrunkInactive) -> State: ipv4_addr = subscription.iptrunk.iptrunk_ipv4_network[index] ipv6_addr = subscription.iptrunk.iptrunk_ipv6_network[index + 1] - infoblox.create_host_by_ip(fqdn, ipv4_addr, ipv6_addr, "TRUNK", str(subscription.subscription_id)) + infoblox.create_host_by_ip(fqdn, "TRUNK", str(subscription.subscription_id), ipv4_addr, ipv6_addr) return {"subscription": subscription} diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 7e7f32b45877333447d69453cad37c55da86b12e..50dd28232b2c09cf542f7af94fc8138a122d031b 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -740,7 +740,7 @@ def update_ipam(subscription: Iptrunk, replace_index: int, new_node: Router, new # And in with the new new_fqdn = f"{new_lag_interface}-0.{new_node.router.router_fqdn}" comment = str(subscription.subscription_id) - infoblox.create_host_by_ip(new_fqdn, v4_addr, v6_addr, "TRUNK", comment) + infoblox.create_host_by_ip(new_fqdn, "TRUNK", comment, v4_addr, v6_addr) return {"subscription": subscription} diff --git a/gso/workflows/l3_core_service/create_imported_l3_core_service.py b/gso/workflows/l3_core_service/create_imported_l3_core_service.py index 7280a225e4977aec5cee0245b9aac94b507bf1c2..a4948f7dc378668b077eb1e72be0b0b8452e8dfc 100644 --- a/gso/workflows/l3_core_service/create_imported_l3_core_service.py +++ b/gso/workflows/l3_core_service/create_imported_l3_core_service.py @@ -22,7 +22,7 @@ from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import SBPType from gso.utils.types.geant_ids import IMPORTED_GS_ID -from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask from gso.utils.types.virtual_identifiers import VLAN_ID @@ -57,9 +57,9 @@ def initial_input_form_generator() -> FormGenerator: vlan_id: VLAN_ID custom_firewall_filters: bool = False ipv4_address: IPv4AddressType - ipv4_mask: IPV4Netmask + ipv4_mask: IPv4Netmask ipv6_address: IPv6AddressType - ipv6_mask: IPV6Netmask + ipv6_mask: IPv6Netmask rtbh_enabled: bool = True is_multi_hop: bool = True bgp_peers: list[BaseBGPPeer] diff --git a/gso/workflows/l3_core_service/create_l3_core_service.py b/gso/workflows/l3_core_service/create_l3_core_service.py index cd42c7613a391213ad9908797ef8a3dedb7dc200..3cf358d2316173bcd3d6c8dadc83bbf6a2857c44 100644 --- a/gso/workflows/l3_core_service/create_l3_core_service.py +++ b/gso/workflows/l3_core_service/create_l3_core_service.py @@ -26,7 +26,7 @@ from gso.utils.helpers import ( partner_choice, ) from gso.utils.shared_enums import APType, SBPType -from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask from gso.utils.types.tt_number import TTNumber from gso.utils.types.virtual_identifiers import VLAN_ID from gso.utils.workflow_steps import start_moodi, stop_moodi @@ -120,12 +120,12 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: divider: Divider = Field(None, exclude=True) v4_label: Label = Field("IPV4 SBP interface params", exclude=True) ipv4_address: IPv4AddressType - ipv4_mask: IPV4Netmask + ipv4_mask: IPv4Netmask v4_bfd_settings: BFDSettingsForm divider2: Divider = Field(None, exclude=True) v6_label: Label = Field("IPV6 SBP interface params", exclude=True) ipv6_address: IPv6AddressType - ipv6_mask: IPV6Netmask + ipv6_mask: IPv6Netmask v6_bfd_settings: BFDSettingsForm divider3: Divider = Field(None, exclude=True) v4_bgp_peer: IPv4BGPPeer diff --git a/gso/workflows/l3_core_service/modify_l3_core_service.py b/gso/workflows/l3_core_service/modify_l3_core_service.py index 24b408ab8a11144f3bdccd4f61d1f12b5dbfe5f7..af3218c54f88d082d87fa89698e6ec833485dcf7 100644 --- a/gso/workflows/l3_core_service/modify_l3_core_service.py +++ b/gso/workflows/l3_core_service/modify_l3_core_service.py @@ -21,7 +21,7 @@ from gso.products.product_types.edge_port import EdgePort from gso.products.product_types.l3_core_service import L3CoreService from gso.utils.helpers import active_edge_port_selector from gso.utils.shared_enums import APType, SBPType -from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.ip_address import IPv4AddressType, IPv4Netmask, IPv6AddressType, IPv6Netmask from gso.utils.types.virtual_identifiers import VLAN_ID @@ -158,9 +158,9 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: # 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_sbp.vlan_id # type: ignore[assignment] ipv4_address: IPv4AddressType = current_sbp.ipv4_address # type: ignore[assignment] - ipv4_mask: IPV4Netmask = current_sbp.ipv4_mask # type: ignore[assignment] + ipv4_mask: IPv4Netmask = current_sbp.ipv4_mask # type: ignore[assignment] ipv6_address: IPv6AddressType = current_sbp.ipv6_address # type: ignore[assignment] - ipv6_mask: IPV6Netmask = current_sbp.ipv6_mask # type: ignore[assignment] + ipv6_mask: IPv6Netmask = current_sbp.ipv6_mask # type: ignore[assignment] custom_firewall_filters: bool = current_sbp.custom_firewall_filters v4_bfd_settings: BFDInputModel = BFDInputModel( bfd_enabled=current_sbp.v4_bfd_settings.bfd_enabled, diff --git a/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py index ae26125f37beb8b876a1edbc691e7c37f9787fed..6d6632b2b10c0b1e33d49d1fd7d7e387670ed13a 100644 --- a/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/create_imported_lan_switch_interconnect.py @@ -21,14 +21,11 @@ from gso.products.product_types.router import Router from gso.products.product_types.switch import Switch from gso.services.partners import get_partner_by_name from gso.services.subscriptions import get_product_id_by_name -from gso.utils.types.ip_address import AddressSpace, IPv4NetworkType def _initial_input_form_generator() -> FormGenerator: class ImportLanSwitchInterconnect(FormPage): lan_switch_interconnect_description: str - lan_switch_interconnect_ip_network: IPv4NetworkType | None - address_space: AddressSpace minimum_links: int router_side: LanSwitchInterconnectRouterSideImportModel switch_side: LanSwitchInterconnectSwitchSideImportModel @@ -51,16 +48,12 @@ def create_subscription(partner: str) -> State: def initialize_subscription( subscription: ImportedLanSwitchInterconnectInactive, lan_switch_interconnect_description: str, - lan_switch_interconnect_ip_network: IPv4NetworkType | None, - address_space: AddressSpace, minimum_links: int, router_side: dict, switch_side: dict, ) -> State: """Initialize the subscription using input data.""" subscription.lan_switch_interconnect.lan_switch_interconnect_description = lan_switch_interconnect_description - subscription.lan_switch_interconnect.lan_switch_interconnect_ip_network = lan_switch_interconnect_ip_network - subscription.lan_switch_interconnect.address_space = address_space subscription.lan_switch_interconnect.minimum_links = minimum_links router_block = Router.from_subscription(router_side.pop("node")).router diff --git a/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py index eeec84e17d08e53cf3bf9fcf3531f38314772c11..4f92cc6d4e8ed75ec22a1b2873a65b45f8f5ac11 100644 --- a/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/create_lan_switch_interconnect.py @@ -1,5 +1,6 @@ """A creation workflow for creating a new interconnect between a switch and a router.""" +from ipaddress import IPv4Address from typing import Annotated from uuid import uuid4 @@ -7,6 +8,7 @@ from annotated_types import Len from orchestrator.forms import FormPage from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr +from orchestrator.utils.errors import ProcessFailureError from orchestrator.workflow import StepList, begin, done, step, workflow from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.utils import wrap_create_initial_input_form @@ -19,12 +21,15 @@ from gso.products.product_blocks.lan_switch_interconnect import ( from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnectInactive from gso.products.product_types.router import Router from gso.products.product_types.switch import Switch +from gso.services.infoblox import create_host_by_ip, create_v4_network_by_ip from gso.services.partners import get_partner_by_name +from gso.settings import load_oss_params from gso.utils.helpers import ( active_router_selector, active_switch_selector, available_interfaces_choices, available_lags_choices, + generate_lan_switch_interconnect_subnet, ) from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import ( @@ -35,7 +40,6 @@ from gso.utils.types.interfaces import ( PhysicalPortCapacity, validate_interface_names_are_unique, ) -from gso.utils.types.ip_address import AddressSpace from gso.utils.types.tt_number import TTNumber from gso.workflows.shared import create_summary_form @@ -48,7 +52,6 @@ def _initial_input_form(product_name: str) -> FormGenerator: partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type] router_side: active_router_selector() # type: ignore[valid-type] switch_side: active_switch_selector() # type: ignore[valid-type] - address_space: AddressSpace description: str minimum_link_count: int divider: Divider @@ -107,7 +110,6 @@ def _initial_input_form(product_name: str) -> FormGenerator: "partner", "router_side", "switch_side", - "address_space", "description", "minimum_link_count", "vlan_id", @@ -133,7 +135,6 @@ def create_subscription(product: UUIDstr, partner: str) -> State: def initialize_subscription( subscription: LanSwitchInterconnectInactive, description: str, - address_space: AddressSpace, minimum_link_count: int, router_side: UUIDstr, router_side_iface: JuniperPhyInterface, @@ -144,7 +145,6 @@ def initialize_subscription( ) -> State: """Update the product model with all input from the operator.""" subscription.lan_switch_interconnect.lan_switch_interconnect_description = description - subscription.lan_switch_interconnect.address_space = address_space subscription.lan_switch_interconnect.minimum_links = minimum_link_count subscription.lan_switch_interconnect.router_side.node = Router.from_subscription(router_side).router subscription.lan_switch_interconnect.router_side.ae_iface = router_side_iface @@ -162,6 +162,47 @@ def initialize_subscription( return {"subscription": subscription} +@step("Register network in IPAM") +def register_dns_records_network(subscription: LanSwitchInterconnectInactive) -> State: + """Add :term:`DNS` records in :term:`IPAM`.""" + router_site = subscription.lan_switch_interconnect.router_side.node.router_site + if not router_site or not router_site.site_internal_id: + msg = "Site internal ID not set. Cannot continue." + raise ProcessFailureError(msg, details=router_site) + + new_network = generate_lan_switch_interconnect_subnet(router_site.site_internal_id) + ipam_oss_params = load_oss_params().IPAM.LAN_SWITCH_INTERCONNECT + create_v4_network_by_ip( + ipam_oss_params.dns_view, ipam_oss_params.network_view, new_network, str(subscription.subscription_id) + ) + + return {"ipam_registrations": {"network": new_network}} + + +@step("Register devices in IPAM") +def register_dns_records_devices( + subscription: LanSwitchInterconnectInactive, subscription_id: UUIDstr, ipam_registrations: dict[str, str] +) -> State: + """Register :term:`DNS` records for both switch and router side in :term:`IPAM`.""" + switch_hostname = subscription.lan_switch_interconnect.switch_side.switch.fqdn + router_hostname = ( + f"{subscription.lan_switch_interconnect.router_side.ae_iface}." + f"{subscription.lan_switch_interconnect.router_side.node.router_fqdn}" + ) + if not (switch_hostname and router_hostname): + msg = "Missing switch or router hostname, cannot continue." + raise ProcessFailureError(msg, details=subscription.lan_switch_interconnect) + + ip_network_prefix = ipam_registrations["network"].split(".")[:3] # Take the first three octets of the network. + switch_side_ip = IPv4Address(".".join([*ip_network_prefix, "10"])) # Add .10 as the fourth octet of the switch. + router_side_ip = IPv4Address(".".join([*ip_network_prefix, "1"])) # Add .1 as the fourth octet of the router. + + create_host_by_ip(switch_hostname, "LAN_SWITCH_INTERCONNECT", subscription_id, switch_side_ip) + create_host_by_ip(router_hostname, "LAN_SWITCH_INTERCONNECT", subscription_id, router_side_ip) + + return {"ipam_registrations": {switch_hostname: switch_side_ip, router_hostname: router_side_ip}} + + @workflow( "Create LAN Switch Interconnect", initial_input_form=wrap_create_initial_input_form(_initial_input_form), @@ -174,6 +215,8 @@ def create_lan_switch_interconnect() -> StepList: >> create_subscription >> store_process_subscription(Target.CREATE) >> initialize_subscription + >> register_dns_records_network + >> register_dns_records_devices >> set_status(SubscriptionLifecycle.ACTIVE) >> resync >> done diff --git a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py index ac9aef3f6d75d868555f8b23d6a9ea3c03b1a71f..8b9549c03bb5436b1b4d6e911171d1ce1816c3ce 100644 --- a/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/terminate_lan_switch_interconnect.py @@ -4,13 +4,15 @@ from orchestrator import begin, workflow from orchestrator.forms import FormPage from orchestrator.targets import Target from orchestrator.types import SubscriptionLifecycle, UUIDstr -from orchestrator.workflow import StepList, done +from orchestrator.workflow import StepList, done, step from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync from orchestrator.workflows.utils import wrap_modify_initial_input_form from pydantic_forms.types import FormGenerator from pydantic_forms.validators import Label from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect +from gso.services.infoblox import delete_host_by_fqdn, delete_network +from gso.utils.helpers import generate_lan_switch_interconnect_subnet from gso.utils.types.tt_number import TTNumber @@ -31,6 +33,25 @@ def _input_form_generator(subscription_id: UUIDstr) -> FormGenerator: return {"subscription": lan_switch_interconnect} +@step("Release IPAM resources") +def clean_up_ipam(subscription: LanSwitchInterconnect) -> None: + """Clear up :term:`IPAM` resources in :term:`DNS`. + + First, remove :term:`DNS` records for both the router and switch side of the interconnect. + Second, remove the registered network for this interconnect. + """ + delete_host_by_fqdn(subscription.lan_switch_interconnect.switch_side.switch.fqdn) + delete_host_by_fqdn( + f"{subscription.lan_switch_interconnect.router_side.ae_iface}." + f"{subscription.lan_switch_interconnect.router_side.node.router_fqdn}" + ) + delete_network( + generate_lan_switch_interconnect_subnet( + subscription.lan_switch_interconnect.router_side.node.router_site.site_internal_id + ) + ) + + @workflow( "Terminate LAN Switch Interconnect", initial_input_form=wrap_modify_initial_input_form(_input_form_generator), @@ -42,6 +63,7 @@ def terminate_lan_switch_interconnect() -> StepList: begin >> store_process_subscription(Target.TERMINATE) >> unsync + >> clean_up_ipam >> set_status(SubscriptionLifecycle.TERMINATED) >> resync >> done diff --git a/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py b/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py index 9e6cfdd36d5d58c54e7b5319ed129932b068093c..fbb6886d66eb36971d046a41421b20bf62382b8a 100644 --- a/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py +++ b/gso/workflows/lan_switch_interconnect/validate_lan_switch_interconnect.py @@ -3,11 +3,39 @@ from typing import Any from orchestrator.targets import Target +from orchestrator.utils.errors import ProcessFailureError from orchestrator.workflow import StepList, begin, done, 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_types.lan_switch_interconnect import LanSwitchInterconnect +from gso.services.infoblox import find_host_by_fqdn, find_network_by_cidr from gso.services.lso_client import LSOState, anonymous_lso_interaction +from gso.utils.helpers import generate_lan_switch_interconnect_subnet + + +@step("Validate IPAM configuration") +def validate_ipam(subscription: LanSwitchInterconnect) -> None: + """Validate that :term:`DNS` records are registered in :term:`IPAM` correctly.""" + switch_fqdn = subscription.lan_switch_interconnect.switch_side.switch.fqdn + router_fqdn = ( + f"{subscription.lan_switch_interconnect.router_side.ae_iface}." + f"{subscription.lan_switch_interconnect.router_side.node.router_fqdn}" + ) + + for fqdn in [switch_fqdn, router_fqdn]: + host_record = find_host_by_fqdn(fqdn) + if not host_record or str(subscription.subscription_id) not in host_record.comment: + msg = "DNS record is incorrectly configured in IPAM, please investigate this manually!" + raise ProcessFailureError(msg) + + lan_interconnect_network = generate_lan_switch_interconnect_subnet( + subscription.lan_switch_interconnect.router_side.node.router_site.site_internal_id + ) + network_record = find_network_by_cidr(lan_interconnect_network) + if not network_record or str(subscription.subscription_id) not in network_record.comment: + msg = "LAN Switch Interconnect network is incorrectly configured in IPAM, please investigate this manually!" + raise ProcessFailureError(msg) @step("Check config for drift") @@ -15,7 +43,14 @@ def validate_config(subscription: dict[str, Any]) -> LSOState: """Workflow step for running a playbook that checks whether config has drifted.""" return { "playbook_name": "lan_switch_interconnect.yaml", - "inventory": {"all": {"hosts": {None: None}}}, + "inventory": { + "all": { + "hosts": { + subscription["lan_switch_interconnect"]["switch_side"]["switch"]["fqdn"]: None, + subscription["lan_switch_interconnect"]["router_side"]["node"]["router_fqdn"]: None, + } + } + }, "extra_vars": { "subscription_json": subscription, "verb": "deploy", @@ -34,6 +69,7 @@ def validate_lan_switch_interconnect() -> StepList: begin >> store_process_subscription(Target.SYSTEM) >> unsync + >> validate_ipam >> anonymous_lso_interaction(validate_config) >> resync >> done diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 571e64b6a4444f319c4ab8df73179894925b10a9..3a915979ea41095af32ef17470879418a5aa6a72 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -30,7 +30,6 @@ from gso.products.product_types.site import Site from gso.utils.helpers import generate_unique_vc_id, iso_from_ipv4 from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity -from gso.utils.types.ip_address import AddressSpace ############## @@ -230,8 +229,6 @@ def lan_switch_interconnect_data(temp_file, faker, switch_subscription_factory, def _lan_switch_interconnect_data(**kwargs): lan_switch_interconnect_data = { "lan_switch_interconnect_description": faker.sentence(), - "lan_switch_interconnect_ip_network": str(faker.ipv4_network()), - "address_space": AddressSpace.PUBLIC, "minimum_links": 1, "router_side": { "node": router_subscription_factory(), @@ -240,7 +237,6 @@ def lan_switch_interconnect_data(temp_file, faker, switch_subscription_factory, {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(2) ], - "ipv4_address": faker.ipv4(), }, "switch_side": { "switch": switch_subscription_factory(), @@ -249,7 +245,6 @@ def lan_switch_interconnect_data(temp_file, faker, switch_subscription_factory, {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(2) ], - "ipv4_address": faker.ipv4(), }, } lan_switch_interconnect_data.update(**kwargs) diff --git a/test/fixtures/lan_switch_interconnect_fixtures.py b/test/fixtures/lan_switch_interconnect_fixtures.py index fe0cb87f304b7eb04991bedcb5ad893d743b1483..0ea52e20252045f436172633036c6eb1435c68e1 100644 --- a/test/fixtures/lan_switch_interconnect_fixtures.py +++ b/test/fixtures/lan_switch_interconnect_fixtures.py @@ -18,7 +18,6 @@ from gso.products.product_types.lan_switch_interconnect import ( from gso.products.product_types.router import Router from gso.products.product_types.switch import Switch from gso.services.subscriptions import get_product_id_by_name -from gso.utils.types.ip_address import AddressSpace, IPv4AddressType, IPv4NetworkType @pytest.fixture() @@ -31,17 +30,13 @@ def lan_switch_interconnect_subscription_factory( status: SubscriptionLifecycle | None = None, start_date: str | None = "2024-01-01T10:20:30+01:02", lan_switch_interconnect_description: str | None = None, - lan_switch_interconnect_ip_network: IPv4NetworkType | None = None, - address_space: AddressSpace | None = None, minimum_links: int | None = None, router_side_node: UUIDstr | None = None, router_side_ae_iface: str | None = None, router_side_ae_members: list[dict[str, str]] | None = None, - router_side_ipv4_address: IPv4AddressType | None = None, switch_side_switch: UUIDstr | None = None, switch_side_ae_iface: str | None = None, switch_side_ae_members: list[dict[str, str]] | None = None, - switch_side_ipv4_address: IPv4AddressType | None = None, *, is_imported: bool | None = True, ) -> UUIDstr: @@ -70,24 +65,18 @@ def lan_switch_interconnect_subscription_factory( subscription.lan_switch_interconnect.lan_switch_interconnect_description = ( lan_switch_interconnect_description or faker.sentence() ) - subscription.lan_switch_interconnect.lan_switch_interconnect_ip_network = ( - lan_switch_interconnect_ip_network or faker.ipv4_network() - ) - subscription.lan_switch_interconnect.address_space = address_space or AddressSpace.PRIVATE subscription.lan_switch_interconnect.minimum_links = minimum_links or 1 subscription.lan_switch_interconnect.router_side = LanSwitchInterconnectRouterSideBlockInactive.new( uuid4(), node=router_side_node or Router.from_subscription(router_subscription_factory()).router, ae_iface=router_side_ae_iface or faker.network_interface(), ae_members=router_side_ae_members, - ipv4_address=router_side_ipv4_address or faker.ipv4(), ) subscription.lan_switch_interconnect.switch_side = LanSwitchInterconnectSwitchSideBlockInactive.new( uuid4(), switch=switch_side_switch or Switch.from_subscription(switch_subscription_factory()).switch, ae_iface=switch_side_ae_iface or faker.network_interface(), ae_members=switch_side_ae_members, - ipv4_address=switch_side_ipv4_address or faker.ipv4(), ) subscription = SubscriptionModel.from_other_lifecycle(subscription, SubscriptionLifecycle.ACTIVE) diff --git a/test/fixtures/site_fixtures.py b/test/fixtures/site_fixtures.py index 6fa904183f0d8effe142f2c199f180494cbfd7c2..d14025e9b93ae44a471e66c24711b85ecba2a576 100644 --- a/test/fixtures/site_fixtures.py +++ b/test/fixtures/site_fixtures.py @@ -51,7 +51,7 @@ def site_subscription_factory(faker, geant_partner): site_subscription.site.site_latitude = site_latitude or str(faker.latitude()) site_subscription.site.site_longitude = site_longitude or str(faker.longitude()) site_subscription.site.site_bgp_community_id = site_bgp_community_id or faker.pyint() - site_subscription.site.site_internal_id = site_internal_id or faker.pyint() + site_subscription.site.site_internal_id = site_internal_id or faker.pyint(max_value=254) site_subscription.site.site_tier = site_tier or SiteTier.TIER1 site_subscription.site.site_ts_address = site_ts_address or faker.ipv4() diff --git a/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py index 69349ae02c7d58dbbdc670aad70b855edb5f7a11..74e6e879e3d2ea4b9de54fa4f4f764237586e56b 100644 --- a/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py +++ b/test/workflows/lan_switch_interconnect/test_create_imported_lan_switch_interconnect.py @@ -3,7 +3,6 @@ from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName from gso.products.product_types.lan_switch_interconnect import ImportedLanSwitchInterconnect -from gso.utils.types.ip_address import AddressSpace from test.workflows import ( assert_complete, extract_state, @@ -15,20 +14,16 @@ from test.workflows import ( def workflow_input_data(faker, router_subscription_factory, switch_subscription_factory): return { "lan_switch_interconnect_description": faker.sentence(), - "lan_switch_interconnect_ip_network": faker.ipv4_network(), - "address_space": AddressSpace.PUBLIC, "minimum_links": 1, "router_side": { "node": router_subscription_factory(), "ae_iface": faker.nokia_lag_interface_name(), "ae_members": faker.link_members_nokia(), - "ipv4_address": faker.ipv4(), }, "switch_side": { "switch": switch_subscription_factory(), "ae_iface": faker.juniper_ae_interface_name(), "ae_members": faker.link_members_juniper(), - "ipv4_address": faker.ipv4(), }, } diff --git a/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py index 2c6657d260a88b8871dcc843a8fb1e7a636685f4..9d69017f639c31964f2888720cee207561517bc0 100644 --- a/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py +++ b/test/workflows/lan_switch_interconnect/test_create_lan_switch_interconnect.py @@ -6,7 +6,6 @@ from orchestrator.types import SubscriptionLifecycle from gso.products import ProductName from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect from gso.services.subscriptions import get_product_id_by_name -from gso.utils.types.ip_address import AddressSpace from test.services.conftest import MockedNetboxClient from test.workflows import assert_complete, extract_state, run_workflow @@ -28,47 +27,45 @@ def _netbox_client_mock(): @pytest.fixture() def input_form_data(faker, router_subscription_factory, switch_subscription_factory): - def _generate_form_data(address_space: AddressSpace): - return [ - { - "product": get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT), - }, - { - "tt_number": faker.tt_number(), - "router_side": router_subscription_factory(), - "switch_side": switch_subscription_factory(), - "address_space": address_space, - "description": faker.sentence(), - "minimum_link_count": 2, - "vlan_id": 111, # VLAN ID for new interconnections is always 111 - }, - { - "router_side_iface": "lag-1", - "router_side_ae_members": faker.link_members_nokia()[:2], - }, - { - "switch_side_iface": faker.network_interface(), - "switch_side_ae_members": faker.link_members_juniper()[:2], - }, - {}, - ] + return [ + { + "product": get_product_id_by_name(ProductName.LAN_SWITCH_INTERCONNECT), + }, + { + "tt_number": faker.tt_number(), + "router_side": router_subscription_factory(), + "switch_side": switch_subscription_factory(), + "description": faker.sentence(), + "minimum_link_count": 2, + "vlan_id": 111, # VLAN ID for new interconnections is always 111 + }, + { + "router_side_iface": "lag-1", + "router_side_ae_members": faker.link_members_nokia()[:2], + }, + { + "switch_side_iface": faker.network_interface(), + "switch_side_ae_members": faker.link_members_juniper()[:2], + }, + {}, + ] - return _generate_form_data - -@pytest.mark.parametrize("address_space", [AddressSpace.PRIVATE, AddressSpace.PUBLIC]) @pytest.mark.workflow() +@patch("gso.services.infoblox.create_v4_network_by_ip") +@patch("gso.services.infoblox.create_host_by_ip") def test_create_lan_switch_interconnect_success( - address_space, + mock_create_host, + mock_create_v4_network, input_form_data, _netbox_client_mock, # noqa: PT019 ): - initial_data = input_form_data(address_space) - - result, _, _ = run_workflow("create_lan_switch_interconnect", initial_data) + result, _, _ = run_workflow("create_lan_switch_interconnect", input_form_data) assert_complete(result) state = extract_state(result) subscription_id = state["subscription_id"] subscription = LanSwitchInterconnect.from_subscription(subscription_id) assert subscription.status == SubscriptionLifecycle.ACTIVE + assert mock_create_v4_network.call_count == 1 + assert mock_create_host.call_count == 2 diff --git a/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py b/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py index 6d5ae5383dd4559662f293535a3b3253d7123033..9b69c628e440e90c76460656addf7c447abe4793 100644 --- a/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py +++ b/test/workflows/lan_switch_interconnect/test_terminate_lan_switch_interconnect.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect @@ -5,7 +7,11 @@ from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -def test_terminate_lan_switch_interconnect(lan_switch_interconnect_subscription_factory, faker): +@patch("gso.services.infoblox.find_host_by_fqdn") +@patch("gso.services.infoblox.delete_network") +def test_terminate_lan_switch_interconnect( + mock_delete_network, mock_find_host, lan_switch_interconnect_subscription_factory, faker +): subscription_id = lan_switch_interconnect_subscription_factory() initial_lan_switch_interconnect_data = [{"subscription_id": subscription_id}, {"tt_number": faker.tt_number()}] result, _, _ = run_workflow("terminate_lan_switch_interconnect", initial_lan_switch_interconnect_data) @@ -15,3 +21,5 @@ def test_terminate_lan_switch_interconnect(lan_switch_interconnect_subscription_ subscription_id = state["subscription_id"] subscription = LanSwitchInterconnect.from_subscription(subscription_id) assert subscription.status == "terminated" + assert mock_find_host.call_count == 2 + assert mock_delete_network.call_count == 1