nac-test-pyats-common 0.1.0__py3-none-any.whl → 0.2.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.
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """Base device resolver for SSH/D2D testing.
2
5
 
3
6
  Provides the Template Method pattern for device inventory resolution.
@@ -5,14 +8,12 @@ Architecture-specific resolvers extend this class and implement the
5
8
  abstract methods for schema navigation and credential retrieval.
6
9
  """
7
10
 
11
+ import ipaddress
8
12
  import logging
9
13
  import os
10
14
  from abc import ABC, abstractmethod
11
15
  from typing import Any
12
16
 
13
- import yaml
14
- from nac_test.utils.file_discovery import find_data_file
15
-
16
17
  logger = logging.getLogger(__name__)
17
18
 
18
19
 
@@ -20,28 +21,27 @@ class BaseDeviceResolver(ABC):
20
21
  """Abstract base class for architecture-specific device resolvers.
21
22
 
22
23
  This class implements the Template Method pattern for device inventory
23
- resolution. It handles common logic (inventory loading, credential
24
- injection, device dict construction) while delegating schema-specific
25
- work to abstract methods.
24
+ resolution. It handles common logic (credential injection, device dict
25
+ construction) while delegating schema-specific work to abstract methods.
26
26
 
27
27
  Subclasses MUST implement:
28
28
  - get_architecture_name(): Return architecture identifier (e.g., "sdwan")
29
29
  - get_schema_root_key(): Return the root key in data model (e.g., "sdwan")
30
30
  - navigate_to_devices(): Navigate schema to get iterable of device data
31
- - extract_device_id(): Extract unique device identifier from device data
32
31
  - extract_hostname(): Extract hostname from device data
33
32
  - extract_host_ip(): Extract management IP from device data
34
33
  - extract_os_type(): Extract OS type from device data
35
34
  - get_credential_env_vars(): Return (username_env_var, password_env_var)
36
35
 
37
36
  Subclasses MAY override:
38
- - get_inventory_filename(): Return inventory filename (default: "test_inventory.yaml")
37
+ - extract_device_id(): Extract device identifier (default: uses hostname)
39
38
  - build_device_dict(): Customize device dict construction
40
- - _load_inventory(): Customize inventory loading
39
+ - validate_device_data(): Pre-extraction validation hook
41
40
 
42
41
  Attributes:
43
42
  data_model: The merged NAC data model dictionary.
44
- test_inventory: The test inventory dictionary (devices to test).
43
+ skipped_devices: List of dicts with device_id and reason for devices
44
+ that failed resolution. Populated after get_resolved_inventory().
45
45
 
46
46
  Example:
47
47
  >>> class SDWANDeviceResolver(BaseDeviceResolver):
@@ -57,33 +57,24 @@ class BaseDeviceResolver(ABC):
57
57
  >>> devices = resolver.get_resolved_inventory()
58
58
  """
59
59
 
60
- def __init__(
61
- self,
62
- data_model: dict[str, Any],
63
- test_inventory: dict[str, Any] | None = None,
64
- ) -> None:
60
+ def __init__(self, data_model: dict[str, Any]) -> None:
65
61
  """Initialize the device resolver.
66
62
 
67
63
  Args:
68
64
  data_model: The merged NAC data model containing all architecture
69
65
  data with resolved variables.
70
- test_inventory: Optional test inventory specifying which devices
71
- to test. If not provided, will attempt to load from file.
72
66
  """
73
67
  self.data_model = data_model
74
- self.test_inventory = test_inventory or self._load_inventory()
75
- logger.debug(
76
- f"Initialized {self.get_architecture_name()} resolver with "
77
- f"{len(self.test_inventory.get('devices', []))} devices in inventory"
78
- )
68
+ self.skipped_devices: list[dict[str, str]] = []
69
+ logger.debug(f"Initialized {self.get_architecture_name()} resolver")
79
70
 
80
71
  def get_resolved_inventory(self) -> list[dict[str, Any]]:
81
72
  """Get resolved device inventory ready for SSH connection.
82
73
 
83
74
  This is the main entry point. It:
84
75
  1. Navigates the data model to find device data
85
- 2. Matches devices against test inventory (if provided)
86
- 3. Extracts hostname, IP, OS from each device
76
+ 2. Extracts hostname and management IP from each device
77
+ 3. Sets OS type (architecture-specific, e.g., hardcoded to 'iosxe' for SD-WAN)
87
78
  4. Injects SSH credentials from environment variables
88
79
  5. Returns list of device dicts ready for nac-test
89
80
 
@@ -102,10 +93,15 @@ class BaseDeviceResolver(ABC):
102
93
  logger.info(f"Resolving device inventory for {self.get_architecture_name()}")
103
94
 
104
95
  resolved_devices: list[dict[str, Any]] = []
105
- devices_to_test = self._get_devices_to_test()
96
+ self.skipped_devices = [] # Reset for each resolution
97
+ all_devices = list(self.navigate_to_devices())
98
+ logger.debug(f"Found {len(all_devices)} devices in data model")
106
99
 
107
- for device_data in devices_to_test:
100
+ for device_data in all_devices:
108
101
  try:
102
+ # Validate device data before extraction (optional hook)
103
+ self.validate_device_data(device_data)
104
+
109
105
  device_dict = self.build_device_dict(device_data)
110
106
 
111
107
  # Validate extracted fields
@@ -125,18 +121,53 @@ class BaseDeviceResolver(ABC):
125
121
  )
126
122
  except (KeyError, ValueError) as e:
127
123
  device_id = self._safe_extract_device_id(device_data)
128
- logger.warning(f"Skipping device {device_id}: {e}")
124
+ logger.debug(f"Skipping device {device_id}: {e}")
125
+ self.skipped_devices.append(
126
+ {
127
+ "device_id": device_id,
128
+ "reason": str(e),
129
+ }
130
+ )
129
131
  continue
130
132
 
131
133
  # Inject credentials (fail fast if missing)
132
134
  self._inject_credentials(resolved_devices)
133
135
 
136
+ skipped_msg = (
137
+ f", skipped {len(self.skipped_devices)}" if self.skipped_devices else ""
138
+ )
134
139
  logger.info(
135
140
  f"Resolved {len(resolved_devices)} devices for "
136
- f"{self.get_architecture_name()} D2D testing"
141
+ f"{self.get_architecture_name()} D2D testing{skipped_msg}"
137
142
  )
138
143
  return resolved_devices
139
144
 
145
+ def validate_device_data(self, _device_data: dict[str, Any]) -> None: # noqa: B027
146
+ """Validate device data before extraction (optional hook).
147
+
148
+ Override this method to perform architecture-specific validation
149
+ before device field extraction. This is useful for filtering devices
150
+ based on state, type, or other criteria.
151
+
152
+ The default implementation does nothing - all devices pass validation.
153
+ Subclasses can override this to implement custom validation logic.
154
+
155
+ Args:
156
+ _device_data: Raw device data from the data model (unused in base class).
157
+
158
+ Raises:
159
+ ValueError: If the device should be skipped. The error message
160
+ will be logged and included in skipped_devices tracking.
161
+
162
+ Example (Catalyst Center - skip devices in INIT/PNP states):
163
+ >>> def validate_device_data(self, device_data):
164
+ ... state = device_data.get("state", "").upper()
165
+ ... if state in ("INIT", "PNP"):
166
+ ... raise ValueError(f"Device has unsupported state '{state}'")
167
+ """
168
+ # Default implementation does nothing - subclasses can override
169
+ pass
170
+
140
171
  def build_device_dict(self, device_data: dict[str, Any]) -> dict[str, Any]:
141
172
  """Build a device dictionary from raw device data.
142
173
 
@@ -147,8 +178,7 @@ class BaseDeviceResolver(ABC):
147
178
  device_data: Raw device data from the data model.
148
179
 
149
180
  Returns:
150
- Device dictionary with hostname, host, os, device_id fields,
151
- plus any test-relevant fields like connection_options.
181
+ Device dictionary with hostname, host, os, device_id fields.
152
182
  Credentials are injected separately.
153
183
 
154
184
  Raises:
@@ -164,146 +194,31 @@ class BaseDeviceResolver(ABC):
164
194
  raise ValueError(f"Invalid hostname: {hostname!r}")
165
195
  if not isinstance(host, str) or not host:
166
196
  raise ValueError(f"Invalid host IP: {host!r}")
197
+
198
+ # Validate IP address format (after CIDR stripping done by subclass)
199
+ try:
200
+ ipaddress.ip_address(host)
201
+ except ValueError:
202
+ raise ValueError(
203
+ f"Invalid IP address format: '{host}'. "
204
+ "Ensure the field contains a valid IPv4 or IPv6 address."
205
+ ) from None
167
206
  if not isinstance(os_type, str) or not os_type:
168
207
  raise ValueError(f"Invalid OS type: {os_type!r}")
169
208
  if not isinstance(device_id, str) or not device_id:
170
209
  raise ValueError(f"Invalid device ID: {device_id!r}")
171
210
 
172
- # Start with required fields
173
- result = {
211
+ return {
174
212
  "hostname": hostname,
175
213
  "host": host,
176
214
  "os": os_type,
177
215
  "device_id": device_id,
178
216
  }
179
217
 
180
- # Preserve connection_options from test_inventory if present
181
- # This allows specifying custom SSH ports, protocols, etc.
182
- if "connection_options" in device_data:
183
- result["connection_options"] = device_data["connection_options"]
184
-
185
- return result
186
-
187
218
  # -------------------------------------------------------------------------
188
219
  # Private helper methods
189
220
  # -------------------------------------------------------------------------
190
221
 
191
- def _load_inventory(self) -> dict[str, Any]:
192
- """Load test inventory from file.
193
-
194
- Searches for the inventory file using the generic file discovery
195
- utility. Subclasses can override this to customize loading behavior.
196
-
197
- Returns:
198
- Test inventory dictionary, or empty dict if not found.
199
- """
200
- filename = self.get_inventory_filename()
201
- inventory_path = find_data_file(filename)
202
-
203
- if inventory_path is None:
204
- logger.warning(
205
- f"Test inventory file '{filename}' not found for "
206
- f"{self.get_architecture_name()}. Using empty inventory."
207
- )
208
- return {}
209
-
210
- logger.info(f"Loading test inventory from {inventory_path}")
211
- try:
212
- with open(inventory_path) as f:
213
- raw_data = yaml.safe_load(f) or {}
214
-
215
- # Support both nested and flat formats:
216
- # Nested: {arch: {test_inventory: {...}}}
217
- # Flat: {test_inventory: {...}}
218
- arch_key = self.get_schema_root_key()
219
- if arch_key in raw_data and "test_inventory" in raw_data[arch_key]:
220
- return raw_data[arch_key]["test_inventory"] # type: ignore[no-any-return]
221
- elif "test_inventory" in raw_data:
222
- return raw_data["test_inventory"] # type: ignore[no-any-return]
223
- else:
224
- return raw_data
225
-
226
- except yaml.YAMLError as e:
227
- logger.error(f"Failed to parse test inventory YAML: {e}")
228
- return {}
229
- except OSError as e:
230
- logger.error(f"Failed to read test inventory file: {e}")
231
- return {}
232
-
233
- def _get_devices_to_test(self) -> list[dict[str, Any]]:
234
- """Get the list of device data dicts to process.
235
-
236
- If test_inventory specifies devices, filter to only those.
237
- Otherwise, return all devices from the data model.
238
-
239
- Returns:
240
- List of device data dictionaries from the data model.
241
- """
242
- all_devices = list(self.navigate_to_devices())
243
- logger.debug(f"Found {len(all_devices)} total devices in data model")
244
-
245
- # If no test inventory, test all devices
246
- inventory_devices = self.test_inventory.get("devices", [])
247
- if not inventory_devices:
248
- logger.debug("No test inventory devices specified, testing all devices")
249
- return all_devices
250
-
251
- # Build index for efficient lookup
252
- device_index = self._build_device_index(all_devices)
253
-
254
- # Filter to devices in test inventory
255
- devices_to_test: list[dict[str, Any]] = []
256
- for inventory_entry in inventory_devices:
257
- device_id = self._get_device_id_from_inventory(inventory_entry)
258
- if device_id in device_index:
259
- # Merge inventory entry data with device data
260
- merged = {**device_index[device_id], **inventory_entry}
261
- devices_to_test.append(merged)
262
- logger.debug(f"Added device {device_id} from test inventory")
263
- else:
264
- logger.warning(
265
- f"Device '{device_id}' from test_inventory not found in "
266
- f"{self.get_architecture_name()} data model"
267
- )
268
-
269
- logger.debug(f"Filtered to {len(devices_to_test)} devices from test inventory")
270
- return devices_to_test
271
-
272
- def _build_device_index(self, devices: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
273
- """Build a lookup index of devices by their ID.
274
-
275
- Args:
276
- devices: List of device data dictionaries.
277
-
278
- Returns:
279
- Dictionary mapping device ID to device data.
280
- """
281
- device_index: dict[str, dict[str, Any]] = {}
282
- for device_data in devices:
283
- device_id = self._safe_extract_device_id(device_data)
284
- if device_id:
285
- device_index[device_id] = device_data
286
- return device_index
287
-
288
- def _get_device_id_from_inventory(self, inventory_entry: dict[str, Any]) -> str:
289
- """Extract device ID from a test inventory entry.
290
-
291
- Override this if your inventory uses a different field name.
292
-
293
- Args:
294
- inventory_entry: Entry from test_inventory.devices[]
295
-
296
- Returns:
297
- Device identifier string.
298
- """
299
- # Common patterns across architectures
300
- for key in ["chassis_id", "device_id", "node_id", "hostname", "name"]:
301
- if key in inventory_entry:
302
- return str(inventory_entry[key])
303
-
304
- logger.warning(f"Could not extract device ID from inventory entry: {inventory_entry}")
305
- return ""
306
-
307
222
  def _safe_extract_device_id(self, device_data: dict[str, Any]) -> str:
308
223
  """Safely extract device ID, returning empty string on failure."""
309
224
  try:
@@ -333,7 +248,8 @@ class BaseDeviceResolver(ABC):
333
248
 
334
249
  if missing_vars:
335
250
  raise ValueError(
336
- f"Missing required credential environment variables: {', '.join(missing_vars)}. "
251
+ f"Missing required credential environment variables: "
252
+ f"{', '.join(missing_vars)}. "
337
253
  f"These are required for {self.get_architecture_name()} D2D testing."
338
254
  )
339
255
 
@@ -361,7 +277,7 @@ class BaseDeviceResolver(ABC):
361
277
  def get_schema_root_key(self) -> str:
362
278
  """Return the root key in the data model for this architecture.
363
279
 
364
- Used when loading test inventory and navigating the schema.
280
+ Used when navigating the schema.
365
281
 
366
282
  Returns:
367
283
  Root key (e.g., "sdwan", "apic", "cc").
@@ -387,11 +303,14 @@ class BaseDeviceResolver(ABC):
387
303
  """
388
304
  ...
389
305
 
390
- @abstractmethod
391
306
  def extract_device_id(self, device_data: dict[str, Any]) -> str:
392
307
  """Extract unique device identifier from device data.
393
308
 
394
- This ID is used to match test_inventory entries with data model devices.
309
+ Default implementation delegates to extract_hostname(), which is
310
+ appropriate when device_id and hostname are the same field.
311
+
312
+ Override this method when your architecture has a distinct device
313
+ identifier (e.g., SD-WAN uses chassis_id, not hostname).
395
314
 
396
315
  Args:
397
316
  device_data: Device data dict from navigate_to_devices().
@@ -399,11 +318,11 @@ class BaseDeviceResolver(ABC):
399
318
  Returns:
400
319
  Unique device identifier string.
401
320
 
402
- Example (SD-WAN):
321
+ Example (SD-WAN override):
403
322
  >>> def extract_device_id(self, device_data):
404
323
  ... return device_data["chassis_id"]
405
324
  """
406
- ...
325
+ return self.extract_hostname(device_data)
407
326
 
408
327
  @abstractmethod
409
328
  def extract_hostname(self, device_data: dict[str, Any]) -> str:
@@ -426,6 +345,8 @@ class BaseDeviceResolver(ABC):
426
345
  """Extract management IP address from device data.
427
346
 
428
347
  Should handle any IP formatting (e.g., strip CIDR notation).
348
+ The returned value will be validated by the base class to ensure
349
+ it is a valid IPv4 or IPv6 address.
429
350
 
430
351
  Args:
431
352
  device_data: Device data dict from navigate_to_devices().
@@ -435,7 +356,7 @@ class BaseDeviceResolver(ABC):
435
356
 
436
357
  Example (SD-WAN):
437
358
  >>> def extract_host_ip(self, device_data):
438
- ... ip_var = device_data.get("management_ip_variable", "mgmt_ip")
359
+ ... ip_var = device_data.get("management_ip_variable")
439
360
  ... ip = device_data["device_variables"].get(ip_var, "")
440
361
  ... return ip.split("/")[0] if "/" in ip else ip
441
362
  """
@@ -476,17 +397,3 @@ class BaseDeviceResolver(ABC):
476
397
  ... return ("NXOS_SSH_USERNAME", "NXOS_SSH_PASSWORD")
477
398
  """
478
399
  ...
479
-
480
- # -------------------------------------------------------------------------
481
- # Optional overrides
482
- # -------------------------------------------------------------------------
483
-
484
- def get_inventory_filename(self) -> str:
485
- """Return the test inventory filename.
486
-
487
- Override to use a different filename.
488
-
489
- Returns:
490
- Filename (default: "test_inventory.yaml").
491
- """
492
- return "test_inventory.yaml"
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """IOS-XE adapter module for NAC PyATS testing.
2
5
 
3
6
  This module provides a generic IOS-XE test base class and resolver registry
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """IOSXE device resolver placeholder for direct IOS-XE device access.
2
5
 
3
6
  This is a placeholder for the IOSXE resolver that will be implemented
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """Registry for IOS-XE device resolvers.
2
5
 
3
6
  This module implements a plugin architecture for registering and retrieving
@@ -79,20 +82,24 @@ def register_iosxe_resolver(controller_type: str) -> Callable[[type[T]], type[T]
79
82
  """
80
83
  # Validate the class extends BaseDeviceResolver
81
84
  if not issubclass(cls, BaseDeviceResolver):
82
- raise TypeError(f"Resolver class {cls.__name__} must extend BaseDeviceResolver")
85
+ raise TypeError(
86
+ f"Resolver class {cls.__name__} must extend BaseDeviceResolver"
87
+ )
83
88
 
84
89
  # Check for duplicate registration
85
90
  if controller_type in _IOSXE_RESOLVER_REGISTRY:
86
91
  existing_class = _IOSXE_RESOLVER_REGISTRY[controller_type]
87
92
  raise ValueError(
88
- f"A resolver is already registered for controller type '{controller_type}': "
93
+ f"A resolver is already registered for controller type "
94
+ f"'{controller_type}': "
89
95
  f"{existing_class.__module__}.{existing_class.__name__}"
90
96
  )
91
97
 
92
98
  # Register the resolver
93
99
  _IOSXE_RESOLVER_REGISTRY[controller_type] = cls
94
100
  logger.debug(
95
- f"Registered IOS-XE resolver {cls.__name__} for controller type '{controller_type}'"
101
+ f"Registered IOS-XE resolver {cls.__name__} for controller type "
102
+ f"'{controller_type}'"
96
103
  )
97
104
 
98
105
  return cls
@@ -100,7 +107,9 @@ def register_iosxe_resolver(controller_type: str) -> Callable[[type[T]], type[T]
100
107
  return decorator
101
108
 
102
109
 
103
- def get_resolver_for_controller(controller_type: str) -> type[BaseDeviceResolver] | None:
110
+ def get_resolver_for_controller(
111
+ controller_type: str,
112
+ ) -> type[BaseDeviceResolver] | None:
104
113
  """Get the device resolver class for a specific controller type.
105
114
 
106
115
  Retrieves the registered resolver class for the specified controller type.
@@ -124,7 +133,8 @@ def get_resolver_for_controller(controller_type: str) -> type[BaseDeviceResolver
124
133
 
125
134
  if resolver_class:
126
135
  logger.debug(
127
- f"Found resolver {resolver_class.__name__} for controller type '{controller_type}'"
136
+ f"Found resolver {resolver_class.__name__} for controller type "
137
+ f"'{controller_type}'"
128
138
  )
129
139
  else:
130
140
  logger.debug(
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """IOS-XE test base class for SSH/D2D testing."""
2
5
 
3
6
  import os
@@ -18,8 +21,15 @@ class IOSXETestBase(SSHTestBase): # type: ignore[misc]
18
21
  - IOS-XE (direct device access via IOSXE_URL)
19
22
  """
20
23
 
24
+ # Class-level storage for the last resolver instance
25
+ # This allows nac-test to access skipped_devices after calling
26
+ # get_ssh_device_inventory()
27
+ _last_resolver: Any = None
28
+
21
29
  @classmethod
22
- def get_ssh_device_inventory(cls, data_model: dict[str, Any]) -> list[dict[str, Any]]:
30
+ def get_ssh_device_inventory(
31
+ cls, data_model: dict[str, Any]
32
+ ) -> list[dict[str, Any]]:
23
33
  """Get the SSH device inventory for IOS-XE devices.
24
34
 
25
35
  Main entry point that detects the architecture and returns
@@ -58,6 +68,7 @@ class IOSXETestBase(SSHTestBase): # type: ignore[misc]
58
68
  f"No device resolver registered for controller type '{controller_type}'"
59
69
  )
60
70
  resolver = resolver_class(data_model)
71
+ cls._last_resolver = resolver # Store for skipped_devices access
61
72
 
62
73
  # Inline validation: Check data model has expected root key
63
74
  expected_keys = {
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """SD-WAN (SDWAN Manager) adapter module for NAC PyATS testing.
2
5
 
3
6
  This module provides SD-WAN-specific authentication, test base classes, and device
@@ -5,7 +8,8 @@ resolver implementations for use with the nac-test framework. It includes suppor
5
8
  for both SDWAN Manager API testing and SSH-based device-to-device (D2D) testing.
6
9
 
7
10
  Classes:
8
- SDWANManagerAuth: SDWAN Manager authentication with JSESSIONID and XSRF token management.
11
+ SDWANManagerAuth: SDWAN Manager authentication with JSESSIONID and XSRF token
12
+ management.
9
13
  SDWANManagerTestBase: Base class for SDWAN Manager API tests with tracking.
10
14
  SDWANTestBase: Base class for SD-WAN SSH/D2D tests with device inventory.
11
15
  SDWANDeviceResolver: Resolves device information from the SD-WAN data model.
@@ -1,9 +1,13 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """SDWAN Manager-specific base test class for SD-WAN API testing.
2
5
 
3
- This module provides the SDWANManagerTestBase class, which extends the generic NACTestBase
4
- to add SDWAN Manager-specific functionality for testing SD-WAN controllers. It handles
5
- session management (JSESSIONID and XSRF token), client configuration, and provides
6
- a standardized interface for running asynchronous verification tests against SDWAN Manager.
6
+ This module provides the SDWANManagerTestBase class, which extends the generic
7
+ NACTestBase to add SDWAN Manager-specific functionality for testing SD-WAN
8
+ controllers. It handles session management (JSESSIONID and XSRF token), client
9
+ configuration, and provides a standardized interface for running asynchronous
10
+ verification tests against SDWAN Manager.
7
11
 
8
12
  The class integrates with PyATS/Genie test frameworks and provides automatic
9
13
  API call tracking for enhanced HTML reporting.
@@ -13,7 +17,9 @@ import asyncio
13
17
  from typing import Any
14
18
 
15
19
  import httpx
16
- from nac_test.pyats_core.common.base_test import NACTestBase # type: ignore[import-untyped]
20
+ from nac_test.pyats_core.common.base_test import (
21
+ NACTestBase, # type: ignore[import-untyped]
22
+ )
17
23
  from pyats import aetest # type: ignore[import-untyped]
18
24
 
19
25
  from .auth import SDWANManagerAuth
@@ -43,7 +49,8 @@ class SDWANManagerTestBase(NACTestBase): # type: ignore[misc]
43
49
 
44
50
  Methods:
45
51
  setup(): Initialize SDWAN Manager authentication and client.
46
- get_sdwan_manager_client(): Create and configure an SDWAN Manager-specific HTTP client.
52
+ get_sdwan_manager_client(): Create and configure an SDWAN Manager-specific
53
+ HTTP client.
47
54
  run_async_verification_test(): Execute async verification tests with PyATS.
48
55
 
49
56
  Example:
@@ -68,7 +75,8 @@ class SDWANManagerTestBase(NACTestBase): # type: ignore[misc]
68
75
 
69
76
  Initializes the SDWAN Manager test environment by:
70
77
  1. Calling the parent class setup method
71
- 2. Obtaining SDWAN Manager session data (jsessionid, xsrf_token) using cached auth
78
+ 2. Obtaining SDWAN Manager session data (jsessionid, xsrf_token) using
79
+ cached auth
72
80
  3. Creating and storing an SDWAN Manager client for use in verification methods
73
81
 
74
82
  The session data is obtained through the SDWANManagerAuth utility which
@@ -84,12 +92,14 @@ class SDWANManagerTestBase(NACTestBase): # type: ignore[misc]
84
92
  self.client = self.get_sdwan_manager_client()
85
93
 
86
94
  def get_sdwan_manager_client(self) -> httpx.AsyncClient:
87
- """Get an httpx async client configured for SDWAN Manager with response tracking.
95
+ """Get an httpx async client configured for SDWAN Manager.
96
+
97
+ Configured with response tracking.
88
98
 
89
- Creates an HTTP client specifically configured for SDWAN Manager API communication
90
- with session headers, base URL, and automatic response tracking for HTML
91
- report generation. The client is wrapped to capture all API interactions
92
- for detailed test reporting.
99
+ Creates an HTTP client specifically configured for SDWAN Manager API
100
+ communication with session headers, base URL, and automatic response
101
+ tracking for HTML report generation. The client is wrapped to capture all
102
+ API interactions for detailed test reporting.
93
103
 
94
104
  The client includes:
95
105
  - JSESSIONID cookie in all requests (via Cookie header)
@@ -98,9 +108,9 @@ class SDWANManagerTestBase(NACTestBase): # type: ignore[misc]
98
108
  - Automatic API call tracking for reporting
99
109
 
100
110
  Returns:
101
- httpx.AsyncClient: Configured client with SDWAN Manager session data, base URL,
102
- and wrapped for automatic API call tracking. The client has SSL
103
- verification disabled for lab environment compatibility.
111
+ httpx.AsyncClient: Configured client with SDWAN Manager session data,
112
+ base URL, and wrapped for automatic API call tracking. The client
113
+ has SSL verification disabled for lab environment compatibility.
104
114
 
105
115
  Note:
106
116
  SSL verification is disabled (verify=False) to support lab environments