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):