From 919e63d808a02b19a9a73c2ec9a0fd5bcf04cc22 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Thu, 28 Mar 2024 18:06:41 +0100 Subject: [PATCH] Add OSPFv3IP4 converter. --- .../feature_profile/sdwan/service/__init__.py | 8 +- .../feature_profile/sdwan/service/ospfv3.py | 17 +- .../converters/feature_template/gre.py | 2 +- .../converters/feature_template/ipsec.py | 2 +- .../converters/feature_template/ospfv3.py | 201 ++++++++++++++++++ .../feature_template/parcel_factory.py | 2 + catalystwan/workflows/config_migration.py | 4 + 7 files changed, 225 insertions(+), 11 deletions(-) create mode 100644 catalystwan/utils/config_migration/converters/feature_template/ospfv3.py diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py index 94c3cfd3..08452aed 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py @@ -3,8 +3,6 @@ from pydantic import Field from typing_extensions import Annotated -from catalystwan.models.configuration.feature_profile.sdwan.service.ospf import OspfParcel - from .appqoe import AppqoeParcel from .dhcp_server import LanVpnDhcpServerParcel from .lan.ethernet import InterfaceEthernetParcel @@ -12,6 +10,8 @@ from .lan.ipsec import InterfaceIpsecParcel from .lan.svi import InterfaceSviParcel from .lan.vpn import LanVpnParcel +from .ospf import OspfParcel +from .ospfv3 import Ospfv3IPv4Parcel, Ospfv3IPv6Parcel AnyTopLevelServiceParcel = Annotated[ Union[ @@ -19,6 +19,8 @@ AppqoeParcel, LanVpnParcel, OspfParcel, + Ospfv3IPv4Parcel, + Ospfv3IPv6Parcel, # TrackerGroupData, # WirelessLanData, # SwitchportData @@ -46,6 +48,8 @@ "AppqoeParcel", "LanVpnParcel", "OspfParcel", + "Ospfv3IPv4Parcel", + "Ospfv3IPv6Parcel", "InterfaceSviParcel", "InterfaceGreParcel", "AnyServiceParcel", diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/ospfv3.py b/catalystwan/models/configuration/feature_profile/sdwan/service/ospfv3.py index 9ccfdbd5..d60c2f14 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/ospfv3.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/ospfv3.py @@ -1,11 +1,13 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates +from ipaddress import IPv4Address from typing import List, Literal, Optional, Union from uuid import UUID from pydantic import AliasPath, BaseModel, ConfigDict, Field from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase +from catalystwan.models.common import MetricType from catalystwan.models.configuration.feature_profile.common import Prefix NetworkType = Literal[ @@ -40,7 +42,6 @@ "omp", "eigrp", ] -MetricType = Literal["type1", "type2"] class NoAuth(BaseModel): @@ -171,7 +172,7 @@ class Ospfv3IPv6Area(BaseModel): serialization_alias="areaTypeConfig", validation_alias="areaTypeConfig", default=None ) interfaces: List[Ospfv3InterfaceParametres] - ranges: Optional[List[SummaryRoute]] = None + ranges: Optional[List[SummaryRouteIPv6]] = None class MaxMetricRouterLsa(BaseModel): @@ -210,7 +211,9 @@ class DefaultOriginate(BaseModel): originate: Union[Global[bool], Default[bool]] always: Optional[Union[Global[bool], Variable, Default[bool]]] = None metric: Optional[Union[Global[str], Variable, Default[None]]] = None - metricType: Optional[Union[Global[MetricType], Variable, Default[None]]] = None + metric_type: Optional[Union[Global[MetricType], Variable, Default[None]]] = Field( + default=None, serialization_alias="metricType", validation_alias="metricType" + ) class SpfTimers(BaseModel): @@ -243,7 +246,7 @@ class AdvancedOspfv3Attributes(BaseModel): class BasicOspfv3Attributes(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - router_id: Optional[Union[Global[str], Variable, Default[None]]] = Field( + router_id: Optional[Union[Global[str], Global[IPv4Address], Variable, Default[None]]] = Field( serialization_alias="routerId", validation_alias="routerId", default=None ) distance: Optional[Union[Global[int], Variable, Default[int]]] = None @@ -264,13 +267,13 @@ class Ospfv3IPv4Parcel(_ParcelBase): basic: Optional[BasicOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "basic")) advanced: Optional[AdvancedOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "advanced")) - redistribute: Optional[RedistributedRouteIPv6] = Field( + redistribute: Optional[List[RedistributedRoute]] = Field( default=None, validation_alias=AliasPath("data", "redistribute") ) max_metric_router_lsa: Optional[MaxMetricRouterLsa] = Field( validation_alias=AliasPath("data", "maxMetricRouterLsa"), default=None ) - area: List[Ospfv3IPv6Area] = Field(validation_alias=AliasPath("data", "area")) + area: List[Ospfv3IPv4Area] = Field(validation_alias=AliasPath("data", "area")) class Ospfv3IPv6Parcel(_ParcelBase): @@ -279,7 +282,7 @@ class Ospfv3IPv6Parcel(_ParcelBase): basic: Optional[BasicOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "basic")) advanced: Optional[AdvancedOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "advanced")) - redistribute: Optional[RedistributedRouteIPv6] = Field( + redistribute: Optional[List[RedistributedRouteIPv6]] = Field( default=None, validation_alias=AliasPath("data", "redistribute") ) max_metric_router_lsa: Optional[MaxMetricRouterLsa] = Field( diff --git a/catalystwan/utils/config_migration/converters/feature_template/gre.py b/catalystwan/utils/config_migration/converters/feature_template/gre.py index 80f4adf2..37b3fa7d 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/gre.py +++ b/catalystwan/utils/config_migration/converters/feature_template/gre.py @@ -13,7 +13,7 @@ class InterfaceGRETemplateConverter: - supported_template_types = ("cisco_vpn_interface_gre",) + supported_template_types = ("cisco_vpn_interface_gre", "vpn-vedge-interface-gre") tunnel_destination_ip4 = "{{gre_tunnelDestination_ip4}}" diff --git a/catalystwan/utils/config_migration/converters/feature_template/ipsec.py b/catalystwan/utils/config_migration/converters/feature_template/ipsec.py index fb18de2b..02a9cd21 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ipsec.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ipsec.py @@ -7,7 +7,7 @@ class InterfaceIpsecTemplateConverter: - supported_template_types = ("cisco_vpn_interface_ipsec",) + supported_template_types = ("cisco_vpn_interface_ipsec", "vpn-vedge-interface-ipsec") delete_keys = ( "dead_peer_detection", diff --git a/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py b/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py new file mode 100644 index 00000000..47afd9c2 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py @@ -0,0 +1,201 @@ +from copy import deepcopy +from typing import List, Optional, Tuple, Union + +from catalystwan.api.configuration_groups.parcel import as_global +from catalystwan.models.configuration.feature_profile.common import Prefix +from catalystwan.models.configuration.feature_profile.sdwan.service import Ospfv3IPv4Parcel +from catalystwan.models.configuration.feature_profile.sdwan.service.ospfv3 import ( + AdvancedOspfv3Attributes, + BasicOspfv3Attributes, + DefaultArea, + DefaultOriginate, + MaxMetricRouterLsa, + MaxMetricRouterLsaAction, + NormalArea, + NssaArea, + Ospfv3InterfaceParametres, + Ospfv3IPv4Area, + Ospfv3IPv6Parcel, + RedistributedRoute, + RedistributeProtocol, + SpfTimers, + StubArea, + SummaryRoute, +) +from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException + + +class Ospfv3TemplateConverter: + """ + Warning: This class returns a tuple of Ospfv3IPv4Parcel and Ospfv3IPv6Parcel objects, + because the Feature Template has two definitions inside one for IPv4 and one for IPv6. + """ + + supported_template_types = ("cisco_ospfv3",) + + def create_parcel( + self, name: str, description: str, template_values: dict + ) -> Tuple[Ospfv3IPv4Parcel, Ospfv3IPv6Parcel]: + if template_values.get("ospfv3") is None: + raise CatalystwanConverterCantConvertException("Feature Template does not contain OSPFv3 configuration") + ospfv3ipv4 = Ospfv3Ipv4TemplateSubconverter().create_parcel(name, description, template_values) + return ospfv3ipv4 # type: ignore + + +class Ospfv3Ipv4TemplateSubconverter: + delete_keys = ( + "default_information", + "router_id", + "table_map", + "max_metric", + "timers", + "distance_ipv4", + "auto_cost", + "compatible", + ) + + def create_parcel(self, name: str, description: str, template_values: dict) -> Ospfv3IPv4Parcel: + values = deepcopy(template_values).get("ospfv3", {}).get("address_family", {}).get("ipv4", {}) + self.configure_basic_ospf_v3_attributes(values) + self.configure_advanced_ospf_v3_attributes(values) + self.configure_max_metric_router_lsa(values) + self.configure_area(values) + self.configure_redistribute(values) + self.cleanup_keys(values) + return Ospfv3IPv4Parcel(parcel_name=name, parcel_description=description, **values) + + def configure_basic_ospf_v3_attributes(self, values: dict) -> None: + distance_configuration = self._get_distance_configuration(values) + basic_values = self._get_basic_values(distance_configuration) + values["basic"] = BasicOspfv3Attributes(router_id=values.get("router_id"), **basic_values) + + def _get_distance_configuration(self, values: dict) -> dict: + return values.get("distance_ipv4", {}) + + def _get_basic_values(self, values: dict) -> dict: + return { + "distance": values.get("distance"), + "external_distance": values.get("ospf", {}).get("external"), + "inter_area_distance": values.get("ospf", {}).get("inter_area"), + "intra_area_distance": values.get("ospf", {}).get("intra_area"), + } + + def configure_advanced_ospf_v3_attributes(self, values: dict) -> None: + values["advanced"] = AdvancedOspfv3Attributes( + default_originate=self._configure_originate(values), + spf_timers=self._configure_spf_timers(values), + filter=values.get("table_map", {}).get("filter"), + policy_name=values.get("table_map", {}).get("policy_name"), + reference_bandwidth=values.get("auto_cost", {}).get("reference_bandwidth"), + compatible_rfc1583=values.get("compatible", {}).get("rfc1583"), + ) + + def _configure_originate(self, values: dict) -> Optional[DefaultOriginate]: + originate = values.get("default_information", {}).get("originate") + if originate is None: + return None + metric = originate.get("metric") + if metric is not None: + metric = as_global(str(metric.value)) + return DefaultOriginate( + originate=as_global(True), + always=originate.get("always"), + metric=metric, + metric_type=originate.get("metric_type"), + ) + + def _configure_spf_timers(self, values: dict) -> Optional[SpfTimers]: + timers = values.get("timers", {}).get("throttle", {}).get("spf") + if timers is None: + return None + return SpfTimers( + delay=timers.get("delay"), + initial_hold=timers.get("initial_hold"), + max_hold=timers.get("max_hold"), + ) + + def configure_max_metric_router_lsa(self, values: dict) -> None: + router_lsa = values.get("max_metric", {}).get("router_lsa", [])[0] # Payload contains only one item + if router_lsa == []: + return + + action = router_lsa.get("ad_type") + if action is not None: + action = as_global(action.value, MaxMetricRouterLsaAction) + + values["max_metric_router_lsa"] = MaxMetricRouterLsa( + action=action, + on_startup_time=router_lsa.get("time"), + ) + + def configure_area(self, values: dict) -> None: + area = values.get("area") + if area is None: + raise CatalystwanConverterCantConvertException("Area is required for OSPFv3") + area_list = [] + for area_value in area: + area_list.append( + Ospfv3IPv4Area( + area_number=area_value.get("a_num"), + area_type_config=self._set_area_type_config(area_value), + interfaces=self._set_interfaces(area_value), + ranges=self._set_range(area_value), + ) + ) + values["area"] = area_list + + def _set_area_type_config(self, area_value: dict) -> Optional[Union[StubArea, NssaArea, NormalArea, DefaultArea]]: + if "stub" in area_value: + return StubArea(no_summary=area_value.get("stub", {}).get("no_summary")) + elif "nssa" in area_value: + return NssaArea(no_summary=area_value.get("nssa", {}).get("no_summary")) + elif "normal" in area_value: + return NormalArea() + return DefaultArea() + + def _set_interfaces(self, area_value: dict) -> List[Ospfv3InterfaceParametres]: + interfaces = area_value.get("interface", []) + if interfaces == []: + return [] + interface_list = [] + for interface in interfaces: + if authentication := interface.pop("authentication", None): + area_value["authentication_type"] = authentication.get("type") + interface_list.append(Ospfv3InterfaceParametres(**interface)) + return interface_list + + def _set_range(self, area_value: dict) -> Optional[List[SummaryRoute]]: + ranges = area_value.get("range") + if ranges is None: + return None + range_list = [] + for range_ in ranges: + self._set_summary_prefix(range_) + range_list.append(SummaryRoute(**range_)) + return range_list + + def _set_summary_prefix(self, range_: dict) -> None: + if address := range_.pop("address"): + range_["network"] = Prefix( + address=as_global(str(address.value.network)), mask=as_global(str(address.value.netmask)) + ) + + def configure_redistribute(self, values: dict) -> None: + redistributes = values.get("redistribute", []) + if redistributes == []: + return None + redistribute_list = [] + for redistribute in redistributes: + print(redistribute) + redistribute_list.append( + RedistributedRoute( + protocol=as_global(redistribute.get("protocol").value, RedistributeProtocol), + route_policy=redistribute.get("route_map"), + nat_dia=redistribute.get("dia"), + ) + ) + values["redistribute"] = redistribute_list + + 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/parcel_factory.py b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py index 743e3a04..0408aecf 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py +++ b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py @@ -25,6 +25,7 @@ from .ntp import NTPTemplateConverter from .omp import OMPTemplateConverter from .ospf import OspfTemplateConverter +from .ospfv3 import Ospfv3TemplateConverter from .security import SecurityTemplateConverter from .svi import InterfaceSviTemplateConverter from .thousandeyes import ThousandEyesTemplateConverter @@ -55,6 +56,7 @@ InterfaceEthernetTemplateConverter, InterfaceIpsecTemplateConverter, OspfTemplateConverter, + Ospfv3TemplateConverter, ] diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 0059baa8..08e41a64 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -182,6 +182,10 @@ def transform(ux1: UX1Config) -> UX2Config: for ft in ux1.templates.feature_templates: if ft.template_type in SUPPORTED_TEMPLATE_TYPES: parcel = create_parcel_from_template(ft) + # if isinstance(parcel, tuple): + # for p in parcel: + # ..... + # find uuid in transformed_parcel = TransformedParcel( header=TransformHeader( type=parcel._get_parcel_type(),