diff --git a/catalystwan/models/configuration/config_migration.py b/catalystwan/models/configuration/config_migration.py index 511fa8fc..e76dcbb3 100644 --- a/catalystwan/models/configuration/config_migration.py +++ b/catalystwan/models/configuration/config_migration.py @@ -612,3 +612,6 @@ class ConvertResult(Generic[TO]): def update_status(self, status: ConvertStatus, message: str): self.status = status self.info.append(message) + + def get_info(self) -> str: + return "\n".join(self.info) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py index 241cdf85..fc78d9a0 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py @@ -1,4 +1,3 @@ -# Copyright 2023 Cisco Systems, Inc. and its affiliates # Copyright 2024 Cisco Systems, Inc. and its affiliates from typing import List, Union @@ -14,6 +13,7 @@ from .management.ethernet import InterfaceEthernetParcel as ManagementInterfaceEthernetParcel from .t1e1controller import T1E1ControllerParcel from .vpn import ManagementVpnParcel, TransportVpnParcel +from .wan.interface.cellular import InterfaceCellularParcel from .wan.interface.ethernet import InterfaceEthernetParcel from .wan.interface.gre import InterfaceGreParcel from .wan.interface.ipsec import InterfaceIpsecParcel @@ -37,6 +37,7 @@ InterfaceIpsecParcel, InterfaceEthernetParcel, InterfaceMultilinkParcel, + InterfaceCellularParcel, # Add wan interfaces here ], Field(discriminator="type_"), diff --git a/catalystwan/utils/config_migration/converters/feature_template/aaa.py b/catalystwan/utils/config_migration/converters/feature_template/aaa.py index 5259b925..cc41b9f9 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/aaa.py +++ b/catalystwan/utils/config_migration/converters/feature_template/aaa.py @@ -4,8 +4,10 @@ from catalystwan.api.configuration_groups.parcel import Global from catalystwan.models.configuration.feature_profile.sdwan.system import AAAParcel +from .base import FTConverter -class AAATemplateConverter: + +class AAAConverter(FTConverter): supported_template_types = ("cisco_aaa", "cedge_aaa", "aaa") def create_parcel(self, name: str, description: str, template_values: dict) -> AAAParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/appqoe.py b/catalystwan/utils/config_migration/converters/feature_template/appqoe.py index 98d9cf61..8b1f312d 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/appqoe.py +++ b/catalystwan/utils/config_migration/converters/feature_template/appqoe.py @@ -19,8 +19,10 @@ VirtualApplicationType, ) +from .base import FTConverter -class AppqoeTemplateConverter: + +class AppqoeConverter(FTConverter): supported_template_types = ("appqoe",) def create_parcel(self, name: str, description: str, template_values: dict) -> AppqoeParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/banner.py b/catalystwan/utils/config_migration/converters/feature_template/banner.py index a3d24849..c66d55a3 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/banner.py +++ b/catalystwan/utils/config_migration/converters/feature_template/banner.py @@ -1,7 +1,9 @@ from catalystwan.models.configuration.feature_profile.sdwan.system import BannerParcel +from .base import FTConverter -class BannerTemplateConverter: + +class BannerConverter(FTConverter): supported_template_types = ("cisco_banner",) def create_parcel(self, name: str, description: str, template_values: dict) -> BannerParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/base.py b/catalystwan/utils/config_migration/converters/feature_template/base.py index c178ac61..87ec70a1 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/base.py +++ b/catalystwan/utils/config_migration/converters/feature_template/base.py @@ -1,8 +1,35 @@ -from typing_extensions import Protocol +# Copyright 2024 Cisco Systems, Inc. and its affiliates +from abc import ABC, abstractmethod +from typing import Optional, Tuple +from catalystwan.models.configuration.config_migration import ConvertResult from catalystwan.models.configuration.feature_profile.parcel import AnyParcel +from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException -class FeatureTemplateConverter(Protocol): - def create_parcel(self, name: str, description: str, template_values: dict) -> AnyParcel: +class FTConverter(ABC): + """Feature Template Converter abstract base class.""" + + supported_template_types: Tuple = tuple() + + def __init__(self): + self._convert_result = ConvertResult[AnyParcel]() + + @abstractmethod + def create_parcel(self, name: str, description: str, template_values: dict) -> Optional[AnyParcel]: + """This function can modify status and info attributes of the conversion process.""" ... + + def convert(self, name: str, description: str, template_values: dict) -> ConvertResult[AnyParcel]: + if not self.supported_template_types: + self._convert_result.update_status( + "unsupported", "supported_template_types must be defined in the subclass" + ) + return self._convert_result + + try: + parcel = self.create_parcel(name, description, template_values) + self._convert_result.output = parcel + except CatalystwanConverterCantConvertException as e: + self._convert_result.update_status("failed", str(e)) + return self._convert_result diff --git a/catalystwan/utils/config_migration/converters/feature_template/basic.py b/catalystwan/utils/config_migration/converters/feature_template/basic.py index af4c962c..9350ddaa 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/basic.py +++ b/catalystwan/utils/config_migration/converters/feature_template/basic.py @@ -13,8 +13,10 @@ from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none from catalystwan.utils.timezone import Timezone +from .base import FTConverter -class SystemToBasicTemplateConverter: + +class SystemToBasicConverter(FTConverter): supported_template_types = ("cisco_system", "system-vsmart", "system-vedge") def create_parcel(self, name: str, description: str, template_values: dict) -> BasicParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/bfd.py b/catalystwan/utils/config_migration/converters/feature_template/bfd.py index 64356848..77d00f6f 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/bfd.py +++ b/catalystwan/utils/config_migration/converters/feature_template/bfd.py @@ -1,7 +1,9 @@ from catalystwan.models.configuration.feature_profile.sdwan.system import BFDParcel +from .base import FTConverter -class BFDTemplateConverter: + +class BFDConverter(FTConverter): supported_template_types = ("cisco_bfd", "bfd-vedge") def create_parcel(self, name: str, description: str, template_values: dict) -> BFDParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/bgp.py b/catalystwan/utils/config_migration/converters/feature_template/bgp.py index d578132e..6e32e516 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/bgp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/bgp.py @@ -21,10 +21,12 @@ from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none from catalystwan.utils.config_migration.steps.constants import LAN_BGP, WAN_BGP +from .base import FTConverter + logger = logging.getLogger(__name__) -class BgpRoutingTemplateConverter: +class BgpRoutingConverter(FTConverter): supported_template_types = (WAN_BGP, LAN_BGP) def create_parcel(self, name: str, description: str, template_values: dict) -> RoutingBgpParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/cellular_controller.py b/catalystwan/utils/config_migration/converters/feature_template/cellular_controller.py index 6937fdee..21f2b578 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/cellular_controller.py +++ b/catalystwan/utils/config_migration/converters/feature_template/cellular_controller.py @@ -9,8 +9,10 @@ ) from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from .base import FTConverter -class CellularControllerTemplateConverter: + +class CellularControllerConverter(FTConverter): supported_template_types = ("cellular-cedge-controller",) def create_parcel(self, name: str, description: str, template_values: dict) -> CellularControllerParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/cellular_profile.py b/catalystwan/utils/config_migration/converters/feature_template/cellular_profile.py index d17e1227..e7a93a9f 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/cellular_profile.py +++ b/catalystwan/utils/config_migration/converters/feature_template/cellular_profile.py @@ -14,10 +14,12 @@ ) from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from .base import FTConverter + logger = logging.getLogger(__name__) -class CellularProfileTemplateConverter: +class CellularProfileConverter(FTConverter): supported_template_types = ("cellular-cedge-profile",) def create_parcel(self, name: str, description: str, template_values: dict) -> CellularProfileParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/cli.py b/catalystwan/utils/config_migration/converters/feature_template/cli.py index 43d31dec..90f0b1aa 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/cli.py +++ b/catalystwan/utils/config_migration/converters/feature_template/cli.py @@ -3,8 +3,10 @@ from catalystwan.models.configuration.feature_profile.sdwan.cli.config import ConfigParcel from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from .base import FTConverter -class CliTemplateConverter: + +class CliConverter(FTConverter): supported_template_types = ("cli-template",) def create_parcel(self, name: str, description: str, template_values: dict) -> ConfigParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/dhcp.py b/catalystwan/utils/config_migration/converters/feature_template/dhcp.py index 6e901904..c2cbec62 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/dhcp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/dhcp.py @@ -14,10 +14,12 @@ from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none +from .base import FTConverter + logger = logging.getLogger(__name__) -class DhcpTemplateConverter: +class DhcpConverter(FTConverter): supported_template_types = ("dhcp", "cisco_dhcp_server", "dhcp-server") variable_address_pool = "{{dhcp_1_addressPool_networkAddress}}" diff --git a/catalystwan/utils/config_migration/converters/feature_template/eigrp.py b/catalystwan/utils/config_migration/converters/feature_template/eigrp.py index a4b7fbb1..b3647a34 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/eigrp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/eigrp.py @@ -14,8 +14,10 @@ TableMap, ) +from .base import FTConverter -class EigrpTemplateConverter: + +class EigrpConverter(FTConverter): supported_template_types = ("eigrp",) delete_keys = ("as_num",) diff --git a/catalystwan/utils/config_migration/converters/feature_template/ethernet.py b/catalystwan/utils/config_migration/converters/feature_template/ethernet.py index d6c429aa..6ccf19d2 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ethernet.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ethernet.py @@ -26,8 +26,10 @@ from catalystwan.utils.config_migration.converters.utils import parse_interface_name from catalystwan.utils.config_migration.steps.constants import MANAGEMENT_VPN_ETHERNET +from .base import FTConverter -class ManagementInterfaceEthernetTemplateConverter: + +class ManagementInterfaceEthernetConverter(FTConverter): supported_template_types = (MANAGEMENT_VPN_ETHERNET,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceEthernetParcel: @@ -37,7 +39,7 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> I parcel_name=name, parcel_description=description, advanced=self.parse_advanced(data), - interface_name=parse_interface_name(data), + interface_name=parse_interface_name(self, data), interface_description=data.get("description", Default[None](value=None)), intf_ip_address=self.parse_ipv4_address(data), shutdown=data.get("shutdown"), diff --git a/catalystwan/utils/config_migration/converters/feature_template/global_.py b/catalystwan/utils/config_migration/converters/feature_template/global_.py index 36178e37..73607b7d 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/global_.py +++ b/catalystwan/utils/config_migration/converters/feature_template/global_.py @@ -1,7 +1,9 @@ from catalystwan.models.configuration.feature_profile.sdwan.system import GlobalParcel +from .base import FTConverter -class GlobalTemplateConverter: + +class GlobalConverter(FTConverter): supported_template_types = ("cedge_global",) def create_parcel(self, name: str, description: str, template_values: dict) -> GlobalParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/gps.py b/catalystwan/utils/config_migration/converters/feature_template/gps.py index 47a01e11..c95f2f11 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/gps.py +++ b/catalystwan/utils/config_migration/converters/feature_template/gps.py @@ -5,8 +5,10 @@ from catalystwan.models.configuration.feature_profile.sdwan.transport.gps import GpsMode, GpsParcel from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none +from .base import FTConverter -class GpsTemplateConverter: + +class GpsConverter(FTConverter): supported_template_types = ("cellular-cedge-gps-controller",) def create_parcel(self, name: str, description: str, template_values: dict) -> GpsParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/lan/ethernet.py b/catalystwan/utils/config_migration/converters/feature_template/lan/ethernet.py index 3fe9b13c..86aae815 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/lan/ethernet.py +++ b/catalystwan/utils/config_migration/converters/feature_template/lan/ethernet.py @@ -26,11 +26,12 @@ Trustsec, VrrpIPv4, ) +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.converters.utils import parse_interface_name from catalystwan.utils.config_migration.steps.constants import LAN_VPN_ETHERNET -class LanInterfaceEthernetTemplateConverter: +class LanInterfaceEthernetConverter(FTConverter): supported_template_types = (LAN_VPN_ETHERNET,) delete_keys = ( @@ -113,7 +114,7 @@ def prepare_parcel_values(self, name: str, description: str, values: dict) -> di return {"parcel_name": name, "parcel_description": description, **values} def configure_interface_name(self, values: dict) -> None: - values["interface_name"] = parse_interface_name(values) + values["interface_name"] = parse_interface_name(self, values) def configure_ethernet_description(self, values: dict) -> None: values["ethernet_description"] = values.get("description") diff --git a/catalystwan/utils/config_migration/converters/feature_template/lan/gre.py b/catalystwan/utils/config_migration/converters/feature_template/lan/gre.py index 12cf57d0..e33eeda2 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/lan/gre.py +++ b/catalystwan/utils/config_migration/converters/feature_template/lan/gre.py @@ -18,10 +18,11 @@ TunnelSourceIPv6, ) from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.steps.constants import LAN_VPN_GRE -class LanInterfaceGreTemplateConverter: +class LanInterfaceGreConverter(FTConverter): supported_template_types = (LAN_VPN_GRE,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceGreParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/lan/ipsec.py b/catalystwan/utils/config_migration/converters/feature_template/lan/ipsec.py index 220dfddb..969e623c 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/lan/ipsec.py +++ b/catalystwan/utils/config_migration/converters/feature_template/lan/ipsec.py @@ -7,10 +7,11 @@ from catalystwan.models.configuration.feature_profile.common import AddressWithMask, TunnelApplication from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import InterfaceIpsecParcel from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.steps.constants import LAN_VPN_IPSEC -class LanInterfaceIpsecTemplateConverter: +class LanInterfaceIpsecConverter(FTConverter): supported_template_types = (LAN_VPN_IPSEC,) # Default Values diff --git a/catalystwan/utils/config_migration/converters/feature_template/lan/multilink.py b/catalystwan/utils/config_migration/converters/feature_template/lan/multilink.py index 3a194a17..56348b25 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/lan/multilink.py +++ b/catalystwan/utils/config_migration/converters/feature_template/lan/multilink.py @@ -9,6 +9,7 @@ MultilinkNimList, ) from catalystwan.models.configuration.feature_profile.sdwan.service.lan.multilink import InterfaceMultilinkParcel +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.converters.feature_template.model_definition_normalizer import ( flatten_datapaths, normalize_to_model_definition, @@ -16,7 +17,7 @@ from catalystwan.utils.config_migration.steps.constants import LAN_VPN_MULTILINK -class LanMultilinkTemplateConverter: +class LanMultilinkConverter(FTConverter): supported_template_types = (LAN_VPN_MULTILINK,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceMultilinkParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/lan/svi.py b/catalystwan/utils/config_migration/converters/feature_template/lan/svi.py index 12692985..0e877479 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/lan/svi.py +++ b/catalystwan/utils/config_migration/converters/feature_template/lan/svi.py @@ -11,15 +11,16 @@ VrrpIPv4, VrrpIPv4SecondaryAddress, ) -from catalystwan.utils.config_migration.steps.constants import LAN_VPN_SVI +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter +from catalystwan.utils.config_migration.steps.constants import LAN_VPN_SVI, WAN_VPN_SVI -class InterfaceSviTemplateConverter: +class InterfaceSviConverter(FTConverter): """ A class for converting template values into a InterfaceSviParcel object. """ - supported_template_types = (LAN_VPN_SVI,) + supported_template_types = (LAN_VPN_SVI, WAN_VPN_SVI) delete_keys = ( "if_name", diff --git a/catalystwan/utils/config_migration/converters/feature_template/logging_.py b/catalystwan/utils/config_migration/converters/feature_template/logging_.py index ec02f24e..cea83419 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/logging_.py +++ b/catalystwan/utils/config_migration/converters/feature_template/logging_.py @@ -5,8 +5,10 @@ from catalystwan.models.configuration.feature_profile.sdwan.system import LoggingParcel from catalystwan.models.configuration.feature_profile.sdwan.system.logging_parcel import CypherSuite +from .base import FTConverter -class LoggingTemplateConverter: + +class LoggingConverter(FTConverter): supported_template_types = ("cisco_logging", "logging") def create_parcel(self, name: str, description: str, template_values: dict) -> LoggingParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/multicast.py b/catalystwan/utils/config_migration/converters/feature_template/multicast.py index 4d2dadb2..07f42e53 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/multicast.py +++ b/catalystwan/utils/config_migration/converters/feature_template/multicast.py @@ -23,8 +23,10 @@ StaticRpAddress, ) +from .base import FTConverter -class MulticastToMulticastTemplateConverter: + +class MulticastToMulticastConverter(FTConverter): """This is corner case. Multicast Parcel is not a direct conversion from template. It is a combination of multiple templates. @@ -67,7 +69,7 @@ def get_example_payload(self): } -class PimToMulticastTemplateConverter: +class PimToMulticastConverter(FTConverter): """This is corner case. Multicast Parcel is not a direct conversion from template. It is a combination of multiple templates. @@ -245,7 +247,7 @@ def get_example_payload(self): } -class IgmpToMulticastTemplateConverter: +class IgmpToMulticastConverter(FTConverter): """This is corner case. Multicast Parcel is not a direct conversion from template. It is a combination of multiple templates. diff --git a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py index e049742f..96503e21 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py +++ b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py @@ -1,4 +1,5 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates +import logging from ipaddress import AddressValueError, IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from typing import List, Optional, Union, get_args @@ -103,6 +104,8 @@ Variable, ] +logger = logging.getLogger(__name__) + def to_snake_case(s: str) -> str: """Converts a string from kebab-case to snake_case.""" @@ -111,7 +114,10 @@ def to_snake_case(s: str) -> str: def cast_value_to_global(value: Union[DeviceVariable, str, int, List[str], List[int]]) -> CastedTypes: if isinstance(value, DeviceVariable): - return as_variable(value=convert_varname(value.name)) + converted_name = convert_varname(value.name) + if converted_name != value.name: + logger.info(f"Converted variable name: {value.name} -> {converted_name}") + return as_variable(value=converted_name) if isinstance(value, list): value_type = Global[List[int]] if isinstance(value[0], int) else Global[List[str]] return value_type(value=value) # type: ignore diff --git a/catalystwan/utils/config_migration/converters/feature_template/ntp.py b/catalystwan/utils/config_migration/converters/feature_template/ntp.py index a732c6d6..bce851bb 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ntp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ntp.py @@ -10,8 +10,10 @@ ) from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none +from .base import FTConverter -class NtpTemplateConverter: + +class NtpConverter(FTConverter): supported_template_types = ("cisco_ntp", "ntp") def create_parcel(self, name: str, description: str, template_values: dict) -> NtpParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/omp.py b/catalystwan/utils/config_migration/converters/feature_template/omp.py index cc5dad83..efe6ee18 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/omp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/omp.py @@ -4,8 +4,10 @@ from catalystwan.api.configuration_groups.parcel import Global, as_default, as_global from catalystwan.models.configuration.feature_profile.sdwan.system import OMPParcel +from .base import FTConverter -class OMPTemplateConverter: + +class OMPConverter(FTConverter): supported_template_types = ("cisco_omp", "omp-vedge", "omp-vsmart") def create_parcel(self, name: str, description: str, template_values: dict) -> OMPParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/ospf.py b/catalystwan/utils/config_migration/converters/feature_template/ospf.py index 246b2dcb..b58b4db6 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ospf.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ospf.py @@ -14,8 +14,10 @@ ) from catalystwan.utils.config_migration.steps.constants import LAN_OSPF, WAN_OSPF +from .base import FTConverter -class OspfTemplateConverter: + +class OspfConverter(FTConverter): supported_template_types = (LAN_OSPF, WAN_OSPF) delete_keys = ("max_metric", "timers", "distance", "auto_cost", "default_information", "compatible") diff --git a/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py b/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py index 6f794961..ac44006d 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py @@ -1,3 +1,4 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates # Copyright 2023 Cisco Systems, Inc. and its affiliates from copy import deepcopy from typing import List, Optional, Type, Union, get_args @@ -30,26 +31,11 @@ from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException from catalystwan.utils.config_migration.steps.constants import LAN_OSPFV3, WAN_OSPFV3 +from .base import FTConverter -class Ospfv3TemplateConverter: - """ - Warning: This class returns a tuple of RoutingOspfv3IPv4Parcel and RoutingOspfv3IPv6Parcel objects, - because the Feature Template has two definitions inside one for IPv4 and one for IPv6. - """ - supported_template_types = (WAN_OSPFV3, LAN_OSPFV3) - - def create_parcel( - self, name: str, description: str, template_values: dict - ) -> List[Union[RoutingOspfv3IPv4Parcel, RoutingOspfv3IPv6Parcel]]: - if template_values.get("ospfv3") is None: - raise CatalystwanConverterCantConvertException("Feature Template does not contain OSPFv3 configuration") - ospfv3ipv4 = Ospfv3Ipv4TemplateSubconverter().create_parcel(name, description, template_values) - ospfv3ipv6 = Ospfv3Ipv6TemplateSubconverter().create_parcel(name, description, template_values) - return [ospfv3ipv4, ospfv3ipv6] - - -class BaseOspfv3TemplateSubconverter: +class BaseOspfv3Converter(FTConverter): + supported_template_types = (LAN_OSPFV3, WAN_OSPFV3) name_suffix: str key_address_family: str key_distance: str @@ -208,7 +194,7 @@ def cleanup_keys(self, values: dict) -> None: values.pop(key, None) -class Ospfv3Ipv4TemplateSubconverter(BaseOspfv3TemplateSubconverter): +class Ospfv3Ipv4Converter(BaseOspfv3Converter): name_suffix = "_IPV4" key_address_family = "ipv4" key_distance = "distance_ipv4" @@ -248,7 +234,7 @@ def configure_redistribute(self, values: dict) -> None: values["redistribute"] = redistribute_list -class Ospfv3Ipv6TemplateSubconverter(BaseOspfv3TemplateSubconverter): +class Ospfv3Ipv6Converter(BaseOspfv3Converter): name_suffix = "_IPV6" key_address_family = "ipv6" key_distance = "distance_ipv6" 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 46e4ddbf..638348ed 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py +++ b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py @@ -1,112 +1,107 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates # Copyright 2023 Cisco Systems, Inc. and its affiliates import json import logging -from typing import Any, Callable, Dict, List, Union, cast +from typing import Any, Callable, Dict, Optional, cast from catalystwan.api.template_api import FeatureTemplateInformation -from catalystwan.exceptions import CatalystwanException +from catalystwan.models.configuration.config_migration import ConvertResult from catalystwan.models.configuration.feature_profile.parcel import AnyParcel -from catalystwan.utils.config_migration.converters.feature_template.lan.multilink import LanMultilinkTemplateConverter +from catalystwan.utils.config_migration.converters.feature_template.lan.multilink import LanMultilinkConverter from catalystwan.utils.feature_template.find_template_values import find_template_values -from .aaa import AAATemplateConverter -from .appqoe import AppqoeTemplateConverter -from .banner import BannerTemplateConverter -from .base import FeatureTemplateConverter -from .basic import SystemToBasicTemplateConverter -from .bfd import BFDTemplateConverter -from .bgp import BgpRoutingTemplateConverter -from .cellular_controller import CellularControllerTemplateConverter -from .cellular_profile import CellularProfileTemplateConverter -from .cli import CliTemplateConverter -from .dhcp import DhcpTemplateConverter -from .ethernet import ManagementInterfaceEthernetTemplateConverter -from .global_ import GlobalTemplateConverter -from .gps import GpsTemplateConverter -from .lan.ethernet import LanInterfaceEthernetTemplateConverter -from .lan.gre import LanInterfaceGreTemplateConverter -from .lan.ipsec import LanInterfaceIpsecTemplateConverter -from .lan.svi import InterfaceSviTemplateConverter -from .logging_ import LoggingTemplateConverter -from .multicast import ( - IgmpToMulticastTemplateConverter, - MulticastToMulticastTemplateConverter, - PimToMulticastTemplateConverter, -) +from .aaa import AAAConverter +from .appqoe import AppqoeConverter +from .banner import BannerConverter +from .base import FTConverter +from .basic import SystemToBasicConverter +from .bfd import BFDConverter +from .bgp import BgpRoutingConverter +from .cellular_controller import CellularControllerConverter +from .cellular_profile import CellularProfileConverter +from .cli import CliConverter +from .dhcp import DhcpConverter +from .ethernet import ManagementInterfaceEthernetConverter +from .global_ import GlobalConverter +from .gps import GpsConverter +from .lan.ethernet import LanInterfaceEthernetConverter +from .lan.gre import LanInterfaceGreConverter +from .lan.ipsec import LanInterfaceIpsecConverter +from .lan.svi import InterfaceSviConverter +from .logging_ import LoggingConverter +from .multicast import IgmpToMulticastConverter, MulticastToMulticastConverter, PimToMulticastConverter from .normalizer import template_values_normalization -from .ntp import NtpTemplateConverter -from .omp import OMPTemplateConverter -from .ospf import OspfTemplateConverter -from .ospfv3 import Ospfv3TemplateConverter -from .security import SecurityTemplateConverter -from .sig import SIGTemplateConverter -from .snmp import SNMPTemplateConverter -from .switchport import SwitchportTemplateConverter -from .thousandeyes import ThousandEyesTemplateConverter -from .ucse import UcseTemplateConverter -from .vpn import VpnTemplateConverter -from .wan.cellular import InterfaceCellularTemplateConverter -from .wan.ethernet import WanInterfaceEthernetTemplateConverter -from .wan.gre import WanInterfaceGreTemplateConverter -from .wan.ipsec import WanInterfaceIpsecTemplateConverter -from .wan.multilink import WanMultilinkTemplateConverter +from .ntp import NtpConverter +from .omp import OMPConverter +from .ospf import OspfConverter +from .security import SecurityConverter +from .sig import SIGConverter +from .snmp import SNMPConverter +from .switchport import SwitchportConverter +from .thousandeyes import ThousandEyesConverter +from .ucse import UcseConverter +from .vpn import VpnConverter +from .wan.cellular import InterfaceCellularConverter +from .wan.ethernet import WanInterfaceEthernetConverter +from .wan.gre import WanInterfaceGreConverter +from .wan.ipsec import WanInterfaceIpsecConverter +from .wan.multilink import WanMultilinkConverter from .wan.protocol_over import ( - InterfaceDslIPoETemplateConverter, - InterfaceDslPppoaTemplateConverter, - InterfaceDslPppoeTemplateConverter, - InterfaceEthernetPppoeTemplateConverter, + InterfaceDslIPoEConverter, + InterfaceDslPppoaConverter, + InterfaceDslPppoeConverter, + InterfaceEthernetPppoeConverter, ) -from .wan.t1e1serial import T1E1SerialTemplateConverter -from .wireless_lan import WirelessLanTemplateConverter +from .wan.t1e1serial import T1E1SerialConverter +from .wireless_lan import WirelessLanConverter logger = logging.getLogger(__name__) available_converters = [ - AAATemplateConverter, - BannerTemplateConverter, - SecurityTemplateConverter, - SystemToBasicTemplateConverter, - BFDTemplateConverter, - GlobalTemplateConverter, - LoggingTemplateConverter, - OMPTemplateConverter, - NtpTemplateConverter, - BgpRoutingTemplateConverter, - ThousandEyesTemplateConverter, - UcseTemplateConverter, - DhcpTemplateConverter, - SNMPTemplateConverter, - AppqoeTemplateConverter, - VpnTemplateConverter, - LanInterfaceGreTemplateConverter, - LanMultilinkTemplateConverter, - InterfaceSviTemplateConverter, - LanInterfaceEthernetTemplateConverter, - LanInterfaceIpsecTemplateConverter, - OspfTemplateConverter, - Ospfv3TemplateConverter, - SwitchportTemplateConverter, - MulticastToMulticastTemplateConverter, - PimToMulticastTemplateConverter, - IgmpToMulticastTemplateConverter, - WirelessLanTemplateConverter, - T1E1SerialTemplateConverter, - InterfaceEthernetPppoeTemplateConverter, - InterfaceDslPppoeTemplateConverter, - InterfaceDslPppoaTemplateConverter, - InterfaceDslIPoETemplateConverter, - LanInterfaceGreTemplateConverter, - WanInterfaceGreTemplateConverter, - WanInterfaceIpsecTemplateConverter, - InterfaceCellularTemplateConverter, - GpsTemplateConverter, - CellularControllerTemplateConverter, - CellularProfileTemplateConverter, - CliTemplateConverter, - WanInterfaceEthernetTemplateConverter, - SIGTemplateConverter, - WanMultilinkTemplateConverter, - ManagementInterfaceEthernetTemplateConverter, + AAAConverter, + BannerConverter, + SecurityConverter, + SystemToBasicConverter, + BFDConverter, + GlobalConverter, + LoggingConverter, + OMPConverter, + NtpConverter, + BgpRoutingConverter, + ThousandEyesConverter, + UcseConverter, + DhcpConverter, + SNMPConverter, + AppqoeConverter, + VpnConverter, + LanInterfaceGreConverter, + LanMultilinkConverter, + InterfaceSviConverter, + LanInterfaceEthernetConverter, + LanInterfaceIpsecConverter, + OspfConverter, + SwitchportConverter, + MulticastToMulticastConverter, + PimToMulticastConverter, + IgmpToMulticastConverter, + WirelessLanConverter, + T1E1SerialConverter, + InterfaceEthernetPppoeConverter, + InterfaceDslPppoeConverter, + InterfaceDslPppoaConverter, + InterfaceDslIPoEConverter, + LanInterfaceGreConverter, + WanInterfaceGreConverter, + WanInterfaceIpsecConverter, + InterfaceCellularConverter, + GpsConverter, + CellularControllerConverter, + CellularProfileConverter, + CliConverter, + WanInterfaceEthernetConverter, + SIGConverter, + WanMultilinkConverter, + ManagementInterfaceEthernetConverter, ] @@ -115,7 +110,7 @@ } -def choose_parcel_converter(template_type: str) -> Callable[..., FeatureTemplateConverter]: +def choose_parcel_converter(template_type: str) -> Optional[Callable[..., FTConverter]]: """ This function is used to choose the correct parcel factory based on the template type. @@ -123,35 +118,49 @@ def choose_parcel_converter(template_type: str) -> Callable[..., FeatureTemplate template_type (str): The template type used to determine the correct factory. Returns: - BaseFactory: The chosen parcel factory. - - Raises: - ValueError: If the template type is not supported. + BaseFactory | None: The chosen parcel factory or None if there is no supported template type. """ for key in supported_parcel_converters.keys(): if template_type in key: converter = supported_parcel_converters[key] logger.debug(f"Choosen converter {converter} based on template type {template_type}") return converter - raise CatalystwanException(f"Template type {template_type} not supported") + logger.warning(f"Template type {template_type} not supported") + return None + + +def unsupported_type(template_type: str) -> ConvertResult[AnyParcel]: + return ConvertResult(status="unsupported", output=None, info=[f"Template type {template_type} not supported"]) -def create_parcel_from_template(template: FeatureTemplateInformation) -> Union[AnyParcel, List[AnyParcel]]: +def extract_template_values(template_definiton: str) -> Dict[str, Any]: + """Extracts the template values from the template definition and create easy to consume dictionary.""" + template_definition_as_dict = json.loads(cast(str, template_definiton)) + template_values = find_template_values(template_definition_as_dict) + template_values_normalized = template_values_normalization(template_values) + return template_values_normalized + + +def convert(converter: FTConverter, template: FeatureTemplateInformation) -> ConvertResult[AnyParcel]: + definition = template.template_definiton + if definition is None: + return ConvertResult(status="failed", output=None, info=["Template definition is empty"]) + template_values = extract_template_values(definition) + return converter.convert(template.name, template.description, template_values) + + +def create_parcel_from_template(template: FeatureTemplateInformation) -> ConvertResult[AnyParcel]: """ - Creates a new instance of a _ParcelBase based on the given template. + Creates a new instance of a ConvertResult[AnyParcel] based on the given template. Args: - template (FeatureTemplateInformation): The template to use for creating the _ParcelBase instance. + template (FeatureTemplateInformation): The template to use for creating the AnyParcel instance. Returns: - _ParcelBase: The created _ParcelBase instance. - - Raises: - ValueError: If the given template type is not supported. + ConvertResult[AnyParcel]: The convert result of the operation. """ - converter = choose_parcel_converter(template.template_type)() - template_definition_as_dict = json.loads(cast(str, template.template_definiton)) - template_values = find_template_values(template_definition_as_dict) - template_values_normalized = template_values_normalization(template_values) - logger.debug(f"Normalized template {template.name}: {template_values_normalized}") - return converter.create_parcel(template.name, template.description, template_values_normalized) + converter_class = choose_parcel_converter(template.template_type) + if converter_class is None: + return unsupported_type(template.template_type) + converter = converter_class() + return convert(converter, template) diff --git a/catalystwan/utils/config_migration/converters/feature_template/security.py b/catalystwan/utils/config_migration/converters/feature_template/security.py index c080efc1..ba27fd2c 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/security.py +++ b/catalystwan/utils/config_migration/converters/feature_template/security.py @@ -1,11 +1,14 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates from typing import List from catalystwan.api.configuration_groups.parcel import Global, as_default from catalystwan.models.configuration.feature_profile.sdwan.system import SecurityParcel from catalystwan.models.configuration.feature_profile.sdwan.system.security import IntegrityType +from .base import FTConverter -class SecurityTemplateConverter: + +class SecurityConverter(FTConverter): """ A class for converting template values into a SecurityParcel object. @@ -20,8 +23,7 @@ class SecurityTemplateConverter: "security-vedge", ) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> SecurityParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> SecurityParcel: """ Creates a SecurityParcel object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/sig.py b/catalystwan/utils/config_migration/converters/feature_template/sig.py index 9a1fd18a..28950cc2 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/sig.py +++ b/catalystwan/utils/config_migration/converters/feature_template/sig.py @@ -16,8 +16,10 @@ normalize_to_model_definition, ) +from .base import FTConverter -class SIGTemplateConverter: + +class SIGConverter(FTConverter): supported_template_types = ("secure-internet-gateway", "cisco_secure_internet_gateway") PROVIDER_MAP = { diff --git a/catalystwan/utils/config_migration/converters/feature_template/snmp.py b/catalystwan/utils/config_migration/converters/feature_template/snmp.py index 2e06da51..47b010d3 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/snmp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/snmp.py @@ -1,13 +1,16 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates import logging from copy import deepcopy from catalystwan.api.configuration_groups.parcel import Variable, as_global, as_variable from catalystwan.models.configuration.feature_profile.sdwan.system.snmp import Authorization, SNMPParcel +from .base import FTConverter + logger = logging.getLogger(__name__) -class SNMPTemplateConverter: +class SNMPConverter(FTConverter): """ A class for converting template values into a SecurityParcel object. @@ -21,15 +24,15 @@ class SNMPTemplateConverter: def create_parcel(self, name: str, description: str, template_values: dict) -> SNMPParcel: """ - Creates a SecurityParcel object based on the provided template values. + Creates a SNMPParcel object based on the provided template values. Args: - name (str): The name of the SecurityParcel. - description (str): The description of the SecurityParcel. + name (str): The name of the SNMPParcel. + description (str): The description of the SNMPParcel. template_values (dict): A dictionary containing the template values. Returns: - SecurityParcel: A SecurityParcel object with the provided template values. + SNMPParcel: A SNMPParcel object with the provided template values. """ values = deepcopy(template_values) self.configure_community(values) @@ -52,4 +55,5 @@ def configure_target(self, values: dict) -> None: logger.info( f"OID ID is not set, using device specific variable {self.default_view_oid_id.format(i + 1)}" ) + oid["id"] = id_ diff --git a/catalystwan/utils/config_migration/converters/feature_template/switchport.py b/catalystwan/utils/config_migration/converters/feature_template/switchport.py index ed86bf3b..76465f6e 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/switchport.py +++ b/catalystwan/utils/config_migration/converters/feature_template/switchport.py @@ -10,8 +10,10 @@ SwitchportParcel, ) +from .base import FTConverter -class SwitchportTemplateConverter: + +class SwitchportConverter(FTConverter): supported_template_types = ("switchport",) delete_keys = ("slot", "module", "subslot") diff --git a/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py b/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py index a16faada..78b34b0c 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py +++ b/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py @@ -9,8 +9,10 @@ ProxyConfigStatic, ) +from .base import FTConverter -class ThousandEyesTemplateConverter: + +class ThousandEyesConverter(FTConverter): """ A class for converting template values into a ThousandEyesParcel object. """ diff --git a/catalystwan/utils/config_migration/converters/feature_template/ucse.py b/catalystwan/utils/config_migration/converters/feature_template/ucse.py index e719662b..071d2b48 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ucse.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ucse.py @@ -4,8 +4,10 @@ from catalystwan.models.configuration.feature_profile.sdwan.other import UcseParcel from catalystwan.models.configuration.feature_profile.sdwan.other.ucse import LomType +from .base import FTConverter -class UcseTemplateConverter: + +class UcseConverter(FTConverter): """ A class for converting template values into a UcseParcel object. """ diff --git a/catalystwan/utils/config_migration/converters/feature_template/vpn.py b/catalystwan/utils/config_migration/converters/feature_template/vpn.py index 85559f14..3fda72a9 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/vpn.py +++ b/catalystwan/utils/config_migration/converters/feature_template/vpn.py @@ -2,7 +2,7 @@ import logging from copy import deepcopy from ipaddress import IPv4Interface, IPv6Interface -from typing import Dict, List, Literal, Optional, Tuple, Type, Union +from typing import Callable, Dict, List, Literal, Optional, Tuple, Type, Union from pydantic import BaseModel @@ -69,6 +69,8 @@ from catalystwan.models.configuration.feature_profile.sdwan.transport.vpn import TransportVpnParcel from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none +from .base import FTConverter + logger = logging.getLogger(__name__) @@ -88,7 +90,9 @@ class OmpMappingItem(BaseModel): ux2_field: Literal["omp_advertise_ipv4", "omp_advertise_ipv6"] -class BaseTransportAndManagementTemplateConverter: +class BaseTransportAndManagementConverter(FTConverter): + supported_template_types = ("cisco_vpn", "vpn-vedge", "vpn-vsmart") + def parse_host_mapping(self, values: dict) -> Optional[List[HostMapping]]: hosts = values.get("host", []) if not hosts: @@ -197,7 +201,7 @@ def parse_route_ipv6(self, values: dict) -> Optional[List[Ipv6RouteItem]]: return static_routes -class ManagementVpnTemplateConverter(BaseTransportAndManagementTemplateConverter): +class ManagementVpnConverter(BaseTransportAndManagementConverter): def create_parcel(self, name: str, description: str, template_values: dict) -> ManagementVpnParcel: """ Creates a ManagementVpnParcel parcel. @@ -229,7 +233,7 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> M return ManagementVpnParcel(parcel_name=name, parcel_description=description, **payload) -class TransportVpnTemplateConverter(BaseTransportAndManagementTemplateConverter): +class TransportVpnConverter(BaseTransportAndManagementConverter): def create_parcel(self, name: str, description: str, template_values: dict) -> TransportVpnParcel: """ Creates a TransportVpnParcel parcel. @@ -274,11 +278,13 @@ def parse_service(self, services: List[Dict]) -> Optional[List[ServiceItem]]: ] -class ServiceVpnTemplateConverter: +class ServiceVpnConverter(FTConverter): """ A class for converting template values into a LanVpnParcel object. """ + supported_template_types = ("cisco_vpn", "vpn-vedge", "vpn-vsmart") + route_leaks_mapping = { "route_import": RouteLeakMappingItem(ux2_model=RouteLeakFromGlobal, ux2_field="route_leak_from_global"), "route_export": RouteLeakMappingItem(ux2_model=RouteLeakFromService, ux2_field="route_leak_from_service"), @@ -718,7 +724,7 @@ def _parse_leak(self, values: dict, route_leaks: list, pydantic_model, pydantic_ values[pydantic_field] = items -class VpnTemplateConverter: +class VpnConverter(FTConverter): supported_template_types = ("cisco_vpn", "vpn-vedge", "vpn-vsmart") def create_parcel( @@ -736,12 +742,18 @@ def create_parcel( VPN: The created VPN object. """ - if 0 == self.get_vpn_id(template_values): - return TransportVpnTemplateConverter().create_parcel(name, description, template_values) - elif 512 == self.get_vpn_id(template_values): - return ManagementVpnTemplateConverter().create_parcel(name, description, template_values) - else: - return ServiceVpnTemplateConverter().create_parcel(name, description, template_values) + vpn_converters: Dict[ + int, Callable[..., Union[TransportVpnConverter, ManagementVpnConverter, ServiceVpnConverter]] + ] = { + 0: TransportVpnConverter, + 512: ManagementVpnConverter, + } + vpn_id = self.get_vpn_id(template_values) + converter = vpn_converters.get(vpn_id, ServiceVpnConverter)() + parcel = converter.create_parcel(name, description, template_values) + self._convert_result.info = converter._convert_result.info + self._convert_result.status = converter._convert_result.status + return parcel def get_vpn_id(self, values: dict) -> int: return int(values["vpn_id"].value) diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/cellular.py b/catalystwan/utils/config_migration/converters/feature_template/wan/cellular.py index f648b187..e01cb6fb 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/cellular.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/cellular.py @@ -12,10 +12,11 @@ NatAttributesIpv4, Tunnel, ) +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none -class InterfaceCellularTemplateConverter: +class InterfaceCellularConverter(FTConverter): supported_template_types = ("vpn-vedge-interface-cellular", "vpn-cedge-interface-cellular") def create_parcel(self, name: str, description: str, template_values: Dict) -> InterfaceCellularParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/ethernet.py b/catalystwan/utils/config_migration/converters/feature_template/wan/ethernet.py index 47aa7b5e..ac75dabb 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/ethernet.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/ethernet.py @@ -29,6 +29,7 @@ TlocExtensionGreFrom, Tunnel, ) +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none from catalystwan.utils.config_migration.converters.utils import parse_interface_name from catalystwan.utils.config_migration.steps.constants import WAN_VPN_ETHERNET @@ -36,14 +37,14 @@ logger = logging.getLogger(__name__) -class WanInterfaceEthernetTemplateConverter: +class WanInterfaceEthernetConverter(FTConverter): supported_template_types = (WAN_VPN_ETHERNET,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceEthernetParcel: data = deepcopy(template_values) encapsulation = self.parse_encapsulations(data.get("tunnel_interface", {}).get("encapsulation", [])) - interface_name = parse_interface_name(data) + interface_name = parse_interface_name(self, data) interface_description = data.get( "description", as_global(description) ) # Edge case where model doesn't have description but its required diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/gre.py b/catalystwan/utils/config_migration/converters/feature_template/wan/gre.py index 1f8c4fd8..500f5d02 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/gre.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/gre.py @@ -13,10 +13,11 @@ ) from catalystwan.models.configuration.feature_profile.sdwan.transport.wan.interface.gre import Basic, InterfaceGreParcel from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.steps.constants import WAN_VPN_GRE -class WanInterfaceGreTemplateConverter: +class WanInterfaceGreConverter(FTConverter): supported_template_types = (WAN_VPN_GRE,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceGreParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/ipsec.py b/catalystwan/utils/config_migration/converters/feature_template/wan/ipsec.py index 8ea80ce9..f8a94198 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/ipsec.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/ipsec.py @@ -1,3 +1,4 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates from copy import deepcopy from ipaddress import IPv6Address from typing import Optional, Union @@ -10,11 +11,12 @@ PerfectForwardSecrecy, ) from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.converters.feature_template.helpers import create_dict_without_none from catalystwan.utils.config_migration.steps.constants import WAN_VPN_IPSEC -class WanInterfaceIpsecTemplateConverter: +class WanInterfaceIpsecConverter(FTConverter): supported_template_types = (WAN_VPN_IPSEC,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceIpsecParcel: @@ -141,4 +143,7 @@ def parse_ike_group(self, data: dict) -> Optional[Global[str]]: def parse_tracker(self, data: dict) -> Default[None]: # TODO: Implement tracker + self._convert_result.update_status( + "partial", "Tracker is set as default value. It should be implemented in the future." + ) return Default[None](value=None) diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/multilink.py b/catalystwan/utils/config_migration/converters/feature_template/wan/multilink.py index d0e20483..09c613f8 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/multilink.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/multilink.py @@ -12,6 +12,7 @@ from catalystwan.models.configuration.feature_profile.sdwan.transport.wan.interface.multilink import ( InterfaceMultilinkParcel, ) +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter from catalystwan.utils.config_migration.converters.feature_template.model_definition_normalizer import ( flatten_datapaths, normalize_to_model_definition, @@ -19,7 +20,7 @@ from catalystwan.utils.config_migration.steps.constants import WAN_VPN_MULTILINK -class WanMultilinkTemplateConverter: +class WanMultilinkConverter(FTConverter): supported_template_types = (WAN_VPN_MULTILINK,) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceMultilinkParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/protocol_over.py b/catalystwan/utils/config_migration/converters/feature_template/wan/protocol_over.py index 605d3a08..29186532 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/protocol_over.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/protocol_over.py @@ -34,6 +34,7 @@ VdslMode, ) from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter @dataclass @@ -43,7 +44,7 @@ class EncapsulationOption: weight: Optional[Global[int]] = None -class InterfaceBaseTemplateConverter: +class InterfaceBaseConverter(FTConverter): def parse_tunnel_advanced_option(self, data: dict) -> Optional[TunnelAdvancedOption]: tunnel = data.get("tunnel_interface", {}) if not tunnel: @@ -243,7 +244,7 @@ def parse_mode(self, mode: dict) -> Optional[Global[VdslMode]]: return None -class InterfaceEthernetPppoeTemplateConverter(InterfaceBaseTemplateConverter): +class InterfaceEthernetPppoeConverter(InterfaceBaseConverter): supported_template_types = ("vpn-interface-ethpppoe",) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceEthPPPoEParcel: @@ -276,7 +277,7 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> I ) -class InterfaceDslPppoeTemplateConverter(InterfaceBaseTemplateConverter): +class InterfaceDslPppoeConverter(InterfaceBaseConverter): supported_template_types = ("vpn-interface-pppoe",) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceDslPPPoEParcel: @@ -311,7 +312,7 @@ def create_parcel(self, name: str, description: str, template_values: dict) -> I ) -class InterfaceDslPppoaTemplateConverter(InterfaceBaseTemplateConverter): +class InterfaceDslPppoaConverter(InterfaceBaseConverter): supported_template_types = ("vpn-interface-pppoa",) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceDslPPPoAParcel: @@ -391,7 +392,7 @@ def parse_vbr_rt_config(self, vbr_rt: Dict) -> Optional[VbrRtConfig]: ) -class InterfaceDslIPoETemplateConverter(InterfaceBaseTemplateConverter): +class InterfaceDslIPoEConverter(InterfaceBaseConverter): supported_template_types = ("vpn-interface-ipoe",) def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceDslIPoEParcel: diff --git a/catalystwan/utils/config_migration/converters/feature_template/wan/t1e1serial.py b/catalystwan/utils/config_migration/converters/feature_template/wan/t1e1serial.py index c319c62a..d80b00cd 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wan/t1e1serial.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wan/t1e1serial.py @@ -9,9 +9,10 @@ T1E1SerialParcel, Tunnel, ) +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter -class T1E1SerialTemplateConverter: +class T1E1SerialConverter(FTConverter): supported_template_types = ("vpn-interface-t1-e1",) delete_keys = ( "description", 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 83430c2f..2d4f2f8a 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/wireless_lan.py +++ b/catalystwan/utils/config_migration/converters/feature_template/wireless_lan.py @@ -16,8 +16,10 @@ ) from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException +from .base import FTConverter -class WirelessLanTemplateConverter: + +class WirelessLanConverter(FTConverter): supported_template_types = ("cisco_wireless_lan",) delete_keys = ("radio", "mgmt") diff --git a/catalystwan/utils/config_migration/converters/utils.py b/catalystwan/utils/config_migration/converters/utils.py index ddaa7362..56ecb67a 100644 --- a/catalystwan/utils/config_migration/converters/utils.py +++ b/catalystwan/utils/config_migration/converters/utils.py @@ -1,12 +1,10 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates -import logging import re from typing import Dict, Union from catalystwan.api.configuration_groups.parcel import Global, Variable, as_global from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException - -logger = logging.getLogger(__name__) +from catalystwan.utils.config_migration.converters.feature_template.base import FTConverter NEGATIVE_VARIABLE_REGEX = re.compile(r"[^.\/\[\]a-zA-Z0-9_-]") @@ -24,15 +22,19 @@ def convert_interface_name(if_name: str) -> str: for key, value in INTERFACE_NAME_MAPPING.items(): if if_name.startswith(key): new_if_name = value + if_name[len(key) :] - logger.info(f"Converted interface name: {if_name} -> {new_if_name}") return new_if_name return if_name -def parse_interface_name(data: Dict) -> Union[Global[str], Variable]: +def parse_interface_name(converter: FTConverter, data: Dict) -> Union[Global[str], Variable]: if_name = data.get("if_name") if isinstance(if_name, Variable): return if_name elif isinstance(if_name, Global): - return as_global(convert_interface_name(if_name.value)) + converted_if_name = convert_interface_name(if_name.value) + if converted_if_name != if_name.value: + converter._convert_result.update_status( + "partial", f"Converted interface name: {if_name.value} -> {converted_if_name}" + ) + return as_global(converted_if_name) raise CatalystwanConverterCantConvertException("Interface name is required") diff --git a/catalystwan/utils/config_migration/steps/transform.py b/catalystwan/utils/config_migration/steps/transform.py index 1e263266..a5616f72 100644 --- a/catalystwan/utils/config_migration/steps/transform.py +++ b/catalystwan/utils/config_migration/steps/transform.py @@ -1,24 +1,40 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates # Copyright 2023 Cisco Systems, Inc. and its affiliates import logging -from typing import List, Set, Tuple, cast +from typing import List, Optional, Set, Tuple, cast from uuid import UUID, uuid4 from catalystwan.api.templates.device_template.device_template import GeneralTemplate -from catalystwan.models.configuration.config_migration import TransformedParcel, TransformHeader, UX1Config, UX2Config +from catalystwan.models.configuration.config_migration import ( + ConfigTransformResult, + ConvertResult, + TransformedParcel, + TransformHeader, + UX1Config, + UX2Config, +) from catalystwan.models.configuration.feature_profile.sdwan.service.multicast import ( MulticastBasicAttributes, MulticastParcel, ) from catalystwan.models.templates import FeatureTemplateInformation -from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException -from catalystwan.utils.config_migration.converters.feature_template.parcel_factory import create_parcel_from_template +from catalystwan.utils.config_migration.converters.feature_template.ospfv3 import ( + Ospfv3Ipv4Converter, + Ospfv3Ipv6Converter, +) +from catalystwan.utils.config_migration.converters.feature_template.parcel_factory import ( + convert, + create_parcel_from_template, +) from catalystwan.utils.config_migration.steps.constants import ( + LAN_OSPFV3, NO_SUBSTITUTE_ERROR, VPN_ADDITIONAL_TEMPLATES, VPN_MANAGEMENT, VPN_SERVICE, VPN_TEMPLATE_MAPPINGS, VPN_TRANSPORT, + WAN_OSPFV3, ) logger = logging.getLogger(__name__) @@ -107,7 +123,7 @@ def resolve_vpn_and_subtemplates_type(cisco_vpn_template: GeneralTemplate, ux1_c return set() # Determine the VPN type based on the VPN ID - vpn_id = create_parcel_from_template(target_feature_template).vpn_id.value # type: ignore + vpn_id = create_parcel_from_template(target_feature_template).output.vpn_id.value # type: ignore if vpn_id == 0: cisco_vpn_template.templateType = VPN_TRANSPORT elif vpn_id == 512: @@ -201,41 +217,79 @@ def create_feature_template_and_general_template_pairs( return pairs -def handle_multi_parcel_feature_template( - feature_template: FeatureTemplateInformation, ux2_config: UX2Config -) -> List[TransformedParcel]: - """ - Handle feature templates that produce multiple parcels. - """ - - parcels = create_parcel_from_template(feature_template) - if not isinstance(parcels, list): - # For type checker... - parcels = [parcels] - transformed_parcels = [] - for parcel in parcels: - transformed_parcels.append( - TransformedParcel( - header=TransformHeader( - type=parcel._get_parcel_type(), - origin=uuid4(), - ), - parcel=parcel, - ) +def add_result( + ux2: UX2Config, + transform_result: ConfigTransformResult, + ft: FeatureTemplateInformation, + result: ConvertResult, + subtemplates_mapping: dict, + provided_uuid: Optional[UUID] = None, +): + ft_template_uuid = UUID(ft.id) + if result.status == "unsupported": + transform_result.add_unsupported_item(name=ft.name, uuid=ft_template_uuid, type=ft.template_type) + elif result.status == "failed": + transform_result.add_failed_conversion_parcel( + exception_message=result.get_info(), + feature_template=ft, + ) + elif result.output is not None: + parcel = result.output + transformed_parcel = TransformedParcel( + header=TransformHeader( + type=parcel._get_parcel_type(), + origin=ft_template_uuid if provided_uuid is None else provided_uuid, + subelements=subtemplates_mapping[ft_template_uuid], + status=result.status, + info=result.info, + ), + parcel=parcel, ) + ux2.profile_parcels.append(transformed_parcel) + + +def convert_single_parcel_feature_template( + ux2: UX2Config, transform_result: ConfigTransformResult, ft: FeatureTemplateInformation, subtemplates_mapping: dict +) -> None: + result = create_parcel_from_template(ft) + add_result(ux2, transform_result, ft, result, subtemplates_mapping) + + +def convert_multi_parcel_feature_template( + ux2: UX2Config, transform_result: ConfigTransformResult, ft: FeatureTemplateInformation, subtemplates_mapping: dict +) -> None: + multi_parcel_converters = {(LAN_OSPFV3, WAN_OSPFV3): (Ospfv3Ipv4Converter, Ospfv3Ipv6Converter)} + converters = None + for template_types, mulit_converters in multi_parcel_converters.items(): + if ft.template_type in template_types: + converters = [c() for c in mulit_converters] + if converters is None: + transform_result.add_unsupported_item(ft.name, UUID(ft.id), ft.template_type) + return - parent_feature_template = next( - (p for p in ux2_config.profile_parcels if UUID(feature_template.id) in p.header.subelements), None - ) + results = [convert(converter, ft) for converter in converters] + if any(r.status == "failed" for r in results): + transform_result.add_failed_conversion_parcel( + exception_message="One or more feature templates failed to convert.\n" + f"{[r.get_info() for r in results if r.status == 'failed']}", + feature_template=ft, + ) + return - if parent_feature_template: - parent_feature_template.header.subelements.remove(UUID(feature_template.id)) - for transformed_parcel in transformed_parcels: - parent_feature_template.header.subelements.add(transformed_parcel.header.origin) - else: - raise CatalystwanConverterCantConvertException(f"Parent parcel for {feature_template.name} not found.") + parent_feature_template = next((p for p in ux2.profile_parcels if UUID(ft.id) in p.header.subelements), None) + if not parent_feature_template: + transform_result.add_failed_conversion_parcel( + exception_message=f"Parent parcel for {ft.name} not found.", + feature_template=ft, + ) + return - return transformed_parcels + parent_feature_template.header.subelements.remove(UUID(ft.id)) + for r in results: + generated_origin = uuid4() + logger.debug(f"Generated origin for {ft.name} is {generated_origin}") + add_result(ux2, transform_result, ft, r, subtemplates_mapping, provided_uuid=generated_origin) + parent_feature_template.header.subelements.add(generated_origin) def remove_unused_feature_templates(ux1: UX1Config, used_feature_templates: Set[str]): diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 5d4d36d5..c5669e9b 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -1,3 +1,4 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates # Copyright 2023 Cisco Systems, Inc. and its affiliates import logging from collections import defaultdict @@ -24,12 +25,9 @@ VersionInfo, ) from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload -from catalystwan.models.configuration.feature_profile.parcel import AnyParcel from catalystwan.models.configuration.topology_group import TopologyGroup from catalystwan.models.policy import AnyPolicyDefinitionInfo from catalystwan.session import ManagerSession -from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException -from catalystwan.utils.config_migration.converters.feature_template import create_parcel_from_template from catalystwan.utils.config_migration.converters.feature_template.cloud_credentials import ( create_cloud_credentials_from_templates, ) @@ -58,7 +56,8 @@ WAN_VPN_MULTILINK, ) from catalystwan.utils.config_migration.steps.transform import ( - handle_multi_parcel_feature_template, + convert_multi_parcel_feature_template, + convert_single_parcel_feature_template, merge_parcels, remove_unused_feature_templates, resolve_vpn_and_subtemplates_type, @@ -359,35 +358,12 @@ def transform(ux1: UX1Config, add_suffix: bool = True) -> ConfigTransformResult: cloud_credential_templates = [] for ft in ux1.templates.feature_templates: - if ft.template_type in SUPPORTED_TEMPLATE_TYPES: - try: - if ft.template_type in MULTI_PARCEL_FEATURE_TEMPLATES: - transformed_parcels = handle_multi_parcel_feature_template(ft, ux2) - ux2.profile_parcels.extend(transformed_parcels) - else: - parcel = cast(AnyParcel, create_parcel_from_template(ft)) - ft_template_uuid = UUID(ft.id) - transformed_parcel = TransformedParcel( - header=TransformHeader( - type=parcel._get_parcel_type(), - origin=ft_template_uuid, - subelements=subtemplates_mapping[ft_template_uuid], - ), - parcel=parcel, - ) - # Add to UX2. We can indentify the parcels as subelements of the feature profiles by the UUIDs - ux2.profile_parcels.append(transformed_parcel) - except (CatalystwanConverterCantConvertException, ValidationError, Exception) as e: - exception_message = f"Feature Template ({ft.name})[{ft.template_type}] conversion error: {e}." - logger.warning(exception_message) - ft.device_type = [""] # This takes to much space in the logs - transform_result.add_failed_conversion_parcel( - exception_message=exception_message, - feature_template=ft, - ) - - elif ft.template_type in CLOUD_CREDENTIALS_FEATURE_TEMPLATES: + if ft.template_type in CLOUD_CREDENTIALS_FEATURE_TEMPLATES: cloud_credential_templates.append(ft) + elif ft.template_type in MULTI_PARCEL_FEATURE_TEMPLATES: + convert_multi_parcel_feature_template(ux2, transform_result, ft, subtemplates_mapping) + else: + convert_single_parcel_feature_template(ux2, transform_result, ft, subtemplates_mapping) # Add Cloud Credentials to UX2 if cloud_credential_templates: