From cb903316120cc41f52dae69fa99659808f952f0d Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 8 Feb 2024 20:38:55 +0100 Subject: [PATCH 01/21] Migration - Software and Versions --- catalystwan/api/partition_manager_api.py | 9 ++- catalystwan/api/software_action_api.py | 7 +- catalystwan/api/versions_utils.py | 76 +++++++++++-------- .../tests/test_partition_manager_api.py | 12 +-- catalystwan/tests/test_software_action_api.py | 10 +-- catalystwan/tests/test_version_utils.py | 22 ++++-- 6 files changed, 78 insertions(+), 58 deletions(-) diff --git a/catalystwan/api/partition_manager_api.py b/catalystwan/api/partition_manager_api.py index 289874e78..6e1f5ab85 100644 --- a/catalystwan/api/partition_manager_api.py +++ b/catalystwan/api/partition_manager_api.py @@ -7,7 +7,6 @@ from catalystwan.api.versions_utils import DeviceVersions, RemovePartitionPayload, RepositoryAPI from catalystwan.dataclasses import Device from catalystwan.typed_list import DataSequence -from catalystwan.utils.creation_tools import asdict from catalystwan.utils.upgrades_helper import get_install_specification, validate_personality_homogeneity logger = logging.getLogger(__name__) @@ -64,7 +63,7 @@ def set_default_partition(self, devices: DataSequence[Device], partition: Option url = "/dataservice/device/action/defaultpartition" payload = { "action": "defaultpartition", - "devices": [asdict(device) for device in payload_devices], # type: ignore + "devices": [device.model_dump() for device in payload_devices], # type: ignore "deviceType": get_install_specification(devices.first()).device_type.value, } set_default = dict(self.session.post(url, json=payload).json()) @@ -92,14 +91,16 @@ def remove_partition( payload_devices = self.device_version.get_devices_available_versions(devices) remove_partition_payload = [ - RemovePartitionPayload(device.deviceId, device.deviceIP, device.version) # type: ignore + RemovePartitionPayload( + device_id=device.device_id, device_ip=device.device_id, version=device.version + ) # type: ignore for device in payload_devices ] url = "/dataservice/device/action/removepartition" payload = { "action": "removepartition", - "devices": [asdict(device) for device in remove_partition_payload], # type: ignore + "devices": [device.model_dump() for device in remove_partition_payload], # type: ignore "deviceType": get_install_specification(devices.first()).device_type.value, } if force is False: diff --git a/catalystwan/api/software_action_api.py b/catalystwan/api/software_action_api.py index 0fae3a80d..ce4d340c8 100644 --- a/catalystwan/api/software_action_api.py +++ b/catalystwan/api/software_action_api.py @@ -8,7 +8,6 @@ from catalystwan.dataclasses import Device from catalystwan.exceptions import VersionDeclarationError # type: ignore from catalystwan.typed_list import DataSequence -from catalystwan.utils.creation_tools import asdict from catalystwan.utils.personality import Personality from catalystwan.utils.upgrades_helper import get_install_specification, validate_personality_homogeneity from catalystwan.version import parse_vmanage_version @@ -78,9 +77,7 @@ def activate( url = "/dataservice/device/action/changepartition" payload = { "action": "changepartition", - "devices": [ - asdict(device) for device in self.device_versions.get_device_available(version, devices) # type: ignore - ], + "devices": [device.model_dump() for device in self.device_versions.get_device_available(version, devices)], "deviceType": get_install_specification(devices.first()).device_type.value, } activate = dict(self.session.post(url, json=payload).json()) @@ -137,7 +134,7 @@ def install( "sync": sync, }, "devices": [ - {"deviceId": device.deviceId, "deviceIP": device.deviceIP} + {"deviceId": device.device_id, "deviceIP": device.device_ip} for device in self.device_versions.get_device_list(devices) ], # type: ignore "deviceType": install_specification.device_type.value, diff --git a/catalystwan/api/versions_utils.py b/catalystwan/api/versions_utils.py index dde729bf8..204e6fc2b 100644 --- a/catalystwan/api/versions_utils.py +++ b/catalystwan/api/versions_utils.py @@ -2,16 +2,17 @@ import logging from pathlib import PurePath -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Union -from attr import Factory, define, field # type: ignore from clint.textui.progress import Bar as ProgressBar # type: ignore +from pydantic import BaseModel, ConfigDict, Field +from pydantic.functional_validators import BeforeValidator from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor # type: ignore +from typing_extensions import Annotated -from catalystwan.dataclasses import DataclassBase, Device +from catalystwan.dataclasses import Device from catalystwan.exceptions import ImageNotInRepositoryError from catalystwan.typed_list import DataSequence -from catalystwan.utils.creation_tools import FIELD_NAME, create_dataclass if TYPE_CHECKING: from catalystwan.session import vManageSession @@ -19,27 +20,40 @@ logger = logging.getLogger(__name__) -@define -class DeviceSoftwareRepository(DataclassBase): - installed_versions: List[str] = field(default=None) - available_versions: List[str] = field(default=Factory(list), metadata={FIELD_NAME: "availableVersions"}) - current_version: str = field(default=None, metadata={FIELD_NAME: "version"}) - default_version: str = field(default=None, metadata={FIELD_NAME: "defaultVersion"}) - device_id: str = field(default=None, metadata={FIELD_NAME: "uuid"}) +class DeviceSoftwareRepository(BaseModel): + model_config = ConfigDict(extra="ignore") + installed_versions: List[str] = Field(default_factory=list) + available_versions: List[str] = Field( + default_factory=list, serialization_alias="availableVersions", validation_alias="availableVersions" + ) + current_version: str = Field( + default="", + serialization_alias="version", + validation_alias="version", + description="Current active version of software on device", + ) + default_version: str = Field(default="", serialization_alias="defaultVersion", validation_alias="defaultVersion") + device_id: str = Field(default="", serialization_alias="uuid", validation_alias="uuid") -@define -class DeviceVersionPayload(DataclassBase): - deviceId: str - deviceIP: str - version: Optional[Union[str, List[str]]] = "" +class DeviceVersionPayload(BaseModel): + device_id: str = Field(serialization_alias="deviceId") + device_ip: str = Field(serialization_alias="deviceIP") + version: Union[str, List[str]] = Field(default="") -@define(frozen=False) -class RemovePartitionPayload(DataclassBase): - deviceId: str - deviceIP: str - version: Union[str, List[str]] = field(converter=(lambda x: [x] if isinstance(x, str) else x)) + +def convert_to_list(element: Union[str, List[str]]) -> List[str]: + return [element] if isinstance(element, str) else element + + +VersionList = Annotated[Union[str, List[str]], BeforeValidator(convert_to_list)] + + +class RemovePartitionPayload(BaseModel): + device_id: str = Field(serialization_alias="deviceId") + device_ip: str = Field(serialization_alias="deviceIP") + version: VersionList class RepositoryAPI: @@ -89,10 +103,10 @@ def get_devices_versions_repository(self) -> Dict[str, DeviceSoftwareRepository] edges_versions_info = self.session.get_data(url) devices_versions_repository = {} for device in controllers_versions_info + edges_versions_info: - device_all_versions = create_dataclass(DeviceSoftwareRepository, device) - device_all_versions.installed_versions = [version for version in device_all_versions.available_versions] - device_all_versions.installed_versions.append(device_all_versions.current_version) - devices_versions_repository[device_all_versions.device_id] = device_all_versions + device_software_repository = DeviceSoftwareRepository(**device) + device_software_repository.installed_versions = [a for a in device_software_repository.available_versions] + device_software_repository.installed_versions.append(device_software_repository.current_version) + devices_versions_repository[device_software_repository.device_id] = device_software_repository return devices_versions_repository def get_image_version(self, software_image: str) -> Union[str, None]: @@ -189,11 +203,12 @@ def _get_device_list_in( list : list of devices """ devices_payload = DataSequence( - DeviceVersionPayload, [DeviceVersionPayload(device.uuid, device.id) for device in devices] # type: ignore + DeviceVersionPayload, + [DeviceVersionPayload(device_id=device.uuid, device_ip=device.id) for device in devices], ) all_dev_versions = self.repository.get_devices_versions_repository() for device in devices_payload: - device_versions = getattr(all_dev_versions[device.deviceId], version_type) + device_versions = getattr(all_dev_versions[device.device_id], version_type) try: for version in device_versions: if version_to_set_up in version: @@ -254,11 +269,12 @@ def _get_devices_chosen_version( list : list of devices """ devices_payload = DataSequence( - DeviceVersionPayload, [DeviceVersionPayload(device.uuid, device.id) for device in devices] # type: ignore + DeviceVersionPayload, + [DeviceVersionPayload(device_id=device.uuid, device_ip=device.id) for device in devices], ) all_dev_versions = self.repository.get_devices_versions_repository() for device in devices_payload: - device.version = getattr(all_dev_versions[device.deviceId], version_type) + device.version = getattr(all_dev_versions[device.device_id], version_type) return devices_payload def get_devices_current_version(self, devices: DataSequence[Device]) -> DataSequence[DeviceVersionPayload]: @@ -291,4 +307,4 @@ def get_devices_available_versions(self, devices: DataSequence[Device]) -> DataS return self._get_devices_chosen_version(devices, "available_versions") def get_device_list(self, devices: DataSequence[Device]) -> List[DeviceVersionPayload]: - return [DeviceVersionPayload(device.uuid, device.id) for device in devices] # type: ignore + return [DeviceVersionPayload(device_id=device.uuid, device_ip=device.id) for device in devices] # type: ignore diff --git a/catalystwan/tests/test_partition_manager_api.py b/catalystwan/tests/test_partition_manager_api.py index be1abc7f2..4656d5f6a 100644 --- a/catalystwan/tests/test_partition_manager_api.py +++ b/catalystwan/tests/test_partition_manager_api.py @@ -28,17 +28,17 @@ def setUp(self): ) self.DeviceSoftwareRepository_obj = { "mock_uuid": DeviceSoftwareRepository( - ["ver1", "ver2", "curr_ver"], - ["ver1", "ver2"], - "curr_ver", - "def_ver", - "mock_uuid", + installed_versions=["ver1", "ver2", "curr_ver"], + availableVersions=["ver1", "ver2"], + version="curr_ver", + defaultVersion="def_ver", + uuid="mock_uuid", ), } self.mock_devices = [{"deviceId": "mock_uuid", "deviceIP": "mock_ip", "version": "ver1"}] self.mock_device_version_payload = DataSequence( - DeviceVersionPayload, [DeviceVersionPayload("mock_uuid", "mock_ip", "ver1")] + DeviceVersionPayload, [DeviceVersionPayload(device_id="mock_uuid", device_ip="mock_ip", version="ver1")] ) mock_session = Mock() self.mock_repository_object = RepositoryAPI(mock_session) diff --git a/catalystwan/tests/test_software_action_api.py b/catalystwan/tests/test_software_action_api.py index 1f04c2e08..3d4924f81 100644 --- a/catalystwan/tests/test_software_action_api.py +++ b/catalystwan/tests/test_software_action_api.py @@ -24,11 +24,11 @@ def setUp(self): ) self.DeviceSoftwareRepository_obj = { "mock_uuid": DeviceSoftwareRepository( - ["ver1", "ver2", "curr_ver"], - ["ver1", "ver2"], - "20.8", - "def_ver", - "mock_uuid", + installed_versions=["ver1", "ver2", "curr_ver"], + availableVersions=["ver1", "ver2"], + version="20.8", + defaultVersion="def_ver", + uuid="mock_uuid", ), } diff --git a/catalystwan/tests/test_version_utils.py b/catalystwan/tests/test_version_utils.py index 63b2d75af..4648118a8 100644 --- a/catalystwan/tests/test_version_utils.py +++ b/catalystwan/tests/test_version_utils.py @@ -22,11 +22,11 @@ def setUp(self): ) self.DeviceSoftwareRepository_obj = { "mock_uuid": DeviceSoftwareRepository( - ["ver1", "ver2", "curr_ver"], - ["ver1", "ver2"], - "curr_ver", - "def_ver", - "mock_uuid", + installed_versions=["ver1", "ver2", "curr_ver"], + availableVersions=["ver1", "ver2"], + version="curr_ver", + defaultVersion="def_ver", + uuid="mock_uuid", ) } @@ -78,7 +78,9 @@ def test_get_device_available(self, mock_get_devices_versions_repository): mock_device_versions = DeviceVersions(mock_repository_object) mock_get_devices_versions_repository.return_value = self.DeviceSoftwareRepository_obj answer = mock_device_versions.get_device_available("ver1", [self.device]) - expected_result = DataSequence(DeviceVersionPayload, [DeviceVersionPayload("mock_uuid", "mock_ip", "ver1")]) + expected_result = DataSequence( + DeviceVersionPayload, [DeviceVersionPayload(device_id="mock_uuid", device_ip="mock_ip", version="ver1")] + ) # Assert self.assertEqual( @@ -96,7 +98,9 @@ def test_get_device_list_if_in_installed(self, mock_get_devices_versions_reposit mock_device_versions = DeviceVersions(mock_repository_object) mock_get_devices_versions_repository.return_value = self.DeviceSoftwareRepository_obj answer = mock_device_versions.get_device_list_in_installed("ver1", [self.device]) - expected_result = DataSequence(DeviceVersionPayload, [DeviceVersionPayload("mock_uuid", "mock_ip", "ver1")]) + expected_result = DataSequence( + DeviceVersionPayload, [DeviceVersionPayload(device_id="mock_uuid", device_ip="mock_ip", version="ver1")] + ) # Assert self.assertEqual( @@ -116,5 +120,7 @@ def test_get_devices_current_version(self, mock_get_devices_versions_repository) # Act answer = mock_device_versions.get_devices_current_version([self.device]) # Answer - proper_answer = DataSequence(DeviceVersionPayload, [DeviceVersionPayload("mock_uuid", "mock_ip", "curr_ver")]) + proper_answer = DataSequence( + DeviceVersionPayload, [DeviceVersionPayload(device_id="mock_uuid", device_ip="mock_ip", version="curr_ver")] + ) self.assertEqual(answer, proper_answer) From ba611f0b845977667d78951170e846ce340968bd Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 22 Feb 2024 14:09:39 +0100 Subject: [PATCH 02/21] System Endpoints --- catalystwan/api/feature_profile_api.py | 61 ++++++++++++++++++- .../configuration_feature_profile.py | 28 +++++++++ .../feature_profile/sdwan/system/__init__.py | 29 +++++++++ .../feature_profile/sdwan/system/aaa.py | 2 +- 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index abdf84871..aea40008f 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -10,7 +10,10 @@ from catalystwan.api.parcel_api import SDRoutingFullConfigParcelAPI from catalystwan.endpoints.configuration.feature_profile.sdwan.policy_object import PolicyObjectFeatureProfile -from catalystwan.endpoints.configuration_feature_profile import SDRoutingConfigurationFeatureProfile +from catalystwan.endpoints.configuration_feature_profile import ( + SDRoutingConfigurationFeatureProfile, + SystemConfigurationFeatureProfile, +) from catalystwan.models.configuration.feature_profile.common import ( FeatureProfileCreationPayload, FeatureProfileCreationResponse, @@ -45,6 +48,10 @@ URLAllowParcel, URLBlockParcel, ) +from catalystwan.models.configuration.feature_profile.sdwan.system import ( + SYSTEM_PAYLOAD_ENDPOINT_MAPPING, + AnySystemParcel, +) class SDRoutingFeatureProfilesAPI: @@ -103,6 +110,58 @@ def delete(self, fp_id: str) -> None: self.endpoint.delete_cli_feature_profile(cli_fp_id=fp_id) +class SystemFeatureProfileAPI: + """ + SDWAN Feature Profile System APIs + """ + + def __init__(self, session: ManagerSession): + self.session = session + self.endpoint = SystemConfigurationFeatureProfile(session) + + def get( + self, + profile_id: UUID, + parcel_type: Type[AnySystemParcel], + parcel_id: Union[UUID, None] = None, + ) -> DataSequence[Parcel[Any]]: + """ + Get all System Parcels for selected profile_id and selected type or get one Policy Object given parcel id + """ + + parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] + if not parcel_id: + return self.endpoint.get_all(profile_id=profile_id, parcel_type=parcel_type_) + parcel = self.endpoint.get_by_id(profile_id=profile_id, parcel_type=parcel_type_, list_object_id=parcel_id) + return DataSequence(Parcel, [parcel]) + + def create(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCreationResponse: + """ + Create System Parcel for selected profile_id based on payload type + """ + + parcel_type = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[type(payload)] + return self.endpoint.create(profile_id=profile_id, parcel_type=parcel_type, payload=payload) + + def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID): + """ + Update System Parcel for selected profile_id based on payload type + """ + + policy_type = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[type(payload)] + return self.endpoint.update( + profile_id=profile_id, parcel_type=policy_type, parcel_id=parcel_id, payload=payload + ) + + def delete(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id: UUID) -> None: + """ + Delete System Parcel for selected profile_id based on payload type + """ + + parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] + return self.endpoint.delete(profile_id=profile_id, parcel_type=parcel_type_, parcel_id=parcel_id) + + class PolicyObjectFeatureProfileAPI: """ SDWAN Feature Profile Policy Object APIs diff --git a/catalystwan/endpoints/configuration_feature_profile.py b/catalystwan/endpoints/configuration_feature_profile.py index f5ce2edd2..5f78c9941 100644 --- a/catalystwan/endpoints/configuration_feature_profile.py +++ b/catalystwan/endpoints/configuration_feature_profile.py @@ -1,6 +1,7 @@ # mypy: disable-error-code="empty-body" from enum import Enum from typing import Optional +from uuid import UUID from pydantic import BaseModel, ConfigDict, Field @@ -102,6 +103,33 @@ def get_sdwan_feature_profiles(self) -> DataSequence[FeatureProfileInfo]: ... +class SystemConfigurationFeatureProfile(APIEndpoints): + @versions(supported_versions=(">=20.9"), raises=False) + @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") + def get_all(self, profile_id: UUID, parcel_type: str) -> None: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") + def get_by_id(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> _ParcelBase: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @put("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") + def update(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> ParcelId: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @delete("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") + def delete(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> None: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @post("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") + def create(self, profile_id: UUID, parcel_type: str, payload: _ParcelBase) -> ParcelId: + ... + + class SDRoutingConfigurationFeatureProfile(APIEndpoints): @versions(supported_versions=(">=20.13"), raises=False) @post("/v1/feature-profile/sd-routing/cli") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py new file mode 100644 index 000000000..203e2ad54 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -0,0 +1,29 @@ +from typing import List, Mapping, Union + +from pydantic import Field +from typing_extensions import Annotated + +from .aaa import AAAParcel + +AnySystemParcel = Annotated[Union[AAAParcel], Field(discriminator="type")] + +SYSTEM_PAYLOAD_ENDPOINT_MAPPING: Mapping[type, str] = { + AAAParcel: "aaa", + # BFDParcel: "bfd", + # LoggingParcel: "logging", + # BannerParcel: "banner", + # BasicParcel: "basic", + # GlobalParcel: "global", + # NTPParcel: "ntp", + # MRFParcel: "mrf", + # OMPParcel: "omp", + # SecurityParcel: "security", + # SNMPParcel: "snmp", +} + + +__all__ = ["AAAParcel", "AnySystemParcel", "SYSTEM_PAYLOAD_ENDPOINT_MAPPING"] + + +def __dir__() -> "List[str]": + return list(__all__) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py index 7a1f09bed..4fd8dc1f6 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py @@ -258,7 +258,7 @@ class AuthorizationRuleItem(BaseModel): ) -class AAA(_ParcelBase): +class AAAParcel(_ParcelBase): authentication_group: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "authenticationGroup"), From 23f11ad1242fd9a725074e5e083cff86ae840199 Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 22 Feb 2024 14:33:12 +0100 Subject: [PATCH 03/21] Add more endpoints --- catalystwan/api/feature_profile_api.py | 47 ++++++++++++++++--- .../feature_profile/sdwan/system.py | 42 +++++++++++++---- .../configuration_feature_profile.py | 28 ----------- 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index aea40008f..bbd3cf75b 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, Type, Union, overload +from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, overload from uuid import UUID +from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile from catalystwan.typed_list import DataSequence if TYPE_CHECKING: @@ -10,13 +11,12 @@ from catalystwan.api.parcel_api import SDRoutingFullConfigParcelAPI from catalystwan.endpoints.configuration.feature_profile.sdwan.policy_object import PolicyObjectFeatureProfile -from catalystwan.endpoints.configuration_feature_profile import ( - SDRoutingConfigurationFeatureProfile, - SystemConfigurationFeatureProfile, -) +from catalystwan.endpoints.configuration_feature_profile import SDRoutingConfigurationFeatureProfile from catalystwan.models.configuration.feature_profile.common import ( FeatureProfileCreationPayload, FeatureProfileCreationResponse, + FeatureProfileInfo, + GetFeatureProfilesPayload, Parcel, ParcelCreationResponse, ) @@ -117,7 +117,42 @@ class SystemFeatureProfileAPI: def __init__(self, session: ManagerSession): self.session = session - self.endpoint = SystemConfigurationFeatureProfile(session) + self.endpoint = SystemFeatureProfile(session) + + def get_profiles( + self, limit: Optional[int] = None, offset: Optional[int] = None + ) -> DataSequence[FeatureProfileInfo]: + """ + Get all System Feature Profiles + """ + payload = GetFeatureProfilesPayload(limit=limit if limit else None, offset=offset if offset else None) + + return self.endpoint.get_sdwan_system_feature_profiles(payload) + + def create_profile(self, name: str, description: str) -> FeatureProfileCreationResponse: + """ + Create System Feature Profile + """ + payload = FeatureProfileCreationPayload(name=name, description=description) + return self.endpoint.create_sdwan_system_feature_profile(payload=payload) + + def delete_profile(self, profile_id: UUID) -> None: + """ + Delete System Feature Profile + """ + self.endpoint.delete_sdwan_system_feature_profile(profile_id=profile_id) + + def get_schema( + self, + profile_id: UUID, + parcel_type: Type[AnySystemParcel], + ) -> DataSequence[Parcel[Any]]: + """ + Get all System Parcels for selected profile_id and selected type or get one Policy Object given parcel id + """ + + parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] + return self.endpoint.get_schema(profile_id=profile_id, parcel_type=parcel_type_) def get( self, diff --git a/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py b/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py index 3ca5c33f4..dea91e307 100644 --- a/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py +++ b/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py @@ -1,5 +1,6 @@ # mypy: disable-error-code="empty-body" from typing import Optional +from uuid import UUID from catalystwan.api.configuration_groups.parcel import _ParcelBase from catalystwan.endpoints import JSON, APIEndpoints, delete, get, post, put, versions @@ -16,8 +17,8 @@ class SystemFeatureProfile(APIEndpoints): @versions(supported_versions=(">=20.9"), raises=False) - @get("/v1/feature-profile/sdwan/system/aaa/schema", resp_json_key="request") - def get_sdwan_system_aaa_parcel_schema(self, params: SchemaTypeQuery) -> JSON: + @get("/v1/feature-profile/sdwan/system/{parcel_type}/schema", resp_json_key="request") + def get_schema(self, parcel_type: str, params: SchemaTypeQuery) -> JSON: ... @versions(supported_versions=(">=20.9"), raises=False) @@ -35,16 +36,41 @@ def create_sdwan_system_feature_profile( ... @versions(supported_versions=(">=20.9"), raises=False) - @delete("/v1/feature-profile/sdwan/system/{system_id}") - def delete_sdwan_system_feature_profile(self, system_id: str) -> None: + @delete("/v1/feature-profile/sdwan/system/{profile_id}") + def delete_sdwan_system_feature_profile(self, profile_id: UUID) -> None: ... @versions(supported_versions=(">=20.9"), raises=False) - @post("/v1/feature-profile/sdwan/system/{system_id}/aaa") - def create_aaa_profile_parcel_for_system(self, system_id: str, payload: _ParcelBase) -> ParcelId: + @post("/v1/feature-profile/sdwan/system/{profile_id}/aaa") + def create_aaa_profile_parcel_for_system(self, profile_id: UUID, payload: _ParcelBase) -> ParcelId: ... @versions(supported_versions=(">=20.9"), raises=False) - @put("/v1/feature-profile/sdwan/system/{system_id}/aaa/{parcel_id}") - def edit_aaa_profile_parcel_for_system(self, system_id: str, parcel_id: str, payload: _ParcelBase) -> ParcelId: + @put("/v1/feature-profile/sdwan/system/{profile_id}/aaa/{parcel_id}") + def edit_aaa_profile_parcel_for_system(self, profile_id: UUID, parcel_id: UUID, payload: _ParcelBase) -> ParcelId: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") + def get_all(self, profile_id: UUID, parcel_type: UUID) -> None: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") + def get_by_id(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> _ParcelBase: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @put("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") + def update(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> ParcelId: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @delete("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") + def delete(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> None: + ... + + @versions(supported_versions=(">=20.9"), raises=False) + @post("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") + def create(self, profile_id: UUID, parcel_type: str, payload: _ParcelBase) -> ParcelId: ... diff --git a/catalystwan/endpoints/configuration_feature_profile.py b/catalystwan/endpoints/configuration_feature_profile.py index 5f78c9941..f5ce2edd2 100644 --- a/catalystwan/endpoints/configuration_feature_profile.py +++ b/catalystwan/endpoints/configuration_feature_profile.py @@ -1,7 +1,6 @@ # mypy: disable-error-code="empty-body" from enum import Enum from typing import Optional -from uuid import UUID from pydantic import BaseModel, ConfigDict, Field @@ -103,33 +102,6 @@ def get_sdwan_feature_profiles(self) -> DataSequence[FeatureProfileInfo]: ... -class SystemConfigurationFeatureProfile(APIEndpoints): - @versions(supported_versions=(">=20.9"), raises=False) - @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") - def get_all(self, profile_id: UUID, parcel_type: str) -> None: - ... - - @versions(supported_versions=(">=20.9"), raises=False) - @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") - def get_by_id(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> _ParcelBase: - ... - - @versions(supported_versions=(">=20.9"), raises=False) - @put("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") - def update(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> ParcelId: - ... - - @versions(supported_versions=(">=20.9"), raises=False) - @delete("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") - def delete(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> None: - ... - - @versions(supported_versions=(">=20.9"), raises=False) - @post("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") - def create(self, profile_id: UUID, parcel_type: str, payload: _ParcelBase) -> ParcelId: - ... - - class SDRoutingConfigurationFeatureProfile(APIEndpoints): @versions(supported_versions=(">=20.13"), raises=False) @post("/v1/feature-profile/sd-routing/cli") From 2b6cdd7e27d653c6363fa9a3cf7402eac2f68b9f Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 22 Feb 2024 14:35:57 +0100 Subject: [PATCH 04/21] Add response --- catalystwan/api/feature_profile_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index bbd3cf75b..dfbab38a4 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -178,7 +178,7 @@ def create(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCreationRe parcel_type = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[type(payload)] return self.endpoint.create(profile_id=profile_id, parcel_type=parcel_type, payload=payload) - def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID): + def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> ParcelCreationResponse: """ Update System Parcel for selected profile_id based on payload type """ From a72fa3f3c8ab7e01b8179ff0919411ca63825bbf Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 22 Feb 2024 15:08:44 +0100 Subject: [PATCH 05/21] Move container, add overloads --- catalystwan/api/api_container.py | 3 +- catalystwan/api/feature_profile_api.py | 308 +++++++++++++++++- .../feature_profile/sdwan/system.py | 10 +- .../feature_profile/sdwan/system/__init__.py | 63 +++- .../feature_profile/sdwan/system/banner.py | 5 + .../feature_profile/sdwan/system/basic.py | 5 + .../feature_profile/sdwan/system/bfd.py | 5 + .../sdwan/system/global_parcel.py | 5 + .../sdwan/system/logging_parcel.py | 5 + .../feature_profile/sdwan/system/mrf.py | 5 + .../feature_profile/sdwan/system/ntp.py | 5 + .../feature_profile/sdwan/system/omp.py | 5 + .../feature_profile/sdwan/system/security.py | 5 + .../feature_profile/sdwan/system/snmp.py | 5 + 14 files changed, 416 insertions(+), 18 deletions(-) create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/banner.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/basic.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/omp.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/security.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py diff --git a/catalystwan/api/api_container.py b/catalystwan/api/api_container.py index fce5a21a0..cbd916710 100644 --- a/catalystwan/api/api_container.py +++ b/catalystwan/api/api_container.py @@ -16,7 +16,7 @@ from catalystwan.api.config_device_inventory_api import ConfigurationDeviceInventoryAPI from catalystwan.api.config_group_api import ConfigGroupAPI from catalystwan.api.dashboard_api import DashboardAPI -from catalystwan.api.feature_profile_api import SDRoutingFeatureProfilesAPI +from catalystwan.api.feature_profile_api import SDRoutingFeatureProfilesAPI, SDWANFeatureProfilesAPI from catalystwan.api.logs_api import LogsAPI from catalystwan.api.omp_api import OmpAPI from catalystwan.api.packet_capture_api import PacketCaptureAPI @@ -64,3 +64,4 @@ def __init__(self, session: ManagerSession): self.sessions = SessionsAPI(session) self.policy = PolicyAPI(session) self.sd_routing_feature_profiles = SDRoutingFeatureProfilesAPI(session) + self.sdwan_feature_profiles = SDWANFeatureProfilesAPI(session) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index dfbab38a4..408bc7b0b 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, overload from uuid import UUID +from pydantic import Json + from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile from catalystwan.typed_list import DataSequence @@ -50,14 +52,30 @@ ) from catalystwan.models.configuration.feature_profile.sdwan.system import ( SYSTEM_PAYLOAD_ENDPOINT_MAPPING, + AAAParcel, AnySystemParcel, + BannerParcel, + BasicParcel, + BFDParcel, + GlobalParcel, + LoggingParcel, + MRFParcel, + NTPParcel, + OMPParcel, + SecurityParcel, + SNMPParcel, ) class SDRoutingFeatureProfilesAPI: def __init__(self, session: ManagerSession): self.cli = SDRoutingCLIFeatureProfileAPI(session=session) + + +class SDWANFeatureProfilesAPI: + def __init__(self, session: ManagerSession): self.policy_object = PolicyObjectFeatureProfileAPI(session=session) + self.system = SystemFeatureProfileAPI(session=session) class FeatureProfileAPI(Protocol): @@ -146,7 +164,7 @@ def get_schema( self, profile_id: UUID, parcel_type: Type[AnySystemParcel], - ) -> DataSequence[Parcel[Any]]: + ) -> Json: """ Get all System Parcels for selected profile_id and selected type or get one Policy Object given parcel id """ @@ -154,6 +172,195 @@ def get_schema( parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] return self.endpoint.get_schema(profile_id=profile_id, parcel_type=parcel_type_) + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[AAAParcel], + ) -> DataSequence[Parcel[AAAParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[BFDParcel], + ) -> DataSequence[Parcel[BFDParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[LoggingParcel], + ) -> DataSequence[Parcel[LoggingParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[BannerParcel], + ) -> DataSequence[Parcel[BannerParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[BasicParcel], + ) -> DataSequence[Parcel[BasicParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[GlobalParcel], + ) -> DataSequence[Parcel[GlobalParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[NTPParcel], + ) -> DataSequence[Parcel[NTPParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[MRFParcel], + ) -> DataSequence[Parcel[MRFParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[OMPParcel], + ) -> DataSequence[Parcel[OMPParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[SecurityParcel], + ) -> DataSequence[Parcel[SecurityParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[SNMPParcel], + ) -> DataSequence[Parcel[SNMPParcel]]: + ... + + # get by id + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[AAAParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[AAAParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[BFDParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[BFDParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[LoggingParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[LoggingParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[BannerParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[BannerParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[BasicParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[BasicParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[GlobalParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[GlobalParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[NTPParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[NTPParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[MRFParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[MRFParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[OMPParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[OMPParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[SecurityParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[SecurityParcel]]: + ... + + @overload + def get( + self, + profile_id: UUID, + parcel_type: Type[SNMPParcel], + parcel_id: UUID, + ) -> DataSequence[Parcel[SNMPParcel]]: + ... + def get( self, profile_id: UUID, @@ -188,6 +395,105 @@ def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> profile_id=profile_id, parcel_type=policy_type, parcel_id=parcel_id, payload=payload ) + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[AAAParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[BFDParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[LoggingParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[BannerParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[BasicParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[GlobalParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[NTPParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[MRFParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[OMPParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[SecurityParcel], + parcel_id: UUID, + ) -> None: + ... + + @overload + def delete( + self, + profile_id: UUID, + parcel_type: Type[SNMPParcel], + parcel_id: UUID, + ) -> None: + ... + def delete(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id: UUID) -> None: """ Delete System Parcel for selected profile_id based on payload type diff --git a/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py b/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py index dea91e307..a24670268 100644 --- a/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py +++ b/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py @@ -9,9 +9,11 @@ FeatureProfileCreationResponse, FeatureProfileInfo, GetFeatureProfilesPayload, + Parcel, ParcelId, SchemaTypeQuery, ) +from catalystwan.models.configuration.feature_profile.sdwan.system import AnySystemParcel from catalystwan.typed_list import DataSequence @@ -52,17 +54,17 @@ def edit_aaa_profile_parcel_for_system(self, profile_id: UUID, parcel_id: UUID, @versions(supported_versions=(">=20.9"), raises=False) @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") - def get_all(self, profile_id: UUID, parcel_type: UUID) -> None: + def get_all(self, profile_id: UUID, parcel_type: UUID) -> DataSequence[Parcel]: ... @versions(supported_versions=(">=20.9"), raises=False) @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") - def get_by_id(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> _ParcelBase: + def get_by_id(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> Parcel: ... @versions(supported_versions=(">=20.9"), raises=False) @put("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}/{parcel_id}") - def update(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> ParcelId: + def update(self, profile_id: UUID, parcel_type: str, parcel_id: UUID, payload: AnySystemParcel) -> ParcelId: ... @versions(supported_versions=(">=20.9"), raises=False) @@ -72,5 +74,5 @@ def delete(self, profile_id: UUID, parcel_type: str, parcel_id: UUID) -> None: @versions(supported_versions=(">=20.9"), raises=False) @post("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") - def create(self, profile_id: UUID, parcel_type: str, payload: _ParcelBase) -> ParcelId: + def create(self, profile_id: UUID, parcel_type: str, payload: AnySystemParcel) -> ParcelId: ... diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index 203e2ad54..fb9bfdb11 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -4,25 +4,64 @@ from typing_extensions import Annotated from .aaa import AAAParcel +from .banner import BannerParcel +from .basic import BasicParcel +from .bfd import BFDParcel +from .global_parcel import GlobalParcel +from .logging_parcel import LoggingParcel +from .mrf import MRFParcel +from .ntp import NTPParcel +from .omp import OMPParcel +from .security import SecurityParcel +from .snmp import SNMPParcel -AnySystemParcel = Annotated[Union[AAAParcel], Field(discriminator="type")] +AnySystemParcel = Annotated[ + Union[ + AAAParcel, + BFDParcel, + LoggingParcel, + BannerParcel, + BasicParcel, + GlobalParcel, + NTPParcel, + MRFParcel, + OMPParcel, + SecurityParcel, + SNMPParcel, + ], + Field(discriminator="type"), +] SYSTEM_PAYLOAD_ENDPOINT_MAPPING: Mapping[type, str] = { AAAParcel: "aaa", - # BFDParcel: "bfd", - # LoggingParcel: "logging", - # BannerParcel: "banner", - # BasicParcel: "basic", - # GlobalParcel: "global", - # NTPParcel: "ntp", - # MRFParcel: "mrf", - # OMPParcel: "omp", - # SecurityParcel: "security", - # SNMPParcel: "snmp", + BFDParcel: "bfd", + LoggingParcel: "logging", + BannerParcel: "banner", + BasicParcel: "basic", + GlobalParcel: "global", + NTPParcel: "ntp", + MRFParcel: "mrf", + OMPParcel: "omp", + SecurityParcel: "security", + SNMPParcel: "snmp", } -__all__ = ["AAAParcel", "AnySystemParcel", "SYSTEM_PAYLOAD_ENDPOINT_MAPPING"] +__all__ = [ + "AAAParcel", + "BFDParcel", + "LoggingParcel", + "BannerParcel", + "BasicParcel", + "GlobalParcel", + "NTPParcel", + "MRFParcel", + "OMPParcel", + "SecurityParcel", + "SNMPParcel", + "AnySystemParcel", + "SYSTEM_PAYLOAD_ENDPOINT_MAPPING", +] def __dir__() -> "List[str]": diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py new file mode 100644 index 000000000..a17c2abd0 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class BannerParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py new file mode 100644 index 000000000..e5bf3c93d --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class BasicParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py new file mode 100644 index 000000000..9843eadb7 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class BFDParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py new file mode 100644 index 000000000..ead9ec94c --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class GlobalParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py new file mode 100644 index 000000000..2ef882422 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class LoggingParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py new file mode 100644 index 000000000..2b6a70184 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class MRFParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py new file mode 100644 index 000000000..c5fb4ec29 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class NTPParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py new file mode 100644 index 000000000..05cf9d460 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class OMPParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py new file mode 100644 index 000000000..1993fff1e --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class SecurityParcel(_ParcelBase): + pass diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py new file mode 100644 index 000000000..575ea6a86 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py @@ -0,0 +1,5 @@ +from catalystwan.api.configuration_groups.parcel import _ParcelBase + + +class SNMPParcel(_ParcelBase): + pass From 1f1e977d937264a271333c7c31cb5fa7ce4bb430 Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 22 Feb 2024 18:07:35 +0100 Subject: [PATCH 06/21] Create class method to get the Parcel type. Delete mapping by introducing type --- .../api/configuration_groups/parcel.py | 11 ++++- catalystwan/api/feature_profile_api.py | 46 ++++++++----------- .../feature_profile/sdwan/system/__init__.py | 18 +------- .../feature_profile/sdwan/system/aaa.py | 4 +- .../feature_profile/sdwan/system/banner.py | 5 +- .../feature_profile/sdwan/system/basic.py | 5 +- .../feature_profile/sdwan/system/bfd.py | 5 +- .../sdwan/system/global_parcel.py | 5 +- .../sdwan/system/logging_parcel.py | 6 ++- .../feature_profile/sdwan/system/mrf.py | 5 +- .../feature_profile/sdwan/system/ntp.py | 5 +- .../feature_profile/sdwan/system/omp.py | 5 +- .../feature_profile/sdwan/system/security.py | 5 +- .../feature_profile/sdwan/system/snmp.py | 4 +- 14 files changed, 72 insertions(+), 57 deletions(-) diff --git a/catalystwan/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index a48a2c6e6..15c0c64db 100644 --- a/catalystwan/api/configuration_groups/parcel.py +++ b/catalystwan/api/configuration_groups/parcel.py @@ -1,5 +1,6 @@ from enum import Enum from typing import Any, Dict, Generic, Literal, Optional, TypeVar, get_origin +from catalystwan.exceptions import CatalystwanException from pydantic import AliasPath, BaseModel, ConfigDict, Field, PrivateAttr, model_serializer @@ -40,8 +41,14 @@ def envelope_parcel_data(self, handler) -> Dict[str, Any]: for key in remove_keys: del model_dict[key] return model_dict - - + + @classmethod + def _get_parcel_type(cls) -> str: + field_info = cls.model_fields.get("type_") + if field_info is not None: + return str(field_info.default) + raise CatalystwanException("Field parcel type is not set.") + class OptionType(str, Enum): GLOBAL = "global" DEFAULT = "default" diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index 408bc7b0b..2dfd91773 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -51,7 +51,6 @@ URLBlockParcel, ) from catalystwan.models.configuration.feature_profile.sdwan.system import ( - SYSTEM_PAYLOAD_ENDPOINT_MAPPING, AAAParcel, AnySystemParcel, BannerParcel, @@ -169,8 +168,7 @@ def get_schema( Get all System Parcels for selected profile_id and selected type or get one Policy Object given parcel id """ - parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] - return self.endpoint.get_schema(profile_id=profile_id, parcel_type=parcel_type_) + return self.endpoint.get_schema(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type()) @overload def get( @@ -268,7 +266,7 @@ def get( profile_id: UUID, parcel_type: Type[AAAParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[AAAParcel]]: + ) -> Parcel[AAAParcel]: ... @overload @@ -277,7 +275,7 @@ def get( profile_id: UUID, parcel_type: Type[BFDParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[BFDParcel]]: + ) -> Parcel[BFDParcel]: ... @overload @@ -286,7 +284,7 @@ def get( profile_id: UUID, parcel_type: Type[LoggingParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[LoggingParcel]]: + ) -> Parcel[LoggingParcel]: ... @overload @@ -295,7 +293,7 @@ def get( profile_id: UUID, parcel_type: Type[BannerParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[BannerParcel]]: + ) -> Parcel[BannerParcel]: ... @overload @@ -304,7 +302,7 @@ def get( profile_id: UUID, parcel_type: Type[BasicParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[BasicParcel]]: + ) -> Parcel[BasicParcel]: ... @overload @@ -313,7 +311,7 @@ def get( profile_id: UUID, parcel_type: Type[GlobalParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[GlobalParcel]]: + ) -> Parcel[GlobalParcel]: ... @overload @@ -322,7 +320,7 @@ def get( profile_id: UUID, parcel_type: Type[NTPParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[NTPParcel]]: + ) -> Parcel[NTPParcel]: ... @overload @@ -331,7 +329,7 @@ def get( profile_id: UUID, parcel_type: Type[MRFParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[MRFParcel]]: + ) -> Parcel[MRFParcel]: ... @overload @@ -340,7 +338,7 @@ def get( profile_id: UUID, parcel_type: Type[OMPParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[OMPParcel]]: + ) -> Parcel[OMPParcel]: ... @overload @@ -349,7 +347,7 @@ def get( profile_id: UUID, parcel_type: Type[SecurityParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[SecurityParcel]]: + ) -> Parcel[SecurityParcel]: ... @overload @@ -358,7 +356,7 @@ def get( profile_id: UUID, parcel_type: Type[SNMPParcel], parcel_id: UUID, - ) -> DataSequence[Parcel[SNMPParcel]]: + ) -> Parcel[SNMPParcel]: ... def get( @@ -366,33 +364,29 @@ def get( profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id: Union[UUID, None] = None, - ) -> DataSequence[Parcel[Any]]: + ) -> Union[DataSequence[Parcel[AnySystemParcel]], Parcel[AnySystemParcel]]: """ - Get all System Parcels for selected profile_id and selected type or get one Policy Object given parcel id + Get all System Parcels for selected profile_id and selected type or get one System Parcel given parcel id """ - parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] if not parcel_id: - return self.endpoint.get_all(profile_id=profile_id, parcel_type=parcel_type_) - parcel = self.endpoint.get_by_id(profile_id=profile_id, parcel_type=parcel_type_, list_object_id=parcel_id) - return DataSequence(Parcel, [parcel]) + return self.endpoint.get_all(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type()) + return self.endpoint.get_by_id(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id) def create(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCreationResponse: """ Create System Parcel for selected profile_id based on payload type """ - parcel_type = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[type(payload)] - return self.endpoint.create(profile_id=profile_id, parcel_type=parcel_type, payload=payload) + return self.endpoint.create(profile_id=profile_id, parcel_type=payload._get_parcel_type(), payload=payload) def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> ParcelCreationResponse: """ Update System Parcel for selected profile_id based on payload type """ - policy_type = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[type(payload)] return self.endpoint.update( - profile_id=profile_id, parcel_type=policy_type, parcel_id=parcel_id, payload=payload + profile_id=profile_id, parcel_type=payload._get_parcel_type(), parcel_id=parcel_id, payload=payload ) @overload @@ -498,9 +492,7 @@ def delete(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id """ Delete System Parcel for selected profile_id based on payload type """ - - parcel_type_ = SYSTEM_PAYLOAD_ENDPOINT_MAPPING[parcel_type] - return self.endpoint.delete(profile_id=profile_id, parcel_type=parcel_type_, parcel_id=parcel_id) + return self.endpoint.delete(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id) class PolicyObjectFeatureProfileAPI: diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index fb9bfdb11..0518f4fee 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -29,24 +29,9 @@ SecurityParcel, SNMPParcel, ], - Field(discriminator="type"), + Field(discriminator="type_"), ] -SYSTEM_PAYLOAD_ENDPOINT_MAPPING: Mapping[type, str] = { - AAAParcel: "aaa", - BFDParcel: "bfd", - LoggingParcel: "logging", - BannerParcel: "banner", - BasicParcel: "basic", - GlobalParcel: "global", - NTPParcel: "ntp", - MRFParcel: "mrf", - OMPParcel: "omp", - SecurityParcel: "security", - SNMPParcel: "snmp", -} - - __all__ = [ "AAAParcel", "BFDParcel", @@ -60,7 +45,6 @@ "SecurityParcel", "SNMPParcel", "AnySystemParcel", - "SYSTEM_PAYLOAD_ENDPOINT_MAPPING", ] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py index 4fd8dc1f6..9961d1e05 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address, IPv6Address -from typing import List, Optional, Union +from typing import List, Optional, Union, Literal from pydantic import AliasPath, BaseModel, ConfigDict, Field @@ -259,6 +259,8 @@ class AuthorizationRuleItem(BaseModel): class AAAParcel(_ParcelBase): + type_: Literal["aaa"] = Field(default="aaa", exclude=True) + authentication_group: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "authenticationGroup"), diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py index a17c2abd0..909623396 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class BannerParcel(_ParcelBase): - pass + type_: Literal["banner"] = Field(default="banner", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py index e5bf3c93d..e1fc2a9de 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class BasicParcel(_ParcelBase): - pass + type_: Literal["basic"] = Field(default="basic", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py index 9843eadb7..5f8d41474 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class BFDParcel(_ParcelBase): - pass + type_: Literal["bfd"] = Field(default="bfd", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py index ead9ec94c..dbf4fcb34 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class GlobalParcel(_ParcelBase): - pass + type_: Literal["global"] = Field(default="global", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index 2ef882422..d0212bbb0 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -1,5 +1,9 @@ + +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class LoggingParcel(_ParcelBase): - pass + type_: Literal["logging"] = Field(default="logging", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py index 2b6a70184..4d8d42251 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class MRFParcel(_ParcelBase): - pass + type_: Literal["mrf"] = Field(default="mrf", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py index c5fb4ec29..c0dc1e5eb 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class NTPParcel(_ParcelBase): - pass + type_: Literal["ntp"] = Field(default="ntp", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py index 05cf9d460..cda005eb8 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class OMPParcel(_ParcelBase): - pass + type_: Literal["omp"] = Field(default="omp", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py index 1993fff1e..8efb6b567 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py @@ -1,5 +1,8 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class SecurityParcel(_ParcelBase): - pass + type_: Literal["security"] = Field(default="security", exclude=True) + diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py index 575ea6a86..a63439aca 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py @@ -1,5 +1,7 @@ +from typing import Literal from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import Field class SNMPParcel(_ParcelBase): - pass + type_: Literal["snmp"] = Field(default="snmp", exclude=True) From 57cdd421053d78d975418ca607690d7c2e191d77 Mon Sep 17 00:00:00 2001 From: Kuba Date: Thu, 22 Feb 2024 18:40:18 +0100 Subject: [PATCH 07/21] Fix --- .../api/configuration_groups/parcel.py | 8 +++-- catalystwan/api/feature_profile_api.py | 32 +++++++++++-------- .../feature_profile/sdwan/system/__init__.py | 2 +- .../feature_profile/sdwan/system/aaa.py | 2 +- .../feature_profile/sdwan/system/banner.py | 5 +-- .../feature_profile/sdwan/system/basic.py | 5 +-- .../feature_profile/sdwan/system/bfd.py | 5 +-- .../sdwan/system/global_parcel.py | 5 +-- .../sdwan/system/logging_parcel.py | 6 ++-- .../feature_profile/sdwan/system/mrf.py | 5 +-- .../feature_profile/sdwan/system/ntp.py | 5 +-- .../feature_profile/sdwan/system/omp.py | 5 +-- .../feature_profile/sdwan/system/security.py | 5 +-- .../feature_profile/sdwan/system/snmp.py | 4 ++- 14 files changed, 55 insertions(+), 39 deletions(-) diff --git a/catalystwan/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index 15c0c64db..b0097b1be 100644 --- a/catalystwan/api/configuration_groups/parcel.py +++ b/catalystwan/api/configuration_groups/parcel.py @@ -1,9 +1,10 @@ from enum import Enum from typing import Any, Dict, Generic, Literal, Optional, TypeVar, get_origin -from catalystwan.exceptions import CatalystwanException from pydantic import AliasPath, BaseModel, ConfigDict, Field, PrivateAttr, model_serializer +from catalystwan.exceptions import CatalystwanException + T = TypeVar("T") @@ -41,14 +42,15 @@ def envelope_parcel_data(self, handler) -> Dict[str, Any]: for key in remove_keys: del model_dict[key] return model_dict - + @classmethod def _get_parcel_type(cls) -> str: field_info = cls.model_fields.get("type_") if field_info is not None: return str(field_info.default) raise CatalystwanException("Field parcel type is not set.") - + + class OptionType(str, Enum): GLOBAL = "global" DEFAULT = "default" diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index 2dfd91773..4dba1de5a 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -266,7 +266,7 @@ def get( profile_id: UUID, parcel_type: Type[AAAParcel], parcel_id: UUID, - ) -> Parcel[AAAParcel]: + ) -> DataSequence[Parcel[AAAParcel]]: ... @overload @@ -275,7 +275,7 @@ def get( profile_id: UUID, parcel_type: Type[BFDParcel], parcel_id: UUID, - ) -> Parcel[BFDParcel]: + ) -> DataSequence[Parcel[BFDParcel]]: ... @overload @@ -284,7 +284,7 @@ def get( profile_id: UUID, parcel_type: Type[LoggingParcel], parcel_id: UUID, - ) -> Parcel[LoggingParcel]: + ) -> DataSequence[Parcel[LoggingParcel]]: ... @overload @@ -293,7 +293,7 @@ def get( profile_id: UUID, parcel_type: Type[BannerParcel], parcel_id: UUID, - ) -> Parcel[BannerParcel]: + ) -> DataSequence[Parcel[BannerParcel]]: ... @overload @@ -302,7 +302,7 @@ def get( profile_id: UUID, parcel_type: Type[BasicParcel], parcel_id: UUID, - ) -> Parcel[BasicParcel]: + ) -> DataSequence[Parcel[BasicParcel]]: ... @overload @@ -311,7 +311,7 @@ def get( profile_id: UUID, parcel_type: Type[GlobalParcel], parcel_id: UUID, - ) -> Parcel[GlobalParcel]: + ) -> DataSequence[Parcel[GlobalParcel]]: ... @overload @@ -320,7 +320,7 @@ def get( profile_id: UUID, parcel_type: Type[NTPParcel], parcel_id: UUID, - ) -> Parcel[NTPParcel]: + ) -> DataSequence[Parcel[NTPParcel]]: ... @overload @@ -329,7 +329,7 @@ def get( profile_id: UUID, parcel_type: Type[MRFParcel], parcel_id: UUID, - ) -> Parcel[MRFParcel]: + ) -> DataSequence[Parcel[MRFParcel]]: ... @overload @@ -338,7 +338,7 @@ def get( profile_id: UUID, parcel_type: Type[OMPParcel], parcel_id: UUID, - ) -> Parcel[OMPParcel]: + ) -> DataSequence[Parcel[OMPParcel]]: ... @overload @@ -347,7 +347,7 @@ def get( profile_id: UUID, parcel_type: Type[SecurityParcel], parcel_id: UUID, - ) -> Parcel[SecurityParcel]: + ) -> DataSequence[Parcel[SecurityParcel]]: ... @overload @@ -356,7 +356,7 @@ def get( profile_id: UUID, parcel_type: Type[SNMPParcel], parcel_id: UUID, - ) -> Parcel[SNMPParcel]: + ) -> DataSequence[Parcel[SNMPParcel]]: ... def get( @@ -364,14 +364,16 @@ def get( profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id: Union[UUID, None] = None, - ) -> Union[DataSequence[Parcel[AnySystemParcel]], Parcel[AnySystemParcel]]: + ) -> DataSequence[Parcel[Any]]: """ Get all System Parcels for selected profile_id and selected type or get one System Parcel given parcel id """ if not parcel_id: return self.endpoint.get_all(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type()) - return self.endpoint.get_by_id(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id) + return self.endpoint.get_by_id( + profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id + ) def create(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCreationResponse: """ @@ -492,7 +494,9 @@ def delete(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id """ Delete System Parcel for selected profile_id based on payload type """ - return self.endpoint.delete(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id) + return self.endpoint.delete( + profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id + ) class PolicyObjectFeatureProfileAPI: diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index 0518f4fee..b4edc5b3d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Mapping, Union +from typing import List, Union from pydantic import Field from typing_extensions import Annotated diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py index 9961d1e05..09bd5a0b2 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address, IPv6Address -from typing import List, Optional, Union, Literal +from typing import List, Literal, Optional, Union from pydantic import AliasPath, BaseModel, ConfigDict, Field diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py index 909623396..7e51b157d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class BannerParcel(_ParcelBase): type_: Literal["banner"] = Field(default="banner", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py index e1fc2a9de..963535dc3 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class BasicParcel(_ParcelBase): type_: Literal["basic"] = Field(default="basic", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py index 5f8d41474..3ee52e751 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class BFDParcel(_ParcelBase): type_: Literal["bfd"] = Field(default="bfd", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py index dbf4fcb34..ac977e70f 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class GlobalParcel(_ParcelBase): type_: Literal["global"] = Field(default="global", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index d0212bbb0..291cb22b8 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -1,9 +1,9 @@ - from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class LoggingParcel(_ParcelBase): type_: Literal["logging"] = Field(default="logging", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py index 4d8d42251..a24390530 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class MRFParcel(_ParcelBase): type_: Literal["mrf"] = Field(default="mrf", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py index c0dc1e5eb..466a7b939 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class NTPParcel(_ParcelBase): type_: Literal["ntp"] = Field(default="ntp", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py index cda005eb8..59c99e553 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class OMPParcel(_ParcelBase): type_: Literal["omp"] = Field(default="omp", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py index 8efb6b567..bbd11aeda 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py @@ -1,8 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class SecurityParcel(_ParcelBase): type_: Literal["security"] = Field(default="security", exclude=True) - diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py index a63439aca..34967879b 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py @@ -1,7 +1,9 @@ from typing import Literal -from catalystwan.api.configuration_groups.parcel import _ParcelBase + from pydantic import Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase + class SNMPParcel(_ParcelBase): type_: Literal["snmp"] = Field(default="snmp", exclude=True) From e79a479731e0d806bee9f50df67bad503608cc5d Mon Sep 17 00:00:00 2001 From: Szymon Basan <116343782+sbasan@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:44:46 +0100 Subject: [PATCH 08/21] fix config_migration (#490) * fix ux-2 schema breaking models, fix transform * remove URL(Allow/Block)Parcel from overloaded methods --- .../api/configuration_groups/parcel.py | 11 ++++- catalystwan/api/feature_profile_api.py | 47 +++++++++---------- .../sdwan/policy_object/__init__.py | 32 +------------ catalystwan/models/policy/lists_entries.py | 15 +++--- pyproject.toml | 2 +- 5 files changed, 44 insertions(+), 63 deletions(-) diff --git a/catalystwan/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index 4490f4bdb..28aea22cf 100644 --- a/catalystwan/api/configuration_groups/parcel.py +++ b/catalystwan/api/configuration_groups/parcel.py @@ -11,12 +11,14 @@ model_serializer, ) +from catalystwan.exceptions import CatalystwanException + T = TypeVar("T") class _ParcelBase(BaseModel): model_config = ConfigDict( - extra="allow", arbitrary_types_allowed=True, populate_by_name=True, # json_schema_mode_override="validation" + extra="allow", arbitrary_types_allowed=True, populate_by_name=True, json_schema_mode_override="validation" ) parcel_name: str = Field( min_length=1, @@ -33,6 +35,13 @@ class _ParcelBase(BaseModel): ) _parcel_data_key: str = PrivateAttr(default="data") + @classmethod + def _get_parcel_type(cls) -> str: + field_info = cls.model_fields.get("type_") + if field_info is not None: + return str(field_info.default) + raise CatalystwanException("Cannot obtain parcel type string") + @model_serializer(mode="wrap") def envelope_parcel_data(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]: """ diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index abdf84871..045e69713 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -18,7 +18,6 @@ ParcelCreationResponse, ) from catalystwan.models.configuration.feature_profile.sdwan.policy_object import ( - POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING, AnyPolicyObjectParcel, ApplicationListParcel, AppProbeParcel, @@ -42,8 +41,6 @@ SecurityZoneListParcel, StandardCommunityParcel, TlocParcel, - URLAllowParcel, - URLBlockParcel, ) @@ -200,13 +197,13 @@ def get(self, profile_id: UUID, parcel_type: Type[StandardCommunityParcel]) -> D def get(self, profile_id: UUID, parcel_type: Type[TlocParcel]) -> DataSequence[Parcel[Any]]: ... - @overload - def get(self, profile_id: UUID, parcel_type: Type[URLAllowParcel]) -> DataSequence[Parcel[Any]]: - ... + # @overload + # def get(self, profile_id: UUID, parcel_type: Type[URLAllowParcel]) -> DataSequence[Parcel[Any]]: + # ... - @overload - def get(self, profile_id: UUID, parcel_type: Type[URLBlockParcel]) -> DataSequence[Parcel[Any]]: - ... + # @overload + # def get(self, profile_id: UUID, parcel_type: Type[URLBlockParcel]) -> DataSequence[Parcel[Any]]: + # ... # get by id @@ -326,13 +323,13 @@ def get( def get(self, profile_id: UUID, parcel_type: Type[TlocParcel], parcel_id: UUID) -> DataSequence[Parcel[Any]]: ... - @overload - def get(self, profile_id: UUID, parcel_type: Type[URLAllowParcel], parcel_id: UUID) -> DataSequence[Parcel[Any]]: - ... + # @overload + # def get(self, profile_id: UUID, parcel_type: Type[URLAllowParcel], parcel_id: UUID) -> DataSequence[Parcel[Any]]: + # ... - @overload - def get(self, profile_id: UUID, parcel_type: Type[URLBlockParcel], parcel_id: UUID) -> DataSequence[Parcel[Any]]: - ... + # @overload + # def get(self, profile_id: UUID, parcel_type: Type[URLBlockParcel], parcel_id: UUID) -> DataSequence[Parcel[Any]]: + # ... def get( self, @@ -344,7 +341,7 @@ def get( Get all Policy Objects for selected profile_id and selected type or get one Policy Object given parcel id """ - policy_object_list_type = POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING[parcel_type] + policy_object_list_type = parcel_type._get_parcel_type() if not parcel_id: return self.endpoint.get_all(profile_id=profile_id, policy_object_list_type=policy_object_list_type) parcel = self.endpoint.get_by_id( @@ -357,7 +354,7 @@ def create(self, profile_id: UUID, payload: AnyPolicyObjectParcel) -> ParcelCrea Create Policy Object for selected profile_id based on payload type """ - policy_object_list_type = POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING[type(payload)] + policy_object_list_type = payload._get_parcel_type() return self.endpoint.create( profile_id=profile_id, policy_object_list_type=policy_object_list_type, payload=payload ) @@ -367,7 +364,7 @@ def update(self, profile_id: UUID, payload: AnyPolicyObjectParcel, list_object_i Update Policy Object for selected profile_id based on payload type """ - policy_type = POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING[type(payload)] + policy_type = payload._get_parcel_type() return self.endpoint.update( profile_id=profile_id, policy_object_list_type=policy_type, list_object_id=list_object_id, payload=payload ) @@ -460,20 +457,20 @@ def delete(self, profile_id: UUID, parcel_type: Type[StandardCommunityParcel], l def delete(self, profile_id: UUID, parcel_type: Type[TlocParcel], list_object_id: UUID) -> None: ... - @overload - def delete(self, profile_id: UUID, parcel_type: Type[URLAllowParcel], list_object_id: UUID) -> None: - ... + # @overload + # def delete(self, profile_id: UUID, parcel_type: Type[URLAllowParcel], list_object_id: UUID) -> None: + # ... - @overload - def delete(self, profile_id: UUID, parcel_type: Type[URLBlockParcel], list_object_id: UUID) -> None: - ... + # @overload + # def delete(self, profile_id: UUID, parcel_type: Type[URLBlockParcel], list_object_id: UUID) -> None: + # ... def delete(self, profile_id: UUID, parcel_type: Type[AnyPolicyObjectParcel], list_object_id: UUID) -> None: """ Delete Policy Object for selected profile_id based on payload type """ - policy_object_list_type = POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING[parcel_type] + policy_object_list_type = parcel_type._get_parcel_type() return self.endpoint.delete( profile_id=profile_id, policy_object_list_type=policy_object_list_type, list_object_id=list_object_id ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/policy_object/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/policy_object/__init__.py index 8a07d1fb7..cdb853742 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/policy_object/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/policy_object/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Mapping, Union +from typing import List, Union from pydantic import Field from typing_extensions import Annotated @@ -42,7 +42,7 @@ AnyPolicyObjectParcel = Annotated[ Union[ - AnyURLParcel, + # AnyURLParcel, ApplicationListParcel, AppProbeParcel, ColorParcel, @@ -70,34 +70,6 @@ Field(discriminator="type_"), ] -POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING: Mapping[type, str] = { - AppProbeParcel: "app-probe", - ApplicationListParcel: "app-list", - ColorParcel: "color", - DataPrefixParcel: "data-prefix", - ExpandedCommunityParcel: "expanded-community", - FowardingClassParcel: "class", - IPv6DataPrefixParcel: "data-ipv6-prefix", - IPv6PrefixListParcel: "ipv6-prefix", - PrefixListParcel: "prefix", - PolicierParcel: "policer", - PreferredColorGroupParcel: "preferred-color-group", - SLAClassParcel: "sla-class", - TlocParcel: "tloc", - StandardCommunityParcel: "standard-community", - LocalDomainParcel: "security-localdomain", - FQDNDomainParcel: "security-fqdn", - IPSSignatureParcel: "security-ipssignature", - URLAllowParcel: "security-urllist", - URLBlockParcel: "security-urllist", - SecurityPortParcel: "security-port", - ProtocolListParcel: "security-protocolname", - GeoLocationListParcel: "security-geolocation", - SecurityZoneListParcel: "security-zone", - SecurityApplicationListParcel: "security-localapp", - SecurityDataPrefixParcel: "security-data-ip-prefix", -} - __all__ = ( "AnyPolicyObjectParcel", "ApplicationFamilyListEntry", diff --git a/catalystwan/models/policy/lists_entries.py b/catalystwan/models/policy/lists_entries.py index 823805338..747d77a54 100644 --- a/catalystwan/models/policy/lists_entries.py +++ b/catalystwan/models/policy/lists_entries.py @@ -7,18 +7,21 @@ from catalystwan.models.common import InterfaceType, TLOCColor, check_fields_exclusive -def check_jitter_ms(jitter_str: str) -> str: - assert 1 <= int(jitter_str) <= 1000 +def check_jitter_ms(jitter_str: Optional[str]) -> Optional[str]: + if jitter_str is not None: + assert 1 <= int(jitter_str) <= 1000 return jitter_str -def check_latency_ms(latency_str: str) -> str: - assert 1 <= int(latency_str) <= 1000 +def check_latency_ms(latency_str: Optional[str]) -> Optional[str]: + if latency_str is not None: + assert 1 <= int(latency_str) <= 1000 return latency_str -def check_loss_percent(loss_str: str) -> str: - assert 0 <= int(loss_str) <= 100 +def check_loss_percent(loss_str: Optional[str]) -> Optional[str]: + if loss_str is not None: + assert 0 <= int(loss_str) <= 100 return loss_str diff --git a/pyproject.toml b/pyproject.toml index 0ab064150..e947c3a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "catalystwan" -version = "0.31.0dev2" +version = "0.31.0dev3" description = "Cisco Catalyst WAN SDK for Python" authors = ["kagorski "] readme = "README.md" From 1ecfc0da13b49342e0f2282856bbd8d71b871780 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski <95274389+jpkrajewski@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:56:10 +0100 Subject: [PATCH 09/21] Push UX2 - Create config group and feature profiles (#492) * Push UX2 - Create config group and feature profiles * Simplify push_ux2_config * UX2Config rework * Refactor code for template definition normalization This commit refactors the code responsible for normalizing template definitions by implementing improved type annotations, better function organization, and enhanced error handling. Key changes include: 1. Refactoring the to_snake_case function to use the str.replace method for kebab-case to snake_case conversion. 2. Improving the cast_value_to_global function to handle different types of input values, including lists and IP addresses, and utilizing IPv4Address and IPv6Address from the ipaddress module. 3. Enhancing the transform_dict function to handle nested dictionaries and lists, casting leaf values to global types recursively. 4. Updating the template_definition_normalization function to utilize the refactored transform_dict function for key transformation and value normalization. These changes improve code readability, maintainability, and robustness, providing a more efficient and reliable solution for template definition normalization. * Add tests for normalization and Literal casting --------- Co-authored-by: Kuba Co-authored-by: sbasan Co-authored-by: Jakub Krajewski --- catalystwan/api/config_group_api.py | 16 ++- catalystwan/endpoints/configuration_group.py | 19 ++- .../models/configuration/config_migration.py | 26 ++-- .../converters/feature_template/logging.py | 27 ---- .../feature_template/normalizator.py | 80 ------------ .../feature_profile/sdwan/system/__init__.py | 21 ++- .../feature_profile/sdwan/system/aaa.py | 38 ++---- .../feature_profile/sdwan/system/bfd.py | 7 +- .../feature_profile/sdwan/system/literals.py | 7 + .../feature_profile/sdwan/system/logging.py | 52 -------- .../sdwan/system/logging_parcel.py | 82 ++++++++++++ .../test_converter_chooser.py | 26 ++++ .../tests/config_migration/test_normalizer.py | 98 ++++++++++++++ .../converters/feature_template/__init__.py | 10 ++ .../converters/feature_template/aaa.py | 6 +- .../converters/feature_template/base.py | 0 .../converters/feature_template/bfd.py | 6 +- .../feature_template/factory_method.py} | 32 +++-- .../converters/feature_template/logging_.py | 25 ++++ .../converters/feature_template/normalizer.py | 67 ++++++++++ .../config_migration}/converters/recast.py | 0 .../config_migration/creators/config_group.py | 123 ++++++++++++++++++ catalystwan/workflows/config_migration.py | 39 +++++- 23 files changed, 567 insertions(+), 240 deletions(-) delete mode 100644 catalystwan/models/configuration/feature_profile/converters/feature_template/logging.py delete mode 100644 catalystwan/models/configuration/feature_profile/converters/feature_template/normalizator.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/literals.py delete mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/logging.py create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py create mode 100644 catalystwan/tests/config_migration/test_converter_chooser.py create mode 100644 catalystwan/tests/config_migration/test_normalizer.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/__init__.py rename catalystwan/{models/configuration/feature_profile => utils/config_migration}/converters/feature_template/aaa.py (90%) rename catalystwan/{models/configuration/feature_profile => utils/config_migration}/converters/feature_template/base.py (100%) rename catalystwan/{models/configuration/feature_profile => utils/config_migration}/converters/feature_template/bfd.py (87%) rename catalystwan/{models/configuration/feature_profile/converters/feature_template/__init__.py => utils/config_migration/converters/feature_template/factory_method.py} (60%) create mode 100644 catalystwan/utils/config_migration/converters/feature_template/logging_.py create mode 100644 catalystwan/utils/config_migration/converters/feature_template/normalizer.py rename catalystwan/{models/configuration/feature_profile => utils/config_migration}/converters/recast.py (100%) create mode 100644 catalystwan/utils/config_migration/creators/config_group.py diff --git a/catalystwan/api/config_group_api.py b/catalystwan/api/config_group_api.py index 1d7555bfe..80aaa4345 100644 --- a/catalystwan/api/config_group_api.py +++ b/catalystwan/api/config_group_api.py @@ -1,11 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Union +from uuid import UUID + +from catalystwan.typed_list import DataSequence if TYPE_CHECKING: from catalystwan.session import ManagerSession from catalystwan.endpoints.configuration_group import ( + ConfigGroup, ConfigGroupAssociatePayload, ConfigGroupCreationPayload, ConfigGroupCreationResponse, @@ -14,7 +18,6 @@ ConfigGroupDisassociateResponse, ConfigGroupEditPayload, ConfigGroupEditResponse, - ConfigGroupResponsePayload, ConfigGroupVariablesCreatePayload, ConfigGroupVariablesCreateResponse, ConfigGroupVariablesEditPayload, @@ -108,11 +111,14 @@ def edit( return self.endpoint.edit_config_group(config_group_id=cg_id, payload=payload) - def get(self) -> ConfigGroupResponsePayload: + def get(self, group_id: Optional[UUID] = None) -> Union[DataSequence[ConfigGroup], ConfigGroup, None]: """ - Gets list of existing config-groups + Gets list of existing config-groups or single config-group with given ID + If given ID is not correct return None """ - return self.endpoint.get() + if group_id is None: + return self.endpoint.get() + return self.endpoint.get().filter(id=group_id).single_or_default() def update_variables(self, cg_id: str, solution: Solution, device_variables: list) -> None: """ diff --git a/catalystwan/endpoints/configuration_group.py b/catalystwan/endpoints/configuration_group.py index 75412b16c..f792a3923 100644 --- a/catalystwan/endpoints/configuration_group.py +++ b/catalystwan/endpoints/configuration_group.py @@ -1,6 +1,7 @@ # mypy: disable-error-code="empty-body" from datetime import datetime from typing import List, Optional +from uuid import UUID from pydantic import BaseModel, Field @@ -11,7 +12,7 @@ class ProfileId(BaseModel): - id: str + id: UUID # TODO Get mode from schema @@ -35,10 +36,24 @@ class FeatureProfile(BaseModel): class ConfigGroup(BaseModel): + id: UUID name: str description: Optional[str] solution: Solution profiles: Optional[List[FeatureProfile]] + source: Optional[str] = None + state: Optional[str] = None + devices: List = Field(default=[]) + created_by: Optional[str] = Field(alias="createdBy") + last_updated_by: Optional[str] = Field(alias="lastUpdatedBy") + created_on: Optional[datetime] = Field(alias="createdOn") + last_updated_on: Optional[datetime] = Field(alias="lastUpdatedOn") + version: int + number_of_devices: int = Field(alias="numberOfDevices") + number_of_devices_up_to_date: int = Field(alias="numberOfDevicesUpToDate") + origin: Optional[str] + topology: Optional[str] = None + full_config_cli: bool = Field(alias="fullConfigCli") class ConfigGroupResponsePayload(BaseModel): @@ -104,7 +119,7 @@ class ConfigGroupDisassociateResponse(BaseModel): class ConfigGroupCreationResponse(BaseModel): - id: str + id: UUID class EditedProfileId(BaseModel): diff --git a/catalystwan/models/configuration/config_migration.py b/catalystwan/models/configuration/config_migration.py index 36cf8673f..39d63ef15 100644 --- a/catalystwan/models/configuration/config_migration.py +++ b/catalystwan/models/configuration/config_migration.py @@ -1,11 +1,9 @@ -from typing import List, Union +from typing import List, Literal, Union from pydantic import BaseModel, ConfigDict, Field from typing_extensions import Annotated from catalystwan.api.template_api import DeviceTemplateInformation, FeatureTemplateInformation -from catalystwan.endpoints.configuration_group import ConfigGroup -from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload from catalystwan.models.configuration.feature_profile.sdwan.policy_object import AnyPolicyObjectParcel from catalystwan.models.configuration.feature_profile.sdwan.system import AnySystemParcel from catalystwan.models.policy import ( @@ -49,6 +47,14 @@ class UX1Templates(BaseModel): devices: List[DeviceTemplateInformation] = Field(default=[]) +class ConfigGroupPreset(BaseModel): + config_group_name: str = Field(serialization_alias="name", validation_alias="name") + solution: Literal["sdwan"] = "sdwan" + profile_parcels: List[AnyParcel] = Field( + default=[], serialization_alias="profileParcels", validation_alias="profileParcels" + ) + + class UX1Config(BaseModel): # All UX1 Configuration items - Mega Model model_config = ConfigDict(populate_by_name=True) @@ -59,15 +65,7 @@ class UX1Config(BaseModel): class UX2Config(BaseModel): # All UX2 Configuration items - Mega Model model_config = ConfigDict(populate_by_name=True) - config_groups: List[ConfigGroup] = Field( - default=[], serialization_alias="configurationGroups", validation_alias="configurationGroups" - ) - policy_groups: List[ConfigGroup] = Field( - default=[], serialization_alias="policyGroups", validation_alias="policyGroups" - ) - feature_profiles: List[FeatureProfileCreationPayload] = Field( - default=[], serialization_alias="featureProfiles", validation_alias="featureProfiles" - ) - profile_parcels: List[AnyParcel] = Field( - default=[], serialization_alias="profileParcels", validation_alias="profileParcels" + # TODO: config group name + config_group_presets: List[ConfigGroupPreset] = Field( + default=[], serialization_alias="configGroupPresets", validation_alias="configGroupPresets" ) diff --git a/catalystwan/models/configuration/feature_profile/converters/feature_template/logging.py b/catalystwan/models/configuration/feature_profile/converters/feature_template/logging.py deleted file mode 100644 index 4eaf95ffc..000000000 --- a/catalystwan/models/configuration/feature_profile/converters/feature_template/logging.py +++ /dev/null @@ -1,27 +0,0 @@ -# from catalystwan.models.configuration.feature_profile.sdwan.system import Logging - -# class LoggingTemplateConverter: - -# @staticmethod -# def create_parcel(name, description, template_values: dict): -# """ -# Creates an Logging object based on the provided template values. - -# Returns: -# Logging: An Logging object with the provided template values. -# """ -# template_values["name"] = name -# template_values["description"] = description - -# template_values["disk"] = { -# "disk_enable": template_values["enable"], -# "file": { -# "disk_file_size": template_values["size"], -# "disk_file_rotate": template_values["rotate"] -# } -# } -# del template_values["enable"] -# del template_values["size"] -# del template_values["rotate"] - -# return Logging(**template_values) diff --git a/catalystwan/models/configuration/feature_profile/converters/feature_template/normalizator.py b/catalystwan/models/configuration/feature_profile/converters/feature_template/normalizator.py deleted file mode 100644 index 21e3bf734..000000000 --- a/catalystwan/models/configuration/feature_profile/converters/feature_template/normalizator.py +++ /dev/null @@ -1,80 +0,0 @@ -import json -from typing import cast - -from catalystwan.api.configuration_groups.parcel import as_global -from catalystwan.utils.feature_template import find_template_values - - -def template_definition_normalization(template_definition): - """ - Normalizes a template definition by changing keys to snake_case and casting all leafs values to global types. - - Args: - template_definition (str): The template definition in JSON format. - - Returns: - dict: The normalized template values. - - """ - - def to_snake_case(s: str): - """ - Converts a string from kebab-case to snake_case. - - Args: - s (str): The string to be converted. - - Returns: - str: The converted string. - - """ - if "-" in s: - temp = s.split("-") - return "_".join(ele for ele in temp) - return s - - def transform_dict(d): - """ - Transforms a nested dictionary into a normalized form. - - Args: - d (dict): The nested dictionary to be transformed. - - Returns: - dict: The transformed dictionary. - - """ - if isinstance(d, list): - return [transform_dict(i) if isinstance(i, (dict, list)) else i for i in d] - return {to_snake_case(a): transform_dict(b) if isinstance(b, (dict, list)) else b for a, b in d.items()} - - def cast_leafs_to_global(node: dict): - """ - Recursively casts all leaf values in a nested dictionary or list to the global configuration type. - - Args: - node (dict): The nested dictionary or list to be processed. - - Returns: - None - - """ - for key, item in node.items(): - if isinstance(item, dict): - cast_leafs_to_global(item) - elif isinstance(item, list): - for i in item: - if isinstance(i, dict): - cast_leafs_to_global(i) - else: - node[key] = as_global(item) - - template_definition_as_dict = json.loads(cast(str, template_definition)) - - template_values = find_template_values(template_definition_as_dict) - - template_values = transform_dict(template_values) - - cast_leafs_to_global(template_values) - - return template_values diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index f06f24ee0..92ee265dc 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -1,16 +1,25 @@ from typing import List, Mapping, Union -from .aaa import AAA -from .bfd import BFD +from .aaa import AAAParcel +from .bfd import BFDParcel +from .literals import SYSTEM_LITERALS +from .logging_parcel import LoggingParcel SYSTEM_PAYLOAD_ENDPOINT_MAPPING: Mapping[type, str] = { - AAA: "aaa", - BFD: "bfd", + AAAParcel: "aaa", + BFDParcel: "bfd", } -AnySystemParcel = Union[AAA, BFD] +AnySystemParcel = Union[AAAParcel, BFDParcel, LoggingParcel] -__all__ = ["AAA", "BFD", "AnySystemParcel", "SYSTEM_PAYLOAD_ENDPOINT_MAPPING"] +__all__ = [ + "AAAParcel", + "BFDParcel", + "LoggingParcel", + "AnySystemParcel", + "SYSTEM_LITERALS", + "SYSTEM_PAYLOAD_ENDPOINT_MAPPING", +] def __dir__() -> "List[str]": diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py index 21b4afdc2..54929929f 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py @@ -4,12 +4,6 @@ from pydantic import AliasPath, BaseModel, ConfigDict, Field from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default, as_global -from catalystwan.models.configuration.feature_profile.converters.recast import ( - DefaultGlobalBool, - DefaultGlobalIPAddress, - DefaultGlobalList, - DefaultGlobalStr, -) class PubkeyChainItem(BaseModel): @@ -67,9 +61,7 @@ def add_pubkey_chain_item(self, key: str) -> PubkeyChainItem: class RadiusServerItem(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) - address: Union[DefaultGlobalIPAddress, Global[IPv4Address], Global[IPv6Address]] = Field( - description="Set IP address of Radius server" - ) + address: Union[Global[IPv4Address], Global[IPv6Address]] = Field(description="Set IP address of Radius server") auth_port: Union[Global[int], Default[int], Variable, None] = Field( default=as_default(1812), validation_alias="authPort", @@ -101,7 +93,7 @@ class RadiusServerItem(BaseModel): description="Set the TACACS server shared type 7 encrypted key", ) # Literal["6", "7"] - key_enum: Union[DefaultGlobalStr, Global[str], Default[None], None] = Field( + key_enum: Union[Global[str], Default[None], None] = Field( default=None, validation_alias="keyEnum", serialization_alias="keyEnum", @@ -164,9 +156,7 @@ def generate_radius_server( class TacacsServerItem(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) - address: Union[DefaultGlobalIPAddress, Global[IPv4Address], Global[IPv6Address]] = Field( - description="Set IP address of TACACS server" - ) + address: Union[Global[IPv4Address], Global[IPv6Address]] = Field(description="Set IP address of TACACS server") port: Union[Variable, Global[int], Default[int], None] = Field(default=None, description="TACACS Port") timeout: Union[Variable, Global[int], Default[int], None] = Field( default=None, @@ -186,7 +176,7 @@ class TacacsServerItem(BaseModel): description="Set the TACACS server shared type 7 encrypted key", ) # Literal["6", "7"] - key_enum: Union[DefaultGlobalStr, Global[str], Default[None], None] = Field( + key_enum: Union[Global[str], Default[None], None] = Field( default=None, validation_alias="keyEnum", serialization_alias="keyEnum", @@ -241,13 +231,13 @@ class AccountingRuleItem(BaseModel): method: Global[str] = Field(description="Configure Accounting Method") # Literal['1', '15'] level: Union[Global[str], Default[None], None] = Field(None, description="Privilege level when method is commands") - start_stop: Union[DefaultGlobalBool, Variable, Global[bool], Default[bool], None] = Field( + start_stop: Union[Variable, Global[bool], Default[bool], None] = Field( default=None, validation_alias="startStop", serialization_alias="startStop", description="Record start and stop without waiting", ) - group: Union[DefaultGlobalList, Global[List[str]]] = Field(description="Use Server-group") + group: Global[List[str]] = Field(description="Use Server-group") class AuthorizationRuleItem(BaseModel): @@ -259,8 +249,8 @@ class AuthorizationRuleItem(BaseModel): method: Global[str] # Literal['1', '15'] level: Global[str] = Field(description="Privilege level when method is commands") - group: Union[DefaultGlobalList, Global[List[str]]] = Field(description="Use Server-group") - if_authenticated: Union[DefaultGlobalBool, Global[bool], Default[bool], None] = Field( + group: Global[List[str]] = Field(description="Use Server-group") + if_authenticated: Union[Global[bool], Default[bool], None] = Field( default=None, validation_alias="ifAuthenticated", serialization_alias="ifAuthenticated", @@ -268,20 +258,20 @@ class AuthorizationRuleItem(BaseModel): ) -class AAA(_ParcelBase): +class AAAParcel(_ParcelBase): type_: Literal["aaa"] = Field(default="aaa", exclude=True) - authentication_group: Union[DefaultGlobalBool, Variable, Global[bool], Default[bool]] = Field( + authentication_group: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "authenticationGroup"), description="Authentication configurations parameters", ) - accounting_group: Union[DefaultGlobalBool, Variable, Global[bool], Default[bool]] = Field( + accounting_group: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "accountingGroup"), description="Accounting configurations parameters", ) # local, radius, tacacs - server_auth_order: Union[DefaultGlobalList, Global[List[str]]] = Field( + server_auth_order: Global[List[str]] = Field( validation_alias=AliasPath("data", "serverAuthOrder"), min_length=1, max_length=4, @@ -299,12 +289,12 @@ class AAA(_ParcelBase): accounting_rule: Optional[List[AccountingRuleItem]] = Field( default=None, validation_alias=AliasPath("data", "accountingRule"), description="Configure the accounting rules" ) - authorization_console: Union[DefaultGlobalBool, Variable, Global[bool], Default[bool]] = Field( + authorization_console: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "authorizationConsole"), description="For enabling console authorization", ) - authorization_config_commands: Union[DefaultGlobalBool, Variable, Global[bool], Default[bool]] = Field( + authorization_config_commands: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "authorizationConfigCommands"), description="For configuration mode commands.", diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py index 123e9ccff..bf2247e3b 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -4,10 +4,7 @@ from catalystwan.api.configuration_groups.parcel import Global, _ParcelBase, as_global from catalystwan.models.common import TLOCColor -from catalystwan.models.configuration.feature_profile.converters.recast import ( - DefaultGlobalBool, - DefaultGlobalColorLiteral, -) +from catalystwan.utils.config_migration.converters.recast import DefaultGlobalBool, DefaultGlobalColorLiteral DEFAULT_BFD_COLOR_MULTIPLIER = as_global(7) DEFAULT_BFD_DSCP = as_global(48) @@ -29,7 +26,7 @@ class Color(BaseModel): model_config = ConfigDict(populate_by_name=True) -class BFD(_ParcelBase): +class BFDParcel(_ParcelBase): type_: Literal["bfd"] = Field(default="bfd", exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py new file mode 100644 index 000000000..a92724f35 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py @@ -0,0 +1,7 @@ +from typing import Literal + +Priority = Literal["information", "debugging", "notice", "warn", "error", "critical", "alert", "emergency"] +Version = Literal["TLSv1.1", "TLSv1.2"] +AuthType = Literal["Server", "Mutual"] + +SYSTEM_LITERALS = [Priority, Version, AuthType] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging.py deleted file mode 100644 index 64ac7033b..000000000 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging.py +++ /dev/null @@ -1,52 +0,0 @@ -# flake8: noqa - -# import enum -# from typing import List, Literal, Optional, Union -# from catalystwan.api.configuration_groups.parcel import _ParcelBase, Global, as_global -# from catalystwan.models.configuration.common import AuthType, Priority, Version -# from pydantic import AliasPath, BaseModel, ConfigDict, Field -# from catalystwan.models.configuration.feature_profile.converters.recast import ( -# DefaultGlobalBool, -# DefaultGlobalStr, -# ) - -# class Server(BaseModel): -# name: Global[str] -# vpn: Optional[Union[DefaultGlobalStr, Global[str]]] = None -# source_interface: Optional[Global[str]] = Field(default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface") -# priority: Optional[Union[DefaultGlobalLiteral, Global[Priority]]] = "information" -# enable_tls: Optional[Union[DefaultGlobalBool, Global[bool]]] = Field(default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable") -# custom_profile: Optional[Union[DefaultGlobalBool, Global[bool]]] = Field( -# default=as_global(False), serialization_alias="tlsPropertiesCustomProfile", validation_alias="tlsPropertiesCustomProfile" -# ) -# profile_properties: Optional[Global[str]] = Field(default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile") -# model_config = ConfigDict(populate_by_name=True) - - -# class Ipv6Server(BaseModel): -# name: Global[str] -# vpn: Optional[Union[DefaultGlobalStr, Global[str]]] = None -# source_interface: Optional[Global[str]] = Field(default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface") -# priority: Optional[Union[DefaultGlobalLiteral, Global[Priority]]] = "information" -# enable_tls: Optional[Union[DefaultGlobalBool, Global[bool]]] = Field(default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable") -# custom_profile: Optional[Union[DefaultGlobalBool, Global[bool]]] = Field( -# default=as_global(False), serialization_alias="tlsPropertiesCustomProfile", validation_alias="tlsPropertiesCustomProfile" -# ) -# profile_properties: Optional[Global[str]] = Field(default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile") -# model_config = ConfigDict(populate_by_name=True) - -# # -# class File(BaseModel): -# disk_file_size: Optional[Global[int]] = Field(default=None, serialization_alias="diskFileSize", validation_alias="diskFileSize") -# disk_file_rotate: Optional[Global[int]] = Field(default=None, serialization_alias="diskFileRotate", validation_alias="diskFileRotate") - - -# class Disk(BaseModel): -# disk_enable: Optional[Global[bool]] = Field(default=False, serialization_alias="diskEnable", validation_alias="diskEnable") -# file: File - -# class Logging(_ParcelBase): -# disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) -# tls_profile: Optional[List[TlsProfile]] = Field(default=None, validation_alias=AliasPath("data", "tlsProfile")) -# server: Optional[List[Server]] = Field(default=None, validation_alias=AliasPath("data", "server")) -# ipv6_server: Optional[List[Ipv6Server]] = Field(default=None, validation_alias=AliasPath("data", "ipv6Server")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py new file mode 100644 index 000000000..d0a726863 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -0,0 +1,82 @@ +from typing import List, Optional, Union + +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase, as_global +from catalystwan.models.configuration.feature_profile.sdwan.system.literals import AuthType, Priority, Version +from catalystwan.utils.pydantic_validators import ConvertBoolToStringModel + + +class TlsProfile(ConvertBoolToStringModel): + profile: str + version: Optional[Version] = Field(default="TLSv1.1", json_schema_extra={"data_path": ["tls-version"]}) + auth_type: AuthType = Field(json_schema_extra={"vmanage_key": "auth-type"}) + ciphersuite_list: Optional[List] = Field( + default=None, json_schema_extra={"data_path": ["ciphersuite"], "vmanage_key": "ciphersuite-list"} + ) + model_config = ConfigDict(populate_by_name=True) + + +class Server(BaseModel): + name: Global[str] + vpn: Optional[Global[str]] = None + source_interface: Optional[Global[str]] = Field( + default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface" + ) + priority: Optional[Global[Priority]] = Field(default="information") + enable_tls: Optional[Global[bool]] = Field( + default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" + ) + custom_profile: Optional[Global[bool]] = Field( + default=as_global(False), + serialization_alias="tlsPropertiesCustomProfile", + validation_alias="tlsPropertiesCustomProfile", + ) + profile_properties: Optional[Global[str]] = Field( + default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile" + ) + model_config = ConfigDict(populate_by_name=True) + + +class Ipv6Server(BaseModel): + name: Global[str] + vpn: Optional[Global[str]] = None + source_interface: Optional[Global[str]] = Field( + default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface" + ) + priority: Optional[Global[Priority]] = Field(default="information") + enable_tls: Optional[Global[bool]] = Field( + default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" + ) + custom_profile: Optional[Global[bool]] = Field( + default=as_global(False), + serialization_alias="tlsPropertiesCustomProfile", + validation_alias="tlsPropertiesCustomProfile", + ) + profile_properties: Optional[Global[str]] = Field( + default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile" + ) + model_config = ConfigDict(populate_by_name=True) + + +class File(BaseModel): + disk_file_size: Optional[Union[Global[int], Default[int]]] = Field( + default=Default[int](value=10), serialization_alias="diskFileSize", validation_alias="diskFileSize" + ) + disk_file_rotate: Optional[Union[Global[int], Default[int]]] = Field( + default=Default[int](value=10), serialization_alias="diskFileRotate", validation_alias="diskFileRotate" + ) + + +class Disk(BaseModel): + disk_enable: Optional[Global[bool]] = Field( + default=False, serialization_alias="diskEnable", validation_alias="diskEnable" + ) + file: File + + +class LoggingParcel(_ParcelBase): + disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) + tls_profile: Optional[List[TlsProfile]] = Field(default=[], validation_alias=AliasPath("data", "tlsProfile")) + server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "server")) + ipv6_server: Optional[List[Ipv6Server]] = Field(default=[], validation_alias=AliasPath("data", "ipv6Server")) diff --git a/catalystwan/tests/config_migration/test_converter_chooser.py b/catalystwan/tests/config_migration/test_converter_chooser.py new file mode 100644 index 000000000..0e2935d0a --- /dev/null +++ b/catalystwan/tests/config_migration/test_converter_chooser.py @@ -0,0 +1,26 @@ +import unittest + +from parameterized import parameterized # type: ignore + +from catalystwan.exceptions import CatalystwanException +from catalystwan.utils.config_migration.converters.feature_template import choose_parcel_converter +from catalystwan.utils.config_migration.converters.feature_template.aaa import AAATemplateConverter +from catalystwan.utils.config_migration.converters.feature_template.bfd import BFDTemplateConverter + + +class TestParcelConverterChooser(unittest.TestCase): + @parameterized.expand( + [("cisco_aaa", AAATemplateConverter), ("cedge_aaa", AAATemplateConverter), ("cisco_bfd", BFDTemplateConverter)] + ) + def test_choose_parcel_converter_returns_correct_converter_when_supported(self, template_type, expected): + # Arrange, Act + converter = choose_parcel_converter(template_type) + # Assert + self.assertEqual(converter, expected) + + def test_choose_parcel_converter_throws_exception_when_template_type_not_supported(self): + # Arrange + not_supported_type = "!@#$%^&*()" + # Act, Assert + with self.assertRaises(CatalystwanException, msg=f"Template type {not_supported_type} not supported"): + choose_parcel_converter(not_supported_type) diff --git a/catalystwan/tests/config_migration/test_normalizer.py b/catalystwan/tests/config_migration/test_normalizer.py new file mode 100644 index 000000000..96b521585 --- /dev/null +++ b/catalystwan/tests/config_migration/test_normalizer.py @@ -0,0 +1,98 @@ +import unittest +from ipaddress import IPv4Address, IPv6Address +from typing import List, Literal +from unittest.mock import patch + +from catalystwan.api.configuration_groups.parcel import Global +from catalystwan.utils.config_migration.converters.feature_template import template_definition_normalization + +TestLiteral = Literal["castable_literal"] + + +class TestNormalizer(unittest.TestCase): + def setUp(self): + self.template_values = { + "key-one": "Simple string !@#$%^&*()-=[';/.,`~]", + "keyone": "Simplestring!@#$%^&*()-=[';/.,`~]", + "bool-value-as-string": "true", + "boolvalueasstring": "false", + "simple-int": 1, + "simpleint": 333333331231, + "simple-ints-in-list": [1, 2, 4, 5, 6, 7, 8, 9], + "simple-int-in-list": [1], + "simplestringsinlist": ["1232132", "!@#$%^&*()-=[';/.,`~]", ""], + "objects-in-list": [ + {"color": "lte", "hello-interval": 300000, "pmtu-discovery": "false"}, + {"color": "mpls", "pmtu-discovery": "false"}, + {"color": "biz-internet"}, + {"color": "public-internet"}, + ], + "nested-objects": [{"next-hop": [{"distance": 1}]}], + "ipv4-address": "10.0.0.2", + "ipv6addr": "2000:0:2:3::", + } + self.expected_result = { + "key_one": Global[str](value="Simple string !@#$%^&*()-=[';/.,`~]"), + "keyone": Global[str](value="Simplestring!@#$%^&*()-=[';/.,`~]"), + "bool_value_as_string": Global[bool](value=True), + "boolvalueasstring": Global[bool](value=False), + "simple_int": Global[int](value=1), + "simpleint": Global[int](value=333333331231), + "simple_ints_in_list": Global[List[int]](value=[1, 2, 4, 5, 6, 7, 8, 9]), + "simple_int_in_list": Global[List[int]](value=[1]), + "simplestringsinlist": Global[List[str]](value=["1232132", "!@#$%^&*()-=[';/.,`~]", ""]), + "objects_in_list": [ + { + "color": Global[str](value="lte"), + "hello_interval": Global[int](value=300000), + "pmtu_discovery": Global[bool](value=False), + }, + {"color": Global[str](value="mpls"), "pmtu_discovery": Global[bool](value=False)}, + {"color": Global[str](value="biz-internet")}, + {"color": Global[str](value="public-internet")}, + ], + "nested_objects": [{"next_hop": [{"distance": Global[int](value=1)}]}], + "ipv4_address": Global[IPv4Address](value=IPv4Address("10.0.0.2")), + "ipv6addr": Global[IPv6Address](value=IPv6Address("2000:0:2:3::")), + } + + def test_normalizer_handles_various_types_of_input(self): + # Arrange + expected_result = self.expected_result + # Act + returned_result = template_definition_normalization(self.template_values) + # Assert + self.assertDictEqual(expected_result, returned_result) + + def test_normalizer_handles_super_nested_input(self): + # Arrange + super_nested_input = { + "super_nested": {"level1": {"level2": {"level3": {"key_one": "value_one", "key_two": "value_two"}}}} + } + expected_result = { + "super_nested": { + "level1": { + "level2": { + "level3": {"key_one": Global[str](value="value_one"), "key_two": Global[str](value="value_two")} + } + } + } + } + + # Act + returned_result = template_definition_normalization(super_nested_input) + + # Assert + self.assertDictEqual(expected_result, returned_result) + + @patch("catalystwan.models.configuration.feature_profile.sdwan.system.literals.SYSTEM_LITERALS", [TestLiteral]) + def test_normalizer_literal_casting_when_literal_in_system_literals(self): + # Arrange + simple_input = {"in": "castable_literal"} + expected_result = {"in": Global[TestLiteral](value="castable_literal")} + + # Act + returned_result = template_definition_normalization(simple_input) + + # Assert + self.assertDictEqual(expected_result, returned_result) diff --git a/catalystwan/utils/config_migration/converters/feature_template/__init__.py b/catalystwan/utils/config_migration/converters/feature_template/__init__.py new file mode 100644 index 000000000..477bbd793 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/__init__.py @@ -0,0 +1,10 @@ +from typing import List + +from .factory_method import choose_parcel_converter, create_parcel_from_template +from .normalizer import template_definition_normalization + +__all__ = ["create_parcel_from_template", "choose_parcel_converter", "template_definition_normalization"] + + +def __dir__() -> "List[str]": + return list(__all__) diff --git a/catalystwan/models/configuration/feature_profile/converters/feature_template/aaa.py b/catalystwan/utils/config_migration/converters/feature_template/aaa.py similarity index 90% rename from catalystwan/models/configuration/feature_profile/converters/feature_template/aaa.py rename to catalystwan/utils/config_migration/converters/feature_template/aaa.py index cf25340a1..0cd08aad3 100644 --- a/catalystwan/models/configuration/feature_profile/converters/feature_template/aaa.py +++ b/catalystwan/utils/config_migration/converters/feature_template/aaa.py @@ -1,9 +1,9 @@ -from catalystwan.models.configuration.feature_profile.sdwan.system import AAA +from catalystwan.models.configuration.feature_profile.sdwan.system import AAAParcel class AAATemplateConverter: @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> AAA: + def create_parcel(name: str, description: str, template_values: dict) -> AAAParcel: """ Creates an AAA object based on the provided template values. @@ -27,4 +27,4 @@ def create_parcel(name: str, description: str, template_values: dict) -> AAA: if template_values.get(prop) is not None: del template_values[prop] - return AAA(**template_values) + return AAAParcel(**template_values) diff --git a/catalystwan/models/configuration/feature_profile/converters/feature_template/base.py b/catalystwan/utils/config_migration/converters/feature_template/base.py similarity index 100% rename from catalystwan/models/configuration/feature_profile/converters/feature_template/base.py rename to catalystwan/utils/config_migration/converters/feature_template/base.py diff --git a/catalystwan/models/configuration/feature_profile/converters/feature_template/bfd.py b/catalystwan/utils/config_migration/converters/feature_template/bfd.py similarity index 87% rename from catalystwan/models/configuration/feature_profile/converters/feature_template/bfd.py rename to catalystwan/utils/config_migration/converters/feature_template/bfd.py index d4c0bc6a1..ce1b6711f 100644 --- a/catalystwan/models/configuration/feature_profile/converters/feature_template/bfd.py +++ b/catalystwan/utils/config_migration/converters/feature_template/bfd.py @@ -1,9 +1,9 @@ -from catalystwan.models.configuration.feature_profile.sdwan.system import BFD +from catalystwan.models.configuration.feature_profile.sdwan.system import BFDParcel class BFDTemplateConverter: @staticmethod - def create_parcel(name: str, description: str, template_values: dict) -> BFD: + def create_parcel(name: str, description: str, template_values: dict) -> BFDParcel: """ Creates an BFD object based on the provided template values. @@ -17,4 +17,4 @@ def create_parcel(name: str, description: str, template_values: dict) -> BFD: template_values["colors"] = template_values["color"] del template_values["color"] - return BFD(**template_values) + return BFDParcel(**template_values) diff --git a/catalystwan/models/configuration/feature_profile/converters/feature_template/__init__.py b/catalystwan/utils/config_migration/converters/feature_template/factory_method.py similarity index 60% rename from catalystwan/models/configuration/feature_profile/converters/feature_template/__init__.py rename to catalystwan/utils/config_migration/converters/feature_template/factory_method.py index c241269e4..ca6ac196c 100644 --- a/catalystwan/models/configuration/feature_profile/converters/feature_template/__init__.py +++ b/catalystwan/utils/config_migration/converters/feature_template/factory_method.py @@ -1,16 +1,24 @@ -from typing import Any, Dict, List +import json +import logging +from typing import Any, 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.feature_template import find_template_values from .aaa import AAATemplateConverter from .base import FeatureTemplateConverter from .bfd import BFDTemplateConverter -from .normalizator import template_definition_normalization +from .logging_ import LoggingTemplateConverter +from .normalizer import template_definition_normalization + +logger = logging.getLogger(__name__) supported_parcel_converters: Dict[Any, FeatureTemplateConverter] = { ("cisco_aaa", "cedge_aaa"): AAATemplateConverter, # type: ignore[dict-item] ("cisco_bfd",): BFDTemplateConverter, # type: ignore[dict-item] + ("cisco_logging", "logging"): LoggingTemplateConverter, } @@ -29,8 +37,10 @@ def choose_parcel_converter(template_type: str) -> FeatureTemplateConverter: """ for key in supported_parcel_converters.keys(): if template_type in key: - return supported_parcel_converters[key] - raise ValueError(f"Template type {template_type} not supported") + converter = supported_parcel_converters[key] + logger.debug(f"Choosen converter {converter} based on template type {template_type}") + return converter + raise CatalystwanException(f"Template type {template_type} not supported") def create_parcel_from_template(template: FeatureTemplateInformation) -> AnySystemParcel: @@ -47,12 +57,8 @@ def create_parcel_from_template(template: FeatureTemplateInformation) -> AnySyst ValueError: If the given template type is not supported. """ converter = choose_parcel_converter(template.template_type) - template_values = template_definition_normalization(template.template_definiton) - return converter.create_parcel(template.name, template.description, template_values) - - -__all__ = ["create_parcel_from_template"] - - -def __dir__() -> "List[str]": - return list(__all__) + 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) + logger.debug(f"Normalized template {template.name}: {template_values_normalized}") + return converter.create_parcel(template.name, template.description, template_values_normalized) diff --git a/catalystwan/utils/config_migration/converters/feature_template/logging_.py b/catalystwan/utils/config_migration/converters/feature_template/logging_.py new file mode 100644 index 000000000..eb2c777da --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/logging_.py @@ -0,0 +1,25 @@ +from catalystwan.models.configuration.feature_profile.sdwan.system import LoggingParcel + + +class LoggingTemplateConverter: + @staticmethod + def create_parcel(name: str, description: str, template_values: dict) -> LoggingParcel: + """ + Creates an Logging object based on the provided template values. + + Returns: + Logging: An Logging object with the provided template values. + """ + template_values["name"] = name + template_values["description"] = description + + if template_values.get("disk_enable"): + template_values["disk"] = { + "disk_enable": template_values["enable"], + "file": {"disk_file_size": template_values["size"], "disk_file_rotate": template_values["rotate"]}, + } + del template_values["enable"] + del template_values["size"] + del template_values["rotate"] + + return LoggingParcel(**template_values) diff --git a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py new file mode 100644 index 000000000..5adbb777a --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py @@ -0,0 +1,67 @@ +from ipaddress import AddressValueError, IPv4Address, IPv6Address +from typing import List, Union, get_args + +from catalystwan.api.configuration_groups.parcel import Global, as_global +from catalystwan.models.configuration.feature_profile.sdwan.system import SYSTEM_LITERALS + +CastedTypes = Union[ + Global[bool], + Global[str], + Global[int], + Global[List[str]], + Global[List[int]], + Global[IPv4Address], + Global[IPv6Address], +] + + +def to_snake_case(s: str) -> str: + """Converts a string from kebab-case to snake_case.""" + return s.replace("-", "_") + + +def cast_value_to_global(value: Union[str, int, List[str], List[int]]) -> CastedTypes: + """Casts value to Global.""" + 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 + try: + ipv6_address = IPv6Address(value) + return Global[IPv6Address](value=ipv6_address) + except AddressValueError: + pass + for literal in SYSTEM_LITERALS: + if value in get_args(literal): + return Global[literal](value=value) # type: ignore + + return as_global(value) # type: ignore + + +def transform_dict(d: dict) -> dict: + """Transforms a nested dictionary into a normalized form.""" + + def transform_value(value: Union[dict, list, str, int]) -> Union[CastedTypes, dict, list]: + if isinstance(value, dict): + return transform_dict(value) + elif isinstance(value, list): + if all(isinstance(v, dict) for v in value): + return [transform_value(item) for item in value] + return cast_value_to_global(value) + + return {to_snake_case(key): transform_value(val) for key, val in d.items()} + + +def template_definition_normalization(template_definition: dict) -> dict: + """Normalizes a template definition by changing keys to snake_case and casting all leafs values to global types.""" + return transform_dict(template_definition) diff --git a/catalystwan/models/configuration/feature_profile/converters/recast.py b/catalystwan/utils/config_migration/converters/recast.py similarity index 100% rename from catalystwan/models/configuration/feature_profile/converters/recast.py rename to catalystwan/utils/config_migration/converters/recast.py diff --git a/catalystwan/utils/config_migration/creators/config_group.py b/catalystwan/utils/config_migration/creators/config_group.py new file mode 100644 index 000000000..4294b5460 --- /dev/null +++ b/catalystwan/utils/config_migration/creators/config_group.py @@ -0,0 +1,123 @@ +import logging +from datetime import datetime +from typing import List +from uuid import UUID + +from catalystwan.endpoints.configuration_feature_profile import ConfigurationFeatureProfile +from catalystwan.endpoints.configuration_group import ConfigGroup +from catalystwan.models.configuration.config_migration import UX2Config +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload +from catalystwan.session import ManagerSession + + +class ConfigGroupCreator: + """ + Creates a configuration group and attach feature profiles for migrating UX1 templates to UX2. + """ + + def __init__(self, session: ManagerSession, config: UX2Config, logger: logging.Logger): + """ + Args: + session (ManagerSession): A valid Manager API session. + config (UX2Config): The UX2 configuration to migrate. + logger (logging.Logger): A logger for logging messages. + """ + self.session = session + self.config = config + self.logger = logger + self.profile_ids: List[UUID] = [] + + def create(self) -> ConfigGroup: + """ + Creates a configuration group and attach feature profiles for migrating UX1 templates to UX2. + + Returns: + ConfigGroup: The created configuration group. + """ + self.created_at = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + self._create_sdwan_system_feature_profile() + self._create_sdwan_policy_objects_feature_profile() + config_group_id = self._create_configuration_group() + return self.session.api.config_group.get(config_group_id) # type: ignore[return-value] + + def _create_sdwan_system_feature_profile(self): + """ + Creates a SDWAN System Feature Profile for migrating UX1 Templates to UX2. + + Args: + session (ManagerSession): A valid Manager API session. + name (str): The name of the SDWAN System Feature Profile. + + Returns: + UUID: The ID of the created SDWAN System Feature Profile. + + Raises: + ManagerHTTPError: If the SDWAN System Feature Profile cannot be created. + """ + system_name = f"MIGRATION_SDWAN_SYSTEM_FEATURE_PROFILE_{self.created_at}" + profile_system = FeatureProfileCreationPayload( + name=system_name, description="Profile for migrating UX1 Templates to UX2" + ) + system_id = self.session.endpoints.configuration_feature_profile.create_sdwan_system_feature_profile( + profile_system + ).id + self.logger.info(f"Created SDWAN System Feature Profile {system_name} with ID: {system_id}") + self.profile_ids.append(system_id) + + def _create_sdwan_policy_objects_feature_profile(self): + """ + Creates a SDWAN Policy Objects Feature Profile for migrating UX1 Policies to UX2. + + Args: + session (ManagerSession): A valid Manager API session. + name (str): The name of the SDWAN Policy Objects Feature Profile. + + Returns: + UUID: The ID of the created SDWAN Policy Objects Feature Profile. + + Raises: + ManagerHTTPError: If the SDWAN Policy Objects Feature Profile cannot be created. + """ + policy_objects_name = f"MIGRATION_SDWAN_POLICY_OBJECTS_FEATURE_PROFILE_{self.created_at}" + # TODO: Find a way to create a policy object profile + # for now there is no API or UI for creating a policy object profile + profile_policy_objects = FeatureProfileCreationPayload( # noqa: F841 + name=policy_objects_name, description="Profile for migrating UX1 Policies to UX2" + ) + + # Using default profile name for SDWAN Policy Objects Feature Profile + policy_object_id = ( + ConfigurationFeatureProfile(self.session) + .get_sdwan_feature_profiles() + .filter(profile_name="Default_Policy_Object_Profile") + .single_or_default() + ).profile_id + self.logger.info( + f"Created SDWAN Policy Object Feature Profile {policy_objects_name} with ID: {policy_object_id}" + ) + self.profile_ids.append(policy_object_id) + + def _create_configuration_group(self): + """ + Creates a configuration group and attach feature profiles for migrating UX1 templates to UX2. + + Args: + session (ManagerSession): A valid Manager API session. + name (str): The name of the configuration group. + profile_ids (List[UUID]): The IDs of the feature profiles to include in the configuration group. + + Returns: + UUID: The ID of the created configuration group. + + Raises: + ManagerHTTPError: If the configuration cannot be pushed. + """ + config_group_name = f"SDWAN_CONFIG_GROUP_{self.created_at}" + config_group_id = self.session.api.config_group.create( + name=config_group_name, + description="SDWAN Config Group created for migrating UX1 Templates to UX2", + solution="sdwan", + profile_ids=self.profile_ids, + ).id + self.logger.info(f"Created SDWAN Configuration Group {config_group_name} with ID: {config_group_id}") + return config_group_id diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 3ae015ed3..abac5fcc5 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -2,9 +2,11 @@ from typing import Callable from catalystwan.api.policy_api import POLICY_LIST_ENDPOINTS_MAP -from catalystwan.models.configuration.config_migration import UX1Config, UX2Config -from catalystwan.models.configuration.feature_profile.converters.feature_template import create_parcel_from_template +from catalystwan.endpoints.configuration_group import ConfigGroup +from catalystwan.models.configuration.config_migration import ConfigGroupPreset, UX1Config, UX2Config from catalystwan.session import ManagerSession +from catalystwan.utils.config_migration.converters.feature_template import create_parcel_from_template +from catalystwan.utils.config_migration.creators.config_group import ConfigGroupCreator logger = logging.getLogger(__name__) @@ -17,13 +19,16 @@ def log_progress(task: str, completed: int, total: int) -> None: def transform(ux1: UX1Config) -> UX2Config: ux2 = UX2Config() + ux2.config_group_presets.append(ConfigGroupPreset(config_group_name="Default_Config_Group")) + profile_parcels = ux2.config_group_presets[0].profile_parcels + # Feature Templates for ft in ux1.templates.features: if ft.template_type in SUPPORTED_TEMPLATE_TYPES: - ux2.profile_parcels.append(create_parcel_from_template(ft)) + profile_parcels.append(create_parcel_from_template(ft)) # Policy Lists for policy_list in ux1.policies.policy_lists: if (parcel := policy_list.to_policy_object_parcel()) is not None: - ux2.profile_parcels.append(parcel) + profile_parcels.append(parcel) return ux2 @@ -75,5 +80,27 @@ def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, in return ux1 -def push_ux2_config(session: ManagerSession) -> None: - pass +def push_ux2_config(session: ManagerSession, config: UX2Config) -> ConfigGroup: + """ + Creates configuration group and pushes a UX2 configuration to the Cisco vManage. + + Args: + session (ManagerSession): A valid Manager API session. + config (UX2Config): The UX2 configuration to push. + + Returns: + UX2ConfigPushResult + + Raises: + ManagerHTTPError: If the configuration cannot be pushed. + """ + + config_group_creator = ConfigGroupCreator(session, config, logger) + config_group = config_group_creator.create() + feature_profiles = config_group.profiles # noqa: F841 + for parcels in config.config_group_presets: + # TODO: Create API that supports parcel creation on feature profiles + # Example: session.api.parcels.create(parcels=parcels, feature_profiles=feature_profiles) + pass + + return config_group From 578ab8432930c1ebec7165be7349b402b2c92dea Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Mon, 26 Feb 2024 14:56:57 +0100 Subject: [PATCH 10/21] Fix conflicts --- .../feature_profile/sdwan/system/aaa.py | 9 ++- .../feature_profile/sdwan/system/bfd.py | 39 ++++++++- .../feature_profile/sdwan/system/literals.py | 7 ++ .../sdwan/system/logging_parcel.py | 81 ++++++++++++++++++- .../config_migration/converters/recast.py | 59 ++++++++++++++ 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/literals.py create mode 100644 catalystwan/utils/config_migration/converters/recast.py diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py index 09bd5a0b2..54929929f 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/aaa.py @@ -27,7 +27,7 @@ class PubkeyChainItem(BaseModel): class UserItem(BaseModel): - model_config = ConfigDict(extra="forbid", populate_by_name=True) + model_config = ConfigDict(extra="ignore", populate_by_name=True) name: Union[Global[str], Variable] = Field(description="Set the username") password: Union[Global[str], Variable] = Field( @@ -110,7 +110,7 @@ class RadiusServerItem(BaseModel): class Radius(BaseModel): - model_config = ConfigDict(extra="forbid", populate_by_name=True) + model_config = ConfigDict(extra="ignore", populate_by_name=True) group_name: Global[str] = Field( validation_alias="groupName", serialization_alias="groupName", description="Set Radius server Group Name" ) @@ -260,7 +260,6 @@ class AuthorizationRuleItem(BaseModel): class AAAParcel(_ParcelBase): type_: Literal["aaa"] = Field(default="aaa", exclude=True) - authentication_group: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "authenticationGroup"), @@ -278,7 +277,9 @@ class AAAParcel(_ParcelBase): max_length=4, description="ServerGroups priority order", ) - user: Optional[List[UserItem]] = Field(default=None, description="Create local login account", min_length=1) + user: Optional[List[UserItem]] = Field( + default=None, validation_alias=AliasPath("data", "user"), description="Create local login account", min_length=1 + ) radius: Optional[List[Radius]] = Field( default=None, validation_alias=AliasPath("data", "radius"), description="Configure the Radius serverGroup" ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py index 3ee52e751..bf2247e3b 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -1,9 +1,42 @@ -from typing import Literal +from typing import List, Literal, Optional, Union -from pydantic import Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from catalystwan.api.configuration_groups.parcel import Global, _ParcelBase, as_global +from catalystwan.models.common import TLOCColor +from catalystwan.utils.config_migration.converters.recast import DefaultGlobalBool, DefaultGlobalColorLiteral + +DEFAULT_BFD_COLOR_MULTIPLIER = as_global(7) +DEFAULT_BFD_DSCP = as_global(48) +DEFAULT_BFD_HELLO_INTERVAL = as_global(1000) +DEFAULT_BFD_POLL_INTERVAL = as_global(600000) +DEFAULT_BFD_MULTIPLIER = as_global(6) + + +class Color(BaseModel): + color: Union[DefaultGlobalColorLiteral, Global[TLOCColor]] + hello_interval: Optional[Global[int]] = Field( + default=DEFAULT_BFD_HELLO_INTERVAL, validation_alias="helloInterval", serialization_alias="helloInterval" + ) + multiplier: Optional[Global[int]] = DEFAULT_BFD_COLOR_MULTIPLIER + pmtu_discovery: Optional[Union[DefaultGlobalBool, Global[bool]]] = Field( + default=as_global(True), validation_alias="pmtuDiscovery", serialization_alias="pmtuDiscovery" + ) + dscp: Optional[Global[int]] = DEFAULT_BFD_DSCP + model_config = ConfigDict(populate_by_name=True) class BFDParcel(_ParcelBase): type_: Literal["bfd"] = Field(default="bfd", exclude=True) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) + + multiplier: Optional[Global[int]] = Field( + default=DEFAULT_BFD_MULTIPLIER, validation_alias=AliasPath("data", "multiplier") + ) + poll_interval: Optional[Global[int]] = Field( + default=DEFAULT_BFD_POLL_INTERVAL, validation_alias=AliasPath("data", "pollInterval") + ) + default_dscp: Optional[Global[int]] = Field( + default=DEFAULT_BFD_DSCP, validation_alias=AliasPath("data", "defaultDscp") + ) + colors: Optional[List[Color]] = Field(default=None, validation_alias=AliasPath("data", "colors")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py new file mode 100644 index 000000000..a92724f35 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py @@ -0,0 +1,7 @@ +from typing import Literal + +Priority = Literal["information", "debugging", "notice", "warn", "error", "critical", "alert", "emergency"] +Version = Literal["TLSv1.1", "TLSv1.2"] +AuthType = Literal["Server", "Mutual"] + +SYSTEM_LITERALS = [Priority, Version, AuthType] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index 291cb22b8..d0a726863 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -1,9 +1,82 @@ -from typing import Literal +from typing import List, Optional, Union -from pydantic import Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase, as_global +from catalystwan.models.configuration.feature_profile.sdwan.system.literals import AuthType, Priority, Version +from catalystwan.utils.pydantic_validators import ConvertBoolToStringModel + + +class TlsProfile(ConvertBoolToStringModel): + profile: str + version: Optional[Version] = Field(default="TLSv1.1", json_schema_extra={"data_path": ["tls-version"]}) + auth_type: AuthType = Field(json_schema_extra={"vmanage_key": "auth-type"}) + ciphersuite_list: Optional[List] = Field( + default=None, json_schema_extra={"data_path": ["ciphersuite"], "vmanage_key": "ciphersuite-list"} + ) + model_config = ConfigDict(populate_by_name=True) + + +class Server(BaseModel): + name: Global[str] + vpn: Optional[Global[str]] = None + source_interface: Optional[Global[str]] = Field( + default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface" + ) + priority: Optional[Global[Priority]] = Field(default="information") + enable_tls: Optional[Global[bool]] = Field( + default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" + ) + custom_profile: Optional[Global[bool]] = Field( + default=as_global(False), + serialization_alias="tlsPropertiesCustomProfile", + validation_alias="tlsPropertiesCustomProfile", + ) + profile_properties: Optional[Global[str]] = Field( + default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile" + ) + model_config = ConfigDict(populate_by_name=True) + + +class Ipv6Server(BaseModel): + name: Global[str] + vpn: Optional[Global[str]] = None + source_interface: Optional[Global[str]] = Field( + default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface" + ) + priority: Optional[Global[Priority]] = Field(default="information") + enable_tls: Optional[Global[bool]] = Field( + default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" + ) + custom_profile: Optional[Global[bool]] = Field( + default=as_global(False), + serialization_alias="tlsPropertiesCustomProfile", + validation_alias="tlsPropertiesCustomProfile", + ) + profile_properties: Optional[Global[str]] = Field( + default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile" + ) + model_config = ConfigDict(populate_by_name=True) + + +class File(BaseModel): + disk_file_size: Optional[Union[Global[int], Default[int]]] = Field( + default=Default[int](value=10), serialization_alias="diskFileSize", validation_alias="diskFileSize" + ) + disk_file_rotate: Optional[Union[Global[int], Default[int]]] = Field( + default=Default[int](value=10), serialization_alias="diskFileRotate", validation_alias="diskFileRotate" + ) + + +class Disk(BaseModel): + disk_enable: Optional[Global[bool]] = Field( + default=False, serialization_alias="diskEnable", validation_alias="diskEnable" + ) + file: File class LoggingParcel(_ParcelBase): - type_: Literal["logging"] = Field(default="logging", exclude=True) + disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) + tls_profile: Optional[List[TlsProfile]] = Field(default=[], validation_alias=AliasPath("data", "tlsProfile")) + server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "server")) + ipv6_server: Optional[List[Ipv6Server]] = Field(default=[], validation_alias=AliasPath("data", "ipv6Server")) diff --git a/catalystwan/utils/config_migration/converters/recast.py b/catalystwan/utils/config_migration/converters/recast.py new file mode 100644 index 000000000..8ccc7b0c0 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/recast.py @@ -0,0 +1,59 @@ +from ipaddress import AddressValueError, IPv4Address, IPv6Address +from typing import List, Union + +from pydantic import BeforeValidator +from typing_extensions import Annotated + +from catalystwan.api.configuration_groups.parcel import Global, Variable +from catalystwan.models.common import TLOCColor + + +def recast_as_global_bool(global_: Global[str]): + value = global_.value + if value == "true": + return Global[bool](value=True) + elif value == "false": + return Global[bool](value=False) + + +def recast_as_global_list_str(global_: Global[str]): + value = global_.value + return Global[List[str]](value=[v for v in value.split(",")]) + + +def recast_as_global_ipv6_ipv4(global_: Global[str]): + value = global_.value + try: + return Global[IPv4Address](value=IPv4Address(value)) + except AddressValueError: + pass + try: + return Global[IPv6Address](value=IPv6Address(value)) + except AddressValueError: + pass + return value + + +def recast_as_global_str(global_: Global[int]): + value = global_.value + return Global[str](value=str(value)) + + +def recast_as_variable(global_: Global[str]): + value = global_.value + return Variable(value=value) + + +def recast_as_global_color_literal(global_: Global[str]): + value = global_.value + return Global[TLOCColor](value=value) # type: ignore[arg-type] + + +DefaultGlobalBool = Annotated[Global[bool], BeforeValidator(recast_as_global_bool)] +DefaultGlobalList = Annotated[Global[List[str]], BeforeValidator(recast_as_global_list_str)] +DefaultGlobalIPAddress = Annotated[ + Union[Global[IPv4Address], Global[IPv6Address]], BeforeValidator(recast_as_global_ipv6_ipv4) +] +DefaultGlobalStr = Annotated[Global[str], BeforeValidator(recast_as_global_str)] +DefaultVariableStr = Annotated[Variable, BeforeValidator(recast_as_variable)] +DefaultGlobalColorLiteral = Annotated[Global[TLOCColor], BeforeValidator(recast_as_global_color_literal)] From 9c596d0908d3e4d5e401627f290d1be854326755 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Mon, 26 Feb 2024 15:03:54 +0100 Subject: [PATCH 11/21] Fix duplicated imports and missing variables --- catalystwan/api/configuration_groups/parcel.py | 9 --------- .../feature_profile/sdwan/system/__init__.py | 2 ++ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/catalystwan/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index d9761ea11..a568c11d2 100644 --- a/catalystwan/api/configuration_groups/parcel.py +++ b/catalystwan/api/configuration_groups/parcel.py @@ -13,8 +13,6 @@ from catalystwan.exceptions import CatalystwanException -from catalystwan.exceptions import CatalystwanException - T = TypeVar("T") @@ -37,13 +35,6 @@ class _ParcelBase(BaseModel): ) _parcel_data_key: str = PrivateAttr(default="data") - @classmethod - def _get_parcel_type(cls) -> str: - field_info = cls.model_fields.get("type_") - if field_info is not None: - return str(field_info.default) - raise CatalystwanException("Cannot obtain parcel type string") - @model_serializer(mode="wrap") def envelope_parcel_data(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]: """ diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index b4edc5b3d..e29dcce7f 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -8,6 +8,7 @@ from .basic import BasicParcel from .bfd import BFDParcel from .global_parcel import GlobalParcel +from .literals import SYSTEM_LITERALS from .logging_parcel import LoggingParcel from .mrf import MRFParcel from .ntp import NTPParcel @@ -45,6 +46,7 @@ "SecurityParcel", "SNMPParcel", "AnySystemParcel", + "SYSTEM_LITERALS", ] From 27e6ceeaa925c2dd50ef2f6b7d3f7aaaf5e0bdb6 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 11:52:33 +0100 Subject: [PATCH 12/21] Write tests for API to check correct mapping and request path --- catalystwan/api/feature_profile_api.py | 22 ++--- .../sdwan/system/logging_parcel.py | 4 +- .../sdwan/test_model_creation.py | 0 catalystwan/tests/test_feature_profile_api.py | 82 +++++++++++++++++++ 4 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 catalystwan/tests/feature_profile/sdwan/test_model_creation.py create mode 100644 catalystwan/tests/test_feature_profile_api.py diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index e855afc64..b4ee28838 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -148,13 +148,13 @@ def create_profile(self, name: str, description: str) -> FeatureProfileCreationR Create System Feature Profile """ payload = FeatureProfileCreationPayload(name=name, description=description) - return self.endpoint.create_sdwan_system_feature_profile(payload=payload) + return self.endpoint.create_sdwan_system_feature_profile(payload) def delete_profile(self, profile_id: UUID) -> None: """ Delete System Feature Profile """ - self.endpoint.delete_sdwan_system_feature_profile(profile_id=profile_id) + self.endpoint.delete_sdwan_system_feature_profile(profile_id) def get_schema( self, @@ -165,7 +165,7 @@ def get_schema( Get all System Parcels for selected profile_id and selected type or get one Policy Object given parcel id """ - return self.endpoint.get_schema(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type()) + return self.endpoint.get_schema(profile_id, parcel_type._get_parcel_type()) @overload def get( @@ -367,26 +367,22 @@ def get( """ if not parcel_id: - return self.endpoint.get_all(profile_id=profile_id, parcel_type=parcel_type._get_parcel_type()) - return self.endpoint.get_by_id( - profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id - ) + return self.endpoint.get_all(profile_id, parcel_type._get_parcel_type()) + return self.endpoint.get_by_id(profile_id, parcel_type._get_parcel_type(), parcel_id) def create(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCreationResponse: """ Create System Parcel for selected profile_id based on payload type """ - return self.endpoint.create(profile_id=profile_id, parcel_type=payload._get_parcel_type(), payload=payload) + return self.endpoint.create(profile_id, payload._get_parcel_type(), payload) def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> ParcelCreationResponse: """ Update System Parcel for selected profile_id based on payload type """ - return self.endpoint.update( - profile_id=profile_id, parcel_type=payload._get_parcel_type(), parcel_id=parcel_id, payload=payload - ) + return self.endpoint.update(profile_id, payload._get_parcel_type(), parcel_id, payload=payload) @overload def delete( @@ -491,9 +487,7 @@ def delete(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id """ Delete System Parcel for selected profile_id based on payload type """ - return self.endpoint.delete( - profile_id=profile_id, parcel_type=parcel_type._get_parcel_type(), parcel_id=parcel_id - ) + return self.endpoint.delete(profile_id, parcel_type._get_parcel_type(), parcel_id) class PolicyObjectFeatureProfileAPI: diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index d0a726863..6a0ab03ed 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union from pydantic import AliasPath, BaseModel, ConfigDict, Field @@ -76,6 +76,8 @@ class Disk(BaseModel): class LoggingParcel(_ParcelBase): + type_: Literal["logging"] = Field(default="logging", exclude=True) + disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) tls_profile: Optional[List[TlsProfile]] = Field(default=[], validation_alias=AliasPath("data", "tlsProfile")) server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "server")) diff --git a/catalystwan/tests/feature_profile/sdwan/test_model_creation.py b/catalystwan/tests/feature_profile/sdwan/test_model_creation.py new file mode 100644 index 000000000..e69de29bb diff --git a/catalystwan/tests/test_feature_profile_api.py b/catalystwan/tests/test_feature_profile_api.py new file mode 100644 index 000000000..5ae8cd041 --- /dev/null +++ b/catalystwan/tests/test_feature_profile_api.py @@ -0,0 +1,82 @@ +import unittest +from unittest.mock import patch +from uuid import UUID + +from parameterized import parameterized # type: ignore + +from catalystwan.api.feature_profile_api import SystemFeatureProfileAPI +from catalystwan.models.configuration.feature_profile.sdwan.system import ( + AAAParcel, + BannerParcel, + BasicParcel, + BFDParcel, + GlobalParcel, + LoggingParcel, + MRFParcel, + NTPParcel, + OMPParcel, + SecurityParcel, + SNMPParcel, +) + +endpoint_mapping = { + AAAParcel: "aaa", + BannerParcel: "banner", + BasicParcel: "basic", + BFDParcel: "bfd", + GlobalParcel: "global", + LoggingParcel: "logging", + MRFParcel: "mrf", + NTPParcel: "ntp", + OMPParcel: "omp", + SecurityParcel: "security", + SNMPParcel: "snmp", +} + + +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") + + @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 + + # Act + api.delete(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 + + # Act + api.get(self.profile_uuid, parcel, self.parcel_uuid) + + # Assert + mock_endpoint.get_by_id.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_all_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): + # Arrange + api = SystemFeatureProfileAPI(mock_session) + api.endpoint = mock_endpoint + + # Act + api.get(self.profile_uuid, parcel) + + # Assert + mock_endpoint.get_all.assert_called_once_with(self.profile_uuid, expected_path) From 91d5cc8776c94f7053579845b4134be1e70f7aae Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 12:01:52 +0100 Subject: [PATCH 13/21] Add tests for POST and PUT --- catalystwan/api/feature_profile_api.py | 2 +- catalystwan/tests/test_feature_profile_api.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index b4ee28838..c067401e2 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -382,7 +382,7 @@ def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> Update System Parcel for selected profile_id based on payload type """ - return self.endpoint.update(profile_id, payload._get_parcel_type(), parcel_id, payload=payload) + return self.endpoint.update(profile_id, payload._get_parcel_type(), parcel_id, payload) @overload def delete( diff --git a/catalystwan/tests/test_feature_profile_api.py b/catalystwan/tests/test_feature_profile_api.py index 5ae8cd041..2174f0770 100644 --- a/catalystwan/tests/test_feature_profile_api.py +++ b/catalystwan/tests/test_feature_profile_api.py @@ -80,3 +80,31 @@ def test_get_all_method_with_valid_arguments(self, parcel, expected_path, mock_e # Assert 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_create_method_with_valid_arguments(self, parcel, expected_path, mock_endpoint, mock_session): + # Arrange + api = SystemFeatureProfileAPI(mock_session) + api.endpoint = mock_endpoint + + # Act + api.create(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 + + # Act + api.update(self.profile_uuid, parcel, self.parcel_uuid) + + # Assert + mock_endpoint.update.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid, parcel) From 4659355b4e6ecb8a9a17fbe6326e02e47862f048 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 13:33:05 +0100 Subject: [PATCH 14/21] Add Basic Model --- .../feature_profile/sdwan/system/basic.py | 291 +++++- catalystwan/utils/timezone.py | 838 +++++++++--------- 2 files changed, 707 insertions(+), 422 deletions(-) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py index 963535dc3..8a51ec44c 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -1,9 +1,294 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from typing import List, Literal, Optional, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase +from catalystwan.utils.timezone import Timezone + +ConsoleBaudRate = Literal["1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"] +DefaultConsoleBaudRate = Literal["9600"] +Epfr = Literal["disabled", "aggressive", "moderate", "conservative"] +DefaultEpfr = Literal["disabled"] +SiteType = Literal["type-1", "type-2", "type-3", "cloud", "branch", "br", "spoke"] + +DefaultTimezone = Literal["UTC"] + + +class Clock(BaseModel): + timezone: Union[Variable, Global[Timezone], Default[DefaultTimezone]] = Field( + default="UTC", description="Set the timezone" + ) + + +class MobileNumberItem(BaseModel): + number: Union[Global[str], Variable] = Field(..., description="Mobile number, ex: 1231234414") + + +class Sms(BaseModel): + enable: Optional[Union[Global[bool], Default[Literal[False]]]] = Field( + default=False, description="Global[bool] device’s geo fencing SMS" + ) + mobile_number: Optional[List[MobileNumberItem]] = Field( + None, + serialization_alias="MobileNumber", + validation_alias="MobileNumber", + description="Set device’s geo fencing SMS phone number", + ) + + +class GeoFencing(BaseModel): + enable: Optional[Union[Global[bool], Default[Literal[False]]]] = Field(None, description="Enable Geo fencing") + range: Optional[Union[Global[int], Variable, Default[int]]] = Field( + None, description="Set the device’s geo fencing range" + ) + sms: Optional[Sms] = None + + +class GpsVariable(BaseModel): + longitude: Union[Variable, Global[float], Default[None]] = Field( + default=None, description="Set the device physical longitude" + ) + latitude: Union[Variable, Global[float], Default[None]] = Field( + default=None, description="Set the device physical latitude" + ) + geo_fencing: Optional[GeoFencing] = Field( + None, + serialization_alias="geoFencing", + validation_alias="geoFencing", + ) + + +class OnDemand(BaseModel): + on_demand_enable: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=False, + serialization_alias="onDemandEnable", + validation_alias="onDemandEnable", + description="Enable or disable On-demand Tunnel", + ) + on_demand_idle_timeout: Union[ + Variable, + Global[int], + Default[int], + ] = Field( + default=10, + serialization_alias="onDemandVariable", + validation_alias="onDemandVariable", + description="Set the idle timeout for on-demand tunnels", + ) + + +class AffinityPerVrfItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + affinity_group_number: Union[ + Variable, + Global[int], + Default[None], + ] = Field( + default=None, + serialization_alias="affinityGroupNumber", + validation_alias="affinityGroupNumber", + description="Affinity Group Number", + ) + vrf_range: Union[Variable, Global[str], Default[None]] = Field( + default=None, + serialization_alias="vrfRange", + validation_alias="vrfRange", + description="Range of VRFs", + ) class BasicParcel(_ParcelBase): type_: Literal["basic"] = Field(default="basic", exclude=True) + + model_config = ConfigDict( + extra="forbid", + ) + clock: Clock + description: Union[Variable, Global[str], Default[None]] = Field( + default=None, description="Set a text description of the device" + ) + location: Union[Variable, Global[str], Default[None]] = Field( + default=None, description="Set the location of the device" + ) + gps_location: GpsVariable = Field( + ..., + serialization_alias="gpsVariable", + validation_alias="gpsVariable", + ) + device_groups: Union[Variable, Global[List[str]], Default[None]] = Field( + default=None, + serialization_alias="deviceGroups", + validation_alias="deviceGroups", + description="Device groups", + ) + controller_group_list: Optional[ + Union[ + Variable, + Global[List[int]], + Default[None], + ] + ] = Field( + None, + serialization_alias="controllerGroupList", + validation_alias="controllerGroupList", + description="Configure a list of comma-separated controller groups", + ) + overlay_id: Union[Variable, Global[int], Default[int]] = Field( + default=1, + serialization_alias="overlayId", + validation_alias="overlayId", + description="Set the Overlay ID", + ) + port_offset: Union[Variable, Global[int], Default[int]] = Field( + default=0, + serialization_alias="portOffset", + validation_alias="portOffset", + description="Set the TLOC port offset when multiple devices are behind a NAT", + ) + port_hop: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=True, + serialization_alias="portHop", + validation_alias="portHop", + description="Enable port hopping", + ) + control_session_pps: Optional[Union[Variable, Global[int], Default[int]]] = Field( + None, + serialization_alias="controlSessionPps", + validation_alias="controlSessionPps", + description="Set the policer rate for control sessions", + ) + track_transport: Optional[Union[Variable, Global[bool], Default[Literal[True]]]] = Field( + None, + serialization_alias="trackTransport", + validation_alias="trackTransport", + description="Configure tracking of transport", + ) + track_interface_tag: Optional[Union[Variable, Global[int], Default[None]]] = Field( + None, + serialization_alias="trackInterfaceTag", + validation_alias="trackInterfaceTag", + description="OMP Tag attached to routes based on interface tracking", + ) + console_baud_rate: Union[Variable, Global[ConsoleBaudRate], Default[DefaultConsoleBaudRate]] = Field( + default="9600", + serialization_alias="consoleBaudRate", + validation_alias="consoleBaudRate", + description="Set the console baud rate", + ) + max_omp_sessions: Union[Variable, Global[int], Default[None]] = Field( + default=None, + serialization_alias="maxOmpSessions", + validation_alias="maxOmpSessions", + description="Set the maximum number of OMP sessions <1..100> the device can have", + ) + multi_tenant: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + None, + serialization_alias="multiTenant", + validation_alias="multiTenant", + description="Device is multi-tenant", + ) + track_default_gateway: Optional[ + Union[ + Variable, + Global[bool], + Default[Literal[True]], + ] + ] = Field( + None, + serialization_alias="trackDefaultGateway", + validation_alias="trackDefaultGateway", + description="Enable or disable default gateway tracking", + ) + tracker_dia_stabilize_status: Optional[ + Union[ + Variable, + Global[bool], + Default[Literal[False]], + ] + ] = Field( + None, + serialization_alias="trackerDiaStabilizeStatus", + validation_alias="trackerDiaStabilizeStatus", + description="Enable or disable endpoint tracker diaStabilize status", + ) + admin_tech_on_failure: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=True, + serialization_alias="adminTechOnFailure", + validation_alias="adminTechOnFailure", + description="Collect admin-tech before reboot due to daemon failure", + ) + idle_timeout: Optional[Union[Variable, Global[int], Default[None]]] = Field( + None, + serialization_alias="idleTimeout", + validation_alias="idleTimeout", + description="Idle CLI timeout in minutes", + ) + on_demand: OnDemand = Field( + ..., + serialization_alias="onDemand", + validation_alias="onDemand", + ) + transport_gateway: Optional[Union[Global[bool], Variable, Default[Literal[False]]]] = Field( + None, + serialization_alias="transportGateway", + validation_alias="transportGateway", + description="Enable transport gateway", + ) + epfr: Optional[Union[Global[Epfr], Default[DefaultEpfr], Variable]] = Field( + None, + description="Enable SLA Dampening and Enhanced App Routing.", + ) + site_type: Optional[Union[Variable, Global[List[SiteType]], Default[None]]] = Field( + None, + serialization_alias="siteType", + validation_alias="siteType", + description="Site Type", + ) + affinity_group_number: Optional[ + Union[ + Variable, + Global[int], + Default[None], + ] + ] = Field( + None, + serialization_alias="affinityGroupGlobal[str]", + validation_alias="affinityGroupGlobal[str]", + description="Affinity Group Global[str]", + ) + affinity_group_preference: Optional[ + Union[ + Variable, + Global[List[int]], + Default[None], + ] + ] = Field( + None, + serialization_alias="affinityGroupPreference", + validation_alias="affinityGroupPreference", + description="Affinity Group Preference", + ) + affinity_preference_auto: Optional[ + Union[ + Variable, + Global[bool], + Default[Literal[False]], + ] + ] = Field( + None, + serialization_alias="affinityPreferenceAuto", + validation_alias="affinityPreferenceAuto", + description="Affinity Group Preference Auto", + ) + affinity_per_vrf: Optional[List[AffinityPerVrfItem]] = Field( + None, + serialization_alias="affinityPerVrf", + validation_alias="affinityPerVrf", + description="Affinity Group Global[str] for VRFs", + max_length=4, + min_length=0, + ) diff --git a/catalystwan/utils/timezone.py b/catalystwan/utils/timezone.py index 6b7e9ce25..a607a0d77 100644 --- a/catalystwan/utils/timezone.py +++ b/catalystwan/utils/timezone.py @@ -1,420 +1,420 @@ -from enum import Enum +from typing import Literal - -class Timezone(str, Enum): - EUROPE_ANDORRA = "Europe/Andorra" - ASIA_DUBAI = "Asia/Dubai" - ASIA_KABUL_ = "Asia/Kabul" - AMERICA_ANTIGUA_ = "America/Antigua" - AMERICA_ANGUILLA_ = "America/Anguilla" - EUROPE_TIRANE_ = "Europe/Tirane" - ASIA_YEREVAN_ = "Asia/Yerevan" - AFRICA_LUANDA_ = "Africa/Luanda" - ANTARCTICA_MCMURDO = "Antarctica/McMurdo" - ANTARCTICA_ROTHERA = "Antarctica/Rothera" - ANTARCTICA_PALMER = "Antarctica/Palmer" - ANTARCTICA_MAWSON = "Antarctica/Mawson" - ANTARCTICA_DAVIS = "Antarctica/Davis" - ANTARCTICA_CASEY = "Antarctica/Casey" - ANTARCTICA_VOSTOK = "Antarctica/Vostok" - ANTARCTICA_DUMONTDURVILLE = "Antarctica/DumontDUrville" - ANTARCTICA_SYOWA = "Antarctica/Syowa" - AMERICA_ARGENTINA_BUENOS_AIRES = "America/Argentina/Buenos_Aires" - AMERICA_ARGENTINA_CORDOBA = "America/Argentina/Cordoba" - AMERICA_ARGENTINA_SALTA = "America/Argentina/Salta" - AMERICA_ARGENTINA_JUJUY = "America/Argentina/Jujuy" - AMERICA_ARGENTINA_TUCUMAN = "America/Argentina/Tucuman" - AMERICA_ARGENTINA_CATAMARCA = "America/Argentina/Catamarca" - AMERICA_ARGENTINA_LA_RIOJA = "America/Argentina/La_Rioja" - AMERICA_ARGENTINA_SAN_JUAN = "America/Argentina/San_Juan" - AMERICA_ARGENTINA_MENDOZA = "America/Argentina/Mendoza" - AMERICA_ARGENTINA_SAN_LUIS = "America/Argentina/San_Luis" - AMERICA_ARGENTINA_RIO_GALLEGOS = "America/Argentina/Rio_Gallegos" - AMERICA_ARGENTINA_USHUAIA = "America/Argentina/Ushuaia" - PACIFIC_PAGO_PAGO = "Pacific/Pago_Pago" - EUROPE_VIENNA = "Europe/Vienna" - AUSTRALIA_LORD_HOWE = "Australia/Lord_Howe" - ANTARCTICA_MACQUARIE = "Antarctica/Macquarie" - AUSTRALIA_HOBART = "Australia/Hobart" - AUSTRALIA_CURRIE = "Australia/Currie" - AUSTRALIA_MELBOURNE = "Australia/Melbourne" - AUSTRALIA_SYDNEY = "Australia/Sydney" - AUSTRALIA_BROKEN_HILL = "Australia/Broken_Hill" - AUSTRALIA_BRISBANE = "Australia/Brisbane" - AUSTRALIA_LINDEMAN = "Australia/Lindeman" - AUSTRALIA_ADELAIDE = "Australia/Adelaide" - AUSTRALIA_DARWIN = "Australia/Darwin" - AUSTRALIA_PERTH = "Australia/Perth" - AUSTRALIA_EUCLA = "Australia/Eucla" - AMERICA_ARUBA = "America/Aruba" - EUROPE_MARIEHAMN = "Europe/Mariehamn" - ASIA_BAKU = "Asia/Baku" - EUROPE_SARAJEVO = "Europe/Sarajevo" - AMERICA_BARBADOS = "America/Barbados" - ASIA_DHAKA = "Asia/Dhaka" - EUROPE_BRUSSELS = "Europe/Brussels" - AFRICA_OUAGADOUGOU = "Africa/Ouagadougou" - EUROPE_SOFIA = "Europe/Sofia" - ASIA_BAHRAIN = "Asia/Bahrain" - AFRICA_BUJUMBURA = "Africa/Bujumbura" - AFRICA_PORTO_NOVO = "Africa/Porto-Novo" - AMERICA_ST_BARTHELEMY = "America/St_Barthelemy" - ATLANTIC_BERMUDA = "Atlantic/Bermuda" - ASIA_BRUNEI = "Asia/Brunei" - AMERICA_LA_PAZ = "America/La_Paz" - AMERICA_KRALENDIJK = "America/Kralendijk" - AMERICA_NORONHA = "America/Noronha" - AMERICA_BELEM = "America/Belem" - AMERICA_FORTALEZA = "America/Fortaleza" - AMERICA_RECIFE = "America/Recife" - AMERICA_ARAGUAINA = "America/Araguaina" - AMERICA_MACEIO = "America/Maceio" - AMERICA_BAHIA = "America/Bahia" - AMERICA_SAO_PAULO = "America/Sao_Paulo" - AMERICA_CAMPO_GRANDE = "America/Campo_Grande" - AMERICA_CUIABA = "America/Cuiaba" - AMERICA_SANTAREM = "America/Santarem" - AMERICA_PORTO_VELHO = "America/Porto_Velho" - AMERICA_BOA_VISTA = "America/Boa_Vista" - AMERICA_MANAUS = "America/Manaus" - AMERICA_EIRUNEPE = "America/Eirunepe" - AMERICA_RIO_BRANCO = "America/Rio_Branco" - AMERICA_NASSAU = "America/Nassau" - ASIA_THIMPHU = "Asia/Thimphu" - AFRICA_GABORONE = "Africa/Gaborone" - EUROPE_MINSK = "Europe/Minsk" - AMERICA_BELIZE = "America/Belize" - AMERICA_ST_JOHNS = "America/St_Johns" - AMERICA_HALIFAX = "America/Halifax" - AMERICA_GLACE_BAY = "America/Glace_Bay" - AMERICA_MONCTON = "America/Moncton" - AMERICA_GOOSE_BAY = "America/Goose_Bay" - AMERICA_BLANC_SABLON = "America/Blanc-Sablon" - AMERICA_TORONTO = "America/Toronto" - AMERICA_NIPIGON = "America/Nipigon" - AMERICA_THUNDER_BAY = "America/Thunder_Bay" - AMERICA_IQALUIT = "America/Iqaluit" - AMERICA_PANGNIRTUNG = "America/Pangnirtung" - AMERICA_RESOLUTE = "America/Resolute" - AMERICA_ATIKOKAN = "America/Atikokan" - AMERICA_RANKIN_INLET = "America/Rankin_Inlet" - AMERICA_WINNIPEG = "America/Winnipeg" - AMERICA_RAINY_RIVER = "America/Rainy_River" - AMERICA_REGINA = "America/Regina" - AMERICA_SWIFT_CURRENT = "America/Swift_Current" - AMERICA_EDMONTON = "America/Edmonton" - AMERICA_CAMBRIDGE_BAY = "America/Cambridge_Bay" - AMERICA_YELLOWKNIFE = "America/Yellowknife" - AMERICA_INUVIK = "America/Inuvik" - AMERICA_CRESTON = "America/Creston" - AMERICA_DAWSON_CREEK = "America/Dawson_Creek" - AMERICA_VANCOUVER = "America/Vancouver" - AMERICA_WHITEHORSE = "America/Whitehorse" - AMERICA_DAWSON = "America/Dawson" - INDIAN_COCOS = "Indian/Cocos" - AFRICA_KINSHASA = "Africa/Kinshasa" - AFRICA_LUBUMBASHI = "Africa/Lubumbashi" - AFRICA_BANGUI = "Africa/Bangui" - AFRICA_BRAZZAVILLE = "Africa/Brazzaville" - EUROPE_ZURICH = "Europe/Zurich" - AFRICA_ABIDJAN = "Africa/Abidjan" - PACIFIC_RAROTONGA = "Pacific/Rarotonga" - AMERICA_SANTIAGO = "America/Santiago" - PACIFIC_EASTER = "Pacific/Easter" - AFRICA_DOUALA = "Africa/Douala" - ASIA_SHANGHAI = "Asia/Shanghai" - ASIA_HARBIN = "Asia/Harbin" - ASIA_CHONGQING = "Asia/Chongqing" - ASIA_URUMQI = "Asia/Urumqi" - ASIA_KASHGAR = "Asia/Kashgar" - AMERICA_BOGOTA = "America/Bogota" - AMERICA_COSTA_RICA = "America/Costa_Rica" - AMERICA_HAVANA = "America/Havana" - ATLANTIC_CAPE_VERDE = "Atlantic/Cape_Verde" - AMERICA_CURACAO = "America/Curacao" - INDIAN_CHRISTMAS = "Indian/Christmas" - ASIA_NICOSIA = "Asia/Nicosia" - EUROPE_PRAGUE = "Europe/Prague" - EUROPE_BERLIN = "Europe/Berlin" - EUROPE_BUSINGEN = "Europe/Busingen" - AFRICA_DJIBOUTI = "Africa/Djibouti" - EUROPE_COPENHAGEN = "Europe/Copenhagen" - AMERICA_DOMINICA = "America/Dominica" - AMERICA_SANTO_DOMINGO = "America/Santo_Domingo" - AFRICA_ALGIERS = "Africa/Algiers" - AMERICA_GUAYAQUIL = "America/Guayaquil" - PACIFIC_GALAPAGOS = "Pacific/Galapagos" - EUROPE_TALLINN = "Europe/Tallinn" - AFRICA_CAIRO = "Africa/Cairo" - AFRICA_EL_AAIUN = "Africa/El_Aaiun" - AFRICA_ASMARA = "Africa/Asmara" - EUROPE_MADRID = "Europe/Madrid" - AFRICA_CEUTA = "Africa/Ceuta" - ATLANTIC_CANARY = "Atlantic/Canary" - AFRICA_ADDIS_ABABA = "Africa/Addis_Ababa" - EUROPE_HELSINKI = "Europe/Helsinki" - PACIFIC_FIJI = "Pacific/Fiji" - ATLANTIC_STANLEY = "Atlantic/Stanley" - PACIFIC_CHUUK = "Pacific/Chuuk" - PACIFIC_POHNPEI = "Pacific/Pohnpei" - PACIFIC_KOSRAE = "Pacific/Kosrae" - ATLANTIC_FAROE = "Atlantic/Faroe" - EUROPE_PARIS = "Europe/Paris" - AFRICA_LIBREVILLE = "Africa/Libreville" - EUROPE_LONDON = "Europe/London" - AMERICA_GRENADA = "America/Grenada" - ASIA_TBILISI = "Asia/Tbilisi" - AMERICA_CAYENNE = "America/Cayenne" - EUROPE_GUERNSEY = "Europe/Guernsey" - AFRICA_ACCRA = "Africa/Accra" - EUROPE_GIBRALTAR = "Europe/Gibraltar" - AMERICA_GODTHAB = "America/Godthab" - AMERICA_DANMARKSHAVN = "America/Danmarkshavn" - AMERICA_SCORESBYSUND = "America/Scoresbysund" - AMERICA_THULE = "America/Thule" - AFRICA_BANJUL = "Africa/Banjul" - AFRICA_CONAKRY = "Africa/Conakry" - AMERICA_GUADELOUPE = "America/Guadeloupe" - AFRICA_MALABO = "Africa/Malabo" - EUROPE_ATHENS = "Europe/Athens" - ATLANTIC_SOUTH_GEORGIA = "Atlantic/South_Georgia" - AMERICA_GUATEMALA = "America/Guatemala" - PACIFIC_GUAM = "Pacific/Guam" - AFRICA_BISSAU = "Africa/Bissau" - AMERICA_GUYANA = "America/Guyana" - ASIA_HONG_KONG = "Asia/Hong_Kong" - AMERICA_TEGUCIGALPA = "America/Tegucigalpa" - EUROPE_ZAGREB = "Europe/Zagreb" - AMERICA_PORT_AU_PRINCE = "America/Port-au-Prince" - EUROPE_BUDAPEST = "Europe/Budapest" - ASIA_JAKARTA = "Asia/Jakarta" - ASIA_PONTIANAK = "Asia/Pontianak" - ASIA_MAKASSAR = "Asia/Makassar" - ASIA_JAYAPURA = "Asia/Jayapura" - EUROPE_DUBLIN = "Europe/Dublin" - ASIA_JERUSALEM = "Asia/Jerusalem" - EUROPE_ISLE_OF_MAN = "Europe/Isle_of_Man" - ASIA_KOLKATA = "Asia/Kolkata" - INDIAN_CHAGOS = "Indian/Chagos" - ASIA_BAGHDAD = "Asia/Baghdad" - ASIA_TEHRAN = "Asia/Tehran" - ATLANTIC_REYKJAVIK = "Atlantic/Reykjavik" - EUROPE_ROME = "Europe/Rome" - EUROPE_JERSEY = "Europe/Jersey" - AMERICA_JAMAICA = "America/Jamaica" - ASIA_AMMAN = "Asia/Amman" - ASIA_TOKYO = "Asia/Tokyo" - AFRICA_NAIROBI = "Africa/Nairobi" - ASIA_BISHKEK = "Asia/Bishkek" - ASIA_PHNOM_PENH = "Asia/Phnom_Penh" - PACIFIC_TARAWA = "Pacific/Tarawa" - PACIFIC_ENDERBURY = "Pacific/Enderbury" - PACIFIC_KIRITIMATI = "Pacific/Kiritimati" - INDIAN_COMORO = "Indian/Comoro" - AMERICA_ST_KITTS = "America/St_Kitts" - ASIA_PYONGYANG = "Asia/Pyongyang" - ASIA_SEOUL = "Asia/Seoul" - ASIA_KUWAIT = "Asia/Kuwait" - AMERICA_CAYMAN = "America/Cayman" - ASIA_ALMATY = "Asia/Almaty" - ASIA_QYZYLORDA = "Asia/Qyzylorda" - ASIA_AQTOBE = "Asia/Aqtobe" - ASIA_AQTAU = "Asia/Aqtau" - ASIA_ORAL = "Asia/Oral" - ASIA_VIENTIANE = "Asia/Vientiane" - ASIA_BEIRUT = "Asia/Beirut" - AMERICA_ST_LUCIA = "America/St_Lucia" - EUROPE_VADUZ = "Europe/Vaduz" - ASIA_COLOMBO = "Asia/Colombo" - AFRICA_MONROVIA = "Africa/Monrovia" - AFRICA_MASERU = "Africa/Maseru" - EUROPE_VILNIUS = "Europe/Vilnius" - EUROPE_LUXEMBOURG = "Europe/Luxembourg" - EUROPE_RIGA = "Europe/Riga" - AFRICA_TRIPOLI = "Africa/Tripoli" - AFRICA_CASABLANCA = "Africa/Casablanca" - EUROPE_MONACO = "Europe/Monaco" - EUROPE_CHISINAU = "Europe/Chisinau" - EUROPE_PODGORICA = "Europe/Podgorica" - AMERICA_MARIGOT = "America/Marigot" - INDIAN_ANTANANARIVO = "Indian/Antananarivo" - PACIFIC_MAJURO = "Pacific/Majuro" - PACIFIC_KWAJALEIN = "Pacific/Kwajalein" - EUROPE_SKOPJE = "Europe/Skopje" - AFRICA_BAMAKO = "Africa/Bamako" - ASIA_RANGOON = "Asia/Rangoon" - ASIA_ULAANBAATAR = "Asia/Ulaanbaatar" - ASIA_HOVD = "Asia/Hovd" - ASIA_CHOIBALSAN = "Asia/Choibalsan" - ASIA_MACAU = "Asia/Macau" - PACIFIC_SAIPAN = "Pacific/Saipan" - AMERICA_MARTINIQUE = "America/Martinique" - AFRICA_NOUAKCHOTT = "Africa/Nouakchott" - AMERICA_MONTSERRAT = "America/Montserrat" - EUROPE_MALTA = "Europe/Malta" - INDIAN_MAURITIUS = "Indian/Mauritius" - INDIAN_MALDIVES = "Indian/Maldives" - AFRICA_BLANTYRE = "Africa/Blantyre" - AMERICA_MEXICO_CITY = "America/Mexico_City" - AMERICA_CANCUN = "America/Cancun" - AMERICA_MERIDA = "America/Merida" - AMERICA_MONTERREY = "America/Monterrey" - AMERICA_MATAMOROS = "America/Matamoros" - AMERICA_MAZATLAN = "America/Mazatlan" - AMERICA_CHIHUAHUA = "America/Chihuahua" - AMERICA_OJINAGA = "America/Ojinaga" - AMERICA_HERMOSILLO = "America/Hermosillo" - AMERICA_TIJUANA = "America/Tijuana" - AMERICA_SANTA_ISABEL = "America/Santa_Isabel" - AMERICA_BAHIA_BANDERAS = "America/Bahia_Banderas" - ASIA_KUALA_LUMPUR = "Asia/Kuala_Lumpur" - ASIA_KUCHING = "Asia/Kuching" - AFRICA_MAPUTO = "Africa/Maputo" - AFRICA_WINDHOEK = "Africa/Windhoek" - PACIFIC_NOUMEA = "Pacific/Noumea" - AFRICA_NIAMEY = "Africa/Niamey" - PACIFIC_NORFOLK = "Pacific/Norfolk" - AFRICA_LAGOS = "Africa/Lagos" - AMERICA_MANAGUA = "America/Managua" - EUROPE_AMSTERDAM = "Europe/Amsterdam" - EUROPE_OSLO = "Europe/Oslo" - ASIA_KATHMANDU = "Asia/Kathmandu" - PACIFIC_NAURU = "Pacific/Nauru" - PACIFIC_NIUE = "Pacific/Niue" - PACIFIC_AUCKLAND = "Pacific/Auckland" - PACIFIC_CHATHAM = "Pacific/Chatham" - ASIA_MUSCAT = "Asia/Muscat" - AMERICA_PANAMA = "America/Panama" - AMERICA_LIMA = "America/Lima" - PACIFIC_TAHITI = "Pacific/Tahiti" - PACIFIC_MARQUESAS = "Pacific/Marquesas" - PACIFIC_GAMBIER = "Pacific/Gambier" - PACIFIC_PORT_MORESBY = "Pacific/Port_Moresby" - ASIA_MANILA = "Asia/Manila" - ASIA_KARACHI = "Asia/Karachi" - EUROPE_WARSAW = "Europe/Warsaw" - AMERICA_MIQUELON = "America/Miquelon" - PACIFIC_PITCAIRN = "Pacific/Pitcairn" - AMERICA_PUERTO_RICO = "America/Puerto_Rico" - ASIA_GAZA = "Asia/Gaza" - ASIA_HEBRON = "Asia/Hebron" - EUROPE_LISBON = "Europe/Lisbon" - ATLANTIC_MADEIRA = "Atlantic/Madeira" - ATLANTIC_AZORES = "Atlantic/Azores" - PACIFIC_PALAU = "Pacific/Palau" - AMERICA_ASUNCION = "America/Asuncion" - ASIA_QATAR = "Asia/Qatar" - INDIAN_REUNION = "Indian/Reunion" - EUROPE_BUCHAREST = "Europe/Bucharest" - EUROPE_BELGRADE = "Europe/Belgrade" - EUROPE_KALININGRAD = "Europe/Kaliningrad" - EUROPE_MOSCOW = "Europe/Moscow" - EUROPE_VOLGOGRAD = "Europe/Volgograd" - EUROPE_SAMARA = "Europe/Samara" - ASIA_YEKATERINBURG = "Asia/Yekaterinburg" - ASIA_OMSK = "Asia/Omsk" - ASIA_NOVOSIBIRSK = "Asia/Novosibirsk" - ASIA_NOVOKUZNETSK = "Asia/Novokuznetsk" - ASIA_KRASNOYARSK = "Asia/Krasnoyarsk" - ASIA_IRKUTSK = "Asia/Irkutsk" - ASIA_YAKUTSK = "Asia/Yakutsk" - ASIA_KHANDYGA = "Asia/Khandyga" - ASIA_VLADIVOSTOK = "Asia/Vladivostok" - ASIA_SAKHALIN = "Asia/Sakhalin" - ASIA_UST_NERA = "Asia/Ust-Nera" - ASIA_MAGADAN = "Asia/Magadan" - ASIA_KAMCHATKA = "Asia/Kamchatka" - ASIA_ANADYR = "Asia/Anadyr" - AFRICA_KIGALI = "Africa/Kigali" - ASIA_RIYADH = "Asia/Riyadh" - PACIFIC_GUADALCANAL = "Pacific/Guadalcanal" - INDIAN_MAHE = "Indian/Mahe" - AFRICA_KHARTOUM = "Africa/Khartoum" - EUROPE_STOCKHOLM = "Europe/Stockholm" - ASIA_SINGAPORE = "Asia/Singapore" - ATLANTIC_ST_HELENA = "Atlantic/St_Helena" - EUROPE_LJUBLJANA = "Europe/Ljubljana" - ARCTIC_LONGYEARBYEN = "Arctic/Longyearbyen" - EUROPE_BRATISLAVA = "Europe/Bratislava" - AFRICA_FREETOWN = "Africa/Freetown" - EUROPE_SAN_MARINO = "Europe/San_Marino" - AFRICA_DAKAR = "Africa/Dakar" - AFRICA_MOGADISHU = "Africa/Mogadishu" - AMERICA_PARAMARIBO = "America/Paramaribo" - AFRICA_JUBA = "Africa/Juba" - AFRICA_SAO_TOME = "Africa/Sao_Tome" - AMERICA_EL_SALVADOR = "America/El_Salvador" - AMERICA_LOWER_PRINCES = "America/Lower_Princes" - ASIA_DAMASCUS = "Asia/Damascus" - AFRICA_MBABANE = "Africa/Mbabane" - AMERICA_GRAND_TURK = "America/Grand_Turk" - AFRICA_NDJAMENA = "Africa/Ndjamena" - INDIAN_KERGUELEN = "Indian/Kerguelen" - AFRICA_LOME = "Africa/Lome" - ASIA_BANGKOK = "Asia/Bangkok" - ASIA_DUSHANBE = "Asia/Dushanbe" - PACIFIC_FAKAOFO = "Pacific/Fakaofo" - ASIA_DILI = "Asia/Dili" - ASIA_ASHGABAT = "Asia/Ashgabat" - AFRICA_TUNIS = "Africa/Tunis" - PACIFIC_TONGATAPU = "Pacific/Tongatapu" - EUROPE_ISTANBUL = "Europe/Istanbul" - AMERICA_PORT_OF_SPAIN = "America/Port_of_Spain" - PACIFIC_FUNAFUTI = "Pacific/Funafuti" - ASIA_TAIPEI = "Asia/Taipei" - AFRICA_DAR_ES_SALAAM = "Africa/Dar_es_Salaam" - EUROPE_KIEV = "Europe/Kiev" - EUROPE_UZHGOROD = "Europe/Uzhgorod" - EUROPE_ZAPOROZHYE = "Europe/Zaporozhye" - EUROPE_SIMFEROPOL = "Europe/Simferopol" - AFRICA_KAMPALA = "Africa/Kampala" - PACIFIC_JOHNSTON = "Pacific/Johnston" - PACIFIC_MIDWAY = "Pacific/Midway" - PACIFIC_WAKE = "Pacific/Wake" - AMERICA_NEW_YORK = "America/New_York" - AMERICA_DETROIT = "America/Detroit" - AMERICA_KENTUCKY_LOUISVILLE = "America/Kentucky/Louisville" - AMERICA_KENTUCKY_MONTICELLO = "America/Kentucky/Monticello" - AMERICA_INDIANA_INDIANAPOLIS = "America/Indiana/Indianapolis" - AMERICA_INDIANA_VINCENNES = "America/Indiana/Vincennes" - AMERICA_INDIANA_WINAMAC = "America/Indiana/Winamac" - AMERICA_INDIANA_MARENGO = "America/Indiana/Marengo" - AMERICA_INDIANA_PETERSBURG = "America/Indiana/Petersburg" - AMERICA_INDIANA_VEVAY = "America/Indiana/Vevay" - AMERICA_CHICAGO = "America/Chicago" - AMERICA_INDIANA_TELL_CITY = "America/Indiana/Tell_City" - AMERICA_INDIANA_KNOX = "America/Indiana/Knox" - AMERICA_MENOMINEE = "America/Menominee" - AMERICA_NORTH_DAKOTA_CENTER = "America/North_Dakota/Center" - AMERICA_NORTH_DAKOTA_NEW_SALEM = "America/North_Dakota/New_Salem" - AMERICA_NORTH_DAKOTA_BEULAH = "America/North_Dakota/Beulah" - AMERICA_DENVER = "America/Denver" - AMERICA_BOISE = "America/Boise" - AMERICA_PHOENIX = "America/Phoenix" - AMERICA_LOS_ANGELES = "America/Los_Angeles" - AMERICA_ANCHORAGE = "America/Anchorage" - AMERICA_JUNEAU = "America/Juneau" - AMERICA_SITKA = "America/Sitka" - AMERICA_YAKUTAT = "America/Yakutat" - AMERICA_NOME = "America/Nome" - AMERICA_ADAK = "America/Adak" - AMERICA_METLAKATLA = "America/Metlakatla" - PACIFIC_HONOLULU = "Pacific/Honolulu" - AMERICA_MONTEVIDEO = "America/Montevideo" - ASIA_SAMARKAND = "Asia/Samarkand" - ASIA_TASHKENT = "Asia/Tashkent" - EUROPE_VATICAN = "Europe/Vatican" - AMERICA_ST_VINCENT = "America/St_Vincent" - AMERICA_CARACAS = "America/Caracas" - AMERICA_TORTOLA = "America/Tortola" - AMERICA_ST_THOMAS = "America/St_Thomas" - ASIA_HO_CHI_MINH = "Asia/Ho_Chi_Minh" - PACIFIC_EFATE = "Pacific/Efate" - PACIFIC_WALLIS = "Pacific/Wallis" - PACIFIC_APIA = "Pacific/Apia" - ASIA_ADEN = "Asia/Aden" - INDIAN_MAYOTTE = "Indian/Mayotte" - AFRICA_JOHANNESBURG = "Africa/Johannesburg" - AFRICA_LUSAKA = "Africa/Lusaka" - AFRICA_HARARE = "Africa/Harare" - UTC = "UTC" +Timezone = Literal[ + "Europe/Andorra", + "Asia/Dubai", + "Asia/Kabul", + "America/Antigua", + "America/Anguilla", + "Europe/Tirane", + "Asia/Yerevan", + "Africa/Luanda", + "Antarctica/McMurdo", + "Antarctica/Rothera", + "Antarctica/Palmer", + "Antarctica/Mawson", + "Antarctica/Davis", + "Antarctica/Casey", + "Antarctica/Vostok", + "Antarctica/DumontDUrville", + "Antarctica/Syowa", + "America/Argentina/Buenos_Aires", + "America/Argentina/Cordoba", + "America/Argentina/Salta", + "America/Argentina/Jujuy", + "America/Argentina/Tucuman", + "America/Argentina/Catamarca", + "America/Argentina/La_Rioja", + "America/Argentina/San_Juan", + "America/Argentina/Mendoza", + "America/Argentina/San_Luis", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Ushuaia", + "Pacific/Pago_Pago", + "Europe/Vienna", + "Australia/Lord_Howe", + "Antarctica/Macquarie", + "Australia/Hobart", + "Australia/Currie", + "Australia/Melbourne", + "Australia/Sydney", + "Australia/Broken_Hill", + "Australia/Brisbane", + "Australia/Lindeman", + "Australia/Adelaide", + "Australia/Darwin", + "Australia/Perth", + "Australia/Eucla", + "America/Aruba", + "Europe/Mariehamn", + "Asia/Baku", + "Europe/Sarajevo", + "America/Barbados", + "Asia/Dhaka", + "Europe/Brussels", + "Africa/Ouagadougou", + "Europe/Sofia", + "Asia/Bahrain", + "Africa/Bujumbura", + "Africa/Porto-Novo", + "America/St_Barthelemy", + "Atlantic/Bermuda", + "Asia/Brunei", + "America/La_Paz", + "America/Kralendijk", + "America/Noronha", + "America/Belem", + "America/Fortaleza", + "America/Recife", + "America/Araguaina", + "America/Maceio", + "America/Bahia", + "America/Sao_Paulo", + "America/Campo_Grande", + "America/Cuiaba", + "America/Santarem", + "America/Porto_Velho", + "America/Boa_Vista", + "America/Manaus", + "America/Eirunepe", + "America/Rio_Branco", + "America/Nassau", + "Asia/Thimphu", + "Africa/Gaborone", + "Europe/Minsk", + "America/Belize", + "America/St_Johns", + "America/Halifax", + "America/Glace_Bay", + "America/Moncton", + "America/Goose_Bay", + "America/Blanc-Sablon", + "America/Toronto", + "America/Nipigon", + "America/Thunder_Bay", + "America/Iqaluit", + "America/Pangnirtung", + "America/Resolute", + "America/Atikokan", + "America/Rankin_Inlet", + "America/Winnipeg", + "America/Rainy_River", + "America/Regina", + "America/Swift_Current", + "America/Edmonton", + "America/Cambridge_Bay", + "America/Yellowknife", + "America/Inuvik", + "America/Creston", + "America/Dawson_Creek", + "America/Vancouver", + "America/Whitehorse", + "America/Dawson", + "Indian/Cocos", + "Africa/Kinshasa", + "Africa/Lubumbashi", + "Africa/Bangui", + "Africa/Brazzaville", + "Europe/Zurich", + "Africa/Abidjan", + "Pacific/Rarotonga", + "America/Santiago", + "Pacific/Easter", + "Africa/Douala", + "Asia/Shanghai", + "Asia/Harbin", + "Asia/Chongqing", + "Asia/Urumqi", + "Asia/Kashgar", + "America/Bogota", + "America/Costa_Rica", + "America/Havana", + "Atlantic/Cape_Verde", + "America/Curacao", + "Indian/Christmas", + "Asia/Nicosia", + "Europe/Prague", + "Europe/Berlin", + "Europe/Busingen", + "Africa/Djibouti", + "Europe/Copenhagen", + "America/Dominica", + "America/Santo_Domingo", + "Africa/Algiers", + "America/Guayaquil", + "Pacific/Galapagos", + "Europe/Tallinn", + "Africa/Cairo", + "Africa/El_Aaiun", + "Africa/Asmara", + "Europe/Madrid", + "Africa/Ceuta", + "Atlantic/Canary", + "Africa/Addis_Ababa", + "Europe/Helsinki", + "Pacific/Fiji", + "Atlantic/Stanley", + "Pacific/Chuuk", + "Pacific/Pohnpei", + "Pacific/Kosrae", + "Atlantic/Faroe", + "Europe/Paris", + "Africa/Libreville", + "Europe/London", + "America/Grenada", + "Asia/Tbilisi", + "America/Cayenne", + "Europe/Guernsey", + "Africa/Accra", + "Europe/Gibraltar", + "America/Godthab", + "America/Danmarkshavn", + "America/Scoresbysund", + "America/Thule", + "Africa/Banjul", + "Africa/Conakry", + "America/Guadeloupe", + "Africa/Malabo", + "Europe/Athens", + "Atlantic/South_Georgia", + "America/Guatemala", + "Pacific/Guam", + "Africa/Bissau", + "America/Guyana", + "Asia/Hong_Kong", + "America/Tegucigalpa", + "Europe/Zagreb", + "America/Port-au-Prince", + "Europe/Budapest", + "Asia/Jakarta", + "Asia/Pontianak", + "Asia/Makassar", + "Asia/Jayapura", + "Europe/Dublin", + "Asia/Jerusalem", + "Europe/Isle_of_Man", + "Asia/Kolkata", + "Indian/Chagos", + "Asia/Baghdad", + "Asia/Tehran", + "Atlantic/Reykjavik", + "Europe/Rome", + "Europe/Jersey", + "America/Jamaica", + "Asia/Amman", + "Asia/Tokyo", + "Africa/Nairobi", + "Asia/Bishkek", + "Asia/Phnom_Penh", + "Pacific/Tarawa", + "Pacific/Enderbury", + "Pacific/Kiritimati", + "Indian/Comoro", + "America/St_Kitts", + "Asia/Pyongyang", + "Asia/Seoul", + "Asia/Kuwait", + "America/Cayman", + "Asia/Almaty", + "Asia/Qyzylorda", + "Asia/Aqtobe", + "Asia/Aqtau", + "Asia/Oral", + "Asia/Vientiane", + "Asia/Beirut", + "America/St_Lucia", + "Europe/Vaduz", + "Asia/Colombo", + "Africa/Monrovia", + "Africa/Maseru", + "Europe/Vilnius", + "Europe/Luxembourg", + "Europe/Riga", + "Africa/Tripoli", + "Africa/Casablanca", + "Europe/Monaco", + "Europe/Chisinau", + "Europe/Podgorica", + "America/Marigot", + "Indian/Antananarivo", + "Pacific/Majuro", + "Pacific/Kwajalein", + "Europe/Skopje", + "Africa/Bamako", + "Asia/Rangoon", + "Asia/Ulaanbaatar", + "Asia/Hovd", + "Asia/Choibalsan", + "Asia/Macau", + "Pacific/Saipan", + "America/Martinique", + "Africa/Nouakchott", + "America/Montserrat", + "Europe/Malta", + "Indian/Mauritius", + "Indian/Maldives", + "Africa/Blantyre", + "America/Mexico_City", + "America/Cancun", + "America/Merida", + "America/Monterrey", + "America/Matamoros", + "America/Mazatlan", + "America/Chihuahua", + "America/Ojinaga", + "America/Hermosillo", + "America/Tijuana", + "America/Santa_Isabel", + "America/Bahia_Banderas", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Africa/Maputo", + "Africa/Windhoek", + "Pacific/Noumea", + "Africa/Niamey", + "Pacific/Norfolk", + "Africa/Lagos", + "America/Managua", + "Europe/Amsterdam", + "Europe/Oslo", + "Asia/Kathmandu", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Auckland", + "Pacific/Chatham", + "Asia/Muscat", + "America/Panama", + "America/Lima", + "Pacific/Tahiti", + "Pacific/Marquesas", + "Pacific/Gambier", + "Pacific/Port_Moresby", + "Asia/Manila", + "Asia/Karachi", + "Europe/Warsaw", + "America/Miquelon", + "Pacific/Pitcairn", + "America/Puerto_Rico", + "Asia/Gaza", + "Asia/Hebron", + "Europe/Lisbon", + "Atlantic/Madeira", + "Atlantic/Azores", + "Pacific/Palau", + "America/Asuncion", + "Asia/Qatar", + "Indian/Reunion", + "Europe/Bucharest", + "Europe/Belgrade", + "Europe/Kaliningrad", + "Europe/Moscow", + "Europe/Volgograd", + "Europe/Samara", + "Asia/Yekaterinburg", + "Asia/Omsk", + "Asia/Novosibirsk", + "Asia/Novokuznetsk", + "Asia/Krasnoyarsk", + "Asia/Irkutsk", + "Asia/Yakutsk", + "Asia/Khandyga", + "Asia/Vladivostok", + "Asia/Sakhalin", + "Asia/Ust-Nera", + "Asia/Magadan", + "Asia/Kamchatka", + "Asia/Anadyr", + "Africa/Kigali", + "Asia/Riyadh", + "Pacific/Guadalcanal", + "Indian/Mahe", + "Africa/Khartoum", + "Europe/Stockholm", + "Asia/Singapore", + "Atlantic/St_Helena", + "Europe/Ljubljana", + "Arctic/Longyearbyen", + "Europe/Bratislava", + "Africa/Freetown", + "Europe/San_Marino", + "Africa/Dakar", + "Africa/Mogadishu", + "America/Paramaribo", + "Africa/Juba", + "Africa/Sao_Tome", + "America/El_Salvador", + "America/Lower_Princes", + "Asia/Damascus", + "Africa/Mbabane", + "America/Grand_Turk", + "Africa/Ndjamena", + "Indian/Kerguelen", + "Africa/Lome", + "Asia/Bangkok", + "Asia/Dushanbe", + "Pacific/Fakaofo", + "Asia/Dili", + "Asia/Ashgabat", + "Africa/Tunis", + "Pacific/Tongatapu", + "Europe/Istanbul", + "America/Port_of_Spain", + "Pacific/Funafuti", + "Asia/Taipei", + "Africa/Dar_es_Salaam", + "Europe/Kiev", + "Europe/Uzhgorod", + "Europe/Zaporozhye", + "Europe/Simferopol", + "Africa/Kampala", + "Pacific/Johnston", + "Pacific/Midway", + "Pacific/Wake", + "America/New_York", + "America/Detroit", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Indiana/Indianapolis", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Vevay", + "America/Chicago", + "America/Indiana/Tell_City", + "America/Indiana/Knox", + "America/Menominee", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/North_Dakota/Beulah", + "America/Denver", + "America/Boise", + "America/Phoenix", + "America/Los_Angeles", + "America/Anchorage", + "America/Juneau", + "America/Sitka", + "America/Yakutat", + "America/Nome", + "America/Adak", + "America/Metlakatla", + "Pacific/Honolulu", + "America/Montevideo", + "Asia/Samarkand", + "Asia/Tashkent", + "Europe/Vatican", + "America/St_Vincent", + "America/Caracas", + "America/Tortola", + "America/St_Thomas", + "Asia/Ho_Chi_Minh", + "Pacific/Efate", + "Pacific/Wallis", + "Pacific/Apia", + "Asia/Aden", + "Indian/Mayotte", + "Africa/Johannesburg", + "Africa/Lusaka", + "Africa/Harare", + "UTC", +] From 2d5f12313ca3340d638cab154d69e4584316e3b2 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 13:52:14 +0100 Subject: [PATCH 15/21] Add Banner Model --- .../feature_profile/sdwan/system/banner.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py index 7e51b157d..6e84fe0cd 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -1,9 +1,17 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from typing import Literal, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase class BannerParcel(_ParcelBase): type_: Literal["banner"] = Field(default="banner", exclude=True) + + model_config = ConfigDict( + extra="forbid", + ) + login: Union[Variable, Global[str], Default[Literal[""]]] = Field(default="") + motd: Union[Variable, Global[str], Default[Literal[""]]] = Field(default="") From 626f8810ee3ae492f951ba58f54ea86c64902b61 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 13:59:04 +0100 Subject: [PATCH 16/21] Add MRF model --- .../feature_profile/sdwan/system/mrf.py | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py index a24390530..ed084bad1 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -1,9 +1,70 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from typing import List, Literal, Optional, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase + +EnableMrfMigration = Literal["enabled", "enabled-from-bgp-core"] +Role = Literal["edge-router", "border-router"] + + +class ManagementRegion(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + vrf_id: Optional[Union[Global[int], Default[None], Variable]] = Field( + None, serialization_alias="vrfId", validation_alias="vrfId", description="VRF name for management region" + ) + gateway_preference: Optional[Union[Global[List[int]], Default[None], Variable]] = Field( + None, + serialization_alias="gatewayPreference", + validation_alias="gatewayPreference", + description="List of affinity group preferences for VRF", + ) + management_gateway: Optional[Union[Global[bool], Default[Literal[False]], Variable]] = Field( + None, + serialization_alias="managementGateway", + validation_alias="managementGateway", + description="Enable management gateway", + ) class MRFParcel(_ParcelBase): type_: Literal["mrf"] = Field(default="mrf", exclude=True) + + model_config = ConfigDict( + extra="forbid", + ) + secondary_region: Optional[Union[Global[int], Variable, Default[None]]] = Field( + None, + serialization_alias="secondaryRegion", + validation_alias="secondaryRegion", + description="Set secondary region ID", + ) + role: Optional[Union[Global[Role], Variable, Default[None]]] = Field(None, description="Set the role for router") + enable_mrf_migration: Optional[Union[Global[EnableMrfMigration], Default[None]]] = Field( + None, + serialization_alias="enableMrfMigration", + validation_alias="enableMrfMigration", + description="Enable migration mode to Multi-Region Fabric", + ) + migration_bgp_community: Optional[Union[Global[int], Default[None]]] = Field( + None, + serialization_alias="migrationBgpCommunity", + validation_alias="migrationBgpCommunity", + description="Set BGP community during migration from BGP-core based network", + ) + enable_management_region: Optional[Union[Global[bool], Default[Literal[False]], Variable]] = Field( + None, + serialization_alias="enableManagementRegion", + validation_alias="enableManagementRegion", + description="Enable management region", + ) + management_region: Optional[ManagementRegion] = Field( + None, + serialization_alias="managementRegion", + validation_alias="managementRegion", + description="Management Region", + ) From ec376922da94cadee7b26f3a30e9cf0033e1d2c1 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 14:15:34 +0100 Subject: [PATCH 17/21] Add ntp model. --- .../feature_profile/sdwan/system/basic.py | 32 +++---- .../feature_profile/sdwan/system/ntp.py | 84 ++++++++++++++++++- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py index 8a51ec44c..e82c05447 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default from catalystwan.utils.timezone import Timezone ConsoleBaudRate = Literal["1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"] @@ -18,7 +18,7 @@ class Clock(BaseModel): timezone: Union[Variable, Global[Timezone], Default[DefaultTimezone]] = Field( - default="UTC", description="Set the timezone" + default=as_default("UTC"), description="Set the timezone" ) @@ -28,7 +28,7 @@ class MobileNumberItem(BaseModel): class Sms(BaseModel): enable: Optional[Union[Global[bool], Default[Literal[False]]]] = Field( - default=False, description="Global[bool] device’s geo fencing SMS" + None, description="Global[bool] device’s geo fencing SMS" ) mobile_number: Optional[List[MobileNumberItem]] = Field( None, @@ -48,10 +48,10 @@ class GeoFencing(BaseModel): class GpsVariable(BaseModel): longitude: Union[Variable, Global[float], Default[None]] = Field( - default=None, description="Set the device physical longitude" + default=as_default(None), description="Set the device physical longitude" ) latitude: Union[Variable, Global[float], Default[None]] = Field( - default=None, description="Set the device physical latitude" + default=as_default(None), description="Set the device physical latitude" ) geo_fencing: Optional[GeoFencing] = Field( None, @@ -72,7 +72,7 @@ class OnDemand(BaseModel): Global[int], Default[int], ] = Field( - default=10, + default=as_default(10), serialization_alias="onDemandVariable", validation_alias="onDemandVariable", description="Set the idle timeout for on-demand tunnels", @@ -88,13 +88,13 @@ class AffinityPerVrfItem(BaseModel): Global[int], Default[None], ] = Field( - default=None, + default=as_default(None), serialization_alias="affinityGroupNumber", validation_alias="affinityGroupNumber", description="Affinity Group Number", ) vrf_range: Union[Variable, Global[str], Default[None]] = Field( - default=None, + default=as_default(None), serialization_alias="vrfRange", validation_alias="vrfRange", description="Range of VRFs", @@ -109,10 +109,10 @@ class BasicParcel(_ParcelBase): ) clock: Clock description: Union[Variable, Global[str], Default[None]] = Field( - default=None, description="Set a text description of the device" + default=as_default(None), description="Set a text description of the device" ) location: Union[Variable, Global[str], Default[None]] = Field( - default=None, description="Set the location of the device" + default=as_default(None), description="Set the location of the device" ) gps_location: GpsVariable = Field( ..., @@ -120,7 +120,7 @@ class BasicParcel(_ParcelBase): validation_alias="gpsVariable", ) device_groups: Union[Variable, Global[List[str]], Default[None]] = Field( - default=None, + default=as_default(None), serialization_alias="deviceGroups", validation_alias="deviceGroups", description="Device groups", @@ -138,13 +138,13 @@ class BasicParcel(_ParcelBase): description="Configure a list of comma-separated controller groups", ) overlay_id: Union[Variable, Global[int], Default[int]] = Field( - default=1, + default=as_default(1), serialization_alias="overlayId", validation_alias="overlayId", description="Set the Overlay ID", ) port_offset: Union[Variable, Global[int], Default[int]] = Field( - default=0, + default=as_default(0), serialization_alias="portOffset", validation_alias="portOffset", description="Set the TLOC port offset when multiple devices are behind a NAT", @@ -174,13 +174,13 @@ class BasicParcel(_ParcelBase): description="OMP Tag attached to routes based on interface tracking", ) console_baud_rate: Union[Variable, Global[ConsoleBaudRate], Default[DefaultConsoleBaudRate]] = Field( - default="9600", + default=as_default("9600"), serialization_alias="consoleBaudRate", validation_alias="consoleBaudRate", description="Set the console baud rate", ) max_omp_sessions: Union[Variable, Global[int], Default[None]] = Field( - default=None, + default=as_default(None), serialization_alias="maxOmpSessions", validation_alias="maxOmpSessions", description="Set the maximum number of OMP sessions <1..100> the device can have", @@ -216,7 +216,7 @@ class BasicParcel(_ParcelBase): description="Enable or disable endpoint tracker diaStabilize status", ) admin_tech_on_failure: Union[Variable, Global[bool], Default[Literal[True]]] = Field( - default=True, + default=as_default(True), serialization_alias="adminTechOnFailure", validation_alias="adminTechOnFailure", description="Collect admin-tech before reboot due to daemon failure", diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py index 466a7b939..76d108968 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -1,9 +1,87 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from ipaddress import IPv6Address +from typing import List, Literal, Optional, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default + + +class ServerItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + name: Union[Variable, Global[IPv6Address]] = Field(..., description="Set hostname or IP address of server") + key: Optional[Union[Variable, Global[int], Default[None]]] = Field( + None, description="Set authentication key for the server" + ) + vpn: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(0), description="Set VPN in which NTP server is located" + ) + version: Union[Variable, Global[int], Default[int]] = Field(..., description="Set NTP version") + source_interface: Optional[Union[Variable, Global[str], Default[None]]] = Field( + None, + serialization_alias="sourceInterface", + validation_alias="sourceInterface", + description="Set interface to use to reach NTP server", + ) + prefer: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), description="Variable this NTP server" + ) + + +class AuthenticationVariable(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + key_id: Union[Variable, Global[int]] = Field( + ..., serialization_alias="keyId", validation_alias="keyId", description="MD5 authentication key ID" + ) + md5_value: Union[Variable, Global[str]] = Field( + ..., + alias="md5Value", + description="Enter cleartext or AES-encrypted MD5 authentication key" + "[Note: Catalyst SD-WAN Manager will encrypt this field before saving." + "Cleartext strings will not be returned back to the user in GET responses for sensitive fields.]", + ) + + +class Authentication(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + authentication_keys: List[AuthenticationVariable] = Field( + ..., + serialization_alias="authenticationVariables", + validation_alias="authenticationVariables", + description="Set MD5 authentication key", + ) + trusted_keys: Optional[Union[Variable, Global[List[int]], Default[None]]] = Field( + None, + serialization_alias="trustedVariables", + validation_alias="trustedVariables", + description="Designate authentication key as trustworthy", + ) + + +class Leader(BaseModel): + enable: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + None, description="Variable device as NTP Leader" + ) + stratum: Optional[Union[Variable, Global[int], Default[None]]] = Field( + None, description="Variable device as NTP Leader" + ) + source: Optional[Union[Variable, Global[str], Default[None]]] = Field( + None, description="Variable device as NTP Leader" + ) class NTPParcel(_ParcelBase): type_: Literal["ntp"] = Field(default="ntp", exclude=True) + model_config = ConfigDict( + extra="forbid", + ) + server: List[ServerItem] = Field(..., description="Configure NTP servers") + authentication: Optional[Authentication] = None + leader: Optional[Leader] = None From 79c46be136c66c5001dfa2b4e492d2917cae2710 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Wed, 28 Feb 2024 18:32:32 +0100 Subject: [PATCH 18/21] All models --- .../feature_profile/sdwan/system/banner.py | 14 +- .../feature_profile/sdwan/system/basic.py | 102 +++++------ .../sdwan/system/global_parcel.py | 135 +++++++++++++- .../sdwan/system/logging_parcel.py | 5 +- .../feature_profile/sdwan/system/mrf.py | 23 ++- .../feature_profile/sdwan/system/ntp.py | 4 + .../feature_profile/sdwan/system/omp.py | 121 ++++++++++++- .../feature_profile/sdwan/system/security.py | 171 +++++++++++++++++- .../feature_profile/sdwan/system/snmp.py | 167 ++++++++++++++++- 9 files changed, 658 insertions(+), 84 deletions(-) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py index 6e84fe0cd..0c1df2d99 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -2,16 +2,18 @@ from typing import Literal, Union -from pydantic import ConfigDict, Field +from pydantic import AliasPath, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default class BannerParcel(_ParcelBase): type_: Literal["banner"] = Field(default="banner", exclude=True) - model_config = ConfigDict( - extra="forbid", + model_config = ConfigDict(extra="forbid", populate_by_name=True) + login: Union[Variable, Global[str], Default[Literal[""]]] = Field( + default=as_default(""), validation_alias=AliasPath("data", "login") + ) + motd: Union[Variable, Global[str], Default[Literal[""]]] = Field( + default=as_default(""), validation_alias=AliasPath("data", "motd") ) - login: Union[Variable, Global[str], Default[Literal[""]]] = Field(default="") - motd: Union[Variable, Global[str], Default[Literal[""]]] = Field(default="") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py index e82c05447..fbfeec15d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -2,7 +2,7 @@ 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, _ParcelBase, as_default from catalystwan.utils.timezone import Timezone @@ -27,6 +27,10 @@ class MobileNumberItem(BaseModel): class Sms(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) enable: Optional[Union[Global[bool], Default[Literal[False]]]] = Field( None, description="Global[bool] device’s geo fencing SMS" ) @@ -47,6 +51,10 @@ class GeoFencing(BaseModel): class GpsVariable(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) longitude: Union[Variable, Global[float], Default[None]] = Field( default=as_default(None), description="Set the device physical longitude" ) @@ -61,8 +69,12 @@ class GpsVariable(BaseModel): class OnDemand(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) on_demand_enable: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=False, + default=as_default(False), serialization_alias="onDemandEnable", validation_alias="onDemandEnable", description="Enable or disable On-demand Tunnel", @@ -82,6 +94,7 @@ class OnDemand(BaseModel): class AffinityPerVrfItem(BaseModel): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) affinity_group_number: Union[ Variable, @@ -106,23 +119,26 @@ class BasicParcel(_ParcelBase): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) - clock: Clock + clock: Clock = Field(validation_alias=AliasPath("data", "clock")) description: Union[Variable, Global[str], Default[None]] = Field( - default=as_default(None), description="Set a text description of the device" + default=as_default(None), + validation_alias=AliasPath("data", "description"), + description="Set a text description of the device", ) location: Union[Variable, Global[str], Default[None]] = Field( - default=as_default(None), description="Set the location of the device" + default=as_default(None), + validation_alias=AliasPath("data", "location"), + description="Set the location of the device", ) gps_location: GpsVariable = Field( ..., - serialization_alias="gpsVariable", - validation_alias="gpsVariable", + validation_alias=AliasPath("data", "gpsVariable"), ) device_groups: Union[Variable, Global[List[str]], Default[None]] = Field( default=as_default(None), - serialization_alias="deviceGroups", - validation_alias="deviceGroups", + validation_alias=AliasPath("data", "deviceGroups"), description="Device groups", ) controller_group_list: Optional[ @@ -133,62 +149,52 @@ class BasicParcel(_ParcelBase): ] ] = Field( None, - serialization_alias="controllerGroupList", - validation_alias="controllerGroupList", + validation_alias=AliasPath("data", "controllerGroupList"), description="Configure a list of comma-separated controller groups", ) overlay_id: Union[Variable, Global[int], Default[int]] = Field( default=as_default(1), - serialization_alias="overlayId", - validation_alias="overlayId", + validation_alias=AliasPath("data", "overlayId"), description="Set the Overlay ID", ) port_offset: Union[Variable, Global[int], Default[int]] = Field( default=as_default(0), - serialization_alias="portOffset", - validation_alias="portOffset", + validation_alias=AliasPath("data", "portOffset"), description="Set the TLOC port offset when multiple devices are behind a NAT", ) port_hop: Union[Variable, Global[bool], Default[Literal[True]]] = Field( default=True, - serialization_alias="portHop", - validation_alias="portHop", + validation_alias=AliasPath("data", "portHop"), description="Enable port hopping", ) control_session_pps: Optional[Union[Variable, Global[int], Default[int]]] = Field( None, - serialization_alias="controlSessionPps", - validation_alias="controlSessionPps", + validation_alias=AliasPath("data", "controlSessionPps"), description="Set the policer rate for control sessions", ) track_transport: Optional[Union[Variable, Global[bool], Default[Literal[True]]]] = Field( None, - serialization_alias="trackTransport", - validation_alias="trackTransport", + validation_alias=AliasPath("data", "trackTransport"), description="Configure tracking of transport", ) track_interface_tag: Optional[Union[Variable, Global[int], Default[None]]] = Field( None, - serialization_alias="trackInterfaceTag", - validation_alias="trackInterfaceTag", + validation_alias=AliasPath("data", "trackInterfaceTag"), description="OMP Tag attached to routes based on interface tracking", ) console_baud_rate: Union[Variable, Global[ConsoleBaudRate], Default[DefaultConsoleBaudRate]] = Field( default=as_default("9600"), - serialization_alias="consoleBaudRate", - validation_alias="consoleBaudRate", + validation_alias=AliasPath("data", "consoleBaudRate"), description="Set the console baud rate", ) max_omp_sessions: Union[Variable, Global[int], Default[None]] = Field( default=as_default(None), - serialization_alias="maxOmpSessions", - validation_alias="maxOmpSessions", + validation_alias=AliasPath("data", "maxOmpSessions"), description="Set the maximum number of OMP sessions <1..100> the device can have", ) multi_tenant: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( None, - serialization_alias="multiTenant", - validation_alias="multiTenant", + validation_alias=AliasPath("data", "multiTenant"), description="Device is multi-tenant", ) track_default_gateway: Optional[ @@ -199,8 +205,7 @@ class BasicParcel(_ParcelBase): ] ] = Field( None, - serialization_alias="trackDefaultGateway", - validation_alias="trackDefaultGateway", + validation_alias=AliasPath("data", "trackDefaultGateway"), description="Enable or disable default gateway tracking", ) tracker_dia_stabilize_status: Optional[ @@ -211,41 +216,36 @@ class BasicParcel(_ParcelBase): ] ] = Field( None, - serialization_alias="trackerDiaStabilizeStatus", - validation_alias="trackerDiaStabilizeStatus", + validation_alias=AliasPath("data", "trackerDiaStabilizeStatus"), description="Enable or disable endpoint tracker diaStabilize status", ) admin_tech_on_failure: Union[Variable, Global[bool], Default[Literal[True]]] = Field( default=as_default(True), - serialization_alias="adminTechOnFailure", - validation_alias="adminTechOnFailure", + validation_alias=AliasPath("data", "adminTechOnFailure"), description="Collect admin-tech before reboot due to daemon failure", ) idle_timeout: Optional[Union[Variable, Global[int], Default[None]]] = Field( None, - serialization_alias="idleTimeout", - validation_alias="idleTimeout", + validation_alias=AliasPath("data", "idleTimeout"), description="Idle CLI timeout in minutes", ) on_demand: OnDemand = Field( ..., - serialization_alias="onDemand", - validation_alias="onDemand", + validation_alias=AliasPath("data", "onDemand"), ) transport_gateway: Optional[Union[Global[bool], Variable, Default[Literal[False]]]] = Field( None, - serialization_alias="transportGateway", - validation_alias="transportGateway", + validation_alias=AliasPath("data", "transportGateway"), description="Enable transport gateway", ) epfr: Optional[Union[Global[Epfr], Default[DefaultEpfr], Variable]] = Field( None, + validation_alias=AliasPath("data", "epfr"), description="Enable SLA Dampening and Enhanced App Routing.", ) site_type: Optional[Union[Variable, Global[List[SiteType]], Default[None]]] = Field( None, - serialization_alias="siteType", - validation_alias="siteType", + validation_alias=AliasPath("data", "siteType"), description="Site Type", ) affinity_group_number: Optional[ @@ -256,9 +256,8 @@ class BasicParcel(_ParcelBase): ] ] = Field( None, - serialization_alias="affinityGroupGlobal[str]", - validation_alias="affinityGroupGlobal[str]", - description="Affinity Group Global[str]", + validation_alias=AliasPath("data", "affinityGroupNumber"), + description="Affinity Group Number", ) affinity_group_preference: Optional[ Union[ @@ -268,8 +267,7 @@ class BasicParcel(_ParcelBase): ] ] = Field( None, - serialization_alias="affinityGroupPreference", - validation_alias="affinityGroupPreference", + validation_alias=AliasPath("data", "affinityGroupPreference"), description="Affinity Group Preference", ) affinity_preference_auto: Optional[ @@ -280,15 +278,13 @@ class BasicParcel(_ParcelBase): ] ] = Field( None, - serialization_alias="affinityPreferenceAuto", - validation_alias="affinityPreferenceAuto", + validation_alias=AliasPath("data", "affinityPreferenceAuto"), description="Affinity Group Preference Auto", ) affinity_per_vrf: Optional[List[AffinityPerVrfItem]] = Field( None, - serialization_alias="affinityPerVrf", - validation_alias="affinityPerVrf", - description="Affinity Group Global[str] for VRFs", + validation_alias=AliasPath("data", "affinityPerVrf"), + description="Affinity Group Range for VRFs", max_length=4, min_length=0, ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py index ac977e70f..cc0fce868 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py @@ -1,9 +1,138 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from typing import Literal, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default + + +class ServicesIp(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + http_server: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpHttpServer", + validation_alias="servicesGlobalServicesIpHttpServer", + ) + https_server: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpHttpsServer", + validation_alias="servicesGlobalServicesIpHttpsServer", + ) + ftp_passive: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpFtpPassive", + validation_alias="servicesGlobalServicesIpFtpPassive", + ) + domain_lookup: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpDomainLookup", + validation_alias="servicesGlobalServicesIpDomainLookup", + ) + arp_proxy: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpArpProxy", + validation_alias="servicesGlobalServicesIpArpProxy", + ) + rcmd: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpRcmd", + validation_alias="servicesGlobalServicesIpRcmd", + ) + line_vty: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpLineVty", + validation_alias="servicesGlobalServicesIpLineVty", + ) + cdp: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), + serialization_alias="servicesGlobalServicesIpCdp", + validation_alias="servicesGlobalServicesIpCdp", + ) + lldp: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), + serialization_alias="servicesGlobalServicesIpLldp", + validation_alias="servicesGlobalServicesIpLldp", + ) + source_intrf: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpSourceIntrf", + validation_alias="servicesGlobalServicesIpSourceIntrf", + ) + tcp_keepalives_in: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsTcpKeepalivesIn", + validation_alias="globalOtherSettingsTcpKeepalivesIn", + ) + keepalives_out: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsTcpKeepalivesOut", + validation_alias="globalOtherSettingsTcpKeepalivesOut", + ) + small_servers: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalOtherSettingsTcpSmallServers", + validation_alias="globalOtherSettingsTcpSmallServers", + ) + udp_small_servers: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalOtherSettingsUdpSmallServers", + validation_alias="globalOtherSettingsUdpSmallServers", + ) + console_logging: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsConsoleLogging", + validation_alias="globalOtherSettingsConsoleLogging", + ) + ip_source_route: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalOtherSettingsIPSourceRoute", + validation_alias="globalOtherSettingsIPSourceRoute", + ) + vty_line_logging: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalOtherSettingsVtyLineLogging", + validation_alias="globalOtherSettingsVtyLineLogging", + ) + snmp_ifindex_persist: (Union[Variable, Global[bool], Default[Literal[True]]]) = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsSnmpIfindexPersist", + validation_alias="globalOtherSettingsSnmpIfindexPersist", + ) + ignore_bootp: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsIgnoreBootp", + validation_alias="globalOtherSettingsIgnoreBootp", + ) + nat64_udp_timeout: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalSettingsNat64UdpTimeout", + validation_alias="globalSettingsNat64UdpTimeout", + ) + nat64_tcp_timeout: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalSettingsNat64TcpTimeout", + validation_alias="globalSettingsNat64TcpTimeout", + ) + http_authentication: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalSettingsHttpAuthentication", + validation_alias="globalSettingsHttpAuthentication", + ) + ssh_version: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), + serialization_alias="globalSettingsSSHVersion", + validation_alias="globalSettingsSSHVersion", + ) class GlobalParcel(_ParcelBase): type_: Literal["global"] = Field(default="global", exclude=True) + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + services_ip: ServicesIp # Key format is snake case in schema diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index 6a0ab03ed..1b4abd236 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -77,7 +77,10 @@ class Disk(BaseModel): class LoggingParcel(_ParcelBase): type_: Literal["logging"] = Field(default="logging", exclude=True) - + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) tls_profile: Optional[List[TlsProfile]] = Field(default=[], validation_alias=AliasPath("data", "tlsProfile")) server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "server")) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py index ed084bad1..7da607605 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -2,7 +2,7 @@ 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, _ParcelBase @@ -13,6 +13,7 @@ class ManagementRegion(BaseModel): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) vrf_id: Optional[Union[Global[int], Default[None], Variable]] = Field( None, serialization_alias="vrfId", validation_alias="vrfId", description="VRF name for management region" @@ -36,35 +37,33 @@ class MRFParcel(_ParcelBase): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) secondary_region: Optional[Union[Global[int], Variable, Default[None]]] = Field( None, - serialization_alias="secondaryRegion", - validation_alias="secondaryRegion", + validation_alias=AliasPath("data", "secondaryRegion"), description="Set secondary region ID", ) - role: Optional[Union[Global[Role], Variable, Default[None]]] = Field(None, description="Set the role for router") + role: Optional[Union[Global[Role], Variable, Default[None]]] = Field( + None, validation_alias=AliasPath("data", "role"), description="Set the role for router" + ) enable_mrf_migration: Optional[Union[Global[EnableMrfMigration], Default[None]]] = Field( None, - serialization_alias="enableMrfMigration", - validation_alias="enableMrfMigration", + validation_alias=AliasPath("data", "enableMrfMigration"), description="Enable migration mode to Multi-Region Fabric", ) migration_bgp_community: Optional[Union[Global[int], Default[None]]] = Field( None, - serialization_alias="migrationBgpCommunity", - validation_alias="migrationBgpCommunity", + validation_alias=AliasPath("data", "migrationBgpCommunity"), description="Set BGP community during migration from BGP-core based network", ) enable_management_region: Optional[Union[Global[bool], Default[Literal[False]], Variable]] = Field( None, - serialization_alias="enableManagementRegion", - validation_alias="enableManagementRegion", + validation_alias=AliasPath("data", "enableManagementRegion"), description="Enable management region", ) management_region: Optional[ManagementRegion] = Field( None, - serialization_alias="managementRegion", - validation_alias="managementRegion", + validation_alias=AliasPath("data", "managementRegion"), description="Management Region", ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py index 76d108968..ebdab47e2 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -11,6 +11,7 @@ class ServerItem(BaseModel): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) name: Union[Variable, Global[IPv6Address]] = Field(..., description="Set hostname or IP address of server") key: Optional[Union[Variable, Global[int], Default[None]]] = Field( @@ -34,6 +35,7 @@ class ServerItem(BaseModel): class AuthenticationVariable(BaseModel): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) key_id: Union[Variable, Global[int]] = Field( ..., serialization_alias="keyId", validation_alias="keyId", description="MD5 authentication key ID" @@ -50,6 +52,7 @@ class AuthenticationVariable(BaseModel): class Authentication(BaseModel): model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) authentication_keys: List[AuthenticationVariable] = Field( ..., @@ -81,6 +84,7 @@ class NTPParcel(_ParcelBase): type_: Literal["ntp"] = Field(default="ntp", exclude=True) model_config = ConfigDict( extra="forbid", + populate_by_name=True, ) server: List[ServerItem] = Field(..., description="Configure NTP servers") authentication: Optional[Authentication] = None diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py index 59c99e553..e83d6590a 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py @@ -1,9 +1,124 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from typing import List, Literal, Optional, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default + +SiteTypesForTransportGateway = Literal["type-1", "type-2", "type-3", "cloud", "branch", "br", "spoke"] +TransportGateway = Literal["prefer", "ecmp-with-direct-path"] + + +class AdvertiseIpv4(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + bgp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="BGP") + ospf: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="OSPF") + ospfv3: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), description="OSPFV3" + ) + connected: Union[Variable, Global[bool], Default[Optional[Literal[True, False]]]] = Field( + default=as_default(False), description="Variable" + ) + static: Union[Variable, Global[bool], Default[Optional[Literal[True, False]]]] = Field( + default=as_default(False), description="Variable" + ) + eigrp: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), description="EIGRP" + ) + lisp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="LISP") + isis: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="ISIS") + + +class AdvertiseIpv6(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + bgp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="BGP") + ospf: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="OSPF") + connected: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), description="Variable" + ) + static: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), description="Variable" + ) + eigrp: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), description="EIGRP" + ) + lisp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="LISP") + isis: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="ISIS") class OMPParcel(_ParcelBase): type_: Literal["omp"] = Field(default="omp", exclude=True) + + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + graceful_restart: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), + validation_alias=AliasPath("data", "gracefulRestart"), + description="Graceful Restart for OMP", + ) + overlay_as: Union[Variable, Global[float], Default[None]] = Field( + default=as_default(None), validation_alias=AliasPath("data", "overlayAs"), description="Overlay AS Number" + ) + send_path_limit: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(4), + validation_alias=AliasPath("data", "sendPathLimit"), + description="Number of Paths Advertised per Prefix", + ) + ecmp_limit: Union[Variable, Global[float], Default[int]] = Field( + default=as_default(4), + validation_alias=AliasPath("data", "ecmpLimit"), + description="Set maximum number of OMP paths to install in cEdge route table", + ) + shutdown: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + default=as_default(False), validation_alias=AliasPath("data", "shutdown"), description="Variable" + ) + omp_admin_distance_ipv4: Union[Variable, Global[int], Default[Optional[int]]] = Field( + default=as_default(251), + validation_alias=AliasPath("data", "ompAdminDistanceIpv4"), + description="OMP Admin Distance IPv4", + ) + omp_admin_distance_ipv6: Union[Variable, Global[int], Default[Optional[int]]] = Field( + default=as_default(251), + validation_alias=AliasPath("data", "ompAdminDistanceIpv6"), + description="OMP Admin Distance IPv6", + ) + advertisement_interval: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(1), + validation_alias=AliasPath("data", "advertisementInterval"), + description="Advertisement Interval (seconds)", + ) + graceful_restart_timer: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(43200), + validation_alias=AliasPath("data", "gracefulRestartTimer"), + description="Graceful Restart Timer (seconds)", + ) + eor_timer: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(300), validation_alias=AliasPath("data", "eorTimer"), description="EOR Timer" + ) + holdtime: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(60), validation_alias=AliasPath("data", "holdtime"), description="Hold Time (seconds)" + ) + advertise_ipv4: AdvertiseIpv4 = Field(..., validation_alias="advertiseIpv4") + advertise_ipv6: AdvertiseIpv6 = Field(..., validation_alias="advertiseIpv6") + ignore_region_path_length: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + None, + validation_alias=AliasPath("data", "ignoreRegionPathLength"), + description="Treat hierarchical and direct (secondary region) paths equally", + ) + transport_gateway: Optional[Union[Variable, Global[TransportGateway], Default[None]]] = Field( + None, validation_alias=AliasPath("data", "transportGateway"), description="Transport Gateway Path Behavior" + ) + site_types_for_transport_gateway: Optional[ + Union[ + Variable, + Global[List[SiteTypesForTransportGateway]], + Default[None], + ] + ] = Field(None, validation_alias=AliasPath("data", "siteTypesForVariable"), description="Site Types") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py index bbd11aeda..c7a7ce7ae 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py @@ -1,9 +1,174 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from typing import List, Literal, Optional, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default + +IntegrityType = Literal["esp", "ip-udp-esp", "none", "ip-udp-esp-no-id"] +ReplayWindow = Literal["64", "128", "256", "512", "1024", "2048", "4096", "8192"] +ReplayWindow2 = Literal["512"] +Tcp = Literal["aes-128-cmac", "hmac-sha-1", "hmac-sha-256"] + + +class KeychainItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + name: Global[str] = Field(..., description="Specify the name of the Keychain") + id: Global[int] = Field(..., description="Specify the Key ID") + + +class OneOfendChoice1(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + infinite: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + default=as_default(True), description="Infinite lifetime" + ) + + +class OneOfendChoice2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + duration: Union[Global[int], Variable] = Field(..., description="Send lifetime Duration (seconds)") + + +class OneOfendChoice3(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + exact: Global[float] = Field(..., description="Configure Key lifetime end time") + + +class SendLifetime(BaseModel): + """ + Send Lifetime Settings + """ + + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + local: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + default=None, description="Configure Send lifetime Local" + ) + start_epoch: Global[float] = Field( + ..., + serialization_alias="startEpoch", + validation_alias="startEpoch", + description="Configure Key lifetime start time", + ) + one_ofend_choice: Optional[Union[OneOfendChoice1, OneOfendChoice2, OneOfendChoice3]] = Field( + default=None, serialization_alias="oneOfendChoice", validation_alias="oneOfendChoice" + ) + + +class AcceptLifetime(BaseModel): + """ + Accept Lifetime Settings + """ + + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + local: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + default=None, description="Configure Send lifetime Local" + ) + start_epoch: Global[float] = Field( + ..., + serialization_alias="startEpoch", + validation_alias="startEpoch", + description="Configure Key lifetime start time", + ) + one_ofend_choice: Optional[Union[OneOfendChoice1, OneOfendChoice2, OneOfendChoice3]] = Field( + default=None, serialization_alias="oneOfendChoice", validation_alias="oneOfendChoice" + ) + + +class KeyItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + id: Global[int] = Field(..., description="Select the Key ID") + name: Global[str] = Field(..., description="Select the chain name") + send_id: Union[Global[int], Variable] = Field( + ..., serialization_alias="recvId", validation_alias="srecvId", description="Specify the Send ID" + ) + recv_id: Union[Global[int], Variable] = Field( + ..., serialization_alias="recvId", validation_alias="recvId", description="Specify the Receiver ID" + ) + include_tcp_options: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + default=None, + serialization_alias="includeTcpOptions", + validation_alias="includeTcpOptions", + description="Configure Include TCP Options", + ) + accept_ao_mismatch: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + default=None, + serialization_alias="acceptAoMismatch", + validation_alias="acceptAoMismatch", + description="Configure Accept AO Mismatch", + ) + tcp: Global[Tcp] = Field(..., description="Crypto Algorithm") + key_string: Union[Global[str], Variable] = Field( + ..., + serialization_alias="keyString", + validation_alias="keyString", + description="Specify the Key String [Note: Catalyst SD-WAN Manager will encrypt this field before saving." + "Cleartext strings will not be returned back to the user in GET responses for sensitive fields.]", + ) + send_lifetime: Optional[SendLifetime] = Field( + default=None, + serialization_alias="sendLifetime", + validation_alias="sendLifetime", + description="Send Lifetime Settings", + ) + accept_lifetime: Optional[AcceptLifetime] = Field( + default=None, + serialization_alias="acceptLifetime", + validation_alias="acceptLifetime", + description="Accept Lifetime Settings", + ) class SecurityParcel(_ParcelBase): type_: Literal["security"] = Field(default="security", exclude=True) + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + rekey: Optional[Union[Global[int], Variable, Default[int]]] = Field( + default=None, + description="Set how often to change the AES key for DTLS connections", + ) + replay_window: Optional[Union[Global[ReplayWindow], Variable, Default[ReplayWindow2]]] = Field( + default=None, + validation_alias=AliasPath("data", "replayWindow"), + description="Set the sliding replay window size", + ) + extended_ar_window: Optional[Union[Global[int], Variable, Default[int]]] = Field( + default=None, + validation_alias=AliasPath("data", "extendedArWindow"), + description="Extended Anti-Replay Window", + ) + integrity_type: Optional[Union[Global[List[IntegrityType]], Variable]] = Field( + default=None, + validation_alias=AliasPath("data", "integrityType"), + description="Set the authentication type for DTLS connections", + ) + pairwise_keying: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + default=None, + validation_alias=AliasPath("data", "pairwiseKeying"), + description="Enable or disable IPsec pairwise-keying", + ) + keychain: Optional[List[KeychainItem]] = Field(default=None, description="Configure a Keychain") + key: Optional[List[KeyItem]] = Field(default=None, description="Configure a Key") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py index 34967879b..ba0ec1caf 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py @@ -1,9 +1,170 @@ -from typing import Literal +from __future__ import annotations -from pydantic import Field +from ipaddress import IPv4Address, IPv6Address +from typing import List, Literal, Optional, Union -from catalystwan.api.configuration_groups.parcel import _ParcelBase +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase + +Authorization = Literal["read-only", "read-write"] +Priv = Literal["aes-cfb-128", "aes-256-cfb-128"] +SecurityLevel = Literal["no-auth-no-priv", "auth-no-priv", "auth-priv"] + + +class OidItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + id: Union[Global[str], Variable] = Field(..., description="Configure identifier of subtree of MIB objects") + exclude: Optional[Union[Global[bool], Variable]] = Field(default=None, description="Exclude the OID") + + +class ViewItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + name: Global[str] = Field(..., description="Set the name of the SNMP view") + oid: Optional[List[OidItem]] = Field(default=None, description="Configure SNMP object identifier") + + +class CommunityItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + name: Global[str] = Field( + ..., + description="Set name of the SNMP community" + "[Note: Catalyst SD-WAN Manager will encrypt this field before saving." + "Cleartext strings will not be returned back to the user in GET responses for sensitive fields.]", + ) + user_label: Optional[Global[str]] = Field( + default=None, + serialization_alias="userLabel", + validation_alias="userLabel", + description="Set user label of the SNMP community", + ) + view: Union[Global[str], Variable] = Field(..., description="Set name of the SNMP view") + authorization: Optional[Union[Global[Authorization], Variable]] = Field( + default=None, description="Configure access permissions" + ) + + +class GroupItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + name: Global[str] = Field(..., description="Name of the SNMP group") + security_level: Global[SecurityLevel] = Field( + ..., + serialization_alias="securityLevel", + validation_alias="securityLevel", + description="Configure security level", + ) + view: Union[Global[str], Variable] = Field(..., description="Name of the SNMP view") + + +class UserItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + name: Global[str] = Field(..., description="Name of the SNMP user") + auth: Optional[Union[Global[Literal["sha"]], Variable, Default[None]]] = Field( + default=None, description="Configure authentication protocol" + ) + auth_password: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=None, + serialization_alias="authPassword", + validation_alias="authPassword", + description="Specify authentication protocol password", + ) + priv: Optional[Union[Global[Priv], Variable, Default[None]]] = Field( + default=None, description="Configure privacy protocol" + ) + priv_password: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=None, + serialization_alias="privPassword", + validation_alias="privPassword", + description="Specify privacy protocol password", + ) + group: Union[Global[str], Variable] = Field(..., description="Name of the SNMP group") + + +class TargetItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + vpn_id: Union[Global[int], Variable] = Field( + ..., + serialization_alias="vpnId", + validation_alias="vpnId", + description="Set VPN in which SNMP server is located", + ) + ip: Union[Global[Union[IPv4Address, IPv6Address]], Variable] = Field( + ..., description="Set IPv4/IPv6 address of SNMP server" + ) + port: Union[Global[int], Variable] = Field(..., description="Set UDP port number to connect to SNMP server") + user_label: Optional[Global[str]] = Field( + default=None, + serialization_alias="userLabel", + validation_alias="userLabel", + description="Set user label of the SNMP community", + ) + community_name: Optional[Union[Global[str], Variable]] = Field( + default=None, + serialization_alias="communityName", + validation_alias="communityName", + description="Set name of the SNMP community" + "[Note: Catalyst SD-WAN Manager will encrypt this field before saving." + "Cleartext strings will not be returned back to the user in GET responses for sensitive fields.]." + "DEPRECATED. Use userLabel field instead", + ) + user: Optional[Union[Global[str], Variable]] = Field(default=None, description="Set name of the SNMP user") + source_interface: Optional[Union[Global[str], Variable]] = Field( + default=None, + serialization_alias="sourceInterface", + validation_alias="sourceInterface", + description="Source interface for outgoing SNMP traps", + ) class SNMPParcel(_ParcelBase): type_: Literal["snmp"] = Field(default="snmp", exclude=True) + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + shutdown: Optional[Union[Global[bool], Variable]] = Field( + default=None, validation_alias=AliasPath("data", "shutdown"), description="Enable or disable SNMP" + ) + contact: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=None, validation_alias=AliasPath("data", "contact"), description="Set the contact for this managed node" + ) + location: Optional[Union[Global[str], Variable, Default[None]]] = Field( + default=None, + validation_alias=AliasPath("data", "location"), + description="Set the physical location of this managed node", + ) + view: Optional[List[ViewItem]] = Field( + default=None, validation_alias=AliasPath("data", "view"), description="Configure a view record" + ) + community: Optional[List[CommunityItem]] = Field( + default=None, validation_alias=AliasPath("data", "community"), description="Configure SNMP community" + ) + group: Optional[List[GroupItem]] = Field( + default=None, validation_alias=AliasPath("data", "group"), description="Configure an SNMP group" + ) + user: Optional[List[UserItem]] = Field( + default=None, validation_alias=AliasPath("data", "user"), description="Configure an SNMP user" + ) + target: Optional[List[TargetItem]] = Field( + default=None, + validation_alias=AliasPath("data", "target"), + description="Configure SNMP server to receive SNMP traps", + ) From eaf954f5253d01bee700fb48b1a77445213093a0 Mon Sep 17 00:00:00 2001 From: sbasan Date: Thu, 29 Feb 2024 10:25:52 +0100 Subject: [PATCH 19/21] update models for sdwandemo items --- .../models/configuration/config_migration.py | 25 +++++++++++++++---- .../sdwan/system/logging_parcel.py | 3 ++- catalystwan/models/policy/centralized.py | 15 +++++++++++ .../policy/definitions/zone_based_firewall.py | 17 +++++++++++-- catalystwan/models/policy/lists.py | 4 +-- catalystwan/models/policy/lists_entries.py | 4 +-- .../models/policy/policy_definition.py | 22 +++++++++++++++- catalystwan/workflows/config_migration.py | 16 ++++++------ 8 files changed, 84 insertions(+), 22 deletions(-) diff --git a/catalystwan/models/configuration/config_migration.py b/catalystwan/models/configuration/config_migration.py index 39d63ef15..acd7b58ba 100644 --- a/catalystwan/models/configuration/config_migration.py +++ b/catalystwan/models/configuration/config_migration.py @@ -4,6 +4,8 @@ from typing_extensions import Annotated from catalystwan.api.template_api import DeviceTemplateInformation, FeatureTemplateInformation +from catalystwan.endpoints.configuration_group import ConfigGroup +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload from catalystwan.models.configuration.feature_profile.sdwan.policy_object import AnyPolicyObjectParcel from catalystwan.models.configuration.feature_profile.sdwan.system import AnySystemParcel from catalystwan.models.policy import ( @@ -43,8 +45,12 @@ class UX1Policies(BaseModel): class UX1Templates(BaseModel): - features: List[FeatureTemplateInformation] = Field(default=[]) - devices: List[DeviceTemplateInformation] = Field(default=[]) + feature_templates: List[FeatureTemplateInformation] = Field( + default=[], serialization_alias="featureTemplates", validation_alias="featureTemplates" + ) + device_templates: List[DeviceTemplateInformation] = Field( + default=[], serialization_alias="deviceTemplates", validation_alias="deviceTemplates" + ) class ConfigGroupPreset(BaseModel): @@ -63,9 +69,18 @@ class UX1Config(BaseModel): class UX2Config(BaseModel): + # All UX2 Configuration items - Mega Model # All UX2 Configuration items - Mega Model model_config = ConfigDict(populate_by_name=True) - # TODO: config group name - config_group_presets: List[ConfigGroupPreset] = Field( - default=[], serialization_alias="configGroupPresets", validation_alias="configGroupPresets" + config_groups: List[ConfigGroup] = Field( + default=[], serialization_alias="configurationGroups", validation_alias="configurationGroups" + ) + policy_groups: List[ConfigGroup] = Field( + default=[], serialization_alias="policyGroups", validation_alias="policyGroups" + ) + feature_profiles: List[FeatureProfileCreationPayload] = Field( + default=[], serialization_alias="featureProfiles", validation_alias="featureProfiles" + ) + profile_parcels: List[AnyParcel] = Field( + default=[], serialization_alias="profileParcels", validation_alias="profileParcels" ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index d0a726863..9344a845b 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union from pydantic import AliasPath, BaseModel, ConfigDict, Field @@ -76,6 +76,7 @@ class Disk(BaseModel): class LoggingParcel(_ParcelBase): + type_: Literal["logging"] = Field(default="logging", exclude=True) disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) tls_profile: Optional[List[TlsProfile]] = Field(default=[], validation_alias=AliasPath("data", "tlsProfile")) server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "server")) diff --git a/catalystwan/models/policy/centralized.py b/catalystwan/models/policy/centralized.py index 9f28f0a7e..39992748a 100644 --- a/catalystwan/models/policy/centralized.py +++ b/catalystwan/models/policy/centralized.py @@ -159,12 +159,27 @@ class MeshPolicyItem(AssemblyItemBase): type: Literal["mesh"] = "mesh" +class AppRoutePolicyItem(AssemblyItemBase): + type: Literal["appRoute"] = "appRoute" + + +class CFlowDPolicyItem(AssemblyItemBase): + type: Literal["cflowd"] = "cflowd" + + +class VpnMembershipGroupPolicyItem(AssemblyItemBase): + type: Literal["vpnMembershipGroup"] = "vpnMembershipGroup" + + AnyAssemblyItem = Annotated[ Union[ TrafficDataPolicyItem, ControlPolicyItem, MeshPolicyItem, HubAndSpokePolicyItem, + AppRoutePolicyItem, + CFlowDPolicyItem, + VpnMembershipGroupPolicyItem, ], Field(discriminator="type"), ] diff --git a/catalystwan/models/policy/definitions/zone_based_firewall.py b/catalystwan/models/policy/definitions/zone_based_firewall.py index 716361994..690e5f0bf 100644 --- a/catalystwan/models/policy/definitions/zone_based_firewall.py +++ b/catalystwan/models/policy/definitions/zone_based_firewall.py @@ -7,7 +7,10 @@ from catalystwan.models.misc.application_protocols import ApplicationProtocol from catalystwan.models.policy.policy_definition import ( + AdvancedInspectionProfileAction, AppListEntry, + AppListFlatEntry, + ConnectionEventsAction, DefinitionWithSequencesCommonBase, DestinationDataPrefixListEntry, DestinationFQDNEntry, @@ -38,6 +41,7 @@ ZoneBasedFWPolicySequenceEntry = Annotated[ Union[ AppListEntry, + AppListFlatEntry, DestinationDataPrefixListEntry, DestinationFQDNEntry, DestinationGeoLocationEntry, @@ -69,6 +73,15 @@ Field(discriminator="field"), ] +ZoneBasedFWPolicyActions = Annotated[ + Union[ + AdvancedInspectionProfileAction, + ConnectionEventsAction, + LogAction, + ], + Field(discriminator="type"), +] + class ZoneBasedFWPolicyMatches(Match): entries: List[ZoneBasedFWPolicySequenceEntry] = [] @@ -80,7 +93,7 @@ class ZoneBasedFWPolicySequenceWithRuleSets(PolicyDefinitionSequenceBase): ) match: ZoneBasedFWPolicyMatches ruleset: bool = True - actions: List[LogAction] = [] + actions: List[ZoneBasedFWPolicyActions] = [] model_config = ConfigDict(populate_by_name=True) def match_rule_set_lists(self, rule_set_ids: Set[UUID]) -> None: @@ -189,7 +202,7 @@ class ZoneBasedFWPolicyDefinition(DefinitionWithSequencesCommonBase): class ZoneBasedFWPolicy(ZoneBasedFWPolicyHeader): type: Literal["zoneBasedFW"] = "zoneBasedFW" - mode: Literal["security"] = "security" + mode: Literal["security", "unified"] = "security" definition: ZoneBasedFWPolicyDefinition = ZoneBasedFWPolicyDefinition() def add_ipv4_rule( diff --git a/catalystwan/models/policy/lists.py b/catalystwan/models/policy/lists.py index ea6944b12..41abcb5a3 100644 --- a/catalystwan/models/policy/lists.py +++ b/catalystwan/models/policy/lists.py @@ -1,4 +1,4 @@ -from ipaddress import IPv4Address, IPv4Network, IPv6Network +from ipaddress import IPv4Address, IPv4Network, IPv6Interface from typing import Any, List, Literal, Optional, Set, Tuple from uuid import UUID @@ -184,7 +184,7 @@ class DataIPv6PrefixList(PolicyListBase): type: Literal["dataIpv6Prefix"] = "dataIpv6Prefix" entries: List[DataIPv6PrefixListEntry] = [] - def add_prefix(self, ipv6_prefix: IPv6Network) -> None: + def add_prefix(self, ipv6_prefix: IPv6Interface) -> None: self._add_entry(DataIPv6PrefixListEntry(ipv6_prefix=ipv6_prefix)) diff --git a/catalystwan/models/policy/lists_entries.py b/catalystwan/models/policy/lists_entries.py index 747d77a54..f03df04db 100644 --- a/catalystwan/models/policy/lists_entries.py +++ b/catalystwan/models/policy/lists_entries.py @@ -1,4 +1,4 @@ -from ipaddress import IPv4Address, IPv4Network, IPv6Network +from ipaddress import IPv4Address, IPv4Network, IPv6Interface, IPv6Network from typing import List, Literal, Optional, Set from uuid import UUID @@ -235,7 +235,7 @@ class ColorListEntry(BaseModel): class DataIPv6PrefixListEntry(BaseModel): model_config = ConfigDict(populate_by_name=True) - ipv6_prefix: IPv6Network = Field(serialization_alias="ipv6Prefix", validation_alias="ipv6Prefix") + ipv6_prefix: IPv6Interface = Field(serialization_alias="ipv6Prefix", validation_alias="ipv6Prefix") class LocalDomainListEntry(BaseModel): diff --git a/catalystwan/models/policy/policy_definition.py b/catalystwan/models/policy/policy_definition.py index c8388eb71..693c84a64 100644 --- a/catalystwan/models/policy/policy_definition.py +++ b/catalystwan/models/policy/policy_definition.py @@ -515,6 +515,11 @@ class AppListEntry(BaseModel): ref: UUID +class AppListFlatEntry(BaseModel): + field: Literal["appListFlat"] = "appListFlat" + ref: UUID + + class SourceFQDNListEntry(BaseModel): field: Literal["sourceFqdnList"] = "sourceFqdnList" ref: UUID @@ -750,6 +755,16 @@ class PolicerAction(BaseModel): parameter: Reference +class ConnectionEventsAction(BaseModel): + type: Literal["connectionEvents"] = "connectionEvents" + parameter: str = "" + + +class AdvancedInspectionProfileAction(BaseModel): + type: Literal["advancedInspectionProfile"] = "advancedInspectionProfile" + parameter: Reference + + ActionSetEntry = Annotated[ Union[ AffinityEntry, @@ -784,8 +799,10 @@ class ActionSet(BaseModel): ActionEntry = Annotated[ Union[ ActionSet, + AdvancedInspectionProfileAction, CFlowDAction, ClassMapAction, + ConnectionEventsAction, CountAction, DREOptimizationAction, FallBackToRoutingAction, @@ -808,6 +825,7 @@ class ActionSet(BaseModel): MatchEntry = Annotated[ Union[ AppListEntry, + AppListFlatEntry, CarrierEntry, ClassMapListEntry, ColorListEntry, @@ -909,7 +927,9 @@ class PolicyDefinitionSequenceBase(BaseModel): default="drop", serialization_alias="baseAction", validation_alias="baseAction" ) sequence_type: SequenceType = Field(serialization_alias="sequenceType", validation_alias="sequenceType") - sequence_ip_type: SequenceIpType = Field(serialization_alias="sequenceIpType", validation_alias="sequenceIpType") + sequence_ip_type: Optional[SequenceIpType] = Field( + default="ipv4", serialization_alias="sequenceIpType", validation_alias="sequenceIpType" + ) ruleset: Optional[bool] = None match: Match actions: Sequence[ActionEntry] diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index abac5fcc5..7d79db6af 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -3,7 +3,7 @@ from catalystwan.api.policy_api import POLICY_LIST_ENDPOINTS_MAP from catalystwan.endpoints.configuration_group import ConfigGroup -from catalystwan.models.configuration.config_migration import ConfigGroupPreset, UX1Config, UX2Config +from catalystwan.models.configuration.config_migration import UX1Config, UX2Config from catalystwan.session import ManagerSession from catalystwan.utils.config_migration.converters.feature_template import create_parcel_from_template from catalystwan.utils.config_migration.creators.config_group import ConfigGroupCreator @@ -19,16 +19,14 @@ def log_progress(task: str, completed: int, total: int) -> None: def transform(ux1: UX1Config) -> UX2Config: ux2 = UX2Config() - ux2.config_group_presets.append(ConfigGroupPreset(config_group_name="Default_Config_Group")) - profile_parcels = ux2.config_group_presets[0].profile_parcels # Feature Templates - for ft in ux1.templates.features: + for ft in ux1.templates.feature_templates: if ft.template_type in SUPPORTED_TEMPLATE_TYPES: - profile_parcels.append(create_parcel_from_template(ft)) + ux2.profile_parcels.append(create_parcel_from_template(ft)) # Policy Lists for policy_list in ux1.policies.policy_lists: if (parcel := policy_list.to_policy_object_parcel()) is not None: - profile_parcels.append(parcel) + ux2.profile_parcels.append(parcel) return ux2 @@ -71,10 +69,10 @@ def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, in template_api = session.api.templates progress("Collecting Templates Info", 0, 2) - ux1.templates.features = [t for t in template_api.get_feature_templates()] + ux1.templates.feature_templates = [t for t in template_api.get_feature_templates()] progress("Collecting Templates Info", 1, 2) - ux1.templates.devices = [t for t in template_api.get_device_templates()] + ux1.templates.device_templates = [t for t in template_api.get_device_templates()] progress("Collecting Templates Info", 2, 2) return ux1 @@ -98,7 +96,7 @@ def push_ux2_config(session: ManagerSession, config: UX2Config) -> ConfigGroup: config_group_creator = ConfigGroupCreator(session, config, logger) config_group = config_group_creator.create() feature_profiles = config_group.profiles # noqa: F841 - for parcels in config.config_group_presets: + for parcels in config.profile_parcels: # TODO: Create API that supports parcel creation on feature profiles # Example: session.api.parcels.create(parcels=parcels, feature_profiles=feature_profiles) pass From 7c9314928c4c4b8fe2f2eda570975f9205a05c75 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Fri, 1 Mar 2024 16:36:58 +0100 Subject: [PATCH 20/21] Integration tests for default system models --- .../sdwan/system/test_models.py | 205 ++++++++++++++++++ .../feature_profile/sdwan/system/banner.py | 20 +- .../feature_profile/sdwan/system/basic.py | 69 +++--- .../feature_profile/sdwan/system/bfd.py | 60 +++-- .../sdwan/system/global_parcel.py | 70 +++--- .../feature_profile/sdwan/system/literals.py | 15 +- .../sdwan/system/logging_parcel.py | 148 +++++++++---- .../feature_profile/sdwan/system/mrf.py | 39 ++-- .../feature_profile/sdwan/system/ntp.py | 30 ++- .../feature_profile/sdwan/system/omp.py | 46 ++-- .../feature_profile/sdwan/system/security.py | 37 ++-- .../feature_profile/sdwan/system/snmp.py | 36 +-- .../sdwan/test_model_creation.py | 0 13 files changed, 554 insertions(+), 221 deletions(-) create mode 100644 catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py delete mode 100644 catalystwan/tests/feature_profile/sdwan/test_model_creation.py diff --git a/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py b/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py new file mode 100644 index 000000000..414806067 --- /dev/null +++ b/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py @@ -0,0 +1,205 @@ +import os +import unittest +from typing import cast + +from catalystwan.models.configuration.feature_profile.sdwan.system import ( + BannerParcel, + BasicParcel, + BFDParcel, + GlobalParcel, + LoggingParcel, + MRFParcel, + NTPParcel, + SecurityParcel, + SNMPParcel, +) +from catalystwan.session import create_manager_session + + +class TestSystemFeatureProfileModels(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.system.create_profile("TestProfile", "Description").id + + def test_when_default_values_banner_parcel_expect_successful_post(self): + # Arrange + banner_parcel = BannerParcel( + parcel_name="BannerDefault", + parcel_description="Banner Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, banner_parcel).id + # Assert + assert parcel_id + + def test_when_fully_specified_banner_parcel_expect_successful_post(self): + # Arrange + banner_parcel = BannerParcel( + parcel_name="BannerFullySpecified", + parcel_description="Banner Parcel", + ) + 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(self.profile_id, banner_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_logging_parcel_expect_successful_post(self): + # Arrange + logging_parcel = LoggingParcel( + parcel_name="LoggingDefault", + parcel_description="Logging Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, logging_parcel).id + # Assert + assert parcel_id + + def test_when_fully_specified_logging_parcel_expect_successful_post(self): + # Arrange + logging_parcel = LoggingParcel( + parcel_name="LoggingFullySpecified", + parcel_description="Logging Parcel", + ) + logging_parcel.set_disk( + enable=True, + disk_file_rotate=10, + disk_file_size=10, + ) + logging_parcel.add_tls_profile( + profile="TLSProfile", + version="TLSv1.2", + ciphersuite_list=[ + "aes-256-cbc-sha", + "aes-128-cbc-sha", + "ecdhe-ecdsa-aes-gcm-sha2", + "ecdhe-rsa-aes-cbc-sha2", + ], + ) + logging_parcel.add_ipv4_server( + name="Server1", + vpn=0, + source_interface="fastethernet1/0", + priority="debugging", + enable_tls=True, + custom_profile=True, + profile_properties="TLSProfile", + ) + logging_parcel.add_ipv6_server( + name="Server2", + vpn=0, + source_interface="fastethernet1/1", + priority="debugging", + enable_tls=True, + custom_profile=True, + profile_properties="TLSProfile", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, logging_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_bfd_parcel_expect_successful_post(self): + # Arrange + bfd_parcel = BFDParcel( + parcel_name="BFDDefault", + parcel_description="BFD Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, bfd_parcel).id + # Assert + assert parcel_id + + def test_when_fully_specified_bfd_parcel_expect_successful_post(self): + # Arrange + bfd_parcel = BFDParcel( + parcel_name="BFDFullySpecified", + parcel_description="BFD Parcel", + ) + bfd_parcel.set_muliplier(1) + bfd_parcel.set_poll_interval(700000) + bfd_parcel.set_default_dscp(51) + bfd_parcel.add_color(color="lte", hello_interval=300000, multiplier=7, pmtu_discovery=False) + bfd_parcel.add_color(color="mpls", pmtu_discovery=False) + 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(self.profile_id, bfd_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_basic_parcel_expect_successful_post(self): + # Arrange + basic_parcel = BasicParcel( + parcel_name="BasicDefault", + parcel_description="Basic Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, basic_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_security_parcel_expect_successful_post(self): + # Arrange + security_parcel = SecurityParcel( + parcel_name="SecurityDefault", + parcel_description="Security Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, security_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_ntp_parcel_expect_successful_post(self): + # Arrange + ntp_parcel = NTPParcel( + parcel_name="NTPDefault", + parcel_description="NTP Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, ntp_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_global_parcel_expect_successful_post(self): + # Arrange + global_parcel = GlobalParcel( + parcel_name="GlobalDefault", + parcel_description="Global Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, global_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_mrf_parcel_expect_successful_post(self): + # Arrange + mrf_parcel = MRFParcel( + parcel_name="MRFDefault", + parcel_description="MRF Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, mrf_parcel).id + # Assert + assert parcel_id + + def test_when_default_values_snmp_parcel_expect_successful_post(self): + # Arrange + snmp_parcel = SNMPParcel( + parcel_name="SNMPDefault", + parcel_description="SNMP Parcel", + ) + # Act + parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, snmp_parcel).id + # Assert + assert parcel_id + + def tearDown(self) -> None: + self.session.api.sdwan_feature_profiles.system.delete_profile(self.profile_id) + self.session.close() diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py index 0c1df2d99..0bdbcd681 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -4,16 +4,26 @@ from pydantic import AliasPath, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default, as_global + +EmptyString = Literal[""] class BannerParcel(_ParcelBase): type_: Literal["banner"] = Field(default="banner", exclude=True) model_config = ConfigDict(extra="forbid", populate_by_name=True) - login: Union[Variable, Global[str], Default[Literal[""]]] = Field( - default=as_default(""), validation_alias=AliasPath("data", "login") + login: Union[Variable, Global[str], Default[EmptyString], str] = Field( + default=as_default("", EmptyString), validation_alias=AliasPath("data", "login") ) - motd: Union[Variable, Global[str], Default[Literal[""]]] = Field( - default=as_default(""), validation_alias=AliasPath("data", "motd") + motd: Union[Variable, Global[str], Default[EmptyString]] = Field( + default=as_default("", EmptyString), + validation_alias=AliasPath("data", "motd"), + description="Message of the day", ) + + def add_login(self, value: str): + self.login = as_global(value) + + def add_motd(self, value: str): + self.motd = as_global(value) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py index fbfeec15d..e840d5e73 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -12,13 +12,12 @@ Epfr = Literal["disabled", "aggressive", "moderate", "conservative"] DefaultEpfr = Literal["disabled"] SiteType = Literal["type-1", "type-2", "type-3", "cloud", "branch", "br", "spoke"] - DefaultTimezone = Literal["UTC"] class Clock(BaseModel): timezone: Union[Variable, Global[Timezone], Default[DefaultTimezone]] = Field( - default=as_default("UTC"), description="Set the timezone" + default=as_default("UTC", DefaultTimezone), description="Set the timezone" ) @@ -31,8 +30,8 @@ class Sms(BaseModel): extra="forbid", populate_by_name=True, ) - enable: Optional[Union[Global[bool], Default[Literal[False]]]] = Field( - None, description="Global[bool] device’s geo fencing SMS" + enable: Union[Global[bool], Default[bool]] = Field( + default=as_default(False), description="Enable device’s geo fencing SMS" ) mobile_number: Optional[List[MobileNumberItem]] = Field( None, @@ -43,11 +42,11 @@ class Sms(BaseModel): class GeoFencing(BaseModel): - enable: Optional[Union[Global[bool], Default[Literal[False]]]] = Field(None, description="Enable Geo fencing") - range: Optional[Union[Global[int], Variable, Default[int]]] = Field( - None, description="Set the device’s geo fencing range" + enable: Union[Global[bool], Default[bool]] = Field(default=as_default(False), description="Enable Geo fencing") + range: Union[Global[int], Variable, Default[int]] = Field( + default=as_default(100), description="Set the device’s geo fencing range" ) - sms: Optional[Sms] = None + sms: Sms = Field(default_factory=Sms, description="Set device’s geo fencing SMS") # type: ignore class GpsVariable(BaseModel): @@ -56,13 +55,13 @@ class GpsVariable(BaseModel): populate_by_name=True, ) longitude: Union[Variable, Global[float], Default[None]] = Field( - default=as_default(None), description="Set the device physical longitude" + default=Default[None](value=None), description="Set the device physical longitude" ) latitude: Union[Variable, Global[float], Default[None]] = Field( - default=as_default(None), description="Set the device physical latitude" + default=Default[None](value=None), description="Set the device physical latitude" ) - geo_fencing: Optional[GeoFencing] = Field( - None, + geo_fencing: GeoFencing = Field( + default_factory=GeoFencing, serialization_alias="geoFencing", validation_alias="geoFencing", ) @@ -73,7 +72,7 @@ class OnDemand(BaseModel): extra="forbid", populate_by_name=True, ) - on_demand_enable: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + on_demand_enable: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="onDemandEnable", validation_alias="onDemandEnable", @@ -85,8 +84,8 @@ class OnDemand(BaseModel): Default[int], ] = Field( default=as_default(10), - serialization_alias="onDemandVariable", - validation_alias="onDemandVariable", + serialization_alias="onDemandIdleTimeout", + validation_alias="onDemandIdleTimeout", description="Set the idle timeout for on-demand tunnels", ) @@ -101,13 +100,13 @@ class AffinityPerVrfItem(BaseModel): Global[int], Default[None], ] = Field( - default=as_default(None), + default=Default[None](value=None), serialization_alias="affinityGroupNumber", validation_alias="affinityGroupNumber", description="Affinity Group Number", ) vrf_range: Union[Variable, Global[str], Default[None]] = Field( - default=as_default(None), + default=Default[None](value=None), serialization_alias="vrfRange", validation_alias="vrfRange", description="Range of VRFs", @@ -121,23 +120,23 @@ class BasicParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - clock: Clock = Field(validation_alias=AliasPath("data", "clock")) + clock: Clock = Field(default_factory=Clock, validation_alias=AliasPath("data", "clock")) description: Union[Variable, Global[str], Default[None]] = Field( - default=as_default(None), + default=Default[None](value=None), validation_alias=AliasPath("data", "description"), description="Set a text description of the device", ) location: Union[Variable, Global[str], Default[None]] = Field( - default=as_default(None), + default=Default[None](value=None), validation_alias=AliasPath("data", "location"), description="Set the location of the device", ) gps_location: GpsVariable = Field( - ..., - validation_alias=AliasPath("data", "gpsVariable"), + default_factory=GpsVariable, + validation_alias=AliasPath("data", "gpsLocation"), ) device_groups: Union[Variable, Global[List[str]], Default[None]] = Field( - default=as_default(None), + default=Default[None](value=None), validation_alias=AliasPath("data", "deviceGroups"), description="Device groups", ) @@ -162,8 +161,8 @@ class BasicParcel(_ParcelBase): validation_alias=AliasPath("data", "portOffset"), description="Set the TLOC port offset when multiple devices are behind a NAT", ) - port_hop: Union[Variable, Global[bool], Default[Literal[True]]] = Field( - default=True, + port_hop: Union[Variable, Global[bool], Default[bool]] = Field( + default=as_default(True), validation_alias=AliasPath("data", "portHop"), description="Enable port hopping", ) @@ -172,7 +171,7 @@ class BasicParcel(_ParcelBase): validation_alias=AliasPath("data", "controlSessionPps"), description="Set the policer rate for control sessions", ) - track_transport: Optional[Union[Variable, Global[bool], Default[Literal[True]]]] = Field( + track_transport: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( None, validation_alias=AliasPath("data", "trackTransport"), description="Configure tracking of transport", @@ -183,16 +182,16 @@ class BasicParcel(_ParcelBase): description="OMP Tag attached to routes based on interface tracking", ) console_baud_rate: Union[Variable, Global[ConsoleBaudRate], Default[DefaultConsoleBaudRate]] = Field( - default=as_default("9600"), + default=as_default("9600", DefaultConsoleBaudRate), validation_alias=AliasPath("data", "consoleBaudRate"), description="Set the console baud rate", ) max_omp_sessions: Union[Variable, Global[int], Default[None]] = Field( - default=as_default(None), + default=Default[None](value=None), validation_alias=AliasPath("data", "maxOmpSessions"), description="Set the maximum number of OMP sessions <1..100> the device can have", ) - multi_tenant: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + multi_tenant: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( None, validation_alias=AliasPath("data", "multiTenant"), description="Device is multi-tenant", @@ -201,7 +200,7 @@ class BasicParcel(_ParcelBase): Union[ Variable, Global[bool], - Default[Literal[True]], + Default[bool], ] ] = Field( None, @@ -212,14 +211,14 @@ class BasicParcel(_ParcelBase): Union[ Variable, Global[bool], - Default[Literal[False]], + Default[bool], ] ] = Field( None, validation_alias=AliasPath("data", "trackerDiaStabilizeStatus"), description="Enable or disable endpoint tracker diaStabilize status", ) - admin_tech_on_failure: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + admin_tech_on_failure: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), validation_alias=AliasPath("data", "adminTechOnFailure"), description="Collect admin-tech before reboot due to daemon failure", @@ -230,10 +229,10 @@ class BasicParcel(_ParcelBase): description="Idle CLI timeout in minutes", ) on_demand: OnDemand = Field( - ..., + default_factory=OnDemand, validation_alias=AliasPath("data", "onDemand"), ) - transport_gateway: Optional[Union[Global[bool], Variable, Default[Literal[False]]]] = Field( + transport_gateway: Optional[Union[Global[bool], Variable, Default[bool]]] = Field( None, validation_alias=AliasPath("data", "transportGateway"), description="Enable transport gateway", @@ -274,7 +273,7 @@ class BasicParcel(_ParcelBase): Union[ Variable, Global[bool], - Default[Literal[False]], + Default[bool], ] ] = Field( None, diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py index bf2247e3b..0e47e66e0 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -1,28 +1,21 @@ -from typing import List, Literal, Optional, Union +from typing import List, Literal, Optional from pydantic import AliasPath, BaseModel, ConfigDict, Field from catalystwan.api.configuration_groups.parcel import Global, _ParcelBase, as_global from catalystwan.models.common import TLOCColor -from catalystwan.utils.config_migration.converters.recast import DefaultGlobalBool, DefaultGlobalColorLiteral - -DEFAULT_BFD_COLOR_MULTIPLIER = as_global(7) -DEFAULT_BFD_DSCP = as_global(48) -DEFAULT_BFD_HELLO_INTERVAL = as_global(1000) -DEFAULT_BFD_POLL_INTERVAL = as_global(600000) -DEFAULT_BFD_MULTIPLIER = as_global(6) class Color(BaseModel): - color: Union[DefaultGlobalColorLiteral, Global[TLOCColor]] + color: Global[TLOCColor] hello_interval: Optional[Global[int]] = Field( - default=DEFAULT_BFD_HELLO_INTERVAL, validation_alias="helloInterval", serialization_alias="helloInterval" + default=as_global(1000), validation_alias="helloInterval", serialization_alias="helloInterval" ) - multiplier: Optional[Global[int]] = DEFAULT_BFD_COLOR_MULTIPLIER - pmtu_discovery: Optional[Union[DefaultGlobalBool, Global[bool]]] = Field( + multiplier: Optional[Global[int]] = as_global(7) + pmtu_discovery: Optional[Global[bool]] = Field( default=as_global(True), validation_alias="pmtuDiscovery", serialization_alias="pmtuDiscovery" ) - dscp: Optional[Global[int]] = DEFAULT_BFD_DSCP + dscp: Optional[Global[int]] = as_global(48) model_config = ConfigDict(populate_by_name=True) @@ -30,13 +23,44 @@ class BFDParcel(_ParcelBase): type_: Literal["bfd"] = Field(default="bfd", exclude=True) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) - multiplier: Optional[Global[int]] = Field( - default=DEFAULT_BFD_MULTIPLIER, validation_alias=AliasPath("data", "multiplier") - ) + multiplier: Optional[Global[int]] = Field(default=as_global(6), validation_alias=AliasPath("data", "multiplier")) poll_interval: Optional[Global[int]] = Field( - default=DEFAULT_BFD_POLL_INTERVAL, validation_alias=AliasPath("data", "pollInterval") + default=as_global(600000), + validation_alias=AliasPath("data", "pollInterval"), + description="Poll Interval (In Millisecond)", ) default_dscp: Optional[Global[int]] = Field( - default=DEFAULT_BFD_DSCP, validation_alias=AliasPath("data", "defaultDscp") + default=as_global(48), + validation_alias=AliasPath("data", "defaultDscp"), + description="DSCP Values for BFD Packets (decimal)", ) colors: Optional[List[Color]] = Field(default=None, validation_alias=AliasPath("data", "colors")) + + def set_muliplier(self, value: int): + self.multiplier = as_global(value) + + def set_poll_interval(self, value: int): + self.poll_interval = as_global(value) + + def set_default_dscp(self, value: int): + self.default_dscp = as_global(value) + + def add_color( + self, + color: TLOCColor, + hello_interval: int = 1000, + multiplier: int = 7, + pmtu_discovery: bool = True, + dscp: int = 48, + ): + if not self.colors: + self.colors = [] + self.colors.append( + Color( + color=Global[TLOCColor](value=color), + hello_interval=as_global(hello_interval), + multiplier=as_global(multiplier), + pmtu_discovery=as_global(pmtu_discovery), + dscp=as_global(dscp), + ) + ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py index cc0fce868..95d39cd66 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py @@ -2,7 +2,7 @@ from typing import Literal, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasPath, BaseModel, ConfigDict, Field from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default @@ -12,127 +12,137 @@ class ServicesIp(BaseModel): extra="forbid", populate_by_name=True, ) - http_server: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + http_server: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpHttpServer", validation_alias="servicesGlobalServicesIpHttpServer", ) - https_server: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + https_server: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpHttpsServer", validation_alias="servicesGlobalServicesIpHttpsServer", ) - ftp_passive: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + ftp_passive: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpFtpPassive", validation_alias="servicesGlobalServicesIpFtpPassive", ) - domain_lookup: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + domain_lookup: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpDomainLookup", validation_alias="servicesGlobalServicesIpDomainLookup", ) - arp_proxy: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + arp_proxy: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpArpProxy", validation_alias="servicesGlobalServicesIpArpProxy", ) - rcmd: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + rcmd: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpRcmd", validation_alias="servicesGlobalServicesIpRcmd", ) - line_vty: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + line_vty: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="servicesGlobalServicesIpLineVty", validation_alias="servicesGlobalServicesIpLineVty", ) - cdp: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + cdp: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), serialization_alias="servicesGlobalServicesIpCdp", validation_alias="servicesGlobalServicesIpCdp", ) - lldp: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + lldp: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), serialization_alias="servicesGlobalServicesIpLldp", validation_alias="servicesGlobalServicesIpLldp", ) - source_intrf: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), + source_intrf: Union[Variable, Global[bool], Default[None]] = Field( + default=Default[None](value=None), serialization_alias="servicesGlobalServicesIpSourceIntrf", validation_alias="servicesGlobalServicesIpSourceIntrf", ) - tcp_keepalives_in: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + tcp_keepalives_in: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), serialization_alias="globalOtherSettingsTcpKeepalivesIn", validation_alias="globalOtherSettingsTcpKeepalivesIn", ) - keepalives_out: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + keepalives_out: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), serialization_alias="globalOtherSettingsTcpKeepalivesOut", validation_alias="globalOtherSettingsTcpKeepalivesOut", ) - small_servers: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + small_servers: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="globalOtherSettingsTcpSmallServers", validation_alias="globalOtherSettingsTcpSmallServers", ) - udp_small_servers: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + udp_small_servers: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="globalOtherSettingsUdpSmallServers", validation_alias="globalOtherSettingsUdpSmallServers", ) - console_logging: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + console_logging: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), serialization_alias="globalOtherSettingsConsoleLogging", validation_alias="globalOtherSettingsConsoleLogging", ) - ip_source_route: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + ip_source_route: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="globalOtherSettingsIPSourceRoute", validation_alias="globalOtherSettingsIPSourceRoute", ) - vty_line_logging: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + vty_line_logging: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), serialization_alias="globalOtherSettingsVtyLineLogging", validation_alias="globalOtherSettingsVtyLineLogging", ) - snmp_ifindex_persist: (Union[Variable, Global[bool], Default[Literal[True]]]) = Field( + snmp_ifindex_persist: (Union[Variable, Global[bool], Default[bool]]) = Field( default=as_default(True), serialization_alias="globalOtherSettingsSnmpIfindexPersist", validation_alias="globalOtherSettingsSnmpIfindexPersist", ) - ignore_bootp: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + ignore_bootp: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), serialization_alias="globalOtherSettingsIgnoreBootp", validation_alias="globalOtherSettingsIgnoreBootp", ) - nat64_udp_timeout: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), + nat64_udp_timeout: Union[Variable, Global[bool], Default[int]] = Field( + default=as_default(300), serialization_alias="globalSettingsNat64UdpTimeout", validation_alias="globalSettingsNat64UdpTimeout", ) - nat64_tcp_timeout: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), + nat64_tcp_timeout: Union[Variable, Global[bool], Default[int]] = Field( + default=as_default(3600), serialization_alias="globalSettingsNat64TcpTimeout", validation_alias="globalSettingsNat64TcpTimeout", ) - http_authentication: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), + http_authentication: Union[Variable, Global[bool], Default[None]] = Field( + default=Default[None](value=None), serialization_alias="globalSettingsHttpAuthentication", validation_alias="globalSettingsHttpAuthentication", ) - ssh_version: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), + ssh_version: Union[Variable, Global[bool], Default[None]] = Field( + default=Default[None](value=None), serialization_alias="globalSettingsSSHVersion", validation_alias="globalSettingsSSHVersion", ) +class ServicesGlobal(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + services_ip: ServicesIp = Field(default_factory=ServicesIp) + + class GlobalParcel(_ParcelBase): type_: Literal["global"] = Field(default="global", exclude=True) model_config = ConfigDict( extra="forbid", populate_by_name=True, ) - services_ip: ServicesIp # Key format is snake case in schema + services_global: ServicesGlobal = Field( + default_factory=ServicesGlobal, validation_alias=AliasPath("data", "services_global") + ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py index a92724f35..5fc13df92 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py @@ -1,7 +1,18 @@ from typing import Literal Priority = Literal["information", "debugging", "notice", "warn", "error", "critical", "alert", "emergency"] -Version = Literal["TLSv1.1", "TLSv1.2"] +TlsVersion = Literal["TLSv1.1", "TLSv1.2"] AuthType = Literal["Server", "Mutual"] +CypherSuite = Literal[ + "rsa-aes-cbc-sha2", + "rsa-aes-gcm-sha2", + "ecdhe-rsa-aes-gcm-sha2", + "aes-128-cbc-sha", + "aes-256-cbc-sha", + "dhe-aes-cbc-sha2", + "dhe-aes-gcm-sha2", + "ecdhe-ecdsa-aes-gcm-sha2", + "ecdhe-rsa-aes-cbc-sha2", +] -SYSTEM_LITERALS = [Priority, Version, AuthType] +SYSTEM_LITERALS = [Priority, TlsVersion, AuthType, CypherSuite] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index 1b4abd236..3fe345157 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -2,54 +2,42 @@ from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase, as_global -from catalystwan.models.configuration.feature_profile.sdwan.system.literals import AuthType, Priority, Version +from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase, as_default, as_global +from catalystwan.models.configuration.feature_profile.sdwan.system.literals import ( + AuthType, + CypherSuite, + Priority, + TlsVersion, +) from catalystwan.utils.pydantic_validators import ConvertBoolToStringModel class TlsProfile(ConvertBoolToStringModel): - profile: str - version: Optional[Version] = Field(default="TLSv1.1", json_schema_extra={"data_path": ["tls-version"]}) - auth_type: AuthType = Field(json_schema_extra={"vmanage_key": "auth-type"}) - ciphersuite_list: Optional[List] = Field( - default=None, json_schema_extra={"data_path": ["ciphersuite"], "vmanage_key": "ciphersuite-list"} + profile: Global[str] + version: Union[Global[TlsVersion], Default[TlsVersion]] = Field( + default=as_default("TLSv1.1", TlsVersion), serialization_alias="tlsVersion", validation_alias="tlsVersion" ) - model_config = ConfigDict(populate_by_name=True) - - -class Server(BaseModel): - name: Global[str] - vpn: Optional[Global[str]] = None - source_interface: Optional[Global[str]] = Field( - default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface" - ) - priority: Optional[Global[Priority]] = Field(default="information") - enable_tls: Optional[Global[bool]] = Field( - default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" - ) - custom_profile: Optional[Global[bool]] = Field( - default=as_global(False), - serialization_alias="tlsPropertiesCustomProfile", - validation_alias="tlsPropertiesCustomProfile", - ) - profile_properties: Optional[Global[str]] = Field( - default=None, serialization_alias="tlsPropertiesProfile", validation_alias="tlsPropertiesProfile" + auth_type: Default[AuthType] = Field( + default=as_default("Server", AuthType), serialization_alias="authType", validation_alias="authType" + ) # Value can't be changed in the UI + ciphersuite_list: Union[Global[List[CypherSuite]], Default[None]] = Field( + default=Default[None](value=None), serialization_alias="cipherSuiteList", validation_alias="cipherSuiteList" ) model_config = ConfigDict(populate_by_name=True) -class Ipv6Server(BaseModel): +class Server(BaseModel): name: Global[str] - vpn: Optional[Global[str]] = None - source_interface: Optional[Global[str]] = Field( - default=None, serialization_alias="sourceInterface", validation_alias="sourceInterface" + vpn: Union[Global[int], Default[int]] = Field(default=as_default(0)) + source_interface: Union[Global[str], Default[None]] = Field( + default=Default[None](value=None), serialization_alias="sourceInterface", validation_alias="sourceInterface" ) - priority: Optional[Global[Priority]] = Field(default="information") - enable_tls: Optional[Global[bool]] = Field( - default=as_global(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" + priority: Union[Global[Priority], Default[Priority]] = Field(default=as_default("information", Priority)) + enable_tls: Union[Global[bool], Default[bool]] = Field( + default=as_default(False), serialization_alias="tlsEnable", validation_alias="tlsEnable" ) - custom_profile: Optional[Global[bool]] = Field( - default=as_global(False), + custom_profile: Optional[Union[Global[bool], Default[bool]]] = Field( + default=None, serialization_alias="tlsPropertiesCustomProfile", validation_alias="tlsPropertiesCustomProfile", ) @@ -61,18 +49,18 @@ class Ipv6Server(BaseModel): class File(BaseModel): disk_file_size: Optional[Union[Global[int], Default[int]]] = Field( - default=Default[int](value=10), serialization_alias="diskFileSize", validation_alias="diskFileSize" + default=as_default(10), serialization_alias="diskFileSize", validation_alias="diskFileSize" ) disk_file_rotate: Optional[Union[Global[int], Default[int]]] = Field( - default=Default[int](value=10), serialization_alias="diskFileRotate", validation_alias="diskFileRotate" + default=as_default(10), serialization_alias="diskFileRotate", validation_alias="diskFileRotate" ) class Disk(BaseModel): disk_enable: Optional[Global[bool]] = Field( - default=False, serialization_alias="diskEnable", validation_alias="diskEnable" + default=None, serialization_alias="diskEnable", validation_alias="diskEnable" ) - file: File + file: File = Field(default_factory=File) class LoggingParcel(_ParcelBase): @@ -81,7 +69,83 @@ class LoggingParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - disk: Optional[Disk] = Field(default=None, validation_alias=AliasPath("data", "disk")) + disk: Disk = Field(default_factory=Disk, validation_alias=AliasPath("data", "disk")) tls_profile: Optional[List[TlsProfile]] = Field(default=[], validation_alias=AliasPath("data", "tlsProfile")) server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "server")) - ipv6_server: Optional[List[Ipv6Server]] = Field(default=[], validation_alias=AliasPath("data", "ipv6Server")) + ipv6_server: Optional[List[Server]] = Field(default=[], validation_alias=AliasPath("data", "ipv6Server")) + + def set_disk(self, enable: bool, disk_file_size: int = 10, disk_file_rotate: int = 10): + self.disk.disk_enable = as_global(enable) + self.disk.file.disk_file_size = as_global(disk_file_size) + self.disk.file.disk_file_rotate = as_global(disk_file_rotate) + + def add_tls_profile( + self, profile: str, version: TlsVersion = "TLSv1.1", ciphersuite_list: Optional[List[CypherSuite]] = None + ): + if not self.tls_profile: + self.tls_profile = [] + self.tls_profile.append( + TlsProfile( + profile=as_global(profile), + version=as_global(version, TlsVersion), + ciphersuite_list=Global[List[CypherSuite]](value=ciphersuite_list) + if ciphersuite_list + else Default[None](value=None), + ) + ) + + def add_ipv4_server( + self, + name: str, + vpn: int = 0, + source_interface: Optional[str] = None, + priority: Priority = "information", + enable_tls: bool = False, + custom_profile: bool = False, + profile_properties: Optional[str] = None, + ): + item = self._create_server_item( + name, vpn, source_interface, priority, enable_tls, custom_profile, profile_properties + ) + if not self.server: + self.server = [] + self.server.append(item) + return item + + def add_ipv6_server( + self, + name: str, + vpn: int = 0, + source_interface: Optional[str] = None, + priority: Priority = "information", + enable_tls: bool = False, + custom_profile: bool = False, + profile_properties: Optional[str] = None, + ): + item = self._create_server_item( + name, vpn, source_interface, priority, enable_tls, custom_profile, profile_properties + ) + if not self.ipv6_server: + self.ipv6_server = [] + self.ipv6_server.append(item) + return item + + def _create_server_item( + self, + name: str, + vpn: int, + source_interface: Optional[str] = None, + priority: Priority = "information", + enable_tls: bool = False, + custom_profile: bool = False, + profile_properties: Optional[str] = None, + ): + return Server( + name=as_global(name), + vpn=as_global(vpn), + source_interface=as_global(source_interface) if source_interface else Default[None](value=None), + priority=as_global(priority, Priority), + enable_tls=as_global(enable_tls), + custom_profile=as_global(custom_profile) if custom_profile else None, + profile_properties=as_global(profile_properties) if profile_properties else None, + ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py index 7da607605..cd49d0fea 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -4,7 +4,7 @@ from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default EnableMrfMigration = Literal["enabled", "enabled-from-bgp-core"] Role = Literal["edge-router", "border-router"] @@ -15,17 +15,20 @@ class ManagementRegion(BaseModel): extra="forbid", populate_by_name=True, ) - vrf_id: Optional[Union[Global[int], Default[None], Variable]] = Field( - None, serialization_alias="vrfId", validation_alias="vrfId", description="VRF name for management region" + vrf_id: Union[Global[int], Default[None], Variable] = Field( + default=Default[None](value=None), + serialization_alias="vrfId", + validation_alias="vrfId", + description="VRF name for management region", ) gateway_preference: Optional[Union[Global[List[int]], Default[None], Variable]] = Field( - None, + default=Default[None](value=None), serialization_alias="gatewayPreference", validation_alias="gatewayPreference", description="List of affinity group preferences for VRF", ) - management_gateway: Optional[Union[Global[bool], Default[Literal[False]], Variable]] = Field( - None, + management_gateway: Union[Global[bool], Default[bool], Variable] = Field( + default=as_default(False), serialization_alias="managementGateway", validation_alias="managementGateway", description="Enable management gateway", @@ -39,31 +42,33 @@ class MRFParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - secondary_region: Optional[Union[Global[int], Variable, Default[None]]] = Field( - None, + secondary_region: Union[Global[int], Variable, Default[None]] = Field( + default=Default[None](value=None), validation_alias=AliasPath("data", "secondaryRegion"), description="Set secondary region ID", ) - role: Optional[Union[Global[Role], Variable, Default[None]]] = Field( - None, validation_alias=AliasPath("data", "role"), description="Set the role for router" + role: Union[Global[Role], Variable, Default[None]] = Field( + default=Default[None](value=None), + validation_alias=AliasPath("data", "role"), + description="Set the role for router", ) - enable_mrf_migration: Optional[Union[Global[EnableMrfMigration], Default[None]]] = Field( - None, + enable_mrf_migration: Union[Global[EnableMrfMigration], Default[None]] = Field( + default=Default[None](value=None), validation_alias=AliasPath("data", "enableMrfMigration"), description="Enable migration mode to Multi-Region Fabric", ) migration_bgp_community: Optional[Union[Global[int], Default[None]]] = Field( - None, + default=Default[None](value=None), validation_alias=AliasPath("data", "migrationBgpCommunity"), description="Set BGP community during migration from BGP-core based network", ) - enable_management_region: Optional[Union[Global[bool], Default[Literal[False]], Variable]] = Field( - None, + enable_management_region: Union[Global[bool], Default[bool], Variable] = Field( + default=as_default(False), validation_alias=AliasPath("data", "enableManagementRegion"), description="Enable management region", ) - management_region: Optional[ManagementRegion] = Field( - None, + management_region: ManagementRegion = Field( + default_factory=ManagementRegion, validation_alias=AliasPath("data", "managementRegion"), description="Management Region", ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py index ebdab47e2..1b7a9acb4 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -3,7 +3,7 @@ from ipaddress import IPv6Address 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, _ParcelBase, as_default @@ -55,22 +55,26 @@ class Authentication(BaseModel): populate_by_name=True, ) authentication_keys: List[AuthenticationVariable] = Field( - ..., - serialization_alias="authenticationVariables", - validation_alias="authenticationVariables", + default=[], + serialization_alias="authenticationKeys", + validation_alias="authenticationKeys", description="Set MD5 authentication key", ) trusted_keys: Optional[Union[Variable, Global[List[int]], Default[None]]] = Field( None, - serialization_alias="trustedVariables", - validation_alias="trustedVariables", + serialization_alias="trustedKeys", + validation_alias="trustedKeys", description="Designate authentication key as trustworthy", ) class Leader(BaseModel): - enable: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( - None, description="Variable device as NTP Leader" + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + enable: Union[Variable, Global[bool], Default[bool]] = Field( + default=as_default(False), description="Variable device as NTP Leader" ) stratum: Optional[Union[Variable, Global[int], Default[None]]] = Field( None, description="Variable device as NTP Leader" @@ -86,6 +90,10 @@ class NTPParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - server: List[ServerItem] = Field(..., description="Configure NTP servers") - authentication: Optional[Authentication] = None - leader: Optional[Leader] = None + server: List[ServerItem] = Field( + default=[], validation_alias=AliasPath("data", "server"), description="Configure NTP servers" + ) + authentication: Authentication = Field( + default_factory=Authentication, validation_alias=AliasPath("data", "authentication") # type: ignore + ) + leader: Leader = Field(default_factory=Leader, validation_alias=AliasPath("data", "leader")) # type: ignore diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py index e83d6590a..be75768ee 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py @@ -14,41 +14,31 @@ class AdvertiseIpv4(BaseModel): model_config = ConfigDict( extra="forbid", ) - bgp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="BGP") - ospf: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="OSPF") - ospfv3: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), description="OSPFV3" - ) + bgp: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="BGP") + ospf: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="OSPF") + ospfv3: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="OSPFV3") connected: Union[Variable, Global[bool], Default[Optional[Literal[True, False]]]] = Field( default=as_default(False), description="Variable" ) static: Union[Variable, Global[bool], Default[Optional[Literal[True, False]]]] = Field( default=as_default(False), description="Variable" ) - eigrp: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), description="EIGRP" - ) - lisp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="LISP") - isis: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="ISIS") + eigrp: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="EIGRP") + lisp: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="LISP") + isis: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="ISIS") class AdvertiseIpv6(BaseModel): model_config = ConfigDict( extra="forbid", ) - bgp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="BGP") - ospf: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="OSPF") - connected: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), description="Variable" - ) - static: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), description="Variable" - ) - eigrp: Union[Variable, Global[bool], Default[Literal[False]]] = Field( - default=as_default(False), description="EIGRP" - ) - lisp: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="LISP") - isis: Union[Variable, Global[bool], Default[Literal[False]]] = Field(default=as_default(False), description="ISIS") + bgp: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="BGP") + ospf: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="OSPF") + connected: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="Variable") + static: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="Variable") + eigrp: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="EIGRP") + lisp: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="LISP") + isis: Union[Variable, Global[bool], Default[bool]] = Field(default=as_default(False), description="ISIS") class OMPParcel(_ParcelBase): @@ -58,13 +48,15 @@ class OMPParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - graceful_restart: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + graceful_restart: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), validation_alias=AliasPath("data", "gracefulRestart"), description="Graceful Restart for OMP", ) overlay_as: Union[Variable, Global[float], Default[None]] = Field( - default=as_default(None), validation_alias=AliasPath("data", "overlayAs"), description="Overlay AS Number" + default=Default[None](value=None), + validation_alias=AliasPath("data", "overlayAs"), + description="Overlay AS Number", ) send_path_limit: Union[Variable, Global[int], Default[int]] = Field( default=as_default(4), @@ -76,7 +68,7 @@ class OMPParcel(_ParcelBase): validation_alias=AliasPath("data", "ecmpLimit"), description="Set maximum number of OMP paths to install in cEdge route table", ) - shutdown: Union[Variable, Global[bool], Default[Literal[False]]] = Field( + shutdown: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(False), validation_alias=AliasPath("data", "shutdown"), description="Variable" ) omp_admin_distance_ipv4: Union[Variable, Global[int], Default[Optional[int]]] = Field( @@ -107,7 +99,7 @@ class OMPParcel(_ParcelBase): ) advertise_ipv4: AdvertiseIpv4 = Field(..., validation_alias="advertiseIpv4") advertise_ipv6: AdvertiseIpv6 = Field(..., validation_alias="advertiseIpv6") - ignore_region_path_length: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + ignore_region_path_length: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( None, validation_alias=AliasPath("data", "ignoreRegionPathLength"), description="Treat hierarchical and direct (secondary region) paths equally", diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py index c7a7ce7ae..7b82402df 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/security.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py @@ -4,11 +4,11 @@ from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default, as_variable IntegrityType = Literal["esp", "ip-udp-esp", "none", "ip-udp-esp-no-id"] ReplayWindow = Literal["64", "128", "256", "512", "1024", "2048", "4096", "8192"] -ReplayWindow2 = Literal["512"] +DefaultReplayWindow = Literal["512"] Tcp = Literal["aes-128-cmac", "hmac-sha-1", "hmac-sha-256"] @@ -26,7 +26,7 @@ class OneOfendChoice1(BaseModel): extra="forbid", populate_by_name=True, ) - infinite: Union[Variable, Global[bool], Default[Literal[True]]] = Field( + infinite: Union[Variable, Global[bool], Default[bool]] = Field( default=as_default(True), description="Infinite lifetime" ) @@ -56,7 +56,7 @@ class SendLifetime(BaseModel): extra="forbid", populate_by_name=True, ) - local: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + local: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( default=None, description="Configure Send lifetime Local" ) start_epoch: Global[float] = Field( @@ -79,7 +79,7 @@ class AcceptLifetime(BaseModel): extra="forbid", populate_by_name=True, ) - local: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + local: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( default=None, description="Configure Send lifetime Local" ) start_epoch: Global[float] = Field( @@ -106,13 +106,13 @@ class KeyItem(BaseModel): recv_id: Union[Global[int], Variable] = Field( ..., serialization_alias="recvId", validation_alias="recvId", description="Specify the Receiver ID" ) - include_tcp_options: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + include_tcp_options: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( default=None, serialization_alias="includeTcpOptions", validation_alias="includeTcpOptions", description="Configure Include TCP Options", ) - accept_ao_mismatch: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( + accept_ao_mismatch: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( default=None, serialization_alias="acceptAoMismatch", validation_alias="acceptAoMismatch", @@ -146,12 +146,13 @@ class SecurityParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - rekey: Optional[Union[Global[int], Variable, Default[int]]] = Field( - default=None, + rekey: Union[Global[int], Variable, Default[int]] = Field( + default=as_default(86400), + validation_alias=AliasPath("data", "rekey"), description="Set how often to change the AES key for DTLS connections", ) - replay_window: Optional[Union[Global[ReplayWindow], Variable, Default[ReplayWindow2]]] = Field( - default=None, + replay_window: Optional[Union[Global[ReplayWindow], Variable, Default[DefaultReplayWindow]]] = Field( + default=as_default("512", DefaultReplayWindow), validation_alias=AliasPath("data", "replayWindow"), description="Set the sliding replay window size", ) @@ -160,15 +161,17 @@ class SecurityParcel(_ParcelBase): validation_alias=AliasPath("data", "extendedArWindow"), description="Extended Anti-Replay Window", ) - integrity_type: Optional[Union[Global[List[IntegrityType]], Variable]] = Field( - default=None, + integrity_type: Union[Global[List[IntegrityType]], Variable] = Field( + default=as_variable("{{security_auth_type_inte}}"), validation_alias=AliasPath("data", "integrityType"), description="Set the authentication type for DTLS connections", ) - pairwise_keying: Optional[Union[Variable, Global[bool], Default[Literal[False]]]] = Field( - default=None, + pairwise_keying: Union[Variable, Global[bool], Default[bool]] = Field( + default=as_default(False), validation_alias=AliasPath("data", "pairwiseKeying"), description="Enable or disable IPsec pairwise-keying", ) - keychain: Optional[List[KeychainItem]] = Field(default=None, description="Configure a Keychain") - key: Optional[List[KeyItem]] = Field(default=None, description="Configure a Key") + keychain: List[KeychainItem] = Field( + default=[], validation_alias=AliasPath("data", "keychain"), description="Configure a Keychain" + ) + key: List[KeyItem] = Field(default=[], validation_alias=AliasPath("data", "key"), description="Configure a Key") diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py index ba0ec1caf..bbddffa0d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py @@ -5,7 +5,7 @@ from pydantic import AliasPath, BaseModel, ConfigDict, Field -from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default Authorization = Literal["read-only", "read-write"] Priv = Literal["aes-cfb-128", "aes-256-cfb-128"] @@ -140,31 +140,33 @@ class SNMPParcel(_ParcelBase): extra="forbid", populate_by_name=True, ) - shutdown: Optional[Union[Global[bool], Variable]] = Field( - default=None, validation_alias=AliasPath("data", "shutdown"), description="Enable or disable SNMP" + shutdown: Union[Global[bool], Variable, Default[bool]] = Field( + default=as_default(False), validation_alias=AliasPath("data", "shutdown"), description="Enable or disable SNMP" ) - contact: Optional[Union[Global[str], Variable, Default[None]]] = Field( - default=None, validation_alias=AliasPath("data", "contact"), description="Set the contact for this managed node" + contact: Union[Global[str], Variable, Default[None]] = Field( + default=Default[None](value=None), + validation_alias=AliasPath("data", "contact"), + description="Set the contact for this managed node", ) - location: Optional[Union[Global[str], Variable, Default[None]]] = Field( - default=None, + location: Union[Global[str], Variable, Default[None]] = Field( + default=Default[None](value=None), validation_alias=AliasPath("data", "location"), description="Set the physical location of this managed node", ) - view: Optional[List[ViewItem]] = Field( - default=None, validation_alias=AliasPath("data", "view"), description="Configure a view record" + view: List[ViewItem] = Field( + default=[], validation_alias=AliasPath("data", "view"), description="Configure a view record" ) - community: Optional[List[CommunityItem]] = Field( - default=None, validation_alias=AliasPath("data", "community"), description="Configure SNMP community" + community: List[CommunityItem] = Field( + default=[], validation_alias=AliasPath("data", "community"), description="Configure SNMP community" ) - group: Optional[List[GroupItem]] = Field( - default=None, validation_alias=AliasPath("data", "group"), description="Configure an SNMP group" + group: List[GroupItem] = Field( + default=[], validation_alias=AliasPath("data", "group"), description="Configure an SNMP group" ) - user: Optional[List[UserItem]] = Field( - default=None, validation_alias=AliasPath("data", "user"), description="Configure an SNMP user" + user: List[UserItem] = Field( + default=[], validation_alias=AliasPath("data", "user"), description="Configure an SNMP user" ) - target: Optional[List[TargetItem]] = Field( - default=None, + target: List[TargetItem] = Field( + default=[], validation_alias=AliasPath("data", "target"), description="Configure SNMP server to receive SNMP traps", ) diff --git a/catalystwan/tests/feature_profile/sdwan/test_model_creation.py b/catalystwan/tests/feature_profile/sdwan/test_model_creation.py deleted file mode 100644 index e69de29bb..000000000 From 47eac006ef58e12a21a11a41b4f78d487179d993 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Fri, 1 Mar 2024 17:32:41 +0100 Subject: [PATCH 21/21] Remove literals.py, define castable literals in normalizer file --- .../feature_profile/sdwan/system/__init__.py | 2 -- .../feature_profile/sdwan/system/literals.py | 18 ------------- .../sdwan/system/logging_parcel.py | 26 ++++++++++++------- .../tests/config_migration/test_normalizer.py | 2 +- .../converters/feature_template/normalizer.py | 12 +++++++-- 5 files changed, 28 insertions(+), 32 deletions(-) delete mode 100644 catalystwan/models/configuration/feature_profile/sdwan/system/literals.py diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index e29dcce7f..b4edc5b3d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -8,7 +8,6 @@ from .basic import BasicParcel from .bfd import BFDParcel from .global_parcel import GlobalParcel -from .literals import SYSTEM_LITERALS from .logging_parcel import LoggingParcel from .mrf import MRFParcel from .ntp import NTPParcel @@ -46,7 +45,6 @@ "SecurityParcel", "SNMPParcel", "AnySystemParcel", - "SYSTEM_LITERALS", ] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py b/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py deleted file mode 100644 index 5fc13df92..000000000 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/literals.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal - -Priority = Literal["information", "debugging", "notice", "warn", "error", "critical", "alert", "emergency"] -TlsVersion = Literal["TLSv1.1", "TLSv1.2"] -AuthType = Literal["Server", "Mutual"] -CypherSuite = Literal[ - "rsa-aes-cbc-sha2", - "rsa-aes-gcm-sha2", - "ecdhe-rsa-aes-gcm-sha2", - "aes-128-cbc-sha", - "aes-256-cbc-sha", - "dhe-aes-cbc-sha2", - "dhe-aes-gcm-sha2", - "ecdhe-ecdsa-aes-gcm-sha2", - "ecdhe-rsa-aes-cbc-sha2", -] - -SYSTEM_LITERALS = [Priority, TlsVersion, AuthType, CypherSuite] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py index 3fe345157..a2f2b409b 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -3,16 +3,24 @@ from pydantic import AliasPath, BaseModel, ConfigDict, Field from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase, as_default, as_global -from catalystwan.models.configuration.feature_profile.sdwan.system.literals import ( - AuthType, - CypherSuite, - Priority, - TlsVersion, -) -from catalystwan.utils.pydantic_validators import ConvertBoolToStringModel - -class TlsProfile(ConvertBoolToStringModel): +Priority = Literal["information", "debugging", "notice", "warn", "error", "critical", "alert", "emergency"] +TlsVersion = Literal["TLSv1.1", "TLSv1.2"] +AuthType = Literal["Server", "Mutual"] +CypherSuite = Literal[ + "rsa-aes-cbc-sha2", + "rsa-aes-gcm-sha2", + "ecdhe-rsa-aes-gcm-sha2", + "aes-128-cbc-sha", + "aes-256-cbc-sha", + "dhe-aes-cbc-sha2", + "dhe-aes-gcm-sha2", + "ecdhe-ecdsa-aes-gcm-sha2", + "ecdhe-rsa-aes-cbc-sha2", +] + + +class TlsProfile(BaseModel): profile: Global[str] version: Union[Global[TlsVersion], Default[TlsVersion]] = Field( default=as_default("TLSv1.1", TlsVersion), serialization_alias="tlsVersion", validation_alias="tlsVersion" diff --git a/catalystwan/tests/config_migration/test_normalizer.py b/catalystwan/tests/config_migration/test_normalizer.py index 96b521585..00ad48da6 100644 --- a/catalystwan/tests/config_migration/test_normalizer.py +++ b/catalystwan/tests/config_migration/test_normalizer.py @@ -85,7 +85,7 @@ def test_normalizer_handles_super_nested_input(self): # Assert self.assertDictEqual(expected_result, returned_result) - @patch("catalystwan.models.configuration.feature_profile.sdwan.system.literals.SYSTEM_LITERALS", [TestLiteral]) + @patch("catalystwan.utils.config_migration.converters.feature_template.normalizer.CastableLiterals", [TestLiteral]) def test_normalizer_literal_casting_when_literal_in_system_literals(self): # Arrange simple_input = {"in": "castable_literal"} diff --git a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py index 5adbb777a..205ea5797 100644 --- a/catalystwan/utils/config_migration/converters/feature_template/normalizer.py +++ b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py @@ -2,7 +2,15 @@ from typing import List, Union, get_args from catalystwan.api.configuration_groups.parcel import Global, as_global -from catalystwan.models.configuration.feature_profile.sdwan.system import SYSTEM_LITERALS +from catalystwan.models.configuration.feature_profile.sdwan.system.logging_parcel import ( + AuthType, + CypherSuite, + Priority, + TlsVersion, +) +from catalystwan.models.configuration.feature_profile.sdwan.system.mrf import EnableMrfMigration, Role + +CastableLiterals = [Priority, TlsVersion, AuthType, CypherSuite, Role, EnableMrfMigration] CastedTypes = Union[ Global[bool], @@ -41,7 +49,7 @@ def cast_value_to_global(value: Union[str, int, List[str], List[int]]) -> Casted return Global[IPv6Address](value=ipv6_address) except AddressValueError: pass - for literal in SYSTEM_LITERALS: + for literal in CastableLiterals: if value in get_args(literal): return Global[literal](value=value) # type: ignore