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/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/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index 4490f4bdb..a568c11d2 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, @@ -58,6 +60,13 @@ def envelope_parcel_data(self, handler: SerializerFunctionWrapHandler) -> Dict[s 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" diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index abdf84871..c067401e2 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -1,8 +1,11 @@ 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 pydantic import Json + +from catalystwan.endpoints.configuration.feature_profile.sdwan.system import SystemFeatureProfile from catalystwan.typed_list import DataSequence if TYPE_CHECKING: @@ -14,11 +17,12 @@ from catalystwan.models.configuration.feature_profile.common import ( FeatureProfileCreationPayload, FeatureProfileCreationResponse, + FeatureProfileInfo, + GetFeatureProfilesPayload, Parcel, ParcelCreationResponse, ) from catalystwan.models.configuration.feature_profile.sdwan.policy_object import ( - POLICY_OBJECT_PAYLOAD_ENDPOINT_MAPPING, AnyPolicyObjectParcel, ApplicationListParcel, AppProbeParcel, @@ -42,15 +46,32 @@ SecurityZoneListParcel, StandardCommunityParcel, TlocParcel, - URLAllowParcel, - URLBlockParcel, +) +from catalystwan.models.configuration.feature_profile.sdwan.system import ( + 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): @@ -103,6 +124,372 @@ 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 = 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) + + def delete_profile(self, profile_id: UUID) -> None: + """ + Delete System Feature Profile + """ + self.endpoint.delete_sdwan_system_feature_profile(profile_id) + + def get_schema( + self, + profile_id: UUID, + parcel_type: Type[AnySystemParcel], + ) -> Json: + """ + 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, parcel_type._get_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, + 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 System Parcel given parcel id + """ + + if not 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, 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, payload._get_parcel_type(), parcel_id, 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 + """ + return self.endpoint.delete(profile_id, parcel_type._get_parcel_type(), parcel_id) + + class PolicyObjectFeatureProfileAPI: """ SDWAN Feature Profile Policy Object APIs @@ -200,13 +587,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 +713,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 +731,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 +744,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 +754,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 +847,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/endpoints/configuration/feature_profile/sdwan/system.py b/catalystwan/endpoints/configuration/feature_profile/sdwan/system.py index ed788b40f..a24670268 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 @@ -8,16 +9,18 @@ FeatureProfileCreationResponse, FeatureProfileInfo, GetFeatureProfilesPayload, + Parcel, ParcelId, SchemaTypeQuery, ) +from catalystwan.models.configuration.feature_profile.sdwan.system import AnySystemParcel from catalystwan.typed_list import DataSequence 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,21 +38,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) - @post("/v1/feature-profile/sdwan/system/{system_id}/bfd") - def create_bfd_profile_parcel_for_system(self, system_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) - @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: + @get("/v1/feature-profile/sdwan/system/{profile_id}/{parcel_type}") + 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) -> 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, payload: AnySystemParcel) -> 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: AnySystemParcel) -> ParcelId: ... 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/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/config_migration.py b/catalystwan/models/configuration/config_migration.py index 36cf8673f..acd7b58ba 100644 --- a/catalystwan/models/configuration/config_migration.py +++ b/catalystwan/models/configuration/config_migration.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List, Literal, Union from pydantic import BaseModel, ConfigDict, Field from typing_extensions import Annotated @@ -45,8 +45,20 @@ 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): + 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): @@ -57,6 +69,7 @@ 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) config_groups: List[ConfigGroup] = Field( 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/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/configuration/feature_profile/sdwan/system/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py index f06f24ee0..b4edc5b3d 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/__init__.py @@ -1,16 +1,51 @@ -from typing import List, Mapping, Union +from typing import List, Union -from .aaa import AAA -from .bfd import BFD +from pydantic import Field +from typing_extensions import Annotated -SYSTEM_PAYLOAD_ENDPOINT_MAPPING: Mapping[type, str] = { - AAA: "aaa", - BFD: "bfd", -} +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 = Union[AAA, BFD] +AnySystemParcel = Annotated[ + Union[ + AAAParcel, + BFDParcel, + LoggingParcel, + BannerParcel, + BasicParcel, + GlobalParcel, + NTPParcel, + MRFParcel, + OMPParcel, + SecurityParcel, + SNMPParcel, + ], + Field(discriminator="type_"), +] -__all__ = ["AAA", "BFD", "AnySystemParcel", "SYSTEM_PAYLOAD_ENDPOINT_MAPPING"] +__all__ = [ + "AAAParcel", + "BFDParcel", + "LoggingParcel", + "BannerParcel", + "BasicParcel", + "GlobalParcel", + "NTPParcel", + "MRFParcel", + "OMPParcel", + "SecurityParcel", + "SNMPParcel", + "AnySystemParcel", +] 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/banner.py b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py new file mode 100644 index 000000000..0bdbcd681 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/banner.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import Literal, Union + +from pydantic import AliasPath, ConfigDict, Field + +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[EmptyString], str] = Field( + default=as_default("", EmptyString), validation_alias=AliasPath("data", "login") + ) + 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 new file mode 100644 index 000000000..e840d5e73 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/basic.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +from typing import List, Literal, Optional, Union + +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 + +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=as_default("UTC", DefaultTimezone), description="Set the timezone" + ) + + +class MobileNumberItem(BaseModel): + number: Union[Global[str], Variable] = Field(..., description="Mobile number, ex: 1231234414") + + +class Sms(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + 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, + serialization_alias="MobileNumber", + validation_alias="MobileNumber", + description="Set device’s geo fencing SMS phone number", + ) + + +class GeoFencing(BaseModel): + 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: Sms = Field(default_factory=Sms, description="Set device’s geo fencing SMS") # type: ignore + + +class GpsVariable(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + longitude: Union[Variable, Global[float], Default[None]] = Field( + default=Default[None](value=None), description="Set the device physical longitude" + ) + latitude: Union[Variable, Global[float], Default[None]] = Field( + default=Default[None](value=None), description="Set the device physical latitude" + ) + geo_fencing: GeoFencing = Field( + default_factory=GeoFencing, + serialization_alias="geoFencing", + validation_alias="geoFencing", + ) + + +class OnDemand(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + on_demand_enable: Union[Variable, Global[bool], Default[bool]] = Field( + default=as_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=as_default(10), + serialization_alias="onDemandIdleTimeout", + validation_alias="onDemandIdleTimeout", + description="Set the idle timeout for on-demand tunnels", + ) + + +class AffinityPerVrfItem(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + affinity_group_number: Union[ + Variable, + Global[int], + Default[None], + ] = Field( + 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=Default[None](value=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", + populate_by_name=True, + ) + clock: Clock = Field(default_factory=Clock, validation_alias=AliasPath("data", "clock")) + description: Union[Variable, Global[str], Default[None]] = Field( + 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=Default[None](value=None), + validation_alias=AliasPath("data", "location"), + description="Set the location of the device", + ) + gps_location: GpsVariable = Field( + default_factory=GpsVariable, + validation_alias=AliasPath("data", "gpsLocation"), + ) + device_groups: Union[Variable, Global[List[str]], Default[None]] = Field( + default=Default[None](value=None), + validation_alias=AliasPath("data", "deviceGroups"), + description="Device groups", + ) + controller_group_list: Optional[ + Union[ + Variable, + Global[List[int]], + Default[None], + ] + ] = Field( + None, + 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), + validation_alias=AliasPath("data", "overlayId"), + description="Set the Overlay ID", + ) + port_offset: Union[Variable, Global[int], Default[int]] = Field( + default=as_default(0), + 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[bool]] = Field( + default=as_default(True), + validation_alias=AliasPath("data", "portHop"), + description="Enable port hopping", + ) + control_session_pps: Optional[Union[Variable, Global[int], Default[int]]] = Field( + None, + validation_alias=AliasPath("data", "controlSessionPps"), + description="Set the policer rate for control sessions", + ) + track_transport: Optional[Union[Variable, Global[bool], Default[bool]]] = Field( + None, + validation_alias=AliasPath("data", "trackTransport"), + description="Configure tracking of transport", + ) + track_interface_tag: Optional[Union[Variable, Global[int], Default[None]]] = Field( + None, + 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", DefaultConsoleBaudRate), + validation_alias=AliasPath("data", "consoleBaudRate"), + description="Set the console baud rate", + ) + max_omp_sessions: Union[Variable, Global[int], Default[None]] = Field( + 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[bool]]] = Field( + None, + validation_alias=AliasPath("data", "multiTenant"), + description="Device is multi-tenant", + ) + track_default_gateway: Optional[ + Union[ + Variable, + Global[bool], + Default[bool], + ] + ] = Field( + None, + validation_alias=AliasPath("data", "trackDefaultGateway"), + description="Enable or disable default gateway tracking", + ) + tracker_dia_stabilize_status: Optional[ + Union[ + Variable, + Global[bool], + 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[bool]] = Field( + default=as_default(True), + 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, + validation_alias=AliasPath("data", "idleTimeout"), + 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[bool]]] = Field( + None, + 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, + validation_alias=AliasPath("data", "siteType"), + description="Site Type", + ) + affinity_group_number: Optional[ + Union[ + Variable, + Global[int], + Default[None], + ] + ] = Field( + None, + validation_alias=AliasPath("data", "affinityGroupNumber"), + description="Affinity Group Number", + ) + affinity_group_preference: Optional[ + Union[ + Variable, + Global[List[int]], + Default[None], + ] + ] = Field( + None, + validation_alias=AliasPath("data", "affinityGroupPreference"), + description="Affinity Group Preference", + ) + affinity_preference_auto: Optional[ + Union[ + Variable, + Global[bool], + Default[bool], + ] + ] = Field( + None, + validation_alias=AliasPath("data", "affinityPreferenceAuto"), + description="Affinity Group Preference Auto", + ) + affinity_per_vrf: Optional[List[AffinityPerVrfItem]] = Field( + None, + 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/bfd.py b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py index 123e9ccff..0e47e66e0 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/bfd.py @@ -1,45 +1,66 @@ -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.models.configuration.feature_profile.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) -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) - 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 new file mode 100644 index 000000000..95d39cd66 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/global_parcel.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from typing import Literal, Union + +from pydantic import AliasPath, 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[bool]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpHttpServer", + validation_alias="servicesGlobalServicesIpHttpServer", + ) + 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[bool]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpFtpPassive", + validation_alias="servicesGlobalServicesIpFtpPassive", + ) + 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[bool]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpArpProxy", + validation_alias="servicesGlobalServicesIpArpProxy", + ) + 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[bool]] = Field( + default=as_default(False), + serialization_alias="servicesGlobalServicesIpLineVty", + validation_alias="servicesGlobalServicesIpLineVty", + ) + cdp: Union[Variable, Global[bool], Default[bool]] = Field( + default=as_default(True), + serialization_alias="servicesGlobalServicesIpCdp", + validation_alias="servicesGlobalServicesIpCdp", + ) + 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[None]] = Field( + default=Default[None](value=None), + serialization_alias="servicesGlobalServicesIpSourceIntrf", + validation_alias="servicesGlobalServicesIpSourceIntrf", + ) + 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[bool]] = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsTcpKeepalivesOut", + validation_alias="globalOtherSettingsTcpKeepalivesOut", + ) + 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[bool]] = Field( + default=as_default(False), + serialization_alias="globalOtherSettingsUdpSmallServers", + validation_alias="globalOtherSettingsUdpSmallServers", + ) + 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[bool]] = Field( + default=as_default(False), + serialization_alias="globalOtherSettingsIPSourceRoute", + validation_alias="globalOtherSettingsIPSourceRoute", + ) + 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[bool]]) = Field( + default=as_default(True), + serialization_alias="globalOtherSettingsSnmpIfindexPersist", + validation_alias="globalOtherSettingsSnmpIfindexPersist", + ) + 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[int]] = Field( + default=as_default(300), + serialization_alias="globalSettingsNat64UdpTimeout", + validation_alias="globalSettingsNat64UdpTimeout", + ) + 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[None]] = Field( + default=Default[None](value=None), + serialization_alias="globalSettingsHttpAuthentication", + validation_alias="globalSettingsHttpAuthentication", + ) + 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_global: ServicesGlobal = Field( + default_factory=ServicesGlobal, validation_alias=AliasPath("data", "services_global") + ) 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..a2f2b409b --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/logging_parcel.py @@ -0,0 +1,159 @@ +from typing import List, Literal, Optional, Union + +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, _ParcelBase, as_default, as_global + +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" + ) + 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 Server(BaseModel): + name: Global[str] + 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: 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[Union[Global[bool], Default[bool]]] = Field( + default=None, + 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=as_default(10), serialization_alias="diskFileSize", validation_alias="diskFileSize" + ) + disk_file_rotate: Optional[Union[Global[int], Default[int]]] = Field( + default=as_default(10), serialization_alias="diskFileRotate", validation_alias="diskFileRotate" + ) + + +class Disk(BaseModel): + disk_enable: Optional[Global[bool]] = Field( + default=None, serialization_alias="diskEnable", validation_alias="diskEnable" + ) + file: File = Field(default_factory=File) + + +class LoggingParcel(_ParcelBase): + type_: Literal["logging"] = Field(default="logging", exclude=True) + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + 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[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 new file mode 100644 index 000000000..cd49d0fea --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/mrf.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import List, Literal, Optional, Union + +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +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"] + + +class ManagementRegion(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + 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( + default=Default[None](value=None), + serialization_alias="gatewayPreference", + validation_alias="gatewayPreference", + description="List of affinity group preferences for VRF", + ) + management_gateway: Union[Global[bool], Default[bool], Variable] = Field( + default=as_default(False), + 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", + populate_by_name=True, + ) + 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: 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: 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( + default=Default[None](value=None), + validation_alias=AliasPath("data", "migrationBgpCommunity"), + description="Set BGP community during migration from BGP-core based network", + ) + 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: 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 new file mode 100644 index 000000000..1b7a9acb4 --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/ntp.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from ipaddress import IPv6Address +from typing import List, Literal, Optional, Union + +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +from catalystwan.api.configuration_groups.parcel import Default, Global, Variable, _ParcelBase, as_default + + +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( + 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", + populate_by_name=True, + ) + 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", + populate_by_name=True, + ) + authentication_keys: List[AuthenticationVariable] = Field( + 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="trustedKeys", + validation_alias="trustedKeys", + description="Designate authentication key as trustworthy", + ) + + +class Leader(BaseModel): + 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" + ) + 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", + populate_by_name=True, + ) + 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 new file mode 100644 index 000000000..be75768ee --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/omp.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from typing import List, Literal, Optional, Union + +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[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[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[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): + type_: Literal["omp"] = Field(default="omp", exclude=True) + + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + 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=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), + 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[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( + 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[bool]]] = 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 new file mode 100644 index 000000000..7b82402df --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/security.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +from typing import List, Literal, Optional, Union + +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +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"] +DefaultReplayWindow = 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[bool]] = 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[bool]]] = 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[bool]]] = 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[bool]]] = Field( + default=None, + serialization_alias="includeTcpOptions", + validation_alias="includeTcpOptions", + description="Configure Include TCP Options", + ) + accept_ao_mismatch: Optional[Union[Variable, Global[bool], Default[bool]]] = 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: 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[DefaultReplayWindow]]] = Field( + default=as_default("512", DefaultReplayWindow), + 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: 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: Union[Variable, Global[bool], Default[bool]] = Field( + default=as_default(False), + validation_alias=AliasPath("data", "pairwiseKeying"), + description="Enable or disable IPsec pairwise-keying", + ) + 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 new file mode 100644 index 000000000..bbddffa0d --- /dev/null +++ b/catalystwan/models/configuration/feature_profile/sdwan/system/snmp.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +from ipaddress import IPv4Address, IPv6Address +from typing import List, Literal, Optional, Union + +from pydantic import AliasPath, BaseModel, ConfigDict, Field + +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"] +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: Union[Global[bool], Variable, Default[bool]] = Field( + default=as_default(False), validation_alias=AliasPath("data", "shutdown"), description="Enable or disable SNMP" + ) + 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: 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: List[ViewItem] = Field( + default=[], validation_alias=AliasPath("data", "view"), description="Configure a view record" + ) + community: List[CommunityItem] = Field( + default=[], validation_alias=AliasPath("data", "community"), description="Configure SNMP community" + ) + group: List[GroupItem] = Field( + default=[], validation_alias=AliasPath("data", "group"), description="Configure an SNMP group" + ) + user: List[UserItem] = Field( + default=[], validation_alias=AliasPath("data", "user"), description="Configure an SNMP user" + ) + target: List[TargetItem] = Field( + default=[], + validation_alias=AliasPath("data", "target"), + description="Configure SNMP server to receive SNMP traps", + ) 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 823805338..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 @@ -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 @@ -232,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/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..00ad48da6 --- /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.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"} + 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/tests/test_feature_profile_api.py b/catalystwan/tests/test_feature_profile_api.py new file mode 100644 index 000000000..2174f0770 --- /dev/null +++ b/catalystwan/tests/test_feature_profile_api.py @@ -0,0 +1,110 @@ +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) + + @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) 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..205ea5797 --- /dev/null +++ b/catalystwan/utils/config_migration/converters/feature_template/normalizer.py @@ -0,0 +1,75 @@ +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.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], + 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 CastableLiterals: + 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/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", +] diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 3ae015ed3..7d79db6af 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.endpoints.configuration_group import ConfigGroup 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.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,7 +19,8 @@ def log_progress(task: str, completed: int, total: int) -> None: def transform(ux1: UX1Config) -> UX2Config: ux2 = UX2Config() - for ft in ux1.templates.features: + # Feature Templates + for ft in ux1.templates.feature_templates: if ft.template_type in SUPPORTED_TEMPLATE_TYPES: ux2.profile_parcels.append(create_parcel_from_template(ft)) # Policy Lists @@ -66,14 +69,36 @@ 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 -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.profile_parcels: + # 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 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"