Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

Commit

Permalink
dev-uxmt: dev release prep (#676)
Browse files Browse the repository at this point in the history
* fix model_validators with before mode

* remove print statement

* skip not validated policy definitions and lists during collect

* fix literal default value

* add version field in UX1/UX2Config models

* fix bgp parcel enveloping alias path

* use UX2ConfigPushResult as push return value for push_ux2_config

* allow disabling of response validation against pydantic model

* include original cause and info from server in ManagerHTTPError

* fix: request and response must be provided as kwargs

* introduce find method for DataSequence (#627)

* api and endpoints containers as cached properties (#629)

* fix typo

* hotfix ManagerErrorInfo model

* do not collect templates for specific device types

* bump version, generate ENDPOINTS.md

* fix OspfArea model
  • Loading branch information
sbasan authored May 20, 2024
1 parent fa19e19 commit a7ae53d
Show file tree
Hide file tree
Showing 31 changed files with 604 additions and 386 deletions.
473 changes: 255 additions & 218 deletions ENDPOINTS.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion catalystwan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

import urllib3

USER_AGENT = f"{__package__}/{metadata.version(__package__)}"
PACKAGE_VERSION = metadata.version(__package__)
USER_AGENT = f"{__package__}/{PACKAGE_VERSION}"


def with_proc_info_header(method: Callable[..., str]) -> Callable[..., str]:
Expand Down
8 changes: 6 additions & 2 deletions catalystwan/abstractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ def text(self) -> str:
def content(self) -> bytes:
...

def dataobj(self, cls: Type[T], sourcekey: Optional[str]) -> T:
def dataobj(self, cls: Type[T], sourcekey: Optional[str], validate: bool) -> T:
...

def dataseq(self, cls: Type[T], sourcekey: Optional[str]) -> DataSequence[T]:
def dataseq(self, cls: Type[T], sourcekey: Optional[str], validate: bool) -> DataSequence[T]:
...

def json(self) -> dict:
Expand All @@ -52,3 +52,7 @@ def api_version(self) -> Optional[Version]:
@property
def session_type(self) -> Optional[SessionType]:
...

@property
def validate_response(self) -> bool:
...
4 changes: 2 additions & 2 deletions catalystwan/api/builders/feature_profiles/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ def build(self) -> FeatureProfileBuildReport:
"""

profile_uuid = self._endpoints.create_profile(self._profile).id
self.build_raport = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
self.build_report = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
for config_parcel in self._cli_configs:
self._create_parcel(profile_uuid, config_parcel)
return self.build_raport
return self.build_report

@handle_build_report
def _create_parcel(self, profile_uuid: UUID, parcel: AnyCliParcel) -> UUID:
Expand Down
4 changes: 2 additions & 2 deletions catalystwan/api/builders/feature_profiles/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ def build(self) -> FeatureProfileBuildReport:
"""

profile_uuid = self._endpoints.create_sdwan_other_feature_profile(self._profile).id
self.build_raport = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
self.build_report = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
for parcel in self._independent_items:
self._create_parcel(profile_uuid, parcel)
return self.build_raport
return self.build_report

@handle_build_report
def _create_parcel(self, profile_uuid: UUID, parcel: AnyOtherParcel) -> UUID:
Expand Down
4 changes: 2 additions & 2 deletions catalystwan/api/builders/feature_profiles/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ def handle_build_report(func):
def wrapper(self, profile_uuid: UUID, parcel: AnyParcel, *args, **kwargs) -> Optional[UUID]:
try:
uuid = func(self, profile_uuid, parcel, *args, **kwargs)
self.build_raport.add_created_parcel(parcel.parcel_name, uuid)
self.build_report.add_created_parcel(parcel.parcel_name, uuid)
return uuid
except ManagerHTTPError as e:
self.build_raport.add_failed_parcel(
self.build_report.add_failed_parcel(
parcel_name=parcel.parcel_name,
parcel_type=parcel._get_parcel_type(),
error_info=e.info,
Expand Down
6 changes: 3 additions & 3 deletions catalystwan/api/builders/feature_profiles/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def build(self) -> FeatureProfileBuildReport:
Service feature profile UUID
"""
profile_uuid = self._endpoints.create_sdwan_service_feature_profile(self._profile).id
self.build_raport = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
self.build_report = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
for parcel in self._independent_items:
self._create_parcel(profile_uuid, parcel)
for vpn_tag, vpn_parcel in self._independent_items_vpns.items():
Expand All @@ -129,7 +129,7 @@ def build(self) -> FeatureProfileBuildReport:
subparcel_fail_message = (
f"Parent parcel: {vpn_parcel.parcel_name} failed to create. This subparcel is dependent on it."
)
self.build_raport.add_failed_parcel(
self.build_report.add_failed_parcel(
sub_parcel.parcel_name,
sub_parcel._get_parcel_type(), # type: ignore
subparcel_fail_message
Expand All @@ -140,7 +140,7 @@ def build(self) -> FeatureProfileBuildReport:
for sub_parcel in self._depended_items_on_vpns[vpn_tag]:
self._create_parcel(profile_uuid, sub_parcel, vpn_uuid)

return self.build_raport
return self.build_report

@handle_build_report
def _create_parcel(self, profile_uuid: UUID, parcel: AnyServiceParcel, vpn_uuid: Optional[None] = None) -> UUID:
Expand Down
4 changes: 2 additions & 2 deletions catalystwan/api/builders/feature_profiles/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ def build(self) -> FeatureProfileBuildReport:
"""

profile_uuid = self._endpoints.create_sdwan_system_feature_profile(self._profile).id
self.build_raport = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
self.build_report = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
for parcel in self._independent_items:
self._create_parcels(profile_uuid, parcel)
return self.build_raport
return self.build_report

@handle_build_report
def _create_parcels(self, profile_uuid: UUID, parcel: AnySystemParcel) -> UUID:
Expand Down
4 changes: 2 additions & 2 deletions catalystwan/api/builders/feature_profiles/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ def build(self) -> FeatureProfileBuildReport:
"""

profile_uuid = self._endpoints.create_transport_feature_profile(self._profile).id
self.build_raport = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
self.build_report = FeatureProfileBuildReport(profile_uuid=profile_uuid, profile_name=self._profile.name)
for parcel in self._independent_items:
self._create_parcel(profile_uuid, parcel)
for vpn_tag, vpn_parcel in self._independent_items_vpns.items():
# TODO: Add subparcels to VPN
vpn_uuid = self._create_parcel(profile_uuid, vpn_parcel)
for subparcel in self._dependent_items_on_vpns[vpn_tag]:
self._create_parcel(profile_uuid, subparcel, vpn_uuid)
return self.build_raport
return self.build_report

@handle_build_report
def _create_parcel(self, profile_uuid: UUID, parcel: AnyTransportParcel, vpn_uuid: Optional[None] = None) -> UUID:
Expand Down
10 changes: 8 additions & 2 deletions catalystwan/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,9 +592,15 @@ def wrapper(*args, **kwargs):
pass
elif issubclass(self.return_spec.payload_type, BaseModel):
if self.return_spec.sequence_type == DataSequence:
return response.dataseq(self.return_spec.payload_type, self.resp_json_key)
return response.dataseq(
cls=self.return_spec.payload_type,
sourcekey=self.resp_json_key,
validate=_self._client.validate_response,
)
else:
return response.dataobj(self.return_spec.payload_type, self.resp_json_key)
return response.dataobj(
self.return_spec.payload_type, self.resp_json_key, validate=_self._client.validate_response
)
elif issubclass(self.return_spec.payload_type, str):
return response.text
elif issubclass(self.return_spec.payload_type, bytes):
Expand Down
2 changes: 2 additions & 0 deletions catalystwan/endpoints/tenant_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class MigrationTokenQueryParams(BaseModel):
@model_validator(mode="before")
@classmethod
def single_migration_id_required(cls, values: Any):
if not isinstance(values, dict):
return values
migration_id = values.get("migrationId")
if isinstance(migration_id, list) and len(migration_id) == 1:
values["migrationId"] = migration_id[0]
Expand Down
22 changes: 12 additions & 10 deletions catalystwan/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Copyright 2023 Cisco Systems, Inc. and its affiliates

from typing import Any, Optional, Union
from typing import Union

from pydantic import BaseModel
from requests import HTTPError, RequestException


class ManagerErrorInfo(BaseModel):
message: Union[str, None]
details: Union[str, None]
code: Union[str, None]
message: Union[str, None] = None
details: Union[str, None] = None
code: Union[str, None] = None


class CatalystwanException(Exception):
Expand All @@ -19,16 +19,18 @@ class CatalystwanException(Exception):
class ManagerRequestException(RequestException, CatalystwanException):
"""Exception raised when there is ambigous problem during sending of request to Manager"""

def __init__(self, *args, **kwargs):
"""Initialize RequestException with `request` and `response` objects."""
super().__init__(*args, **kwargs)


class ManagerHTTPError(HTTPError, ManagerRequestException):
def __init__(self, *, error_info: Optional[ManagerErrorInfo], request: Any, response: Any):
def __init__(self, *args, error_info: ManagerErrorInfo, **kwargs):
"""Initialize RequestException with `error_info`, `request` and `response` objects."""
self.info = error_info
super().__init__(request=request, response=response)
info_str = str(self.info)
_args = args
if not _args:
_args = (info_str,)
else:
_args = (str(_args[0]) + "\n" + info_str,) + _args[1:]
super().__init__(*_args, **kwargs)

def __str__(self):
error_info_str = str(self.info) if self.info else "No error information"
Expand Down
42 changes: 32 additions & 10 deletions catalystwan/models/configuration/config_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from uuid import UUID

from packaging.version import Version
from pydantic import BaseModel, ConfigDict, Field, model_validator

from catalystwan import PACKAGE_VERSION
from catalystwan.api.builders.feature_profiles.report import FailedParcel, FeatureProfileBuildReport
from catalystwan.api.templates.device_template.device_template import DeviceTemplate, GeneralTemplate
from catalystwan.endpoints.configuration_group import ConfigGroupCreationPayload
Expand All @@ -17,6 +19,19 @@
from catalystwan.models.policy.localized import LocalizedPolicyInfo
from catalystwan.models.policy.security import AnySecurityPolicyInfo
from catalystwan.models.templates import FeatureTemplateInformation, TemplateInformation
from catalystwan.version import parse_api_version


class VersionInfo(BaseModel):
platform: str = "unknown"
sdk: str = PACKAGE_VERSION

@property
def platform_api(self) -> Version:
return parse_api_version(self.platform)

def is_compatible(self, other: "VersionInfo"):
return True if (self.sdk == other.sdk and self.platform_api == other.platform_api) else False


class DeviceTemplateWithInfo(DeviceTemplate):
Expand Down Expand Up @@ -55,7 +70,7 @@ def get_flattened_general_templates(self) -> List[GeneralTemplate]:


class UX1Policies(BaseModel):
model_config = ConfigDict(populate_by_name=True)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
centralized_policies: List[CentralizedPolicyInfo] = Field(
default=[],
serialization_alias="centralizedPolicies",
Expand Down Expand Up @@ -97,6 +112,7 @@ class UX1Templates(BaseModel):

class UX1Config(BaseModel):
# All UX1 Configuration items - Mega Model
version: VersionInfo = VersionInfo()
policies: UX1Policies = UX1Policies()
templates: UX1Templates = UX1Templates()

Expand Down Expand Up @@ -149,7 +165,8 @@ class FailedConversionItem(BaseModel):

class UX2Config(BaseModel):
# All UX2 Configuration items - Mega Model
model_config = ConfigDict(populate_by_name=True)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
version: VersionInfo = VersionInfo()
topology_groups: List[TransformedTopologyGroup] = Field(
default=[],
serialization_alias="topologyGroups",
Expand All @@ -173,11 +190,15 @@ class UX2Config(BaseModel):
serialization_alias="profileParcels",
validation_alias="profileParcels",
)
cloud_credentials: Optional[CloudCredentials] = None
cloud_credentials: Optional[CloudCredentials] = Field(
default=None, serialization_alias="cloudCredentials", validation_alias="cloudCredentials"
)

@model_validator(mode="before")
@classmethod
def insert_parcel_type_from_headers(cls, values: Dict[str, Any]):
if not isinstance(values, dict):
return values
profile_parcels = values.get("profileParcels", [])
if not profile_parcels:
profile_parcels = values.get("profile_parcels", [])
Expand Down Expand Up @@ -276,7 +297,8 @@ def get_summary(self) -> str:
return success_rate_message


class UX2ConfigRollback(BaseModel):
class UX2RollbackInfo(BaseModel):
model_config = ConfigDict(populate_by_name=True)
config_group_ids: List[UUID] = Field(
default_factory=list,
serialization_alias="ConfigGroupIds",
Expand All @@ -288,14 +310,14 @@ class UX2ConfigRollback(BaseModel):
validation_alias="FeatureProfileIds",
)

report: UX2ConfigPushReport = Field(
default=UX2ConfigPushReport(),
serialization_alias="ConfigPushReport",
validation_alias="ConfigPushReport",
)

def add_config_group(self, config_group_id: UUID) -> None:
self.config_group_ids.append(config_group_id)

def add_feature_profile(self, feature_profile_id: UUID, profile_type: ProfileType) -> None:
self.feature_profile_ids.append((feature_profile_id, profile_type))


class UX2ConfigPushResult(BaseModel):
model_config = ConfigDict(populate_by_name=True)
rollback: UX2RollbackInfo = UX2RollbackInfo()
report: UX2ConfigPushReport = UX2ConfigPushReport()
Loading

0 comments on commit a7ae53d

Please sign in to comment.