From 42b282bf48ec9933f5d3dd0a5fd76733f338c88e Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Tue, 7 May 2024 18:21:17 +0200 Subject: [PATCH] Fix feature profile api, add transport profile builder, add description to t1e1 model, add TransportAndManagementParcelPusher --- .../feature_profiles/builder_factory.py | 9 +- .../builders/feature_profiles/transport.py | 117 +++++++++++++++++- catalystwan/api/feature_profile_api.py | 6 +- .../feature_profile/sdwan/transport.py | 5 +- .../sdwan/transport/__init__.py | 21 +++- .../transport/wan/interface/t1e1serial.py | 7 +- .../policy/definition/intrusion_prevention.py | 9 +- .../creators/config_pusher.py | 2 +- .../creators/strategy/parcels.py | 40 +++++- .../factories/feature_profile_api.py | 8 +- .../factories/parcel_pusher.py | 2 + catalystwan/workflows/config_migration.py | 14 ++- pyproject.toml | 2 +- 13 files changed, 215 insertions(+), 27 deletions(-) diff --git a/catalystwan/api/builders/feature_profiles/builder_factory.py b/catalystwan/api/builders/feature_profiles/builder_factory.py index 5a9b98cb..dd060ff0 100644 --- a/catalystwan/api/builders/feature_profiles/builder_factory.py +++ b/catalystwan/api/builders/feature_profiles/builder_factory.py @@ -5,18 +5,25 @@ from catalystwan.api.builders.feature_profiles.other import OtherFeatureProfileBuilder from catalystwan.api.builders.feature_profiles.service import ServiceFeatureProfileBuilder from catalystwan.api.builders.feature_profiles.system import SystemFeatureProfileBuilder +from catalystwan.api.builders.feature_profiles.transport import TransportAndManagementProfileBuilder from catalystwan.exceptions import CatalystwanException from catalystwan.models.configuration.feature_profile.common import ProfileType if TYPE_CHECKING: from catalystwan.session import ManagerSession -FeatureProfileBuilder = Union[ServiceFeatureProfileBuilder, SystemFeatureProfileBuilder, OtherFeatureProfileBuilder] +FeatureProfileBuilder = Union[ + ServiceFeatureProfileBuilder, + SystemFeatureProfileBuilder, + OtherFeatureProfileBuilder, + TransportAndManagementProfileBuilder, +] BUILDER_MAPPING: Mapping[ProfileType, Callable] = { "service": ServiceFeatureProfileBuilder, "system": SystemFeatureProfileBuilder, "other": OtherFeatureProfileBuilder, + "transport": TransportAndManagementProfileBuilder, } diff --git a/catalystwan/api/builders/feature_profiles/transport.py b/catalystwan/api/builders/feature_profiles/transport.py index ab0e4ef4..8393dba9 100644 --- a/catalystwan/api/builders/feature_profiles/transport.py +++ b/catalystwan/api/builders/feature_profiles/transport.py @@ -1,3 +1,116 @@ +# Copyright 2023 Cisco Systems, Inc. and its affiliates +from __future__ import annotations + +import logging +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List +from uuid import UUID, uuid4 + +from catalystwan.api.feature_profile_api import TransportFeatureProfileAPI +from catalystwan.endpoints.configuration.feature_profile.sdwan.transport import TransportFeatureProfile +from catalystwan.exceptions import ManagerHTTPError +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload +from catalystwan.models.configuration.feature_profile.sdwan.transport import ( + AnyTransportSuperParcel, + AnyTransportVpnParcel, + AnyTransportVpnSubParcel, +) + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from catalystwan.session import ManagerSession + + class TransportAndManagementProfileBuilder: - # TODO: Implement after all parcels for Transport and Management are ready - pass + """ + A class for building system feature profiles. + """ + + def __init__(self, session: ManagerSession) -> None: + """ + Initialize a new instance of the Service class. + + Args: + session (ManagerSession): The ManagerSession object used for API communication. + profile_uuid (UUID): The UUID of the profile. + """ + self._profile: FeatureProfileCreationPayload + self._api = TransportFeatureProfileAPI(session) + self._endpoints = TransportFeatureProfile(session) + self._independent_items: List[AnyTransportSuperParcel] = [] + self._independent_items_vpns: Dict[UUID, AnyTransportVpnParcel] = {} + self._dependent_items_on_vpns: Dict[UUID, List[AnyTransportVpnSubParcel]] = defaultdict(list) + + def add_profile_name_and_description(self, feature_profile: FeatureProfileCreationPayload) -> None: + """ + Adds a name and description to the feature profile. + + Args: + name (str): The name of the feature profile. + description (str): The description of the feature profile. + + Returns: + None + """ + self._profile = feature_profile + + def add_parcel(self, parcel: AnyTransportSuperParcel) -> None: + """ + Adds a parcel to the feature profile. + + Args: + parcel (AnyTransportSuperParcel): The parcel to add. + + Returns: + None + """ + self._independent_items.append(parcel) + + def add_parcel_vpn(self, parcel: AnyTransportVpnParcel) -> UUID: + """ + Adds a VPN parcel to the builder. + + Args: + parcel (LanVpnParcel): The VPN parcel to add. + + Returns: + UUID: The UUID tag of the added VPN parcel. + """ + vpn_tag = uuid4() + logger.debug(f"Adding VPN parcel {parcel.parcel_name} with tag {vpn_tag}") + self._independent_items_vpns[vpn_tag] = parcel + return vpn_tag + + def add_vpn_subparcel(self, parcel: AnyTransportVpnSubParcel, vpn_tag: UUID) -> None: + """ + Adds a parcel to the feature profile. + + Args: + parcel (AnyTransportVpnSubParcel): The parcel to add. + + Returns: + None + """ + self._dependent_items_on_vpns[vpn_tag].append(parcel) + + def build(self) -> UUID: + """ + Builds the feature profile. + + Returns: + UUID: The UUID of the created feature profile. + """ + + profile_uuid = self._endpoints.create_transport_feature_profile(self._profile).id + try: + for parcel in self._independent_items: + self._api.create_parcel(profile_uuid, parcel) + for vpn_tag, vpn_parcel in self._independent_items_vpns.items(): + # TODO: Add subparcels to VPN + vpn_uuid = self._api.create_parcel(profile_uuid, vpn_parcel).id + for subparcel in self._dependent_items_on_vpns[vpn_tag]: + self._api.create_parcel(profile_uuid, subparcel, vpn_uuid) + except ManagerHTTPError as e: + logger.error(f"Error occured during building profile: {e.info}") + return profile_uuid diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index 5500bf86..7e57edc8 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -186,14 +186,14 @@ def create_parcel( Create Transport Parcel for selected profile_id based on payload type """ if vpn_uuid is not None: - vpn_parcel = self._get_vpn_parcel(profile_id, vpn_uuid) - if vpn_parcel.payload._get_parcel_type() == TransportVpnParcel._get_parcel_type(): + vpn_parcel = self._get_vpn_parcel(profile_id, vpn_uuid).payload + if vpn_parcel._get_parcel_type() == TransportVpnParcel._get_parcel_type(): return self.endpoint.create_transport_vpn_sub_parcel( profile_id, vpn_uuid, payload._get_parcel_type(), payload ) else: return self.endpoint.create_management_vpn_sub_parcel( - profile_id, vpn_uuid, payload._get_parcel_type(), vpn_parcel.parcel_id, payload + profile_id, vpn_uuid, payload._get_parcel_type(), payload ) return self.endpoint.create_transport_parcel(profile_id, payload._get_parcel_type(), payload) diff --git a/catalystwan/endpoints/configuration/feature_profile/sdwan/transport.py b/catalystwan/endpoints/configuration/feature_profile/sdwan/transport.py index 2949a987..fa40cf9b 100644 --- a/catalystwan/endpoints/configuration/feature_profile/sdwan/transport.py +++ b/catalystwan/endpoints/configuration/feature_profile/sdwan/transport.py @@ -18,7 +18,6 @@ from catalystwan.models.configuration.feature_profile.parcel import Parcel, ParcelCreationResponse, ParcelId from catalystwan.models.configuration.feature_profile.sdwan.transport import ( AnyTransportParcel, - AnyTransportVpnSubParcel, CellularControllerParcel, ) from catalystwan.models.configuration.feature_profile.sdwan.transport.vpn import ManagementVpnParcel @@ -60,14 +59,14 @@ def delete_transport_feature_profile(self, profile_id: UUID) -> None: @versions(supported_versions=(">=20.13"), raises=False) @post("/v1/feature-profile/sdwan/transport/{profile_id}/{parcel_type}") def create_transport_parcel( - self, profile_id: UUID, parcel_type: str, payload: AnyTransportParcel + self, profile_id: UUID, parcel_type: str, payload: _ParcelBase ) -> ParcelCreationResponse: ... @versions(supported_versions=(">=20.13"), raises=False) @post("/v1/feature-profile/sdwan/transport/{profile_id}/wan/vpn/{vpn_id}/{parcel_type}") def create_transport_vpn_sub_parcel( - self, profile_id: UUID, vpn_id: UUID, parcel_type: str, payload: AnyTransportVpnSubParcel + self, profile_id: UUID, vpn_id: UUID, parcel_type: str, payload: _ParcelBase ) -> ParcelCreationResponse: ... diff --git a/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py b/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py index 5f631469..87f74498 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/transport/__init__.py @@ -9,11 +9,21 @@ from .cellular_controller import CellularControllerParcel from .t1e1controller import T1E1ControllerParcel from .vpn import ManagementVpnParcel, TransportVpnParcel +from .wan.interface.t1e1serial import T1E1SerialParcel -AnyTransportVpnSubParcel = Annotated[Union[T1E1ControllerParcel, BGPParcel], Field(discriminator="type_")] - +AnyTransportVpnSubParcel = Annotated[ + Union[ + T1E1SerialParcel + # Add wan interfaces here + ], + Field(discriminator="type_"), +] +AnyTransportVpnParcel = Annotated[Union[ManagementVpnParcel, TransportVpnParcel], Field(discriminator="type_")] +AnyTransportSuperParcel = Annotated[ + Union[T1E1ControllerParcel, CellularControllerParcel, BGPParcel, T1E1ControllerParcel], Field(discriminator="type_") +] AnyTransportParcel = Annotated[ - Union[CellularControllerParcel, ManagementVpnParcel, TransportVpnParcel, AnyTransportVpnSubParcel], + Union[AnyTransportSuperParcel, AnyTransportVpnParcel, AnyTransportVpnSubParcel], Field(discriminator="type_"), ] @@ -23,8 +33,9 @@ "ManagementVpnParcel", "TransportVpnParcel", "AnyTransportParcel", - "AnyTransportVpnSubParcel", - "T1E1ControllerParcel", + "AnyTransportSuperParcel", + "AnyTransportVpnSubParcel" "T1E1ControllerParcel", + "T1E1SerialParcel", ] diff --git a/catalystwan/models/configuration/feature_profile/sdwan/transport/wan/interface/t1e1serial.py b/catalystwan/models/configuration/feature_profile/sdwan/transport/wan/interface/t1e1serial.py index ff4b38f2..b92b2203 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/transport/wan/interface/t1e1serial.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/transport/wan/interface/t1e1serial.py @@ -198,11 +198,14 @@ class Advanced(BaseModel): populate_by_name=True, ) ip_mtu: Optional[Union[Global[int], Variable, Default[int]]] = Field( - default=None, validation_alias="ipMtu", serialization_alias="ipMtu" + default=None, validation_alias="ipMtu", serialization_alias="ipMtu", description="Value cannot be less than 576" ) mtu: Optional[Union[Global[int], Variable, Default[int]]] = Field(default=None) tcp_mss_adjust: Optional[Union[Global[int], Variable, Default[None]]] = Field( - default=None, validation_alias="tcpMssAdjust", serialization_alias="tcpMssAdjust" + default=None, + validation_alias="tcpMssAdjust", + serialization_alias="tcpMssAdjust", + description="Value must cannot be greater then 1460", ) tloc_extension: Optional[Union[Global[str], Variable, Default[None]]] = Field( default=None, validation_alias="tlocExtension", serialization_alias="tlocExtension" diff --git a/catalystwan/models/policy/definition/intrusion_prevention.py b/catalystwan/models/policy/definition/intrusion_prevention.py index c7a0d91f..d1b78619 100644 --- a/catalystwan/models/policy/definition/intrusion_prevention.py +++ b/catalystwan/models/policy/definition/intrusion_prevention.py @@ -2,7 +2,7 @@ from typing import List, Literal, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from catalystwan.models.common import PolicyModeType, VpnId from catalystwan.models.policy.policy_definition import ( @@ -31,6 +31,13 @@ class IntrusionPreventionDefinition(BaseModel): default=False, validation_alias="customSignature", serialization_alias="customSignature" ) + @field_validator("signature_white_list", mode="before") + @classmethod + def convert_empty_dict_to_none(cls, value): + if not value: + return None + return value + class IntrusionPreventionPolicy(PolicyDefinitionBase): type: Literal["intrusionPrevention"] = "intrusionPrevention" diff --git a/catalystwan/utils/config_migration/creators/config_pusher.py b/catalystwan/utils/config_migration/creators/config_pusher.py index 0214b3ac..39667aeb 100644 --- a/catalystwan/utils/config_migration/creators/config_pusher.py +++ b/catalystwan/utils/config_migration/creators/config_pusher.py @@ -72,7 +72,7 @@ def _create_feature_profile_and_parcels(self, feature_profiles_ids: List[UUID]) f"and parcels: {transformed_feature_profile.header.subelements}" ) profile_type = cast(ProfileType, transformed_feature_profile.header.type) - if profile_type in ["policy-object", "transport"]: + if profile_type in ["policy-object"]: # TODO: Add builders for those profiles logger.debug(f"Skipping profile: {transformed_feature_profile.feature_profile.name}") continue diff --git a/catalystwan/utils/config_migration/creators/strategy/parcels.py b/catalystwan/utils/config_migration/creators/strategy/parcels.py index a01080fa..b3ff36ec 100644 --- a/catalystwan/utils/config_migration/creators/strategy/parcels.py +++ b/catalystwan/utils/config_migration/creators/strategy/parcels.py @@ -1,13 +1,18 @@ +import logging from collections import defaultdict from typing import Dict, List, cast from uuid import UUID +from catalystwan.api.builders.feature_profiles.transport import TransportAndManagementProfileBuilder from catalystwan.models.configuration.config_migration import TransformedParcel from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload, ProfileType from catalystwan.models.configuration.feature_profile.sdwan.service import AnyAssociatoryParcel from catalystwan.models.configuration.feature_profile.sdwan.service.lan.vpn import LanVpnParcel +from catalystwan.models.configuration.feature_profile.sdwan.transport.vpn import ManagementVpnParcel, TransportVpnParcel from catalystwan.session import ManagerSession +logger = logging.getLogger(__name__) + class ParcelPusher: """ @@ -65,9 +70,11 @@ def push( self.builder.add_parcel(parcel) # type: ignore else: vpn_tag = self.builder.add_parcel_vpn(parcel) # type: ignore - for transformed_subparcel in [ - all_parcels.get(element) for element in transformed_parcel.header.subelements - ]: + for element in transformed_parcel.header.subelements: + transformed_subparcel = all_parcels.get(element) + if transformed_subparcel is None: + logger.error(f"Subparcel {element} not found in profile parcels. Skipping.") + continue parcel = self._resolve_parcel_naming(transformed_subparcel) # type: ignore self.builder.add_parcel_vpn_subparcel(vpn_tag, parcel) # type: ignore self.builder.add_profile_name_and_description(feature_profile) @@ -86,3 +93,30 @@ def _resolve_parcel_naming(self, transformed_subparcel: TransformedParcel) -> An parcel = parcel.model_copy(deep=True) parcel.parcel_name += f"_{count_value}" return parcel + + +class TransportAndManagementParcelPusher(ParcelPusher): + """ + Parcel pusher for transport and management feature profiles. + """ + + builder: TransportAndManagementProfileBuilder + + def push( + self, + feature_profile: FeatureProfileCreationPayload, + target_parcels: List[TransformedParcel], + all_parcels: Dict[UUID, TransformedParcel], + ) -> UUID: + for transformed_parcel in target_parcels: + parcel = transformed_parcel.parcel + if isinstance(parcel, (ManagementVpnParcel, TransportVpnParcel)): + vpn_tag = self.builder.add_parcel_vpn(parcel) # type: ignore + for element in transformed_parcel.header.subelements: + transformed_subparcel = all_parcels.get(element) + if transformed_subparcel is None: + logger.error(f"Subparcel {element} not found in profile parcels. Skipping.") + continue + self.builder.add_vpn_subparcel(transformed_subparcel.parcel, vpn_tag) # type: ignore + self.builder.add_profile_name_and_description(feature_profile) + return self.builder.build() diff --git a/catalystwan/utils/config_migration/factories/feature_profile_api.py b/catalystwan/utils/config_migration/factories/feature_profile_api.py index a23faf96..b6728ccc 100644 --- a/catalystwan/utils/config_migration/factories/feature_profile_api.py +++ b/catalystwan/utils/config_migration/factories/feature_profile_api.py @@ -5,6 +5,7 @@ PolicyObjectFeatureProfileAPI, ServiceFeatureProfileAPI, SystemFeatureProfileAPI, + TransportFeatureProfileAPI, ) from catalystwan.models.configuration.feature_profile.common import ProfileType from catalystwan.session import ManagerSession @@ -14,10 +15,15 @@ "other": OtherFeatureProfileAPI, "policy-object": PolicyObjectFeatureProfileAPI, "service": ServiceFeatureProfileAPI, + "transport": TransportFeatureProfileAPI, } FeatureProfile = Union[ - SystemFeatureProfileAPI, OtherFeatureProfileAPI, PolicyObjectFeatureProfileAPI, ServiceFeatureProfileAPI + SystemFeatureProfileAPI, + OtherFeatureProfileAPI, + PolicyObjectFeatureProfileAPI, + ServiceFeatureProfileAPI, + TransportFeatureProfileAPI, ] diff --git a/catalystwan/utils/config_migration/factories/parcel_pusher.py b/catalystwan/utils/config_migration/factories/parcel_pusher.py index 604ed9ba..788185bb 100644 --- a/catalystwan/utils/config_migration/factories/parcel_pusher.py +++ b/catalystwan/utils/config_migration/factories/parcel_pusher.py @@ -8,6 +8,7 @@ ParcelPusher, ServiceParcelPusher, SimpleParcelPusher, + TransportAndManagementParcelPusher, ) logger = logging.getLogger(__name__) @@ -16,6 +17,7 @@ "other": SimpleParcelPusher, "system": SimpleParcelPusher, "service": ServiceParcelPusher, + "transport": TransportAndManagementParcelPusher, } diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 08286a45..0c43d659 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -80,6 +80,7 @@ "cedge_igmp", "cedge_multicast", "cedge_pim", + "vpn-interface-t1-e1", ] FEATURE_PROFILE_SYSTEM = [ @@ -109,7 +110,13 @@ "cisco_snmp", ] -FEATURE_PROFILE_TRANSPORT = ["dhcp", "cisco_dhcp_server", "dhcp-server", CISCO_VPN_TRANSPORT_AND_MANAGEMENT] +FEATURE_PROFILE_TRANSPORT = [ + "dhcp", + "cisco_dhcp_server", + "dhcp-server", + "vpn-interface-t1-e1", + CISCO_VPN_TRANSPORT_AND_MANAGEMENT, +] FEATURE_PROFILE_OTHER = [ "cisco_thousandeyes", @@ -117,7 +124,6 @@ ] FEATURE_PROFILE_SERVICE = [ - "cisco_vpn", "cisco_vpn_interface_gre", "vpn-vsmart-interface", "vpn-vedge-interface", @@ -189,7 +195,7 @@ def transform(ux1: UX1Config) -> UX2Config: fp_transport_and_management_uuid = uuid4() transformed_fp_transport_and_management = TransformedFeatureProfile( header=TransformHeader( - type="transport_and_management", + type="transport", origin=fp_transport_and_management_uuid, ), feature_profile=FeatureProfileCreationPayload( @@ -221,7 +227,7 @@ def transform(ux1: UX1Config) -> UX2Config: header=TransformHeader( type="config_group", origin=UUID(dt.template_id), - subelements=set([fp_system_uuid, fp_other_uuid, fp_service_uuid]), + subelements=set([fp_system_uuid, fp_other_uuid, fp_service_uuid, fp_transport_and_management_uuid]), ), config_group=ConfigGroupCreationPayload( name=dt.template_name, diff --git a/pyproject.toml b/pyproject.toml index 337d0a63..1ea7bc3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "catalystwan" -version = "0.33.4dev3" +version = "0.33.4dev5" description = "Cisco Catalyst WAN SDK for Python" authors = ["kagorski "] readme = "README.md"