nac-test-pyats-common 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nac_test_pyats_common/__init__.py +49 -0
- nac_test_pyats_common/aci/__init__.py +40 -0
- nac_test_pyats_common/aci/auth.py +157 -0
- nac_test_pyats_common/aci/test_base.py +138 -0
- nac_test_pyats_common/catc/__init__.py +35 -0
- nac_test_pyats_common/catc/auth.py +205 -0
- nac_test_pyats_common/catc/test_base.py +168 -0
- nac_test_pyats_common/common/__init__.py +29 -0
- nac_test_pyats_common/common/base_device_resolver.py +492 -0
- nac_test_pyats_common/iosxe/__init__.py +58 -0
- nac_test_pyats_common/iosxe/catc_resolver.py +64 -0
- nac_test_pyats_common/iosxe/iosxe_resolver.py +62 -0
- nac_test_pyats_common/iosxe/registry.py +153 -0
- nac_test_pyats_common/iosxe/test_base.py +116 -0
- nac_test_pyats_common/py.typed +1 -0
- nac_test_pyats_common/sdwan/__init__.py +47 -0
- nac_test_pyats_common/sdwan/api_test_base.py +164 -0
- nac_test_pyats_common/sdwan/auth.py +215 -0
- nac_test_pyats_common/sdwan/device_resolver.py +179 -0
- nac_test_pyats_common/sdwan/ssh_test_base.py +101 -0
- nac_test_pyats_common-0.1.0.dist-info/METADATA +304 -0
- nac_test_pyats_common-0.1.0.dist-info/RECORD +24 -0
- nac_test_pyats_common-0.1.0.dist-info/WHEEL +4 -0
- nac_test_pyats_common-0.1.0.dist-info/licenses/LICENSE +385 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""SDWAN Manager authentication implementation for Cisco SD-WAN.
|
|
2
|
+
|
|
3
|
+
This module provides authentication functionality for Cisco SDWAN Manager (formerly
|
|
4
|
+
vManage), which manages the software-defined WAN fabric. The authentication mechanism
|
|
5
|
+
uses form-based login with JSESSIONID cookie and optional XSRF token for CSRF protection.
|
|
6
|
+
|
|
7
|
+
The module implements a two-tier API design:
|
|
8
|
+
1. _authenticate() - Low-level method that performs direct SDWAN Manager authentication
|
|
9
|
+
2. get_auth() - High-level method that leverages caching for efficient token reuse
|
|
10
|
+
|
|
11
|
+
This design ensures efficient session management by reusing valid sessions and only
|
|
12
|
+
re-authenticating when necessary, reducing unnecessary API calls to the SDWAN Manager.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
from nac_test.pyats_core.common.auth_cache import AuthCache # type: ignore[import-untyped]
|
|
20
|
+
|
|
21
|
+
# Default session lifetime for SDWAN Manager authentication in seconds
|
|
22
|
+
# SDWAN Manager sessions are typically valid for 30 minutes (1800 seconds) by default
|
|
23
|
+
SDWAN_MANAGER_SESSION_LIFETIME_SECONDS: int = 1800
|
|
24
|
+
|
|
25
|
+
# HTTP timeout for XSRF token fetch (shorter than auth timeout since it's optional)
|
|
26
|
+
XSRF_TOKEN_FETCH_TIMEOUT_SECONDS: float = 10.0
|
|
27
|
+
|
|
28
|
+
# HTTP timeout for authentication request
|
|
29
|
+
AUTH_REQUEST_TIMEOUT_SECONDS: float = 30.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SDWANManagerAuth:
|
|
33
|
+
"""SDWAN Manager authentication implementation with session caching.
|
|
34
|
+
|
|
35
|
+
This class provides a two-tier API for SDWAN Manager authentication:
|
|
36
|
+
|
|
37
|
+
1. Low-level _authenticate() method: Directly authenticates with SDWAN Manager using
|
|
38
|
+
form-based login and returns session data along with expiration time. This is
|
|
39
|
+
typically used by the caching layer and not called directly by consumers.
|
|
40
|
+
|
|
41
|
+
2. High-level get_auth() method: Provides cached session management, automatically
|
|
42
|
+
handling session renewal when expired. This is the primary method that consumers
|
|
43
|
+
should use for obtaining SDWAN Manager authentication data.
|
|
44
|
+
|
|
45
|
+
The authentication flow supports both:
|
|
46
|
+
- Pre-19.2 versions: JSESSIONID cookie only
|
|
47
|
+
- 19.2+ versions: JSESSIONID cookie plus X-XSRF-TOKEN header for CSRF protection
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> # Get authentication data for SDWAN Manager API calls
|
|
51
|
+
>>> auth_data = SDWANManagerAuth.get_auth()
|
|
52
|
+
>>> # Use in requests
|
|
53
|
+
>>> headers = {"Cookie": f"JSESSIONID={auth_data['jsessionid']}"}
|
|
54
|
+
>>> if auth_data.get("xsrf_token"):
|
|
55
|
+
... headers["X-XSRF-TOKEN"] = auth_data["xsrf_token"]
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _authenticate(url: str, username: str, password: str) -> tuple[dict[str, Any], int]:
|
|
60
|
+
"""Perform direct SDWAN Manager authentication and obtain session data.
|
|
61
|
+
|
|
62
|
+
This method performs a direct authentication request to the SDWAN Manager
|
|
63
|
+
using form-based login. It returns both the session data and its lifetime
|
|
64
|
+
for proper cache management.
|
|
65
|
+
|
|
66
|
+
The authentication process:
|
|
67
|
+
1. POST form credentials to /j_security_check endpoint
|
|
68
|
+
2. Extract JSESSIONID cookie from response
|
|
69
|
+
3. Attempt to fetch XSRF token (for 19.2+ only)
|
|
70
|
+
4. Return session data with TTL
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
url: Base URL of the SDWAN Manager (e.g., "https://sdwan-manager.example.com").
|
|
74
|
+
Should not include trailing slashes or API paths.
|
|
75
|
+
username: SDWAN Manager username for authentication. This should be a valid
|
|
76
|
+
user configured with appropriate permissions.
|
|
77
|
+
password: Password for the specified user account.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A tuple containing:
|
|
81
|
+
- auth_dict (dict): Dictionary with 'jsessionid' (str) and 'xsrf_token'
|
|
82
|
+
(str | None). The xsrf_token is None for pre-19.2 versions.
|
|
83
|
+
- expires_in (int): Session lifetime in seconds (typically 1800).
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
httpx.HTTPStatusError: If SDWAN Manager returns a non-2xx status code,
|
|
87
|
+
typically indicating authentication failure (401) or server error.
|
|
88
|
+
httpx.RequestError: If the request fails due to network issues,
|
|
89
|
+
connection timeouts, or other transport-level problems.
|
|
90
|
+
ValueError: If the JSESSIONID cookie is not received in the response,
|
|
91
|
+
indicating a malformed or unexpected response.
|
|
92
|
+
|
|
93
|
+
Note:
|
|
94
|
+
SSL verification is disabled (verify=False) to handle self-signed
|
|
95
|
+
certificates commonly used in lab and development deployments.
|
|
96
|
+
In production environments, proper certificate validation should be enabled
|
|
97
|
+
by either installing the certificate in the trust store or providing
|
|
98
|
+
a custom CA bundle via the verify parameter.
|
|
99
|
+
"""
|
|
100
|
+
# NOTE: SSL verification is disabled (verify=False) to handle self-signed
|
|
101
|
+
# certificates commonly used in lab and development deployments.
|
|
102
|
+
with httpx.Client(verify=False, timeout=AUTH_REQUEST_TIMEOUT_SECONDS) as client:
|
|
103
|
+
# Step 1: Form-based login to SDWAN Manager
|
|
104
|
+
auth_response = client.post(
|
|
105
|
+
f"{url}/j_security_check",
|
|
106
|
+
data={"j_username": username, "j_password": password},
|
|
107
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
108
|
+
follow_redirects=False,
|
|
109
|
+
)
|
|
110
|
+
auth_response.raise_for_status()
|
|
111
|
+
|
|
112
|
+
# Validate JSESSIONID cookie was received
|
|
113
|
+
if "JSESSIONID" not in auth_response.cookies:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"No JSESSIONID cookie received from SDWAN Manager. "
|
|
116
|
+
"This may indicate invalid credentials or a server error. "
|
|
117
|
+
f"Response status: {auth_response.status_code}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
jsessionid = auth_response.cookies["JSESSIONID"]
|
|
121
|
+
|
|
122
|
+
# Step 2: Attempt to get XSRF token (19.2+ only)
|
|
123
|
+
# Pre-19.2 versions do not require XSRF token, so failures are expected
|
|
124
|
+
xsrf_token: str | None = None
|
|
125
|
+
try:
|
|
126
|
+
token_response = client.get(
|
|
127
|
+
f"{url}/dataservice/client/token",
|
|
128
|
+
cookies={"JSESSIONID": jsessionid},
|
|
129
|
+
timeout=XSRF_TOKEN_FETCH_TIMEOUT_SECONDS,
|
|
130
|
+
)
|
|
131
|
+
if token_response.status_code == 200:
|
|
132
|
+
xsrf_token = token_response.text.strip()
|
|
133
|
+
except (httpx.HTTPError, httpx.TimeoutException):
|
|
134
|
+
# Pre-19.2 does not support XSRF tokens, continue without
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"jsessionid": jsessionid,
|
|
139
|
+
"xsrf_token": xsrf_token,
|
|
140
|
+
}, SDWAN_MANAGER_SESSION_LIFETIME_SECONDS
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def get_auth(cls) -> dict[str, Any]:
|
|
144
|
+
"""Get SDWAN Manager authentication data with automatic caching and renewal.
|
|
145
|
+
|
|
146
|
+
This is the primary method that consumers should use to obtain SDWAN Manager
|
|
147
|
+
authentication data. It leverages the AuthCache to efficiently manage
|
|
148
|
+
session lifecycle, reusing valid sessions and automatically renewing
|
|
149
|
+
expired ones. This significantly reduces the number of authentication
|
|
150
|
+
requests to the SDWAN Manager.
|
|
151
|
+
|
|
152
|
+
The method uses a cache key based on the controller type ("SDWAN_MANAGER") and
|
|
153
|
+
URL to ensure proper session isolation between different SDWAN Manager instances.
|
|
154
|
+
|
|
155
|
+
Environment Variables Required:
|
|
156
|
+
SDWAN_URL: Base URL of the SDWAN Manager
|
|
157
|
+
SDWAN_USERNAME: SDWAN Manager username for authentication
|
|
158
|
+
SDWAN_PASSWORD: SDWAN Manager password for authentication
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A dictionary containing:
|
|
162
|
+
- jsessionid (str): The session cookie value for API requests
|
|
163
|
+
- xsrf_token (str | None): The XSRF token for CSRF protection
|
|
164
|
+
(None for pre-19.2 versions)
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If any required environment variables (SDWAN_URL,
|
|
168
|
+
SDWAN_USERNAME, SDWAN_PASSWORD) are not set.
|
|
169
|
+
httpx.HTTPStatusError: If SDWAN Manager returns a non-2xx status code during
|
|
170
|
+
authentication, typically indicating invalid credentials (401) or
|
|
171
|
+
server issues (5xx).
|
|
172
|
+
httpx.RequestError: If the request fails due to network issues,
|
|
173
|
+
connection timeouts, or other transport-level problems.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
>>> # Set environment variables first
|
|
177
|
+
>>> import os
|
|
178
|
+
>>> os.environ["SDWAN_URL"] = "https://sdwan-manager.example.com"
|
|
179
|
+
>>> os.environ["SDWAN_USERNAME"] = "admin"
|
|
180
|
+
>>> os.environ["SDWAN_PASSWORD"] = "password123"
|
|
181
|
+
>>> # Get authentication data
|
|
182
|
+
>>> auth_data = SDWANManagerAuth.get_auth()
|
|
183
|
+
>>> # Use in API requests
|
|
184
|
+
>>> headers = {"Cookie": f"JSESSIONID={auth_data['jsessionid']}"}
|
|
185
|
+
>>> if auth_data.get("xsrf_token"):
|
|
186
|
+
... headers["X-XSRF-TOKEN"] = auth_data["xsrf_token"]
|
|
187
|
+
"""
|
|
188
|
+
url = os.environ.get("SDWAN_URL")
|
|
189
|
+
username = os.environ.get("SDWAN_USERNAME")
|
|
190
|
+
password = os.environ.get("SDWAN_PASSWORD")
|
|
191
|
+
|
|
192
|
+
if not all([url, username, password]):
|
|
193
|
+
missing_vars: list[str] = []
|
|
194
|
+
if not url:
|
|
195
|
+
missing_vars.append("SDWAN_URL")
|
|
196
|
+
if not username:
|
|
197
|
+
missing_vars.append("SDWAN_USERNAME")
|
|
198
|
+
if not password:
|
|
199
|
+
missing_vars.append("SDWAN_PASSWORD")
|
|
200
|
+
raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
|
|
201
|
+
|
|
202
|
+
# Normalize URL by removing trailing slash
|
|
203
|
+
url = url.rstrip("/") # type: ignore[union-attr]
|
|
204
|
+
|
|
205
|
+
def auth_wrapper() -> tuple[dict[str, Any], int]:
|
|
206
|
+
"""Wrapper for authentication that captures closure variables."""
|
|
207
|
+
return cls._authenticate(url, username, password) # type: ignore[arg-type]
|
|
208
|
+
|
|
209
|
+
# AuthCache.get_or_create returns dict[str, Any], but mypy can't verify this
|
|
210
|
+
# because nac_test lacks py.typed marker.
|
|
211
|
+
return AuthCache.get_or_create( # type: ignore[no-any-return]
|
|
212
|
+
controller_type="SDWAN_MANAGER",
|
|
213
|
+
url=url,
|
|
214
|
+
auth_func=auth_wrapper,
|
|
215
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""SD-WAN-specific device resolver for parsing the NAC data model.
|
|
2
|
+
|
|
3
|
+
This module provides the SDWANDeviceResolver class, which extends
|
|
4
|
+
BaseDeviceResolver to implement SD-WAN schema navigation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from nac_test_pyats_common.common import BaseDeviceResolver
|
|
11
|
+
from nac_test_pyats_common.iosxe.registry import register_iosxe_resolver
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register_iosxe_resolver("SDWAN")
|
|
17
|
+
class SDWANDeviceResolver(BaseDeviceResolver):
|
|
18
|
+
"""SD-WAN device resolver for D2D testing.
|
|
19
|
+
|
|
20
|
+
Navigates the SD-WAN NAC schema (sites[].routers[]) to extract
|
|
21
|
+
device information for SSH testing.
|
|
22
|
+
|
|
23
|
+
Schema structure:
|
|
24
|
+
sdwan:
|
|
25
|
+
sites:
|
|
26
|
+
- name: "site1"
|
|
27
|
+
routers:
|
|
28
|
+
- chassis_id: "abc123"
|
|
29
|
+
device_variables:
|
|
30
|
+
system_hostname: "router1"
|
|
31
|
+
vpn10_mgmt_ip: "10.1.1.100/32"
|
|
32
|
+
|
|
33
|
+
Credentials:
|
|
34
|
+
Uses IOSXE_USERNAME and IOSXE_PASSWORD environment variables
|
|
35
|
+
because SD-WAN edge devices are IOS-XE based.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> resolver = SDWANDeviceResolver(data_model)
|
|
39
|
+
>>> devices = resolver.get_resolved_inventory()
|
|
40
|
+
>>> for device in devices:
|
|
41
|
+
... print(f"{device['hostname']}: {device['host']}")
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def get_architecture_name(self) -> str:
|
|
45
|
+
"""Return 'sdwan' as the architecture identifier.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Architecture name used in logging and error messages.
|
|
49
|
+
"""
|
|
50
|
+
return "sdwan"
|
|
51
|
+
|
|
52
|
+
def get_schema_root_key(self) -> str:
|
|
53
|
+
"""Return 'sdwan' as the root key in the data model.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Root key used when loading test inventory and navigating schema.
|
|
57
|
+
"""
|
|
58
|
+
return "sdwan"
|
|
59
|
+
|
|
60
|
+
def navigate_to_devices(self) -> list[dict[str, Any]]:
|
|
61
|
+
"""Navigate SD-WAN schema: sdwan.sites[].routers[].
|
|
62
|
+
|
|
63
|
+
Traverses the SD-WAN data model structure to find all router
|
|
64
|
+
devices across all sites.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of router dictionaries from all sites.
|
|
68
|
+
"""
|
|
69
|
+
devices: list[dict[str, Any]] = []
|
|
70
|
+
sdwan_data = self.data_model.get("sdwan", {})
|
|
71
|
+
|
|
72
|
+
for site in sdwan_data.get("sites", []):
|
|
73
|
+
routers = site.get("routers", [])
|
|
74
|
+
devices.extend(routers)
|
|
75
|
+
|
|
76
|
+
return devices
|
|
77
|
+
|
|
78
|
+
def extract_device_id(self, device_data: dict[str, Any]) -> str:
|
|
79
|
+
"""Extract chassis_id as the device identifier.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
device_data: Router data dictionary from the data model.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Unique chassis_id string.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If the router is missing the chassis_id field.
|
|
89
|
+
"""
|
|
90
|
+
chassis_id = device_data.get("chassis_id")
|
|
91
|
+
if not chassis_id:
|
|
92
|
+
raise ValueError("Router missing 'chassis_id' field")
|
|
93
|
+
return str(chassis_id)
|
|
94
|
+
|
|
95
|
+
def extract_hostname(self, device_data: dict[str, Any]) -> str:
|
|
96
|
+
"""Extract hostname from device_variables.system_hostname.
|
|
97
|
+
|
|
98
|
+
Looks for system_hostname in the device_variables section.
|
|
99
|
+
Falls back to chassis_id if system_hostname is not available.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
device_data: Router data dictionary from the data model.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Device hostname string.
|
|
106
|
+
"""
|
|
107
|
+
device_vars = device_data.get("device_variables", {})
|
|
108
|
+
|
|
109
|
+
if "system_hostname" in device_vars:
|
|
110
|
+
return str(device_vars["system_hostname"])
|
|
111
|
+
|
|
112
|
+
# Fallback to chassis_id
|
|
113
|
+
chassis_id = device_data.get("chassis_id", "unknown")
|
|
114
|
+
logger.warning(f"No system_hostname found for {chassis_id}, using chassis_id as hostname")
|
|
115
|
+
return str(chassis_id)
|
|
116
|
+
|
|
117
|
+
def extract_host_ip(self, device_data: dict[str, Any]) -> str:
|
|
118
|
+
"""Extract management IP from device_variables.
|
|
119
|
+
|
|
120
|
+
Handles CIDR notation (e.g., "10.1.1.100/32" -> "10.1.1.100").
|
|
121
|
+
Uses management_ip_variable field to determine which variable
|
|
122
|
+
contains the management IP.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
device_data: Router data dictionary from the data model.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
IP address string without CIDR notation (e.g., "10.1.1.100").
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: If no management IP can be found.
|
|
132
|
+
"""
|
|
133
|
+
device_vars = device_data.get("device_variables", {})
|
|
134
|
+
|
|
135
|
+
# Get the variable name that contains the management IP
|
|
136
|
+
ip_var = device_data.get("management_ip_variable")
|
|
137
|
+
|
|
138
|
+
if ip_var and ip_var in device_vars:
|
|
139
|
+
ip_value = str(device_vars[ip_var])
|
|
140
|
+
else:
|
|
141
|
+
# Fallback: try common variable names
|
|
142
|
+
for fallback_var in ["mgmt_ip", "management_ip", "vpn0_ip"]:
|
|
143
|
+
if fallback_var in device_vars:
|
|
144
|
+
ip_value = str(device_vars[fallback_var])
|
|
145
|
+
break
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"Could not find management IP for device. "
|
|
149
|
+
"Set 'management_ip_variable' in test_inventory or use "
|
|
150
|
+
"standard variable names (mgmt_ip, management_ip, vpn0_ip)."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Strip CIDR notation if present
|
|
154
|
+
if "/" in ip_value:
|
|
155
|
+
ip_value = ip_value.split("/")[0]
|
|
156
|
+
|
|
157
|
+
return ip_value
|
|
158
|
+
|
|
159
|
+
def extract_os_type(self, device_data: dict[str, Any]) -> str:
|
|
160
|
+
"""Extract OS type, defaulting to 'iosxe' for SD-WAN edges.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
device_data: Router data dictionary from the data model.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
OS type string (e.g., "iosxe", "nxos", "iosxr").
|
|
167
|
+
"""
|
|
168
|
+
return str(device_data.get("os", "iosxe"))
|
|
169
|
+
|
|
170
|
+
def get_credential_env_vars(self) -> tuple[str, str]:
|
|
171
|
+
"""Return IOS-XE credential env vars for SD-WAN edge devices.
|
|
172
|
+
|
|
173
|
+
SD-WAN D2D tests connect to IOS-XE based edge devices,
|
|
174
|
+
NOT the vManage/SDWAN Manager controller.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Tuple of (username_env_var, password_env_var).
|
|
178
|
+
"""
|
|
179
|
+
return ("IOSXE_USERNAME", "IOSXE_PASSWORD")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""SD-WAN specific base test class for SSH/Direct-to-Device testing.
|
|
2
|
+
|
|
3
|
+
This module provides the SDWANTestBase class, which extends the generic SSHTestBase
|
|
4
|
+
to add SD-WAN-specific functionality for device-to-device (D2D) testing.
|
|
5
|
+
|
|
6
|
+
The class delegates device inventory resolution to SDWANDeviceResolver, which
|
|
7
|
+
handles all SD-WAN schema navigation and credential injection.
|
|
8
|
+
|
|
9
|
+
Credentials:
|
|
10
|
+
SD-WAN D2D tests connect to IOS-XE based edge devices, NOT the SDWAN Manager
|
|
11
|
+
controller. Set these environment variables:
|
|
12
|
+
- IOSXE_USERNAME: SSH username for edge devices
|
|
13
|
+
- IOSXE_PASSWORD: SSH password for edge devices
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from nac_test.pyats_core.common.ssh_base_test import SSHTestBase # type: ignore[import-untyped]
|
|
21
|
+
|
|
22
|
+
from .device_resolver import SDWANDeviceResolver
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SDWANTestBase(SSHTestBase): # type: ignore[misc]
|
|
28
|
+
"""SD-WAN-specific base test class for SSH/D2D testing.
|
|
29
|
+
|
|
30
|
+
This class extends SSHTestBase and implements the contract required by
|
|
31
|
+
nac-test's SSH execution engine. Device inventory resolution is fully
|
|
32
|
+
delegated to SDWANDeviceResolver.
|
|
33
|
+
|
|
34
|
+
Credentials:
|
|
35
|
+
SD-WAN D2D tests require IOSXE_USERNAME and IOSXE_PASSWORD environment
|
|
36
|
+
variables (NOT SDWAN_* which are for the controller).
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
class MySDWANSSHTest(SDWANTestBase):
|
|
40
|
+
@aetest.test
|
|
41
|
+
def verify_device_connectivity(self, steps, device):
|
|
42
|
+
# SSH-based verification logic here
|
|
43
|
+
pass
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_ssh_device_inventory(cls, data_model: dict[str, Any]) -> list[dict[str, Any]]:
|
|
48
|
+
"""Parse the SD-WAN data model to retrieve the device inventory.
|
|
49
|
+
|
|
50
|
+
This method is the entry point called by nac-test's orchestrator.
|
|
51
|
+
All device inventory resolution is delegated to SDWANDeviceResolver,
|
|
52
|
+
which handles:
|
|
53
|
+
- Test inventory loading (test_inventory.yaml)
|
|
54
|
+
- Schema navigation (sites[].routers[])
|
|
55
|
+
- Variable resolution (hostnames, IPs)
|
|
56
|
+
- Credential injection (IOSXE_USERNAME, IOSXE_PASSWORD)
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
data_model: The merged data model from nac-test containing all
|
|
60
|
+
sites.nac.yaml data with resolved variables.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of device dictionaries, each containing:
|
|
64
|
+
- hostname (str): Device hostname
|
|
65
|
+
- host (str): Management IP address for SSH connection
|
|
66
|
+
- os (str): Operating system type (e.g., "iosxe")
|
|
67
|
+
- username (str): SSH username from IOSXE_USERNAME
|
|
68
|
+
- password (str): SSH password from IOSXE_PASSWORD
|
|
69
|
+
- device_id (str): Device identifier (chassis_id)
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If IOSXE_USERNAME or IOSXE_PASSWORD env vars are not set.
|
|
73
|
+
"""
|
|
74
|
+
logger.info("SDWANTestBase: Resolving device inventory via SDWANDeviceResolver")
|
|
75
|
+
|
|
76
|
+
# Delegate entirely to the resolver
|
|
77
|
+
# SDWANDeviceResolver handles:
|
|
78
|
+
# 1. Test inventory loading via BaseDeviceResolver._load_inventory()
|
|
79
|
+
# 2. Schema navigation via navigate_to_devices()
|
|
80
|
+
# 3. Credential injection via _inject_credentials() using IOSXE_* env vars
|
|
81
|
+
resolver = SDWANDeviceResolver(data_model)
|
|
82
|
+
return resolver.get_resolved_inventory()
|
|
83
|
+
|
|
84
|
+
def get_device_credentials(self, device: dict[str, Any]) -> dict[str, str | None]:
|
|
85
|
+
"""Get SD-WAN edge device SSH credentials from environment variables.
|
|
86
|
+
|
|
87
|
+
SD-WAN D2D tests connect to IOS-XE edge devices (vEdge, ISR, etc.),
|
|
88
|
+
NOT the SDWAN Manager controller. Use IOSXE_* environment variables.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
device: Device dictionary (not used - all devices share credentials).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary containing:
|
|
95
|
+
- username (str | None): SSH username from IOSXE_USERNAME
|
|
96
|
+
- password (str | None): SSH password from IOSXE_PASSWORD
|
|
97
|
+
"""
|
|
98
|
+
return {
|
|
99
|
+
"username": os.environ.get("IOSXE_USERNAME"),
|
|
100
|
+
"password": os.environ.get("IOSXE_PASSWORD"),
|
|
101
|
+
}
|