From 39b633135082d2e1fb608dd0d27ca1af6826c567 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Tue, 23 Apr 2024 09:29:48 +0200 Subject: [PATCH] Add logic for assigning subparcels --- .../api/builders/feature_profiles/service.py | 21 ++--- catalystwan/api/feature_profile_api.py | 20 +++-- .../feature_profile/sdwan/service.py | 12 ++- .../configuration/feature_profile/common.py | 2 +- .../feature_profile/sdwan/service/__init__.py | 8 ++ .../sdwan/service/lan/ethernet.py | 2 +- .../feature_profile/sdwan/service/lan/gre.py | 2 +- .../sdwan/service/lan/ipsec.py | 8 +- .../feature_profile/sdwan/service/lan/svi.py | 4 +- .../feature_profile/sdwan/service/lan/vpn.py | 2 +- .../converters/feature_template/gre.py | 1 - .../converters/feature_template/ipsec.py | 34 ++++++-- .../converters/feature_template/multicast.py | 1 - .../feature_template/parcel_factory.py | 4 +- .../converters/feature_template/svi.py | 1 + .../converters/feature_template/vpn.py | 38 +++++---- .../feature_template/wireless_lan.py | 2 +- .../creators/config_pusher.py | 4 +- .../creators/strategy/parcels.py | 82 +++++++++++-------- 19 files changed, 159 insertions(+), 89 deletions(-) diff --git a/catalystwan/api/builders/feature_profiles/service.py b/catalystwan/api/builders/feature_profiles/service.py index ebabaa5b..6888f6ad 100644 --- a/catalystwan/api/builders/feature_profiles/service.py +++ b/catalystwan/api/builders/feature_profiles/service.py @@ -20,6 +20,7 @@ LanVpnDhcpServerParcel, LanVpnParcel, ) +from catalystwan.models.configuration.feature_profile.sdwan.service.multicast import MulticastParcel if TYPE_CHECKING: from catalystwan.session import ManagerSession @@ -27,8 +28,8 @@ logger = logging.getLogger(__name__) IndependedParcels = Annotated[Union[AppqoeParcel, LanVpnDhcpServerParcel], Field(discriminator="type_")] -DependedInterfaceParcels = Annotated[ - Union[InterfaceGreParcel, InterfaceSviParcel, InterfaceEthernetParcel, InterfaceIpsecParcel], +DependedVpnSubparcels = Annotated[ + Union[InterfaceGreParcel, InterfaceSviParcel, InterfaceEthernetParcel, InterfaceIpsecParcel, MulticastParcel], Field(discriminator="type_"), ] @@ -51,7 +52,7 @@ def __init__(self, session: ManagerSession): self._endpoints = ServiceFeatureProfile(session) self._independent_items: List[IndependedParcels] = [] self._independent_items_vpns: Dict[UUID, LanVpnParcel] = {} - self._depended_items_on_vpns: Dict[UUID, List[DependedInterfaceParcels]] = defaultdict(list) + self._depended_items_on_vpns: Dict[UUID, List[DependedVpnSubparcels]] = defaultdict(list) def add_profile_name_and_description(self, feature_profile: FeatureProfileCreationPayload) -> None: """ @@ -93,9 +94,9 @@ def add_parcel_vpn(self, parcel: LanVpnParcel) -> UUID: self._independent_items_vpns[vpn_tag] = parcel return vpn_tag - def add_parcel_vpn_interface(self, vpn_tag: UUID, parcel: DependedInterfaceParcels) -> None: + def add_parcel_vpn_subparcel(self, vpn_tag: UUID, parcel: DependedVpnSubparcels) -> None: """ - Adds an interface parcel dependent on a VPN to the builder. + Adds an subparcel parcel dependent on a VPN to the builder. Args: vpn_tag (UUID): The UUID of the VPN. @@ -104,13 +105,13 @@ def add_parcel_vpn_interface(self, vpn_tag: UUID, parcel: DependedInterfaceParce Returns: None """ - logger.debug(f"Adding interface parcel {parcel.parcel_name} to VPN {vpn_tag}") + logger.debug(f"Adding subparcel parcel {parcel.parcel_name} to VPN {vpn_tag}") self._depended_items_on_vpns[vpn_tag].append(parcel) def build(self) -> UUID: """ Builds the feature profile by creating parcels for independent items, - VPNs, and interface parcels dependent on VPNs. + VPNs, and sub-parcels dependent on VPNs. Returns: Service feature profile UUID @@ -123,8 +124,8 @@ def build(self) -> UUID: for vpn_tag, vpn_parcel in self._independent_items_vpns.items(): vpn_uuid = self._api.create_parcel(profile_uuid, vpn_parcel).id - for interface_parcel in self._depended_items_on_vpns[vpn_tag]: - logger.debug(f"Creating interface parcel {interface_parcel.parcel_name} to VPN {vpn_tag}") - self._api.create_parcel(profile_uuid, interface_parcel, vpn_uuid) + for sub_parcel in self._depended_items_on_vpns[vpn_tag]: + logger.debug(f"Creating subparcel parcel {sub_parcel.parcel_name} to VPN {vpn_uuid}") + self._api.create_parcel(profile_uuid, sub_parcel, vpn_uuid) return profile_uuid diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index a9c7860f..152ecdf3 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, get_args, overload +from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, overload from uuid import UUID from pydantic import Json @@ -12,7 +12,8 @@ from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile from catalystwan.models.configuration.feature_profile.sdwan.other import AnyOtherParcel from catalystwan.models.configuration.feature_profile.sdwan.policy_object.security.url import URLParcel -from catalystwan.models.configuration.feature_profile.sdwan.service import AnyLanVpnInterfaceParcel, AnyServiceParcel +from catalystwan.models.configuration.feature_profile.sdwan.service import AnyServiceParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.multicast import MulticastParcel from catalystwan.typed_list import DataSequence if TYPE_CHECKING: @@ -27,6 +28,7 @@ FeatureProfileInfo, GetFeatureProfilesPayload, Parcel, + ParcelAssociationPayload, ParcelCreationResponse, ) from catalystwan.models.configuration.feature_profile.sdwan.policy_object import ( @@ -238,10 +240,16 @@ def create_parcel( """ Create Service Parcel for selected profile_id based on payload type """ - if type(payload) in get_args(AnyLanVpnInterfaceParcel)[0].__args__: - return self.endpoint.create_lan_vpn_interface_parcel( - profile_uuid, vpn_uuid, payload._get_parcel_type(), payload - ) + if vpn_uuid is not None: + if isinstance(payload, MulticastParcel): + response = self.endpoint.create_service_parcel(profile_uuid, payload._get_parcel_type(), payload) + return self.endpoint.associate_parcel_with_vpn( + profile_uuid, vpn_uuid, payload._get_parcel_type(), ParcelAssociationPayload(parcel_id=response.id) + ) + else: + return self.endpoint.create_lan_vpn_sub_parcel( + profile_uuid, vpn_uuid, payload._get_parcel_type(), payload + ) return self.endpoint.create_service_parcel(profile_uuid, payload._get_parcel_type(), payload) diff --git a/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py b/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py index d6b8db64..e600a6ea 100644 --- a/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py +++ b/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py @@ -10,6 +10,7 @@ FeatureProfileCreationResponse, FeatureProfileInfo, GetFeatureProfilesPayload, + ParcelAssociationPayload, ParcelCreationResponse, ) from catalystwan.models.configuration.feature_profile.sdwan.service import ( @@ -47,8 +48,15 @@ def create_service_parcel( ... @versions(supported_versions=(">=20.9"), raises=False) - @post("/v1/feature-profile/sdwan/service/{profile_uuid}/lan/vpn/{vpn_uuid}/interface/{parcel_type}") - def create_lan_vpn_interface_parcel( + @post("/v1/feature-profile/sdwan/service/{profile_uuid}/lan/vpn/{vpn_uuid}/{parcel_type}") + def create_lan_vpn_sub_parcel( self, profile_uuid: UUID, vpn_uuid: UUID, parcel_type: str, payload: AnyLanVpnInterfaceParcel ) -> ParcelCreationResponse: ... + + @versions(supported_versions=(">=20.9"), raises=False) + @post("/v1/feature-profile/sdwan/service/{profile_uuid}/lan/vpn/{vpn_uuid}/{parcel_type}") + def associate_parcel_with_vpn( + self, profile_uuid: UUID, vpn_uuid: UUID, parcel_type: str, payload: ParcelAssociationPayload + ) -> ParcelCreationResponse: + ... diff --git a/catalystwan/models/configuration/feature_profile/common.py b/catalystwan/models/configuration/feature_profile/common.py index 3bca973f..1bf0e148 100644 --- a/catalystwan/models/configuration/feature_profile/common.py +++ b/catalystwan/models/configuration/feature_profile/common.py @@ -157,7 +157,7 @@ class ParcelInfo(BaseModel, Generic[T]): class ParcelAssociationPayload(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - parcel_id: str = Field(alias="parcelId") + parcel_id: UUID = Field(serialization_alias="parcelId", validation_alias="parcelId") class Prefix(BaseModel): diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py index ec643867..68e68479 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py @@ -51,6 +51,14 @@ Field(discriminator="type_"), ] +AnyAssociatoryParcel = Annotated[ + Union[ + MulticastParcel, + # DHCP + ], + Field(discriminator="type_"), +] + AnyServiceParcel = Annotated[ Union[AnyTopLevelServiceParcel, AnyLanVpnInterfaceParcel], Field(discriminator="type_"), diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py index 463e06dc..1294e40d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py @@ -293,7 +293,7 @@ class AdvancedEthernetAttributes(BaseModel): class InterfaceEthernetParcel(_ParcelBase): - type_: Literal["ethernet"] = Field(default="ethernet", exclude=True) + type_: Literal["interface/ethernet"] = Field(default="interface/ethernet", exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") shutdown: Union[Global[bool], Variable, Default[bool]] = Field( diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py index bbfa03b8..e3f704c2 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py @@ -192,7 +192,7 @@ class AdvancedGre(BaseModel): class InterfaceGreParcel(_ParcelBase): - type_: Literal["gre"] = Field(default="gre", exclude=True) + type_: Literal["interface/gre"] = Field(default="interface/gre", exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") basic: BasicGre = Field(validation_alias=AliasPath("data", "basic")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py index 89e3b613..c02a11d9 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py @@ -30,7 +30,7 @@ class IpsecAddress(BaseModel): class InterfaceIpsecParcel(_ParcelBase): - type_: Literal["ipsec"] = Field(default="ipsec", exclude=True) + type_: Literal["interface/ipsec"] = Field(default="interface/ipsec", exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "ifName")) @@ -39,7 +39,7 @@ class InterfaceIpsecParcel(_ParcelBase): ) tunnel_mode: Optional[Union[Global[IpsecTunnelMode], Default[IpsecTunnelMode]]] = Field( validation_alias=AliasPath("data", "tunnelMode"), - default=Default[IpsecTunnelMode](value="ipv4"), + default=None, ) ipsec_description: Union[Global[str], Variable, Default[None]] = Field( default=Default[None](value=None), validation_alias=AliasPath("data", "description") @@ -65,8 +65,8 @@ class InterfaceIpsecParcel(_ParcelBase): tcp_mss_adjust: Union[Global[int], Variable, Default[None]] = Field( validation_alias=AliasPath("data", "tcpMssAdjust"), default=Default[None](value=None) ) - tcp_mss_adjust_v6: Union[Global[int], Variable, Default[None]] = Field( - validation_alias=AliasPath("data", "tcpMssAdjustV6"), default=Default[None](value=None) + tcp_mss_adjust_v6: Optional[Union[Global[int], Variable, Default[None]]] = Field( + validation_alias=AliasPath("data", "tcpMssAdjustV6"), default=None ) clear_dont_fragment: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( validation_alias=AliasPath("data", "clearDontFragment"), diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py index 75c33fe0..338931d2 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py @@ -144,7 +144,7 @@ class AclQos(BaseModel): class InterfaceSviParcel(_ParcelBase): - type_: Literal["svi"] = Field(default="svi", exclude=True) + type_: Literal["interface/svi"] = Field(default="interface/svi", exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") shutdown: Union[Global[bool], Variable, Default[bool]] = Field( @@ -152,7 +152,7 @@ class InterfaceSviParcel(_ParcelBase): ) interface_name: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "interfaceName")) svi_description: Optional[Union[Global[str], Variable, Default[None]]] = Field( - default=Default[bool](value=True), validation_alias=AliasPath("data", "description") + default=Default[None](value=None), validation_alias=AliasPath("data", "description") ) interface_mtu: Optional[Union[Global[int], Variable, Default[int]]] = Field( validation_alias=AliasPath("data", "ifMtu"), default=Default[int](value=1500) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py index d89670ae..47b37c14 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py @@ -445,7 +445,7 @@ class StaticNat(BaseModel): serialization_alias="sourceIp", validation_alias="sourceIp" ) translated_source_ip: Union[Variable, Global[str], Global[IPv4Address]] = Field( - serialization_alias="translatedSourceIp", validation_alias="translatedSourceIp" + serialization_alias="TranslatedSourceIp", validation_alias="TranslatedSourceIp" ) static_nat_direction: Union[Variable, Global[Direction]] = Field( serialization_alias="staticNatDirection", validation_alias="staticNatDirection" diff --git a/catalystwan/utils/config_migration/converters/feature_template/gre.py b/catalystwan/utils/config_migration/converters/feature_template/gre.py index 37b3fa7d..06c5aa0b 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/gre.py +++ b/catalystwan/utils/config_migration/converters/feature_template/gre.py @@ -44,7 +44,6 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> I Returns: InterfaceGreParcel: The created InterfaceGreParcel object. """ - print(template_values) basic_values, advanced_values = self.prepare_values(template_values) self.configure_dead_peer_detection(basic_values) self.configure_ike(basic_values) diff --git a/catalystwan/utils/config_migration/converters/feature_template/ipsec.py b/catalystwan/utils/config_migration/converters/feature_template/ipsec.py index 02a9cd21..8bd06bba 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ipsec.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ipsec.py @@ -1,14 +1,18 @@ from copy import deepcopy from ipaddress import IPv4Interface, IPv6Address -from catalystwan.api.configuration_groups.parcel import Default, as_global -from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import IkeGroup +from catalystwan.api.configuration_groups.parcel import Default, as_default, as_global, as_variable +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import IkeGroup, TunnelApplication from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import InterfaceIpsecParcel, IpsecAddress +from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException class InterfaceIpsecTemplateConverter: supported_template_types = ("cisco_vpn_interface_ipsec", "vpn-vedge-interface-ipsec") + # Default Values + pre_shared_secret = "{{vpn_if_pre_shared_secret}}" + delete_keys = ( "dead_peer_detection", "if_name", @@ -18,9 +22,11 @@ class InterfaceIpsecTemplateConverter: "multiplexing", "ipsec", "ipv6", + "ip", ) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceIpsecParcel: + print(template_values) values = deepcopy(template_values) self.configure_interface_name(values) self.configure_description(values) @@ -32,6 +38,8 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> I self.configure_ipv6_address(values) self.configure_address(values) self.configure_tracker(values) + self.configure_application(values) + self.configure_pre_shared_secret(values) self.cleanup_keys(values) return InterfaceIpsecParcel(parcel_name=name, parcel_description=description, **values) @@ -42,19 +50,22 @@ def configure_description(self, values: dict) -> None: values["ipsec_description"] = values.get("description", Default[None](value=None)) def configure_dead_peer_detection(self, values: dict) -> None: - values["dpd_interval"] = values.get("dead_peer_detection", {}).get("dpd_interval") - values["dpd_retries"] = values.get("dead_peer_detection", {}).get("dpd_retries") + values["dpd_interval"] = values.get("dead_peer_detection", {}).get("dpd_interval", as_default(10)) + values["dpd_retries"] = values.get("dead_peer_detection", {}).get("dpd_retries", as_default(3)) def configure_ipv6_address(self, values: dict) -> None: values["ipv6_address"] = values.get("ipv6", {}).get("address") def configure_address(self, values: dict) -> None: address = values.get("ip", {}).get("address", {}) - if address: - values["address"] = IpsecAddress( - address=as_global(str(address.network.network_address)), - mask=as_global(str(address.network.netmask)), + if not address: + raise CatalystwanConverterCantConvertException( + "Ipsec Address is required in UX2 parcel but in a Feature Template can be optional." ) + values["address"] = IpsecAddress( + address=as_global(str(address.value.network.network_address)), + mask=as_global(str(address.value.network.netmask)), + ) def configure_ike(self, values: dict) -> None: ike = values.get("ike", {}) @@ -91,6 +102,13 @@ def configure_tracker(self, values: dict) -> None: tracker = as_global("".join(tracker.value)) values["tracker"] = tracker + def configure_application(self, values: dict) -> None: + if application := values.get("application"): + values["application"] = as_global(application.value, TunnelApplication) + + def configure_pre_shared_secret(self, values: dict) -> None: + values["pre_shared_secret"] = values.get("pre_shared_secret", as_variable(self.pre_shared_secret)) + def cleanup_keys(self, values: dict) -> None: for key in self.delete_keys: values.pop(key, None) diff --git a/catalystwan/utils/config_migration/converters/feature_template/multicast.py b/catalystwan/utils/config_migration/converters/feature_template/multicast.py index 60dcf9f8..d7041aeb 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/multicast.py +++ b/catalystwan/utils/config_migration/converters/feature_template/multicast.py @@ -255,7 +255,6 @@ class IgmpToMulticastTemplateConverter: supported_template_types = ("cedge_igmp", "cisco_IGMP", "igmp") def create_parcel(self, name: str, description: str, template_values: dict) -> MulticastParcel: - print(template_values) values = self.prepare_values(template_values) return MulticastParcel( parcel_name=name, diff --git a/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py index 42b286c5..c20e204a 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py +++ b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py @@ -37,7 +37,7 @@ from .svi import InterfaceSviTemplateConverter from .thousandeyes import ThousandEyesTemplateConverter from .ucse import UcseTemplateConverter -from .vpn import LanVpnParcelTemplateConverter +from .vpn import VpnParcelsTemplateConverter logger = logging.getLogger(__name__) @@ -57,7 +57,7 @@ DhcpTemplateConverter, SNMPTemplateConverter, AppqoeTemplateConverter, - LanVpnParcelTemplateConverter, + VpnParcelsTemplateConverter, InterfaceGRETemplateConverter, InterfaceSviTemplateConverter, InterfaceEthernetTemplateConverter, diff --git a/catalystwan/utils/config_migration/converters/feature_template/svi.py b/catalystwan/utils/config_migration/converters/feature_template/svi.py index 8b15cbb1..604b734b 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/svi.py +++ b/catalystwan/utils/config_migration/converters/feature_template/svi.py @@ -42,6 +42,7 @@ class InterfaceSviTemplateConverter: def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceSviParcel: values = deepcopy(template_values) self.configure_interface_name(values) + self.configure_svi_description(values) self.configure_ipv4_address(values) self.configure_ipv6_address(values) self.configure_arp(values) diff --git a/catalystwan/utils/config_migration/converters/feature_template/vpn.py b/catalystwan/utils/config_migration/converters/feature_template/vpn.py index 9964c529..2f010e94 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/vpn.py +++ b/catalystwan/utils/config_migration/converters/feature_template/vpn.py @@ -67,7 +67,7 @@ class OmpMappingItem(BaseModel): ux2_field: Literal["omp_advertise_ipv4", "omp_advertise_ipv6"] -class LanVpnParcelTemplateConverter: +class VpnParcelsTemplateConverter: """ A class for converting template values into a LanVpnParcel object. """ @@ -154,7 +154,7 @@ class LanVpnParcelTemplateConverter: def create_parcel(self, name: str, description: str, template_values: dict) -> LanVpnParcel: """ - Creates a LanVpnParcel object based on the provided parameters. + Creates a parcel from VPN family object based on the provided parameters. Args: name (str): The name of the parcel. @@ -162,11 +162,25 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> L template_values (dict): A dictionary containing the template values. Returns: - LanVpnParcel: The created LanVpnParcel object. + VPN: The created VPN object. """ values = deepcopy(template_values) + vpn_id = self.get_vpn_id(values) + if vpn_id == 0: + return LanVpnParcel( + parcel_name=name, + parcel_description=f"{description} - This should be a VPN 0", + vpn_id=as_global(77), + ) + elif vpn_id == 512: + return LanVpnParcel( + parcel_name=name, + parcel_description=f"{description} - This should be a VPN 512", + vpn_id=as_global(77), + ) + + self.configure_vpn_id(values) self.configure_vpn_name(values) - self.configure_vpn_id(name, values) self.configure_natpool(values) self.configure_port_forwarding(values) self.configure_static_nat(values) @@ -197,19 +211,13 @@ def configure_vpn_name(self, values: dict) -> None: if vpn_name := values.get("name", None): values["vpn_name"] = vpn_name - def configure_vpn_id(self, name: str, values: dict) -> None: + def get_vpn_id(self, values: dict) -> int: + return int(values["vpn_id"].value) + + def configure_vpn_id(self, values: dict) -> None: if vpn_id := values.get("vpn_id"): vpn_id_value = int(vpn_id.value) - # VPN 0 contains all of a device's interfaces except for the management interface, - # and all interfaces are disabled. - # VPN 512 is RESERVED for OOB network management - if vpn_id_value in (0, 512): - logger.warning( - f"VPN ID {vpn_id_value} is reserved for system use. " f"VPN ID will be set to 1 for VPN {name}." - ) - values["vpn_id"] = as_global(1) - else: - values["vpn_id"] = as_global(vpn_id_value) + values["vpn_id"] = as_global(vpn_id_value) def configure_dns(self, values: dict) -> None: if dns := values.get("dns", []): diff --git a/catalystwan/utils/config_migration/converters/feature_template/wireless_lan.py b/catalystwan/utils/config_migration/converters/feature_template/wireless_lan.py index 86baa144..83430c2f 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wireless_lan.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wireless_lan.py @@ -74,7 +74,7 @@ def configure_me_ip_address(self, values: dict) -> None: values["me_ip_config"] = MeIpConfig(me_dynamic_ip_enabled=as_default(True)) else: values["me_ip_config"] = MeIpConfig( - me_dynamic_ip_enabled=as_default(False), + me_dynamic_ip_enabled=as_global(False), me_static_ip_config=MeStaticIpConfig( me_ipv4_address=address, netmask=netmask, diff --git a/catalystwan/utils/config_migration/creators/config_pusher.py b/catalystwan/utils/config_migration/creators/config_pusher.py index 5fc85f74..3e8279b4 100644 --- a/catalystwan/utils/config_migration/creators/config_pusher.py +++ b/catalystwan/utils/config_migration/creators/config_pusher.py @@ -80,7 +80,9 @@ def _create_feature_profile_and_parcels(self, feature_profiles_ids: List[UUID]) continue pusher = ParcelPusherFactory.get_pusher(self._session, profile_type) parcels = self._create_parcels_list(transformed_feature_profile) - created_profile_id = pusher.push(transformed_feature_profile.feature_profile, parcels) + created_profile_id = pusher.push( + transformed_feature_profile.feature_profile, parcels, self._config_map.parcel_map + ) config_group_profiles.append(ProfileId(id=created_profile_id)) self._config_rollback.add_feature_profile(created_profile_id, profile_type) return config_group_profiles diff --git a/catalystwan/utils/config_migration/creators/strategy/parcels.py b/catalystwan/utils/config_migration/creators/strategy/parcels.py index cc4fea73..a01080fa 100644 --- a/catalystwan/utils/config_migration/creators/strategy/parcels.py +++ b/catalystwan/utils/config_migration/creators/strategy/parcels.py @@ -1,12 +1,10 @@ -from typing import List +from collections import defaultdict +from typing import Dict, List, cast from uuid import UUID from catalystwan.models.configuration.config_migration import TransformedParcel from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload, ProfileType -from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ethernet import InterfaceEthernetParcel -from catalystwan.models.configuration.feature_profile.sdwan.service.lan.gre import InterfaceGreParcel -from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import InterfaceIpsecParcel -from catalystwan.models.configuration.feature_profile.sdwan.service.lan.svi import InterfaceSviParcel +from catalystwan.models.configuration.feature_profile.sdwan.service import AnyAssociatoryParcel from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import LanVpnParcel from catalystwan.session import ManagerSession @@ -19,7 +17,12 @@ class ParcelPusher: def __init__(self, session: ManagerSession, profile_type: ProfileType): self.builder = session.api.builders.feature_profiles.create_builder(profile_type) - def push(self, feature_profile: FeatureProfileCreationPayload, parcels: List[TransformedParcel]) -> UUID: + def push( + self, + feature_profile: FeatureProfileCreationPayload, + target_parcels: List[TransformedParcel], + all_parcels: Dict[UUID, TransformedParcel], + ) -> UUID: raise NotImplementedError @@ -29,9 +32,13 @@ class SimpleParcelPusher(ParcelPusher): Includes: Other and System feature profiles. """ - def push(self, feature_profile: FeatureProfileCreationPayload, parcels: List[TransformedParcel]) -> UUID: - # Parcels don't have references to other parcels, so we can create them directly - for transformed_parcel in parcels: + def push( + self, + feature_profile: FeatureProfileCreationPayload, + target_parcels: List[TransformedParcel], + all_parcels: Dict[UUID, TransformedParcel], + ) -> UUID: + for transformed_parcel in target_parcels: self.builder.add_parcel(transformed_parcel.parcel) # type: ignore self.builder.add_profile_name_and_description(feature_profile) return self.builder.build() @@ -42,29 +49,40 @@ class ServiceParcelPusher(ParcelPusher): Parcel pusher for service feature profiles. """ - def push(self, feature_profile: FeatureProfileCreationPayload, parcels: List[TransformedParcel]) -> UUID: - # Service feature profiles have references to other parcels, so we need to create them in order - self._move_vpn_parcel_to_first_position(parcels) - for transformed_parcel in parcels: - self._resolve_and_add_parcel(transformed_parcel) + def __init__(self, session: ManagerSession, profile_type: ProfileType): + super().__init__(session, profile_type) + self.subparcel_name_counter: Dict[str, int] = defaultdict(lambda: 0) + + def push( + self, + feature_profile: FeatureProfileCreationPayload, + target_parcels: List[TransformedParcel], + all_parcels: Dict[UUID, TransformedParcel], + ) -> UUID: + for transformed_parcel in target_parcels: + parcel = transformed_parcel.parcel + if not isinstance(parcel, LanVpnParcel): + self.builder.add_parcel(parcel) # type: ignore + else: + vpn_tag = self.builder.add_parcel_vpn(parcel) # type: ignore + for transformed_subparcel in [ + all_parcels.get(element) for element in transformed_parcel.header.subelements + ]: + parcel = self._resolve_parcel_naming(transformed_subparcel) # type: ignore + self.builder.add_parcel_vpn_subparcel(vpn_tag, parcel) # type: ignore self.builder.add_profile_name_and_description(feature_profile) return self.builder.build() - def _resolve_and_add_parcel(self, transformed_parcel: TransformedParcel) -> None: - parcel = transformed_parcel.parcel - if isinstance(parcel, LanVpnParcel): - self.builder.add_parcel_vpn(parcel) # type: ignore - elif isinstance( - parcel, (InterfaceEthernetParcel, InterfaceGreParcel, InterfaceIpsecParcel, InterfaceSviParcel) - ): - # TODO: Assiging logic - # Every Service VPN parcel can have interface childs and - # there can be multiple service VPNs in one feature profile - pass - else: - self.builder.add_parcel(parcel) # type: ignore - - def _move_vpn_parcel_to_first_position(self, parcels: List[TransformedParcel]) -> None: - """Move the VPN parcel to the first position in the list. - Without this there is posibility to add_parcel_vpn_interface with None tag.""" - parcels.sort(key=lambda x: x.parcel._get_parcel_type() == "lan/vpn", reverse=True) + def _resolve_parcel_naming(self, transformed_subparcel: TransformedParcel) -> AnyAssociatoryParcel: + """This occurs when a Device Template has many VPNs and the VPNs have assigned the same subcomponent. + In UX2 every subparcel has to be unique, so we have to rename the subparcels to avoid conflicts. + We check if the same name exist and if it does we add a counter to the name, + create copy and change the name with the counter value. + """ + self.subparcel_name_counter[transformed_subparcel.parcel.parcel_name] += 1 + parcel = cast(AnyAssociatoryParcel, transformed_subparcel.parcel) + count_value = self.subparcel_name_counter[parcel.parcel_name] + if count_value > 1: + parcel = parcel.model_copy(deep=True) + parcel.parcel_name += f"_{count_value}" + return parcel