Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flaky network adaptations #7

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion catalystwan/api/templates/models/cisco_bgp_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class Ipv6Neighbor(FeatureTemplateValidator):
address: str
description: Optional[str] = None
shutdown: Optional[BoolStr] = None
remote_as: int = Field(default=None, json_schema_extra={"vmanage_key": "remote-as"})
remote_as: int = Field(json_schema_extra={"vmanage_key": "remote-as"})
keepalive: Optional[int] = Field(default=None, json_schema_extra={"data_path": ["timers"]})
holdtime: Optional[int] = Field(default=None, json_schema_extra={"data_path": ["timers"]})
if_name: Optional[str] = Field(
Expand Down
4 changes: 2 additions & 2 deletions catalystwan/api/templates/models/cisco_snmp_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ class Target(FeatureTemplateValidator):
vpn_id: int = Field(json_schema_extra={"vmanage_key": "vpn-id"})
ip: str
port: int
community_name: str = Field(default=None, json_schema_extra={"vmanage_key": "community-name"})
community_name: Optional[str] = Field(default=None, json_schema_extra={"vmanage_key": "community-name"})
user: Optional[str] = None
source_interface: str = Field(default=None, json_schema_extra={"vmanage_key": "source-interface"})
source_interface: Optional[str] = Field(default=None, json_schema_extra={"vmanage_key": "source-interface"})
model_config = ConfigDict(populate_by_name=True)


Expand Down
6 changes: 4 additions & 2 deletions catalystwan/api/templates/models/cisco_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from enum import Enum
from pathlib import Path
from typing import ClassVar, List, Optional
from typing import ClassVar, List, Optional, Union

from pydantic import ConfigDict, Field

Expand Down Expand Up @@ -151,7 +151,9 @@ class CiscoSystemModel(FeatureTemplate):
default=DeviceVariable(name="system_system_ip"), json_schema_extra={"vmanage_key": "system-ip"}
)
overlay_id: Optional[int] = Field(default=None, json_schema_extra={"vmanage_key": "overlay-id"})
site_id: int = Field(default=DeviceVariable(name="system_site_id"), json_schema_extra={"vmanage_key": "site-id"})
site_id: Union[int, DeviceVariable] = Field(
default=DeviceVariable(name="system_site_id"), json_schema_extra={"vmanage_key": "site-id"}
)
site_type: Optional[List[SiteType]] = Field(default=None, json_schema_extra={"vmanage_key": "site-type"})
port_offset: Optional[int] = Field(default=None, json_schema_extra={"vmanage_key": "port-offset"})
port_hop: Optional[BoolStr] = Field(default=None, json_schema_extra={"vmanage_key": "port-hop"})
Expand Down
2 changes: 1 addition & 1 deletion catalystwan/api/templates/models/cisco_vpn_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ class Overload(str, Enum):
class Natpool(FeatureTemplateValidator):
name: int
prefix_length: Optional[int] = Field(default=None, json_schema_extra={"vmanage_key": "prefix-length"})
range_start: str = Field(default=None, json_schema_extra={"vmanage_key": "range-start"})
range_start: str = Field(json_schema_extra={"vmanage_key": "range-start"})
range_end: Optional[str] = Field(default=None, json_schema_extra={"vmanage_key": "range-end"})
overload: Overload = Overload.TRUE
direction: Direction
Expand Down
2 changes: 1 addition & 1 deletion catalystwan/api/templates/models/system_vsmart_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SystemVsmart(FeatureTemplate):
device_groups: Optional[str] = Field(default=None, json_schema_extra={"vmanage_key": "device-groups"})
longitude: Optional[int] = Field(default=None, ge=-180, le=180)
latitude: Optional[int] = Field(default=None, ge=-90, le=90)
system_tunnel_mtu: Optional[str] = Field(default=1024, json_schema_extra={"vmanage_key": "system-tunnel-mtu"})
system_tunnel_mtu: Optional[int] = Field(default=1024, json_schema_extra={"vmanage_key": "system-tunnel-mtu"})
location: Optional[str] = None
host_name: Optional[str] = Field(default=None, json_schema_extra={"vmanage_key": "host-name"})

Expand Down
8 changes: 5 additions & 3 deletions catalystwan/endpoints/configuration/software_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ class RemoteServerInfo(BaseModel):
class SoftwareRemoteServer(BaseModel):
model_config = ConfigDict(populate_by_name=True)

filename: str = Field(default=None, serialization_alias="fileName", validation_alias="fileName")
remote_server_id: str = Field(default=None, serialization_alias="remoteServerId", validation_alias="remoteServerId")
filename: str = Field(serialization_alias="fileName", validation_alias="fileName")
remote_server_id: str = Field(serialization_alias="remoteServerId", validation_alias="remoteServerId")
smu_defect_id: Optional[str] = Field(
default=None, serialization_alias="smuDefectId", validation_alias="smuDefectId"
)
Expand Down Expand Up @@ -158,7 +158,9 @@ class SoftwareImageDetails(BaseModel):
vnf_properties_json: Optional[str] = Field(
default=None, serialization_alias="vnfPropertiesJson", validation_alias="vnfPropertiesJson"
)
remote_server_id: str = Field(default=None, serialization_alias="remoteServerId", validation_alias="remoteServerId")
remote_server_id: Optional[str] = Field(
default=None, serialization_alias="remoteServerId", validation_alias="remoteServerId"
)


class ConfigurationSoftwareActions(APIEndpoints):
Expand Down
4 changes: 2 additions & 2 deletions catalystwan/endpoints/configuration_dashboard_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Validation(BaseModel):
device_id: Optional[str] = Field(default=None, serialization_alias="deviceID", validation_alias="deviceID")
uuid: Optional[str] = Field(default=None, serialization_alias="uuid", validation_alias="uuid")
rid: Optional[int] = Field(default=None, serialization_alias="@rid", validation_alias="@rid")
status_id: str = Field(default=None, serialization_alias="statusId", validation_alias="statusId")
status_id: str = Field(serialization_alias="statusId", validation_alias="statusId")
process_id: Optional[str] = Field(default=None, serialization_alias="processId", validation_alias="processId")
action_config: Optional[Union[str, Dict]] = Field(
default=None, serialization_alias="actionConfig", validation_alias="actionConfig"
Expand All @@ -74,7 +74,7 @@ class Validation(BaseModel):
request_status: Optional[str] = Field(
default=None, serialization_alias="requestStatus", validation_alias="requestStatus"
)
status: OperationStatus = Field(default=None, serialization_alias="status", validation_alias="status")
status: OperationStatus = Field(serialization_alias="status", validation_alias="status")
order: Optional[int] = Field(default=None, serialization_alias="order", validation_alias="order")


Expand Down
2 changes: 1 addition & 1 deletion catalystwan/models/configuration/feature_profile/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ class IPv4Prefix(BaseModel):
class WANIPv4StaticRoute(BaseModel):
prefix: IPv4Prefix = Field()
gateway: Global[Literal["nextHop", "null0", "dhcp"]] = Field(default=Global(value="nextHop"), alias="gateway")
next_hops: Optional[List[NextHop]] = Field(default_factory=list, alias="nextHop")
next_hops: Optional[List[NextHop]] = Field(default=None, serialization_alias="nextHop", validation_alias="nextHop")
distance: Optional[Global[int]] = Field(default=None, alias="distance")

def set_to_next_hop(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
class ManagementVPN(BaseModel):
# TODO (mlembke): vpn_id can't have other value, it needs to be constant. How to do that?
vpn_id: Default[int] = Field(default=Default(value=512), frozen=True, alias="vpnId")
ipv4_routes: Optional[List[WANIPv4StaticRoute]] = Field(default=[], alias="ipv4Route")
ipv6_routes: Optional[List[WANIPv6StaticRoute]] = Field(default=[], alias="ipv6Route")
ipv4_routes: Optional[List[WANIPv4StaticRoute]] = Field(default=None, alias="ipv4Route")
ipv6_routes: Optional[List[WANIPv6StaticRoute]] = Field(default=None, alias="ipv6Route")
dns_ipv4: Optional[DNSIPv4] = Field(default=None, alias="dnsIpv4")
dns_ipv6: Optional[DNSIPv6] = Field(default=None, alias="dnsIpv6")
new_host_mapping: Optional[List[HostMapping]] = Field(default=[], alias="newHostMapping")
new_host_mapping: Optional[List[HostMapping]] = Field(default=None, alias="newHostMapping")
# TODO (mlembke): add interfaces
interface: Optional[Any] = Field(default=None)
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class BgpIPv4Neighbor(BaseModel):
description: Optional[Union[Global[str], Variable, Default[None]]] = None
shutdown: Optional[Union[Global[bool], Variable, Default[bool]]] = Default[bool](value=False)
remote_as: Union[Global[int], Variable] = Field(serialization_alias="remoteAs", validation_alias="remoteAs")
local_as: Union[Global[int], Variable] = Field(
local_as: Optional[Union[Global[str], Global[int], Variable, Default[None]]] = Field(
serialization_alias="localAs", validation_alias="localAs", default=None
)
keepalive: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=60)
Expand Down Expand Up @@ -199,7 +199,7 @@ class BgpIPv6Neighbor(BaseModel):
description: Optional[Union[Global[str], Variable, Default[None]]] = None
shutdown: Optional[Union[Global[bool], Variable, Default[bool]]] = Default[bool](value=False)
remote_as: Union[Global[int], Variable] = Field(serialization_alias="remoteAs", validation_alias="remoteAs")
local_as: Union[Global[int], Variable] = Field(
local_as: Optional[Union[Global[str], Global[int], Variable, Default[None]]] = Field(
serialization_alias="localAs", validation_alias="localAs", default=None
)
keepalive: Optional[Union[Global[int], Variable, Default[int]]] = Default[int](value=60)
Expand Down
35 changes: 35 additions & 0 deletions catalystwan/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2024 Cisco Systems, Inc. and its affiliates
from logging import getLogger
from typing import Callable, Tuple, Type, TypeVar

from requests import delete, get, head, options, patch, post, put, request
from requests.exceptions import ConnectionError, Timeout
from typing_extensions import Concatenate, ParamSpec

T = TypeVar("T")
P = ParamSpec("P")
logger = getLogger(__name__)


def retry(function: Callable[P, T], catch: Tuple[Type[Exception], ...]) -> Callable[Concatenate[int, P], T]:
def decorator(retries: int, *args: P.args, **kwargs: P.kwargs) -> T:
for _ in range(retries):
try:
return function(*args, **kwargs)
except catch as e:
logger.warning(f"Retrying: {e}")
return function(*args, **kwargs)

return decorator


# retry decorators for request methods, retries count added as first positional argument
catch = (ConnectionError, Timeout)
retry_request = retry(request, catch)
retry_get = retry(get, catch)
retry_options = retry(options, catch)
retry_head = retry(head, catch)
retry_post = retry(post, catch)
retry_put = retry(put, catch)
retry_patch = retry(patch, catch)
retry_delete = retry(delete, catch)
12 changes: 11 additions & 1 deletion catalystwan/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

from packaging.version import Version # type: ignore
from requests import PreparedRequest, Request, Response, Session, get, head
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError, HTTPError, RequestException
from urllib3 import Retry

from catalystwan import USER_AGENT
from catalystwan.apigw_auth import ApiGwAuth, ApiGwLogin, LoginMode
Expand Down Expand Up @@ -251,11 +253,19 @@ def __init__(
self._api_version: Version = NullVersion # type: ignore
self.restart_timeout: int = 1200
self.polling_requests_timeout: int = 10
self.request_timeout: Optional[int] = None
self.request_timeout: Optional[int] = 10
self._validate_responses = True
self._state: ManagerSessionState = ManagerSessionState.OPERATIVE
self._last_request: Optional[PreparedRequest] = None
self._limiter: RequestLimiter = request_limiter or RequestLimiter()
retry = Retry(
total=3,
backoff_factor=0.5,
backoff_jitter=0.3,
status_forcelist=[429, 503],
)
self.http_adapter = HTTPAdapter(max_retries=retry)
self.mount(self.base_url, self.http_adapter)

@cached_property
def api(self) -> APIContainer:
Expand Down
9 changes: 8 additions & 1 deletion catalystwan/tests/test_vmanage_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ def test_get_cookie(self, mock_post):

# Assert
mock_post.assert_called_with(
vmanage_auth.request_retries,
url="https://1.1.1.1:1111/j_security_check",
data=security_payload,
verify=False,
headers={"Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT},
timeout=vmanage_auth.request_timeout,
)

@mock.patch("catalystwan.vmanage_auth.post", side_effect=mock_request_j_security_check)
Expand All @@ -99,16 +101,19 @@ def test_get_cookie_invalid_username(self, mock_post):
"j_username": username,
"j_password": self.password,
}
vmanage_auth = vManageAuth(username, self.password)
# Act
with self.assertRaises(UnauthorizedAccessError):
vManageAuth(username, self.password).get_jsessionid()
vmanage_auth.get_jsessionid()

# Assert
mock_post.assert_called_with(
vmanage_auth.request_retries,
url="/j_security_check",
data=security_payload,
verify=False,
headers={"Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT},
timeout=vmanage_auth.request_timeout,
)

@mock.patch("catalystwan.vmanage_auth.get", side_effect=mock_valid_token)
Expand All @@ -128,10 +133,12 @@ def test_fetch_token(self, mock_get):
self.assertEqual(token, "valid-token")

mock_get.assert_called_with(
vmanage_auth.request_retries,
url=valid_url,
verify=False,
headers={"Content-Type": "application/json", "User-Agent": USER_AGENT},
cookies=cookies,
timeout=vmanage_auth.request_timeout,
)

@mock.patch("catalystwan.vmanage_auth.get", side_effect=mock_invalid_token_status)
Expand Down
35 changes: 31 additions & 4 deletions catalystwan/vmanage_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
from urllib.parse import urlparse

from packaging.version import Version # type: ignore
from requests import PreparedRequest, Response, get, post
from requests import PreparedRequest, Response
from requests.auth import AuthBase
from requests.cookies import RequestsCookieJar, merge_cookies

from catalystwan import USER_AGENT
from catalystwan.abstractions import APIEndpointClient, AuthProtocol
from catalystwan.exceptions import CatalystwanException, TenantSubdomainNotFound
from catalystwan.models.tenant import Tenant
from catalystwan.request import retry_get as get
from catalystwan.request import retry_post as post
from catalystwan.response import ManagerResponse, auth_response_debug
from catalystwan.version import NullVersion

Expand Down Expand Up @@ -81,6 +83,8 @@ def __init__(self, username: str, password: str, logger: Optional[logging.Logger
self._base_url: str = ""
self.session_count: int = 0
self.lock: RLock = RLock()
self.request_retries = 1
self.request_timeout = 10

def __str__(self) -> str:
return f"vManageAuth(username={self.username})"
Expand Down Expand Up @@ -109,7 +113,14 @@ def get_jsessionid(self) -> str:
}
url = self._base_url + "/j_security_check"
headers = {"Content-Type": "application/x-www-form-urlencoded", "User-Agent": USER_AGENT}
response: Response = post(url=url, headers=headers, data=security_payload, verify=self.verify)
response: Response = post(
self.request_retries,
url=url,
headers=headers,
data=security_payload,
verify=self.verify,
timeout=self.request_timeout,
)
self.sync_cookies(response.cookies)
self.logger.debug(auth_response_debug(response, str(self)))
if response.text != "" or not isinstance(self.jsessionid, str) or self.jsessionid == "":
Expand All @@ -120,10 +131,12 @@ def get_xsrftoken(self) -> str:
url = self._base_url + "/dataservice/client/token"
headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT}
response: Response = get(
self.request_retries,
url=url,
cookies=self.cookies,
headers=headers,
verify=self.verify,
timeout=self.request_timeout,
)
self.sync_cookies(response.cookies)
self.logger.debug(auth_response_debug(response, str(self)))
Expand Down Expand Up @@ -151,11 +164,21 @@ def logout(self, client: APIEndpointClient) -> None:
headers = {"x-xsrf-token": self.xsrftoken, "User-Agent": USER_AGENT}
if version >= Version("20.12"):
response = post(
f"{self._base_url}/logout", headers=headers, cookies=self.cookies, verify=self.verify
self.request_retries,
url=f"{self._base_url}/logout",
headers=headers,
cookies=self.cookies,
verify=self.verify,
timeout=self.request_timeout,
)
else:
response = get(
f"{self._base_url}/logout", headers=headers, cookies=self.cookies, verify=self.verify
self.request_retries,
url=f"{self._base_url}/logout",
headers=headers,
cookies=self.cookies,
verify=self.verify,
timeout=self.request_timeout,
)
self.logger.debug(auth_response_debug(response, str(self)))
if response.status_code != 200:
Expand Down Expand Up @@ -227,10 +250,12 @@ def get_tenantid(self) -> str:
url = self._base_url + "/dataservice/tenant"
headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT, "x-xsrf-token": self.xsrftoken}
response: Response = get(
self.request_retries,
url=url,
cookies=self.cookies,
headers=headers,
verify=self.verify,
timeout=self.request_timeout,
)
self.sync_cookies(response.cookies)
self.logger.debug(auth_response_debug(response, str(self)))
Expand All @@ -244,10 +269,12 @@ def get_vsessionid(self, tenantid: str) -> str:
url = self._base_url + f"/dataservice/tenant/{tenantid}/vsessionid"
headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT, "x-xsrf-token": self.xsrftoken}
response: Response = post(
self.request_retries,
url=url,
cookies=self.cookies,
headers=headers,
verify=self.verify,
timeout=self.request_timeout,
)
self.sync_cookies(response.cookies)
self.logger.debug(auth_response_debug(response, str(self)))
Expand Down