nac-test-pyats-common 0.1.1__py3-none-any.whl → 0.2.1__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 +8 -1
- nac_test_pyats_common/aci/auth.py +177 -53
- nac_test_pyats_common/aci/test_base.py +35 -12
- nac_test_pyats_common/catc/__init__.py +27 -9
- nac_test_pyats_common/catc/{test_base.py → api_test_base.py} +35 -11
- nac_test_pyats_common/catc/auth.py +116 -49
- nac_test_pyats_common/catc/device_resolver.py +171 -0
- nac_test_pyats_common/catc/ssh_test_base.py +115 -0
- nac_test_pyats_common/common/base_device_resolver.py +144 -199
- nac_test_pyats_common/iosxe/iosxe_resolver.py +2 -2
- nac_test_pyats_common/iosxe/test_base.py +6 -0
- nac_test_pyats_common/sdwan/api_test_base.py +28 -7
- nac_test_pyats_common/sdwan/auth.py +125 -64
- nac_test_pyats_common/sdwan/device_resolver.py +69 -22
- nac_test_pyats_common/sdwan/ssh_test_base.py +11 -9
- {nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/METADATA +3 -2
- nac_test_pyats_common-0.2.1.dist-info/RECORD +25 -0
- nac_test_pyats_common/iosxe/catc_resolver.py +0 -67
- nac_test_pyats_common-0.1.1.dist-info/RECORD +0 -24
- {nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/WHEEL +0 -0
- {nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,14 +14,21 @@ The module implements a two-tier API design:
|
|
|
14
14
|
|
|
15
15
|
This design ensures efficient session management by reusing valid sessions and only
|
|
16
16
|
re-authenticating when necessary, reducing unnecessary API calls to the SDWAN Manager.
|
|
17
|
+
|
|
18
|
+
Note on Fork Safety:
|
|
19
|
+
This module uses urllib instead of httpx for synchronous authentication requests.
|
|
20
|
+
httpx is NOT fork-safe on macOS - creating httpx.Client after fork() causes
|
|
21
|
+
silent crashes due to OpenSSL threading issues. urllib uses simpler primitives
|
|
22
|
+
that work correctly after fork().
|
|
17
23
|
"""
|
|
18
24
|
|
|
19
25
|
import os
|
|
20
26
|
from typing import Any
|
|
21
27
|
|
|
22
|
-
import
|
|
23
|
-
from nac_test.pyats_core.common.
|
|
24
|
-
|
|
28
|
+
from nac_test.pyats_core.common.auth_cache import AuthCache
|
|
29
|
+
from nac_test.pyats_core.common.subprocess_auth import (
|
|
30
|
+
SubprocessAuthError, # noqa: F401 - re-exported for callers to catch
|
|
31
|
+
execute_auth_subprocess,
|
|
25
32
|
)
|
|
26
33
|
|
|
27
34
|
# Default session lifetime for SDWAN Manager authentication in seconds
|
|
@@ -63,7 +70,7 @@ class SDWANManagerAuth:
|
|
|
63
70
|
|
|
64
71
|
@staticmethod
|
|
65
72
|
def _authenticate(
|
|
66
|
-
url: str, username: str, password: str
|
|
73
|
+
url: str, username: str, password: str, verify_ssl: bool = False
|
|
67
74
|
) -> tuple[dict[str, Any], int]:
|
|
68
75
|
"""Perform direct SDWAN Manager authentication and obtain session data.
|
|
69
76
|
|
|
@@ -77,12 +84,19 @@ class SDWANManagerAuth:
|
|
|
77
84
|
3. Attempt to fetch XSRF token (for 19.2+ only)
|
|
78
85
|
4. Return session data with TTL
|
|
79
86
|
|
|
87
|
+
Note: On macOS, SSL operations in forked processes crash due to OpenSSL
|
|
88
|
+
threading issues. This method uses subprocess with spawn context to perform
|
|
89
|
+
authentication in a fresh process, avoiding the fork+SSL crash.
|
|
90
|
+
|
|
80
91
|
Args:
|
|
81
92
|
url: Base URL of the SDWAN Manager (e.g., "https://sdwan-manager.example.com").
|
|
82
93
|
Should not include trailing slashes or API paths.
|
|
83
94
|
username: SDWAN Manager username for authentication. This should be a valid
|
|
84
95
|
user configured with appropriate permissions.
|
|
85
96
|
password: Password for the specified user account.
|
|
97
|
+
verify_ssl: Whether to verify SSL certificates. Defaults to False to
|
|
98
|
+
handle self-signed certificates commonly used in lab and development
|
|
99
|
+
deployments.
|
|
86
100
|
|
|
87
101
|
Returns:
|
|
88
102
|
A tuple containing:
|
|
@@ -91,61 +105,99 @@ class SDWANManagerAuth:
|
|
|
91
105
|
- expires_in (int): Session lifetime in seconds (typically 1800).
|
|
92
106
|
|
|
93
107
|
Raises:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
httpx.RequestError: If the request fails due to network issues,
|
|
97
|
-
connection timeouts, or other transport-level problems.
|
|
98
|
-
ValueError: If the JSESSIONID cookie is not received in the response,
|
|
99
|
-
indicating a malformed or unexpected response.
|
|
100
|
-
|
|
101
|
-
Note:
|
|
102
|
-
SSL verification is disabled (verify=False) to handle self-signed
|
|
103
|
-
certificates commonly used in lab and development deployments.
|
|
104
|
-
In production environments, proper certificate validation should be enabled
|
|
105
|
-
by either installing the certificate in the trust store or providing
|
|
106
|
-
a custom CA bundle via the verify parameter.
|
|
108
|
+
SubprocessAuthError: If authentication subprocess fails.
|
|
109
|
+
ValueError: If the authentication response is malformed.
|
|
107
110
|
"""
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
111
|
+
# Build auth parameters for subprocess
|
|
112
|
+
auth_params = {
|
|
113
|
+
"url": url,
|
|
114
|
+
"username": username,
|
|
115
|
+
"password": password,
|
|
116
|
+
"timeout": AUTH_REQUEST_TIMEOUT_SECONDS,
|
|
117
|
+
"xsrf_timeout": XSRF_TOKEN_FETCH_TIMEOUT_SECONDS,
|
|
118
|
+
"verify_ssl": verify_ssl,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# SDWAN-specific authentication logic
|
|
122
|
+
# This script assumes `params` dict is already loaded by execute_auth_subprocess
|
|
123
|
+
auth_script_body = """
|
|
124
|
+
import http.cookiejar
|
|
125
|
+
import ssl
|
|
126
|
+
import urllib.parse
|
|
127
|
+
import urllib.request
|
|
128
|
+
|
|
129
|
+
url = params["url"]
|
|
130
|
+
username = params["username"]
|
|
131
|
+
password = params["password"]
|
|
132
|
+
timeout = params["timeout"]
|
|
133
|
+
xsrf_timeout = params["xsrf_timeout"]
|
|
134
|
+
verify_ssl = params["verify_ssl"]
|
|
135
|
+
|
|
136
|
+
# Create SSL context
|
|
137
|
+
ssl_context = ssl.create_default_context()
|
|
138
|
+
if not verify_ssl:
|
|
139
|
+
ssl_context.check_hostname = False
|
|
140
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
141
|
+
|
|
142
|
+
# Create cookie jar and opener
|
|
143
|
+
cookie_jar = http.cookiejar.CookieJar()
|
|
144
|
+
https_handler = urllib.request.HTTPSHandler(context=ssl_context)
|
|
145
|
+
cookie_handler = urllib.request.HTTPCookieProcessor(cookie_jar)
|
|
146
|
+
opener = urllib.request.build_opener(https_handler, cookie_handler)
|
|
147
|
+
|
|
148
|
+
# Step 1: Form-based login to /j_security_check
|
|
149
|
+
auth_data = urllib.parse.urlencode({
|
|
150
|
+
"j_username": username,
|
|
151
|
+
"j_password": password
|
|
152
|
+
}).encode("utf-8")
|
|
153
|
+
|
|
154
|
+
auth_request = urllib.request.Request(
|
|
155
|
+
f"{url}/j_security_check",
|
|
156
|
+
data=auth_data,
|
|
157
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
158
|
+
method="POST"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
opener.open(auth_request, timeout=timeout)
|
|
163
|
+
except urllib.error.HTTPError as e:
|
|
164
|
+
if e.code != 302: # 302 redirect is expected on successful login
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
# Extract JSESSIONID from cookies
|
|
168
|
+
jsessionid = None
|
|
169
|
+
for cookie in cookie_jar:
|
|
170
|
+
if cookie.name == "JSESSIONID":
|
|
171
|
+
jsessionid = cookie.value
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if jsessionid is None:
|
|
175
|
+
result = {"error": "No JSESSIONID cookie received - authentication may have failed"}
|
|
176
|
+
else:
|
|
177
|
+
# Step 2: Fetch XSRF token (required for SDWAN Manager 19.2+)
|
|
178
|
+
xsrf_token = None
|
|
179
|
+
try:
|
|
180
|
+
token_request = urllib.request.Request(
|
|
181
|
+
f"{url}/dataservice/client/token",
|
|
182
|
+
headers={"Cookie": f"JSESSIONID={jsessionid}"},
|
|
183
|
+
method="GET"
|
|
184
|
+
)
|
|
185
|
+
token_response = opener.open(token_request, timeout=xsrf_timeout)
|
|
186
|
+
if token_response.status == 200:
|
|
187
|
+
xsrf_token = token_response.read().decode("utf-8").strip()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass # Pre-19.2 versions do not support XSRF tokens
|
|
190
|
+
|
|
191
|
+
result = {"jsessionid": jsessionid, "xsrf_token": xsrf_token}
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
# Execute authentication in subprocess (fork-safe on macOS)
|
|
195
|
+
auth_result = execute_auth_subprocess(auth_params, auth_script_body)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"jsessionid": auth_result["jsessionid"],
|
|
199
|
+
"xsrf_token": auth_result.get("xsrf_token"),
|
|
200
|
+
}, SDWAN_MANAGER_SESSION_LIFETIME_SECONDS
|
|
149
201
|
|
|
150
202
|
@classmethod
|
|
151
203
|
def get_auth(cls) -> dict[str, Any]:
|
|
@@ -165,6 +217,8 @@ class SDWANManagerAuth:
|
|
|
165
217
|
SDWAN_URL: Base URL of the SDWAN Manager
|
|
166
218
|
SDWAN_USERNAME: SDWAN Manager username for authentication
|
|
167
219
|
SDWAN_PASSWORD: SDWAN Manager password for authentication
|
|
220
|
+
SDWAN_INSECURE: If "True", "1", or "yes" (default: "True"), SSL certificate
|
|
221
|
+
verification is disabled. Set to "False" to enable SSL verification.
|
|
168
222
|
|
|
169
223
|
Returns:
|
|
170
224
|
A dictionary containing:
|
|
@@ -175,11 +229,10 @@ class SDWANManagerAuth:
|
|
|
175
229
|
Raises:
|
|
176
230
|
ValueError: If any required environment variables (SDWAN_URL,
|
|
177
231
|
SDWAN_USERNAME, SDWAN_PASSWORD) are not set.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
connection timeouts, or other transport-level problems.
|
|
232
|
+
SubprocessAuthError: If authentication fails due to invalid credentials,
|
|
233
|
+
network issues, connection timeouts, or SDWAN Manager server errors.
|
|
234
|
+
The error message will contain details from the authentication
|
|
235
|
+
subprocess.
|
|
183
236
|
|
|
184
237
|
Example:
|
|
185
238
|
>>> # Set environment variables first
|
|
@@ -197,6 +250,11 @@ class SDWANManagerAuth:
|
|
|
197
250
|
url = os.environ.get("SDWAN_URL")
|
|
198
251
|
username = os.environ.get("SDWAN_USERNAME")
|
|
199
252
|
password = os.environ.get("SDWAN_PASSWORD")
|
|
253
|
+
insecure = os.environ.get("SDWAN_INSECURE", "True").lower() in (
|
|
254
|
+
"true",
|
|
255
|
+
"1",
|
|
256
|
+
"yes",
|
|
257
|
+
)
|
|
200
258
|
|
|
201
259
|
if not all([url, username, password]):
|
|
202
260
|
missing_vars: list[str] = []
|
|
@@ -213,9 +271,12 @@ class SDWANManagerAuth:
|
|
|
213
271
|
# Normalize URL by removing trailing slash
|
|
214
272
|
url = url.rstrip("/") # type: ignore[union-attr]
|
|
215
273
|
|
|
274
|
+
# SDWAN_INSECURE=True means verify_ssl=False
|
|
275
|
+
verify_ssl = not insecure
|
|
276
|
+
|
|
216
277
|
def auth_wrapper() -> tuple[dict[str, Any], int]:
|
|
217
278
|
"""Wrapper for authentication that captures closure variables."""
|
|
218
|
-
return cls._authenticate(url, username, password) # type: ignore[arg-type]
|
|
279
|
+
return cls._authenticate(url, username, password, verify_ssl) # type: ignore[arg-type]
|
|
219
280
|
|
|
220
281
|
# AuthCache.get_or_create returns dict[str, Any], but mypy can't verify this
|
|
221
282
|
# because nac_test lacks py.typed marker.
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
This module provides the SDWANDeviceResolver class, which extends
|
|
7
7
|
BaseDeviceResolver to implement SD-WAN schema navigation.
|
|
8
|
+
|
|
9
|
+
Device Fields Returned:
|
|
10
|
+
- hostname: System hostname from device_variables.system_hostname
|
|
11
|
+
- host: Management IP address (CIDR stripped)
|
|
12
|
+
- os: Always 'iosxe' (SD-WAN edges are IOS-XE based)
|
|
13
|
+
- platform: Always 'sdwan' (for PyATS abstraction optimization)
|
|
14
|
+
- device_id: Chassis ID
|
|
15
|
+
- type: Always 'router'
|
|
16
|
+
- username: From IOSXE_USERNAME environment variable
|
|
17
|
+
- password: From IOSXE_PASSWORD environment variable
|
|
8
18
|
"""
|
|
9
19
|
|
|
10
20
|
import logging
|
|
@@ -25,13 +35,20 @@ class SDWANDeviceResolver(BaseDeviceResolver):
|
|
|
25
35
|
|
|
26
36
|
Schema structure:
|
|
27
37
|
sdwan:
|
|
38
|
+
management_ip_variable: "vpn511_int1_if_ipv4_address" # Global default
|
|
28
39
|
sites:
|
|
29
40
|
- name: "site1"
|
|
30
41
|
routers:
|
|
31
42
|
- chassis_id: "abc123"
|
|
43
|
+
management_ip_variable: "custom_mgmt_ip" # Router override
|
|
32
44
|
device_variables:
|
|
33
45
|
system_hostname: "router1"
|
|
34
|
-
|
|
46
|
+
vpn511_int1_if_ipv4_address: "10.1.1.100/32"
|
|
47
|
+
custom_mgmt_ip: "10.2.2.200/32"
|
|
48
|
+
|
|
49
|
+
Management IP Resolution Priority:
|
|
50
|
+
1. Router-level management_ip_variable (highest priority)
|
|
51
|
+
2. Global sdwan-level management_ip_variable (fallback)
|
|
35
52
|
|
|
36
53
|
Credentials:
|
|
37
54
|
Uses IOSXE_USERNAME and IOSXE_PASSWORD environment variables
|
|
@@ -56,7 +73,7 @@ class SDWANDeviceResolver(BaseDeviceResolver):
|
|
|
56
73
|
"""Return 'sdwan' as the root key in the data model.
|
|
57
74
|
|
|
58
75
|
Returns:
|
|
59
|
-
Root key used when
|
|
76
|
+
Root key used when navigating the schema.
|
|
60
77
|
"""
|
|
61
78
|
return "sdwan"
|
|
62
79
|
|
|
@@ -126,6 +143,10 @@ class SDWANDeviceResolver(BaseDeviceResolver):
|
|
|
126
143
|
Uses management_ip_variable field to determine which variable
|
|
127
144
|
contains the management IP.
|
|
128
145
|
|
|
146
|
+
Resolution priority:
|
|
147
|
+
1. Router-level management_ip_variable (highest priority)
|
|
148
|
+
2. Global sdwan-level management_ip_variable (fallback)
|
|
149
|
+
|
|
129
150
|
Args:
|
|
130
151
|
device_data: Router data dictionary from the data model.
|
|
131
152
|
|
|
@@ -133,27 +154,28 @@ class SDWANDeviceResolver(BaseDeviceResolver):
|
|
|
133
154
|
IP address string without CIDR notation (e.g., "10.1.1.100").
|
|
134
155
|
|
|
135
156
|
Raises:
|
|
136
|
-
ValueError: If
|
|
157
|
+
ValueError: If management_ip_variable is not configured or
|
|
158
|
+
the referenced variable is not found in device_variables.
|
|
137
159
|
"""
|
|
138
160
|
device_vars = device_data.get("device_variables", {})
|
|
139
161
|
|
|
140
|
-
#
|
|
162
|
+
# Cascading lookup: router-level > global sdwan-level
|
|
141
163
|
ip_var = device_data.get("management_ip_variable")
|
|
164
|
+
if not ip_var:
|
|
165
|
+
ip_var = self.data_model.get("sdwan", {}).get("management_ip_variable")
|
|
166
|
+
|
|
167
|
+
if not ip_var:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"management_ip_variable not configured. "
|
|
170
|
+
"Set it at router level or sdwan level in sites.nac.yaml."
|
|
171
|
+
)
|
|
142
172
|
|
|
143
|
-
if ip_var
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
ip_value = str(device_vars[fallback_var])
|
|
150
|
-
break
|
|
151
|
-
else:
|
|
152
|
-
raise ValueError(
|
|
153
|
-
"Could not find management IP for device. "
|
|
154
|
-
"Set 'management_ip_variable' in test_inventory or use "
|
|
155
|
-
"standard variable names (mgmt_ip, management_ip, vpn0_ip)."
|
|
156
|
-
)
|
|
173
|
+
if ip_var not in device_vars:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"management_ip_variable '{ip_var}' not found in device_variables."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
ip_value = str(device_vars[ip_var])
|
|
157
179
|
|
|
158
180
|
# Strip CIDR notation if present
|
|
159
181
|
if "/" in ip_value:
|
|
@@ -161,16 +183,41 @@ class SDWANDeviceResolver(BaseDeviceResolver):
|
|
|
161
183
|
|
|
162
184
|
return ip_value
|
|
163
185
|
|
|
164
|
-
def
|
|
165
|
-
"""
|
|
186
|
+
def extract_os_platform_type(self, device_data: dict[str, Any]) -> dict[str, str]:
|
|
187
|
+
"""Return PyATS abstraction info for SD-WAN edge devices.
|
|
188
|
+
|
|
189
|
+
All SD-WAN edge devices are IOS-XE based with 'sdwan' platform.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
device_data: Router data dictionary (unused, values are hardcoded).
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dictionary with 'os' and 'platform' keys.
|
|
196
|
+
"""
|
|
197
|
+
return {
|
|
198
|
+
"os": "iosxe",
|
|
199
|
+
"platform": "sdwan",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def build_device_dict(self, device_data: dict[str, Any]) -> dict[str, Any]:
|
|
203
|
+
"""Build device dictionary with SD-WAN specific defaults.
|
|
204
|
+
|
|
205
|
+
Extends the base implementation to add type='router'.
|
|
206
|
+
Platform is set to 'sdwan' via extract_os_platform_type().
|
|
166
207
|
|
|
167
208
|
Args:
|
|
168
209
|
device_data: Router data dictionary from the data model.
|
|
169
210
|
|
|
170
211
|
Returns:
|
|
171
|
-
|
|
212
|
+
Device dictionary with hostname, host, os, platform, device_id, and type.
|
|
172
213
|
"""
|
|
173
|
-
|
|
214
|
+
# Get base device dict from parent
|
|
215
|
+
device_dict = super().build_device_dict(device_data)
|
|
216
|
+
|
|
217
|
+
# Add type - all SD-WAN edges are routers
|
|
218
|
+
device_dict["type"] = "router"
|
|
219
|
+
|
|
220
|
+
return device_dict
|
|
174
221
|
|
|
175
222
|
def get_credential_env_vars(self) -> tuple[str, str]:
|
|
176
223
|
"""Return IOS-XE credential env vars for SD-WAN edge devices.
|
|
@@ -48,6 +48,11 @@ class SDWANTestBase(SSHTestBase): # type: ignore[misc]
|
|
|
48
48
|
pass
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
+
# Class-level storage for the last resolver instance
|
|
52
|
+
# This allows nac-test to access skipped_devices after calling
|
|
53
|
+
# get_ssh_device_inventory()
|
|
54
|
+
_last_resolver: "SDWANDeviceResolver | None" = None
|
|
55
|
+
|
|
51
56
|
@classmethod
|
|
52
57
|
def get_ssh_device_inventory(
|
|
53
58
|
cls, data_model: dict[str, Any]
|
|
@@ -57,11 +62,13 @@ class SDWANTestBase(SSHTestBase): # type: ignore[misc]
|
|
|
57
62
|
This method is the entry point called by nac-test's orchestrator.
|
|
58
63
|
All device inventory resolution is delegated to SDWANDeviceResolver,
|
|
59
64
|
which handles:
|
|
60
|
-
- Test inventory loading (test_inventory.yaml)
|
|
61
65
|
- Schema navigation (sites[].routers[])
|
|
62
|
-
-
|
|
66
|
+
- Management IP resolution via management_ip_variable
|
|
63
67
|
- Credential injection (IOSXE_USERNAME, IOSXE_PASSWORD)
|
|
64
68
|
|
|
69
|
+
After calling this method, access cls._last_resolver.skipped_devices
|
|
70
|
+
to get information about devices that failed resolution.
|
|
71
|
+
|
|
65
72
|
Args:
|
|
66
73
|
data_model: The merged data model from nac-test containing all
|
|
67
74
|
sites.nac.yaml data with resolved variables.
|
|
@@ -80,13 +87,8 @@ class SDWANTestBase(SSHTestBase): # type: ignore[misc]
|
|
|
80
87
|
"""
|
|
81
88
|
logger.info("SDWANTestBase: Resolving device inventory via SDWANDeviceResolver")
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# 1. Test inventory loading via BaseDeviceResolver._load_inventory()
|
|
86
|
-
# 2. Schema navigation via navigate_to_devices()
|
|
87
|
-
# 3. Credential injection via _inject_credentials() using IOSXE_* env vars
|
|
88
|
-
resolver = SDWANDeviceResolver(data_model)
|
|
89
|
-
return resolver.get_resolved_inventory()
|
|
90
|
+
cls._last_resolver = SDWANDeviceResolver(data_model)
|
|
91
|
+
return cls._last_resolver.get_resolved_inventory()
|
|
90
92
|
|
|
91
93
|
def get_device_credentials(self, device: dict[str, Any]) -> dict[str, str | None]:
|
|
92
94
|
"""Get SD-WAN edge device SSH credentials from environment variables.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nac-test-pyats-common
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Architecture adapters for Network as Code (NaC) PyATS testing - auth classes, test base classes, and device resolvers
|
|
5
5
|
Project-URL: Homepage, https://github.com/netascode/nac-test-pyats-common
|
|
6
6
|
Project-URL: Documentation, https://github.com/netascode/nac-test-pyats-common
|
|
@@ -23,8 +23,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Testing
|
|
24
24
|
Classifier: Topic :: System :: Networking
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: filelock>=3.20.1
|
|
26
27
|
Requires-Dist: httpx>=0.28
|
|
27
|
-
Requires-Dist: nac-test==1.1.
|
|
28
|
+
Requires-Dist: nac-test==1.1.0b3
|
|
28
29
|
Provides-Extra: dev
|
|
29
30
|
Requires-Dist: bandit[toml]>=1.8.6; extra == 'dev'
|
|
30
31
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
nac_test_pyats_common/__init__.py,sha256=4fOaRhzkJJDyW00-wPzEzgWdmLXSLfxkiV-Z39RWmq8,1770
|
|
2
|
+
nac_test_pyats_common/py.typed,sha256=BrP39il8_cNN1K0KDeyBUtySS9oI2_SK5xKx1SxdSoY,59
|
|
3
|
+
nac_test_pyats_common/aci/__init__.py,sha256=Y9VhBZ3_GXLBk3_Wtg1SRG_Dxx134Etf9k-0wvrOMwQ,1335
|
|
4
|
+
nac_test_pyats_common/aci/auth.py,sha256=pkM7BRWJ1doajqZr73T4Gd3vPWydttk_QtOIEcf8tS4,11558
|
|
5
|
+
nac_test_pyats_common/aci/test_base.py,sha256=Kz_TpY3yeveAfSUMVGhSiQ3s7AHOzIJEz97rpy6pVWE,7575
|
|
6
|
+
nac_test_pyats_common/catc/__init__.py,sha256=ZGOQBvjFvq7h9LtfOyxFEmlir7htpy8sfWoJuDwGjsA,1986
|
|
7
|
+
nac_test_pyats_common/catc/api_test_base.py,sha256=F1zNKaYfHV_vRxJ30TcebDj26itNZ05lLHtUa4wslA0,8905
|
|
8
|
+
nac_test_pyats_common/catc/auth.py,sha256=u-kasgmoZCvZBKwrZUsoo9e1azxJUJg9odZkpXkUq7g,11623
|
|
9
|
+
nac_test_pyats_common/catc/device_resolver.py,sha256=I17QuPQpJj4p7zXzAjHJrHZcaICcHdAX9DV_0qgQni8,5754
|
|
10
|
+
nac_test_pyats_common/catc/ssh_test_base.py,sha256=wUldp2T9cmSEALBtbOgUQvQ2txuid3ErJEO27kM-kiI,4482
|
|
11
|
+
nac_test_pyats_common/common/__init__.py,sha256=Y0qrZEKx2g8Zm1CTHJpeLocwMasspP6NQuhyrRiA1wA,1084
|
|
12
|
+
nac_test_pyats_common/common/base_device_resolver.py,sha256=ul-zao-qcLZc1p_xvDCUAnvR5zqZM8mZda4iBP5S7LU,17625
|
|
13
|
+
nac_test_pyats_common/iosxe/__init__.py,sha256=GSuLGyxcu8dFouspTjXuQ1Mi27vDluVR8ZCcJrPcIiY,2147
|
|
14
|
+
nac_test_pyats_common/iosxe/iosxe_resolver.py,sha256=_2cug-eZfO5iKE3Po2kh9hJB7StRjs-Cm-BdXqKb5pg,2388
|
|
15
|
+
nac_test_pyats_common/iosxe/registry.py,sha256=djRFzxuEu8IdyTmi1puhIHGZGSMjys0l1v4-uzYCwAU,5723
|
|
16
|
+
nac_test_pyats_common/iosxe/test_base.py,sha256=NjXbf0ZY-CBWhd3jpIAhylL5kyRrHM6FlhGlFtjLSjM,4569
|
|
17
|
+
nac_test_pyats_common/sdwan/__init__.py,sha256=mrziVp5kMgykKnFPvWykuVLuky6xwEv6oEcfrbxFXxY,1777
|
|
18
|
+
nac_test_pyats_common/sdwan/api_test_base.py,sha256=dtPKd094gdQTNHaYeoPQxNle6uCULNII2sroAweZ5Jw,8834
|
|
19
|
+
nac_test_pyats_common/sdwan/auth.py,sha256=4qIi2ypMPK2Pkw6rfhSkxpoJGDRRvt4fJ5rO01p-4II,11790
|
|
20
|
+
nac_test_pyats_common/sdwan/device_resolver.py,sha256=5cTfcH6OUH98AiJCDo9uQL3GUcudGkE2V_nlysl6eHk,7811
|
|
21
|
+
nac_test_pyats_common/sdwan/ssh_test_base.py,sha256=rZF7txhu0FGtPtTzGMZFqB9hwClmtggroZwRuX3BGU4,4210
|
|
22
|
+
nac_test_pyats_common-0.2.1.dist-info/METADATA,sha256=cOKNshimaxKY2EHl51EWxhnhsOG8wteZGTRPqf6G6OY,13104
|
|
23
|
+
nac_test_pyats_common-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
24
|
+
nac_test_pyats_common-0.2.1.dist-info/licenses/LICENSE,sha256=zt2sx-c0iEk6-OO0iqRQ4l6fIGazRKW_qLMqfDpLm6M,16295
|
|
25
|
+
nac_test_pyats_common-0.2.1.dist-info/RECORD,,
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: MPL-2.0
|
|
2
|
-
# Copyright (c) 2025 Daniel Schmidt
|
|
3
|
-
|
|
4
|
-
"""Catalyst Center device resolver placeholder for D2D testing.
|
|
5
|
-
|
|
6
|
-
This is a placeholder for the Catalyst Center resolver that will be implemented
|
|
7
|
-
when Catalyst Center D2D testing support is added. This resolver will handle
|
|
8
|
-
IOS-XE devices managed by Catalyst Center.
|
|
9
|
-
|
|
10
|
-
Expected environment variables:
|
|
11
|
-
- CC_URL: Catalyst Center controller URL
|
|
12
|
-
- CC_USERNAME: Catalyst Center username
|
|
13
|
-
- CC_PASSWORD: Catalyst Center password
|
|
14
|
-
- IOSXE_USERNAME: SSH username for devices
|
|
15
|
-
- IOSXE_PASSWORD: SSH password for devices
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from typing import Any
|
|
20
|
-
|
|
21
|
-
from nac_test_pyats_common.common import BaseDeviceResolver
|
|
22
|
-
|
|
23
|
-
from .registry import register_iosxe_resolver
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@register_iosxe_resolver("CC")
|
|
27
|
-
class CatalystCenterDeviceResolver(BaseDeviceResolver):
|
|
28
|
-
"""Placeholder resolver for Catalyst Center D2D testing.
|
|
29
|
-
|
|
30
|
-
This resolver will be implemented when Catalyst Center D2D testing
|
|
31
|
-
support is added. Currently a placeholder to reserve the CC registry slot.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
def get_architecture_name(self) -> str:
|
|
35
|
-
"""Return architecture name."""
|
|
36
|
-
return "catalyst_center"
|
|
37
|
-
|
|
38
|
-
def get_schema_root_key(self) -> str:
|
|
39
|
-
"""Return data model root key."""
|
|
40
|
-
return "catalyst_center"
|
|
41
|
-
|
|
42
|
-
def navigate_to_devices(self) -> list[dict[str, Any]]:
|
|
43
|
-
"""Navigate to devices in data model."""
|
|
44
|
-
raise NotImplementedError(
|
|
45
|
-
"CatalystCenterDeviceResolver is not yet implemented. "
|
|
46
|
-
"This placeholder reserves the CC registry slot for future use."
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
def extract_device_id(self, device_data: dict[str, Any]) -> str:
|
|
50
|
-
"""Extract device ID."""
|
|
51
|
-
raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
|
|
52
|
-
|
|
53
|
-
def extract_hostname(self, device_data: dict[str, Any]) -> str:
|
|
54
|
-
"""Extract hostname."""
|
|
55
|
-
raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
|
|
56
|
-
|
|
57
|
-
def extract_host_ip(self, device_data: dict[str, Any]) -> str:
|
|
58
|
-
"""Extract management IP."""
|
|
59
|
-
raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
|
|
60
|
-
|
|
61
|
-
def extract_os_type(self, device_data: dict[str, Any]) -> str:
|
|
62
|
-
"""Extract OS type."""
|
|
63
|
-
raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
|
|
64
|
-
|
|
65
|
-
def get_credential_env_vars(self) -> tuple[str, str]:
|
|
66
|
-
"""Return credential environment variable names."""
|
|
67
|
-
return ("IOSXE_USERNAME", "IOSXE_PASSWORD")
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
nac_test_pyats_common/__init__.py,sha256=bQ3oB1eajVj8pmMnWCxxQEHUW-FkQX94j-6ocBq9OxA,1623
|
|
2
|
-
nac_test_pyats_common/py.typed,sha256=BrP39il8_cNN1K0KDeyBUtySS9oI2_SK5xKx1SxdSoY,59
|
|
3
|
-
nac_test_pyats_common/aci/__init__.py,sha256=Y9VhBZ3_GXLBk3_Wtg1SRG_Dxx134Etf9k-0wvrOMwQ,1335
|
|
4
|
-
nac_test_pyats_common/aci/auth.py,sha256=1Rk2uBGb_CGgkBh9dD3LagIsbJPKRgsSFDYEeyiC50U,7867
|
|
5
|
-
nac_test_pyats_common/aci/test_base.py,sha256=SoPh91AXPvMshJXIyjiZumSgxOAN4xPkpSb3t0B9kdE,6256
|
|
6
|
-
nac_test_pyats_common/catc/__init__.py,sha256=NKIHm6zOiiG8T_wcanPTh8621_RGcVJCMOeQLygTsy8,1319
|
|
7
|
-
nac_test_pyats_common/catc/auth.py,sha256=1oDZr_PEWOz1IZOUm_kyKzIFPQubMo_wemj-Q3D7ALU,9522
|
|
8
|
-
nac_test_pyats_common/catc/test_base.py,sha256=kGp6rZ4S3zP3MW5BD02K5uVxmtE-vRlG2A99kaLgyDo,7507
|
|
9
|
-
nac_test_pyats_common/common/__init__.py,sha256=Y0qrZEKx2g8Zm1CTHJpeLocwMasspP6NQuhyrRiA1wA,1084
|
|
10
|
-
nac_test_pyats_common/common/base_device_resolver.py,sha256=bPZP_dJjBfvaUGg_8F1JhfGAnWzAqI8oIC_JEejDJ4E,18558
|
|
11
|
-
nac_test_pyats_common/iosxe/__init__.py,sha256=GSuLGyxcu8dFouspTjXuQ1Mi27vDluVR8ZCcJrPcIiY,2147
|
|
12
|
-
nac_test_pyats_common/iosxe/catc_resolver.py,sha256=t14Pa6WjrjrbinXTtwc0Wa14Cctcae2yLLgdIpByzoU,2513
|
|
13
|
-
nac_test_pyats_common/iosxe/iosxe_resolver.py,sha256=XzzwRCa1cuQ6HmeUB1Z2vITOwYdhuDlugDD1eVXAoIE,2355
|
|
14
|
-
nac_test_pyats_common/iosxe/registry.py,sha256=djRFzxuEu8IdyTmi1puhIHGZGSMjys0l1v4-uzYCwAU,5723
|
|
15
|
-
nac_test_pyats_common/iosxe/test_base.py,sha256=s-x9hGJ4Yi6313gVamXnHWO7Jzr8tTKXzSOVJ6HcTxU,4306
|
|
16
|
-
nac_test_pyats_common/sdwan/__init__.py,sha256=mrziVp5kMgykKnFPvWykuVLuky6xwEv6oEcfrbxFXxY,1777
|
|
17
|
-
nac_test_pyats_common/sdwan/api_test_base.py,sha256=NuqrxODrRw7ZvEIuoc1Bup60thKqJOfiGz96T4QJ8Ng,7632
|
|
18
|
-
nac_test_pyats_common/sdwan/auth.py,sha256=CMgf_AYCalVQNjdUfDlE-0-z5tnhU_Wnscgt5ZW61RY,10322
|
|
19
|
-
nac_test_pyats_common/sdwan/device_resolver.py,sha256=3eKdIpoZsWHL8EzoKma1ZL6cFiMAQRZiMLp_xf9ZcEw,6062
|
|
20
|
-
nac_test_pyats_common/sdwan/ssh_test_base.py,sha256=8ddzOOyrxVF1uYiOOg2v8kXBI628c6hg-P-hiOC-v3Q,4173
|
|
21
|
-
nac_test_pyats_common-0.1.1.dist-info/METADATA,sha256=1vxKpKhdqZbR0N7wFJH_VYcpGB9MiJiumhUvzaP_4Hg,13072
|
|
22
|
-
nac_test_pyats_common-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
23
|
-
nac_test_pyats_common-0.1.1.dist-info/licenses/LICENSE,sha256=zt2sx-c0iEk6-OO0iqRQ4l6fIGazRKW_qLMqfDpLm6M,16295
|
|
24
|
-
nac_test_pyats_common-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
{nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|