diff --git a/ENDPOINTS.md b/ENDPOINTS.md index 4599f4af..60b8934f 100644 --- a/ENDPOINTS.md +++ b/ENDPOINTS.md @@ -1,6 +1,6 @@ **THIS FILE WAS AUTO-GENERATED DO NOT EDIT** -Generated for: catalystwan-0.33.7.dev3 +Generated for: catalystwan-0.33.7.dev4 All URIs are relative to */dataservice* HTTP request | Supported Versions | Method | Payload Type | Return Type | Tenancy Mode @@ -533,6 +533,9 @@ PUT /template/policy/vsmart/central/{id}||[**ConfigurationVSmartTemplatePolicy.e PUT /template/policy/vsmart/{id}||[**ConfigurationVSmartTemplatePolicy.edit_vsmart_template**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/policy/vsmart_template.py#L65)|[**CentralizedPolicyEditPayload**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/models/policy/centralized.py#L249)|None| GET /template/policy/vsmart||[**ConfigurationVSmartTemplatePolicy.generate_vsmart_policy_template_list**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/policy/vsmart_template.py#L69)||DataSequence[[**CentralizedPolicyInfo**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/models/policy/centralized.py#L253)]| GET /template/policy/vsmart/definition/{id}||[**ConfigurationVSmartTemplatePolicy.get_template_by_policy_id**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/policy/vsmart_template.py#L73)||[**CentralizedPolicy**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/models/policy/centralized.py#L203)| +POST /v1/policy-group|>=20.12|[**PolicyGroupEndpoints.create_policy_group**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/policy_group.py#L12)|[**PolicyGroup**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/models/configuration/policy_group.py#L22)|[**PolicyGroupId**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/models/configuration/policy_group.py#L43)| +GET /v1/policy-group|>=20.12|[**PolicyGroupEndpoints.get_all**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/policy_group.py#L17)||DataSequence[[**PolicyGroupId**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/models/configuration/policy_group.py#L43)]| +DELETE /v1/policy-group/{group_id}|>=20.12|[**PolicyGroupEndpoints.delete**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/policy_group.py#L22)||None| GET /device/action/remote-server||[**ConfigurationSoftwareActions.get_list_of_remote_servers**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/software_actions.py#L165)||DataSequence[[**RemoteServerInfo**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/software_actions.py#L70)]| POST /device/action/remote-server||[**ConfigurationSoftwareActions.add_new_remote_server**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/software_actions.py#L169)|[**RemoteServer**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/software_actions.py#L45)|None| GET /device/action/remote-server/{id}||[**ConfigurationSoftwareActions.get_remote_server**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/software_actions.py#L173)||[**RemoteServerInfo**](https://github.com/cisco-open/cisco-catalyst-wan-sdk/blob/main/catalystwan/endpoints/configuration/software_actions.py#L70)| diff --git a/catalystwan/api/config_group_api.py b/catalystwan/api/config_group_api.py index 84beae22..430e008a 100644 --- a/catalystwan/api/config_group_api.py +++ b/catalystwan/api/config_group_api.py @@ -137,3 +137,11 @@ def update_variables(self, cg_id: str, solution: Solution, device_variables: lis payload = ConfigGroupVariablesEditPayload(solution=solution, devices=device_variables) self._endpoints.update_variables(config_group_id=cg_id, payload=payload) + + def delete_all(self) -> None: + """ + Deletes all config-group + """ + config_groups = self.get() + for config_group in config_groups: + self.delete(config_group.id) diff --git a/catalystwan/api/feature_profile_api.py b/catalystwan/api/feature_profile_api.py index c4bde5a0..1ff6fa9b 100644 --- a/catalystwan/api/feature_profile_api.py +++ b/catalystwan/api/feature_profile_api.py @@ -233,6 +233,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_transport_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all Transport Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def create_parcel( self, profile_id: UUID, payload: AnyTransportParcel, vpn_uuid: Optional[UUID] = None ) -> ParcelCreationResponse: @@ -339,6 +347,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_sdwan_other_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all Other Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def get( self, profile_id: UUID, @@ -406,6 +422,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_sdwan_service_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all Service Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def create_parcel( self, profile_uuid: UUID, payload: AnyServiceParcel, vpn_uuid: Optional[UUID] = None ) -> ParcelCreationResponse: @@ -462,6 +486,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_sdwan_system_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all System Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def get_schema( self, profile_id: UUID, @@ -1225,6 +1257,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_embedded_security_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete Embedded Security Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + @overload def get_parcels( self, @@ -1335,6 +1375,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all CLI Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def get_parcel_by_id( self, profile_id: UUID, @@ -1396,6 +1444,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_dns_security_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete DNS Security Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def get_parcels( self, profile_id: UUID, @@ -1464,6 +1520,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_sig_security_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all SIG Security Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def create_parcel(self, profile_uuid: UUID, payload: SIGParcel) -> ParcelCreationResponse: """ Create SIG Security Parcel for selected profile_id @@ -1509,6 +1573,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_application_priority_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all Application Priority Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + @overload def get_parcels( self, @@ -1635,6 +1707,14 @@ def delete_profile(self, profile_id: UUID) -> None: """ self.endpoint.delete_topology_feature_profile(profile_id) + def delete_all_profiles(self) -> None: + """ + Delete all Topology Feature Profiles + """ + profiles = self.get_profiles() + for profile in profiles: + self.delete_profile(profile.profile_id) + def create_parcel(self, profile_id: UUID, parcel: AnyTopologyParcel) -> ParcelCreationResponse: """ Create Topology Parcel for selected profile_id based on payload type diff --git a/catalystwan/endpoints/configuration/policy_group.py b/catalystwan/endpoints/configuration/policy_group.py new file mode 100644 index 00000000..53529a5b --- /dev/null +++ b/catalystwan/endpoints/configuration/policy_group.py @@ -0,0 +1,25 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates + +# mypy: disable-error-code="empty-body" +from uuid import UUID + +from catalystwan.endpoints import APIEndpoints, delete, get, post, versions +from catalystwan.models.configuration.policy_group import PolicyGroup, PolicyGroupId +from catalystwan.typed_list import DataSequence + + +class PolicyGroupEndpoints(APIEndpoints): + @post("/v1/policy-group") + @versions(">=20.12") + def create_policy_group(self, payload: PolicyGroup) -> PolicyGroupId: + ... + + @get("/v1/policy-group") + @versions(">=20.12") + def get_all(self) -> DataSequence[PolicyGroupId]: + ... + + @delete("/v1/policy-group/{group_id}") + @versions(">=20.12") + def delete(self, group_id: UUID) -> None: + ... diff --git a/catalystwan/endpoints/endpoints_container.py b/catalystwan/endpoints/endpoints_container.py index 73bb5b05..702ea865 100644 --- a/catalystwan/endpoints/endpoints_container.py +++ b/catalystwan/endpoints/endpoints_container.py @@ -76,6 +76,7 @@ from catalystwan.endpoints.configuration.policy.security_template import ConfigurationSecurityTemplatePolicy from catalystwan.endpoints.configuration.policy.vedge_template import ConfigurationVEdgeTemplatePolicy from catalystwan.endpoints.configuration.policy.vsmart_template import ConfigurationVSmartTemplatePolicy +from catalystwan.endpoints.configuration.policy_group import PolicyGroupEndpoints from catalystwan.endpoints.configuration.software_actions import ConfigurationSoftwareActions from catalystwan.endpoints.configuration.topology_group import TopologyGroupEndpoints from catalystwan.endpoints.configuration_dashboard_status import ConfigurationDashboardStatus @@ -182,6 +183,7 @@ def __init__(self, session: ManagerSession): self.policy = ConfigurationPolicyContainer(session) self.feature_profile = ConfigurationFeatureProfileContainer(session) self.topology_group = TopologyGroupEndpoints(session) + self.policy_group = PolicyGroupEndpoints(session) class TroubleshootingToolsContainer: diff --git a/catalystwan/models/configuration/policy_group.py b/catalystwan/models/configuration/policy_group.py new file mode 100644 index 00000000..f2a4886a --- /dev/null +++ b/catalystwan/models/configuration/policy_group.py @@ -0,0 +1,49 @@ +from typing import List, Literal, Optional +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + + +class Profile(BaseModel): + id: UUID + + +Solution = Literal[ + "sd-routing", + "sdwan", +] + + +class FromPolicyGroup(BaseModel): + model_config = ConfigDict(populate_by_name=True) + copy_: UUID = Field(validation_alias="copy", serialization_alias="copy") + + +class PolicyGroup(BaseModel): + model_config = ConfigDict(populate_by_name=True) + description: str = Field() + name: str = Field(pattern='^[^&<>! "]+$') + from_policy_group: Optional[FromPolicyGroup] = Field( + default=None, validation_alias="fromPolicyGroup", serialization_alias="fromPolicyGroup" + ) + profiles: Optional[List[Profile]] = Field( + default=None, description="list of profile ids that belongs to the policy group" + ) + solution: Optional[Solution] = Field(default=None) + + +class ProfileInfo(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id: str = Field() + profile_type: Optional[Literal["global"]] = Field( + default=None, validation_alias="profileType", serialization_alias="profileType" + ) + + +class PolicyGroupId(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id: UUID + profiles: Optional[List[ProfileInfo]] = Field( + default=None, + description="(Optional - only applicable for AON) List of profile ids that belongs to the policy group", + ) diff --git a/catalystwan/utils/config_migration/runner.py b/catalystwan/utils/config_migration/runner.py new file mode 100644 index 00000000..203bca4b --- /dev/null +++ b/catalystwan/utils/config_migration/runner.py @@ -0,0 +1,160 @@ +from dataclasses import dataclass +from json import dumps +from pathlib import Path +from typing import Callable, cast +from uuid import UUID + +from catalystwan.models.configuration.config_migration import ConfigTransformResult, UX1Config, UX2ConfigPushResult +from catalystwan.models.configuration.feature_profile.parcel import Parcel, list_types +from catalystwan.models.configuration.feature_profile.sdwan.policy_object import AnyPolicyObjectParcel +from catalystwan.session import ManagerSession +from catalystwan.typed_list import DataSequence +from catalystwan.workflows.config_migration import ( + collect_ux1_config, + log_progress, + push_ux2_config, + rollback_ux2_config, + transform, +) + +DEFAULT_ARTIFACT_DIR = "artifacts" + + +@dataclass +class ConfigMigrationRunner: + session: ManagerSession + collect: bool = True + push: bool = True + rollback: bool = False + artifact_dir = Path(DEFAULT_ARTIFACT_DIR) + progress: Callable[[str, int, int], None] = log_progress + + def __post_init__(self) -> None: + self.artifact_dir.mkdir(parents=True, exist_ok=True) + self.ux1_dump: Path = self.artifact_dir / Path("ux1.json") + self.ux2_dump: Path = self.artifact_dir / Path("ux2.json") + self.ux2_push_dump: Path = self.artifact_dir / Path("ux2-push-result.json") + self.ux1_schema_dump: Path = self.artifact_dir / Path("ux1-schema.json") + self.transform_schema_dump: Path = self.artifact_dir / Path("transform-result-schema.json") + self.push_schema_dump: Path = self.artifact_dir / Path("push-result-schema.json") + + @staticmethod + def collect_only(session: ManagerSession) -> "ConfigMigrationRunner": + return ConfigMigrationRunner(session=session, collect=True, push=False, rollback=False) + + @staticmethod + def collect_and_push(session: ManagerSession) -> "ConfigMigrationRunner": + return ConfigMigrationRunner(session=session, collect=True, push=True, rollback=False) + + @staticmethod + def rollback_only(session: ManagerSession) -> "ConfigMigrationRunner": + return ConfigMigrationRunner(session=session, collect=False, push=False, rollback=True) + + @staticmethod + def transform_only(session: ManagerSession) -> "ConfigMigrationRunner": + return ConfigMigrationRunner(session=session, collect=False, push=False, rollback=False) + + @staticmethod + def push_only(session: ManagerSession) -> "ConfigMigrationRunner": + return ConfigMigrationRunner(session=session, collect=False, push=True, rollback=False) + + @staticmethod + def push_and_rollback(session: ManagerSession) -> "ConfigMigrationRunner": + return ConfigMigrationRunner(session=session, collect=False, push=True, rollback=True) + + def dump_schemas(self): + with open(self.ux1_schema_dump, "w") as f: + f.write(dumps(UX1Config.model_json_schema(by_alias=True), indent=4)) + with open(self.transform_schema_dump, "w") as f: + f.write(dumps(ConfigTransformResult.model_json_schema(by_alias=True), indent=4)) + with open(self.push_schema_dump, "w") as f: + f.write(dumps(UX2ConfigPushResult.model_json_schema(by_alias=True), indent=4)) + + def clear_ux2(self) -> None: + with self.session.login() as session: + # GROUPS + self.progress("deleting config groups...", 1, 12) + session.api.config_group.delete_all() + + self.progress("deleting topology groups...", 2, 12) + tg_api = session.endpoints.configuration.topology_group + for tg in tg_api.get_all(): + tg_api.delete(tg.id) + + self.progress("deleting policy groups...", 2, 12) + pg_api = session.endpoints.configuration.policy_group + for pg in pg_api.get_all(): + pg_api.delete(pg.id) + + # PROFILES + fp_api = session.api.sdwan_feature_profiles + + self.progress("deleting application priority profiles...", 3, 12) + fp_api.application_priority.delete_all_profiles() + + self.progress("deleting cli profiles...", 4, 12) + fp_api.cli.delete_all_profiles() + + self.progress("deleting dns security profiles...", 5, 12) + fp_api.dns_security.delete_all_profiles() + + self.progress("deleting embedded security profiles...", 5, 12) + fp_api.embedded_security.delete_all_profiles() + + self.progress("deleting other profiles...", 6, 12) + fp_api.other.delete_all_profiles() + + self.progress("deleting default policy object profile parcels...", 7, 12) + po_profiles = fp_api.policy_object.get_profiles() + if len(po_profiles) > 1: + print("WARNING MORE THAN ONE DEFAULT POLICY OBJECT PROFILE DETECTED") + for po_profile in po_profiles: + for dpo_parcel_type in list_types(AnyPolicyObjectParcel): + for parcel in cast( + DataSequence[Parcel[AnyPolicyObjectParcel]], + fp_api.policy_object.get(po_profile.profile_id, dpo_parcel_type), + ): + if parcel.created_by != "system": + parcel_uuid = UUID(str(parcel.parcel_id)) + fp_api.policy_object.delete(po_profile.profile_id, type(parcel.payload), parcel_uuid) + + self.progress("deleting service profiles...", 8, 12) + fp_api.service.delete_all_profiles() + + self.progress("deleting sig security profiles...", 9, 12) + fp_api.sig_security.delete_all_profiles() + + self.progress("deleting system profiles...", 10, 12) + fp_api.system.delete_all_profiles() + + self.progress("deleting transport profiles...", 11, 12) + fp_api.transport.delete_all_profiles() + + self.progress("deleting topology profiles...", 12, 12) + fp_api.topology.delete_all_profiles() + + def run(self): + with self.session.login() as session: + # collext and dump ux1 to json file + if self.collect: + ux1 = collect_ux1_config(session, self.progress) + # ux1.templates = UX1Templates() + with open(self.ux1_dump, "w") as f: + f.write(ux1.model_dump_json(exclude_none=True, by_alias=True, indent=4, warnings=False)) + + # transform to ux2 and dump to json file + _transform_result = transform(UX1Config.model_validate_json(open(self.ux1_dump).read()), self.progress) + with open(self.ux2_dump, "w") as f: + f.write(_transform_result.model_dump_json(exclude_none=True, by_alias=True, indent=4, warnings=False)) + + # push ux2 to remote and dump push result + if self.push: + transform_result = ConfigTransformResult.model_validate_json(open(self.ux2_dump).read()) + ux2_push_result = push_ux2_config(session, transform_result.ux2_config, self.progress) + with open(self.ux2_push_dump, "w") as f: + f.write(ux2_push_result.model_dump_json(exclude_none=True, by_alias=True, indent=4, warnings=False)) + + # rollback + if self.rollback: + ux2_push_result = UX2ConfigPushResult.model_validate_json(open(self.ux2_push_dump).read()) + rollback_ux2_config(session, ux2_push_result.rollback, self.progress) diff --git a/pyproject.toml b/pyproject.toml index 8e5f9f01..8ec4f0f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "catalystwan" -version = "0.33.7dev3" +version = "0.33.7dev4" description = "Cisco Catalyst WAN SDK for Python" authors = ["kagorski "] readme = "README.md"