diff --git a/catalystwan/api/configuration_groups/parcel.py b/catalystwan/api/configuration_groups/parcel.py index a48a2c6e6..792088226 100644 --- a/catalystwan/api/configuration_groups/parcel.py +++ b/catalystwan/api/configuration_groups/parcel.py @@ -21,10 +21,10 @@ class _ParcelBase(BaseModel): validation_alias="description", description="Set the parcel description", ) - data: Optional[Any] = None + # data: Optional[Any] = None _parcel_data_key: str = PrivateAttr(default="data") - @model_serializer(mode="wrap") + @model_serializer(mode="wrap", when_used="json") def envelope_parcel_data(self, handler) -> Dict[str, Any]: model_dict = handler(self) model_dict[self._parcel_data_key] = {} @@ -49,7 +49,7 @@ class OptionType(str, Enum): class ParcelAttribute(BaseModel): - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", populate_by_name=True) option_type: OptionType = Field(serialization_alias="optionType", validation_alias="optionType") diff --git a/catalystwan/api/policy_api.py b/catalystwan/api/policy_api.py index bb73b2931..ec32519bb 100644 --- a/catalystwan/api/policy_api.py +++ b/catalystwan/api/policy_api.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Type, overload +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Type, overload from uuid import UUID from catalystwan.api.task_status_api import Task @@ -637,6 +637,12 @@ def get(self, type: Type[AnyPolicyList], id: Optional[UUID] = None) -> Any: return endpoints.get_lists_by_id(id=id) return endpoints.get_policy_lists() + def get_all(self) -> List[AnyPolicyList]: + infos: List[AnyPolicyList] = [] + for list_type, _ in POLICY_LIST_ENDPOINTS_MAP.items(): + infos.extend(self.get(list_type)) + return infos + class PolicyDefinitionsAPI: def __init__(self, session: ManagerSession): @@ -780,6 +786,12 @@ def get(self, type: Type[AnyPolicyDefinition], id: Optional[UUID] = None) -> Any return endpoints.get_policy_definition(id=id) return endpoints.get_definitions() + def get_all(self) -> List[Tuple[type, PolicyDefinitionInfo]]: + all_items: List[Tuple[type, PolicyDefinitionInfo]] = [] + for definition_type, _ in POLICY_DEFINITION_ENDPOINTS_MAP.items(): + all_items.extend([(definition_type, info) for info in self.get(definition_type)]) + return all_items + class PolicyAPI: """This is exposing so called 'UX 1.0' API""" diff --git a/catalystwan/endpoints/configuration_group.py b/catalystwan/endpoints/configuration_group.py index 74c2629b7..75412b16c 100644 --- a/catalystwan/endpoints/configuration_group.py +++ b/catalystwan/endpoints/configuration_group.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import List, Optional -from pydantic.v1 import BaseModel, Field +from pydantic import BaseModel, Field from catalystwan.endpoints import APIEndpoints, delete, get, post, put, versions from catalystwan.models.configuration.common import Solution diff --git a/catalystwan/models/configuration/config_migration.py b/catalystwan/models/configuration/config_migration.py index 1d94fb157..22f5a7742 100644 --- a/catalystwan/models/configuration/config_migration.py +++ b/catalystwan/models/configuration/config_migration.py @@ -1,7 +1,10 @@ from typing import List -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase +from catalystwan.endpoints.configuration_group import ConfigGroup +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload from catalystwan.models.policy import ( AnyPolicyDefinition, AnyPolicyList, @@ -12,11 +15,22 @@ class UX1Policies(BaseModel): - centralized_policies: List[CentralizedPolicy] = Field(default=[], serialization_alias="centralizedPolicies") - localized_policies: List[LocalizedPolicy] = Field(default=[], serialization_alias="localizedPolicies") - security_policies: List[SecurityPolicy] = Field(default=[], serialization_alias="securityPolicies") - policy_definitions: List[AnyPolicyDefinition] = Field(default=[], serialization_alias="policyDefinitions") - policy_lists: List[AnyPolicyList] = Field(default=[], serialization_alias="policyLists") + model_config = ConfigDict(populate_by_name=True) + centralized_policies: List[CentralizedPolicy] = Field( + default=[], serialization_alias="centralizedPolicies", validation_alias="centralizedPolicies" + ) + localized_policies: List[LocalizedPolicy] = Field( + default=[], serialization_alias="localizedPolicies", validation_alias="localizedPolicies" + ) + security_policies: List[SecurityPolicy] = Field( + default=[], serialization_alias="securityPolicies", validation_alias="securityPolicies" + ) + policy_definitions: List[AnyPolicyDefinition] = Field( + default=[], serialization_alias="policyDefinitions", validation_alias="policyDefinitions" + ) + policy_lists: List[AnyPolicyList] = Field( + default=[], serialization_alias="policyLists", validation_alias="policyLists" + ) class UX1Templates(BaseModel): @@ -25,10 +39,23 @@ class UX1Templates(BaseModel): class UX1Config(BaseModel): # All UX1 Configuration items - Mega Model - policies: UX1Policies - templates: UX1Templates + model_config = ConfigDict(populate_by_name=True) + policies: UX1Policies = UX1Policies() + templates: UX1Templates = UX1Templates() class UX2Config(BaseModel): # All UX2 Configuration items - Mega Model - pass + model_config = ConfigDict(populate_by_name=True) + config_groups: List[ConfigGroup] = Field( + default=[], serialization_alias="configurationGroups", validation_alias="configurationGroups" + ) + policy_groups: List[ConfigGroup] = Field( + default=[], serialization_alias="policyGroups", validation_alias="policyGroups" + ) + feature_profiles: List[FeatureProfileCreationPayload] = Field( + default=[], serialization_alias="featureProfiles", validation_alias="featureProfiles" + ) + profile_parcels: List[_ParcelBase] = Field( + default=[], serialization_alias="profileParcels", validation_alias="profileParcels" + ) diff --git a/catalystwan/models/configuration/feature_profile/sdwan/policy_object/policy/data_prefix.py b/catalystwan/models/configuration/feature_profile/sdwan/policy_object/policy/data_prefix.py index b4d7721f7..b549a3316 100644 --- a/catalystwan/models/configuration/feature_profile/sdwan/policy_object/policy/data_prefix.py +++ b/catalystwan/models/configuration/feature_profile/sdwan/policy_object/policy/data_prefix.py @@ -11,14 +11,16 @@ class DataPrefixEntry(BaseModel): ipv4_address: Global[IPv4Address] = Field(serialization_alias="ipv4Address", validation_alias="ipv4Address") ipv4_prefix_length: Global[int] = Field(serialization_alias="ipv4PrefixLength", validation_alias="ipv4PrefixLength") + @staticmethod + def from_ipv4_network(ipv4_network: IPv4Network) -> "DataPrefixEntry": + return DataPrefixEntry( + ipv4_address=as_global(ipv4_network.network_address), + ipv4_prefix_length=as_global(ipv4_network.prefixlen), + ) + class DataPrefixParcel(_ParcelBase): entries: List[DataPrefixEntry] = Field(default_factory=list, validation_alias=AliasPath("data", "entries")) def add_data_prefix(self, ipv4_network: IPv4Network): - self.entries.append( - DataPrefixEntry( - ipv4_address=as_global(ipv4_network.network_address), - ipv4_prefix_length=as_global(ipv4_network.prefixlen), - ) - ) + self.entries.append(DataPrefixEntry.from_ipv4_network(ipv4_network)) diff --git a/catalystwan/models/policy/centralized.py b/catalystwan/models/policy/centralized.py index c06d188f2..9f28f0a7e 100644 --- a/catalystwan/models/policy/centralized.py +++ b/catalystwan/models/policy/centralized.py @@ -211,12 +211,12 @@ def try_parse(cls, policy_definition): # while POST /template/policy/vsmart requires a regular object # it makes sense to reuse that model for both requests and present parsed data to the user if isinstance(policy_definition, str): - return CentralizedPolicyDefinition.parse_raw(policy_definition) + return CentralizedPolicyDefinition.model_validate_json(policy_definition) return policy_definition class CentralizedPolicyEditPayload(PolicyEditPayload, CentralizedPolicy): - rid: Optional[str] = Field(default=None, serialization_alias="@rid", validation_alias="@rid") + rid: Optional[int] = Field(default=None, serialization_alias="@rid", validation_alias="@rid") class CentralizedPolicyInfo(PolicyInfo, CentralizedPolicyEditPayload): diff --git a/catalystwan/models/policy/lists.py b/catalystwan/models/policy/lists.py index c707b35ec..5686c3f66 100644 --- a/catalystwan/models/policy/lists.py +++ b/catalystwan/models/policy/lists.py @@ -4,7 +4,12 @@ from pydantic import BaseModel, Field +from catalystwan.api.configuration_groups.parcel import _ParcelBase from catalystwan.models.common import InterfaceType, TLOCColor, WellKnownBGPCommunities +from catalystwan.models.configuration.feature_profile.sdwan.policy_object.policy.data_prefix import ( + DataPrefixEntry, + DataPrefixParcel, +) from catalystwan.models.policy.lists_entries import ( AppListEntry, AppProbeClassListEntry, @@ -55,6 +60,9 @@ def _add_entry(self, entry: Any, single: bool = False) -> None: else: self.entries.append(entry) + def to_policy_object_parcel(self) -> _ParcelBase: + return _ParcelBase(parcel_name=self.name, parcel_description=self.description) + class DataPrefixList(PolicyListBase): type: Literal["dataPrefix"] = "dataPrefix" @@ -63,6 +71,11 @@ class DataPrefixList(PolicyListBase): def add_prefix(self, ip_prefix: IPv4Network) -> None: self._add_entry(DataPrefixListEntry(ip_prefix=ip_prefix)) + def to_policy_object_parcel(self) -> DataPrefixParcel: + parcel = DataPrefixParcel(parcel_name=self.name, parcel_description=self.description) + parcel.entries = [DataPrefixEntry.from_ipv4_network(i.ip_prefix) for i in self.entries] + return parcel + class SiteList(PolicyListBase): type: Literal["site"] = "site" diff --git a/catalystwan/models/policy/lists_entries.py b/catalystwan/models/policy/lists_entries.py index 6b75eaad9..823805338 100644 --- a/catalystwan/models/policy/lists_entries.py +++ b/catalystwan/models/policy/lists_entries.py @@ -326,7 +326,9 @@ class SLAClassListEntry(BaseModel): latency: Optional[str] = None loss: Optional[str] = None jitter: Optional[str] = None - app_probe_class: Optional[UUID] = Field(serialization_alias="appProbeClass", validation_alias="appProbeClass") + app_probe_class: Optional[UUID] = Field( + default=None, serialization_alias="appProbeClass", validation_alias="appProbeClass" + ) fallback_best_tunnel: Optional[FallbackBestTunnel] = Field( default=None, serialization_alias="fallbackBestTunnel", validation_alias="fallbackBestTunnel" ) diff --git a/catalystwan/workflows/config_migration.py b/catalystwan/workflows/config_migration.py new file mode 100644 index 000000000..2ddd0f567 --- /dev/null +++ b/catalystwan/workflows/config_migration.py @@ -0,0 +1,64 @@ +import logging +from typing import Callable + +from catalystwan.api.policy_api import POLICY_LIST_ENDPOINTS_MAP +from catalystwan.models.configuration.config_migration import UX1Config, UX2Config +from catalystwan.session import ManagerSession + +logger = logging.getLogger(__name__) + + +def log_progress(task: str, completed: int, total: int) -> None: + logger.info(f"{task} {completed}/{total}") + + +def transform(ux1: UX1Config) -> UX2Config: + ux2 = UX2Config() + ux2.profile_parcels.extend([lst.to_policy_object_parcel() for lst in ux1.policies.policy_lists]) + return ux2 + + +def collect_ux1_config(session: ManagerSession, progress: Callable[[str, int, int], None] = log_progress) -> UX1Config: + ux1 = UX1Config() + + """Collect Policies""" + policy_api = session.api.policy + progress("Collecting Policy Info", 0, 3) + + centralized_policy_ids = [info.policy_id for info in policy_api.centralized.get()] + progress("Collecting Policy Info", 1, 3) + + localized_policy_ids = [info.policy_id for info in policy_api.localized.get()] + progress("Collecting Policy Info", 2, 3) + + policy_definition_types_and_ids = [ + (policy_type, info.definition_id) for policy_type, info in policy_api.definitions.get_all() + ] + progress("Collecting Policy Info", 3, 3) + + policy_list_types = POLICY_LIST_ENDPOINTS_MAP.keys() + for i, policy_list_type in enumerate(policy_list_types): + ux1.policies.policy_lists.extend(policy_api.lists.get(policy_list_type)) + progress("Collecting Policy Lists", i + 1, len(policy_list_types)) + + for i, type_and_id in enumerate(policy_definition_types_and_ids): + ux1.policies.policy_definitions.append(policy_api.definitions.get(*type_and_id)) + progress("Collecting Policy Definitions", i + 1, len(policy_definition_types_and_ids)) + + for i, cpid in enumerate(centralized_policy_ids): + ux1.policies.centralized_policies.append(policy_api.centralized.get(id=cpid)) + progress("Collecting Centralized Policies", i + 1, len(centralized_policy_ids)) + + for i, lpid in enumerate(localized_policy_ids): + ux1.policies.localized_policies.append(policy_api.localized.get(id=lpid)) + progress("Collecting Localized Policies", i + 1, len(localized_policy_ids)) + + ux1.policies.policy_lists = policy_api.lists.get_all() + + """Collect Templates""" + # TODO + return ux1 + + +def push_ux2_config(session: ManagerSession) -> None: + pass diff --git a/examples/policies_configuration_guide.py b/examples/policies_configuration_guide.py index 7c2abe9df..e38ea7d2a 100644 --- a/examples/policies_configuration_guide.py +++ b/examples/policies_configuration_guide.py @@ -46,12 +46,12 @@ PrefixList, RegionList, SiteList, - SLAClassList, TLOCList, TrafficDataPolicy, VPNList, VPNMembershipPolicy, ) +from catalystwan.models.policy.lists import SLAClassList logger = logging.getLogger(__name__) @@ -184,7 +184,7 @@ def configure_groups_of_interest(api: PolicyAPI) -> List[ConfigItem]: configured_items.append(ConfigItem(ClassMapList, class_map.name, class_map_id)) app_probe_class = AppProbeClassList(name="MyAppProbeClass") - app_probe_class.assign_forwarding_class("MyClassMap").add_color_mapping("3g", 5) + app_probe_class.assign_forwarding_class("MyClassMap").add_color_mapping("green", 5) app_probe_class_id = api.lists.create(app_probe_class) configured_items.append(ConfigItem(AppProbeClassList, app_probe_class.name, app_probe_class_id)) diff --git a/pyproject.toml b/pyproject.toml index 3b731f807..69efdd4f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "catalystwan" -version = "0.30.0" +version = "0.31.0dev1" description = "Cisco Catalyst WAN SDK for Python" authors = ["kagorski "] readme = "README.md"