nac-test-pyats-common 0.2.0__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.
@@ -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 httpx
23
- from nac_test.pyats_core.common.auth_cache import (
24
- AuthCache, # type: ignore[import-untyped]
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
- httpx.HTTPStatusError: If SDWAN Manager returns a non-2xx status code,
95
- typically indicating authentication failure (401) or server error.
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
- # NOTE: SSL verification is disabled (verify=False) to handle self-signed
109
- # certificates commonly used in lab and development deployments.
110
- with httpx.Client(verify=False, timeout=AUTH_REQUEST_TIMEOUT_SECONDS) as client:
111
- # Step 1: Form-based login to SDWAN Manager
112
- auth_response = client.post(
113
- f"{url}/j_security_check",
114
- data={"j_username": username, "j_password": password},
115
- headers={"Content-Type": "application/x-www-form-urlencoded"},
116
- follow_redirects=False,
117
- )
118
- auth_response.raise_for_status()
119
-
120
- # Validate JSESSIONID cookie was received
121
- if "JSESSIONID" not in auth_response.cookies:
122
- raise ValueError(
123
- "No JSESSIONID cookie received from SDWAN Manager. "
124
- "This may indicate invalid credentials or a server error. "
125
- f"Response status: {auth_response.status_code}"
126
- )
127
-
128
- jsessionid = auth_response.cookies["JSESSIONID"]
129
-
130
- # Step 2: Attempt to get XSRF token (19.2+ only)
131
- # Pre-19.2 versions do not require XSRF token, so failures are expected
132
- xsrf_token: str | None = None
133
- try:
134
- token_response = client.get(
135
- f"{url}/dataservice/client/token",
136
- cookies={"JSESSIONID": jsessionid},
137
- timeout=XSRF_TOKEN_FETCH_TIMEOUT_SECONDS,
138
- )
139
- if token_response.status_code == 200:
140
- xsrf_token = token_response.text.strip()
141
- except (httpx.HTTPError, httpx.TimeoutException):
142
- # Pre-19.2 does not support XSRF tokens, continue without
143
- pass
144
-
145
- return {
146
- "jsessionid": jsessionid,
147
- "xsrf_token": xsrf_token,
148
- }, SDWAN_MANAGER_SESSION_LIFETIME_SECONDS
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
- httpx.HTTPStatusError: If SDWAN Manager returns a non-2xx status code during
179
- authentication, typically indicating invalid credentials (401) or
180
- server issues (5xx).
181
- httpx.RequestError: If the request fails due to network issues,
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
@@ -173,28 +183,33 @@ class SDWANDeviceResolver(BaseDeviceResolver):
173
183
 
174
184
  return ip_value
175
185
 
176
- def extract_os_type(self, device_data: dict[str, Any]) -> str:
177
- """Return 'iosxe' as all SD-WAN edge devices are IOS-XE based.
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.
178
190
 
179
191
  Args:
180
- device_data: Router data dictionary (unused, OS is hardcoded).
192
+ device_data: Router data dictionary (unused, values are hardcoded).
181
193
 
182
194
  Returns:
183
- Always returns 'iosxe'.
195
+ Dictionary with 'os' and 'platform' keys.
184
196
  """
185
- return "iosxe"
197
+ return {
198
+ "os": "iosxe",
199
+ "platform": "sdwan",
200
+ }
186
201
 
187
202
  def build_device_dict(self, device_data: dict[str, Any]) -> dict[str, Any]:
188
203
  """Build device dictionary with SD-WAN specific defaults.
189
204
 
190
- Extends the base implementation to add type='router' since
191
- all SD-WAN edge devices are routers.
205
+ Extends the base implementation to add type='router'.
206
+ Platform is set to 'sdwan' via extract_os_platform_type().
192
207
 
193
208
  Args:
194
209
  device_data: Router data dictionary from the data model.
195
210
 
196
211
  Returns:
197
- Device dictionary with hostname, host, os, device_id, and type.
212
+ Device dictionary with hostname, host, os, platform, device_id, and type.
198
213
  """
199
214
  # Get base device dict from parent
200
215
  device_dict = super().build_device_dict(device_data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nac-test-pyats-common
3
- Version: 0.2.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.0b2
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,25 +0,0 @@
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=1Rk2uBGb_CGgkBh9dD3LagIsbJPKRgsSFDYEeyiC50U,7867
5
- nac_test_pyats_common/aci/test_base.py,sha256=SoPh91AXPvMshJXIyjiZumSgxOAN4xPkpSb3t0B9kdE,6256
6
- nac_test_pyats_common/catc/__init__.py,sha256=ZGOQBvjFvq7h9LtfOyxFEmlir7htpy8sfWoJuDwGjsA,1986
7
- nac_test_pyats_common/catc/api_test_base.py,sha256=kGp6rZ4S3zP3MW5BD02K5uVxmtE-vRlG2A99kaLgyDo,7507
8
- nac_test_pyats_common/catc/auth.py,sha256=1oDZr_PEWOz1IZOUm_kyKzIFPQubMo_wemj-Q3D7ALU,9522
9
- nac_test_pyats_common/catc/device_resolver.py,sha256=YgJJn1pXONXCAMnfXA-_Iuo6tkLtD9EekHbPbae6mRg,5363
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=xP4QF-kNp_8rFo9N6wzfJSIpNOgnPkeZmWAbH0k5tGo,15091
13
- nac_test_pyats_common/iosxe/__init__.py,sha256=GSuLGyxcu8dFouspTjXuQ1Mi27vDluVR8ZCcJrPcIiY,2147
14
- nac_test_pyats_common/iosxe/iosxe_resolver.py,sha256=XzzwRCa1cuQ6HmeUB1Z2vITOwYdhuDlugDD1eVXAoIE,2355
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=NuqrxODrRw7ZvEIuoc1Bup60thKqJOfiGz96T4QJ8Ng,7632
19
- nac_test_pyats_common/sdwan/auth.py,sha256=CMgf_AYCalVQNjdUfDlE-0-z5tnhU_Wnscgt5ZW61RY,10322
20
- nac_test_pyats_common/sdwan/device_resolver.py,sha256=1GdM5QKftj4F5bGmsbH90--VqoFZaDPMerHF1dAceMQ,7169
21
- nac_test_pyats_common/sdwan/ssh_test_base.py,sha256=rZF7txhu0FGtPtTzGMZFqB9hwClmtggroZwRuX3BGU4,4210
22
- nac_test_pyats_common-0.2.0.dist-info/METADATA,sha256=9Qbd0AYAGd33wI1iwbynSClOmApEC-Iny0geUrBoIzA,13072
23
- nac_test_pyats_common-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- nac_test_pyats_common-0.2.0.dist-info/licenses/LICENSE,sha256=zt2sx-c0iEk6-OO0iqRQ4l6fIGazRKW_qLMqfDpLm6M,16295
25
- nac_test_pyats_common-0.2.0.dist-info/RECORD,,