From c7de06cc9cdfd1ae712345eca7d2c499b4673c05 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Fri, 15 Mar 2024 16:10:30 +0100 Subject: [PATCH 1/5] Start work --- catalystwan/api/feature_profile_api.py | 78 +++++----- .../device_template/device_template.py | 2 + .../sdwan/other/test_models.py | 4 +- .../sdwan/system/test_models.py | 26 ++-- .../models/configuration/config_migration.py | 4 +- .../configuration/feature_profile/common.py | 1 + .../models/configuration/profile_type.py | 7 +- catalystwan/tests/test_feature_profile_api.py | 8 +- .../config_migration/creators/config_group.py | 134 +++-------------- catalystwan/workflows/config_migration.py | 141 +++++++++++++----- 10 files changed, 186 insertions(+), 219 deletions(-) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index e5c9a097d..c553ebfd1 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -178,21 +178,21 @@ def get( 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: AnyOtherParcel) -> ParcelCreationResponse: + def create_parcel(self, profile_id: UUID, payload: AnyOtherParcel) -> ParcelCreationResponse: """ Create Other 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: AnyOtherParcel, parcel_id: UUID) -> ParcelCreationResponse: + def update_parcel(self, profile_id: UUID, payload: AnyOtherParcel, parcel_id: UUID) -> ParcelCreationResponse: """ Update Other Parcel for selected profile_id based on payload type """ return self.endpoint.update(profile_id, payload._get_parcel_type(), parcel_id, payload) - def delete(self, profile_id: UUID, parcel_type: Type[AnyOtherParcel], parcel_id: UUID) -> None: + def delete_parcel(self, profile_id: UUID, parcel_type: Type[AnyOtherParcel], parcel_id: UUID) -> None: """ Delete Other Parcel for selected profile_id based on payload type """ @@ -276,7 +276,7 @@ def get_schema( return self.endpoint.get_schema(profile_id, parcel_type._get_parcel_type()) @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[AAAParcel], @@ -284,7 +284,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[BFDParcel], @@ -292,7 +292,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[LoggingParcel], @@ -300,7 +300,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[BannerParcel], @@ -308,7 +308,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[BasicParcel], @@ -316,7 +316,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[GlobalParcel], @@ -324,7 +324,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[NTPParcel], @@ -332,7 +332,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[MRFParcel], @@ -340,7 +340,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[OMPParcel], @@ -348,7 +348,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[SecurityParcel], @@ -356,7 +356,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[SNMPParcel], @@ -366,7 +366,7 @@ def get( # get by id @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[AAAParcel], @@ -375,7 +375,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[BFDParcel], @@ -384,7 +384,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[LoggingParcel], @@ -393,7 +393,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[BannerParcel], @@ -402,7 +402,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[BasicParcel], @@ -411,7 +411,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[GlobalParcel], @@ -420,7 +420,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[NTPParcel], @@ -429,7 +429,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[MRFParcel], @@ -438,7 +438,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[OMPParcel], @@ -447,7 +447,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[SecurityParcel], @@ -456,7 +456,7 @@ def get( ... @overload - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[SNMPParcel], @@ -464,7 +464,7 @@ def get( ) -> DataSequence[Parcel[SNMPParcel]]: ... - def get( + def get_parcels( self, profile_id: UUID, parcel_type: Type[AnySystemParcel], @@ -478,7 +478,7 @@ def get( 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: + def create_parcel(self, profile_id: UUID, payload: AnySystemParcel) -> ParcelCreationResponse: """ Create System Parcel for selected profile_id based on payload type """ @@ -493,7 +493,7 @@ def update(self, profile_id: UUID, payload: AnySystemParcel, parcel_id: UUID) -> return self.endpoint.update(profile_id, payload._get_parcel_type(), parcel_id, payload) @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[AAAParcel], @@ -502,7 +502,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[BFDParcel], @@ -511,7 +511,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[LoggingParcel], @@ -520,7 +520,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[BannerParcel], @@ -529,7 +529,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[BasicParcel], @@ -538,7 +538,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[GlobalParcel], @@ -547,7 +547,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[NTPParcel], @@ -556,7 +556,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[MRFParcel], @@ -565,7 +565,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[OMPParcel], @@ -574,7 +574,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[SecurityParcel], @@ -583,7 +583,7 @@ def delete( ... @overload - def delete( + def delete_parcel( self, profile_id: UUID, parcel_type: Type[SNMPParcel], @@ -591,7 +591,7 @@ def delete( ) -> None: ... - def delete(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id: UUID) -> None: + def delete_parcel(self, profile_id: UUID, parcel_type: Type[AnySystemParcel], parcel_id: UUID) -> None: """ Delete System Parcel for selected profile_id based on payload type """ diff --git a/catalystwan/api/templates/device_template/device_template.py b/catalystwan/api/templates/device_template/device_template.py index 85103afc2..a6925c696 100644 --- a/catalystwan/api/templates/device_template/device_template.py +++ b/catalystwan/api/templates/device_template/device_template.py @@ -5,6 +5,7 @@ import logging from pathlib import Path from typing import TYPE_CHECKING, Final, List +from uuid import UUID from jinja2 import DebugUndefined, Environment, FileSystemLoader, meta # type: ignore from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -42,6 +43,7 @@ class DeviceTemplate(BaseModel): >>> session.api.templates.create(device_template) """ + id: UUID = Field(alias="templateId") template_name: str = Field(alias="templateName") template_description: str = Field(alias="templateDescription") general_templates: List[GeneralTemplate] = Field(default=[], alias="generalTemplates") diff --git a/catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py b/catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py index 361eb61ad..cde5f51c5 100644 --- a/catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py +++ b/catalystwan/integration_tests/feature_profile/sdwan/other/test_models.py @@ -25,7 +25,7 @@ def test_when_default_values_thousandeyes_parcel_expect_successful_post(self): parcel_description="ThousandEyes Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.other.create(self.profile_id, te_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.other.create_parcel(self.profile_id, te_parcel).id # Assert assert parcel_id @@ -45,7 +45,7 @@ def test_when_default_values_ucse_parcel_expect_successful_post(self): ), ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.other.create(self.profile_id, ucse_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.other.create_parcel(self.profile_id, ucse_parcel).id # Assert assert parcel_id diff --git a/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py b/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py index 9bc5aad62..814c88bf2 100644 --- a/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py +++ b/catalystwan/integration_tests/feature_profile/sdwan/system/test_models.py @@ -34,7 +34,7 @@ def test_when_default_values_banner_parcel_expect_successful_post(self): parcel_description="Banner Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, banner_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, banner_parcel).id # Assert assert parcel_id @@ -47,7 +47,7 @@ def test_when_fully_specified_banner_parcel_expect_successful_post(self): banner_parcel.add_login("Login") banner_parcel.add_motd("Hello! Welcome to the network!") # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, banner_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, banner_parcel).id # Assert assert parcel_id @@ -58,7 +58,7 @@ def test_when_default_values_logging_parcel_expect_successful_post(self): parcel_description="Logging Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, logging_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, logging_parcel).id # Assert assert parcel_id @@ -102,7 +102,7 @@ def test_when_fully_specified_logging_parcel_expect_successful_post(self): profile_properties="TLSProfile", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, logging_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, logging_parcel).id # Assert assert parcel_id @@ -113,7 +113,7 @@ def test_when_default_values_bfd_parcel_expect_successful_post(self): parcel_description="BFD Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, bfd_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, bfd_parcel).id # Assert assert parcel_id @@ -131,7 +131,7 @@ def test_when_fully_specified_bfd_parcel_expect_successful_post(self): bfd_parcel.add_color(color="biz-internet") bfd_parcel.add_color(color="public-internet") # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, bfd_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, bfd_parcel).id # Assert assert parcel_id @@ -142,7 +142,7 @@ def test_when_default_values_basic_parcel_expect_successful_post(self): parcel_description="Basic Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, basic_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, basic_parcel).id # Assert assert parcel_id @@ -153,7 +153,7 @@ def test_when_default_values_security_parcel_expect_successful_post(self): parcel_description="Security Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, security_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, security_parcel).id # Assert assert parcel_id @@ -164,7 +164,7 @@ def test_when_default_values_ntp_parcel_expect_successful_post(self): parcel_description="NTP Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, ntp_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, ntp_parcel).id # Assert assert parcel_id @@ -175,7 +175,7 @@ def test_when_default_values_global_parcel_expect_successful_post(self): parcel_description="Global Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, global_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, global_parcel).id # Assert assert parcel_id @@ -186,7 +186,7 @@ def test_when_default_values_mrf_parcel_expect_successful_post(self): parcel_description="MRF Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, mrf_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, mrf_parcel).id # Assert assert parcel_id @@ -197,7 +197,7 @@ def test_when_default_values_snmp_parcel_expect_successful_post(self): parcel_description="SNMP Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, snmp_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, snmp_parcel).id # Assert assert parcel_id @@ -208,7 +208,7 @@ def test_when_default_values_omp_parcel_expect_successful_post(self): parcel_description="OMP Parcel", ) # Act - parcel_id = self.session.api.sdwan_feature_profiles.system.create(self.profile_id, omp_parcel).id + parcel_id = self.session.api.sdwan_feature_profiles.system.create_parcel(self.profile_id, omp_parcel).id # Assert assert parcel_id diff --git a/catalystwan/models/configuration/config_migration.py b/catalystwan/models/configuration/config_migration.py index 75f9c5e6b..f2f370681 100644 --- a/catalystwan/models/configuration/config_migration.py +++ b/catalystwan/models/configuration/config_migration.py @@ -1,6 +1,6 @@ # Copyright 2024 Cisco Systems, Inc. and its affiliates -from typing import List, Union +from typing import List, Set, Union from uuid import UUID from pydantic import BaseModel, ConfigDict, Field @@ -68,7 +68,7 @@ class TransformHeader(BaseModel): "Type discriminator is not present in many UX2 item payloads" ) origin: UUID = Field(description="Original UUID of converted item") - subelements: List[UUID] = [] + subelements: Set[UUID] = Field(default_factory=set) class TransformedTopologyGroup(BaseModel): diff --git a/catalystwan/models/configuration/feature_profile/common.py b/catalystwan/models/configuration/feature_profile/common.py index 48b78872b..585dd5207 100644 --- a/catalystwan/models/configuration/feature_profile/common.py +++ b/catalystwan/models/configuration/feature_profile/common.py @@ -68,6 +68,7 @@ "application-priority", "policy-object", "embedded-security", + "other", ] SchemaType = Literal[ diff --git a/catalystwan/models/configuration/profile_type.py b/catalystwan/models/configuration/profile_type.py index 272ba310e..f332a670f 100644 --- a/catalystwan/models/configuration/profile_type.py +++ b/catalystwan/models/configuration/profile_type.py @@ -2,9 +2,4 @@ from typing import Literal -ProfileType = Literal[ - "transport", - "system", - "cli", - "service", -] +ProfileType = Literal["transport", "system", "cli", "service", "other"] diff --git a/catalystwan/tests/test_feature_profile_api.py b/catalystwan/tests/test_feature_profile_api.py index 2174f0770..955f4ed0c 100644 --- a/catalystwan/tests/test_feature_profile_api.py +++ b/catalystwan/tests/test_feature_profile_api.py @@ -48,7 +48,7 @@ def test_delete_method_with_valid_arguments(self, parcel, expected_path, mock_en api.endpoint = mock_endpoint # Act - api.delete(self.profile_uuid, parcel, self.parcel_uuid) + api.delete_parcel(self.profile_uuid, parcel, self.parcel_uuid) # Assert mock_endpoint.delete.assert_called_once_with(self.profile_uuid, expected_path, self.parcel_uuid) @@ -62,7 +62,7 @@ def test_get_method_with_valid_arguments(self, parcel, expected_path, mock_endpo api.endpoint = mock_endpoint # Act - api.get(self.profile_uuid, parcel, self.parcel_uuid) + api.get_parcels(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) @@ -76,7 +76,7 @@ def test_get_all_method_with_valid_arguments(self, parcel, expected_path, mock_e api.endpoint = mock_endpoint # Act - api.get(self.profile_uuid, parcel) + api.get_parcels(self.profile_uuid, parcel) # Assert mock_endpoint.get_all.assert_called_once_with(self.profile_uuid, expected_path) @@ -90,7 +90,7 @@ def test_create_method_with_valid_arguments(self, parcel, expected_path, mock_en api.endpoint = mock_endpoint # Act - api.create(self.profile_uuid, parcel) + api.create_parcel(self.profile_uuid, parcel) # Assert mock_endpoint.create.assert_called_once_with(self.profile_uuid, expected_path, parcel) diff --git a/catalystwan/utils/config_migration/creators/config_group.py b/catalystwan/utils/config_migration/creators/config_group.py index 4294b5460..638ed70ed 100644 --- a/catalystwan/utils/config_migration/creators/config_group.py +++ b/catalystwan/utils/config_migration/creators/config_group.py @@ -1,123 +1,23 @@ -import logging -from datetime import datetime -from typing import List +from dataclasses import dataclass +from typing import Dict, 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 +from catalystwan.models.configuration.config_migration import ( + TransformedConfigGroup, + TransformedFeatureProfile, + TransformedParcel, +) +from catalystwan.models.configuration.profile_type import ProfileType -class ConfigGroupCreator: - """ - Creates a configuration group and attach feature profiles for migrating UX1 templates to UX2. - """ +@dataclass +class UX2ConfigMap: + config_group_map: Dict[UUID, TransformedConfigGroup] + feature_profile_map: Dict[UUID, TransformedFeatureProfile] + parcel_map: Dict[UUID, TransformedParcel] - 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 +@dataclass +class UX2ConfigRollback: + config_groups_ids: List[UUID] + feature_profiles_ids: List[UUID, ProfileType] diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 7b6cc71a1..ca50afcd4 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -1,9 +1,9 @@ import logging -from typing import Callable +from typing import Callable, List, Literal, Tuple from uuid import UUID, uuid4 from catalystwan.api.policy_api import POLICY_LIST_ENDPOINTS_MAP -from catalystwan.endpoints.configuration_group import ConfigGroup, ConfigGroupCreationPayload +from catalystwan.endpoints.configuration_group import ConfigGroupCreationPayload from catalystwan.models.configuration.config_migration import ( TransformedConfigGroup, TransformedFeatureProfile, @@ -16,7 +16,6 @@ from catalystwan.session import ManagerSession from catalystwan.utils.config_migration.converters.feature_template import create_parcel_from_template from catalystwan.utils.config_migration.converters.policy.policy_lists import convert as convert_policy_list -from catalystwan.utils.config_migration.creators.config_group import ConfigGroupCreator from catalystwan.utils.config_migration.device_templates import flatten_general_templates logger = logging.getLogger(__name__) @@ -43,8 +42,8 @@ "omp-vsmart", "cisco_ntp", "ntp", - "bgp", - "cisco_bgp", + # "bgp", + # "cisco_bgp", "cisco_thousandeyes", "ucse", "dhcp", @@ -73,8 +72,8 @@ "omp-vsmart", "cisco_ntp", "ntp", - "bgp", - "cisco_bgp", + # "bgp", + # "cisco_bgp", ] FEATURE_PROFILE_TRANSPORT = ["dhcp", "cisco_dhcp_server", "dhcp-server"] @@ -133,17 +132,17 @@ def transform(ux1: UX1Config) -> UX2Config: for template in templates: # Those feature templates IDs are real UUIDs and are used to map to the feature profiles if template.templateType in FEATURE_PROFILE_SYSTEM: - transformed_fp_system.header.subelements.append(UUID(template.templateId)) + transformed_fp_system.header.subelements.add(UUID(template.templateId)) elif template.templateType in FEATURE_PROFILE_TRANSPORT: - transformed_fp_transport.header.subelements.append(UUID(template.templateId)) + transformed_fp_transport.header.subelements.add(UUID(template.templateId)) elif template.templateType in FEATURE_PROFILE_OTHER: - transformed_fp_other.header.subelements.append(UUID(template.templateId)) + transformed_fp_other.header.subelements.add(UUID(template.templateId)) transformed_cg = TransformedConfigGroup( header=TransformHeader( type="config_group", - origin=uuid4(), - subelements=[fp_system_uuid, fp_transport_uuid, fp_other_uuid], + origin=dt.id, + subelements=set([fp_system_uuid, fp_transport_uuid, fp_other_uuid]), ), config_group=ConfigGroupCreationPayload( name=dt.template_name, @@ -230,27 +229,97 @@ def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, in return ux1 -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 +def push_ux2_config( + session: ManagerSession, config: UX2Config, progress: Callable[[str, int, int], None] = log_progress +): + # Create mapping from origin ids + # do dataclass + mapping = { + "config_group": {item.header.origin: item for item in config.config_groups}, + "feature_profile": {item.header.origin: item for item in config.feature_profiles}, + "profile_parcels": {item.header.origin: item for item in config.profile_parcels}, + } + rollback_config_groups_ids: List[UUID] = [] + rollback_feature_profiles_ids: List[Tuple[UUID, Literal["system", "other", "transport"]]] = [] + + try: + for config_group in config.config_groups: + config_group_profiles = [] + + for feature_profile_id in config_group.header.subelements: + feature_profile = mapping["feature_profile"][feature_profile_id] + + if feature_profile.header.type == "system": + # Feature Profile System Parcels don't have references to other parcels so we can create them directly + system_api = session.api.sdwan_feature_profiles.system + + feature_profile_system = system_api.create_profile( + name=feature_profile.feature_profile.name, + description=feature_profile.feature_profile.description, + ) + config_group_profiles.append(feature_profile_system) + rollback_feature_profiles_ids.append((feature_profile_system.id, "system")) + + logger.info( + f"Creating Feature Profile {feature_profile_system.id} {feature_profile.feature_profile.name}" + ) + logger.info( + f"Subelements Feature Profile {feature_profile_system.id} {feature_profile.header.subelements}" + ) + + for parcel_id in feature_profile.header.subelements: + logger.info(f"Creating Parcel {parcel_id} in Feature Profile {feature_profile_system.id}") + + parcel = mapping["profile_parcels"][parcel_id] + system_api.create_parcel(feature_profile_system.id, parcel.parcel) + + elif feature_profile.header.type == "other": + # Feature Profile Other Parcels don't have references to other parcels so we can create them directly + other_api = session.api.sdwan_feature_profiles.other + + feature_profile_other = other_api.create_profile( + name=feature_profile.feature_profile.name, + description=feature_profile.feature_profile.description, + ) + config_group_profiles.append(feature_profile_other) + rollback_feature_profiles_ids.append((feature_profile_other.id, "other")) + + for parcel_id in feature_profile.header.subelements: + parcel = mapping["profile_parcels"][parcel_id] + other_api.create_parcel(feature_profile_other.id, parcel.parcel) + + elif feature_profile.header.type == "transport": + # Feature Profile Transport Parcels have references to other parcels so we need to create them in order + pass + + # Create Config Group and add created Feature Profiles + config_group_payload = config_group.config_group + config_group_payload.profiles = config_group_profiles + cg_id = session.endpoints.configuration_group.create_config_group(config_group_payload).id + rollback_config_groups_ids.append(cg_id) + + except Exception as e: + logger.error(f"Error pushing UX2 config: {e}") + rollback_ux2_config(session, rollback_config_groups_ids, rollback_feature_profiles_ids) + raise e + + return rollback_config_groups_ids, rollback_feature_profiles_ids + + +def rollback_ux2_config( + session: ManagerSession, + rollback_config_groups_ids: List[UUID], + rollback_feature_profiles_ids: List[Tuple[UUID, Literal["system", "other", "transport"]]], +): + for cg_id in rollback_config_groups_ids: + session.endpoints.configuration_group.delete_config_group(cg_id) + + for feature_profile_id, type in rollback_feature_profiles_ids: + if type == "system": + session.api.sdwan_feature_profiles.system.delete_profile(feature_profile_id) + elif type == "other": + session.api.sdwan_feature_profiles.other.delete_profile(feature_profile_id) + elif type == "transport": + pass + else: + print(f"Unknown feature profile type {type}") From 08ed52cfbf097e6ada788ba7f000832318018ae4 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Sun, 17 Mar 2024 23:39:32 +0100 Subject: [PATCH 2/5] Working push for System and Other. Add Pusher class. Add Config revert class. Create factory for parcel pushing strategy. Create factory for api classes --- .../device_template/device_template.py | 2 - .../config_migration/creators/config_group.py | 23 --- .../creators/config_pusher.py | 81 ++++++++ .../creators/strategy/parcels.py | 37 ++++ .../factories/feature_profile_api.py | 47 +++++ .../factories/parcel_pusher.py | 33 ++++ .../reverters/config_reverter.py | 24 +++ catalystwan/workflows/config_migration.py | 185 ++++++++---------- 8 files changed, 300 insertions(+), 132 deletions(-) delete mode 100644 catalystwan/utils/config_migration/creators/config_group.py create mode 100644 catalystwan/utils/config_migration/creators/config_pusher.py create mode 100644 catalystwan/utils/config_migration/creators/strategy/parcels.py create mode 100644 catalystwan/utils/config_migration/factories/feature_profile_api.py create mode 100644 catalystwan/utils/config_migration/factories/parcel_pusher.py create mode 100644 catalystwan/utils/config_migration/reverters/config_reverter.py diff --git a/catalystwan/api/templates/device_template/device_template.py b/catalystwan/api/templates/device_template/device_template.py index a6925c696..85103afc2 100644 --- a/catalystwan/api/templates/device_template/device_template.py +++ b/catalystwan/api/templates/device_template/device_template.py @@ -5,7 +5,6 @@ import logging from pathlib import Path from typing import TYPE_CHECKING, Final, List -from uuid import UUID from jinja2 import DebugUndefined, Environment, FileSystemLoader, meta # type: ignore from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -43,7 +42,6 @@ class DeviceTemplate(BaseModel): >>> session.api.templates.create(device_template) """ - id: UUID = Field(alias="templateId") template_name: str = Field(alias="templateName") template_description: str = Field(alias="templateDescription") general_templates: List[GeneralTemplate] = Field(default=[], alias="generalTemplates") diff --git a/catalystwan/utils/config_migration/creators/config_group.py b/catalystwan/utils/config_migration/creators/config_group.py deleted file mode 100644 index 638ed70ed..000000000 --- a/catalystwan/utils/config_migration/creators/config_group.py +++ /dev/null @@ -1,23 +0,0 @@ -from dataclasses import dataclass -from typing import Dict, List -from uuid import UUID - -from catalystwan.models.configuration.config_migration import ( - TransformedConfigGroup, - TransformedFeatureProfile, - TransformedParcel, -) -from catalystwan.models.configuration.profile_type import ProfileType - - -@dataclass -class UX2ConfigMap: - config_group_map: Dict[UUID, TransformedConfigGroup] - feature_profile_map: Dict[UUID, TransformedFeatureProfile] - parcel_map: Dict[UUID, TransformedParcel] - - -@dataclass -class UX2ConfigRollback: - config_groups_ids: List[UUID] - feature_profiles_ids: List[UUID, ProfileType] diff --git a/catalystwan/utils/config_migration/creators/config_pusher.py b/catalystwan/utils/config_migration/creators/config_pusher.py new file mode 100644 index 000000000..b7b145a48 --- /dev/null +++ b/catalystwan/utils/config_migration/creators/config_pusher.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass +from typing import Dict, List, Tuple, cast +from uuid import UUID +from venv import logger + +from catalystwan.endpoints.configuration_group import ProfileId +from catalystwan.exceptions import CatalystwanException +from catalystwan.models.configuration.config_migration import TransformedFeatureProfile, TransformedParcel, UX2Config +from catalystwan.models.configuration.feature_profile.common import ProfileType +from catalystwan.session import ManagerSession +from catalystwan.utils.config_migration.factories.feature_profile_api import FeatureProfileAPIFactory +from catalystwan.utils.config_migration.factories.parcel_pusher import ParcelPusherFactory + + +@dataclass +class ConfigurationMapping: + feature_profile_map: Dict[UUID, TransformedFeatureProfile] + parcel_map: Dict[UUID, TransformedParcel] + + +class UX2ConfigRollback: + def __init__(self) -> None: + self.config_groups_ids: List[UUID] = [] + self.feature_profiles_ids: List[Tuple[UUID, ProfileType]] = [] + + def add_config_group(self, config_group_id: UUID) -> None: + self.config_groups_ids.append(config_group_id) + + def add_feature_profile(self, feature_profile_id: UUID, profile_type: ProfileType) -> None: + self.feature_profiles_ids.append((feature_profile_id, profile_type)) + + +class UX2ConfigPusher: + def __init__(self, session: ManagerSession, ux2_config: UX2Config) -> None: + self._session = session + self._config_map = self._create_config_map(ux2_config) + self._config_rollback = UX2ConfigRollback() + self._ux2_config = ux2_config + + def _create_config_map(self, ux2_config: UX2Config) -> ConfigurationMapping: + return ConfigurationMapping( + feature_profile_map={item.header.origin: item for item in ux2_config.feature_profiles}, + parcel_map={item.header.origin: item for item in ux2_config.profile_parcels}, + ) + + def push(self) -> UX2ConfigRollback: + try: + self._create_config_groups() + except CatalystwanException as e: + logger.error(f"Error occured during config push: {e}") + return self._config_rollback + + def _create_config_groups(self): + for transformed_config_group in self._ux2_config.config_groups: + config_group_payload = transformed_config_group.config_group + config_group_payload.profiles = self._create_feature_profile_and_parcels( + transformed_config_group.header.subelements + ) + cg_id = self._session.endpoints.configuration_group.create_config_group(config_group_payload).id + self._config_rollback.add_config_group(cg_id) + + def _create_feature_profile_and_parcels(self, feature_profiles_ids: List[UUID]) -> List[ProfileId]: + config_group_profiles = [] + for feature_profile_id in feature_profiles_ids: + transformed_feature_profile = self._config_map.feature_profile_map[feature_profile_id] + profile_type = cast(ProfileType, transformed_feature_profile.header.type) + api = FeatureProfileAPIFactory.get_api(profile_type, self._session) + name = transformed_feature_profile.feature_profile.name + description = transformed_feature_profile.feature_profile.description + if profile_type == "policy-object": + # TODO: Get default policy profile + continue + created_profile_id = api.create_profile(name, description).id # type: ignore + config_group_profiles.append(ProfileId(id=created_profile_id)) + self._create_parcels(api, created_profile_id, profile_type, transformed_feature_profile.header.subelements) + self._config_rollback.add_feature_profile(created_profile_id, profile_type) + return config_group_profiles + + def _create_parcels(self, api, profile_uuid, profile_type, parcels_uuids): + pusher = ParcelPusherFactory.get_pusher(profile_type, api) + pusher.push(profile_uuid, parcels_uuids, self._config_map.parcel_map) diff --git a/catalystwan/utils/config_migration/creators/strategy/parcels.py b/catalystwan/utils/config_migration/creators/strategy/parcels.py new file mode 100644 index 000000000..077f949d9 --- /dev/null +++ b/catalystwan/utils/config_migration/creators/strategy/parcels.py @@ -0,0 +1,37 @@ +from typing import Dict, List +from uuid import UUID + +from catalystwan.models.configuration.config_migration import TransformedParcel +from catalystwan.utils.config_migration.factories.feature_profile_api import FeatureProfile + + +class ParcelPusher: + """ + Base class for pushing parcels to a feature profile. + """ + + def __init__(self, api: FeatureProfile): + self.api = api + + def push(self, profile_uuid: UUID, parcel_uuids: List[UUID], mapping: Dict[UUID, TransformedParcel]): + """ + Push parcels to the given feature profile. + + Args: + profile_uuid (UUID): The UUID of the feature profile. + parcel_uuids (List[UUID]): The list of parcel UUIDs to push. + mapping (Dict[UUID, TransformedParcel]): The mapping of parcel UUIDs to transformed parcels. + """ + raise NotImplementedError + + +class SimpleParcelPusher(ParcelPusher): + """ + Simple implementation of ParcelPusher that creates parcels directly. + """ + + def push(self, profile_uuid: UUID, parcel_uuids: List[UUID], mapping: Dict[UUID, TransformedParcel]): + # Parcels don't have references to other parcels, so we can create them directly + for parcel_uuid in parcel_uuids: + transformed_parcel = mapping[parcel_uuid] + self.api.create_parcel(profile_uuid, transformed_parcel.parcel) # type: ignore diff --git a/catalystwan/utils/config_migration/factories/feature_profile_api.py b/catalystwan/utils/config_migration/factories/feature_profile_api.py new file mode 100644 index 000000000..af6d6a209 --- /dev/null +++ b/catalystwan/utils/config_migration/factories/feature_profile_api.py @@ -0,0 +1,47 @@ +from typing import Annotated, Callable, Mapping, Union + +from pydantic import Field + +from catalystwan.api.feature_profile_api import ( + OtherFeatureProfileAPI, + PolicyObjectFeatureProfileAPI, + ServiceFeatureProfileAPI, + SystemFeatureProfileAPI, +) +from catalystwan.models.configuration.feature_profile.common import ProfileType +from catalystwan.session import ManagerSession + +FEATURE_PROFILE_API_MAPPING: Mapping[ProfileType, Callable] = { + "system": SystemFeatureProfileAPI, + "other": OtherFeatureProfileAPI, + "policy-object": PolicyObjectFeatureProfileAPI, + "service": ServiceFeatureProfileAPI, +} + +FeatureProfile = Annotated[ + Union[SystemFeatureProfileAPI, OtherFeatureProfileAPI, PolicyObjectFeatureProfileAPI, ServiceFeatureProfileAPI], + Field(discriminator="type"), +] + + +class FeatureProfileAPIFactory: + """ + Factory class for creating FeatureProfileAPI instances. + """ + + @staticmethod + def get_api(profile_type: ProfileType, session: ManagerSession) -> FeatureProfile: + """ + Get the appropriate FeatureProfileAPI instance based on the profile type. + + Args: + profile_type (ProfileType): The type of the feature profile. + session (ManagerSession): The session object. + + Returns: + FeatureProfileAPI: The appropriate FeatureProfileAPI instance. + """ + api_class = FEATURE_PROFILE_API_MAPPING.get(profile_type) + if api_class is None: + raise ValueError(f"Invalid profile type: {profile_type}") + return api_class(session) diff --git a/catalystwan/utils/config_migration/factories/parcel_pusher.py b/catalystwan/utils/config_migration/factories/parcel_pusher.py new file mode 100644 index 000000000..6fb45a17e --- /dev/null +++ b/catalystwan/utils/config_migration/factories/parcel_pusher.py @@ -0,0 +1,33 @@ +from typing import Callable, Mapping + +from catalystwan.api.feature_profile_api import FeatureProfileAPI +from catalystwan.models.configuration.feature_profile.common import ProfileType +from catalystwan.utils.config_migration.creators.strategy.parcels import ParcelPusher, SimpleParcelPusher + +PARCEL_PUSHER_MAPPING: Mapping[ProfileType, Callable] = { + "other": SimpleParcelPusher, + "system": SimpleParcelPusher, +} + + +class ParcelPusherFactory: + """ + Factory class for creating ParcelPusher instances. + """ + + @staticmethod + def get_pusher(profile_type: ProfileType, api: FeatureProfileAPI) -> ParcelPusher: + """ + Get the appropriate ParcelPusher instance based on the profile type. + + Args: + profile_type (ProfileType): The type of the feature profile. + api (FeatureProfileAPI): The API for interacting with feature profiles. + + Returns: + ParcelPusher: The appropriate ParcelPusher instance. + """ + pusher_class = PARCEL_PUSHER_MAPPING.get(profile_type) + if pusher_class is None: + raise ValueError(f"Invalid profile type: {profile_type}") + return pusher_class(api) diff --git a/catalystwan/utils/config_migration/reverters/config_reverter.py b/catalystwan/utils/config_migration/reverters/config_reverter.py new file mode 100644 index 000000000..4469fd933 --- /dev/null +++ b/catalystwan/utils/config_migration/reverters/config_reverter.py @@ -0,0 +1,24 @@ +from venv import logger + +from catalystwan.exceptions import CatalystwanException +from catalystwan.utils.config_migration.creators.config_pusher import UX2ConfigRollback +from catalystwan.utils.config_migration.factories.feature_profile_api import FeatureProfileAPIFactory + + +class UX2ConfigReverter: + def __init__(self, session) -> None: + self._session = session + + def rollback(self, rollback_config: UX2ConfigRollback) -> bool: + try: + for cg_id in rollback_config.config_groups_ids: + self._session.endpoints.configuration_group.delete_config_group(cg_id) + for feature_profile_id, type_ in rollback_config.feature_profiles_ids: + api = FeatureProfileAPIFactory.get_api(type_, self._session) + if type_ == "policy-object": + continue + api.delete_profile(feature_profile_id) # type: ignore + except CatalystwanException as e: + logger.error(f"Error occured during config revert: {e}") + return False + return True diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index ca50afcd4..2160a5583 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, List, Literal, Tuple +from typing import Callable from uuid import UUID, uuid4 from catalystwan.api.policy_api import POLICY_LIST_ENDPOINTS_MAP @@ -16,7 +16,9 @@ from catalystwan.session import ManagerSession from catalystwan.utils.config_migration.converters.feature_template import create_parcel_from_template from catalystwan.utils.config_migration.converters.policy.policy_lists import convert as convert_policy_list +from catalystwan.utils.config_migration.creators.config_pusher import UX2ConfigPusher, UX2ConfigRollback from catalystwan.utils.config_migration.device_templates import flatten_general_templates +from catalystwan.utils.config_migration.reverters.config_reverter import UX2ConfigReverter logger = logging.getLogger(__name__) @@ -106,17 +108,6 @@ def transform(ux1: UX1Config) -> UX2Config: description="system", ), ) - fp_transport_uuid = uuid4() - transformed_fp_transport = TransformedFeatureProfile( - header=TransformHeader( - type="transport", - origin=fp_transport_uuid, - ), - feature_profile=FeatureProfileCreationPayload( - name=f"{dt.template_name}_transport", - description="transport", - ), - ) fp_other_uuid = uuid4() transformed_fp_other = TransformedFeatureProfile( header=TransformHeader( @@ -133,16 +124,14 @@ def transform(ux1: UX1Config) -> UX2Config: # Those feature templates IDs are real UUIDs and are used to map to the feature profiles if template.templateType in FEATURE_PROFILE_SYSTEM: transformed_fp_system.header.subelements.add(UUID(template.templateId)) - elif template.templateType in FEATURE_PROFILE_TRANSPORT: - transformed_fp_transport.header.subelements.add(UUID(template.templateId)) elif template.templateType in FEATURE_PROFILE_OTHER: transformed_fp_other.header.subelements.add(UUID(template.templateId)) transformed_cg = TransformedConfigGroup( header=TransformHeader( type="config_group", - origin=dt.id, - subelements=set([fp_system_uuid, fp_transport_uuid, fp_other_uuid]), + origin=uuid4(), + subelements=set([fp_system_uuid, fp_other_uuid]), ), config_group=ConfigGroupCreationPayload( name=dt.template_name, @@ -153,7 +142,6 @@ def transform(ux1: UX1Config) -> UX2Config: ) # Add to UX2 ux2.feature_profiles.append(transformed_fp_system) - ux2.feature_profiles.append(transformed_fp_transport) ux2.feature_profiles.append(transformed_fp_other) ux2.config_groups.append(transformed_cg) @@ -231,95 +219,78 @@ def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, in def push_ux2_config( session: ManagerSession, config: UX2Config, progress: Callable[[str, int, int], None] = log_progress -): +) -> UX2ConfigRollback: # Create mapping from origin ids # do dataclass - mapping = { - "config_group": {item.header.origin: item for item in config.config_groups}, - "feature_profile": {item.header.origin: item for item in config.feature_profiles}, - "profile_parcels": {item.header.origin: item for item in config.profile_parcels}, - } - rollback_config_groups_ids: List[UUID] = [] - rollback_feature_profiles_ids: List[Tuple[UUID, Literal["system", "other", "transport"]]] = [] - - try: - for config_group in config.config_groups: - config_group_profiles = [] - - for feature_profile_id in config_group.header.subelements: - feature_profile = mapping["feature_profile"][feature_profile_id] - - if feature_profile.header.type == "system": - # Feature Profile System Parcels don't have references to other parcels so we can create them directly - system_api = session.api.sdwan_feature_profiles.system - - feature_profile_system = system_api.create_profile( - name=feature_profile.feature_profile.name, - description=feature_profile.feature_profile.description, - ) - config_group_profiles.append(feature_profile_system) - rollback_feature_profiles_ids.append((feature_profile_system.id, "system")) - - logger.info( - f"Creating Feature Profile {feature_profile_system.id} {feature_profile.feature_profile.name}" - ) - logger.info( - f"Subelements Feature Profile {feature_profile_system.id} {feature_profile.header.subelements}" - ) - - for parcel_id in feature_profile.header.subelements: - logger.info(f"Creating Parcel {parcel_id} in Feature Profile {feature_profile_system.id}") - - parcel = mapping["profile_parcels"][parcel_id] - system_api.create_parcel(feature_profile_system.id, parcel.parcel) - - elif feature_profile.header.type == "other": - # Feature Profile Other Parcels don't have references to other parcels so we can create them directly - other_api = session.api.sdwan_feature_profiles.other - - feature_profile_other = other_api.create_profile( - name=feature_profile.feature_profile.name, - description=feature_profile.feature_profile.description, - ) - config_group_profiles.append(feature_profile_other) - rollback_feature_profiles_ids.append((feature_profile_other.id, "other")) - - for parcel_id in feature_profile.header.subelements: - parcel = mapping["profile_parcels"][parcel_id] - other_api.create_parcel(feature_profile_other.id, parcel.parcel) - - elif feature_profile.header.type == "transport": - # Feature Profile Transport Parcels have references to other parcels so we need to create them in order - pass - - # Create Config Group and add created Feature Profiles - config_group_payload = config_group.config_group - config_group_payload.profiles = config_group_profiles - cg_id = session.endpoints.configuration_group.create_config_group(config_group_payload).id - rollback_config_groups_ids.append(cg_id) - - except Exception as e: - logger.error(f"Error pushing UX2 config: {e}") - rollback_ux2_config(session, rollback_config_groups_ids, rollback_feature_profiles_ids) - raise e - - return rollback_config_groups_ids, rollback_feature_profiles_ids - - -def rollback_ux2_config( - session: ManagerSession, - rollback_config_groups_ids: List[UUID], - rollback_feature_profiles_ids: List[Tuple[UUID, Literal["system", "other", "transport"]]], -): - for cg_id in rollback_config_groups_ids: - session.endpoints.configuration_group.delete_config_group(cg_id) - - for feature_profile_id, type in rollback_feature_profiles_ids: - if type == "system": - session.api.sdwan_feature_profiles.system.delete_profile(feature_profile_id) - elif type == "other": - session.api.sdwan_feature_profiles.other.delete_profile(feature_profile_id) - elif type == "transport": - pass - else: - print(f"Unknown feature profile type {type}") + # mapping = { + # "config_group": {item.header.origin: item for item in config.config_groups}, + # "feature_profile": {item.header.origin: item for item in config.feature_profiles}, + # "profile_parcels": {item.header.origin: item for item in config.profile_parcels}, + # } + # rollback_config_groups_ids: List[UUID] = [] + # rollback_feature_profiles_ids: List[Tuple[UUID, Literal["system", "other", "transport"]]] = [] + + config_pusher = UX2ConfigPusher(session, config) + rollback = config_pusher.push() + # for config_group in config.config_groups: + # config_group_profiles = [] + + # for feature_profile_id in config_group.header.subelements: + # feature_profile = mapping["feature_profile"][feature_profile_id] + + # if feature_profile.header.type == "system": + # # Feature Profile System Parcels don't have references to other parcels so we can create them directly + # system_api = session.api.sdwan_feature_profiles.system + + # feature_profile_system = system_api.create_profile( + # name=feature_profile.feature_profile.name, + # description=feature_profile.feature_profile.description, + # ) + # config_group_profiles.append(feature_profile_system) + # rollback_feature_profiles_ids.append((feature_profile_system.id, "system")) + + # logger.info( + # f"Creating Feature Profile {feature_profile_system.id} {feature_profile.feature_profile.name}" + # ) + # logger.info( + # f"Subelements Feature Profile {feature_profile_system.id} {feature_profile.header.subelements}" + # ) + + # for parcel_id in feature_profile.header.subelements: + # logger.info(f"Creating Parcel {parcel_id} in Feature Profile {feature_profile_system.id}") + + # parcel = mapping["profile_parcels"][parcel_id] + # system_api.create_parcel(feature_profile_system.id, parcel.parcel) + + # elif feature_profile.header.type == "other": + # # Feature Profile Other Parcels don't have references to other parcels so we can create them directly + # other_api = session.api.sdwan_feature_profiles.other + + # feature_profile_other = other_api.create_profile( + # name=feature_profile.feature_profile.name, + # description=feature_profile.feature_profile.description, + # ) + # config_group_profiles.append(feature_profile_other) + # rollback_feature_profiles_ids.append((feature_profile_other.id, "other")) + + # for parcel_id in feature_profile.header.subelements: + # parcel = mapping["profile_parcels"][parcel_id] + # other_api.create_parcel(feature_profile_other.id, parcel.parcel) + + # elif feature_profile.header.type == "transport": + # # Feature Profile Transport Parcels have references to other parcels + # so we need to create them in order + # pass + + # # Create Config Group and add created Feature Profiles + # config_group_payload = config_group.config_group + # config_group_payload.profiles = config_group_profiles + # cg_id = session.endpoints.configuration_group.create_config_group(config_group_payload).id + # rollback_config_groups_ids.append(cg_id) + + return rollback + + +def rollback_ux2_config(session: ManagerSession, rollback_config: UX2ConfigRollback): + config_reverter = UX2ConfigReverter(session) + config_reverter.rollback(rollback_config) From 8f5208ef07c700d73955dd8e3ff6cfb55a1ec020 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Mon, 18 Mar 2024 08:30:56 +0100 Subject: [PATCH 3/5] Use from typing_extensions import Annotated instead --- .../utils/config_migration/factories/feature_profile_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/catalystwan/utils/config_migration/factories/feature_profile_api.py b/catalystwan/utils/config_migration/factories/feature_profile_api.py index af6d6a209..532004568 100644 --- a/catalystwan/utils/config_migration/factories/feature_profile_api.py +++ b/catalystwan/utils/config_migration/factories/feature_profile_api.py @@ -1,6 +1,7 @@ -from typing import Annotated, Callable, Mapping, Union +from typing import Callable, Mapping, Union from pydantic import Field +from typing_extensions import Annotated from catalystwan.api.feature_profile_api import ( OtherFeatureProfileAPI, From 5feaea6aefdf89f5be8633e63ef8990e037c7ca3 Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Mon, 18 Mar 2024 09:49:00 +0100 Subject: [PATCH 4/5] Delete commented code in push_ux2_config, change UX2ConfigRollback to dataclass --- .../creators/config_pusher.py | 8 +-- catalystwan/workflows/config_migration.py | 71 +------------------ 2 files changed, 7 insertions(+), 72 deletions(-) diff --git a/catalystwan/utils/config_migration/creators/config_pusher.py b/catalystwan/utils/config_migration/creators/config_pusher.py index b7b145a48..ff480e95d 100644 --- a/catalystwan/utils/config_migration/creators/config_pusher.py +++ b/catalystwan/utils/config_migration/creators/config_pusher.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Dict, List, Tuple, cast from uuid import UUID from venv import logger @@ -18,10 +18,10 @@ class ConfigurationMapping: parcel_map: Dict[UUID, TransformedParcel] +@dataclass class UX2ConfigRollback: - def __init__(self) -> None: - self.config_groups_ids: List[UUID] = [] - self.feature_profiles_ids: List[Tuple[UUID, ProfileType]] = [] + config_groups_ids: List[UUID] = field(default_factory=list) + feature_profiles_ids: List[Tuple[UUID, ProfileType]] = field(default_factory=list) def add_config_group(self, config_group_id: UUID) -> None: self.config_groups_ids.append(config_group_id) diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py index 2160a5583..e7fe38d2c 100644 --- a/catalystwan/workflows/config_migration.py +++ b/catalystwan/workflows/config_migration.py @@ -220,77 +220,12 @@ def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, in def push_ux2_config( session: ManagerSession, config: UX2Config, progress: Callable[[str, int, int], None] = log_progress ) -> UX2ConfigRollback: - # Create mapping from origin ids - # do dataclass - # mapping = { - # "config_group": {item.header.origin: item for item in config.config_groups}, - # "feature_profile": {item.header.origin: item for item in config.feature_profiles}, - # "profile_parcels": {item.header.origin: item for item in config.profile_parcels}, - # } - # rollback_config_groups_ids: List[UUID] = [] - # rollback_feature_profiles_ids: List[Tuple[UUID, Literal["system", "other", "transport"]]] = [] - config_pusher = UX2ConfigPusher(session, config) rollback = config_pusher.push() - # for config_group in config.config_groups: - # config_group_profiles = [] - - # for feature_profile_id in config_group.header.subelements: - # feature_profile = mapping["feature_profile"][feature_profile_id] - - # if feature_profile.header.type == "system": - # # Feature Profile System Parcels don't have references to other parcels so we can create them directly - # system_api = session.api.sdwan_feature_profiles.system - - # feature_profile_system = system_api.create_profile( - # name=feature_profile.feature_profile.name, - # description=feature_profile.feature_profile.description, - # ) - # config_group_profiles.append(feature_profile_system) - # rollback_feature_profiles_ids.append((feature_profile_system.id, "system")) - - # logger.info( - # f"Creating Feature Profile {feature_profile_system.id} {feature_profile.feature_profile.name}" - # ) - # logger.info( - # f"Subelements Feature Profile {feature_profile_system.id} {feature_profile.header.subelements}" - # ) - - # for parcel_id in feature_profile.header.subelements: - # logger.info(f"Creating Parcel {parcel_id} in Feature Profile {feature_profile_system.id}") - - # parcel = mapping["profile_parcels"][parcel_id] - # system_api.create_parcel(feature_profile_system.id, parcel.parcel) - - # elif feature_profile.header.type == "other": - # # Feature Profile Other Parcels don't have references to other parcels so we can create them directly - # other_api = session.api.sdwan_feature_profiles.other - - # feature_profile_other = other_api.create_profile( - # name=feature_profile.feature_profile.name, - # description=feature_profile.feature_profile.description, - # ) - # config_group_profiles.append(feature_profile_other) - # rollback_feature_profiles_ids.append((feature_profile_other.id, "other")) - - # for parcel_id in feature_profile.header.subelements: - # parcel = mapping["profile_parcels"][parcel_id] - # other_api.create_parcel(feature_profile_other.id, parcel.parcel) - - # elif feature_profile.header.type == "transport": - # # Feature Profile Transport Parcels have references to other parcels - # so we need to create them in order - # pass - - # # Create Config Group and add created Feature Profiles - # config_group_payload = config_group.config_group - # config_group_payload.profiles = config_group_profiles - # cg_id = session.endpoints.configuration_group.create_config_group(config_group_payload).id - # rollback_config_groups_ids.append(cg_id) - return rollback -def rollback_ux2_config(session: ManagerSession, rollback_config: UX2ConfigRollback): +def rollback_ux2_config(session: ManagerSession, rollback_config: UX2ConfigRollback) -> bool: config_reverter = UX2ConfigReverter(session) - config_reverter.rollback(rollback_config) + status = config_reverter.rollback(rollback_config) + return status From 18bf17c13d4dd8a283a8d04e57a18eab2347795c Mon Sep 17 00:00:00 2001 From: Jakub Krajewski Date: Mon, 18 Mar 2024 09:59:59 +0100 Subject: [PATCH 5/5] Use pydantic instead of dataclass. Rollback class will be used in web API so it needs to be serializable --- .../config_migration/creators/config_pusher.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/catalystwan/utils/config_migration/creators/config_pusher.py b/catalystwan/utils/config_migration/creators/config_pusher.py index ff480e95d..0a8b0273c 100644 --- a/catalystwan/utils/config_migration/creators/config_pusher.py +++ b/catalystwan/utils/config_migration/creators/config_pusher.py @@ -1,8 +1,9 @@ -from dataclasses import dataclass, field from typing import Dict, List, Tuple, cast from uuid import UUID from venv import logger +from pydantic import BaseModel, Field + from catalystwan.endpoints.configuration_group import ProfileId from catalystwan.exceptions import CatalystwanException from catalystwan.models.configuration.config_migration import TransformedFeatureProfile, TransformedParcel, UX2Config @@ -12,16 +13,14 @@ from catalystwan.utils.config_migration.factories.parcel_pusher import ParcelPusherFactory -@dataclass -class ConfigurationMapping: +class ConfigurationMapping(BaseModel): feature_profile_map: Dict[UUID, TransformedFeatureProfile] parcel_map: Dict[UUID, TransformedParcel] -@dataclass -class UX2ConfigRollback: - config_groups_ids: List[UUID] = field(default_factory=list) - feature_profiles_ids: List[Tuple[UUID, ProfileType]] = field(default_factory=list) +class UX2ConfigRollback(BaseModel): + config_groups_ids: List[UUID] = Field(default_factory=list) + feature_profiles_ids: List[Tuple[UUID, ProfileType]] = Field(default_factory=list) def add_config_group(self, config_group_id: UUID) -> None: self.config_groups_ids.append(config_group_id)