diff --git a/catalystwan/models/configuration/feature_profile/common.py b/catalystwan/models/configuration/feature_profile/common.py index 585dd520..3bca973f 100644 --- a/catalystwan/models/configuration/feature_profile/common.py +++ b/catalystwan/models/configuration/feature_profile/common.py @@ -1,6 +1,7 @@ # Copyright 2023 Cisco Systems, Inc. and its affiliates from datetime import datetime +from ipaddress import IPv4Address from typing import Generic, List, Literal, Optional, TypeVar, Union from uuid import UUID @@ -160,7 +161,7 @@ class ParcelAssociationPayload(BaseModel): class Prefix(BaseModel): - address: Union[Variable, Global[str]] + address: Union[Variable, Global[str], Global[IPv4Address], Global[IPv6Address]] mask: Union[Variable, Global[str]] 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 83ecc565..762f5deb 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py @@ -1,6 +1,6 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv6Address from typing import List, Literal, Optional, Union from uuid import UUID @@ -124,11 +124,15 @@ class HostMapping(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) host_name: Union[Variable, Global[str]] = Field(serialization_alias="hostName", validation_alias="hostName") - list_of_ip: Union[Variable, Global[str]] = Field(serialization_alias="listOfIp", validation_alias="listOfIp") + list_of_ip: Union[Variable, Global[List[str]]] = Field(serialization_alias="listOfIp", validation_alias="listOfIp") class RoutePrefix(BaseModel): - ip_address: Union[Variable, Global[str]] = Field(serialization_alias="ipAddress", validation_alias="ipAddress") + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + ip_address: Union[Variable, Global[str], Global[IPv4Address], Global[IPv6Address]] = Field( + serialization_alias="ipAddress", validation_alias="ipAddress" + ) subnet_mask: Union[Variable, Global[str]] = Field(serialization_alias="subnetMask", validation_alias="subnetMask") @@ -139,7 +143,7 @@ class IPv4Prefix(BaseModel): aggregate_only: Optional[Union[Global[bool], Default[bool]]] = Field( serialization_alias="aggregateOnly", validation_alias="aggregateOnly", default=None ) - region: Optional[Union[Variable, Global[Region], Default[str]]] = None + region: Optional[Union[Variable, Global[Region], Default[Region]]] = None class IPv6Prefix(BaseModel): @@ -149,7 +153,7 @@ class IPv6Prefix(BaseModel): aggregate_only: Optional[Union[Global[bool], Default[bool]]] = Field( serialization_alias="aggregateOnly", validation_alias="aggregateOnly", default=None ) - region: Optional[Union[Variable, Global[Region], Default[str]]] = None + region: Optional[Union[Variable, Global[Region], Default[Region]]] = None class OmpAdvertiseIPv4(BaseModel): @@ -179,7 +183,7 @@ class OmpAdvertiseIPv6(BaseModel): class IPv4RouteGatewayNextHop(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - address: Union[Variable, Global[str]] + address: Union[Variable, Global[str], Global[IPv4Address]] distance: Union[Variable, Global[int], Default[int]] = Default[int](value=1) @@ -274,7 +278,7 @@ class NextHopInterfaceRoute(BaseModel): class NextHopInterfaceRouteIPv6(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - address: Union[Variable, Global[str], Default[None]] = Default[None](value=None) + address: Union[Variable, Global[str], Global[IPv6Address], Default[None]] = Default[None](value=None) distance: Union[Variable, Global[int], Default[int]] = Default[int](value=1) @@ -293,7 +297,9 @@ class IPv6StaticRouteInterface(BaseModel): interface_name: Union[Variable, Global[str]] = Field( serialization_alias="interfaceName", validation_alias="interfaceName" ) - next_hop: List[NextHopInterfaceRouteIPv6] = Field(serialization_alias="nextHop", validation_alias="nextHop") + interface_next_hop: List[NextHopInterfaceRouteIPv6] = Field( + serialization_alias="nextHop", validation_alias="nextHop" + ) class InterfaceContainer(BaseModel): @@ -375,7 +381,7 @@ class StaticGreRouteIPv4(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) prefix: Prefix - interface: Union[Variable, Global[List[str]], Default[None]] + interface: Union[Variable, Global[List[str]], Default[None]] = Default[None](value=None) vpn: Global[int] = Global[int](value=0) @@ -383,7 +389,7 @@ class StaticIpsecRouteIPv4(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) prefix: Prefix - interface: Union[Variable, Global[List[str]], Default[None]] + interface: Union[Variable, Global[List[str]], Default[None]] = Default[None](value=None) class NatPool(BaseModel): @@ -418,8 +424,10 @@ class NatPortForward(BaseModel): translate_port: Union[Variable, Global[int]] = Field( serialization_alias="translatePort", validation_alias="translatePort" ) - source_ip: Union[Variable, Global[str]] = Field(serialization_alias="sourceIp", validation_alias="sourceIp") - translated_source_ip: Union[Variable, Global[str]] = Field( + source_ip: Union[Variable, Global[str], Global[IPv4Address]] = Field( + serialization_alias="sourceIp", validation_alias="sourceIp" + ) + translated_source_ip: Union[Variable, Global[str], Global[IPv4Address]] = Field( serialization_alias="TranslatedSourceIp", validation_alias="TranslatedSourceIp" ) protocol: Union[Variable, Global[NATPortForwardProtocol]] @@ -431,8 +439,10 @@ class StaticNat(BaseModel): nat_pool_name: Union[Variable, Global[int], Default[None]] = Field( serialization_alias="natPoolName", validation_alias="natPoolName" ) - source_ip: Union[Variable, Global[str]] = Field(serialization_alias="sourceIp", validation_alias="sourceIp") - translated_source_ip: Union[Variable, Global[str]] = Field( + source_ip: Union[Variable, Global[str], Global[IPv4Address]] = Field( + serialization_alias="sourceIp", validation_alias="sourceIp" + ) + translated_source_ip: Union[Variable, Global[str], Global[IPv4Address]] = Field( serialization_alias="TranslatedSourceIP", validation_alias="TranslatedSourceIP" ) static_nat_direction: Union[Variable, Global[Direction]] = Field( @@ -486,14 +496,14 @@ class RedistributeToService(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) protocol: Union[Variable, Global[RedistributeToServiceProtocol]] - policy: Union[Default[None], Global[UUID]] + policy: Union[Default[None], Global[UUID]] = Default[None](value=None) class RedistributeToGlobal(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) protocol: Union[Variable, Global[RedistributeToGlobalProtocol]] - policy: Union[Default[None], Global[UUID]] + policy: Union[Default[None], Global[UUID]] = Default[None](value=None) class RouteLeakFromGlobal(BaseModel): @@ -577,7 +587,7 @@ class LanVpnParcel(_ParcelBase): vpn_name: Union[Variable, Global[str], Default[None]] = Field( default=Default[None](value=None), validation_alias=AliasPath("data", "name") ) - omp_admin_distance: Optional[Union[Variable, Global[int], Default[None]]] = Field( + omp_admin_distance_ipv4: Optional[Union[Variable, Global[int], Default[None]]] = Field( validation_alias=AliasPath("data", "ompAdminDistance"), default=None ) omp_admin_distance_ipv6: Optional[Union[Variable, Global[int], Default[None]]] = Field( diff --git a/catalystwan/utils/config_migration/converters/feature_template/vpn.py b/catalystwan/utils/config_migration/converters/feature_template/vpn.py index 49b42a82..3f913dcd 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/vpn.py +++ b/catalystwan/utils/config_migration/converters/feature_template/vpn.py @@ -1,19 +1,92 @@ +import logging from copy import deepcopy +from ipaddress import IPv4Interface, IPv6Interface -from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import DnsIPv4, LanVpnParcel, Nat64v4Pool +from catalystwan.api.configuration_groups.parcel import as_default, as_global, as_variable +from catalystwan.models.configuration.feature_profile.common import Prefix +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import ( + Direction, + DnsIPv4, + HostMapping, + InterfaceIPv6Container, + InterfaceRouteIPv6Container, + IPv4Prefix, + IPv4RouteGatewayNextHop, + IPv6Prefix, + IPv6StaticRouteInterface, + LanVpnParcel, + Nat64v4Pool, + NatPool, + NatPortForward, + NATPortForwardProtocol, + NextHopContainer, + NextHopRouteContainer, + OmpAdvertiseIPv4, + OmpAdvertiseIPv6, + ProtocolIPv4, + ProtocolIPv6, + RedistributeToService, + RedistributeToServiceProtocol, + Region, + RouteLeakBetweenServices, + RouteLeakFromGlobal, + RouteLeakFromService, + RouteLeakFromServiceProtocol, + RoutePrefix, + Service, + ServiceRoute, + ServiceType, + StaticGreRouteIPv4, + StaticIpsecRouteIPv4, + StaticNat, + StaticRouteIPv4, + StaticRouteIPv6, + StaticRouteVPN, +) + +logger = logging.getLogger(__name__) class LanVpnParcelTemplateConverter: """ - A class for converting template values into a ThousandEyesParcel object. + A class for converting template values into a LanVpnParcel object. """ supported_template_types = ("cisco_vpn",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> LanVpnParcel: + # Default Values - IPv4 Route + ipv4_route_prefix_network_address = "{{{{lan_vpn_ipv4Route_{}_prefix_networkAddress}}}}" + ipv4_route_prefix_subnet_mask = "{{{{lan_vpn_ipv4Route_{}_prefix_subnetMask}}}}" + ipv4_route_next_hop_address = "{{{{lan_vpn_ipv4Route_{}_nextHop_{}_address}}}}" + ipv4_route_next_hop_administrative_distance = "{{{{lan_vpn_ipv4Route_{}_nextHop_{}_administrativeDistance}}}}" + + # Default Values - Service + service_ipv4_addresses = "{{{{lan_vpn_service_{}_ipv4Addresses}}}}" + + # Default Values - NAT + nat_natpool_name = "{{{{lan_vpn_nat_{}_natpoolName}}}}" + nat_prefix_length = "{{{{lan_vpn_nat_{}_prefixLength}}}}" + nat_range_start = "{{{{lan_vpn_nat_{}_rangeStart}}}}" + nat_range_end = "{{{{lan_vpn_nat_{}_rangeEnd}}}}" + nat_overload = "{{{{lan_vpn_nat_{}_overload}}}}" + nat_direction = "{{{{lan_vpn_nat_{}_direction}}}}" + + # Default Values - Static NAT + static_nat_pool_name = "{{{{lan_vpn__staticNat_{}_poolName}}}}" + static_nat_source_ip = "{{{{lan_vpn_staticNat_{}_sourceIp}}}}" + static_nat_translated_source_ip = "{{{{lan_vpn_staticNat_{}_translatedSourceIp}}}}" + static_nat_direction = "{{{{lan_vpn_staticNat_{}_direction}}}}" + + # Default Values - NAT64 + nat64_v4_pool_name = "{{{{lan_vpn_nat64_{}_v4_poolName}}}}" + nat64_v4_pool_range_start = "{{{{lan_vpn_nat64_{}_v4_poolRangeStart}}}}" + nat64_v4_pool_range_end = "{{{{lan_vpn_nat64_{}_v4_poolRangeEnd}}}}" + nat64_v4_pool_overload = "{{{{lan_vpn_nat64_{}_v4_poolOverload}}}}" + + @classmethod + def create_parcel(cls, name: str, description: str, template_values: dict) -> LanVpnParcel: """ - Creates a ThousandEyesParcel object based on the provided template values. + Creates a LanVpnParcel object based on the provided parameters. Args: name (str): The name of the parcel. @@ -21,25 +94,134 @@ def create_parcel(name: str, description: str, template_values: dict) -> LanVpnP template_values (dict): A dictionary containing the template values. Returns: - ThousandEyesParcel: A ThousandEyesParcel object with the provided values. + LanVpnParcel: The created LanVpnParcel object. """ values = deepcopy(template_values) print(values) if vpn_name := values.pop("name", None): values["vpn_name"] = vpn_name - if nat := values.pop("nat", {}).pop("natpool", []): - values["nat_pool"] = nat - if nat64 := values.pop("nat64", {}).pop("v4", {}).pop("pool", []): + + network_address_translation = values.pop("nat", {}) + if natpool := network_address_translation.pop("natpool", []): + nat_items = [] + for nat_i, entry in enumerate(natpool): + direction = entry.get("direction") + if direction: + direction = as_global(direction.value, Direction) + else: + direction = as_variable(cls.nat_direction.format(nat_i + 1)) + nat_items.append( + NatPool( + nat_pool_name=entry.get("name", as_variable(cls.nat_natpool_name.format(nat_i + 1))), + prefix_length=entry.get("prefix_length", as_variable(cls.nat_prefix_length.format(nat_i + 1))), + range_start=entry.get("range_start", as_variable(cls.nat_range_start.format(nat_i + 1))), + range_end=entry.get("range_end", as_variable(cls.nat_range_end.format(nat_i + 1))), + overload=entry.get("overload", as_default(True)), + direction=direction, + ) + ) + values["nat_pool"] = nat_items + + if port_forward := network_address_translation.pop("port_forward", []): + nat_port_forwarding_items = [] + for entry in port_forward: + nat_port_forwarding_items.append( + NatPortForward( + nat_pool_name=entry["pool_name"], + source_port=entry["source_port"], + translate_port=entry["translate_port"], + source_ip=entry["source_ip"], + translated_source_ip=entry["translate_ip"], + protocol=as_global(entry["proto"].value.upper(), NATPortForwardProtocol), + ) + ) + values["nat_port_forwarding"] = nat_port_forwarding_items + + if static_nat := network_address_translation.pop("static", []): + static_nat_items = [] + for static_nat_i, entry in enumerate(static_nat): + static_nat_direction = entry.get("static_nat_direction") + if static_nat_direction: + static_nat_direction = as_global(static_nat_direction.value, Direction) + else: + static_nat_direction = as_variable(cls.static_nat_direction.format(static_nat_i + 1)) + static_nat_items.append( + StaticNat( + nat_pool_name=entry.get( + "pool_name", as_variable(cls.static_nat_pool_name.format(static_nat_i + 1)) + ), + source_ip=entry.get( + "source_ip", as_variable(cls.static_nat_source_ip.format(static_nat_i + 1)) + ), + translated_source_ip=entry.get( + "translate_ip", as_variable(cls.static_nat_translated_source_ip.format(static_nat_i + 1)) + ), + static_nat_direction=static_nat_direction, + ) + ) + values["static_nat"] = static_nat_items + + network_address_translation_64 = values.pop("nat64", {}) + if nat64pool := network_address_translation_64.pop("v4", {}).pop("pool", []): nat64_items = [] - for entry in nat64: - nat64_item = Nat64v4Pool( - nat64_v4_pool_name=entry["name"], - nat64_v4_pool_range_start=entry["start_address"], - nat64_v4_pool_range_end=entry["end_address"], - nat64_v4_pool_overload=entry["overload"], - ) - nat64_items.append(nat64_item) + for nat64pool_i, entry in enumerate(nat64pool): + nat64_items.append( + Nat64v4Pool( + nat64_v4_pool_name=entry.get( + "name", as_variable(cls.nat64_v4_pool_name.format(nat64pool_i + 1)) + ), + nat64_v4_pool_range_start=entry.get( + "start_address", as_variable(cls.nat64_v4_pool_range_start.format(nat64pool_i + 1)) + ), + nat64_v4_pool_range_end=entry.get( + "end_address", as_variable(cls.nat64_v4_pool_range_end.format(nat64pool_i + 1)) + ), + nat64_v4_pool_overload=entry.get( + "overload", as_variable(cls.nat64_v4_pool_overload.format(nat64pool_i + 1)) + ), + ) + ) values["nat64_v4_pool"] = nat64_items + + omp = values.pop("omp", {}) + if omp_advertise_ipv4 := omp.pop("advertise", []): + omp_advertise_ipv4_items = [] + for entry in omp_advertise_ipv4: + ipv4_prefix_list_items = [] + for prefix_entry in entry.pop("prefix_list", []): + ipv4_prefix_item = IPv4Prefix( + prefix=prefix_entry["prefix_entry"], + aggregate_only=prefix_entry["aggregate_only"], + region=prefix_entry["region"], + ) + ipv4_prefix_list_items.append(ipv4_prefix_item) + + omp_advertise_ipv4_item = OmpAdvertiseIPv4( + omp_protocol=as_global(entry["protocol"].value, ProtocolIPv4), + prefix_list=ipv4_prefix_list_items if ipv4_prefix_list_items else None, + ) + omp_advertise_ipv4_items.append(omp_advertise_ipv4_item) + values["omp_advertise_ipv4"] = omp_advertise_ipv4_items + + if omp_advertise_ipv6 := omp.pop("ipv6_advertise", []): + omp_advertise_ipv6_items = [] + for entry in omp_advertise_ipv6: + ipv6_prefix_list_items = [] + for prefix_entry in entry.pop("prefix_list", []): + ipv6_prefix_item = IPv6Prefix( + prefix=prefix_entry["prefix_entry"], + aggregate_only=prefix_entry["aggregate_only"], + region=as_global(prefix_entry["region"].value, Region), + ) + ipv6_prefix_list_items.append(ipv6_prefix_item) + + omp_advertise_ipv6_item = OmpAdvertiseIPv6( + omp_protocol=as_global(entry["protocol"].value, ProtocolIPv6), + prefix_list=ipv6_prefix_list_items if ipv6_prefix_list_items else None, + ) + omp_advertise_ipv6_items.append(omp_advertise_ipv6_item) + values["omp_advertise_ipv6"] = omp_advertise_ipv6_items + if dns := values.pop("dns", {}): dns_ipv4 = DnsIPv4() for entry in dns: @@ -48,6 +230,180 @@ def create_parcel(name: str, description: str, template_values: dict) -> LanVpnP elif entry["role"] == "secondary": dns_ipv4.secondary_dns_address_ipv4 = entry["dns_addr"] + if host := values.pop("host", []): + host_mapping_items = [] + for entry in host: + host_mapping_item = HostMapping( + host_name=entry["hostname"], + list_of_ip=entry["ip"], + ) + host_mapping_items.append(host_mapping_item) + values["new_host_mapping"] = host_mapping_items + + if service := values.get("service", []): + service_items = [] + for service_i, entry in enumerate(service): + service_item = Service( + service_type=as_global(entry["svc_type"].value, ServiceType), + ipv4_addresses=entry.get("address", as_variable(cls.service_ipv4_addresses.format(service_i + 1))), + tracking=entry.get("track_enable", as_default(False)), + ) + service_items.append(service_item) + values["service"] = service_items + + ipv4 = values.pop("ip", {}) + if ipv4_route := ipv4.pop("route", []): + ipv4_route_items = [] + for route_i, route in enumerate(ipv4_route): + prefix = route.pop("prefix", None) + if prefix: + interface = IPv4Interface(prefix.value) + route_prefix = RoutePrefix( + ip_address=as_global(interface.network.network_address), + subnet_mask=as_global(str(interface.netmask)), + ) + else: + route_prefix = RoutePrefix( + ip_address=as_variable(cls.ipv4_route_prefix_network_address.format(route_i + 1)), + subnet_mask=as_variable(cls.ipv4_route_prefix_subnet_mask.format(route_i + 1)), + ) + ip_route_item = None + if "next_hop" in route: + next_hop_items = [] + for next_hop_i, next_hop in enumerate(route.pop("next_hop", [])): + next_hop_items.append( + IPv4RouteGatewayNextHop( + address=next_hop.pop( + "address", + as_variable(cls.ipv4_route_next_hop_address.format(route_i + 1, next_hop_i + 1)), + ), + distance=next_hop.pop( + "distance", + as_variable( + cls.ipv4_route_next_hop_administrative_distance.format( + route_i + 1, next_hop_i + 1 + ) + ), + ), + ) + ) + ip_route_item = NextHopRouteContainer(next_hop_container=NextHopContainer(next_hop=next_hop_items)) + elif "vpn" in route: + ip_route_item = StaticRouteVPN( # type: ignore + vpn=as_global(True), + ) + ipv4_route_items.append( + StaticRouteIPv4(prefix=route_prefix, one_of_ip_route=ip_route_item) # type: ignore + ) + values["ipv4_route"] = ipv4_route_items + + if gre_routes := ipv4.pop("gre_route", []): + gre_route_items = [] + for gre_route in gre_routes: + interface = IPv4Interface(gre_route.pop("prefix").value) + gre_prefix = Prefix( + address=as_global(interface.network.network_address), + mask=as_global(str(interface.netmask)), + ) + gre_route_items.append(StaticGreRouteIPv4(prefix=gre_prefix, vpn=gre_route.pop("vpn"))) + values["gre_route"] = gre_route_items + + if ipsec_routes := ipv4.pop("ipsec_route", []): + ipsec_route_items = [] + for ipsec_route in ipsec_routes: + interface = IPv4Interface(ipsec_route.pop("prefix").value) + ipsec_prefix = Prefix( + address=as_global(interface.network.network_address), + mask=as_global(str(interface.netmask)), + ) + ipsec_route_prefix = StaticIpsecRouteIPv4(prefix=ipsec_prefix) + ipsec_route_items.append(ipsec_route_prefix) + values["ipsec_route"] = ipsec_route_items + + if service_routes := ipv4.pop("service_route", []): + service_route_items = [] + for service_route in service_routes: + ipv4_interface = IPv4Interface(service_route.pop("prefix").value) + service_prefix = Prefix( + address=as_global(ipv4_interface.network.network_address), + mask=as_global(str(ipv4_interface.netmask)), + ) + service_route_prefix = ServiceRoute(prefix=service_prefix, vpn=service_route.pop("vpn")) + service_route_items.append(service_route_prefix) + values["service_route"] = service_route_items + + ipv6 = values.pop("ipv6", {}) + if ipv6_route := ipv6.pop("route", []): + ipv6_route_items = [] + for route in ipv6_route: + ipv6_interface = IPv6Interface(route.pop("prefix").value) + route_prefix = RoutePrefix( + ip_address=as_global(ipv6_interface.network.network_address), + subnet_mask=as_global(str(ipv6_interface.netmask)), + ) + if route_interface := route.pop("route_interface", []): + static_route_interfaces = [IPv6StaticRouteInterface(**entry) for entry in route_interface] + ipv6_route_item = InterfaceRouteIPv6Container( + interface_container=InterfaceIPv6Container(ipv6_static_route_interface=static_route_interfaces) + ) + ipv6_route_items.append(StaticRouteIPv6(prefix=route_prefix, one_of_ip_route=ipv6_route_item)) + values["ipv6_route"] = ipv6_route_items + + if route_leak_between_services := values.pop("route_import_from", []): + rlbs_items = [] + for rl in route_leak_between_services: + redistribute_items = [] + for redistribute_item in rl.get("redistribute_to", []): + RedistributeToService( + protocol=as_global(redistribute_item["protocol"].value, RedistributeToServiceProtocol), + ) + redistribute_items.append(redistribute_item) + + rlbs_item = RouteLeakBetweenServices( + source_vpn=rl["source_vpn"], + route_protocol=as_global(rl["protocol"].value, RouteLeakFromServiceProtocol), + redistribute_to_protocol=redistribute_items if redistribute_items else None, + ) + rlbs_items.append(rlbs_item) + values["route_leak_between_services"] = rlbs_items + + if route_leak_from_global := values.pop("route_import", []): + rlfg_items = [] + for rl in route_leak_from_global: + redistribute_items = [] + for redistribute_item in rl.get("redistribute_to", []): + RedistributeToService( + protocol=as_global(redistribute_item["protocol"].value, RedistributeToServiceProtocol), + ) + redistribute_items.append(redistribute_item) + + rlfg_item = RouteLeakFromGlobal( + route_protocol=as_global(rl["protocol"].value, RouteLeakFromServiceProtocol), + redistribute_to_protocol=redistribute_items if redistribute_items else None, + ) + rlfg_items.append(rlfg_item) + values["route_leak_from_global"] = rlfg_items + + if route_leak_from_service := values.pop("route_export", []): + rlfs_items = [] + for rl in route_leak_from_service: + redistribute_items = [] + for redistribute_item in rl.get("redistribute_to", []): + RedistributeToService( + protocol=as_global(redistribute_item["protocol"].value, RedistributeToServiceProtocol), + ) + redistribute_items.append(redistribute_item) + + rlfs_item = RouteLeakFromService( + route_protocol=as_global(rl["protocol"].value, RouteLeakFromServiceProtocol), + redistribute_to_protocol=redistribute_items if redistribute_items else None, + ) + rlfs_items.append(rlfs_item) + values["route_leak_from_service"] = rlfs_items + + for key in ["ecmp_hash_key"]: + values.pop(key, None) + parcel_values = { "parcel_name": name, "parcel_description": description, diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 1d3431d4..a04c6793 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -50,6 +50,7 @@ "ucse", "dhcp", "cisco_dhcp_server", + "cisco_vpn", ] FEATURE_PROFILE_SYSTEM = [