From 6cd98924d8b9d9e23b98cc17c8b939f98ae012d7 Mon Sep 17 00:00:00 2001 From: Szymon Basan <116343782+sbasan@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:38:01 +0200 Subject: [PATCH] Dev/service parcels (#2) * Start work on Service models * Service VPN passes for the internal vManage * Refactor Service VPN converter. Instantiate the converter in the create_parcel_from_template function. Convert the create_parcel methods to instance methods, allowing them to access class attributes and methods. Split large create_parcel functions * Add Interface GRE model. Add unittests. Add integration tests. Change feature profile integration test structure. Add more Castable literals to the normalizer. Change name factory method to parcel_factory. Change VPN model type to lan/vpn. * Add SVI Interface model. Add unit tests and integration test for the moedl creation. Remove static UUID in tests, instead create dynamicly. Add IPv6Interface and IPv4Interface cast in normalizer. Change models to use casted Global[Interface] values. Fix Svi model * Add Ethernet Interface. Add Unit tests. Add integratio tests. Fix VPN for sdwan demo data. * Rename Feature Profile builder file. Use from typing_extensions import Annotated * Add IPSEC interface model. Add unittests. Add integration tests. * Add Ethernet interface. Add Builders for other and system profile. Improve logging during pushing ux2 config. Minor fixes for service models. Refactor config pusher. Add pushing service FP and parcels. Working for internal vmanage * Fixes * Add default=None for Optional fields. * Add missing imports * Prepare the rest service parcels for converter * Fix add default=None for optional field * Add OSPF converter * Add OSPF intergration test * Add OSPFv3IP4 converter. * Add ospfv3ipv6 converter. add unittest add integration tests * Add ospf model to transform. Add service feature profile in transform for creation. Add default values for ospf model (helps whe default template is empty and endpoint needs values). Comment logic for interface assigement since there can be many vpns and interfaces in one feature profile correct implementation is needed * update deprecated github actions (#544) * update deprecated github actions * fix type error * Remove annotation * Log feature templates that cant be assigned to feature profile --------- Co-authored-by: Jakub Krajewski --- .github/workflows/documentation.yml | 6 +- .github/workflows/linting.yml | 4 +- .github/workflows/release.yml | 4 +- .github/workflows/unittests.yml | 4 +- .github/workflows/version.yml | 4 +- catalystwan/api/api_container.py | 2 + catalystwan/api/builders/__init__.py | 13 + .../feature_profiles/builder_factory.py | 42 ++ .../api/builders/feature_profiles/other.py | 69 +++ .../api/builders/feature_profiles/service.py | 130 +++++ .../api/builders/feature_profiles/system.py | 69 +++ .../api/configuration_groups/parcel.py | 4 + catalystwan/api/feature_profile_api.py | 17 +- .../feature_profile/sdwan/service.py | 23 +- .../feature_profile/sdwan/base.py | 15 + .../sdwan/service/test_models.py | 47 -- .../{other/test_models.py => test_other.py} | 24 +- .../feature_profile/sdwan/test_service.py | 190 +++++++ .../{system/test_models.py => test_system.py} | 46 +- catalystwan/models/common.py | 2 + .../configuration/feature_profile/common.py | 3 +- .../feature_profile/sdwan/other/ucse.py | 8 +- .../feature_profile/sdwan/service/__init__.py | 44 +- .../feature_profile/sdwan/service/acl.py | 86 ++- .../feature_profile/sdwan/service/appqoe.py | 33 +- .../feature_profile/sdwan/service/bgp.py | 95 ++-- .../sdwan/service/dhcp_server.py | 2 +- .../feature_profile/sdwan/service/eigrp.py | 46 +- .../sdwan/service/lan/common.py | 25 +- .../sdwan/service/lan/ethernet.py | 135 ++--- .../feature_profile/sdwan/service/lan/gre.py | 65 +-- .../sdwan/service/lan/ipsec.py | 106 ++-- .../feature_profile/sdwan/service/lan/svi.py | 89 ++- .../feature_profile/sdwan/service/lan/vpn.py | 149 ++--- .../sdwan/service/multicast.py | 68 ++- .../sdwan/service/object_tracker.py | 36 +- .../feature_profile/sdwan/service/ospf.py | 92 ++-- .../feature_profile/sdwan/service/ospfv3.py | 110 ++-- .../sdwan/service/route_policy.py | 36 +- .../service/service_insertion_attachment.py | 42 +- .../sdwan/service/switchport.py | 28 +- .../feature_profile/sdwan/service/tracker.py | 33 +- .../sdwan/service/wireless_lan.py | 42 +- catalystwan/session.py | 3 +- catalystwan/tests/test_feature_profile_api.py | 186 +++++-- .../config_migration/converters/exceptions.py | 9 + .../converters/feature_template/__init__.py | 2 +- .../converters/feature_template/aaa.py | 3 +- .../converters/feature_template/appqoe.py | 30 +- .../converters/feature_template/banner.py | 3 +- .../converters/feature_template/base.py | 3 +- .../converters/feature_template/basic.py | 3 +- .../converters/feature_template/bfd.py | 3 +- .../converters/feature_template/bgp.py | 12 +- .../converters/feature_template/dhcp.py | 27 +- .../converters/feature_template/ethernet.py | 271 +++++++++ .../converters/feature_template/global_.py | 3 +- .../converters/feature_template/gre.py | 118 ++++ .../converters/feature_template/ipsec.py | 96 ++++ .../converters/feature_template/logging_.py | 3 +- .../converters/feature_template/normalizer.py | 101 +++- .../converters/feature_template/ntp.py | 3 +- .../converters/feature_template/omp.py | 19 +- .../converters/feature_template/ospf.py | 139 +++++ .../converters/feature_template/ospfv3.py | 279 ++++++++++ .../{factory_method.py => parcel_factory.py} | 20 +- .../converters/feature_template/snmp.py | 21 +- .../converters/feature_template/svi.py | 164 ++++++ .../feature_template/thousandeyes.py | 50 +- .../converters/feature_template/ucse.py | 32 +- .../converters/feature_template/vpn.py | 521 ++++++++++++++++++ .../creators/config_pusher.py | 57 +- .../creators/strategy/parcels.py | 67 ++- .../factories/feature_profile_api.py | 8 +- .../factories/parcel_pusher.py | 29 +- catalystwan/workflows/config_migration.py | 41 +- 76 files changed, 3369 insertions(+), 1045 deletions(-) create mode 100644 catalystwan/api/builders/__init__.py create mode 100644 catalystwan/api/builders/feature_profiles/builder_factory.py create mode 100644 catalystwan/api/builders/feature_profiles/other.py create mode 100644 catalystwan/api/builders/feature_profiles/service.py create mode 100644 catalystwan/api/builders/feature_profiles/system.py create mode 100644 catalystwan/integration_tests/feature_profile/sdwan/base.py delete mode 100644 catalystwan/integration_tests/feature_profile/sdwan/service/test_models.py rename catalystwan/integration_tests/feature_profile/sdwan/{other/test_models.py => test_other.py} (57%) create mode 100644 catalystwan/integration_tests/feature_profile/sdwan/test_service.py rename catalystwan/integration_tests/feature_profile/sdwan/{system/test_models.py => test_system.py} (71%) create mode 100644 catalystwan/utils/config_migration/converters/exceptions.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/ethernet.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/gre.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/ipsec.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/ospf.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/ospfv3.py rename catalystwan/utils/config_migration/converters/feature_template/{factory_method.py => parcel_factory.py} (80%) create mode 100644 catalystwan/utils/config_migration/converters/feature_template/svi.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/vpn.py diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 8566b040..6d052b43 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -6,9 +6,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Set Up Poetry @@ -20,7 +20,7 @@ jobs: - name: Build HTML run: poetry run sphinx-build -M html docs/source docs/build - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: html-docs path: docs/build/html/ diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index a5265807..f728daa4 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -19,9 +19,9 @@ jobs: run: | curl -fsSL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install -y nodejs - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set Up Poetry diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0d7dca9..60028090 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,9 +17,9 @@ jobs: run: | curl -fsSL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install -y nodejs - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Set Up Poetry diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 1518436b..6ed6ee6a 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -19,9 +19,9 @@ jobs: run: | curl -fsSL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install -y nodejs - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Set Up Poetry diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index b9e5bf11..6bb66cc5 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -12,9 +12,9 @@ jobs: shell: bash steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Set Up Poetry diff --git a/catalystwan/api/api_container.py b/catalystwan/api/api_container.py index d9ea675f..cbe722b5 100644 --- a/catalystwan/api/api_container.py +++ b/catalystwan/api/api_container.py @@ -15,6 +15,7 @@ ) from catalystwan.api.alarms_api import AlarmsAPI from catalystwan.api.basic_api import DevicesAPI, DeviceStateAPI +from catalystwan.api.builders import BuilderAPI from catalystwan.api.config_device_inventory_api import ConfigurationDeviceInventoryAPI from catalystwan.api.config_group_api import ConfigGroupAPI from catalystwan.api.dashboard_api import DashboardAPI @@ -67,3 +68,4 @@ def __init__(self, session: ManagerSession): self.policy = PolicyAPI(session) self.sd_routing_feature_profiles = SDRoutingFeatureProfilesAPI(session) self.sdwan_feature_profiles = SDWANFeatureProfilesAPI(session) + self.builders = BuilderAPI(session) diff --git a/catalystwan/api/builders/__init__.py b/catalystwan/api/builders/__init__.py new file mode 100644 index 00000000..3e8105e5 --- /dev/null +++ b/catalystwan/api/builders/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from catalystwan.api.builders.feature_profiles.builder_factory import FeatureProfileBuilderFactory + +if TYPE_CHECKING: + from catalystwan.session import ManagerSession + + +class BuilderAPI: + def __init__(self, session: ManagerSession): + self.feature_profiles = FeatureProfileBuilderFactory(session=session) diff --git a/catalystwan/api/builders/feature_profiles/builder_factory.py b/catalystwan/api/builders/feature_profiles/builder_factory.py new file mode 100644 index 00000000..5a9b98cb --- /dev/null +++ b/catalystwan/api/builders/feature_profiles/builder_factory.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Mapping, Union + +from catalystwan.api.builders.feature_profiles.other import OtherFeatureProfileBuilder +from catalystwan.api.builders.feature_profiles.service import ServiceFeatureProfileBuilder +from catalystwan.api.builders.feature_profiles.system import SystemFeatureProfileBuilder +from catalystwan.exceptions import CatalystwanException +from catalystwan.models.configuration.feature_profile.common import ProfileType + +if TYPE_CHECKING: + from catalystwan.session import ManagerSession + +FeatureProfileBuilder = Union[ServiceFeatureProfileBuilder, SystemFeatureProfileBuilder, OtherFeatureProfileBuilder] + +BUILDER_MAPPING: Mapping[ProfileType, Callable] = { + "service": ServiceFeatureProfileBuilder, + "system": SystemFeatureProfileBuilder, + "other": OtherFeatureProfileBuilder, +} + + +class FeatureProfileBuilderFactory: + def __init__(self, session: ManagerSession): + self.session = session + + def create_builder(self, profile_type: ProfileType) -> FeatureProfileBuilder: + """ + Creates a builder for the specified feature profile. + + Args: + feature_profile_name (str): The name of the feature profile. + + Returns: + FeatureProfileBuilder: The builder for the specified feature profile. + + Raises: + CatalystwanException: If the feature profile is not found or has an unsupported type. + """ + if profile_type not in BUILDER_MAPPING: + raise CatalystwanException(f"Unsupported builder for type {profile_type}") + return BUILDER_MAPPING[profile_type](self.session) diff --git a/catalystwan/api/builders/feature_profiles/other.py b/catalystwan/api/builders/feature_profiles/other.py new file mode 100644 index 00000000..2490367d --- /dev/null +++ b/catalystwan/api/builders/feature_profiles/other.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List +from uuid import UUID + +from catalystwan.api.feature_profile_api import OtherFeatureProfileAPI +from catalystwan.endpoints.configuration.feature_profile.sdwan.other import OtherFeatureProfile +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload +from catalystwan.models.configuration.feature_profile.sdwan.other import AnyOtherParcel + +if TYPE_CHECKING: + from catalystwan.session import ManagerSession + + +class OtherFeatureProfileBuilder: + """ + A class for building Other feature profiles. + """ + + def __init__(self, session: ManagerSession) -> None: + """ + Initialize a new instance of the Service class. + + Args: + session (ManagerSession): The ManagerSession object used for API communication. + profile_uuid (UUID): The UUID of the profile. + """ + self._profile: FeatureProfileCreationPayload + self._api = OtherFeatureProfileAPI(session) + self._endpoints = OtherFeatureProfile(session) + self._independent_items: List[AnyOtherParcel] = [] + + def add_profile_name_and_description(self, feature_profile: FeatureProfileCreationPayload) -> None: + """ + Adds a name and description to the feature profile. + + Args: + name (str): The name of the feature profile. + description (str): The description of the feature profile. + + Returns: + None + """ + self._profile = feature_profile + + def add_parcel(self, parcel: AnyOtherParcel) -> None: + """ + Adds a parcel to the feature profile. + + Args: + parcel (AnySystemParcel): The parcel to add. + + Returns: + None + """ + self._independent_items.append(parcel) + + def build(self) -> UUID: + """ + Builds the feature profile. + + Returns: + UUID: The UUID of the created feature profile. + """ + + profile_uuid = self._endpoints.create_sdwan_other_feature_profile(self._profile).id + for parcel in self._independent_items: + self._api.create_parcel(profile_uuid, parcel) + return profile_uuid diff --git a/catalystwan/api/builders/feature_profiles/service.py b/catalystwan/api/builders/feature_profiles/service.py new file mode 100644 index 00000000..ebabaa5b --- /dev/null +++ b/catalystwan/api/builders/feature_profiles/service.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import logging +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List, Union +from uuid import UUID, uuid4 + +from pydantic import Field +from typing_extensions import Annotated + +from catalystwan.api.feature_profile_api import ServiceFeatureProfileAPI +from catalystwan.endpoints.configuration.feature_profile.sdwan.service import ServiceFeatureProfile +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload +from catalystwan.models.configuration.feature_profile.sdwan.service import ( + AppqoeParcel, + InterfaceEthernetParcel, + InterfaceGreParcel, + InterfaceIpsecParcel, + InterfaceSviParcel, + LanVpnDhcpServerParcel, + LanVpnParcel, +) + +if TYPE_CHECKING: + from catalystwan.session import ManagerSession + +logger = logging.getLogger(__name__) + +IndependedParcels = Annotated[Union[AppqoeParcel, LanVpnDhcpServerParcel], Field(discriminator="type_")] +DependedInterfaceParcels = Annotated[ + Union[InterfaceGreParcel, InterfaceSviParcel, InterfaceEthernetParcel, InterfaceIpsecParcel], + Field(discriminator="type_"), +] + + +class ServiceFeatureProfileBuilder: + """ + A class for building service feature profiles. + """ + + def __init__(self, session: ManagerSession): + """ + Initialize a new instance of the Service class. + + Args: + session (ManagerSession): The ManagerSession object used for API communication. + profile_uuid (UUID): The UUID of the profile. + """ + self._profile: FeatureProfileCreationPayload + self._api = ServiceFeatureProfileAPI(session) + self._endpoints = ServiceFeatureProfile(session) + self._independent_items: List[IndependedParcels] = [] + self._independent_items_vpns: Dict[UUID, LanVpnParcel] = {} + self._depended_items_on_vpns: Dict[UUID, List[DependedInterfaceParcels]] = defaultdict(list) + + def add_profile_name_and_description(self, feature_profile: FeatureProfileCreationPayload) -> None: + """ + Adds a name and description to the feature profile. + + Args: + name (str): The name of the feature profile. + description (str): The description of the feature profile. + + Returns: + None + """ + self._profile = feature_profile + + def add_parcel(self, parcel: IndependedParcels) -> None: + """ + Adds an independent parcel to the builder. + + Args: + parcel (IndependedParcels): The independent parcel to add. + + Returns: + None + """ + self._independent_items.append(parcel) + + def add_parcel_vpn(self, parcel: LanVpnParcel) -> UUID: + """ + Adds a VPN parcel to the builder. + + Args: + parcel (LanVpnParcel): The VPN parcel to add. + + Returns: + UUID: The UUID tag of the added VPN parcel. + """ + vpn_tag = uuid4() + logger.debug(f"Adding VPN parcel {parcel.parcel_name} with tag {vpn_tag}") + self._independent_items_vpns[vpn_tag] = parcel + return vpn_tag + + def add_parcel_vpn_interface(self, vpn_tag: UUID, parcel: DependedInterfaceParcels) -> None: + """ + Adds an interface parcel dependent on a VPN to the builder. + + Args: + vpn_tag (UUID): The UUID of the VPN. + parcel (DependedInterfaceParcels): The interface parcel to add. + + Returns: + None + """ + logger.debug(f"Adding interface parcel {parcel.parcel_name} to VPN {vpn_tag}") + self._depended_items_on_vpns[vpn_tag].append(parcel) + + def build(self) -> UUID: + """ + Builds the feature profile by creating parcels for independent items, + VPNs, and interface parcels dependent on VPNs. + + Returns: + Service feature profile UUID + """ + profile_uuid = self._endpoints.create_sdwan_service_feature_profile(self._profile).id + + for parcel in self._independent_items: + self._api.create_parcel(profile_uuid, parcel) + + for vpn_tag, vpn_parcel in self._independent_items_vpns.items(): + vpn_uuid = self._api.create_parcel(profile_uuid, vpn_parcel).id + + for interface_parcel in self._depended_items_on_vpns[vpn_tag]: + logger.debug(f"Creating interface parcel {interface_parcel.parcel_name} to VPN {vpn_tag}") + self._api.create_parcel(profile_uuid, interface_parcel, vpn_uuid) + + return profile_uuid diff --git a/catalystwan/api/builders/feature_profiles/system.py b/catalystwan/api/builders/feature_profiles/system.py new file mode 100644 index 00000000..c7975f8d --- /dev/null +++ b/catalystwan/api/builders/feature_profiles/system.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List +from uuid import UUID + +from catalystwan.api.feature_profile_api import SystemFeatureProfileAPI +from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload +from catalystwan.models.configuration.feature_profile.sdwan.system import AnySystemParcel + +if TYPE_CHECKING: + from catalystwan.session import ManagerSession + + +class SystemFeatureProfileBuilder: + """ + A class for building system feature profiles. + """ + + def __init__(self, session: ManagerSession) -> None: + """ + Initialize a new instance of the Service class. + + Args: + session (ManagerSession): The ManagerSession object used for API communication. + profile_uuid (UUID): The UUID of the profile. + """ + self._profile: FeatureProfileCreationPayload + self._api = SystemFeatureProfileAPI(session) + self._endpoints = SystemFeatureProfile(session) + self._independent_items: List[AnySystemParcel] = [] + + def add_profile_name_and_description(self, feature_profile: FeatureProfileCreationPayload) -> None: + """ + Adds a name and description to the feature profile. + + Args: + name (str): The name of the feature profile. + description (str): The description of the feature profile. + + Returns: + None + """ + self._profile = feature_profile + + def add_parcel(self, parcel: AnySystemParcel) -> None: + """ + Adds a parcel to the feature profile. + + Args: + parcel (AnySystemParcel): The parcel to add. + + Returns: + None + """ + self._independent_items.append(parcel) + + def build(self) -> UUID: + """ + Builds the feature profile. + + Returns: + UUID: The UUID of the created feature profile. + """ + + profile_uuid = self._endpoints.create_sdwan_system_feature_profile(self._profile).id + for parcel in self._independent_items: + self._api.create_parcel(profile_uuid, parcel) + return profile_uuid diff --git a/catalystwan/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index c3217ef8..ce53e3ea 100644 --- a/catalystwan/api/configuration_groups/parcel.py +++ b/catalystwan/api/configuration_groups/parcel.py @@ -89,6 +89,10 @@ class Global(ParcelAttribute, Generic[T]): ) value: T + def __bool__(self) -> bool: + # if statements use __len__ when __bool__ is not defined + return True + def __len__(self) -> int: if isinstance(self.value, (str, list)): return len(self.value) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index c553ebfd..a9c7860f 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, overload +from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, get_args, overload from uuid import UUID from pydantic import Json @@ -12,6 +12,7 @@ from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile from catalystwan.models.configuration.feature_profile.sdwan.other import AnyOtherParcel from catalystwan.models.configuration.feature_profile.sdwan.policy_object.security.url import URLParcel +from catalystwan.models.configuration.feature_profile.sdwan.service import AnyLanVpnInterfaceParcel, AnyServiceParcel from catalystwan.typed_list import DataSequence if TYPE_CHECKING: @@ -231,6 +232,18 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_sdwan_service_feature_profile(profile_id) + def create_parcel( + self, profile_uuid: UUID, payload: AnyServiceParcel, vpn_uuid: Optional[UUID] = None + ) -> ParcelCreationResponse: + """ + Create Service Parcel for selected profile_id based on payload type + """ + if type(payload) in get_args(AnyLanVpnInterfaceParcel)[0].__args__: + return self.endpoint.create_lan_vpn_interface_parcel( + profile_uuid, vpn_uuid, payload._get_parcel_type(), payload + ) + return self.endpoint.create_service_parcel(profile_uuid, payload._get_parcel_type(), payload) + class SystemFeatureProfileAPI: """ @@ -485,7 +498,7 @@ def create_parcel(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCre return self.endpoint.create(profile_id, payload._get_parcel_type(), payload) - def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> ParcelCreationResponse: + def update_parcel(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> ParcelCreationResponse: """ Update System Parcel for selected profile_id based on payload type """ diff --git a/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py b/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py index 9a4faa4c..d6b8db64 100644 --- a/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py +++ b/catalystwan/endpoints/configuration/feature_profile/sdwan/service.py @@ -10,6 +10,11 @@ FeatureProfileCreationResponse, FeatureProfileInfo, GetFeatureProfilesPayload, + ParcelCreationResponse, +) +from catalystwan.models.configuration.feature_profile.sdwan.service import ( + AnyLanVpnInterfaceParcel, + AnyTopLevelServiceParcel, ) from catalystwan.typed_list import DataSequence @@ -30,6 +35,20 @@ def create_sdwan_service_feature_profile( ... @versions(supported_versions=(">=20.9"), raises=False) - @delete("/v1/feature-profile/sdwan/service/{profile_id}") - def delete_sdwan_service_feature_profile(self, profile_id: UUID) -> None: + @delete("/v1/feature-profile/sdwan/service/{profile_uuid}") + def delete_sdwan_service_feature_profile(self, profile_uuid: UUID) -> None: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @post("/v1/feature-profile/sdwan/service/{profile_uuid}/{parcel_type}") + def create_service_parcel( + self, profile_uuid: UUID, parcel_type: str, payload: AnyTopLevelServiceParcel + ) -> ParcelCreationResponse: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @post("/v1/feature-profile/sdwan/service/{profile_uuid}/lan/vpn/{vpn_uuid}/interface/{parcel_type}") + def create_lan_vpn_interface_parcel( + self, profile_uuid: UUID, vpn_uuid: UUID, parcel_type: str, payload: AnyLanVpnInterfaceParcel + ) -> ParcelCreationResponse: ... diff --git a/catalystwan/integration_tests/feature_profile/sdwan/base.py b/catalystwan/integration_tests/feature_profile/sdwan/base.py new file mode 100644 index 00000000..4adc15e7 --- /dev/null +++ b/catalystwan/integration_tests/feature_profile/sdwan/base.py @@ -0,0 +1,15 @@ +import os +import unittest + +from catalystwan.session import create_manager_session + + +class TestFeatureProfileModels(unittest.TestCase): + def setUp(self) -> None: + # TODO: Add those params to PyTest + self.session = create_manager_session( + url=os.environ.get("TEST_VMANAGE_URL", "localhost"), + port=int(os.environ.get("TEST_VMANAGE_PORT", 443)), + username=os.environ.get("TEST_VMANAGE_USERNAME", "admin"), + password=os.environ.get("TEST_VMANAGE_PASSWORD", "admin"), + ) diff --git a/catalystwan/integration_tests/feature_profile/sdwan/service/test_models.py b/catalystwan/integration_tests/feature_profile/sdwan/service/test_models.py deleted file mode 100644 index 7233570f..00000000 --- a/catalystwan/integration_tests/feature_profile/sdwan/service/test_models.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import unittest -from ipaddress import IPv4Address -from typing import cast - -from catalystwan.api.configuration_groups.parcel import Global -from catalystwan.models.configuration.feature_profile.sdwan.service.dhcp_server import ( - AddressPool, - LanVpnDhcpServerParcel, - SubnetMask, -) -from catalystwan.session import create_manager_session - - -class TestServiceFeatureProfileModels(unittest.TestCase): - def setUp(self) -> None: - self.session = create_manager_session( - url=cast(str, os.environ.get("TEST_VMANAGE_URL")), - port=cast(int, int(os.environ.get("TEST_VMANAGE_PORT"))), # type: ignore - username=cast(str, os.environ.get("TEST_VMANAGE_USERNAME")), - password=cast(str, os.environ.get("TEST_VMANAGE_PASSWORD")), - ) - self.profile_id = self.session.api.sdwan_feature_profiles.service.create_profile( - "TestProfile", "Description" - ).id - - def test_when_default_values_dhcp_server_parcel_expect_successful_post(self): - # Arrange - url = f"dataservice/v1/feature-profile/sdwan/service/{self.profile_id}/dhcp-server" - dhcp_server_parcel = LanVpnDhcpServerParcel( - parcel_name="DhcpServerDefault", - parcel_description="Dhcp Server Parcel", - address_pool=AddressPool( - network_address=Global[IPv4Address](value=IPv4Address("10.0.0.2")), - subnet_mask=Global[SubnetMask](value="255.255.255.255"), - ), - ) - # Act - response = self.session.post( - url=url, data=dhcp_server_parcel.model_dump_json(by_alias=True, exclude_none=True) - ) # This will be changed to the actual method - # Assert - assert response.status_code == 200 - - def tearDown(self) -> None: - self.session.api.sdwan_feature_profiles.service.delete_profile(self.profile_id) - self.session.close() diff --git a/catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py b/catalystwan/integration_tests/feature_profile/sdwan/test_other.py similarity index 57% rename from catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py rename to catalystwan/integration_tests/feature_profile/sdwan/test_other.py index cde5f51c..339ff58a 100644 --- a/catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py +++ b/catalystwan/integration_tests/feature_profile/sdwan/test_other.py @@ -1,22 +1,14 @@ -import os -import unittest -from typing import cast - from catalystwan.api.configuration_groups.parcel import Global, as_global +from catalystwan.integration_tests.feature_profile.sdwan.base import TestFeatureProfileModels from catalystwan.models.configuration.feature_profile.sdwan.other import ThousandEyesParcel, UcseParcel from catalystwan.models.configuration.feature_profile.sdwan.other.ucse import AccessPort, Imc, LomType, SharedLom -from catalystwan.session import create_manager_session -class TestSystemOtherProfileModels(unittest.TestCase): +class TestSystemOtherProfileModels(TestFeatureProfileModels): def setUp(self) -> None: - self.session = create_manager_session( - url=cast(str, os.environ.get("TEST_VMANAGE_URL")), - port=cast(int, int(os.environ.get("TEST_VMANAGE_PORT"))), # type: ignore - username=cast(str, os.environ.get("TEST_VMANAGE_USERNAME")), - password=cast(str, os.environ.get("TEST_VMANAGE_PASSWORD")), - ) - self.profile_id = self.session.api.sdwan_feature_profiles.other.create_profile("TestProfile", "Description").id + super().setUp() + self.api = self.session.api.sdwan_feature_profiles.other + self.profile_id = self.api.create_profile("TestProfile", "Description").id def test_when_default_values_thousandeyes_parcel_expect_successful_post(self): # Arrange @@ -25,7 +17,7 @@ def test_when_default_values_thousandeyes_parcel_expect_successful_post(self): parcel_description="ThousandEyes Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.other.create_parcel(self.profile_id, te_parcel).id + parcel_id = self.api.create_parcel(te_parcel, self.profile_id).id # Assert assert parcel_id @@ -45,10 +37,10 @@ def test_when_default_values_ucse_parcel_expect_successful_post(self): ), ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.other.create_parcel(self.profile_id, ucse_parcel).id + parcel_id = self.api.create_parcel(ucse_parcel, self.profile_id).id # Assert assert parcel_id def tearDown(self) -> None: - self.session.api.sdwan_feature_profiles.other.delete_profile(self.profile_id) + self.api.delete_profile(self.profile_id) self.session.close() diff --git a/catalystwan/integration_tests/feature_profile/sdwan/test_service.py b/catalystwan/integration_tests/feature_profile/sdwan/test_service.py new file mode 100644 index 00000000..8d34b409 --- /dev/null +++ b/catalystwan/integration_tests/feature_profile/sdwan/test_service.py @@ -0,0 +1,190 @@ +from ipaddress import IPv4Address + +from catalystwan.api.configuration_groups.parcel import Global, as_global, as_variable +from catalystwan.integration_tests.feature_profile.sdwan.base import TestFeatureProfileModels +from catalystwan.models.configuration.feature_profile.sdwan.service.dhcp_server import ( + AddressPool, + LanVpnDhcpServerParcel, + SubnetMask, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ethernet import InterfaceEthernetParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.gre import BasicGre, InterfaceGreParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import ( + InterfaceIpsecParcel, + IpsecAddress, + IpsecTunnelMode, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.svi import InterfaceSviParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import LanVpnParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.ospf import OspfParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.ospfv3 import ( + Ospfv3InterfaceParametres, + Ospfv3IPv4Area, + Ospfv3IPv4Parcel, + Ospfv3IPv6Area, + Ospfv3IPv6Parcel, +) + + +class TestServiceFeatureProfileModels(TestFeatureProfileModels): + def setUp(self) -> None: + super().setUp() + self.api = self.session.api.sdwan_feature_profiles.service + self.profile_uuid = self.api.create_profile("TestProfileService", "Description").id + + def test_when_default_values_dhcp_server_parcel_expect_successful_post(self): + # Arrange + dhcp_server_parcel = LanVpnDhcpServerParcel( + parcel_name="DhcpServerDefault", + parcel_description="Dhcp Server Parcel", + address_pool=AddressPool( + network_address=Global[IPv4Address](value=IPv4Address("10.0.0.2")), + subnet_mask=Global[SubnetMask](value="255.255.255.255"), + ), + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, dhcp_server_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_service_vpn_parcel_expect_successful_post(self): + # Arrange + vpn_parcel = LanVpnParcel( + parcel_name="TestVpnParcel", + parcel_description="Test Vpn Parcel", + vpn_id=Global[int](value=2), + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, vpn_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_ospf_parcel_expect_successful_post(self): + # Arrange + ospf_parcel = OspfParcel( + parcel_name="TestOspfParcel", + parcel_description="Test Ospf Parcel", + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, ospf_parcel).id + # Assert + assert parcel_id + + def test_when_default_ospfv3_ipv4_expect_successful_post(self): + # Arrange + ospfv3ipv4_parcel = Ospfv3IPv4Parcel( + parcel_name="TestOspfv3ipv4", + parcel_description="Test Ospfv3ipv4 Parcel", + area=[ + Ospfv3IPv4Area( + area_number=as_global(5), + interfaces=[Ospfv3InterfaceParametres(name=as_global("GigabitEthernet0/0/0"))], + ) + ], + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, ospfv3ipv4_parcel).id + # Assert + assert parcel_id + + def test_when_default_ospfv3_ipv6_expect_successful_post(self): + # Arrange + ospfv3ipv4_parcel = Ospfv3IPv6Parcel( + parcel_name="TestOspfv3ipv6", + parcel_description="Test Ospfv3ipv6 Parcel", + area=[ + Ospfv3IPv6Area( + area_number=as_global(7), + interfaces=[Ospfv3InterfaceParametres(name=as_global("GigabitEthernet0/0/0"))], + ) + ], + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, ospfv3ipv4_parcel).id + # Assert + assert parcel_id + + def tearDown(self) -> None: + self.api.delete_profile(self.profile_uuid) + self.session.close() + + +class TestServiceFeatureProfileVPNInterfaceModels(TestFeatureProfileModels): + def setUp(self) -> None: + super().setUp() + self.api = self.session.api.sdwan_feature_profiles.service + self.profile_uuid = self.api.create_profile("TestProfileService", "Description").id + self.vpn_parcel_uuid = self.api.create_parcel( + self.profile_uuid, + LanVpnParcel( + parcel_name="TestVpnParcel", parcel_description="Test Vpn Parcel", vpn_id=Global[int](value=2) + ), + ).id + + def test_when_default_values_gre_parcel_expect_successful_post(self): + # Arrange + gre_parcel = InterfaceGreParcel( + parcel_name="TestGreParcel", + parcel_description="Test Gre Parcel", + basic=BasicGre(if_name=as_global("gre1"), tunnel_destination=as_global(IPv4Address("4.4.4.4"))), + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, gre_parcel, self.vpn_parcel_uuid).id + # Assert + assert parcel_id + + def test_when_default_values_svi_parcel_expect_successful_post(self): + # Arrange + svi_parcel = InterfaceSviParcel( + parcel_name="TestSviParcel", + parcel_description="Test Svi Parcel", + interface_name=as_global("Vlan1"), + svi_description=as_global("Test Svi Description"), + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, svi_parcel, self.vpn_parcel_uuid).id + # Assert + assert parcel_id + + def test_when_default_values_ethernet_parcel_expect_successful_post(self): + # Arrange + ethernet_parcel = InterfaceEthernetParcel( + parcel_name="TestEthernetParcel", + parcel_description="Test Ethernet Parcel", + interface_name=as_global("HundredGigE"), + ethernet_description=as_global("Test Ethernet Description"), + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, ethernet_parcel, self.vpn_parcel_uuid).id + # Assert + assert parcel_id + + def test_when_default_values_ipsec_parcel_expect_successful_post(self): + # Arrange + self.maxDiff = None + ipsec_parcel = InterfaceIpsecParcel( + parcel_name="TestIpsecParcel", + parcel_description="Test Ipsec Parcel", + interface_name=as_global("ipsec2"), + ipsec_description=as_global("Test Ipsec Description"), + pre_shared_secret=as_global("123"), + ike_local_id=as_global("123"), + ike_remote_id=as_global("123"), + application=as_variable("{{ipsec_application}}"), + tunnel_mode=Global[IpsecTunnelMode](value="ipv6"), + tunnel_destination_v6=as_variable("{{ipsec_tunnelDestinationV6}}"), + tunnel_source_v6=Global[str](value="::"), + tunnel_source_interface=as_variable("{{ipsec_ipsecSourceInterface}}"), + ipv6_address=as_variable("{{test}}"), + address=IpsecAddress(address=as_global("10.0.0.1"), mask=as_global("255.255.255.0")), + tunnel_destination=IpsecAddress(address=as_global("10.0.0.5"), mask=as_global("255.255.255.0")), + mtu_v6=as_variable("{{test}}"), + ) + # Act + parcel_id = self.api.create_parcel(self.profile_uuid, ipsec_parcel, self.vpn_parcel_uuid).id + # Assert + assert parcel_id + + def tearDown(self) -> None: + self.api.delete_profile(self.profile_uuid) + self.session.close() diff --git a/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py b/catalystwan/integration_tests/feature_profile/sdwan/test_system.py similarity index 71% rename from catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py rename to catalystwan/integration_tests/feature_profile/sdwan/test_system.py index 814c88bf..a3ebdadf 100644 --- a/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py +++ b/catalystwan/integration_tests/feature_profile/sdwan/test_system.py @@ -1,7 +1,4 @@ -import os -import unittest -from typing import cast - +from catalystwan.integration_tests.feature_profile.sdwan.base import TestFeatureProfileModels from catalystwan.models.configuration.feature_profile.sdwan.system import ( BannerParcel, BasicParcel, @@ -14,18 +11,13 @@ SecurityParcel, SNMPParcel, ) -from catalystwan.session import create_manager_session -class TestSystemFeatureProfileModels(unittest.TestCase): +class TestSystemFeatureProfileModels(TestFeatureProfileModels): def setUp(self) -> None: - self.session = create_manager_session( - url=cast(str, os.environ.get("TEST_VMANAGE_URL")), - port=cast(int, int(os.environ.get("TEST_VMANAGE_PORT"))), # type: ignore - username=cast(str, os.environ.get("TEST_VMANAGE_USERNAME")), - password=cast(str, os.environ.get("TEST_VMANAGE_PASSWORD")), - ) - self.profile_id = self.session.api.sdwan_feature_profiles.system.create_profile("TestProfile", "Description").id + super().setUp() + self.api = self.session.api.sdwan_feature_profiles.system + self.profile_id = self.api.create_profile("TestProfile", "Description").id def test_when_default_values_banner_parcel_expect_successful_post(self): # Arrange @@ -34,7 +26,7 @@ def test_when_default_values_banner_parcel_expect_successful_post(self): parcel_description="Banner Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, banner_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, banner_parcel).id # Assert assert parcel_id @@ -47,7 +39,7 @@ def test_when_fully_specified_banner_parcel_expect_successful_post(self): banner_parcel.add_login("Login") banner_parcel.add_motd("Hello! Welcome to the network!") # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, banner_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, banner_parcel).id # Assert assert parcel_id @@ -58,7 +50,7 @@ def test_when_default_values_logging_parcel_expect_successful_post(self): parcel_description="Logging Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, logging_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, logging_parcel).id # Assert assert parcel_id @@ -102,7 +94,7 @@ def test_when_fully_specified_logging_parcel_expect_successful_post(self): profile_properties="TLSProfile", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, logging_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, logging_parcel).id # Assert assert parcel_id @@ -113,7 +105,7 @@ def test_when_default_values_bfd_parcel_expect_successful_post(self): parcel_description="BFD Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, bfd_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, bfd_parcel).id # Assert assert parcel_id @@ -131,7 +123,7 @@ def test_when_fully_specified_bfd_parcel_expect_successful_post(self): bfd_parcel.add_color(color="biz-internet") bfd_parcel.add_color(color="public-internet") # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, bfd_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, bfd_parcel).id # Assert assert parcel_id @@ -142,7 +134,7 @@ def test_when_default_values_basic_parcel_expect_successful_post(self): parcel_description="Basic Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, basic_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, basic_parcel).id # Assert assert parcel_id @@ -153,7 +145,7 @@ def test_when_default_values_security_parcel_expect_successful_post(self): parcel_description="Security Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, security_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, security_parcel).id # Assert assert parcel_id @@ -164,7 +156,7 @@ def test_when_default_values_ntp_parcel_expect_successful_post(self): parcel_description="NTP Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, ntp_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, ntp_parcel).id # Assert assert parcel_id @@ -175,7 +167,7 @@ def test_when_default_values_global_parcel_expect_successful_post(self): parcel_description="Global Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, global_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, global_parcel).id # Assert assert parcel_id @@ -186,7 +178,7 @@ def test_when_default_values_mrf_parcel_expect_successful_post(self): parcel_description="MRF Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, mrf_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, mrf_parcel).id # Assert assert parcel_id @@ -197,7 +189,7 @@ def test_when_default_values_snmp_parcel_expect_successful_post(self): parcel_description="SNMP Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, snmp_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, snmp_parcel).id # Assert assert parcel_id @@ -208,10 +200,10 @@ def test_when_default_values_omp_parcel_expect_successful_post(self): parcel_description="OMP Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, omp_parcel).id + parcel_id = self.api.create_parcel(self.profile_id, omp_parcel).id # Assert assert parcel_id def tearDown(self) -> None: - self.session.api.sdwan_feature_profiles.system.delete_profile(self.profile_id) + self.api.delete_profile(self.profile_id) self.session.close() diff --git a/catalystwan/models/common.py b/catalystwan/models/common.py index 9feafaff..f5fa0e97 100644 --- a/catalystwan/models/common.py +++ b/catalystwan/models/common.py @@ -179,3 +179,5 @@ def str_as_str_list(val: Union[str, Sequence[str]]) -> Sequence[str]: ICMPMessageType = Literal[ "echo", "echo-reply", "unreachable", "net-unreachable", "host-unreachable", "protocol-unreachable" ] + +MetricType = Literal["type1", "type2"] 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/other/ucse.py b/catalystwan/models/configuration/feature_profile/sdwan/other/ucse.py index 2fb5bbd2..e196e299 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/other/ucse.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/other/ucse.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv4Interface from typing import List, Literal, Optional, Union from pydantic import AliasPath, BaseModel, ConfigDict, Field @@ -35,7 +35,7 @@ class Ip(BaseModel): extra="forbid", populate_by_name=True, ) - address: Union[Global[str], Variable] = Field( + address: Union[Global[str], Global[IPv4Interface], Variable] = Field( default=as_variable("{{ipv4Addr}}"), description="Assign IPv4 address" ) default_gateway: Union[Global[IPv4Address], Variable, Default[None]] = Field( @@ -88,7 +88,9 @@ class InterfaceItem(BaseModel): validation_alias="ucseInterfaceVpn", description="UCSE Interface VPN", ) - address: Optional[Union[Global[str], Variable]] = Field(default=None, description="Assign IPv4 address") + address: Optional[Union[Global[str], Global[IPv4Interface], Variable]] = Field( + default=None, description="Assign IPv4 address" + ) class UcseParcel(_ParcelBase): diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py index e8998294..08452aed 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/__init__.py @@ -3,16 +3,58 @@ from pydantic import Field from typing_extensions import Annotated +from .appqoe import AppqoeParcel from .dhcp_server import LanVpnDhcpServerParcel +from .lan.ethernet import InterfaceEthernetParcel +from .lan.gre import InterfaceGreParcel +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[ + LanVpnDhcpServerParcel, + AppqoeParcel, + LanVpnParcel, + OspfParcel, + Ospfv3IPv4Parcel, + Ospfv3IPv6Parcel, + # TrackerGroupData, + # WirelessLanData, + # SwitchportData + ], + Field(discriminator="type_"), +] + +AnyLanVpnInterfaceParcel = Annotated[ + Union[ + InterfaceEthernetParcel, + InterfaceGreParcel, + InterfaceIpsecParcel, + InterfaceSviParcel, + ], + Field(discriminator="type_"), +] AnyServiceParcel = Annotated[ - Union[LanVpnDhcpServerParcel,], # noqa: E231 + Union[AnyTopLevelServiceParcel, AnyLanVpnInterfaceParcel], Field(discriminator="type_"), ] __all__ = [ "LanVpnDhcpServerParcel", + "AppqoeParcel", + "LanVpnParcel", + "OspfParcel", + "Ospfv3IPv4Parcel", + "Ospfv3IPv6Parcel", + "InterfaceSviParcel", + "InterfaceGreParcel", "AnyServiceParcel", + "AnyTopLevelServiceParcel", + "AnyLanVpnInterfaceParcel", ] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/acl.py b/catalystwan/models/configuration/feature_profile/sdwan/service/acl.py index 30c2d653..45474a94 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/acl.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/acl.py @@ -3,9 +3,9 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.common import ServiceChainNumber Action = Literal[ @@ -116,7 +116,7 @@ class SourceDataIPv4Prefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_ip_prefix: Union[Global[str], Variable] = Field( serialization_alias="sourceIpPrefix", validation_alias="sourceIpPrefix" @@ -124,7 +124,7 @@ class SourceDataIPv4Prefix(BaseModel): class SourceDataIPv6Prefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_ip_prefix: Union[Global[str], Variable] = Field( serialization_alias="sourceIpPrefix", validation_alias="sourceIpPrefix" @@ -132,7 +132,7 @@ class SourceDataIPv6Prefix(BaseModel): class SourceDataIPv4PrefixParcel(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_data_prefix_list: Global[UUID] = Field( serialization_alias="sourceDataPrefixList", validation_alias="sourceDataPrefixList" @@ -140,7 +140,7 @@ class SourceDataIPv4PrefixParcel(BaseModel): class SourceDataIPv6PrefixParcel(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_data_prefix_list: Global[UUID] = Field( serialization_alias="sourceDataPrefixList", validation_alias="sourceDataPrefixList" @@ -148,13 +148,13 @@ class SourceDataIPv6PrefixParcel(BaseModel): class SourcePort(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_port: Global[int] = Field(serialization_alias="sourcePort", validation_alias="sourcePort") class DestinationDataIPv4Prefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") destination_ip_prefix: Union[Global[str], Variable] = Field( serialization_alias="destinationIpPrefix", validation_alias="destinationIpPrefix" @@ -162,7 +162,7 @@ class DestinationDataIPv4Prefix(BaseModel): class DestinationDataIPv6Prefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") destination_ip_prefix: Union[Global[str], Variable] = Field( serialization_alias="destinationIpPrefix", validation_alias="destinationIpPrefix" @@ -170,7 +170,7 @@ class DestinationDataIPv6Prefix(BaseModel): class DestinationDataIPv4PrefixParcel(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") destination_data_prefix_list: Global[UUID] = Field( serialization_alias="destinationDataPrefixList", validation_alias="destinationDataPrefixList" @@ -178,7 +178,7 @@ class DestinationDataIPv4PrefixParcel(BaseModel): class DestinationDataIPv6PrefixParcel(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") destination_data_prefix_list: Global[UUID] = Field( serialization_alias="destinationDataPrefixList", validation_alias="destinationDataPrefixList" @@ -186,7 +186,7 @@ class DestinationDataIPv6PrefixParcel(BaseModel): class DestinationPort(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") destination_port: Global[int] = Field(serialization_alias="destinationPort", validation_alias="destinationPort") @@ -195,7 +195,7 @@ class DestinationPort(BaseModel): class IPv4Match(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") dscp: Optional[Global[List[int]]] = None packet_length: Optional[Global[int]] = Field( @@ -221,7 +221,7 @@ class IPv4Match(BaseModel): class IPv6Match(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") next_header: Optional[Global[int]] = Field( serialization_alias="nextHeader", validation_alias="nextHeader", default=None @@ -249,7 +249,7 @@ class IPv6Match(BaseModel): class ServiceChain(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") service_chain_number: Union[Global[ServiceChainNumber], Variable] = Field( serialization_alias="serviceChainNumber", validation_alias="serviceChainNumber" @@ -259,7 +259,7 @@ class ServiceChain(BaseModel): class AcceptActionIPv4(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") set_dscp: Optional[Global[int]] = Field(serialization_alias="setDscp", validation_alias="setDscp", default=None) counter_name: Optional[Global[str]] = Field( @@ -277,7 +277,7 @@ class AcceptActionIPv4(BaseModel): class AcceptActionIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") counter_name: Optional[Global[str]] = Field( serialization_alias="counterName", validation_alias="counterName", default=None @@ -297,7 +297,7 @@ class AcceptActionIPv6(BaseModel): class DropAction(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") counter_name: Optional[Global[str]] = Field( serialization_alias="counterName", validation_alias="counterName", default=None @@ -306,25 +306,25 @@ class DropAction(BaseModel): class AcceptActionsIPv4(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") accept: AcceptActionIPv4 class AcceptActionsIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") accept: AcceptActionIPv6 class DropActions(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") drop: DropAction class IPv4SequenceBaseAction(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") sequence_id: Global[int] = Field(serialization_alias="sequenceId", validation_alias="sequenceId") sequence_name: Global[str] = Field(serialization_alias="sequenceName", validation_alias="sequenceName") @@ -337,7 +337,7 @@ class IPv4SequenceBaseAction(BaseModel): class IPv6SequenceBaseAction(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") sequence_id: Global[int] = Field(serialization_alias="sequenceId", validation_alias="sequenceId") sequence_name: Global[str] = Field(serialization_alias="sequenceName", validation_alias="sequenceName") @@ -350,7 +350,7 @@ class IPv6SequenceBaseAction(BaseModel): class IPv4SequenceActions(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") sequence_id: Global[int] = Field(serialization_alias="sequenceId", validation_alias="sequenceId") sequence_name: Global[str] = Field(serialization_alias="sequenceName", validation_alias="sequenceName") @@ -363,7 +363,7 @@ class IPv4SequenceActions(BaseModel): class IPv6SequenceActions(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") sequence_id: Global[int] = Field(serialization_alias="sequenceId", validation_alias="sequenceId") sequence_name: Global[str] = Field(serialization_alias="sequenceName", validation_alias="sequenceName") @@ -375,35 +375,23 @@ class IPv6SequenceActions(BaseModel): ) -class IPv4AclData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class IPv4AclParcel(_ParcelBase): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") defautl_action: Union[Global[Action], Default[Action]] = Field( - serialization_alias="defaultAction", validation_alias="defaultAction", default=Default[Action](value="drop") + validation_alias=AliasPath("data", "defaultAction"), default=Default[Action](value="drop") + ) + sequences: List[Union[IPv4SequenceBaseAction, IPv4SequenceActions]] = Field( + validation_alias=AliasPath("data", "sequences") ) - sequences: List[Union[IPv4SequenceBaseAction, IPv4SequenceActions]] - - -class IPv4AclCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: IPv4AclData -class IPv6AclData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class IPv6AclParcel(_ParcelBase): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") defautl_action: Union[Global[Action], Default[Action]] = Field( - serialization_alias="defaultAction", validation_alias="defaultAction", default=Default[Action](value="drop") + validation_alias=AliasPath("data", "defaultAction"), default=Default[Action](value="drop") + ) + sequences: List[Union[IPv6SequenceBaseAction, IPv6SequenceActions]] = Field( + validation_alias=AliasPath("data", "sequences") ) - sequences: List[Union[IPv6SequenceBaseAction, IPv6SequenceActions]] - - -class IPv6AclCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: IPv6AclData diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/appqoe.py b/catalystwan/models/configuration/feature_profile/sdwan/service/appqoe.py index a369e9d1..be54e0e7 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/appqoe.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/appqoe.py @@ -70,7 +70,7 @@ class VirtualApplication(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") instance_id: Global[int] = Field( default=Global(value=1), serialization_alias="instanceId", validation_alias="instanceId" @@ -88,7 +88,7 @@ class VirtualApplication(BaseModel): class Appqoe(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Default[str] = Default(value="/1") appnav_controller_group: Global[AppnavControllerGroupName] = Field( @@ -112,7 +112,7 @@ class Appqoe(BaseModel): class ServiceContext(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - appqoe: List[Appqoe] + appqoe: List[Appqoe] = Field(default_factory=lambda: [Appqoe()]) # Frowarder @@ -123,7 +123,7 @@ class ServiceNodeInformation(BaseModel): class ForwarderController(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Global[str], Global[IPv4Address], Variable] vpn: Global[int] = Field( @@ -132,7 +132,7 @@ class ForwarderController(BaseModel): class ForwarderAppnavControllerGroup(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_name: Default[AppnavControllerGroupName] = Field( default=Default(value="ACG-APPQOE"), serialization_alias="groupName", validation_alias="groupName" @@ -143,7 +143,7 @@ class ForwarderAppnavControllerGroup(BaseModel): class ForwarderNodeGroup(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Union[Global[str], Default[ServiceNodeGroupName]] internal: Default[bool] = Default[bool](value=False) @@ -153,7 +153,7 @@ class ForwarderNodeGroup(BaseModel): class ForwarderRole(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") appnav_controller_group: List[ForwarderAppnavControllerGroup] = Field( serialization_alias="appnavControllerGroup", validation_alias="appnavControllerGroup" @@ -161,7 +161,9 @@ class ForwarderRole(BaseModel): service_node_group: List[ForwarderNodeGroup] = Field( serialization_alias="serviceNodeGroup", validation_alias="serviceNodeGroup" ) - service_context: ServiceContext = Field(serialization_alias="serviceContext", validation_alias="serviceContext") + service_context: ServiceContext = Field( + default_factory=ServiceContext, serialization_alias="serviceContext", validation_alias="serviceContext" + ) # Forwarder and Service @@ -178,7 +180,7 @@ class ForwarderAndServiceNodeController(BaseModel): class ForwarderAndServiceNodeAppnavControllerGroup(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_name: Default[AppnavControllerGroupName] = Field( default=Default[AppnavControllerGroupName](value="ACG-APPQOE"), @@ -191,7 +193,7 @@ class ForwarderAndServiceNodeAppnavControllerGroup(BaseModel): class ForwarderAndServiceNodeGroup(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Default[ServiceNodeGroupName] = Default[ServiceNodeGroupName](value="SNG-APPQOE") internal: Default[bool] = Default[bool](value=True) @@ -201,7 +203,7 @@ class ForwarderAndServiceNodeGroup(BaseModel): class ForwarderAndServiceNodeRole(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") appnav_controller_group: List[ForwarderAndServiceNodeAppnavControllerGroup] = Field( serialization_alias="appnavControllerGroup", validation_alias="appnavControllerGroup" @@ -217,7 +219,7 @@ class ForwarderAndServiceNodeRole(BaseModel): class ServiceNodeInformationExternal(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Default[ServiceNodeExternalAddress] = Default[ServiceNodeExternalAddress](value="192.168.2.2") vpg_ip: Default[ServiceNodeExternalVpgIp] = Field( @@ -228,7 +230,7 @@ class ServiceNodeInformationExternal(BaseModel): class ServiceNodeGroup(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Default[ServiceNodeGroupName] = Default[ServiceNodeGroupName](value="SNG-APPQOE") external_node: Default[bool] = Field( @@ -240,7 +242,7 @@ class ServiceNodeGroup(BaseModel): class ServiceNodeRole(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") service_node_group: List[ServiceNodeGroup] = Field( default=[ServiceNodeGroup()], serialization_alias="serviceNodeGroup", validation_alias="serviceNodeGroup" @@ -248,7 +250,8 @@ class ServiceNodeRole(BaseModel): class AppqoeParcel(_ParcelBase): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + type_: Literal["appqoe"] = Field(default="appqoe", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") dreopt: Optional[Union[Global[bool], Default[bool]]] = Field( default=as_default(False), validation_alias=AliasPath("data", "dreopt") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/bgp.py b/catalystwan/models/configuration/feature_profile/sdwan/service/bgp.py index 6abf2ffe..21e08b24 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/bgp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/bgp.py @@ -3,14 +3,14 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.configuration.feature_profile.common import Prefix class AggregatePrefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") prefix: Prefix as_set: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( @@ -22,7 +22,7 @@ class AggregatePrefix(BaseModel): class AggregatePrefixIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") prefix: Union[Global[str], Variable] as_set: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( @@ -34,13 +34,13 @@ class AggregatePrefixIPv6(BaseModel): class NetworkPrefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") prefix: Prefix class NetworkPrefixIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") prefix: Union[Global[str], Variable] @@ -65,7 +65,7 @@ class NetworkPrefixIPv6(BaseModel): class RedistributedRoute(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") protocol: Union[Global[RedistributeProtocol], Variable] route_policy: Optional[Union[Default[None], Global[UUID]]] = Field( @@ -74,7 +74,7 @@ class RedistributedRoute(BaseModel): class RedistributedRouteIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") protocol: Union[Global[RedistributeProtocolIPv6], Variable] route_policy: Optional[Union[Default[None], Global[UUID]]] = Field( @@ -83,7 +83,7 @@ class RedistributedRouteIPv6(BaseModel): class AddressFamilyIPv4(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") aggregate_address: Optional[List[AggregatePrefix]] = Field( serialization_alias="aggregateAddress", validation_alias="aggregateAddress" @@ -97,13 +97,13 @@ class AddressFamilyIPv4(BaseModel): class PolicyType(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") policy_type: Global[str] = Field(serialization_alias="policyType", validation_alias="policyType") class PolicyTypeWithThreshold(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") policy_type: Global[str] = Field(serialization_alias="policyType", validation_alias="policyType") prefix_number: Union[Global[int], Variable] = Field(serialization_alias="prefixNum", validation_alias="prefixNum") @@ -111,7 +111,7 @@ class PolicyTypeWithThreshold(BaseModel): class PolicyTypeWithRestart(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") policy_type: Global[str] = Field(serialization_alias="policyType", validation_alias="policyType") prefix_number: Union[Global[int], Variable] = Field(serialization_alias="prefixNum", validation_alias="prefixNum") @@ -120,7 +120,7 @@ class PolicyTypeWithRestart(BaseModel): class AddressFamilyIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") aggregate_address: Optional[List[AggregatePrefixIPv6]] = Field( serialization_alias="ipv6AggregateAddress", validation_alias="ipv6AggregateAddress" @@ -136,7 +136,7 @@ class AddressFamilyIPv6(BaseModel): class BgpAddressFamily(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") family_type: Global[str] = Field(serialization_alias="familyType", validation_alias="familyType") max_prefix_config: Optional[Union[PolicyType, PolicyTypeWithRestart, PolicyTypeWithThreshold]] = Field( @@ -151,7 +151,7 @@ class BgpAddressFamily(BaseModel): class BgpIPv4Neighbor(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Global[str], Variable] description: Optional[Union[Global[str], Variable, Default[None]]] = None @@ -193,7 +193,7 @@ class BgpIPv4Neighbor(BaseModel): class BgpIPv6Neighbor(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Global[str], Variable] description: Optional[Union[Global[str], Variable, Default[None]]] = None @@ -234,55 +234,58 @@ class BgpIPv6Neighbor(BaseModel): ) -class BgpData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class BgpParcel(_ParcelBase): + type_: Literal["routing/bgp"] = Field(default="routing/bgp", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - as_num: Union[Global[int], Variable] = Field(serialization_alias="asNum", validation_alias="asNum") + as_num: Union[Global[int], Variable] = Field(validation_alias=AliasPath("data", "asNum")) router_id: Optional[Union[Global[str], Variable, Default[None]]] = Field( - serialization_alias="routerId", validation_alias="routerId", default=None + validation_alias=AliasPath("data", "routerId"), default=None ) propagate_aspath: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="propagateAspath", validation_alias="propagateAspath", default=Default[bool](value=False) + validation_alias=AliasPath("data", "propagateAspath"), default=Default[bool](value=False) ) propagate_community: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="propagateCommunity", - validation_alias="propagateCommunity", + validation_alias=AliasPath("data", "propagateCommunity"), default=Default[bool](value=False), ) - external: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=20) - internal: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=200) - local: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=20) - keepalive: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=60) - holdtime: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=180) + external: Optional[Union[Global[int], Variable, Default[int]]] = Field( + validation_alias=AliasPath("data", "external"), default=Default[int](value=20) + ) + internal: Optional[Union[Global[int], Variable, Default[int]]] = Field( + validation_alias=AliasPath("data", "internal"), default=Default[int](value=200) + ) + local: Optional[Union[Global[int], Variable, Default[int]]] = Field( + validation_alias=AliasPath("data", "local"), default=Default[int](value=20) + ) + keepalive: Optional[Union[Global[int], Variable, Default[int]]] = Field( + validation_alias=AliasPath("data", "keepalive"), default=Default[int](value=60) + ) + holdtime: Optional[Union[Global[int], Variable, Default[int]]] = Field( + validation_alias=AliasPath("data", "holdtime"), default=Default[int](value=180) + ) always_compare: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="alwaysCompare", validation_alias="alwaysCompare", default=Default[bool](value=False) + validation_alias=AliasPath("data", "alwaysCompare"), default=Default[bool](value=False) + ) + deterministic: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( + validation_alias=AliasPath("data", "deterministic"), default=Default[bool](value=False) ) - deterministic: Optional[Union[Global[bool], Variable, Default[bool]]] = Default[bool](value=False) missing_as_worst: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="missingAsWorst", validation_alias="missingAsWorst", default=Default[bool](value=False) + validation_alias=AliasPath("data", "missingAsWorst"), default=Default[bool](value=False) ) compare_router_id: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="compareRouterId", validation_alias="compareRouterId", default=Default[bool](value=False) + validation_alias=AliasPath("data", "compareRouterId"), default=Default[bool](value=False) ) multipath_relax: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="multipathRelax", validation_alias="multipathRelax", default=Default[bool](value=False) + validation_alias=AliasPath("data", "multipathRelax"), default=Default[bool](value=False) ) - neighbor: Optional[List[BgpIPv4Neighbor]] = None + neighbor: Optional[List[BgpIPv4Neighbor]] = Field(validation_alias=AliasPath("data", "neighbor"), default=None) ipv6_neighbor: Optional[List[BgpIPv6Neighbor]] = Field( - serialization_alias="ipv6Neighbor", validation_alias="ipv6Neighbor", default=None + validation_alias=AliasPath("data", "ipv6Neighbor"), default=None ) address_family: Optional[AddressFamilyIPv4] = Field( - serialization_alias="addressFamily", validation_alias="addressFamily", default=None + validation_alias=AliasPath("data", "addressFamily"), default=None ) ipv6_address_family: Optional[AddressFamilyIPv6] = Field( - serialization_alias="ipv6AddressFamily", validation_alias="ipv6AddressFamily", default=None + validation_alias=AliasPath("data", "ipv6AddressFamily"), default=None ) - - -class BgpCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: BgpData - metadata: Optional[dict] = None diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/dhcp_server.py b/catalystwan/models/configuration/feature_profile/sdwan/service/dhcp_server.py index 796ced85..1bee6075 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/dhcp_server.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/dhcp_server.py @@ -79,7 +79,7 @@ class StaticLeaseItem(BaseModel): @field_validator("mac_address") @classmethod def check_mac_address(cls, mac_address: Union[Global[str], Variable]): - if isinstance(mac_address, Variable): + if mac_address.option_type == "variable": return mac_address value = mac_address.value if MAC_PATTERN_1.match(value) or MAC_PATTERN_2.match(value): diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/eigrp.py b/catalystwan/models/configuration/feature_profile/sdwan/service/eigrp.py index c04d3153..838e44cd 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/eigrp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/eigrp.py @@ -3,9 +3,9 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.configuration.feature_profile.common import Prefix EigrpAuthType = Literal[ @@ -31,7 +31,7 @@ class KeychainDetails(BaseModel): class EigrpAuthentication(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") auth_type: Union[Global[EigrpAuthType], Variable, Default[None]] = Field( serialization_alias="type", validation_alias="type" @@ -43,20 +43,20 @@ class EigrpAuthentication(BaseModel): class TableMap(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Optional[Union[Default[None], Global[UUID]]] = Default[None](value=None) filter: Optional[Union[Global[bool], Variable, Default[bool]]] = Default[bool](value=False) class SummaryAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") prefix: Prefix class IPv4StaticRoute(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Union[Global[str], Variable] shutdown: Optional[Union[Global[int], Variable, Default[bool]]] = Default[bool](value=False) @@ -66,41 +66,35 @@ class IPv4StaticRoute(BaseModel): class RedistributeIntoEigrp(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") protocol: Union[Global[RedistributeProtocol], Variable] route_policy: Optional[Union[Default[None], Global[UUID]]] = Default[None](value=None) class AddressFamily(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") redistribute: Optional[List[RedistributeIntoEigrp]] = None network: List[SummaryAddress] -class EigrpData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class EigrpParcel(_ParcelBase): + type_: Literal["routing/eigrp"] = Field(default="routing/eigrp", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - as_number: Union[Global[int], Variable] = Field(serialization_alias="asNum", validation_alias="asNum") - address_family: AddressFamily = Field(serialization_alias="addressFamily", validation_alias="addressFamily") + as_number: Union[Global[int], Variable] = Field(validation_alias=AliasPath("data", "asNum")) + address_family: AddressFamily = Field(validation_alias=AliasPath("data", "addressFamily")) hello_interval: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="helloInterval", validation_alias="helloInterval", default=Default[int](value=5) + validation_alias=AliasPath("data", "helloInterval"), default=Default[int](value=5) ) hold_time: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="holdTime", validation_alias="holdTime", default=Default[int](value=15) + validation_alias=AliasPath("data", "holdTime"), default=Default[int](value=15) + ) + authentication: Optional[EigrpAuthentication] = Field( + validation_alias=AliasPath("data", "authentication"), default=None ) - authentication: Optional[EigrpAuthentication] = None af_interface: Optional[List[IPv4StaticRoute]] = Field( - serialization_alias="afInterface", validation_alias="afInterface", default=None + validation_alias=AliasPath("data", "afInterface"), default=None ) - table_map: TableMap = Field(serialization_alias="tableMap", validation_alias="tableMap", default=TableMap()) - - -class EigrpCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: EigrpData - metadata: Optional[dict] = None + table_map: TableMap = Field(validation_alias=AliasPath("data", "tableMap"), default=TableMap()) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/common.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/common.py index 59fd3dcd..2509e26c 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/common.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/common.py @@ -1,5 +1,6 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates +from ipaddress import IPv4Address, IPv6Address, IPv6Interface from typing import Literal, Optional, Union from uuid import UUID @@ -68,14 +69,14 @@ class Arp(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - ip_address: Union[Variable, Global[str], Default[None]] + ip_address: Union[Variable, Global[str], Global[IPv4Address], Default[None]] mac_address: Union[Global[str], Variable] class VrrpTrackingObject(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") tracker_id: Union[Default[None], Global[UUID]] = Field( serialization_alias="trackerId", validation_alias="trackerId" @@ -89,26 +90,26 @@ class VrrpTrackingObject(BaseModel): class VrrpIPv6Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - ipv6_link_local: Union[Global[str], Variable] = Field( + ipv6_link_local: Union[Global[str], Global[IPv6Address], Variable] = Field( serialization_alias="ipv6LinkLocal", validation_alias="ipv6LinkLocal" ) - prefix: Optional[Union[Global[str], Variable, Default[None]]] = None + prefix: Optional[Union[Global[str], Global[IPv6Interface], Variable, Default[None]]] = None class StaticIPv4Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - ip_address: Union[Variable, Global[str], Default[None]] = Field( - serialization_alias="ipAddress", validation_alias="ipAddress" + ip_address: Union[Variable, Global[str], Global[IPv4Address], Default[None]] = Field( + serialization_alias="ipAddress", validation_alias="ipAddress", default=Default[None](value=None) ) subnet_mask: Union[Variable, Global[str], Default[None]] = Field( - serialization_alias="subnetMask", validation_alias="subnetMask" + serialization_alias="subnetMask", validation_alias="subnetMask", default=Default[None](value=None) ) class StaticIPv6Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - address: Union[Global[str], Variable] + address: Union[Global[str], Global[IPv6Interface], Variable] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py index 40c39226..463e06dc 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ethernet.py @@ -1,11 +1,12 @@ # 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 BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( Arp, StaticIPv4Address, @@ -34,60 +35,64 @@ class DynamicDhcpDistance(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - dynamic_dhcp_distance: Union[Variable, Global[int], Default[int]] = Default[int](value=1) + dynamic_dhcp_distance: Union[Variable, Global[int], Default[int]] = Field( + default=Default[int](value=1), serialization_alias="dynamicDhcpDistance", validation_alias="dynamicDhcpDistance" + ) class InterfaceDynamicIPv4Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") dynamic: DynamicDhcpDistance class StaticIPv4AddressConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") primary_ip_address: StaticIPv4Address = Field( - serialization_alias="staticIpV4AddressPrimary", validation_alias="staticIpV4AddressPrimary" + serialization_alias="staticIpV4AddressPrimary", + validation_alias="staticIpV4AddressPrimary", + default_factory=StaticIPv4Address, ) - secondary_ip_address: Optional[StaticIPv4Address] = Field( + secondary_ip_address: Optional[List[StaticIPv4Address]] = Field( serialization_alias="staticIpV4AddressSecondary", validation_alias="staticIpV4AddressSecondary", default=None ) class InterfaceStaticIPv4Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - static: StaticIPv4AddressConfig + static: StaticIPv4AddressConfig = Field(default_factory=StaticIPv4AddressConfig) class DynamicIPv6Dhcp(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - dhcp_client: Global[dict] = Field( + dhcp_client: Union[Global[dict], Global[bool]] = Field( serialization_alias="dhcpClient", validation_alias="dhcpClient", default=Global[dict](value={}) ) secondary_ipv6_address: Optional[List[StaticIPv6Address]] = Field( - serialization_alias="secondaryIpV6Address", validation_alias="secondaryIpV6Address" + serialization_alias="secondaryIpV6Address", validation_alias="secondaryIpV6Address", default=None ) class InterfaceDynamicIPv6Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") dynamic: DynamicIPv6Dhcp class Dhcpv6Helper(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ip_address: Union[Global[str], Variable] = Field(serialization_alias="ipAddress", validation_alias="ipAddress") vpn: Optional[Union[Global[int], Variable, Default[None]]] = None class StaticIPv6AddressConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") primary_ip_address: StaticIPv6Address = Field( serialization_alias="staticIpV6AddressPrimary", validation_alias="staticIpV6AddressPrimary" @@ -101,18 +106,18 @@ class StaticIPv6AddressConfig(BaseModel): class InterfaceStaticIPv6Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") static: StaticIPv6AddressConfig class NatPool(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - range_start: Union[Variable, Global[str], Default[None]] = Field( + range_start: Union[Variable, Global[str], Global[IPv4Address], Default[None]] = Field( serialization_alias="rangeStart", validation_alias="rangeStart" ) - range_end: Union[Variable, Global[str], Default[None]] = Field( + range_end: Union[Variable, Global[str], Global[IPv4Address], Default[None]] = Field( serialization_alias="rangeEnd", validation_alias="rangeEnd" ) prefix_length: Union[Variable, Global[int], Default[None]] = Field( @@ -122,11 +127,13 @@ class NatPool(BaseModel): class StaticNat(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - source_ip: Union[Global[str], Variable] = Field(serialization_alias="sourceIp", validation_alias="sourceIp") + source_ip: Union[Global[str], Global[IPv4Address], Variable] = Field( + serialization_alias="sourceIp", validation_alias="sourceIp" + ) - translate_ip: Union[Global[str], Variable] = Field( + translate_ip: Union[Global[str], Global[IPv4Address], Variable] = Field( serialization_alias="translateIp", validation_alias="translateIp" ) static_nat_direction: Union[Global[Direction], Default[Direction]] = Field( @@ -140,7 +147,7 @@ class StaticNat(BaseModel): class NatAttributesIPv4(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") nat_type: Union[Global[NatType], Variable] = Field(serialization_alias="natType", validation_alias="natType") nat_pool: Optional[NatPool] = Field(serialization_alias="natPool", validation_alias="natPool", default=None) @@ -151,7 +158,7 @@ class NatAttributesIPv4(BaseModel): serialization_alias="udpTimeout", validation_alias="udpTimeout", default=Default[int](value=1) ) tcp_timeout: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="tcpTimeout", validation_alias="tcpTimeout", default=Default[int](value=1) + serialization_alias="tcpTimeout", validation_alias="tcpTimeout", default=Default[int](value=60) ) new_static_nat: Optional[List[StaticNat]] = Field( serialization_alias="newStaticNat", validation_alias="newStaticNat", default=None @@ -159,13 +166,13 @@ class NatAttributesIPv4(BaseModel): class NatAttributesIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") nat64: Optional[Union[Global[bool], Default[bool]]] = Default[bool](value=False) class AclQos(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") shaping_rate: Optional[Union[Global[int], Variable, Default[None]]] = Field( serialization_alias="shapingRate", validation_alias="shapingRate", default=None @@ -185,7 +192,7 @@ class AclQos(BaseModel): class VrrpIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_id: Union[Variable, Global[int]] = Field(serialization_alias="groupId", validation_alias="groupId") priority: Union[Variable, Global[int], Default[int]] = Default[int](value=100) @@ -197,7 +204,7 @@ class VrrpIPv6(BaseModel): class VrrpIPv4(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_id: Union[Variable, Global[int]] = Field(serialization_alias="groupId", validation_alias="groupId") priority: Union[Variable, Global[int], Default[int]] = Default[int](value=100) @@ -205,9 +212,13 @@ class VrrpIPv4(BaseModel): track_omp: Union[Global[bool], Default[bool]] = Field( serialization_alias="trackOmp", validation_alias="trackOmp", default=Default[bool](value=False) ) - ip_address: Union[Global[str], Variable] = Field(serialization_alias="ipAddress", validation_alias="ipAddress") + ip_address: Union[Global[str], Global[IPv4Address], Variable] = Field( + serialization_alias="ipAddress", validation_alias="ipAddress" + ) ip_address_secondary: Optional[List[StaticIPv4Address]] = Field( - serialization_alias="ipAddressSecondary", validation_alias="ipAddressSecondary" + serialization_alias="ipAddressSecondary", + validation_alias="ipAddressSecondary", + default=None, ) tloc_pref_change: Union[Global[bool], Default[bool]] = Field( serialization_alias="tlocPrefChange", validation_alias="tlocPrefChange", default=Default[bool](value=False) @@ -221,7 +232,7 @@ class VrrpIPv4(BaseModel): class Trustsec(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") enable_sgt_propagation: Union[Global[bool], Default[bool]] = Field( serialization_alias="enableSGTPropagation", @@ -240,8 +251,8 @@ class Trustsec(BaseModel): ) -class AdvancedAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class AdvancedEthernetAttributes(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") duplex: Optional[Union[Global[DuplexMode], Variable, Default[None]]] = None mac_address: Optional[Union[Global[str], Variable, Default[None]]] = Field( @@ -273,7 +284,7 @@ class AdvancedAttributes(BaseModel): validation_alias="icmpRedirectDisable", default=Default[bool](value=True), ) - xconnect: Optional[Union[Global[str], Variable, Default[None]]] = None + xconnect: Optional[Union[Global[str], Global[IPv4Address], Variable, Default[None]]] = None ip_directed_broadcast: Union[Global[bool], Variable, Default[bool]] = Field( serialization_alias="ipDirectedBroadcast", validation_alias="ipDirectedBroadcast", @@ -281,47 +292,43 @@ class AdvancedAttributes(BaseModel): ) -class InterfaceEthernetData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class InterfaceEthernetParcel(_ParcelBase): + type_: Literal["ethernet"] = Field(default="ethernet", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - shutdown: Union[Global[bool], Variable, Default[bool]] = Default[bool](value=True) - interface_name: Union[Global[str], Variable] = Field( - serialization_alias="interfaceName", validation_alias="interfaceName" + shutdown: Union[Global[bool], Variable, Default[bool]] = Field( + default=Default[bool](value=True), validation_alias=AliasPath("data", "shutdown") + ) + interface_name: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "interfaceName")) + ethernet_description: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=Default[None](value=None), validation_alias=AliasPath("data", "description") ) - description: Optional[Union[Global[str], Variable, Default[None]]] = None interface_ip_address: Union[InterfaceDynamicIPv4Address, InterfaceStaticIPv4Address] = Field( - serialization_alias="intfIpAddress", validation_alias="intfIpAddress" + validation_alias=AliasPath("data", "intfIpAddress"), default_factory=InterfaceStaticIPv4Address ) dhcp_helper: Optional[Union[Variable, Global[List[str]], Default[None]]] = Field( - serialization_alias="dhcpHelper", validation_alias="dhcpHelper", default=None + validation_alias=AliasPath("data", "dhcpHelper"), default=None ) interface_ipv6_address: Optional[Union[InterfaceDynamicIPv6Address, InterfaceStaticIPv6Address]] = Field( - serialization_alias="intfIpV6Address", validation_alias="intfIpV6Address", default=None + validation_alias=AliasPath("data", "intfIpV6Address"), default=None + ) + nat: Union[Global[bool], Default[bool]] = Field( + validation_alias=AliasPath("data", "nat"), default=Default[bool](value=False) ) - nat: Union[Global[bool], Default[bool]] = Default[bool](value=False) nat_attributes_ipv4: Optional[NatAttributesIPv4] = Field( - serialization_alias="natAttributesIpv4", validation_alias="natAttributesIpv4", default=None + validation_alias=AliasPath("data", "natAttributesIpv4"), default=None ) nat_ipv6: Optional[Union[Global[bool], Default[bool]]] = Field( - serialization_alias="natIpv6", validation_alias="natIpv6", default=Default[bool](value=False) + validation_alias=AliasPath("data", "natIpv6"), default=Default[bool](value=False) ) nat_attributes_ipv6: Optional[NatAttributesIPv6] = Field( - serialization_alias="natAttributesIpv6", validation_alias="natAttributesIpv6", default=None + validation_alias=AliasPath("data", "natAttributesIpv6"), default=None ) - acl_qos: Optional[AclQos] = Field(serialization_alias="aclQos", validation_alias="aclQos", default=None) - vrrp_ipv6: Optional[List[VrrpIPv6]] = Field( - serialization_alias="vrrpIpv6", validation_alias="vrrpIpv6", default=None + acl_qos: Optional[AclQos] = Field(validation_alias=AliasPath("data", "aclQos"), default=None) + vrrp_ipv6: Optional[List[VrrpIPv6]] = Field(validation_alias=AliasPath("data", "vrrpIpv6"), default=None) + vrrp: Optional[List[VrrpIPv4]] = Field(validation_alias=AliasPath("data", "vrrp"), default=None) + arp: Optional[List[Arp]] = Field(validation_alias=AliasPath("data", "arp"), default=None) + trustsec: Optional[Trustsec] = Field(validation_alias=AliasPath("data", "trustsec"), default=None) + advanced: AdvancedEthernetAttributes = Field( + validation_alias=AliasPath("data", "advanced"), default_factory=AdvancedEthernetAttributes ) - vrrp: Optional[List[VrrpIPv4]] = None - arp: Optional[List[Arp]] = None - trustsec: Optional[Trustsec] = None - advanced: AdvancedAttributes = AdvancedAttributes() - - -class InterfaceEthernetCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: InterfaceEthernetData - metadata: Optional[dict] = None diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py index 5858f7d2..bbfa03b8 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/gre.py @@ -1,10 +1,11 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates +from ipaddress import IPv4Address, IPv6Address, IPv6Interface from typing import Literal, Optional, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( IkeCiphersuite, IkeGroup, @@ -21,14 +22,14 @@ class GreAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Variable, Global[str]] mask: Union[Variable, Global[str]] class TunnelSourceIP(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") tunnel_source: Union[Global[str], Variable] = Field( serialization_alias="tunnelSource", validation_alias="tunnelSource" @@ -39,9 +40,9 @@ class TunnelSourceIP(BaseModel): class TunnelSourceIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - tunnel_source_v6: Union[Global[str], Variable] = Field( + tunnel_source_v6: Union[Global[str], Global[IPv6Address], Variable] = Field( serialization_alias="tunnelSourceV6", validation_alias="tunnelSourceV6" ) tunnel_route_via: Optional[Union[Global[str], Variable, Default[None]]] = Field( @@ -50,7 +51,7 @@ class TunnelSourceIPv6(BaseModel): class TunnelSourceInterface(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") tunnel_source_interface: Union[Global[str], Variable] = Field( serialization_alias="tunnelSourceInterface", validation_alias="tunnelSourceInterface" @@ -61,13 +62,13 @@ class TunnelSourceInterface(BaseModel): class GreSourceIp(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_ip: TunnelSourceIP = Field(serialization_alias="sourceIp", validation_alias="sourceIp") class GreSourceNotLoopback(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_not_loopback: TunnelSourceInterface = Field( serialization_alias="sourceNotLoopback", validation_alias="sourceNotLoopback" @@ -75,7 +76,7 @@ class GreSourceNotLoopback(BaseModel): class GreSourceLoopback(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_loopback: TunnelSourceInterface = Field( serialization_alias="sourceLoopback", validation_alias="sourceLoopback" @@ -83,39 +84,39 @@ class GreSourceLoopback(BaseModel): class GreSourceIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") source_ipv6: TunnelSourceIPv6 = Field(serialization_alias="sourceIpv6", validation_alias="sourceIpv6") class BasicGre(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - interface_name: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="ifName", validation_alias="ifName", default=None + if_name: Union[Global[str], Variable] = Field( + serialization_alias="ifName", validation_alias="ifName", description="Minimum length of the value should be 4." ) - description: Optional[Union[Global[str], Variable, Default[None]]] = None + description: Union[Global[str], Variable, Default[None]] = Field(default=Default[None](value=None)) address: Optional[GreAddress] = None - ipv6_address: Optional[Union[Global[str], Variable, Default[None]]] = Field( + ipv6_address: Optional[Union[Global[str], Global[IPv6Interface], Variable, Default[None]]] = Field( serialization_alias="ipv6Address", validation_alias="ipv6Address", default=None ) shutdown: Optional[Union[Global[bool], Variable, Default[bool]]] = Default[bool](value=False) tunnel_protection: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( serialization_alias="tunnelProtection", validation_alias="tunnelProtection", default=Default[bool](value=False) ) - tunnel_mode: Optional[Union[Global[GreTunnelMode], Default[GreTunnelMode]]] = Field( + tunnel_mode: Union[Global[GreTunnelMode], Default[GreTunnelMode]] = Field( + default=Default[GreTunnelMode](value="ipv4"), serialization_alias="tunnelMode", validation_alias="tunnelMode", - default=Default[GreTunnelMode](value="ipv4"), ) tunnel_source_type: Optional[Union[GreSourceIp, GreSourceNotLoopback, GreSourceLoopback, GreSourceIPv6]] = Field( serialization_alias="tunnelSourceType", validation_alias="tunnelSourceType", default=None ) - tunnel_destination: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="tunnelDestination", validation_alias="tunnelDestination", default=None + tunnel_destination: Union[Global[str], Global[IPv4Address], Variable] = Field( + serialization_alias="tunnelDestination", validation_alias="tunnelDestination" ) - tunnel_destination_v6: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="tunnelDestinationV6", validation_alias="tunnelDestinationV6", default=None + tunnel_destination_v6: Optional[Union[Global[str], Global[IPv6Address], Variable]] = Field( + default=None, serialization_alias="tunnelDestinationV6", validation_alias="tunnelDestinationV6" ) mtu: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=1500) mtu_v6: Optional[Union[Global[int], Variable, Default[None]]] = Field( @@ -185,22 +186,14 @@ class BasicGre(BaseModel): class AdvancedGre(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") application: Optional[Union[Global[TunnelApplication], Variable]] = None -class InterfaceGreData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - basic: BasicGre - advanced: Optional[AdvancedGre] = None - - -class InterfaceGreCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class InterfaceGreParcel(_ParcelBase): + type_: Literal["gre"] = Field(default="gre", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - name: str - description: Optional[str] = None - data: InterfaceGreData - metadata: Optional[dict] = None + basic: BasicGre = Field(validation_alias=AliasPath("data", "basic")) + advanced: Optional[AdvancedGre] = Field(default=None, validation_alias=AliasPath("data", "advanced")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py index de9c90d1..89e3b613 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/ipsec.py @@ -1,10 +1,11 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates +from ipaddress import IPv4Interface, IPv6Address, IPv6Interface from typing import Literal, Optional, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( IkeCiphersuite, IkeGroup, @@ -22,117 +23,110 @@ class IpsecAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Variable, Global[str]] mask: Union[Variable, Global[str]] -class InterfaceIpsecData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class InterfaceIpsecParcel(_ParcelBase): + type_: Literal["ipsec"] = Field(default="ipsec", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - interface_name: Union[Global[str], Variable] = Field(serialization_alias="ifName", validation_alias="ifName") - shutdown: Union[Global[bool], Variable, Default[bool]] = Default[bool](value=True) + interface_name: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "ifName")) + shutdown: Union[Global[bool], Variable, Default[bool]] = Field( + default=Default[bool](value=True), validation_alias=AliasPath("data", "shutdown") + ) tunnel_mode: Optional[Union[Global[IpsecTunnelMode], Default[IpsecTunnelMode]]] = Field( - serialization_alias="tunnelMode", - validation_alias="tunnelMode", + validation_alias=AliasPath("data", "tunnelMode"), default=Default[IpsecTunnelMode](value="ipv4"), ) - description: Union[Global[str], Variable, Default[None]] = Default[None](value=None) - address: Optional[IpsecAddress] = None - ipv6_address: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="ipv6Address", validation_alias="ipv6Address", default=None + ipsec_description: Union[Global[str], Variable, Default[None]] = Field( + default=Default[None](value=None), validation_alias=AliasPath("data", "description") ) - tunnel_source: Optional[IpsecAddress] = Field( - serialization_alias="tunnelSource", validation_alias="tunnelSource", default=None + address: Optional[IpsecAddress] = Field(default=None, validation_alias=AliasPath("data", "address")) + ipv6_address: Optional[Union[Global[str], Global[IPv6Interface], Variable]] = Field( + validation_alias=AliasPath("data", "ipv6Address"), default=None ) + tunnel_source: Optional[IpsecAddress] = Field(validation_alias=AliasPath("data", "tunnelSource"), default=None) tunnel_source_v6: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="tunnelSourceV6", validation_alias="tunnelSourceV6", default=None + validation_alias=AliasPath("data", "tunnelSourceV6"), default=None ) - tunnel_source_interface: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="tunnelSourceInterface", validation_alias="tunnelSourceInterface", default=None + tunnel_source_interface: Optional[Union[Global[str], Global[IPv4Interface], Variable]] = Field( + validation_alias=AliasPath("data", "tunnelSourceInterface"), default=None ) tunnel_destination: Optional[IpsecAddress] = Field( - serialization_alias="tunnelDestination", validation_alias="tunnelDestination", default=None + validation_alias=AliasPath("data", "tunnelDestination"), default=None ) - tunnel_destination_v6: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="tunnelDestinationV6", validation_alias="tunnelDestinationV6", default=None + tunnel_destination_v6: Optional[Union[Global[str], Global[IPv6Address], Variable]] = Field( + validation_alias=AliasPath("data", "tunnelDestinationV6"), default=None ) - application: Union[Global[TunnelApplication], Variable] + application: Union[Global[TunnelApplication], Variable] = Field(validation_alias=AliasPath("data", "application")) tcp_mss_adjust: Union[Global[int], Variable, Default[None]] = Field( - serialization_alias="tcpMssAdjust", validation_alias="tcpMssAdjust", default=Default[None](value=None) + validation_alias=AliasPath("data", "tcpMssAdjust"), default=Default[None](value=None) ) tcp_mss_adjust_v6: Union[Global[int], Variable, Default[None]] = Field( - serialization_alias="tcpMssAdjustV6", validation_alias="tcpMssAdjustV6", default=Default[None](value=None) + validation_alias=AliasPath("data", "tcpMssAdjustV6"), default=Default[None](value=None) ) clear_dont_fragment: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="clearDontFragment", - validation_alias="clearDontFragment", + validation_alias=AliasPath("data", "clearDontFragment"), default=Default[bool](value=False), ) - mtu: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=1500) + mtu: Optional[Union[Global[int], Variable, Default[int]]] = Field( + default=Default[int](value=1500), validation_alias=AliasPath("data", "mtu") + ) mtu_v6: Optional[Union[Global[int], Variable, Default[None]]] = Field( - serialization_alias="mtuV6", validation_alias="mtuV6", default=None + validation_alias=AliasPath("data", "mtuV6"), default=None ) dpd_interval: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="dpdInterval", validation_alias="dpdInterval", default=Default[int](value=10) + validation_alias=AliasPath("data", "dpdInterval"), default=Default[int](value=10) ) dpd_retries: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="dpdRetries", validation_alias="dpdRetries", default=Default[int](value=3) + validation_alias=AliasPath("data", "dpdRetries"), default=Default[int](value=3) ) ike_version: Union[Global[int], Default[int]] = Field( - serialization_alias="ikeVersion", validation_alias="ikeVersion", default=Default[int](value=1) + validation_alias=AliasPath("data", "ikeVersion"), default=Default[int](value=1) ) ike_mode: Optional[Union[Global[IkeMode], Variable, Default[IkeMode]]] = Field( - serialization_alias="ikeMode", validation_alias="ikeMode", default=Default[IkeMode](value="main") + validation_alias=AliasPath("data", "ikeMode"), default=Default[IkeMode](value="main") ) ike_rekey_interval: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="ikeRekeyInterval", validation_alias="ikeRekeyInterval", default=Default[int](value=14400) + validation_alias=AliasPath("data", "ikeRekeyInterval"), default=Default[int](value=14400) ) ike_ciphersuite: Union[Global[IkeCiphersuite], Variable, Default[IkeCiphersuite]] = Field( - serialization_alias="ikeCiphersuite", - validation_alias="ikeCiphersuite", + validation_alias=AliasPath("data", "ikeCiphersuite"), default=Default[IkeCiphersuite](value="aes256-cbc-sha1"), ) ike_group: Union[Global[IkeGroup], Variable, Default[IkeGroup]] = Field( - serialization_alias="ikeGroup", validation_alias="ikeGroup", default=Default[IkeGroup](value="16") + validation_alias=AliasPath("data", "ikeGroup"), default=Default[IkeGroup](value="16") ) pre_shared_secret: Union[Global[str], Variable] = Field( - serialization_alias="preSharedSecret", validation_alias="preSharedSecret" + validation_alias=AliasPath("data", "preSharedSecret"), ) ike_local_id: Union[Global[str], Variable, Default[None]] = Field( - serialization_alias="ikeLocalId", validation_alias="ikeLocalId" + validation_alias=AliasPath("data", "ikeLocalId"), default=Default[None](value=None) ) ike_remote_id: Union[Global[str], Variable, Default[None]] = Field( - serialization_alias="ikeRemoteId", validation_alias="ikeRemoteId" + validation_alias=AliasPath("data", "ikeRemoteId"), default=Default[None](value=None) ) ipsec_rekey_interval: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="ipsecRekeyInterval", - validation_alias="ipsecRekeyInterval", + validation_alias=AliasPath("data", "ipsecRekeyInterval"), default=Default[int](value=3600), ) ipsec_replay_window: Union[Global[int], Variable, Default[int]] = Field( - serialization_alias="ipsecReplayWindow", validation_alias="ipsecReplayWindow", default=Default[int](value=512) + validation_alias=AliasPath("data", "ipsecReplayWindow"), default=Default[int](value=512) ) ipsec_ciphersuite: Union[Global[IpsecCiphersuite], Variable, Default[IpsecCiphersuite]] = Field( - serialization_alias="ipsecCiphersuite", - validation_alias="ipsecCiphersuite", + validation_alias=AliasPath("data", "ipsecCiphersuite"), default=Default[IpsecCiphersuite](value="aes256-gcm"), ) perfect_forward_secrecy: Union[Global[PfsGroup], Variable, Default[PfsGroup]] = Field( - serialization_alias="perfectForwardSecrecy", - validation_alias="perfectForwardSecrecy", + validation_alias=AliasPath("data", "perfectForwardSecrecy"), default=Default[PfsGroup](value="group-16"), ) - tracker: Optional[Union[Global[str], Variable, Default[None]]] = None + tracker: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=None, validation_alias=AliasPath("data", "tracker") + ) tunnel_route_via: Optional[Union[Global[str], Variable, Default[None]]] = Field( - serialization_alias="tunnelRouteVia", validation_alias="tunnelRouteVia", default=None + validation_alias=AliasPath("data", "tunnelRouteVia"), default=None ) - - -class InterfaceIpsecCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: InterfaceIpsecData diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py index f4d6d653..75c33fe0 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/svi.py @@ -1,11 +1,12 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates -from typing import List, Optional, Union +from ipaddress import IPv4Address, IPv6Interface +from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( Arp, StaticIPv4Address, @@ -16,19 +17,19 @@ class VrrpIPv4SecondaryAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Variable, Global[str]] class VrrpIPv6SecondaryAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - prefix: Union[Global[str], Variable] + prefix: Union[Global[str], Global[IPv6Interface], Variable] class VrrpIPv4(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_id: Union[Variable, Global[int]] = Field(serialization_alias="groupId", validation_alias="groupId") priority: Union[Variable, Global[int], Default[int]] = Default[int](value=100) @@ -36,9 +37,11 @@ class VrrpIPv4(BaseModel): track_omp: Union[Global[bool], Default[bool]] = Field( serialization_alias="trackOmp", validation_alias="trackOmp", default=Default[bool](value=False) ) - ip_address: Union[Global[str], Variable] = Field(serialization_alias="ipAddress", validation_alias="ipAddress") + ip_address: Union[Global[str], Global[IPv4Address], Variable] = Field( + serialization_alias="ipAddress", validation_alias="ipAddress" + ) ip_address_secondary: Optional[List[VrrpIPv4SecondaryAddress]] = Field( - serialization_alias="ipAddressSecondary", validation_alias="ipAddressSecondary" + serialization_alias="ipAddressSecondary", validation_alias="ipAddressSecondary", default=None ) tloc_pref_change: Union[Global[bool], Default[bool]] = Field( serialization_alias="tlocPrefChange", validation_alias="tlocPrefChange", default=Default[bool](value=False) @@ -54,7 +57,7 @@ class VrrpIPv4(BaseModel): class VrrpIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_id: Union[Variable, Global[int]] = Field(serialization_alias="groupId", validation_alias="groupId") priority: Union[Variable, Global[int], Default[int]] = Default[int](value=100) @@ -70,14 +73,14 @@ class VrrpIPv6(BaseModel): class Dhcpv6Helper(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Global[str], Variable] = Field(serialization_alias="address", validation_alias="address") vpn: Optional[Union[Global[int], Variable, Default[None]]] = None class AdvancedSviAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") tcp_mss: Optional[Union[Global[int], Variable, Default[None]]] = Field( serialization_alias="tcpMss", validation_alias="tcpMss", default=Default[None](value=None) @@ -97,8 +100,8 @@ class AdvancedSviAttributes(BaseModel): ) -class IPv4Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class IPv4AddressConfiguration(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: StaticIPv4Address = Field(serialization_alias="addressV4", validation_alias="addressV4") secondary_address: Optional[List[StaticIPv4Address]] = Field( @@ -109,10 +112,10 @@ class IPv4Address(BaseModel): ) -class IPv6Address(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class IPv6AddressConfiguration(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - address: Union[Global[str], Variable, Default[None]] = Field( + address: Union[Global[str], Global[IPv6Interface], Variable, Default[None]] = Field( serialization_alias="addressV6", validation_alias="addressV6" ) secondary_address: Optional[List[StaticIPv6Address]] = Field( @@ -124,7 +127,7 @@ class IPv6Address(BaseModel): class AclQos(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ipv4_acl_egress: Optional[Global[UUID]] = Field( serialization_alias="ipv4AclEgress", validation_alias="ipv4AclEgress", default=None @@ -140,38 +143,32 @@ class AclQos(BaseModel): ) -class InterfaceSviData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class InterfaceSviParcel(_ParcelBase): + type_: Literal["svi"] = Field(default="svi", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - shutdown: Union[Global[bool], Variable, Default[bool]] = Default[bool](value=True) - interface_name: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="interfaceName", validation_alias="interfaceName" + shutdown: Union[Global[bool], Variable, Default[bool]] = Field( + default=Default[bool](value=True), validation_alias=AliasPath("data", "shutdown") + ) + interface_name: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "interfaceName")) + svi_description: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=Default[bool](value=True), validation_alias=AliasPath("data", "description") ) - description: Optional[Union[Global[str], Variable, Default[None]]] = Default[None](value=None) interface_mtu: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="ifMtu", validation_alias="ifMtu", default=Default[int](value=1500) + validation_alias=AliasPath("data", "ifMtu"), default=Default[int](value=1500) ) ip_mtu: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="ipMtu", validation_alias="ipMtu", default=Default[int](value=1500) - ) - ipv4: Optional[IPv4Address] = None - ipv6: Optional[IPv6Address] = None - acl_qos: Optional[AclQos] = Field(serialization_alias="aclQos", validation_alias="aclQos", default=None) - arp: Optional[List[Arp]] = None - vrrp: Optional[List[VrrpIPv4]] = None - vrrp_ipv6: Optional[List[VrrpIPv6]] = Field( - serialization_alias="vrrpIpv6", validation_alias="vrrpIpv6", default=None - ) + validation_alias=AliasPath("data", "ipMtu"), default=Default[int](value=1500) + ) + ipv4: Optional[IPv4AddressConfiguration] = Field(default=None, validation_alias=AliasPath("data", "ipv4")) + ipv6: Optional[IPv6AddressConfiguration] = Field(default=None, validation_alias=AliasPath("data", "ipv6")) + acl_qos: Optional[AclQos] = Field(validation_alias=AliasPath("data", "aclQos"), default=None) + arp: Optional[List[Arp]] = Field(default=None, validation_alias=AliasPath("data", "arp")) + vrrp: Optional[List[VrrpIPv4]] = Field(default=None, validation_alias=AliasPath("data", "vrrp")) + vrrp_ipv6: Optional[List[VrrpIPv6]] = Field(validation_alias=AliasPath("data", "vrrpIpv6"), default=None) dhcp_client_v6: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( - serialization_alias="dhcpClientV6", validation_alias="dhcpClientV6", default=Default[bool](value=False) + validation_alias=AliasPath("data", "dhcpClientV6"), default=Default[bool](value=False) + ) + advanced: AdvancedSviAttributes = Field( + default_factory=AdvancedSviAttributes, validation_alias=AliasPath("data", "advanced") ) - advanced: AdvancedSviAttributes = AdvancedSviAttributes() - - -class InterfaceSviCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: InterfaceSviData - metadata: Optional[dict] = None 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 e6f0ac95..ed80c6be 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/lan/vpn.py @@ -1,11 +1,12 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates +from ipaddress import IPv4Address, IPv6Address, IPv6Interface from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default from catalystwan.models.configuration.feature_profile.common import Prefix ProtocolIPv4 = Literal[ @@ -97,10 +98,14 @@ class DnsIPv4(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) primary_dns_address_ipv4: Union[Variable, Global[str], Default[None]] = Field( - serialization_alias="primaryDnsAddressIpv4", validation_alias="primaryDnsAddressIpv4" + default=Default[None](value=None), + serialization_alias="primaryDnsAddressIpv4", + validation_alias="primaryDnsAddressIpv4", ) secondary_dns_address_ipv4: Union[Variable, Global[str], Default[None]] = Field( - serialization_alias="secondaryDnsAddressIpv4", validation_alias="secondaryDnsAddressIpv4" + default=Default[None](value=None), + serialization_alias="secondaryDnsAddressIpv4", + validation_alias="secondaryDnsAddressIpv4", ) @@ -119,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") @@ -134,17 +143,17 @@ 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): model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - prefix: Union[Global[str], Variable] + prefix: Union[Global[str], Global[IPv6Interface], Variable] 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): @@ -174,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) @@ -269,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) @@ -288,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): @@ -370,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) @@ -378,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): @@ -390,8 +401,12 @@ class NatPool(BaseModel): prefix_length: Union[Variable, Global[int]] = Field( serialization_alias="prefixLength", validation_alias="prefixLength" ) - range_start: Union[Variable, Global[str]] = Field(serialization_alias="rangeStart", validation_alias="rangeStart") - range_end: Union[Variable, Global[str]] = Field(serialization_alias="rangeEnd", validation_alias="rangeEnd") + range_start: Union[Variable, Global[str], Global[IPv4Address]] = Field( + serialization_alias="rangeStart", validation_alias="rangeStart" + ) + range_end: Union[Variable, Global[str], Global[IPv4Address]] = Field( + serialization_alias="rangeEnd", validation_alias="rangeEnd" + ) overload: Union[Variable, Global[bool], Default[bool]] = Default[bool](value=True) direction: Union[Variable, Global[Direction]] tracking_object: Optional[dict] = Field( @@ -409,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]] @@ -422,9 +439,11 @@ 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( - serialization_alias="TranslatedSourceIP", validation_alias="TranslatedSourceIP" + 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( serialization_alias="staticNatDirection", validation_alias="staticNatDirection" @@ -460,10 +479,10 @@ class Nat64v4Pool(BaseModel): nat64_v4_pool_name: Union[Variable, Global[str]] = Field( serialization_alias="nat64V4PoolName", validation_alias="nat64V4PoolName" ) - nat64_v4_pool_range_start: Union[Variable, Global[str]] = Field( + nat64_v4_pool_range_start: Union[Variable, Global[str], Global[IPv4Address]] = Field( serialization_alias="nat64V4PoolRangeStart", validation_alias="nat64V4PoolRangeStart" ) - nat64_v4_pool_range_end: Union[Variable, Global[str]] = Field( + nat64_v4_pool_range_end: Union[Variable, Global[str], Global[IPv4Address]] = Field( serialization_alias="nat64V4PoolRangeEnd", validation_alias="nat64V4PoolRangeEnd" ) nat64_v4_pool_overload: Union[Variable, Global[bool], Default[bool]] = Field( @@ -477,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): @@ -558,79 +577,67 @@ class MplsVpnIPv6RouteTarget(BaseModel): ) -class LanVpnData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class LanVpnParcel(_ParcelBase): + type_: Literal["lan/vpn"] = Field(default="lan/vpn", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - vpn_id: Union[Variable, Global[int], Default[int]] = Field(serialization_alias="vpnId", validation_alias="vpnId") - name: Union[Variable, Global[str], Default[None]] - omp_admin_distance: Optional[Union[Variable, Global[int], Default[None]]] = Field( - serialization_alias="ompAdminDistance", validation_alias="ompAdminDistance", default=None + vpn_id: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(1), validation_alias=AliasPath("data", "vpnId") + ) + vpn_name: Union[Variable, Global[str], Default[None]] = Field( + default=Default[None](value=None), validation_alias=AliasPath("data", "name") + ) + 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( - serialization_alias="ompAdminDistanceIpv6", validation_alias="ompAdminDistanceIpv6", default=None + validation_alias=AliasPath("data", "ompAdminDistanceIpv6"), default=None ) - dns_ipv4: Optional[DnsIPv4] = Field(serialization_alias="dnsIpv4", validation_alias="dnsIpv4", default=None) - dns_ipv6: Optional[DnsIPv6] = Field(serialization_alias="dnsIpv6", validation_alias="dnsIpv6", default=None) + dns_ipv4: Optional[DnsIPv4] = Field(validation_alias=AliasPath("data", "dnsIpv4"), default=None) + dns_ipv6: Optional[DnsIPv6] = Field(validation_alias=AliasPath("data", "dnsIpv6"), default=None) new_host_mapping: Optional[List[HostMapping]] = Field( - serialization_alias="newHostMapping", validation_alias="newHostMapping", default=None + validation_alias=AliasPath("data", "newHostMapping"), default=None ) omp_advertise_ipv4: Optional[List[OmpAdvertiseIPv4]] = Field( - serialization_alias="ompAdvertiseIpv4", validation_alias="ompAdvertiseIpv4", default=None + validation_alias=AliasPath("data", "ompAdvertiseIp4"), default=None # API typo ) omp_advertise_ipv6: Optional[List[OmpAdvertiseIPv6]] = Field( - serialization_alias="ompAdvertiseIpv6", validation_alias="ompAdvertiseIpv6", default=None - ) - ipv4_route: Optional[List[StaticRouteIPv4]] = Field( - serialization_alias="ipv4Route", validation_alias="ipv4Route", default=None - ) - ipv6_route: Optional[List[StaticRouteIPv6]] = Field( - serialization_alias="ipv6Route", validation_alias="ipv6Route", default=None + validation_alias=AliasPath("data", "ompAdvertiseIpv6"), default=None ) - service: Optional[List[Service]] = None + ipv4_route: Optional[List[StaticRouteIPv4]] = Field(validation_alias=AliasPath("data", "ipv4Route"), default=None) + ipv6_route: Optional[List[StaticRouteIPv6]] = Field(validation_alias=AliasPath("data", "ipv6Route"), default=None) + service: Optional[List[Service]] = Field(default=None, validation_alias=AliasPath("data", "service")) service_route: Optional[List[ServiceRoute]] = Field( - serialization_alias="serviceRoute", validation_alias="serviceRoute", default=None - ) - gre_route: Optional[List[StaticGreRouteIPv4]] = Field( - serialization_alias="greRoute", validation_alias="greRoute", default=None + validation_alias=AliasPath("data", "serviceRoute"), default=None ) + gre_route: Optional[List[StaticGreRouteIPv4]] = Field(validation_alias=AliasPath("data", "greRoute"), default=None) ipsec_route: Optional[List[StaticIpsecRouteIPv4]] = Field( - serialization_alias="ipsecRoute", validation_alias="ipsecRoute", default=None + validation_alias=AliasPath("data", "ipsecRoute"), default=None ) - nat_pool: Optional[List[NatPool]] = Field(serialization_alias="natPool", validation_alias="natPool", default=None) + nat_pool: Optional[List[NatPool]] = Field(validation_alias=AliasPath("data", "natPool"), default=None) nat_port_forwarding: Optional[List[NatPortForward]] = Field( - serialization_alias="natPortForwarding", validation_alias="natPortForwarding", default=None - ) - static_nat: Optional[List[StaticNat]] = Field( - serialization_alias="staticNat", validation_alias="staticNat", default=None + validation_alias=AliasPath("data", "natPortForwarding"), default=None ) + static_nat: Optional[List[StaticNat]] = Field(validation_alias=AliasPath("data", "staticNat"), default=None) static_nat_subnet: Optional[List[StaticNatSubnet]] = Field( - serialization_alias="staticNatSubnet", validation_alias="staticNatSubnet", default=None - ) - nat64_v4_pool: Optional[List[Nat64v4Pool]] = Field( - serialization_alias="nat64V4Pool", validation_alias="nat64V4Pool", default=None + validation_alias=AliasPath("data", "staticNatSubnet"), default=None ) + nat64_v4_pool: Optional[List[Nat64v4Pool]] = Field(validation_alias=AliasPath("data", "nat64V4Pool"), default=None) route_leak_from_global: Optional[List[RouteLeakFromGlobal]] = Field( - serialization_alias="routeLeakFromGlobal", validation_alias="routeLeakFromGlobal", default=None + validation_alias=AliasPath("data", "routeLeakFromGlobal"), default=None ) route_leak_from_service: Optional[List[RouteLeakFromService]] = Field( - serialization_alias="routeLeakFromService", validation_alias="routeLeakFromService", default=None + validation_alias=AliasPath("data", "routeLeakFromService"), default=None ) route_leak_between_services: Optional[List[RouteLeakBetweenServices]] = Field( - serialization_alias="routeLeakBetweenServices", validation_alias="routeLeakBetweenServices", default=None + validation_alias=AliasPath("data", "routeLeakBetweenServices"), default=None ) mpls_vpn_ipv4_route_target: Optional[MplsVpnIPv4RouteTarget] = Field( - serialization_alias="mplsVpnIpv4RouteTarget", validation_alias="mplsVpnIpv4RouteTarget", default=None + validation_alias=AliasPath("data", "mplsVpnIpv4RouteTarget"), default=None ) mpls_vpn_ipv6_route_target: Optional[MplsVpnIPv6RouteTarget] = Field( - serialization_alias="mplsVpnIpv6RouteTarget", validation_alias="mplsVpnIpv6RouteTarget", default=None + validation_alias=AliasPath("data", "mplsVpnIpv6RouteTarget"), default=None ) enable_sdra: Optional[Union[Global[bool], Default[bool]]] = Field( - serialization_alias="enableSdra", validation_alias="enableSdra", default=None + validation_alias=AliasPath("data", "enableSdra"), default=None ) - - -class LanVpnCreationPayload(BaseModel): - name: str - description: Optional[str] = None - data: LanVpnData - metadata: Optional[dict] = None diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/multicast.py b/catalystwan/models/configuration/feature_profile/sdwan/service/multicast.py index 93742bae..17f31921 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/multicast.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/multicast.py @@ -1,22 +1,22 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase class LocalConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") local: Union[Global[bool], Variable, Default[bool]] = Default[bool](value=False) threshold: Optional[Union[Global[int], Variable, Default[None]]] = Default[None](value=None) class MulticastBasicAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") spt_only: Union[Global[bool], Variable, Default[bool]] = Field( serialization_alias="sptOnly", validation_alias="sptOnly", default=Default[bool](value=False) @@ -27,7 +27,7 @@ class MulticastBasicAttributes(BaseModel): class StaticJoin(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") group_address: Union[Global[str], Variable] = Field( serialization_alias="groupAddress", validation_alias="groupAddress" @@ -38,7 +38,7 @@ class StaticJoin(BaseModel): class IgmpInterfaceParameters(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field( serialization_alias="interfaceName", validation_alias="interfaceName" @@ -50,13 +50,13 @@ class IgmpInterfaceParameters(BaseModel): class IgmpAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface: List[IgmpInterfaceParameters] class SmmFlag(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") enable_ssm_flag: Global[bool] = Global[bool](value=True) range: Optional[Union[Global[str], Variable, Default[None]]] = Default[None](value=None) @@ -68,7 +68,7 @@ class SptThreshold: class SsmAttrubutes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ssm_range_config: SmmFlag = Field(serialization_alias="ssmRangeConfig", validation_alias="ssmRangeConfig") spt_threshold: Optional[Union[Global[SptThreshold], Variable, Default[SptThreshold]]] = Field( @@ -79,7 +79,7 @@ class SsmAttrubutes(BaseModel): class PimInterfaceParameters(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field( serialization_alias="interfaceName", validation_alias="interfaceName" @@ -93,7 +93,7 @@ class PimInterfaceParameters(BaseModel): class StaticRpAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Union[Global[str], Variable] access_list: Union[Global[str], Variable] = Field(serialization_alias="accessList", validation_alias="accessList") @@ -101,7 +101,7 @@ class StaticRpAddress(BaseModel): class RPAnnounce(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field( serialization_alias="interfaceName", validation_alias="interfaceName" @@ -110,7 +110,7 @@ class RPAnnounce(BaseModel): class AutoRpAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") enable_auto_rp_flag: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( serialization_alias="enableAutoRPFlag", validation_alias="enableAutoRPFlag", default=Default[bool](value=False) @@ -124,7 +124,7 @@ class AutoRpAttributes(BaseModel): class RpDiscoveryScope(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field( serialization_alias="interfaceName", validation_alias="interfaceName" @@ -137,7 +137,7 @@ class RpDiscoveryScope(BaseModel): class BsrCandidateAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field( serialization_alias="interfaceName", validation_alias="interfaceName" @@ -150,7 +150,7 @@ class BsrCandidateAttributes(BaseModel): class PimBsrAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") rp_candidate: Optional[List[RpDiscoveryScope]] = Field( serialization_alias="rpCandidate", validation_alias="rpCandidate", default=None @@ -161,7 +161,7 @@ class PimBsrAttributes(BaseModel): class PimAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ssm: SsmAttrubutes interface: Optional[List[PimInterfaceParameters]] = None @@ -173,14 +173,14 @@ class PimAttributes(BaseModel): class DefaultMsdpPeer(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") default_peer: Union[Global[bool], Default[bool]] prefix_list: Optional[Global[UUID]] = None class MsdpPeerAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") peer_ip: Union[Global[str], Variable] = Field(serialization_alias="peerIp", validation_alias="peerIp") connect_source_intf: Optional[Union[Global[str], Variable, Default[None]]] = Field( @@ -203,7 +203,7 @@ class MsdpPeerAttributes(BaseModel): class MsdpPeer(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") mesh_group: Optional[Union[Global[str], Variable, Default[None]]] = Field( serialization_alias="meshGroup", validation_alias="meshGroup", default=None @@ -212,7 +212,7 @@ class MsdpPeer(BaseModel): class MsdpAttributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") msdp_list: Optional[List[MsdpPeer]] = Field( serialization_alias="msdpList", validation_alias="msdpList", default=None @@ -225,19 +225,13 @@ class MsdpAttributes(BaseModel): ) -class MulticastData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class MulticastParcel(_ParcelBase): + type_: Literal["routing/multicast"] = Field(default="routing/multicast", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - basic: MulticastBasicAttributes = MulticastBasicAttributes() - igmp: Optional[IgmpAttributes] = None - pim: Optional[PimAttributes] = None - msdp: Optional[MsdpAttributes] = None - - -class MulticastCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: MulticastData - metadata: Optional[dict] = None + basic: MulticastBasicAttributes = Field( + validation_alias=AliasPath("data", "basic"), default_factory=MulticastBasicAttributes + ) + igmp: Optional[IgmpAttributes] = Field(default=None, validation_alias=AliasPath("data", "igmp")) + pim: Optional[PimAttributes] = Field(default=None, validation_alias=AliasPath("data", "pim")) + msdp: Optional[MsdpAttributes] = Field(default=None, validation_alias=AliasPath("data", "msdp")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/object_tracker.py b/catalystwan/models/configuration/feature_profile/sdwan/service/object_tracker.py index bbfcab5b..c0afddfc 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/object_tracker.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/object_tracker.py @@ -3,9 +3,9 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase ObjectTrackerType = Literal[ "Interface", @@ -21,7 +21,7 @@ class SigTracker(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") object_id: Union[Global[int], Variable] = Field(serialization_alias="objectId", validation_alias="objectId") object_tracker_type: Global[ObjectTrackerType] = Field( @@ -32,7 +32,7 @@ class SigTracker(BaseModel): class InterfaceTracker(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") object_id: Union[Global[int], Variable] = Field(serialization_alias="objectId", validation_alias="objectId") object_tracker_type: Global[ObjectTrackerType] = Field( @@ -44,7 +44,7 @@ class InterfaceTracker(BaseModel): class RouteTracker(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") object_id: Union[Global[int], Variable] = Field(serialization_alias="objectId", validation_alias="objectId") object_tracker_type: Global[ObjectTrackerType] = Field( @@ -60,7 +60,7 @@ class RouteTracker(BaseModel): class ObjectTrackerCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: str description: Optional[str] = None @@ -68,23 +68,17 @@ class ObjectTrackerCreationPayload(BaseModel): class ObjectTrackerRef(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") tracker_ref: Global[UUID] = Field(serialization_alias="trackerRef", validation_alias="trackerRef") -class ObjectTrackerGroupData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class ObjectTrackerGroupParcel(_ParcelBase): + type_: Literal["trackergroup"] = Field(default="trackergroup", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - object_id: Union[Global[int], Variable] = Field(serialization_alias="objectId", validation_alias="objectId") - tracker_refs: List[ObjectTrackerRef] = Field(serialization_alias="trackerRefs", validation_alias="trackerRefs") - criteria: Union[Global[Criteria], Variable, Default[Criteria]] = Default[Criteria](value="or") - - -class ObjectTrackerGroupCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: ObjectTrackerGroupData - metadata: Optional[dict] = None + object_id: Union[Global[int], Variable] = Field(validation_alias=AliasPath("data", "objectId")) + tracker_refs: List[ObjectTrackerRef] = Field(validation_alias=AliasPath("data", "trackerRefs")) + criteria: Union[Global[Criteria], Variable, Default[Criteria]] = Field( + validation_alias=AliasPath("data", "trackerRefs"), default=Default[Criteria](value="or") + ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/ospf.py b/catalystwan/models/configuration/feature_profile/sdwan/service/ospf.py index 86f82666..3f448ac4 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/ospf.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/ospf.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 BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default +from catalystwan.models.common import MetricType NetworkType = Literal[ "broadcast", @@ -26,7 +28,7 @@ "on-startup", ] -RedistributeProtocol = Literal[ +RedistributeProtocolOspf = Literal[ "static", "connected", "bgp", @@ -35,18 +37,16 @@ "eigrp", ] -MetricType = Literal["type1", "type2"] - class SummaryPrefix(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ip_address: Optional[Union[Global[str], Variable]] = None subnet_mask: Optional[Union[Global[str], Variable]] = None class SummaryRoute(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") address: Optional[SummaryPrefix] = None cost: Optional[Union[Global[int], Variable, Default[None]]] = None @@ -56,7 +56,7 @@ class SummaryRoute(BaseModel): class OspfInterfaceParametres(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Optional[Union[Global[str], Variable]] hello_interval: Optional[Union[Global[int], Variable, Default[int]]] = Field( @@ -86,7 +86,7 @@ class OspfInterfaceParametres(BaseModel): class OspfArea(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_number: Union[Global[int], Variable] = Field(serialization_alias="aNum", validation_alias="aNum") area_type: Optional[Union[Global[AreaType], Default[None]]] = Field( @@ -100,66 +100,70 @@ class OspfArea(BaseModel): class RouterLsa(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ad_type: Global[AdvertiseType] = Field(serialization_alias="adType", validation_alias="adType") time: Optional[Union[Global[int], Variable]] = None class RedistributedRoute(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - protocol: Union[Global[RedistributeProtocol], Variable] + protocol: Union[Global[RedistributeProtocolOspf], Variable] dia: Optional[Union[Global[bool], Variable, Default[bool]]] = None - route_policy: Optional[Union[Default[None], Global[UUID]]] = Field( + route_policy: Optional[Union[Default[None], Global[str], Global[UUID]]] = Field( serialization_alias="routePolicy", validation_alias="routePolicy", default=None ) -class OspfData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class OspfParcel(_ParcelBase): + type_: Literal["routing/ospf"] = Field(default="routing/ospf", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - router_id: Optional[Union[Global[str], Variable, Default[None]]] = Field( - serialization_alias="routerId", validation_alias="routerId", default=None + router_id: Optional[Union[Global[str], Global[IPv4Address], Variable, Default[None]]] = Field( + validation_alias=AliasPath("data", "routerId"), default=Default[None](value=None) ) reference_bandwidth: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="referenceBandwidth", validation_alias="referenceBandwidth", default=None + validation_alias=AliasPath("data", "referenceBandwidth"), default=as_default(100) + ) + rfc1583: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( + validation_alias=AliasPath("data", "rfc1583"), default=as_default(False) + ) + originate: Optional[Union[Global[bool], Default[bool]]] = Field( + validation_alias=AliasPath("data", "originate"), default=as_default(False) + ) + always: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( + validation_alias=AliasPath("data", "always"), default=None + ) + metric: Optional[Union[Global[int], Variable, Default[None]]] = Field( + validation_alias=AliasPath("data", "metric"), default=None ) - rfc1583: Optional[Union[Global[bool], Variable, Default[bool]]] = None - originate: Optional[Union[Global[bool], Default[bool]]] = None - always: Optional[Union[Global[bool], Variable, Default[bool]]] = None - metric: Optional[Union[Global[int], Variable, Default[None]]] = None metric_type: Optional[Union[Global[MetricType], Variable, Default[None]]] = Field( - serialization_alias="metricType", validation_alias="metricType", default=None + validation_alias=AliasPath("data", "metricType"), default=None + ) + external: Optional[Union[Global[int], Variable, Default[int]]] = Field( + default=as_default(110), validation_alias=AliasPath("data", "external") ) - external: Optional[Union[Global[int], Variable, Default[int]]] = None inter_area: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="interArea", validation_alias="interArea", default=None + validation_alias=AliasPath("data", "interArea"), default=as_default(110) ) intra_area: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="intraArea", validation_alias="intraArea", default=None + validation_alias=AliasPath("data", "intraArea"), default=as_default(110) + ) + delay: Optional[Union[Global[int], Variable, Default[int]]] = Field( + validation_alias=AliasPath("data", "delay"), default=as_default(200) ) - delay: Optional[Union[Global[int], Variable, Default[int]]] = None initial_hold: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="initialHold", validation_alias="initialHold", default=None + validation_alias=AliasPath("data", "initialHold"), default=as_default(1000) ) max_hold: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="maxHold", validation_alias="maxHold", default=None + validation_alias=AliasPath("data", "maxHold"), default=as_default(10000) ) - redistribute: Optional[List[RedistributedRoute]] = None - router_lsa: Optional[List[RouterLsa]] = Field( - serialization_alias="routerLsa", validation_alias="routerLsa", default=None + redistribute: Optional[List[RedistributedRoute]] = Field( + validation_alias=AliasPath("data", "redistribute"), default=None ) - route_policy: Optional[Union[Default[None], Global[UUID]]] = Field( - serialization_alias="routePolicy", validation_alias="routePolicy", default=None + router_lsa: Optional[List[RouterLsa]] = Field(validation_alias=AliasPath("data", "routerLsa"), default=None) + route_policy: Optional[Union[Default[None], Global[str], Global[UUID]]] = Field( + validation_alias=AliasPath("data", "routePolicy"), default=None ) - area: Optional[List[OspfArea]] = None - - -class OspfCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: OspfData - metadata: Optional[dict] = None + area: Optional[List[OspfArea]] = Field(validation_alias=AliasPath("data", "area"), default=None) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/ospfv3.py b/catalystwan/models/configuration/feature_profile/sdwan/service/ospfv3.py index 88ee9fc4..78274ab1 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, IPv6Interface from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +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,11 +42,10 @@ "omp", "eigrp", ] -MetricType = Literal["type1", "type2"] class NoAuth(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") auth_type: Union[Global[NoAuthType], Default[NoAuthType]] = Field( serialization_alias="authType", @@ -54,7 +55,7 @@ class NoAuth(BaseModel): class IpsecSha1Auth(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") auth_type: Global[IpsecSha1AuthType] = Field( serialization_alias="authType", @@ -66,7 +67,7 @@ class IpsecSha1Auth(BaseModel): class Ospfv3InterfaceParametres(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Optional[Union[Global[str], Variable]] = Field(serialization_alias="ifName", validation_alias="ifName") hello_interval: Optional[Union[Global[int], Variable, Default[int]]] = Field( @@ -92,17 +93,17 @@ class Ospfv3InterfaceParametres(BaseModel): class SummaryRouteIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - network: Union[Global[str], Variable] + network: Union[Global[str], Global[IPv6Interface], Variable] no_advertise: Union[Global[bool], Variable, Default[bool]] = Field( - serialization_alias="noAdvertise", validation_alias="noAdvertise" + serialization_alias="noAdvertise", validation_alias="noAdvertise", default=Default[bool](value=False) ) cost: Optional[Union[Global[int], Variable, Default[None]]] = None class SummaryRoute(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") network: Optional[Prefix] = None cost: Optional[Union[Global[int], Variable, Default[None]]] = None @@ -112,7 +113,7 @@ class SummaryRoute(BaseModel): class StubArea(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_type: Global[str] = Field( serialization_alias="areaType", validation_alias="areaType", default=Global[str](value="stub") @@ -123,7 +124,7 @@ class StubArea(BaseModel): class NssaArea(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_type: Global[str] = Field( serialization_alias="areaType", validation_alias="areaType", default=Global[str](value="nssa") @@ -137,7 +138,7 @@ class NssaArea(BaseModel): class NormalArea(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_type: Global[str] = Field( serialization_alias="areaType", validation_alias="areaType", default=Global[str](value="normal") @@ -145,7 +146,7 @@ class NormalArea(BaseModel): class DefaultArea(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_type: Default[None] = Field( serialization_alias="areaType", validation_alias="areaType", default=Default[None](value=None) @@ -153,29 +154,29 @@ class DefaultArea(BaseModel): class Ospfv3IPv4Area(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_number: Union[Global[int], Variable] = Field(serialization_alias="areaNum", validation_alias="areaNum") area_type_config: Optional[Union[StubArea, NssaArea, NormalArea, DefaultArea]] = Field( serialization_alias="areaTypeConfig", validation_alias="areaTypeConfig", default=None ) - interfaces: List[Ospfv3InterfaceParametres] + interfaces: List[Ospfv3InterfaceParametres] = Field(min_length=1) ranges: Optional[List[SummaryRoute]] = None class Ospfv3IPv6Area(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") area_number: Union[Global[int], Variable] = Field(serialization_alias="areaNum", validation_alias="areaNum") area_type_config: Optional[Union[StubArea, NssaArea, NormalArea, DefaultArea]] = Field( serialization_alias="areaTypeConfig", validation_alias="areaTypeConfig", default=None ) - interfaces: List[Ospfv3InterfaceParametres] - ranges: Optional[List[SummaryRoute]] = None + interfaces: List[Ospfv3InterfaceParametres] = Field(min_length=1) + ranges: Optional[List[SummaryRouteIPv6]] = None class MaxMetricRouterLsa(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") action: Global[MaxMetricRouterLsaAction] on_startup_time: Optional[Union[Global[int], Variable]] = Field( @@ -184,7 +185,7 @@ class MaxMetricRouterLsa(BaseModel): class RedistributedRoute(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") protocol: Union[Global[RedistributeProtocol], Variable] nat_dia: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( @@ -196,7 +197,7 @@ class RedistributedRoute(BaseModel): class RedistributedRouteIPv6(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") protocol: Union[Global[RedistributeProtocolIPv6], Variable] route_policy: Optional[Union[Default[None], Global[UUID]]] = Field( @@ -205,16 +206,18 @@ class RedistributedRouteIPv6(BaseModel): class DefaultOriginate(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") 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): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") delay: Optional[Union[Global[int], Variable, Default[int]]] = None initial_hold: Optional[Union[Global[int], Variable, Default[int]]] = None @@ -222,7 +225,7 @@ class SpfTimers(BaseModel): class AdvancedOspfv3Attributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") reference_bandwidth: Optional[Union[Global[int], Variable, Default[int]]] = Field( serialization_alias="referenceBandwidth", validation_alias="referenceBandwidth", default=None @@ -241,9 +244,9 @@ class AdvancedOspfv3Attributes(BaseModel): class BasicOspfv3Attributes(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + 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 @@ -258,42 +261,31 @@ class BasicOspfv3Attributes(BaseModel): ) -class Ospfv3IPv4Data(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class Ospfv3IPv4Parcel(_ParcelBase): + type_: Literal["routing/ospfv3/ipv4"] = Field(default="routing/ospfv3/ipv4", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - basic: Optional[BasicOspfv3Attributes] = None - advanced: Optional[AdvancedOspfv3Attributes] = None - redistribute: Optional[RedistributedRoute] = None + basic: Optional[BasicOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "basic")) + advanced: Optional[AdvancedOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "advanced")) + redistribute: Optional[List[RedistributedRoute]] = Field( + default=None, validation_alias=AliasPath("data", "redistribute") + ) max_metric_router_lsa: Optional[MaxMetricRouterLsa] = Field( - serialization_alias="maxMetricRouterLsa", validation_alias="maxMetricRouterLsa", default=None + validation_alias=AliasPath("data", "maxMetricRouterLsa"), default=None ) - area: List[Ospfv3IPv4Area] + area: List[Ospfv3IPv4Area] = Field(validation_alias=AliasPath("data", "area")) -class Ospfv3IPv6Data(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class Ospfv3IPv6Parcel(_ParcelBase): + type_: Literal["routing/ospfv3/ipv6"] = Field(default="routing/ospfv3/ipv6", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - basic: Optional[BasicOspfv3Attributes] = None - advanced: Optional[AdvancedOspfv3Attributes] = None - redistribute: Optional[RedistributedRouteIPv6] = None + basic: Optional[BasicOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "basic")) + advanced: Optional[AdvancedOspfv3Attributes] = Field(default=None, validation_alias=AliasPath("data", "advanced")) + redistribute: Optional[List[RedistributedRouteIPv6]] = Field( + default=None, validation_alias=AliasPath("data", "redistribute") + ) max_metric_router_lsa: Optional[MaxMetricRouterLsa] = Field( - serialization_alias="maxMetricRouterLsa", validation_alias="maxMetricRouterLsa", default=None + validation_alias=AliasPath("data", "maxMetricRouterLsa"), default=None ) - area: List[Ospfv3IPv6Area] - - -class Ospfv3IPv4CreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: Ospfv3IPv4Data - - -class Ospfv3IPv6CreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: Ospfv3IPv6Data - metadata: Optional[dict] = None + area: List[Ospfv3IPv6Area] = Field(validation_alias=AliasPath("data", "area")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/route_policy.py b/catalystwan/models/configuration/feature_profile/sdwan/service/route_policy.py index cdf753be..1f4e9372 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/route_policy.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/route_policy.py @@ -3,9 +3,9 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global +from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase Action = Literal[ "reject", @@ -37,7 +37,7 @@ class StandardCommunityList(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") criteria: Union[Global[Criteria], Default[Criteria]] = Default[Criteria](value="or") standard_community_list: List[Global[UUID]] = Field( @@ -46,7 +46,7 @@ class StandardCommunityList(BaseModel): class ExpandedCommunityList(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") expanded_community_list: Global[UUID] = Field( serialization_alias="expandedCommunityList", validation_alias="expandedCommunityList" @@ -54,7 +54,7 @@ class ExpandedCommunityList(BaseModel): class RoutePolicyMatch(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") as_path_list: Optional[Global[UUID]] = Field( serialization_alias="asPathList", validation_alias="asPathList", default=None @@ -86,7 +86,7 @@ class RoutePolicyMatch(BaseModel): class AcceptAction(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") enable_accept_action: Default[bool] = Default[bool](value=True) set_as_path: Optional[Global[int]] = Field( @@ -125,19 +125,19 @@ class AcceptAction(BaseModel): class AcceptActions(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") accept: AcceptAction class RejectActions(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") reject: Default[bool] = Default[bool](value=True) class RoutePolicySequence(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") sequence_id: Global[int] = Field(serialization_alias="sequenceId", validation_alias="sequenceId") sequence_name: Global[str] = Field(serialization_alias="sequenceName", validation_alias="sequenceName") @@ -151,21 +151,11 @@ class RoutePolicySequence(BaseModel): actions: Optional[List[Union[AcceptActions, RejectActions]]] = None -class RoutePolicyData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class RoutePolicyData(_ParcelBase): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") defautl_action: Union[Global[Action], Default[Action]] = Field( - serialization_alias="defaultAction", - validation_alias="defaultAction", + validation_alias=AliasPath("data", "defaultAction"), default=Default[Action](value="reject"), ) - sequences: List[RoutePolicySequence] = [] - - -class RoutePolicyCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: RoutePolicyData - metadata: Optional[dict] = None + sequences: List[RoutePolicySequence] = Field(default_factory=list, validation_alias=AliasPath("data", "sequences")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/service_insertion_attachment.py b/catalystwan/models/configuration/feature_profile/sdwan/service/service_insertion_attachment.py index 33ea7b3b..ecd49e17 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/service_insertion_attachment.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/service_insertion_attachment.py @@ -3,9 +3,9 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase AttachmentType = Literal[ "custom", @@ -53,7 +53,7 @@ class GatewayInterface(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Optional[Union[Global[str], Variable]] = Field( serialization_alias="gatewayInterfaceName", validation_alias="gatewayInterfaceName", default=None @@ -67,7 +67,7 @@ class GatewayInterface(BaseModel): class ServiceInterface(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Optional[Union[Global[str], Variable]] = Field( serialization_alias="serviceInterfaceName", validation_alias="serviceInterfaceName", default=None @@ -81,14 +81,14 @@ class ServiceInterface(BaseModel): class TrackingIP(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") ipv4: Optional[Union[Global[str], Variable]] = None ipv6: Optional[Union[Global[str], Variable]] = None class ReachableInterface(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") type: Global[ReachableInterfaceType] = Field( serialization_alias="reachableInterfaceType", validation_alias="reachableInterfaceType" @@ -114,7 +114,7 @@ class ReachableInterface(BaseModel): class InterfaceProperties(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") gateway_interface: Optional[GatewayInterface] = Field( serialization_alias="gatewayInterface", validation_alias="gatewayInterface", default=None @@ -134,7 +134,7 @@ class InterfaceProperties(BaseModel): class Attachment(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_properties: List[InterfaceProperties] = Field( serialization_alias="interfaceProperties", validation_alias="interfaceProperties" @@ -145,7 +145,7 @@ class Attachment(BaseModel): class TrackConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interval: Optional[Union[Global[int], Variable]] = None threshold: Optional[Union[Global[int], Variable]] = None @@ -153,7 +153,7 @@ class TrackConfig(BaseModel): class Service(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") service_type: Union[Global[ServiceType], Variable] = Field( serialization_alias="serviceType", validation_alias="serviceType" @@ -168,25 +168,17 @@ class Service(BaseModel): attachments: Optional[List[Attachment]] = None -class ServiceInsertionAttachmentData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class ServiceInsertionAttachmentParcel(_ParcelBase): + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") attachment_type: Optional[Union[Global[AttachmentType], Variable]] = Field( - serialization_alias="attachmentType", validation_alias="attachmentType", default=None + validation_alias=AliasPath("data", "attachmentType"), default=None ) service_chain_instance_id: Optional[Union[Global[str], Variable]] = Field( - serialization_alias="serviceChainInstanceID", validation_alias="serviceChainInstanceID", default=None + validation_alias=AliasPath("data", "serviceChainInstanceID"), default=None ) service_chain_definition_id: Global[UUID] = Field( - serialization_alias="serviceChainDefinitionID", validation_alias="serviceChainDefinitionID" + validation_alias=AliasPath("data", "serviceChainDefinitionID"), ) - vpn: Union[Global[int], Variable] - services: Optional[List[Service]] = None - - -class ServiceInsertionAttachmentCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: ServiceInsertionAttachmentData + vpn: Union[Global[int], Variable] = Field(validation_alias=AliasPath("data", "vpn")) + services: Optional[List[Service]] = Field(default=None, validation_alias=AliasPath("data", "services")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/switchport.py b/catalystwan/models/configuration/feature_profile/sdwan/service/switchport.py index d7e2cb59..c32177e3 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/switchport.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/switchport.py @@ -2,13 +2,13 @@ from typing import List, Literal, Optional, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase class StaticMacAddress(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") mac_address: Union[Global[str], Variable] = Field(serialization_alias="macaddr", validation_alias="macaddr") vlan: Union[Global[int], Variable] @@ -47,7 +47,7 @@ class StaticMacAddress(BaseModel): class SwitchportInterface(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") interface_name: Union[Global[str], Variable] = Field(serialization_alias="ifName", validation_alias="ifName") mode: Optional[Global[SwitchportMode]] = None @@ -100,22 +100,16 @@ class SwitchportInterface(BaseModel): ) -class SwitchportData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class SwitchportParcel(_ParcelBase): + type_: Literal["switchport"] = Field(default="switchport", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - interface: Optional[List[SwitchportInterface]] = None + interface: Optional[List[SwitchportInterface]] = Field( + default=None, validation_alias=AliasPath("data", "interface") + ) age_time: Optional[Union[Global[int], Variable, Default[int]]] = Field( - serialization_alias="ageTime", validation_alias="ageTime", default=Default[int](value=300) + validation_alias=AliasPath("data", "ageTime"), default=Default[int](value=300) ) static_mac_address: Optional[List[StaticMacAddress]] = Field( serialization_alias="staticMacAddress", validation_alias="staticMacAddress", default=None ) - - -class SwitchportCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: SwitchportData - metadata: Optional[dict] = None diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/tracker.py b/catalystwan/models/configuration/feature_profile/sdwan/service/tracker.py index d9f4c417..2ea53305 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/tracker.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/tracker.py @@ -3,9 +3,9 @@ from typing import List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase EndpointProtocol = Literal[ "tcp", @@ -23,14 +23,14 @@ class EndpointTcpUdp(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") protocol: Optional[Union[Variable, Global[EndpointProtocol]]] = None port: Optional[Union[Variable, Global[int]]] = None class TrackerData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Union[Variable, Global[str]] = Field(serialization_alias="trackerName", validation_alias="trackerName") endpoint_api_url: Optional[Union[Variable, Global[str]]] = Field( @@ -61,7 +61,7 @@ class TrackerData(BaseModel): class TrackerCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: str description: Optional[str] = None @@ -70,32 +70,23 @@ class TrackerCreationPayload(BaseModel): class TrackerRef(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") tracker_ref: Global[UUID] = Field(serialization_alias="trackerRef", validation_alias="trackerRef") -class TrackerGroupData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class TrackerGroupParcel(_ParcelBase): + type_: Literal["trackergroup"] = Field(default="trackergroup", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") - tracker_refs: List[TrackerRef] = Field(serialization_alias="trackerRefs", validation_alias="trackerRefs") + tracker_refs: List[TrackerRef] = Field(validation_alias=AliasPath("data", "trackerRefs")) combine_boolean: Union[Global[CombineBoolean], Variable, Default[CombineBoolean]] = Field( - serialization_alias="combineBoolean", - validation_alias="combineBoolean", + validation_alias=AliasPath("data", "combineBoolean"), default=Default[CombineBoolean](value="or"), ) -class TrackerGroupCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: TrackerGroupData - metadata: Optional[dict] = None - - class TrackerAssociationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") parcel_id: str = Field(serialization_alias="parcelId", validation_alias="parcelId") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/service/wireless_lan.py b/catalystwan/models/configuration/feature_profile/sdwan/service/wireless_lan.py index 0711013e..55431ce4 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/service/wireless_lan.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/service/wireless_lan.py @@ -2,9 +2,9 @@ from typing import List, Literal, Optional, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase CountryCode = Literal[ "AE", @@ -135,7 +135,7 @@ class MeStaticIpConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") me_ipv4_address: Union[Global[str], Variable] netmask: Union[Global[str], Variable] @@ -145,7 +145,7 @@ class MeStaticIpConfig(BaseModel): class MeIpConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") me_dynamic_ip_enabled: Union[Global[bool], Default[bool]] = Field( serialization_alias="meDynamicIpEnabled", @@ -156,7 +156,7 @@ class MeIpConfig(BaseModel): class SecurityConfig(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") security_type: Global[SecurityType] = Field(serialization_alias="securityType", validation_alias="securityType") radius_server_ip: Optional[Union[Global[str], Variable]] = Field( @@ -172,7 +172,7 @@ class SecurityConfig(BaseModel): class SSID(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") name: Global[str] admin_state: Union[Global[bool], Variable, Default[bool]] = Field( @@ -193,28 +193,18 @@ class SSID(BaseModel): ) -class WirelessLanData(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) +class WirelessLanParcel(_ParcelBase): + type_: Literal["wirelesslan"] = Field(default="wirelesslan", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True, extra="forbid") enable_2_4G: Union[Global[bool], Variable, Default[bool]] = Field( - serialization_alias="enable24G", validation_alias="enable24G", default=Default[bool](value=True) + validation_alias=AliasPath("data", "enable24G"), default=Default[bool](value=True) ) enable_5G: Union[Global[bool], Variable, Default[bool]] = Field( - serialization_alias="enable5G", validation_alias="enable5G", default=Default[bool](value=True) + validation_alias=AliasPath("data", "enable5G"), default=Default[bool](value=True) ) - ssid: List[SSID] - country: Union[Global[CountryCode], Variable] - username: Union[Global[str], Variable] - password: Union[Global[str], Variable] - me_ip_config: MeIpConfig = Field( - serialization_alias="meIpConfig", validation_alias="meIpConfig", default=MeIpConfig() - ) - - -class WirelessLanCreationPayload(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - - name: str - description: Optional[str] = None - data: WirelessLanData - metadata: Optional[dict] = None + ssid: List[SSID] = Field(validation_alias=AliasPath("data", "ssid")) + country: Union[Global[CountryCode], Variable] = Field(validation_alias=AliasPath("data", "country")) + username: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "username")) + password: Union[Global[str], Variable] = Field(validation_alias=AliasPath("data", "password")) + me_ip_config: MeIpConfig = Field(validation_alias=AliasPath("data", "meIpConfig"), default=MeIpConfig()) diff --git a/catalystwan/session.py b/catalystwan/session.py index 1bcd40d3..0f95edcc 100644 --- a/catalystwan/session.py +++ b/catalystwan/session.py @@ -259,7 +259,8 @@ def login(self) -> ManagerSession: self.logger.info( f"Logged to vManage({self.platform_version}) as {self.username}. The session type is {self.session_type}" ) - self.cookies.set("JSESSIONID", self.auth.set_cookie.get("JSESSIONID")) + if jsessionid := self.auth.set_cookie.get("JSESSIONID"): + self.cookies.set("JSESSIONID", jsessionid) return self def wait_server_ready(self, timeout: int, poll_period: int = 10) -> None: diff --git a/catalystwan/tests/test_feature_profile_api.py b/catalystwan/tests/test_feature_profile_api.py index 955f4ed0..292175d5 100644 --- a/catalystwan/tests/test_feature_profile_api.py +++ b/catalystwan/tests/test_feature_profile_api.py @@ -1,10 +1,27 @@ import unittest -from unittest.mock import patch -from uuid import UUID +from ipaddress import IPv4Address +from unittest.mock import Mock +from uuid import uuid4 from parameterized import parameterized # type: ignore -from catalystwan.api.feature_profile_api import SystemFeatureProfileAPI +from catalystwan.api.configuration_groups.parcel import Global, as_global, as_variable +from catalystwan.api.feature_profile_api import ServiceFeatureProfileAPI, SystemFeatureProfileAPI +from catalystwan.endpoints.configuration.feature_profile.sdwan.service import ServiceFeatureProfile +from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile +from catalystwan.models.configuration.feature_profile.sdwan.service import ( + AppqoeParcel, + InterfaceEthernetParcel, + InterfaceGreParcel, + InterfaceIpsecParcel, + InterfaceSviParcel, + LanVpnDhcpServerParcel, + LanVpnParcel, + OspfParcel, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.gre import BasicGre +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import IpsecAddress, IpsecTunnelMode +from catalystwan.models.configuration.feature_profile.sdwan.service.ospfv3 import Ospfv3IPv4Parcel, Ospfv3IPv6Parcel from catalystwan.models.configuration.feature_profile.sdwan.system import ( AAAParcel, BannerParcel, @@ -19,7 +36,7 @@ SNMPParcel, ) -endpoint_mapping = { +system_endpoint_mapping = { AAAParcel: "aaa", BannerParcel: "banner", BasicParcel: "basic", @@ -36,75 +53,138 @@ class TestSystemFeatureProfileAPI(unittest.TestCase): def setUp(self): - self.profile_uuid = UUID("054d1b82-9fa7-43c6-98fb-4355da0d77ff") - self.parcel_uuid = UUID("7113505f-8cec-4420-8799-1a209357ba7e") + self.profile_uuid = uuid4() + self.parcel_uuid = uuid4() + self.mock_session = Mock() + self.mock_endpoint = Mock(spec=SystemFeatureProfile) + self.api = SystemFeatureProfileAPI(self.mock_session) + self.api.endpoint = self.mock_endpoint + + @parameterized.expand(system_endpoint_mapping.items()) + def test_delete_method_with_valid_arguments(self, parcel, expected_path): + # Act + self.api.delete_parcel(self.profile_uuid, parcel, self.parcel_uuid) - @parameterized.expand(endpoint_mapping.items()) - @patch("catalystwan.session.ManagerSession") - @patch("catalystwan.endpoints.configuration.feature_profile.sdwan.system.SystemFeatureProfile") - def test_delete_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): - # Arrange - api = SystemFeatureProfileAPI(mock_session) - api.endpoint = mock_endpoint + # Assert + self.mock_endpoint.delete.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid) + @parameterized.expand(system_endpoint_mapping.items()) + def test_get_method_with_valid_arguments(self, parcel, expected_path): # Act - api.delete_parcel(self.profile_uuid, parcel, self.parcel_uuid) + self.api.get_parcels(self.profile_uuid, parcel, self.parcel_uuid) # Assert - mock_endpoint.delete.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid) - - @parameterized.expand(endpoint_mapping.items()) - @patch("catalystwan.session.ManagerSession") - @patch("catalystwan.endpoints.configuration.feature_profile.sdwan.system.SystemFeatureProfile") - def test_get_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): - # Arrange - api = SystemFeatureProfileAPI(mock_session) - api.endpoint = mock_endpoint + self.mock_endpoint.get_by_id.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid) + @parameterized.expand(system_endpoint_mapping.items()) + def test_get_all_method_with_valid_arguments(self, parcel, expected_path): # Act - api.get_parcels(self.profile_uuid, parcel, self.parcel_uuid) + self.api.get_parcels(self.profile_uuid, parcel) # Assert - mock_endpoint.get_by_id.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid) + self.mock_endpoint.get_all.assert_called_once_with(self.profile_uuid, expected_path) - @parameterized.expand(endpoint_mapping.items()) - @patch("catalystwan.session.ManagerSession") - @patch("catalystwan.endpoints.configuration.feature_profile.sdwan.system.SystemFeatureProfile") - def test_get_all_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): - # Arrange - api = SystemFeatureProfileAPI(mock_session) - api.endpoint = mock_endpoint + @parameterized.expand(system_endpoint_mapping.items()) + def test_create_method_with_valid_arguments(self, parcel, expected_path): + # Act + self.api.create_parcel(self.profile_uuid, parcel) + # Assert + self.mock_endpoint.create.assert_called_once_with(self.profile_uuid, expected_path, parcel) + + @parameterized.expand(system_endpoint_mapping.items()) + def test_update_method_with_valid_arguments(self, parcel, expected_path): # Act - api.get_parcels(self.profile_uuid, parcel) + self.api.update_parcel(self.profile_uuid, parcel, self.parcel_uuid) # Assert - mock_endpoint.get_all.assert_called_once_with(self.profile_uuid, expected_path) + self.mock_endpoint.update.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid, parcel) + - @parameterized.expand(endpoint_mapping.items()) - @patch("catalystwan.session.ManagerSession") - @patch("catalystwan.endpoints.configuration.feature_profile.sdwan.system.SystemFeatureProfile") - def test_create_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): - # Arrange - api = SystemFeatureProfileAPI(mock_session) - api.endpoint = mock_endpoint +service_endpoint_mapping = { + LanVpnDhcpServerParcel: "dhcp-server", + AppqoeParcel: "appqoe", + LanVpnParcel: "lan/vpn", + OspfParcel: "routing/ospf", + Ospfv3IPv4Parcel: "routing/ospfv3/ipv4", + Ospfv3IPv6Parcel: "routing/ospfv3/ipv6", +} +service_interface_parcels = [ + ( + "gre", + InterfaceGreParcel( + parcel_name="TestGreParcel", + parcel_description="Test Gre Parcel", + basic=BasicGre(if_name=as_global("gre1"), tunnel_destination=as_global(IPv4Address("4.4.4.4"))), + ), + ), + ( + "svi", + InterfaceSviParcel( + parcel_name="TestSviParcel", + parcel_description="Test Svi Parcel", + interface_name=as_global("Vlan1"), + svi_description=as_global("Test Svi Description"), + ), + ), + ( + "ethernet", + InterfaceEthernetParcel( + parcel_name="TestEthernetParcel", + parcel_description="Test Ethernet Parcel", + interface_name=as_global("HundredGigE"), + ethernet_description=as_global("Test Ethernet Description"), + ), + ), + ( + "ipsec", + InterfaceIpsecParcel( + parcel_name="TestIpsecParcel", + parcel_description="Test Ipsec Parcel", + interface_name=as_global("ipsec2"), + ipsec_description=as_global("Test Ipsec Description"), + pre_shared_secret=as_global("123"), + ike_local_id=as_global("123"), + ike_remote_id=as_global("123"), + application=as_variable("{{ipsec_application}}"), + tunnel_mode=Global[IpsecTunnelMode](value="ipv6"), + tunnel_destination_v6=as_variable("{{ipsec_tunnelDestinationV6}}"), + tunnel_source_v6=Global[str](value="::"), + tunnel_source_interface=as_variable("{{ipsec_ipsecSourceInterface}}"), + ipv6_address=as_variable("{{test}}"), + address=IpsecAddress(address=as_global("10.0.0.1"), mask=as_global("255.255.255.0")), + tunnel_destination=IpsecAddress(address=as_global("10.0.0.5"), mask=as_global("255.255.255.0")), + mtu_v6=as_variable("{{test}}"), + ), + ), +] + + +class TestServiceFeatureProfileAPI(unittest.TestCase): + def setUp(self): + self.profile_uuid = uuid4() + self.vpn_uuid = uuid4() + self.parcel_uuid = uuid4() + self.mock_session = Mock() + self.mock_endpoint = Mock(spec=ServiceFeatureProfile) + self.api = ServiceFeatureProfileAPI(self.mock_session) + self.api.endpoint = self.mock_endpoint + + @parameterized.expand(service_endpoint_mapping.items()) + def test_post_method_parcel(self, parcel, parcel_type): # Act - api.create_parcel(self.profile_uuid, parcel) + self.api.create_parcel(self.profile_uuid, parcel) # Assert - mock_endpoint.create.assert_called_once_with(self.profile_uuid, expected_path, parcel) - - @parameterized.expand(endpoint_mapping.items()) - @patch("catalystwan.session.ManagerSession") - @patch("catalystwan.endpoints.configuration.feature_profile.sdwan.system.SystemFeatureProfile") - def test_update_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): - # Arrange - api = SystemFeatureProfileAPI(mock_session) - api.endpoint = mock_endpoint + self.mock_endpoint.create_service_parcel.assert_called_once_with(self.profile_uuid, parcel_type, parcel) + @parameterized.expand(service_interface_parcels) + def test_post_method_interface_parcel(self, parcel_type, parcel): # Act - api.update(self.profile_uuid, parcel, self.parcel_uuid) + self.api.create_parcel(self.profile_uuid, parcel, self.vpn_uuid) # Assert - mock_endpoint.update.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid, parcel) + self.mock_endpoint.create_lan_vpn_interface_parcel.assert_called_once_with( + self.profile_uuid, self.vpn_uuid, parcel_type, parcel + ) diff --git a/catalystwan/utils/config_migration/converters/exceptions.py b/catalystwan/utils/config_migration/converters/exceptions.py new file mode 100644 index 00000000..eaa13401 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/exceptions.py @@ -0,0 +1,9 @@ +from catalystwan.exceptions import CatalystwanException + + +class CatalystwanConverterCantConvertException(CatalystwanException): + """ + Exception raised when a CatalystwanConverter can't correctly convert a template. + """ + + pass diff --git a/catalystwan/utils/config_migration/converters/feature_template/__init__.py b/catalystwan/utils/config_migration/converters/feature_template/__init__.py index 477bbd79..9a8ba538 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/__init__.py +++ b/catalystwan/utils/config_migration/converters/feature_template/__init__.py @@ -1,7 +1,7 @@ from typing import List -from .factory_method import choose_parcel_converter, create_parcel_from_template from .normalizer import template_definition_normalization +from .parcel_factory import choose_parcel_converter, create_parcel_from_template __all__ = ["create_parcel_from_template", "choose_parcel_converter", "template_definition_normalization"] diff --git a/catalystwan/utils/config_migration/converters/feature_template/aaa.py b/catalystwan/utils/config_migration/converters/feature_template/aaa.py index b2cd0eba..5259b925 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/aaa.py +++ b/catalystwan/utils/config_migration/converters/feature_template/aaa.py @@ -8,8 +8,7 @@ class AAATemplateConverter: supported_template_types = ("cisco_aaa", "cedge_aaa", "aaa") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> AAAParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> AAAParcel: """ Creates an AAAParcel object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/appqoe.py b/catalystwan/utils/config_migration/converters/feature_template/appqoe.py index 5f1a75b0..7a0f536b 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/appqoe.py +++ b/catalystwan/utils/config_migration/converters/feature_template/appqoe.py @@ -7,13 +7,13 @@ ServiceNodeGroupName, ServiceNodeGroupsNames, ) +from catalystwan.utils.config_migration.converters.exceptions import CatalystwanConverterCantConvertException class AppqoeTemplateConverter: supported_template_types = ("appqoe",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> AppqoeParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> AppqoeParcel: """ Create an AppqoeParcel object based on the provided name, description, and template values. @@ -26,6 +26,21 @@ def create_parcel(name: str, description: str, template_values: dict) -> AppqoeP AppqoeParcel: The created AppqoeParcel object. """ values = deepcopy(template_values) + + appnav_controller_group = values.get("appnav_controller_group", []) + if not appnav_controller_group: + raise CatalystwanConverterCantConvertException("Appnav controller group is required for Appqoe parcel") + for appnav in appnav_controller_group: + if group_name := appnav.get("group_name"): + appnav["group_name"] = as_default(group_name.value, AppnavControllerGroupName) + for controller in appnav.get("appnav_controllers", []): + if _vpn := controller.get("vpn"): # noqa: F841 + # VPN field is depended on existence of the Service VPN value + # also from UI this list contains only 1 item and should not be a list. + # AppqoeParcel.forwarder.appnav_controller_group.appnav_controllers[0].vpn + # must be populated in the parcel creation process. + pass + for appqoe_item in values.get("service_context", {}).get("appqoe", []): if item_name := appqoe_item.get("name"): appqoe_item["name"] = as_default(value=item_name.value) @@ -45,16 +60,7 @@ def create_parcel(name: str, description: str, template_values: dict) -> AppqoeP internal = group.get("internal") if internal is not None: group["internal"] = as_default(internal.value) - for appnav in values.get("appnav_controller_group", []): - if group_name := appnav.get("group_name"): - appnav["group_name"] = as_default(group_name.value, AppnavControllerGroupName) - for controller in appnav.get("appnav_controllers", []): - if _vpn := controller.get("vpn"): # noqa: F841 - # VPN field is depended on existence of the Service VPN value - # also from UI this list contains only 1 item and should not be a list. - # AppqoeParcel.forwarder.appnav_controller_group.appnav_controllers[0].vpn - # must be populated in the parcel creation process. - pass + parcel_values = { "parcel_name": name, "parcel_description": description, diff --git a/catalystwan/utils/config_migration/converters/feature_template/banner.py b/catalystwan/utils/config_migration/converters/feature_template/banner.py index 5aa4550b..a3d24849 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/banner.py +++ b/catalystwan/utils/config_migration/converters/feature_template/banner.py @@ -4,8 +4,7 @@ class BannerTemplateConverter: supported_template_types = ("cisco_banner",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> BannerParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> BannerParcel: """ Creates a BannerParcel object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/base.py b/catalystwan/utils/config_migration/converters/feature_template/base.py index c3144e72..9c2e3aae 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/base.py +++ b/catalystwan/utils/config_migration/converters/feature_template/base.py @@ -4,6 +4,5 @@ class FeatureTemplateConverter(Protocol): - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> AnySystemParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> AnySystemParcel: ... diff --git a/catalystwan/utils/config_migration/converters/feature_template/basic.py b/catalystwan/utils/config_migration/converters/feature_template/basic.py index 968c50ae..a859cba2 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/basic.py +++ b/catalystwan/utils/config_migration/converters/feature_template/basic.py @@ -9,8 +9,7 @@ class SystemToBasicTemplateConverter: supported_template_types = ("cisco_system", "system-vsmart", "system-vedge") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> BasicParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> BasicParcel: """ Converts the provided template values into a BasicParcel object. diff --git a/catalystwan/utils/config_migration/converters/feature_template/bfd.py b/catalystwan/utils/config_migration/converters/feature_template/bfd.py index 5b30dc85..64356848 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/bfd.py +++ b/catalystwan/utils/config_migration/converters/feature_template/bfd.py @@ -4,8 +4,7 @@ class BFDTemplateConverter: supported_template_types = ("cisco_bfd", "bfd-vedge") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> BFDParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> BFDParcel: """ Creates a BFDParcel object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/bgp.py b/catalystwan/utils/config_migration/converters/feature_template/bgp.py index 96dc5a26..77f84bbe 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/bgp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/bgp.py @@ -17,8 +17,10 @@ class BGPTemplateConverter: supported_template_types = ("bgp", "cisco_bgp") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> BGPParcel: + device_specific_ipv4_neighbor_address = "{{{{lbgp_1_neighbor_{index}_address}}}}" + device_specific_ipv6_neighbor_address = "{{{{lbgp_1_ipv6_neighbor_{index}_address}}}}" + + def create_parcel(self, name: str, description: str, template_values: dict) -> BGPParcel: """ Creates a BannerParcel object based on the provided template values. @@ -30,8 +32,6 @@ def create_parcel(name: str, description: str, template_values: dict) -> BGPParc Returns: BannerParcel: A BannerParcel object with the provided template values. """ - device_specific_ipv4_neighbor_address = "{{{{lbgp_1_neighbor_{index}_address}}}}" - device_specific_ipv6_neighbor_address = "{{{{lbgp_1_ipv6_neighbor_{index}_address}}}}" parcel_values = {"parcel_name": name, "parcel_description": description, **deepcopy(template_values["bgp"])} @@ -48,7 +48,7 @@ def create_parcel(name: str, description: str, template_values: dict) -> BGPParc family_type["family_type"] = as_global(family_type["family_type"].value, FamilyType) if neighbor.get("address") is None: logger.info("Neighbor address is not set, using device specific variable") - neighbor["address"] = as_variable(device_specific_ipv4_neighbor_address.format(index=(i + 1))) + neighbor["address"] = as_variable(self.device_specific_ipv4_neighbor_address.format(index=(i + 1))) if if_name := neighbor.get("update_source", {}).get("if_name"): neighbor["if_name"] = if_name neighbor.pop("update_source") @@ -73,7 +73,7 @@ def create_parcel(name: str, description: str, template_values: dict) -> BGPParc ) if neighbor.get("address") is None: logger.info("Neighbor address is not set, using device specific variable") - neighbor["address"] = as_variable(device_specific_ipv6_neighbor_address.format(index=(i + 1))) + neighbor["address"] = as_variable(self.device_specific_ipv6_neighbor_address.format(index=(i + 1))) if if_name := neighbor.get("update_source", {}).get("if_name"): neighbor["if_name"] = if_name neighbor.pop("update_source") diff --git a/catalystwan/utils/config_migration/converters/feature_template/dhcp.py b/catalystwan/utils/config_migration/converters/feature_template/dhcp.py index 89cde42a..5c28e7a5 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/dhcp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/dhcp.py @@ -20,8 +20,7 @@ class DhcpTemplateConverter: variable_mac_address = "{{{{dhcp_1_staticLease_{}_macAddress}}}}" variable_ip = "{{{{dhcp_1_staticLease_{}_ip}}}}" - @classmethod - def create_parcel(cls, name: str, description: str, template_values: dict) -> LanVpnDhcpServerParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> LanVpnDhcpServerParcel: """ Create a LanVpnDhcpServerParcel object based on the provided parameters. @@ -50,19 +49,19 @@ def create_parcel(cls, name: str, description: str, template_values: dict) -> La "Assiging variable: dhcp_1_addressPool_networkAddress and dhcp_1_addressPool_subnetMask." ) values["address_pool"] = { - "network_address": as_variable(cls.variable_address_pool), - "subnet_mask": as_variable(cls.variable_subnet_mask), + "network_address": as_variable(self.variable_address_pool), + "subnet_mask": as_variable(self.variable_subnet_mask), } for entry in values.get("option_code", []): - cls._convert_str_list_to_ipv4_list(entry, "ip") + self._convert_str_list_to_ipv4_list(entry, "ip") for key in ("dns_servers", "tftp_servers"): - cls._convert_str_list_to_ipv4_list(values, key) + self._convert_str_list_to_ipv4_list(values, key) static_lease = [] for i, entry in enumerate(values.get("static_lease", [])): - mac_address, ip = cls._get_mac_address_and_ip(entry, i) + mac_address, ip = self._get_mac_address_and_ip(entry, i) static_lease.append( { "mac_address": mac_address, @@ -79,8 +78,7 @@ def create_parcel(cls, name: str, description: str, template_values: dict) -> La return LanVpnDhcpServerParcel(**parcel_values) # type: ignore - @classmethod - def _convert_str_list_to_ipv4_list(cls, d: dict, key: str) -> None: + def _convert_str_list_to_ipv4_list(self, d: dict, key: str) -> None: """ Convert a list of strings representing IPv4 addresses to a list of IPv4Address objects. @@ -95,19 +93,18 @@ def _convert_str_list_to_ipv4_list(cls, d: dict, key: str) -> None: if str_list := d.get(key, as_global([])).value: d[key] = Global[List[IPv4Address]](value=[IPv4Address(ip) for ip in str_list]) - @classmethod - def _get_mac_address_and_ip(cls, entry: dict, i: int) -> tuple: - mac_address = entry.get("mac_address", as_variable(cls.variable_mac_address.format(i + 1))) - ip = entry.get("ip", as_variable(cls.variable_ip.format(i + 1))) + def _get_mac_address_and_ip(self, entry: dict, i: int) -> tuple: + mac_address = entry.get("mac_address", as_variable(self.variable_mac_address.format(i + 1))) + ip = entry.get("ip", as_variable(self.variable_ip.format(i + 1))) if isinstance(mac_address, Variable): logger.warning( f"No MAC address specified for static lease {i + 1}." - f"Assigning variable: {cls.variable_mac_address.format(i + 1)}" + f"Assigning variable: {self.variable_mac_address.format(i + 1)}" ) if isinstance(ip, Variable): logger.warning( f"No IP address specified for static lease {i + 1}." - f"Assigning variable: {cls.variable_ip.format(i + 1)}" + f"Assigning variable: {self.variable_ip.format(i + 1)}" ) return mac_address, ip diff --git a/catalystwan/utils/config_migration/converters/feature_template/ethernet.py b/catalystwan/utils/config_migration/converters/feature_template/ethernet.py new file mode 100644 index 00000000..ebfb11ad --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/ethernet.py @@ -0,0 +1,271 @@ +from copy import deepcopy +from typing import List, Optional + +from catalystwan.api.configuration_groups.parcel import Default, Global, as_default, as_global, as_variable +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( + Arp, + StaticIPv4Address, + StaticIPv6Address, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ethernet import ( + AclQos, + AdvancedEthernetAttributes, + DynamicDhcpDistance, + DynamicIPv6Dhcp, + InterfaceDynamicIPv4Address, + InterfaceDynamicIPv6Address, + InterfaceEthernetParcel, + InterfaceStaticIPv4Address, + InterfaceStaticIPv6Address, + NatAttributesIPv4, + NatPool, + StaticIPv4AddressConfig, + StaticIPv6AddressConfig, + Trustsec, + VrrpIPv4, +) + + +class InterfaceEthernetTemplateConverter: + supported_template_types = ( + "vpn-vsmart-interface", + "vpn-vedge-interface", + "vpn-vmanage-interface", + "cisco_vpn_interface", + ) + + delete_keys = ( + "if_name", + "ip", + "infru_mtu", + "description", + "arp", + "duplex", + "mac_address", + "mtu", + "ipv6_vrrp", + "tcp_mss_adjust", + "arp_timeout", + "autonegotiate", + "media_type", + "load_interval", + "icmp_redirect_disable", + "tloc_extension_gre_from", + "ip_directed_broadcast", + "tracker", + "trustsec", + "intrf_mtu", + "speed", + "qos_map", + "shaping_rate", + "bandwidth_upstream", + "bandwidth_downstream", + "rewrite_rule", + "block_non_source_ip", + "tloc_extension", + "iperf_server", + "auto_bandwidth_detect", + "service_provider", + "ipv6", + "clear_dont_fragment", + "access_list", + "qos_adaptive", + "tunnel_interface", # Not sure if this is correct. There is some data in UX1 that is not transferable to UX2 + "nat66", # Not sure if this is correct. There is some data in UX1 that is not transferable to UX2 + ) + + # Default Values - Interface Name + basic_conf_intf_name = "{{vpn23_1_basicConf_intfName}}" + + # Default Values - NAT Attribute + nat_attribute_nat_choice = "{{natAttr_natChoice}}" + + def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceEthernetParcel: + values = deepcopy(template_values) + self.configure_interface_name(values) + self.configure_ethernet_description(values) + self.configure_ipv4_address(values) + self.configure_ipv6_address(values) + self.configure_arp(values) + self.configure_advanced_attributes(values) + self.configure_trustsec(values) + self.configure_virtual_router_redundancy_protocol_ipv4(values) + self.configure_virtual_router_redundancy_protocol_ipv6(values) + self.configure_network_address_translation(values) + self.configure_acl_qos(values) + self.cleanup_keys(values) + return InterfaceEthernetParcel(**self.prepare_parcel_values(name, description, values)) + + def prepare_parcel_values(self, name: str, description: str, values: dict) -> dict: + """ + Prepare the parcel values by combining the provided name, description, and additional values. + + Args: + name (str): The name of the parcel. + description (str): The description of the parcel. + values (dict): Additional values to include in the parcel. + + Returns: + dict: The prepared parcel values for InterfaceSviParcel to consume. + """ + return {"parcel_name": name, "parcel_description": description, **values} + + def configure_interface_name(self, values: dict) -> None: + if if_name := values.get("if_name"): + values["interface_name"] = if_name + values["interface_name"] = as_variable(self.basic_conf_intf_name) + + def configure_ethernet_description(self, values: dict) -> None: + values["ethernet_description"] = values.get("description") + + def configure_ipv4_address(self, values: dict) -> None: + if ipv4_address_configuration := values.get("ip"): + if "address" in ipv4_address_configuration: + values["interface_ip_address"] = InterfaceStaticIPv4Address( + static=StaticIPv4AddressConfig( + primary_ip_address=self.get_static_ipv4_address(ipv4_address_configuration), + secondary_ip_address=self.get_secondary_static_ipv4_address(ipv4_address_configuration), + ) + ) + elif "dhcp_client" in ipv4_address_configuration: + values["interface_ip_address"] = InterfaceDynamicIPv4Address( + dynamic=DynamicDhcpDistance( + dynamic_dhcp_distance=ipv4_address_configuration.get("dhcp_distance", as_global(1)) + ) + ) + + def get_static_ipv4_address(self, address_configuration: dict) -> StaticIPv4Address: + static_network = address_configuration["address"].value.network + return StaticIPv4Address( + ip_address=as_global(value=static_network.network_address), + subnet_mask=as_global(value=str(static_network.netmask)), + ) + + def get_secondary_static_ipv4_address(self, address_configuration: dict) -> Optional[List[StaticIPv4Address]]: + secondary_address = [] + for address in address_configuration.get("secondary_address", []): + secondary_address.append(self.get_static_ipv4_address(address)) + return secondary_address if secondary_address else None + + def configure_ipv6_address(self, values: dict) -> None: + if ipv6_address_configuration := values.get("ipv6"): + if "address" in ipv6_address_configuration: + values["interface_ipv6_address"] = InterfaceStaticIPv6Address( + static=StaticIPv6AddressConfig( + primary_ip_address=self.get_static_ipv6_address(ipv6_address_configuration), + secondary_ip_address=self.get_secondary_static_ipv6_address(ipv6_address_configuration), + dhcp_helper_v6=ipv6_address_configuration.get("dhcp_helper"), + ) + ) + elif "dhcp_client" in ipv6_address_configuration: + values["interface_ipv6_address"] = InterfaceDynamicIPv6Address( + dynamic=DynamicIPv6Dhcp( + dhcp_client=ipv6_address_configuration.get("dhcp_client"), + secondary_ipv6_address=ipv6_address_configuration.get("secondary_address"), + ) + ) + + def get_static_ipv6_address(self, address_configuration: dict) -> StaticIPv6Address: + return StaticIPv6Address(address=address_configuration["address"]) + + def get_secondary_static_ipv6_address(self, address_configuration: dict) -> Optional[List[StaticIPv6Address]]: + secondary_address = [] + for address in address_configuration.get("secondary_address", []): + secondary_address.append(self.get_static_ipv6_address(address)) + return secondary_address if secondary_address else None + + def configure_arp(self, values: dict) -> None: + if arps := values.get("arp", {}).get("ip", []): + arp_list = [] + for arp in arps: + arp_list.append(Arp(ip_address=arp.get("addr", Default[None](value=None)), mac_address=arp.get("mac"))) + values["arp"] = arp_list + + def configure_advanced_attributes(self, values: dict) -> None: + values["advanced"] = AdvancedEthernetAttributes( + duplex=values.get("duplex"), + mac_address=values.get("mac_address"), + speed=values.get("speed"), + ip_mtu=values.get("mtu", as_default(value=1500)), + interface_mtu=values.get("intrf_mtu", as_default(value=1500)), + tcp_mss=values.get("tcp_mss_adjust"), + arp_timeout=values.get("arp_timeout", as_default(value=1200)), + autonegotiate=values.get("autonegotiate"), + media_type=values.get("media_type"), + load_interval=values.get("load_interval", as_default(value=30)), + tracker=self.get_tracker_value(values), + icmp_redirect_disable=values.get("icmp_redirect_disable", as_default(True)), + xconnect=values.get("tloc_extension_gre_from", {}).get("xconnect"), + ip_directed_broadcast=values.get("ip_directed_broadcast", as_default(False)), + ) + + def get_tracker_value(self, values: dict) -> Optional[Global[str]]: + if tracker := values.get("tracker"): + return as_global(",".join(tracker.value)) + return None + + def configure_trustsec(self, values: dict) -> None: + values["trustsec"] = Trustsec( + enable_sgt_propagation=values.get("propagate", {}).get("sgt", as_default(False)), + security_group_tag=values.get("static", {}).get("sgt"), + propagate=values.get("enable", as_default(False)), + enable_enforced_propagation=values.get("enforced", {}).get("enable", Default[None](value=None)), + enforced_security_group_tag=values.get("enforced", {}).get("sgt", Default[None](value=None)), + ) + + def configure_virtual_router_redundancy_protocol_ipv4(self, values: dict) -> None: + if vrrps := values.get("vrrp", []): + vrrp_list = [] + for vrrp in vrrps: + vrrp_list.append( + VrrpIPv4( + group_id=vrrp.get("grp_id", Default[int](value=1)), + priority=vrrp.get("priority", Default[int](value=100)), + timer=vrrp.get("timer", Default[int](value=1000)), + track_omp=vrrp.get("track_omp", Default[bool](value=False)), + ip_address=vrrp.get("ipv4", {}).get("address"), + ip_address_secondary=self.get_vrrp_ipv4_secondary_addresses(vrrp), + ) + ) + values["vrrp"] = vrrp_list + + def get_vrrp_ipv4_secondary_addresses(self, vrrp: dict) -> Optional[List[StaticIPv4Address]]: + secondary_addresses = [] + for address in vrrp.get("ipv4", {}).get("ipv4_secondary", []): + secondary_addresses.append(StaticIPv4Address(ip_address=address.get("address"))) + return secondary_addresses if secondary_addresses else None + + def configure_virtual_router_redundancy_protocol_ipv6(self, values: dict) -> None: + if vrrps_ipv6 := values.get("ipv6_vrrp", []): + for vrrp_ipv6 in vrrps_ipv6: + vrrp_ipv6["group_id"] = vrrp_ipv6.pop("grp_id") + values["vrrp_ipv6"] = vrrps_ipv6 + + def configure_network_address_translation(self, values: dict) -> None: + if nat := values.get("nat"): + if isinstance(nat, dict): + # Nat can be straight up Global[bool] or a dict with more values + nat_type = nat.get("nat_choice", as_variable(self.nat_attribute_nat_choice)) + if nat_type.value.lower() == "interface": + nat_type = as_variable(self.nat_attribute_nat_choice) + values["nat_attributes_ipv4"] = NatAttributesIPv4( + nat_type=nat_type, + nat_pool=self.get_nat_pool(nat), + udp_timeout=nat.get("udp_timeout", as_default(1)), + tcp_timeout=nat.get("tcp_timeout", as_default(60)), + new_static_nat=nat.get("static"), + ) + values["nat"] = as_global(True) + + def get_nat_pool(self, values: dict) -> Optional[NatPool]: + if nat_pool := values.get("natpool"): + return NatPool(**nat_pool) + return None + + def configure_acl_qos(self, values: dict) -> None: + if shaping_rate := values.get("shaping_rate"): + values["acl_qos"] = AclQos(shaping_rate=shaping_rate) + + 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/global_.py b/catalystwan/utils/config_migration/converters/feature_template/global_.py index ade77de6..36178e37 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/global_.py +++ b/catalystwan/utils/config_migration/converters/feature_template/global_.py @@ -4,8 +4,7 @@ class GlobalTemplateConverter: supported_template_types = ("cedge_global",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> GlobalParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> GlobalParcel: """ Creates an Logging object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/gre.py b/catalystwan/utils/config_migration/converters/feature_template/gre.py new file mode 100644 index 00000000..37b3fa7d --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/gre.py @@ -0,0 +1,118 @@ +from copy import deepcopy +from ipaddress import IPv4Interface +from typing import Tuple + +from catalystwan.api.configuration_groups.parcel import as_global, as_variable +from catalystwan.models.configuration.feature_profile.sdwan.service import InterfaceGreParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import IkeGroup +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.gre import ( + GreAddress, + GreSourceIPv6, + TunnelSourceIPv6, +) + + +class InterfaceGRETemplateConverter: + supported_template_types = ("cisco_vpn_interface_gre", "vpn-vedge-interface-gre") + + tunnel_destination_ip4 = "{{gre_tunnelDestination_ip4}}" + + delete_keys = ( + "dead_peer_detection", + "ike", + "ipsec", + "ip", + "tunnel_source_v6", + "tunnel_route_via", + "authentication_type", + "access_list", + "ipv6", + "rewrite_rule", + "multiplexing", + "tracker", + ) + + def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceGreParcel: + """ + Create a new InterfaceGreParcel object. + + Args: + name (str): The name of the parcel. + description (str): The description of the parcel. + template_values (dict): A dictionary containing template values. + + Returns: + InterfaceGreParcel: The created InterfaceGreParcel object. + """ + print(template_values) + basic_values, advanced_values = self.prepare_values(template_values) + self.configure_dead_peer_detection(basic_values) + self.configure_ike(basic_values) + self.configure_ipsec(basic_values) + self.configure_tunnel_source_v6(basic_values) + self.configure_tunnel_destination(basic_values) + self.configure_ipv6_address(basic_values) + self.configure_gre_address(basic_values) + self.cleanup_keys(basic_values) + parcel_values = self.prepare_parcel_values(name, description, basic_values, advanced_values) + return InterfaceGreParcel(**parcel_values) # type: ignore + + def prepare_values(self, template_values: dict) -> Tuple[dict, dict]: + values = deepcopy(template_values) + advanced_application = values.pop("application", None) + basic_values = {**values} + if advanced_application: + advanced_values = {"application": advanced_application} + return basic_values, advanced_values + + def prepare_parcel_values(self, name, description, basic_values, advanced_values): + return { + "parcel_name": name, + "parcel_description": description, + "basic": basic_values, + "advanced": advanced_values, + } + + def configure_dead_peer_detection(self, values: dict) -> None: + values["dpd_interval"] = values.get("dead_peer_detection", {}).get("dpd_interval") + values["dpd_retries"] = values.get("dead_peer_detection", {}).get("dpd_retries") + + def configure_ipv6_address(self, values: dict) -> None: + values["ipv6_address"] = values.get("ipv6", {}).get("address") + + def configure_gre_address(self, values: dict) -> None: + address = values.get("ip", {}).get("address", {}) + if address: + network = IPv4Interface(address.value).network + gre_address = GreAddress( + address=as_global(str(network.network_address)), + mask=as_global(str(network.netmask)), + ) + values["address"] = gre_address + + def configure_ike(self, values: dict) -> None: + ike = values.get("ike", {}) + if ike: + if ike_group := ike.get("ike_group"): + ike["ike_group"] = as_global(ike_group.value, IkeGroup) + ike.update(ike.get("authentication_type", {}).get("pre_shared_key", {})) + values.update(ike) + + def configure_ipsec(self, values: dict) -> None: + values.update(values.get("ipsec", {})) + + def configure_tunnel_destination(self, values: dict) -> None: + values["tunnel_destination"] = values.get("tunnel_destination", as_variable(self.tunnel_destination_ip4)) + + def configure_tunnel_source_v6(self, values: dict) -> None: + if tunnel_source_v6 := values.get("tunnel_source_v6"): + values["tunnel_source_type"] = GreSourceIPv6( + source_ipv6=TunnelSourceIPv6( + tunnel_source_v6=tunnel_source_v6, + tunnel_route_via=values.get("tunnel_route_via"), + ) + ) + + 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/ipsec.py b/catalystwan/utils/config_migration/converters/feature_template/ipsec.py new file mode 100644 index 00000000..02a9cd21 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/ipsec.py @@ -0,0 +1,96 @@ +from copy import deepcopy +from ipaddress import IPv4Interface, IPv6Address + +from catalystwan.api.configuration_groups.parcel import Default, as_global +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import IkeGroup +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import InterfaceIpsecParcel, IpsecAddress + + +class InterfaceIpsecTemplateConverter: + supported_template_types = ("cisco_vpn_interface_ipsec", "vpn-vedge-interface-ipsec") + + delete_keys = ( + "dead_peer_detection", + "if_name", + "description", + "ike", + "authentication_type", + "multiplexing", + "ipsec", + "ipv6", + ) + + def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceIpsecParcel: + values = deepcopy(template_values) + self.configure_interface_name(values) + self.configure_description(values) + self.configure_dead_peer_detection(values) + self.configure_ike(values) + self.configure_ipsec(values) + self.configure_tunnel_destination(values) + self.configure_tunnel_source(values) + self.configure_ipv6_address(values) + self.configure_address(values) + self.configure_tracker(values) + self.cleanup_keys(values) + return InterfaceIpsecParcel(parcel_name=name, parcel_description=description, **values) + + def configure_interface_name(self, values: dict) -> None: + values["interface_name"] = values.get("if_name") + + def configure_description(self, values: dict) -> None: + values["ipsec_description"] = values.get("description", Default[None](value=None)) + + def configure_dead_peer_detection(self, values: dict) -> None: + values["dpd_interval"] = values.get("dead_peer_detection", {}).get("dpd_interval") + values["dpd_retries"] = values.get("dead_peer_detection", {}).get("dpd_retries") + + def configure_ipv6_address(self, values: dict) -> None: + values["ipv6_address"] = values.get("ipv6", {}).get("address") + + def configure_address(self, values: dict) -> None: + address = values.get("ip", {}).get("address", {}) + if address: + values["address"] = IpsecAddress( + address=as_global(str(address.network.network_address)), + mask=as_global(str(address.network.netmask)), + ) + + def configure_ike(self, values: dict) -> None: + ike = values.get("ike", {}) + if ike: + if ike_group := ike.get("ike_group"): + ike["ike_group"] = as_global(ike_group.value, IkeGroup) + ike.update(ike.get("authentication_type", {}).get("pre_shared_key", {})) + values.update(ike) + + def configure_ipsec(self, values: dict) -> None: + values.update(values.get("ipsec", {})) + + def configure_tunnel_destination(self, values: dict) -> None: + if tunnel_destination := values.get("tunnel_destination"): + if isinstance(tunnel_destination.value, IPv4Interface): + values["tunnel_destination"] = IpsecAddress( + address=as_global(str(tunnel_destination.value.network.network_address)), + mask=as_global(str(tunnel_destination.value.network.netmask)), + ) + elif isinstance(tunnel_destination.value, IPv6Address): + values.pop("tunnel_destination") + values["tunnel_destination_v6"] = tunnel_destination + + def configure_tunnel_source(self, values: dict) -> None: + if tunnel_source := values.get("tunnel_source"): + values["tunnel_source"] = IpsecAddress( + address=as_global(str(tunnel_source.value)), + mask=as_global("0.0.0.0"), + ) + + def configure_tracker(self, values: dict) -> None: + tracker = values.get("tracker") + if tracker: + tracker = as_global("".join(tracker.value)) + values["tracker"] = tracker + + 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/logging_.py b/catalystwan/utils/config_migration/converters/feature_template/logging_.py index 399be09f..ec02f24e 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/logging_.py +++ b/catalystwan/utils/config_migration/converters/feature_template/logging_.py @@ -9,8 +9,7 @@ class LoggingTemplateConverter: supported_template_types = ("cisco_logging", "logging") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> LoggingParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> LoggingParcel: """ Creates an Logging object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py index ecc1aa74..15c1182b 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py +++ b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py @@ -1,8 +1,28 @@ -from ipaddress import AddressValueError, IPv4Address, IPv6Address -from typing import List, Union, get_args +from ipaddress import AddressValueError, IPv4Address, IPv4Interface, IPv6Address, IPv6Interface +from typing import List, Optional, Union, get_args from catalystwan.api.configuration_groups.parcel import Global, as_global -from catalystwan.models.common import TLOCColor +from catalystwan.models.common import MetricType, TLOCColor +from catalystwan.models.configuration.feature_profile.sdwan.service.dhcp_server import SubnetMask +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( + IkeCiphersuite, + IkeMode, + IpsecCiphersuite, + PfsGroup, + TunnelApplication, + VrrpTrackerAction, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ethernet import DuplexMode, MediaType, NatType +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.gre import GreTunnelMode +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import IpsecTunnelMode +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import Direction +from catalystwan.models.configuration.feature_profile.sdwan.service.ospf import ( + AdvertiseType, + AreaType, + AuthenticationType, + RedistributeProtocolOspf, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.ospfv3 import NetworkType from catalystwan.models.configuration.feature_profile.sdwan.system.logging_parcel import ( AuthType, CypherSuite, @@ -11,7 +31,34 @@ ) from catalystwan.models.configuration.feature_profile.sdwan.system.mrf import EnableMrfMigration, Role -CastableLiterals = [Priority, TlsVersion, AuthType, CypherSuite, Role, EnableMrfMigration, TLOCColor] +CastableLiterals = Union[ + Priority, + TlsVersion, + AuthType, + CypherSuite, + Role, + EnableMrfMigration, + TLOCColor, + SubnetMask, + Direction, + IkeCiphersuite, + IkeMode, + IpsecCiphersuite, + PfsGroup, + TunnelApplication, + GreTunnelMode, + VrrpTrackerAction, + DuplexMode, + MediaType, + NatType, + IpsecTunnelMode, + NetworkType, + AuthenticationType, + AreaType, + AdvertiseType, + RedistributeProtocolOspf, + MetricType, +] CastedTypes = Union[ Global[bool], @@ -21,6 +68,9 @@ Global[List[int]], Global[IPv4Address], Global[IPv6Address], + Global[IPv4Interface], + Global[IPv6Interface], + Global[CastableLiterals], ] @@ -34,27 +84,38 @@ def cast_value_to_global(value: Union[str, int, List[str], List[int]]) -> Casted 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 - if isinstance(value, str): - if value.lower() == "true": - return Global[bool](value=True) - elif value.lower() == "false": - return Global[bool](value=False) - try: - ipv4_address = IPv4Address(value) - return Global[IPv4Address](value=ipv4_address) - except AddressValueError: - pass + for cast_func in [try_cast_to_literal, try_cast_to_bool, try_cast_to_address]: + if global_value := cast_func(value): + return global_value + return as_global(value) # type: ignore + + +def try_cast_to_address(value: str) -> Optional[Global]: + """Tries to cast a string to an IP address or interface.""" + for address_type in [IPv4Address, IPv6Address, IPv4Interface, IPv6Interface]: try: - ipv6_address = IPv6Address(value) - return Global[IPv6Address](value=ipv6_address) + return Global[address_type](value=address_type(value)) # type: ignore except AddressValueError: pass - for literal in CastableLiterals: - if value in get_args(literal): - return Global[literal](value=value) # type: ignore + return None - return as_global(value) # type: ignore + +def try_cast_to_literal(value: str) -> Optional[Global[CastableLiterals]]: + """Tries to cast a string to a literal.""" + for literal in get_args(CastableLiterals): + if value in get_args(literal): + return Global[literal](value=value) # type: ignore + return None + + +def try_cast_to_bool(value: str) -> Optional[Global[bool]]: + """Tries to cast the given value to a boolean.""" + if value.lower() == "true": + return Global[bool](value=True) + elif value.lower() == "false": + return Global[bool](value=False) + return None def transform_dict(d: dict) -> dict: diff --git a/catalystwan/utils/config_migration/converters/feature_template/ntp.py b/catalystwan/utils/config_migration/converters/feature_template/ntp.py index a766abcf..580eb290 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ntp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ntp.py @@ -4,8 +4,7 @@ class NTPTemplateConverter: supported_template_types = ("cisco_ntp", "ntp") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> NTPParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> NTPParcel: """ Creates an Logging object based on the provided template values. diff --git a/catalystwan/utils/config_migration/converters/feature_template/omp.py b/catalystwan/utils/config_migration/converters/feature_template/omp.py index e3703f66..cc5dad83 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/omp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/omp.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Dict, List from catalystwan.api.configuration_groups.parcel import Global, as_default, as_global @@ -7,8 +8,7 @@ class OMPTemplateConverter: supported_template_types = ("cisco_omp", "omp-vedge", "omp-vsmart") - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> OMPParcel: + def create_parcel(self, name: str, description: str, template_values: dict) -> OMPParcel: """ Creates an OMPParcel object based on the provided template values. @@ -20,16 +20,15 @@ def create_parcel(name: str, description: str, template_values: dict) -> OMPParc Returns: OMPParcel: An OMPParcel object with the provided template values. """ - - def create_advertise_dict(advertise_list: List) -> Dict: - return {definition["protocol"].value: Global[bool](value=True) for definition in advertise_list} - + values = deepcopy(template_values) parcel_values = { "parcel_name": name, "parcel_description": description, - "ecmp_limit": as_global(float(template_values.get("ecmp_limit", as_default(4)).value)), - "advertise_ipv4": create_advertise_dict(template_values.get("advertise", [])), - "advertise_ipv6": create_advertise_dict(template_values.get("ipv6_advertise", [])), + "ecmp_limit": as_global(float(values.get("ecmp_limit", as_default(4)).value)), + "advertise_ipv4": self.create_advertise_dict(values.get("advertise", [])), + "advertise_ipv6": self.create_advertise_dict(values.get("ipv6_advertise", [])), } - return OMPParcel(**parcel_values) + + def create_advertise_dict(self, advertise_list: List) -> Dict: + return {definition["protocol"].value: Global[bool](value=True) for definition in advertise_list} diff --git a/catalystwan/utils/config_migration/converters/feature_template/ospf.py b/catalystwan/utils/config_migration/converters/feature_template/ospf.py new file mode 100644 index 00000000..7979caa1 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/ospf.py @@ -0,0 +1,139 @@ +from copy import deepcopy +from typing import List, Optional + +from catalystwan.api.configuration_groups.parcel import Global, as_global +from catalystwan.models.configuration.feature_profile.sdwan.service.ospf import ( + AreaType, + OspfArea, + OspfInterfaceParametres, + OspfParcel, + RouterLsa, + SummaryPrefix, + SummaryRoute, +) + + +class OspfTemplateConverter: + supported_template_types = ("cisco_ospf",) + + delete_keys = ("max_metric", "timers", "distance", "auto_cost", "default_information", "compatible") + + def create_parcel(self, name: str, description: str, template_values: dict) -> OspfParcel: + """ + Creates a BannerParcel object based on the provided template values. + + Args: + name (str): The name of the BannerParcel. + description (str): The description of the BannerParcel. + template_values (dict): A dictionary containing the template values. + + Returns: + BannerParcel: A BannerParcel object with the provided template values. + """ + values = deepcopy(template_values).get("ospf", {}) + self.configure_router_lsa(values) + self.configure_timers(values) + self.configure_distance(values) + self.configure_reference_bandwidth(values) + self.configure_originate(values) + self.configure_rfc1583(values) + self.configure_area(values) + self.configure_route_policy(values) + self.cleanup_keys(values) + return OspfParcel(parcel_name=name, parcel_description=description, **values) + + def configure_router_lsa(self, values: dict) -> None: + if router_lsa := values.get("max_metric", {}).get("router_lsa"): + router_lsa_list = [] + for router_lsa_value in router_lsa: + router_lsa_list.append(RouterLsa(**router_lsa_value)) + values["router_lsa"] = router_lsa_list + + def configure_timers(self, values: dict) -> None: + if timers := values.get("timers", {}).get("spf"): + values.update(timers) + + def configure_distance(self, values: dict) -> None: + if distance := values.get("distance"): + values.update(distance) + + def configure_reference_bandwidth(self, values: dict) -> None: + if auto_cost := values.get("auto_cost"): + values["reference_bandwidth"] = auto_cost.get("reference_bandwidth") + + def configure_originate(self, values: dict) -> None: + if originate := values.get("default_information", {}).get("originate"): + values["originate"] = as_global(True) + values["always"] = originate.get("always") + values["metric"] = originate.get("metric") + values["metric_type"] = originate.get("metric_type") + + def configure_rfc1583(self, values: dict) -> None: + if rfc1583 := values.get("compatible", {}).get("rfc1583"): + values["rfc1583"] = rfc1583 + + def configure_area(self, values: dict) -> None: + area = values.get("area") + if area is None: + return + area_list = [] + for area_value in area: + area_list.append( + OspfArea( + area_number=area_value.get("a_num"), + area_type=self._set_area_type(area_value), + no_summary=self._set_no_summary(area_value), + interface=self._set_interface(area_value), + range=self._set_range(area_value), + ) + ) + values["area"] = area_list + + def _set_area_type(self, area_value: dict) -> Optional[Global[AreaType]]: + if "stub" in area_value: + return as_global("stub", AreaType) + elif "nssa" in area_value: + return as_global("nssa", AreaType) + return None + + def _set_no_summary(self, area_value: dict) -> Optional[Global[bool]]: + if "stub" in area_value: + return area_value.get("stub", {}).get("no_summary") + elif "nssa" in area_value: + return area_value.get("nssa", {}).get("no_summary") + return None + + def _set_interface(self, area_value: dict) -> Optional[List[OspfInterfaceParametres]]: + interfaces = area_value.get("interface") + if interfaces is None: + return None + interface_list = [] + for interface in interfaces: + if authentication := interface.pop("authentication", None): + area_value["authentication_type"] = authentication.get("type") + interface_list.append(OspfInterfaceParametres(**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_["address"] = SummaryPrefix( + ip_address=as_global(str(address.value.network)), subnet_mask=as_global(str(address.value.netmask)) + ) + + def configure_route_policy(self, values: dict) -> None: + if route_policy := values.get("route_policy", [{}])[0].get("direction"): + values["route_policy"] = route_policy + + 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/ospfv3.py b/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py new file mode 100644 index 00000000..35fa39a6 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/ospfv3.py @@ -0,0 +1,279 @@ +from copy import deepcopy +from typing import List, Optional, Tuple, Type, Union, cast, get_args + +from catalystwan.api.configuration_groups.parcel import Default, Global, 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, + Ospfv3IPv6Area, + Ospfv3IPv6Parcel, + RedistributedRoute, + RedistributedRouteIPv6, + RedistributeProtocol, + RedistributeProtocolIPv6, + SpfTimers, + StubArea, + SummaryRoute, + SummaryRouteIPv6, +) +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 = cast( + Ospfv3IPv4Parcel, Ospfv3Ipv4TemplateSubconverter().create_parcel(name, description, template_values) + ) + ospfv3ipv6 = cast( + Ospfv3IPv6Parcel, Ospfv3Ipv6TemplateSubconverter().create_parcel(name, description, template_values) + ) + return ospfv3ipv4, ospfv3ipv6 + + +class BaseOspfv3TemplateSubconverter: + key_address_family: str + key_distance: str + parcel_model: Union[Type[Ospfv3IPv4Parcel], Type[Ospfv3IPv6Parcel]] + area_model: Union[Type[Ospfv3IPv4Area], Type[Ospfv3IPv6Area]] + + delete_keys = ( + "default_information", + "router_id", + "table_map", + "max_metric", + "timers", + "distance_ipv4", + "distance_ipv6", + "auto_cost", + "compatible", + ) + + def create_parcel( + self, name: str, description: str, template_values: dict + ) -> Union[Ospfv3IPv4Parcel, Ospfv3IPv6Parcel]: + values = self.get_values(template_values) + 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 self.parcel_model(parcel_name=name, parcel_description=description, **values) + + def get_values(self, template_values: dict) -> dict: + values = deepcopy(template_values).get("ospfv3", {}).get("address_family", {}).get(self.key_address_family, {}) + return 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(self.key_distance, {}) + + 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 + if isinstance(originate, Global): + return DefaultOriginate(originate=originate) + 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( + self.area_model( + 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), # type: ignore + ) + ) + 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") + if network := interface.pop("network", None): + interface["network_type"] = network + interface_list.append(Ospfv3InterfaceParametres(**interface)) + return interface_list + + def _set_range(self, area_value: dict) -> Optional[Union[List[SummaryRoute], List[SummaryRouteIPv6]]]: + raise NotImplementedError + + def configure_redistribute(self, values: dict) -> None: + raise NotImplementedError + + def cleanup_keys(self, values: dict) -> None: + for key in self.delete_keys: + values.pop(key, None) + + +class Ospfv3Ipv4TemplateSubconverter(BaseOspfv3TemplateSubconverter): + key_address_family = "ipv4" + key_distance = "distance_ipv4" + parcel_model = Ospfv3IPv4Parcel + area_model = Ospfv3IPv4Area + + 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 + + +class Ospfv3Ipv6TemplateSubconverter(BaseOspfv3TemplateSubconverter): + key_address_family = "ipv6" + key_distance = "distance_ipv6" + parcel_model = Ospfv3IPv6Parcel + area_model = Ospfv3IPv6Area + + def _set_range(self, area_value: dict) -> Optional[List[SummaryRouteIPv6]]: + ranges = area_value.get("range") + if ranges is None: + return None + range_list = [] + for range_ in ranges: + print(range_) + range_list.append( + SummaryRouteIPv6( + network=range_.get("address"), + cost=range_.get("cost"), + no_advertise=range_.get("no_advertise", Default[bool](value=False)), + ) + ) + return range_list + + def configure_redistribute(self, values: dict) -> None: + redistributes = values.get("redistribute", []) + if redistributes == []: + return None + redistribute_list = [] + for redistribute in redistributes: + print(redistribute) + if redistribute.get("protocol").value not in get_args(RedistributeProtocolIPv6): + continue + redistribute_list.append( + RedistributedRouteIPv6( + protocol=as_global(redistribute.get("protocol").value, RedistributeProtocolIPv6), + route_policy=redistribute.get("route_map"), + ) + ) + values["redistribute"] = redistribute_list diff --git a/catalystwan/utils/config_migration/converters/feature_template/factory_method.py b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py similarity index 80% rename from catalystwan/utils/config_migration/converters/feature_template/factory_method.py rename to catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py index 7cfc2f88..0408aecf 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/factory_method.py +++ b/catalystwan/utils/config_migration/converters/feature_template/parcel_factory.py @@ -1,11 +1,12 @@ import json import logging -from typing import Any, Dict, cast +from typing import Any, Callable, Dict, cast from catalystwan.api.template_api import FeatureTemplateInformation from catalystwan.exceptions import CatalystwanException from catalystwan.models.configuration.feature_profile.sdwan.system import AnySystemParcel from catalystwan.utils.config_migration.converters.feature_template.dhcp import DhcpTemplateConverter +from catalystwan.utils.config_migration.converters.feature_template.ethernet import InterfaceEthernetTemplateConverter from catalystwan.utils.config_migration.converters.feature_template.snmp import SNMPTemplateConverter from catalystwan.utils.feature_template.find_template_values import find_template_values @@ -17,13 +18,19 @@ from .bfd import BFDTemplateConverter from .bgp import BGPTemplateConverter from .global_ import GlobalTemplateConverter +from .gre import InterfaceGRETemplateConverter +from .ipsec import InterfaceIpsecTemplateConverter from .logging_ import LoggingTemplateConverter from .normalizer import template_definition_normalization 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 from .ucse import UcseTemplateConverter +from .vpn import LanVpnParcelTemplateConverter logger = logging.getLogger(__name__) @@ -43,6 +50,13 @@ DhcpTemplateConverter, SNMPTemplateConverter, AppqoeTemplateConverter, + LanVpnParcelTemplateConverter, + InterfaceGRETemplateConverter, + InterfaceSviTemplateConverter, + InterfaceEthernetTemplateConverter, + InterfaceIpsecTemplateConverter, + OspfTemplateConverter, + Ospfv3TemplateConverter, ] @@ -51,7 +65,7 @@ } -def choose_parcel_converter(template_type: str) -> FeatureTemplateConverter: +def choose_parcel_converter(template_type: str) -> Callable[..., FeatureTemplateConverter]: """ This function is used to choose the correct parcel factory based on the template type. @@ -85,7 +99,7 @@ def create_parcel_from_template(template: FeatureTemplateInformation) -> AnySyst Raises: ValueError: If the given template type is not supported. """ - converter = choose_parcel_converter(template.template_type) + 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_definition_normalization(template_values) diff --git a/catalystwan/utils/config_migration/converters/feature_template/snmp.py b/catalystwan/utils/config_migration/converters/feature_template/snmp.py index bce41441..2e06da51 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/snmp.py +++ b/catalystwan/utils/config_migration/converters/feature_template/snmp.py @@ -17,8 +17,9 @@ class SNMPTemplateConverter: supported_template_types = ("cisco_snmp",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> SNMPParcel: + default_view_oid_id = "{{{{l_snmpView_1_snmpOid_{}_id}}}}" + + def create_parcel(self, name: str, description: str, template_values: dict) -> SNMPParcel: """ Creates a SecurityParcel object based on the provided template values. @@ -31,20 +32,24 @@ def create_parcel(name: str, description: str, template_values: dict) -> SNMPPar SecurityParcel: A SecurityParcel object with the provided template values. """ values = deepcopy(template_values) + self.configure_community(values) + self.configure_target(values) + parcel_values = {"parcel_name": name, "parcel_description": description, **values} + return SNMPParcel(**parcel_values) # type: ignore + + def configure_community(self, values: dict): for community_item in values.get("community", []): if authorization := community_item.get("authorization"): community_item["authorization"] = as_global(authorization.value, Authorization) + def configure_target(self, values: dict) -> None: values["target"] = values.pop("trap", {}).get("target", []) - default_view_oid_id = "{{{{l_snmpView_1_snmpOid_{}_id}}}}" + for view in values.get("view", []): for i, oid in enumerate(view.get("oid", [])): - id_ = oid.get("id", as_variable(default_view_oid_id.format(i + 1))) + id_ = oid.get("id", as_variable(self.default_view_oid_id.format(i + 1))) if isinstance(id_, Variable): logger.info( - f"OID ID is not set, using device specific variable {default_view_oid_id.format(i + 1)}" + f"OID ID is not set, using device specific variable {self.default_view_oid_id.format(i + 1)}" ) oid["id"] = id_ - - parcel_values = {"parcel_name": name, "parcel_description": description, **values} - return SNMPParcel(**parcel_values) # type: ignore diff --git a/catalystwan/utils/config_migration/converters/feature_template/svi.py b/catalystwan/utils/config_migration/converters/feature_template/svi.py new file mode 100644 index 00000000..8b15cbb1 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/svi.py @@ -0,0 +1,164 @@ +from copy import deepcopy +from typing import List, Optional + +from catalystwan.api.configuration_groups.parcel import Default, as_global +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.common import ( + Arp, + StaticIPv4Address, + StaticIPv6Address, +) +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.svi import ( + AdvancedSviAttributes, + InterfaceSviParcel, + IPv4AddressConfiguration, + IPv6AddressConfiguration, + VrrpIPv4, + VrrpIPv4SecondaryAddress, +) + + +class InterfaceSviTemplateConverter: + """ + A class for converting template values into a InterfaceSviParcel object. + """ + + supported_template_types = ("vpn-interface-svi",) + + delete_keys = ( + "if_name", + "ip", + "intrf_mtu", + "tcp_mss_adjust", + "dhcp_helper", + "ipv6_vrrp", + "arp_timeout", + "mtu", + "ip_directed_broadcast", + "icmp_redirect_disable", + "description", + "access_list", + ) + + def create_parcel(self, name: str, description: str, template_values: dict) -> InterfaceSviParcel: + values = deepcopy(template_values) + self.configure_interface_name(values) + self.configure_ipv4_address(values) + self.configure_ipv6_address(values) + self.configure_arp(values) + self.configure_interface_mtu(values) + self.configure_advanced_attributes(values) + self.configure_virtual_router_redundancy_protocol_ipv4(values) + self.configure_virtual_router_redundancy_protocol_ipv6(values) + self.cleanup_keys(values) + return InterfaceSviParcel(**self.prepare_pracel_values(name, description, values)) + + def prepare_pracel_values(self, name: str, description: str, values: dict) -> dict: + """ + Prepare the parcel values by combining the provided name, description, and additional values. + + Args: + name (str): The name of the parcel. + description (str): The description of the parcel. + values (dict): Additional values to include in the parcel. + + Returns: + dict: The prepared parcel values for InterfaceSviParcel to consume. + """ + return {"parcel_name": name, "parcel_description": description, **values} + + def configure_interface_name(self, values: dict) -> None: + values["interface_name"] = values.get("if_name") + + def configure_svi_description(self, values: dict) -> None: + values["svi_description"] = values.get("description") + + def configure_ipv4_address(self, values: dict) -> None: + if ipv4_address_configuration := values.get("ip"): + values["ipv4"] = IPv4AddressConfiguration( + address=self.get_static_ipv4_address(ipv4_address_configuration), + secondary_address=self.get_secondary_static_ipv4_address(ipv4_address_configuration), + dhcp_helper=values.get("dhcp_helper"), + ) + + def get_static_ipv4_address(self, address_configuration: dict) -> StaticIPv4Address: + static_network = address_configuration["address"].value.network + return StaticIPv4Address( + ip_address=as_global(value=static_network.network_address), + subnet_mask=as_global(value=str(static_network.netmask)), + ) + + def get_secondary_static_ipv4_address(self, address_configuration: dict) -> Optional[List[StaticIPv4Address]]: + secondary_address = [] + for address in address_configuration.get("secondary_address", []): + secondary_address.append(self.get_static_ipv4_address(address)) + return secondary_address if secondary_address else None + + def configure_ipv6_address(self, values: dict) -> None: + if ipv6_address_configuration := values.get("ipv6"): + values["ipv6"] = IPv6AddressConfiguration( + address=ipv6_address_configuration.get("address", Default[None](value=None)), + secondary_address=self.get_secondary_static_ipv6_address(ipv6_address_configuration), + ) + + def get_static_ipv6_address(self, address_configuration: dict) -> StaticIPv6Address: + return StaticIPv6Address(address=address_configuration["address"]) + + def get_secondary_static_ipv6_address(self, address_configuration: dict) -> Optional[List[StaticIPv6Address]]: + secondary_address = [] + for address in address_configuration.get("secondary_address", []): + secondary_address.append(self.get_static_ipv6_address(address)) + return secondary_address if secondary_address else None + + def configure_arp(self, values: dict) -> None: + if arps := values.get("arp", {}).get("ip", []): + arp_list = [] + for arp in arps: + arp_list.append(Arp(ip_address=arp.get("addr", Default[None](value=None)), mac_address=arp.get("mac"))) + values["arp"] = arp_list + + def configure_interface_mtu(self, values: dict) -> None: + values["interface_mtu"] = values.get("intrf_mtu", Default[int](value=1500)) + + def configure_ip_mtu(self, values: dict) -> None: + values["ip_mtu"] = values.get("mtu", Default[int](value=1500)) + + def configure_advanced_attributes(self, values: dict) -> None: + values["advanced"] = AdvancedSviAttributes( + tcp_mss=values.get("tcp_mss_adjust", Default[None](value=None)), + arp_timeout=values.get("arp_timeout", Default[int](value=1200)), + ip_directed_broadcast=values.get("ip_directed_broadcast", Default[bool](value=False)), + icmp_redirect_disable=values.get("icmp_redirect_disable", Default[bool](value=True)), + ) + + def configure_virtual_router_redundancy_protocol_ipv4(self, values: dict) -> None: + if vrrps := values.get("vrrp", []): + vrrp_list = [] + for vrrp in vrrps: + vrrp_list.append( + VrrpIPv4( + group_id=vrrp.get("grp_id", Default[int](value=1)), + priority=vrrp.get("priority", Default[int](value=100)), + timer=vrrp.get("timer", Default[int](value=1000)), + track_omp=vrrp.get("track_omp", Default[bool](value=False)), + prefix_list=vrrp.get("track_prefix_list", Default[None](value=None)), + ip_address=vrrp.get("ipv4", {}).get("address"), + ip_address_secondary=self.get_vrrp_ipv4_secondary_addresses(vrrp), + ) + ) + values["vrrp"] = vrrp_list + + def get_vrrp_ipv4_secondary_addresses(self, vrrp: dict) -> Optional[List[VrrpIPv4SecondaryAddress]]: + secondary_addresses = [] + for address in vrrp.get("ipv4", {}).get("ipv4_secondary", []): + secondary_addresses.append(VrrpIPv4SecondaryAddress(address=address)) + return secondary_addresses if secondary_addresses else None + + def configure_virtual_router_redundancy_protocol_ipv6(self, values: dict) -> None: + if vrrps_ipv6 := values.get("ipv6_vrrp", []): + for vrrp_ipv6 in vrrps_ipv6: + vrrp_ipv6["group_id"] = vrrp_ipv6.pop("grp_id") + values["vrrp_ipv6"] = vrrps_ipv6 + + 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/thousandeyes.py b/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py index 699a394b..a16faada 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py +++ b/catalystwan/utils/config_migration/converters/feature_template/thousandeyes.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Union from catalystwan.api.configuration_groups.parcel import as_global, as_variable @@ -16,8 +17,12 @@ class ThousandEyesTemplateConverter: supported_template_types = ("cisco_thousandeyes",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> ThousandEyesParcel: + delete_keys = ("proxy_type", "proxy_pac", "proxy_static", "proxy_port") + + # Default Values - TE Management IP + thousand_eyes_mgmt_ip = "{{thousand_eyes_mgmt_ip}}" + + def create_parcel(self, name: str, description: str, template_values: dict) -> ThousandEyesParcel: """ Creates a ThousandEyesParcel object based on the provided template values. @@ -29,35 +34,38 @@ def create_parcel(name: str, description: str, template_values: dict) -> Thousan Returns: ThousandEyesParcel: A ThousandEyesParcel object with the provided values. """ - virtual_application = template_values["virtual_application"][0]["te"] + values = deepcopy(template_values["virtual_application"][0]["te"]) + self.configure_thousand_eyes_mgmt_ip(values) + self.configure_proxy_type(values) + self.cleanup_keys(values) + parcel_values = { + "parcel_name": name, + "parcel_description": description, + "virtual_application": [values], + } + return ThousandEyesParcel(**parcel_values) # type: ignore - if virtual_application.get("te_mgmt_ip"): - virtual_application["te_mgmt_ip"] = as_variable("{{thousand_eyes_mgmt_ip}}") + def configure_thousand_eyes_mgmt_ip(self, values: dict): + if values.get("te_mgmt_ip"): + values["te_mgmt_ip"] = as_variable(self.thousand_eyes_mgmt_ip) - proxy_type = virtual_application.get("proxy_type", as_global("none")) + def configure_proxy_type(self, values: dict): + proxy_type = values.get("proxy_type", as_global("none")) if proxy_type is None: proxy_type == as_global("none") proxy_type = proxy_type.value - proxy_config: Union[ProxyConfigNone, ProxyConfigPac, ProxyConfigStatic] = ProxyConfigNone() if proxy_type == "none": proxy_config = ProxyConfigNone() elif proxy_type == "pac": - proxy_config = ProxyConfigPac(pac_url=virtual_application["proxy_pac"]["pac_url"]) + proxy_config = ProxyConfigPac(pac_url=values["proxy_pac"]["pac_url"]) elif proxy_type == "static": proxy_config = ProxyConfigStatic( - proxy_host=virtual_application["proxy_static"]["proxy_host"], - proxy_port=virtual_application["proxy_static"]["proxy_port"], + proxy_host=values["proxy_static"]["proxy_host"], + proxy_port=values["proxy_static"]["proxy_port"], ) + values["proxy_config"] = proxy_config - virtual_application["proxy_config"] = proxy_config - - for key in ["proxy_type", "proxy_pac", "proxy_static", "proxy_port"]: - virtual_application.pop(key, None) - - parcel_values = { - "parcel_name": name, - "parcel_description": description, - "virtual_application": [virtual_application], - } - return ThousandEyesParcel(**parcel_values) # type: ignore + def cleanup_keys(self, values: dict): + for key in self.delete_keys: + values.pop(key, None) diff --git a/catalystwan/utils/config_migration/converters/feature_template/ucse.py b/catalystwan/utils/config_migration/converters/feature_template/ucse.py index 0c151892..e719662b 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/ucse.py +++ b/catalystwan/utils/config_migration/converters/feature_template/ucse.py @@ -12,8 +12,9 @@ class UcseTemplateConverter: supported_template_types = ("ucse",) - @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> UcseParcel: + delete_keys = ("module_type", "subslot_name") + + def create_parcel(self, name: str, description: str, template_values: dict) -> UcseParcel: """ Creates a UcseParcel object based on the provided template values. @@ -25,27 +26,34 @@ def create_parcel(name: str, description: str, template_values: dict) -> UcsePar Returns: UcseParcel: A UcseParcel object with the provided values. """ - parcel_values = deepcopy(template_values) - - for interface_values in parcel_values.get("interface", []): + values = deepcopy(template_values) + self.configure_interface(values) + self.configure_static_case(values) + self.configure_lom_type(values) + self.cleanup_keys(values) + values.update({"parcel_name": name, "parcel_description": description}) + return UcseParcel(**values) + + def configure_interface(self, values: dict) -> None: + for interface_values in values.get("interface", []): ip = interface_values.pop("ip", None) if ip: interface_values["address"] = ip.get("static_case", {}).get("address") - imc = parcel_values.get("imc", {}) + def configure_static_case(self, values: dict) -> None: + imc = values.get("imc", {}) static_case = imc.get("ip", {}).get("static_case") if static_case: imc["ip"] = static_case - access_port = imc.get("access_port", {}) + def configure_lom_type(self, values: dict) -> None: + access_port = values.get("imc", {}).get("access_port", {}) shared_lom = access_port.get("shared_lom") if shared_lom: lom_type = list(shared_lom.keys())[0] shared_lom.clear() access_port["shared_lom"]["lom_type"] = Global[LomType](value=lom_type) - for key in ["module_type", "subslot_name"]: - parcel_values.pop(key, None) - - parcel_values.update({"parcel_name": name, "parcel_description": description}) - return UcseParcel(**parcel_values) + 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/vpn.py b/catalystwan/utils/config_migration/converters/feature_template/vpn.py new file mode 100644 index 00000000..057adb11 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/vpn.py @@ -0,0 +1,521 @@ +import logging +from copy import deepcopy +from ipaddress import IPv4Interface, IPv6Interface +from typing import Literal, Type, Union + +from pydantic import BaseModel + +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 ( + DHCP, + 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 RouteLeakMappingItem(BaseModel): + ux2_model: Type[Union[RouteLeakFromGlobal, RouteLeakFromService, RouteLeakBetweenServices]] + ux2_field: Literal["route_leak_from_global", "route_leak_from_service", "route_leak_between_services"] + + +class RouteMappingItem(BaseModel): + ux2_model: Type[Union[StaticGreRouteIPv4, StaticIpsecRouteIPv4, ServiceRoute]] + ux2_field: Literal["route_gre", "route_service", "ipsec_route"] + + +class OmpMappingItem(BaseModel): + ux2_model_omp: Type[Union[OmpAdvertiseIPv4, OmpAdvertiseIPv6]] + ux2_model_prefix: Type[Union[IPv4Prefix, IPv6Prefix]] + ux2_field: Literal["omp_advertise_ipv4", "omp_advertise_ipv6"] + + +class LanVpnParcelTemplateConverter: + """ + A class for converting template values into a LanVpnParcel object. + """ + + supported_template_types = ("cisco_vpn", "vpn-vedge", "vpn-vsmart") + + delete_keys = ( + "ecmp_hash_key", + "ip", + "omp", + "nat", + "nat64", + "dns", + "host", + "ipv6", + "name", + "route_import_from", + "route_import", + "route_export", + "tcp_optimization", + ) + + 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"), + "route_import_from": RouteLeakMappingItem( + ux2_model=RouteLeakBetweenServices, ux2_field="route_leak_between_services" + ), + } + + routes_mapping = { + "route_gre": RouteMappingItem(ux2_model=StaticGreRouteIPv4, ux2_field="route_gre"), + "route_service": RouteMappingItem(ux2_model=ServiceRoute, ux2_field="route_service"), + "ipsec_route": RouteMappingItem(ux2_model=StaticIpsecRouteIPv4, ux2_field="ipsec_route"), + } + + omp_mapping = { + "advertise": OmpMappingItem( + ux2_model_omp=OmpAdvertiseIPv4, + ux2_model_prefix=IPv4Prefix, + ux2_field="omp_advertise_ipv4", + ), + "ipv6_advertise": OmpMappingItem( + ux2_model_omp=OmpAdvertiseIPv6, ux2_model_prefix=IPv6Prefix, ux2_field="omp_advertise_ipv6" + ), + } + + # 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 - Port Forwarding + nat_port_foward_natpool_name = "{{{{lan_vpn_natPortForward_{}_natpoolName}}}}" + nat_port_foward_translate_port = "{{{{lan_vpn_natPortForward_{}_translatePort}}}}" + nat_port_foward_translated_source_ip = "{{{{lan_vpn_natPortForward_{}_translatedSourceIp}}}}" + nat_port_foward_source_port = "{{{{lan_vpn_natPortForward_{}_sourcePort}}}}" + nat_port_foward_source_ip = "{{{{lan_vpn_natPortForward_{}_sourceIp}}}}" + nat_port_foward_protocol = "{{{{lan_vpn_natPortForward_{}_protocol}}}}" + + # 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}}}}" + + def create_parcel(self, name: str, description: str, template_values: dict) -> LanVpnParcel: + """ + Creates a LanVpnParcel object based on the provided parameters. + + Args: + name (str): The name of the parcel. + description (str): The description of the parcel. + template_values (dict): A dictionary containing the template values. + + Returns: + LanVpnParcel: The created LanVpnParcel object. + """ + values = deepcopy(template_values) + self.configure_vpn_name(values) + self.configure_vpn_id(name, values) + self.configure_natpool(values) + self.configure_port_forwarding(values) + self.configure_static_nat(values) + self.configure_nat64(values) + self.configure_omp(values) + self.configure_dns(values) + self.configure_hostname_mapping(values) + self.configure_service(values) + self.configure_ipv4_route(values) + self.configure_ipv6_route(values) + self.configure_routes(values) + self.configure_route_leaks(values) + self.cleanup_keys(values) + return LanVpnParcel(**self.prepare_parcel_values(name, description, values)) # type: ignore + + def prepare_parcel_values(self, name: str, description: str, values: dict) -> dict: + return { + "parcel_name": name, + "parcel_description": description, + **values, + } + + def cleanup_keys(self, values: dict) -> None: + for key in self.delete_keys: + values.pop(key, None) + + def configure_vpn_name(self, values: dict) -> None: + if vpn_name := values.get("name", None): + values["vpn_name"] = vpn_name + + def configure_vpn_id(self, name: str, values: dict) -> None: + if vpn_id := values.get("vpn_id"): + vpn_id_value = int(vpn_id.value) + # VPN 0 contains all of a device's interfaces except for the management interface, + # and all interfaces are disabled. + # VPN 512 is RESERVED for OOB network management + if vpn_id_value in (0, 512): + logger.warning( + f"VPN ID {vpn_id_value} is reserved for system use. " f"VPN ID will be set to 1 for VPN {name}." + ) + values["vpn_id"] = as_global(1) + else: + values["vpn_id"] = as_global(vpn_id_value) + + def configure_dns(self, values: dict) -> None: + if dns := values.get("dns", []): + dns_ipv4 = DnsIPv4() + for entry in dns: + if entry["role"] == "primary": + dns_ipv4.primary_dns_address_ipv4 = entry["dns_addr"] + elif entry["role"] == "secondary": + dns_ipv4.secondary_dns_address_ipv4 = entry["dns_addr"] + values["dns"] = dns_ipv4 + + def configure_hostname_mapping(self, values: dict) -> None: + if host := values.get("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 + + def configure_service(self, values: dict) -> None: + 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(self.service_ipv4_addresses.format(service_i + 1))), + tracking=entry.get("track_enable", as_default(True)), + ) + service_items.append(service_item) + values["service"] = service_items + + def configure_natpool(self, values: dict) -> None: + if natpool := values.get("nat", {}).get("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(self.nat_direction.format(nat_i + 1)) + nat_items.append( + NatPool( + nat_pool_name=entry.get("name", as_variable(self.nat_natpool_name.format(nat_i + 1))), + prefix_length=entry.get("prefix_length", as_variable(self.nat_prefix_length.format(nat_i + 1))), + range_start=entry.get("range_start", as_variable(self.nat_range_start.format(nat_i + 1))), + range_end=entry.get("range_end", as_variable(self.nat_range_end.format(nat_i + 1))), + overload=entry.get("overload", as_default(True)), + direction=direction, + ) + ) + values["nat_pool"] = nat_items + + def configure_port_forwarding(self, values: dict) -> None: + if port_forward := values.get("nat", {}).get("port_forward", []): + nat_port_forwarding_items = [] + for net_port_foward_i, entry in enumerate(port_forward): + protocol = entry.get("proto") + if protocol: + protocol = as_global(protocol.value.upper(), NATPortForwardProtocol) + else: + protocol = as_variable(self.nat_port_foward_protocol.format(net_port_foward_i + 1)) + nat_port_forwarding_items.append( + NatPortForward( + nat_pool_name=entry.get( + "pool_name", as_variable(self.nat_port_foward_natpool_name.format(net_port_foward_i + 1)) + ), + source_port=entry.get( + "source_port", as_variable(self.nat_port_foward_source_port.format(net_port_foward_i + 1)) + ), + translate_port=entry.get( + "translate_port", + as_variable(self.nat_port_foward_translate_port.format(net_port_foward_i + 1)), + ), + source_ip=entry.get( + "source_ip", as_variable(self.nat_port_foward_source_ip.format(net_port_foward_i + 1)) + ), + translated_source_ip=entry.get( + "translate_ip", + as_variable(self.nat_port_foward_translated_source_ip.format(net_port_foward_i + 1)), + ), + protocol=protocol, + ) + ) + values["nat_port_forwarding"] = nat_port_forwarding_items + + def configure_static_nat(self, values: dict) -> None: + if static_nat := values.get("nat", {}).get("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(self.static_nat_direction.format(static_nat_i + 1)) + static_nat_items.append( + StaticNat( + nat_pool_name=entry.get( + "pool_name", as_variable(self.static_nat_pool_name.format(static_nat_i + 1)) + ), + source_ip=entry.get( + "source_ip", as_variable(self.static_nat_source_ip.format(static_nat_i + 1)) + ), + translated_source_ip=entry.get( + "translate_ip", as_variable(self.static_nat_translated_source_ip.format(static_nat_i + 1)) + ), + static_nat_direction=static_nat_direction, + ) + ) + values["static_nat"] = static_nat_items + + def configure_nat64(self, values: dict) -> None: + if nat64pool := values.get("nat64", {}).get("v4", {}).get("pool", []): + nat64_items = [] + for nat64pool_i, entry in enumerate(nat64pool): + nat64_items.append( + Nat64v4Pool( + nat64_v4_pool_name=entry.get( + "name", as_variable(self.nat64_v4_pool_name.format(nat64pool_i + 1)) + ), + nat64_v4_pool_range_start=entry.get( + "start_address", as_variable(self.nat64_v4_pool_range_start.format(nat64pool_i + 1)) + ), + nat64_v4_pool_range_end=entry.get( + "end_address", as_variable(self.nat64_v4_pool_range_end.format(nat64pool_i + 1)) + ), + nat64_v4_pool_overload=entry.get( + "overload", as_variable(self.nat64_v4_pool_overload.format(nat64pool_i + 1)) + ), + ) + ) + values["nat64_v4_pool"] = nat64_items + + def configure_ipv4_route(self, values: dict) -> None: + if ipv4_route := values.get("ip", {}).get("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(self.ipv4_route_prefix_network_address.format(route_i + 1)), + subnet_mask=as_variable(self.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(self.ipv4_route_next_hop_address.format(route_i + 1, next_hop_i + 1)), + ), + distance=next_hop.pop( + "distance", + as_variable( + self.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 "next_hop_with_track" in route: + next_hop_with_track_items = [] + for next_hop_with_track_i, next_hop_with_track in enumerate(route.pop("next_hop_with_track", [])): + next_hop_with_track_items.append( + IPv4RouteGatewayNextHop( + address=next_hop_with_track.pop( + "address", + as_variable( + self.ipv4_route_next_hop_address.format(route_i + 1, next_hop_with_track_i + 1) + ), + ), + distance=next_hop_with_track.pop( + "distance", + as_variable( + self.ipv4_route_next_hop_administrative_distance.format( + route_i + 1, next_hop_with_track_i + 1 + ) + ), + ), + ) + ) + + ip_route_item = NextHopRouteContainer( + next_hop_container=NextHopContainer(next_hop_with_tracker=next_hop_items) # type: ignore + ) + elif "vpn" in route: + ip_route_item = StaticRouteVPN( # type: ignore + vpn=as_global(True), + ) + elif "dhcp" in route: + ip_route_item = DHCP( # type: ignore + dhcp=as_global(True), + ) + else: + # Let's assume it's a static route with enabled VPN + 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 + + def configure_ipv6_route(self, values: dict) -> None: + if ipv6_route := values.get("ipv6", {}).get("route", []): + ipv6_route_items = [] + for route in ipv6_route: + ipv6_interface = IPv6Interface(route.get("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 + + def configure_omp(self, values: dict) -> None: + for omp in self.omp_mapping.keys(): + if omp_advertises := values.get("omp", {}).get(omp, []): + pydantic_model_omp = self.omp_mapping[omp].ux2_model_omp + pydantic_model_prefix = self.omp_mapping[omp].ux2_model_prefix + pydantic_field = self.omp_mapping[omp].ux2_field + self._configure_omp(values, omp_advertises, pydantic_model_omp, pydantic_model_prefix, pydantic_field) + + def _configure_omp( + self, values: dict, omp_advertises: list, pydantic_model_omp, pydantic_model_prefix, pydantic_field + ) -> None: + omp_advertise_items = [] + for entry in omp_advertises: + prefix_list_items = [] + for prefix_entry in entry.get("prefix_list", []): + prefix_list_items.append( + pydantic_model_prefix( + prefix=prefix_entry["prefix_entry"], + aggregate_only=prefix_entry["aggregate_only"], + region=as_global(prefix_entry["region"].value, Region), + ) + ) + if pydantic_model_omp == OmpAdvertiseIPv4: + pydantic_model_protocol = ProtocolIPv4 + else: + pydantic_model_protocol = ProtocolIPv6 + omp_advertise_items.append( + pydantic_model_omp( + omp_protocol=as_global(entry["protocol"].value, pydantic_model_protocol), + prefix_list=prefix_list_items if prefix_list_items else None, + ) + ) + values[pydantic_field] = omp_advertise_items + + def configure_routes(self, values: dict) -> None: + for route in self.routes_mapping.keys(): + if routes := values.get("ip", {}).get(route, []): + pydantic_model = self.routes_mapping[route].ux2_model + pydantic_field = self.routes_mapping[route].ux2_field + self._configure_route(values, routes, pydantic_model, pydantic_field) + + def _configure_route(self, values: dict, routes: list, pydantic_model, pydantic_field) -> None: + items = [] + for route in routes: + ipv4_interface = IPv4Interface(route.get("prefix").value) + service_prefix = Prefix( + address=as_global(ipv4_interface.network.network_address), + mask=as_global(str(ipv4_interface.netmask)), + ) + items.append(pydantic_model(prefix=service_prefix, vpn=route.get("vpn"))) + values[pydantic_field] = items + + def configure_route_leaks(self, values: dict) -> None: + for leak in self.route_leaks_mapping.keys(): + if route_leaks := values.get(leak, []): + pydantic_model = self.route_leaks_mapping[leak].ux2_model + pydantic_field = self.route_leaks_mapping[leak].ux2_field + self._configure_leak(values, route_leaks, pydantic_model, pydantic_field) + + def _configure_leak(self, values: dict, route_leaks: list, pydantic_model, pydantic_field) -> None: + items = [] + for rl in route_leaks: + redistribute_items = [] + for redistribute_item in rl.get("redistribute_to", []): + redistribute_items.append( + RedistributeToService( + protocol=as_global(redistribute_item["protocol"].value, RedistributeToServiceProtocol), + ) + ) + configuration = { + "route_protocol": as_global(rl["protocol"].value, RouteLeakFromServiceProtocol), + "redistribute_to_protocol": redistribute_items if redistribute_items else None, + } + if pydantic_model == RouteLeakBetweenServices: + configuration["source_vpn"] = rl["source_vpn"] + items.append(pydantic_model(**configuration)) + values[pydantic_field] = items diff --git a/catalystwan/utils/config_migration/creators/config_pusher.py b/catalystwan/utils/config_migration/creators/config_pusher.py index d21af562..5fc85f74 100644 --- a/catalystwan/utils/config_migration/creators/config_pusher.py +++ b/catalystwan/utils/config_migration/creators/config_pusher.py @@ -1,11 +1,11 @@ +import logging from typing import Callable, Dict, List, cast from uuid import UUID -from venv import logger from pydantic import BaseModel from catalystwan.endpoints.configuration_group import ProfileId -from catalystwan.exceptions import CatalystwanException +from catalystwan.exceptions import ManagerHTTPError from catalystwan.models.configuration.config_migration import ( TransformedFeatureProfile, TransformedParcel, @@ -14,9 +14,10 @@ ) from catalystwan.models.configuration.feature_profile.common import ProfileType from catalystwan.session import ManagerSession -from catalystwan.utils.config_migration.factories.feature_profile_api import FeatureProfileAPIFactory from catalystwan.utils.config_migration.factories.parcel_pusher import ParcelPusherFactory +logger = logging.getLogger(__name__) + class ConfigurationMapping(BaseModel): feature_profile_map: Dict[UUID, TransformedFeatureProfile] @@ -24,9 +25,7 @@ class ConfigurationMapping(BaseModel): class UX2ConfigPusher: - def __init__( - self, session: ManagerSession, ux2_config: UX2Config, logger: Callable[[str, int, int], None] - ) -> None: + def __init__(self, session: ManagerSession, ux2_config: UX2Config, logger: Callable[[str, int, int], None]) -> None: self._session = session self._config_map = self._create_config_map(ux2_config) self._config_rollback = UX2ConfigRollback() @@ -42,39 +41,59 @@ def _create_config_map(self, ux2_config: UX2Config) -> ConfigurationMapping: def push(self) -> UX2ConfigRollback: try: self._create_config_groups() - except CatalystwanException as e: - logger.error(f"Error occured during config push: {e}") + except ManagerHTTPError as e: + logger.error(f"Error occured during config push: {e.info}") + logger.debug(f"Configuration push completed successfully. Rollback configuration {self._config_rollback}") return self._config_rollback def _create_config_groups(self): config_groups = self._ux2_config.config_groups config_groups_length = len(config_groups) for i, transformed_config_group in enumerate(config_groups): + self._logger("Creating Configuration Groups", i + 1, config_groups_length) + logger.debug( + f"Creating config group: {transformed_config_group.config_group.name} " + f"with origin uuid: {transformed_config_group.header.origin} " + f"and feature profiles: {transformed_config_group.header.subelements}" + ) config_group_payload = transformed_config_group.config_group config_group_payload.profiles = self._create_feature_profile_and_parcels( transformed_config_group.header.subelements ) cg_id = self._session.endpoints.configuration_group.create_config_group(config_group_payload).id - self._logger("Creating Configuration Groups", i + 1, config_groups_length) self._config_rollback.add_config_group(cg_id) def _create_feature_profile_and_parcels(self, feature_profiles_ids: List[UUID]) -> List[ProfileId]: config_group_profiles = [] - for feature_profile_id in feature_profiles_ids: + feature_profile_length = len(feature_profiles_ids) + for i, feature_profile_id in enumerate(feature_profiles_ids): + self._logger("Creating Feature Profile", i + 1, feature_profile_length) transformed_feature_profile = self._config_map.feature_profile_map[feature_profile_id] + logger.debug( + f"Creating feature profile: {transformed_feature_profile.feature_profile.name} " + f"with origin uuid: {transformed_feature_profile.header.origin} " + f"and parcels: {transformed_feature_profile.header.subelements}" + ) profile_type = cast(ProfileType, transformed_feature_profile.header.type) - api = FeatureProfileAPIFactory.get_api(profile_type, self._session) - name = transformed_feature_profile.feature_profile.name - description = transformed_feature_profile.feature_profile.description if profile_type == "policy-object": - # TODO: Get default policy profile + logger.debug(f"Skipping policy-object profile: {transformed_feature_profile.feature_profile.name}") continue - created_profile_id = api.create_profile(name, description).id # type: ignore + pusher = ParcelPusherFactory.get_pusher(self._session, profile_type) + parcels = self._create_parcels_list(transformed_feature_profile) + created_profile_id = pusher.push(transformed_feature_profile.feature_profile, parcels) config_group_profiles.append(ProfileId(id=created_profile_id)) - self._create_parcels(api, created_profile_id, profile_type, transformed_feature_profile.header.subelements) self._config_rollback.add_feature_profile(created_profile_id, profile_type) return config_group_profiles - def _create_parcels(self, api, profile_uuid, profile_type, parcels_uuids): - pusher = ParcelPusherFactory.get_pusher(profile_type, api) - pusher.push(profile_uuid, parcels_uuids, self._config_map.parcel_map) + def _create_parcels_list(self, transformed_feature_profile: TransformedFeatureProfile) -> List[TransformedParcel]: + logger.debug(f"Creating parcels for feature profile: {transformed_feature_profile.feature_profile.name}") + parcels = [] + for element_uuid in transformed_feature_profile.header.subelements: + transformed_parcel = self._config_map.parcel_map.get(element_uuid) + if not transformed_parcel: + # Device templates can have assigned feature templates but when we download the + # featrue templates from the enpoint some templates don't exist in the response + logger.error(f"Parcel with origin uuid {element_uuid} not found in the config map") + else: + parcels.append(transformed_parcel) + return parcels diff --git a/catalystwan/utils/config_migration/creators/strategy/parcels.py b/catalystwan/utils/config_migration/creators/strategy/parcels.py index 0cb039f1..93098662 100644 --- a/catalystwan/utils/config_migration/creators/strategy/parcels.py +++ b/catalystwan/utils/config_migration/creators/strategy/parcels.py @@ -1,8 +1,14 @@ -from typing import Dict, List +from typing import List from uuid import UUID from catalystwan.models.configuration.config_migration import TransformedParcel -from catalystwan.utils.config_migration.factories.feature_profile_api import FeatureProfile +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload, ProfileType +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ethernet import InterfaceEthernetParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.gre import InterfaceGreParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.ipsec import InterfaceIpsecParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.svi import InterfaceSviParcel +from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import LanVpnParcel +from catalystwan.session import ManagerSession class ParcelPusher: @@ -10,30 +16,53 @@ class ParcelPusher: Base class for pushing parcels to a feature profile. """ - def __init__(self, api: FeatureProfile): - self.api = api + def __init__(self, session: ManagerSession, profile_type: ProfileType): + self.builder = session.api.builders.feature_profiles.create_builder(profile_type) - def push(self, profile_uuid: UUID, parcel_uuids: List[UUID], mapping: Dict[UUID, TransformedParcel]): - """ - Push parcels to the given feature profile. - - Args: - profile_uuid (UUID): The UUID of the feature profile. - parcel_uuids (List[UUID]): The list of parcel UUIDs to push. - mapping (Dict[UUID, TransformedParcel]): The mapping of parcel UUIDs to transformed parcels. - """ + def push(self, feature_profile: FeatureProfileCreationPayload, parcels: List[TransformedParcel]) -> UUID: raise NotImplementedError class SimpleParcelPusher(ParcelPusher): """ Simple implementation of ParcelPusher that creates parcels directly. + Includes: Other and System feature profiles. """ - def push(self, profile_uuid: UUID, parcel_uuids: List[UUID], mapping: Dict[UUID, TransformedParcel]): + def push(self, feature_profile: FeatureProfileCreationPayload, parcels: List[TransformedParcel]) -> UUID: # Parcels don't have references to other parcels, so we can create them directly - for parcel_uuid in parcel_uuids: - transformed_parcel = mapping.get(parcel_uuid) - if transformed_parcel is None: - continue - self.api.create_parcel(profile_uuid, transformed_parcel.parcel) # type: ignore + for transformed_parcel in parcels: + self.builder.add_parcel(transformed_parcel.parcel) # type: ignore + self.builder.add_profile_name_and_description(feature_profile) + return self.builder.build() + + +class ServiceParcelPusher(ParcelPusher): + """ + Parcel pusher for service feature profiles. + """ + + def push(self, feature_profile: FeatureProfileCreationPayload, parcels: List[TransformedParcel]) -> UUID: + # Service feature profiles have references to other parcels, so we need to create them in order + self._move_vpn_parcel_to_first_position(parcels) + for transformed_parcel in parcels: + self._resolve_and_add_parcel(transformed_parcel) + self.builder.add_profile_name_and_description(feature_profile) + return self.builder.build() + + def _resolve_and_add_parcel(self, transformed_parcel: TransformedParcel) -> None: + parcel = transformed_parcel.parcel + if isinstance(parcel, LanVpnParcel): + self.builder.add_parcel_vpn(parcel) # type: ignore + if isinstance(parcel, (InterfaceEthernetParcel, InterfaceGreParcel, InterfaceIpsecParcel, InterfaceSviParcel)): + # TODO: Assiging logic + # Every Service VPN parcel can have interface childs and + # there can be multiple service VPNs in one feature profile + pass + else: + self.builder.add_parcel(parcel) # type: ignore + + def _move_vpn_parcel_to_first_position(self, parcels: List[TransformedParcel]) -> None: + """Move the VPN parcel to the first position in the list. + Without this there is posibility to add_parcel_vpn_interface with None tag.""" + parcels.sort(key=lambda x: x.parcel._get_parcel_type() == "lan/vpn", reverse=True) diff --git a/catalystwan/utils/config_migration/factories/feature_profile_api.py b/catalystwan/utils/config_migration/factories/feature_profile_api.py index 53200456..a23faf96 100644 --- a/catalystwan/utils/config_migration/factories/feature_profile_api.py +++ b/catalystwan/utils/config_migration/factories/feature_profile_api.py @@ -1,8 +1,5 @@ from typing import Callable, Mapping, Union -from pydantic import Field -from typing_extensions import Annotated - from catalystwan.api.feature_profile_api import ( OtherFeatureProfileAPI, PolicyObjectFeatureProfileAPI, @@ -19,9 +16,8 @@ "service": ServiceFeatureProfileAPI, } -FeatureProfile = Annotated[ - Union[SystemFeatureProfileAPI, OtherFeatureProfileAPI, PolicyObjectFeatureProfileAPI, ServiceFeatureProfileAPI], - Field(discriminator="type"), +FeatureProfile = Union[ + SystemFeatureProfileAPI, OtherFeatureProfileAPI, PolicyObjectFeatureProfileAPI, ServiceFeatureProfileAPI ] diff --git a/catalystwan/utils/config_migration/factories/parcel_pusher.py b/catalystwan/utils/config_migration/factories/parcel_pusher.py index 6fb45a17..5181b350 100644 --- a/catalystwan/utils/config_migration/factories/parcel_pusher.py +++ b/catalystwan/utils/config_migration/factories/parcel_pusher.py @@ -1,12 +1,20 @@ +import logging from typing import Callable, Mapping -from catalystwan.api.feature_profile_api import FeatureProfileAPI from catalystwan.models.configuration.feature_profile.common import ProfileType -from catalystwan.utils.config_migration.creators.strategy.parcels import ParcelPusher, SimpleParcelPusher +from catalystwan.session import ManagerSession +from catalystwan.utils.config_migration.creators.strategy.parcels import ( + ParcelPusher, + ServiceParcelPusher, + SimpleParcelPusher, +) -PARCEL_PUSHER_MAPPING: Mapping[ProfileType, Callable] = { +logger = logging.getLogger(__name__) + +PARCEL_PUSHER_MAPPING: Mapping[ProfileType, Callable[[ManagerSession, ProfileType], ParcelPusher]] = { "other": SimpleParcelPusher, "system": SimpleParcelPusher, + "service": ServiceParcelPusher, } @@ -16,18 +24,9 @@ class ParcelPusherFactory: """ @staticmethod - def get_pusher(profile_type: ProfileType, api: FeatureProfileAPI) -> ParcelPusher: - """ - Get the appropriate ParcelPusher instance based on the profile type. - - Args: - profile_type (ProfileType): The type of the feature profile. - api (FeatureProfileAPI): The API for interacting with feature profiles. - - Returns: - ParcelPusher: The appropriate ParcelPusher instance. - """ + def get_pusher(session: ManagerSession, profile_type: ProfileType) -> ParcelPusher: pusher_class = PARCEL_PUSHER_MAPPING.get(profile_type) if pusher_class is None: raise ValueError(f"Invalid profile type: {profile_type}") - return pusher_class(api) + logger.debug(f"Creating {pusher_class} for profile type: {profile_type}") + return pusher_class(session, profile_type) diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index a9c6aa9f..4ae6eb39 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -50,6 +50,15 @@ "ucse", "dhcp", "cisco_dhcp_server", + "cisco_vpn", + "cisco_vpn_interface_gre", + "vpn-vsmart-interface", + "vpn-vedge-interface", + "vpn-vmanage-interface", + "cisco_vpn_interface", + "cisco_vpn_interface_ipsec", + "vpn-interface-svi", + "cisco_ospf", ] FEATURE_PROFILE_SYSTEM = [ @@ -86,6 +95,18 @@ "ucse", ] +FEATURE_PROFILE_SERVICE = [ + "cisco_vpn", + "cisco_vpn_interface_gre", + "vpn-vsmart-interface", + "vpn-vedge-interface", + "vpn-vmanage-interface", + "cisco_vpn_interface", + "cisco_vpn_interface_ipsec", + "vpn-interface-svi", + "cisco_ospf", +] + def log_progress(task: str, completed: int, total: int) -> None: logger.info(f"{task} {completed}/{total}") @@ -120,6 +141,17 @@ def transform(ux1: UX1Config) -> UX2Config: description="other", ), ) + fp_service_uuid = uuid4() + transformed_fp_service = TransformedFeatureProfile( + header=TransformHeader( + type="service", + origin=fp_service_uuid, + ), + feature_profile=FeatureProfileCreationPayload( + name=f"{dt.template_name}_service", + description="service", + ), + ) for template in templates: # Those feature templates IDs are real UUIDs and are used to map to the feature profiles @@ -127,12 +159,14 @@ def transform(ux1: UX1Config) -> UX2Config: transformed_fp_system.header.subelements.add(UUID(template.templateId)) elif template.templateType in FEATURE_PROFILE_OTHER: transformed_fp_other.header.subelements.add(UUID(template.templateId)) + elif template.templateType in FEATURE_PROFILE_SERVICE: + transformed_fp_service.header.subelements.add(UUID(template.templateId)) transformed_cg = TransformedConfigGroup( header=TransformHeader( type="config_group", origin=UUID(dt.template_id), - subelements=set([fp_system_uuid, fp_other_uuid]), + subelements=set([fp_system_uuid, fp_other_uuid, fp_service_uuid]), ), config_group=ConfigGroupCreationPayload( name=dt.template_name, @@ -144,6 +178,7 @@ def transform(ux1: UX1Config) -> UX2Config: # Add to UX2 ux2.feature_profiles.append(transformed_fp_system) ux2.feature_profiles.append(transformed_fp_other) + ux2.feature_profiles.append(transformed_fp_service) ux2.config_groups.append(transformed_cg) for ft in ux1.templates.feature_templates: @@ -205,10 +240,10 @@ def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, in """Collect Templates""" template_api = session.api.templates - progress("Collecting Templates Info", 0, 2) + progress("Collecting Feature Templates", 0, 1) ux1.templates.feature_templates = [t for t in template_api.get_feature_templates()] - progress("Collecting Feature Templates", 1, 2) + progress("Collecting Feature Templates", 1, 1) device_templates_information = template_api.get_device_templates() for i, device_template_information in enumerate(device_templates_information):