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.
@@ -0,0 +1,58 @@
1
+ """IOS-XE adapter module for NAC PyATS testing.
2
+
3
+ This module provides a generic IOS-XE test base class and resolver registry
4
+ for architecture-specific device resolvers. It supports SSH-based device-to-device
5
+ (D2D) testing across multiple architectures that manage IOS-XE devices (SD-WAN,
6
+ Meraki, Catalyst Center, etc.).
7
+
8
+ The module implements a plugin architecture where each controller type can register
9
+ its own device resolver while sharing common IOS-XE SSH testing capabilities.
10
+
11
+ Classes:
12
+ IOSXETestBase: Base class for IOS-XE SSH/D2D tests with common functionality.
13
+ Provides device iteration, command execution, and result processing.
14
+
15
+ Registry Functions:
16
+ register_iosxe_resolver: Decorator for registering architecture-specific resolvers.
17
+ get_resolver_for_controller: Get the appropriate resolver for a controller type.
18
+ get_supported_controllers: List all registered controller types.
19
+
20
+ Example:
21
+ For registering a new resolver:
22
+
23
+ >>> from nac_test_pyats_common.iosxe import register_iosxe_resolver
24
+ >>> from nac_test_pyats_common.common import BaseDeviceResolver
25
+ >>>
26
+ >>> @register_iosxe_resolver("SDWAN")
27
+ ... class SDWANDeviceResolver(BaseDeviceResolver):
28
+ ... def get_architecture_name(self) -> str:
29
+ ... return "sdwan"
30
+ ... # ... implement other abstract methods
31
+
32
+ For using in tests:
33
+
34
+ >>> from nac_test_pyats_common.iosxe import IOSXETestBase
35
+ >>>
36
+ >>> class VerifyInterfaceStatus(IOSXETestBase):
37
+ ... @aetest.test
38
+ ... def verify_interfaces(self, steps, device):
39
+ ... # SSH-based verification on IOS-XE device
40
+ ... output = device.execute("show ip interface brief")
41
+ ... # ... process output
42
+ """
43
+
44
+ from .registry import (
45
+ get_resolver_for_controller,
46
+ get_supported_controllers,
47
+ register_iosxe_resolver,
48
+ )
49
+ from .test_base import IOSXETestBase
50
+
51
+ __all__ = [
52
+ # Test base class
53
+ "IOSXETestBase",
54
+ # Registry functions
55
+ "register_iosxe_resolver",
56
+ "get_resolver_for_controller",
57
+ "get_supported_controllers",
58
+ ]
@@ -0,0 +1,64 @@
1
+ """Catalyst Center device resolver placeholder for D2D testing.
2
+
3
+ This is a placeholder for the Catalyst Center resolver that will be implemented
4
+ when Catalyst Center D2D testing support is added. This resolver will handle
5
+ IOS-XE devices managed by Catalyst Center.
6
+
7
+ Expected environment variables:
8
+ - CC_URL: Catalyst Center controller URL
9
+ - CC_USERNAME: Catalyst Center username
10
+ - CC_PASSWORD: Catalyst Center password
11
+ - IOSXE_USERNAME: SSH username for devices
12
+ - IOSXE_PASSWORD: SSH password for devices
13
+
14
+ """
15
+
16
+ from typing import Any
17
+
18
+ from nac_test_pyats_common.common import BaseDeviceResolver
19
+
20
+ from .registry import register_iosxe_resolver
21
+
22
+
23
+ @register_iosxe_resolver("CC")
24
+ class CatalystCenterDeviceResolver(BaseDeviceResolver):
25
+ """Placeholder resolver for Catalyst Center D2D testing.
26
+
27
+ This resolver will be implemented when Catalyst Center D2D testing
28
+ support is added. Currently a placeholder to reserve the CC registry slot.
29
+ """
30
+
31
+ def get_architecture_name(self) -> str:
32
+ """Return architecture name."""
33
+ return "catalyst_center"
34
+
35
+ def get_schema_root_key(self) -> str:
36
+ """Return data model root key."""
37
+ return "catalyst_center"
38
+
39
+ def navigate_to_devices(self) -> list[dict[str, Any]]:
40
+ """Navigate to devices in data model."""
41
+ raise NotImplementedError(
42
+ "CatalystCenterDeviceResolver is not yet implemented. "
43
+ "This placeholder reserves the CC registry slot for future use."
44
+ )
45
+
46
+ def extract_device_id(self, device_data: dict[str, Any]) -> str:
47
+ """Extract device ID."""
48
+ raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
49
+
50
+ def extract_hostname(self, device_data: dict[str, Any]) -> str:
51
+ """Extract hostname."""
52
+ raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
53
+
54
+ def extract_host_ip(self, device_data: dict[str, Any]) -> str:
55
+ """Extract management IP."""
56
+ raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
57
+
58
+ def extract_os_type(self, device_data: dict[str, Any]) -> str:
59
+ """Extract OS type."""
60
+ raise NotImplementedError("CatalystCenterDeviceResolver is not yet implemented")
61
+
62
+ def get_credential_env_vars(self) -> tuple[str, str]:
63
+ """Return credential environment variable names."""
64
+ return ("IOSXE_USERNAME", "IOSXE_PASSWORD")
@@ -0,0 +1,62 @@
1
+ """IOSXE device resolver placeholder for direct IOS-XE device access.
2
+
3
+ This is a placeholder for the IOSXE resolver that will be implemented
4
+ when direct IOS-XE device support is added. This resolver will handle
5
+ devices accessed directly via IOSXE_URL without a centralized controller.
6
+
7
+ Expected environment variables:
8
+ - IOSXE_URL: URL for direct device access
9
+ - IOSXE_USERNAME: SSH username for devices
10
+ - IOSXE_PASSWORD: SSH password for devices
11
+ """
12
+
13
+ from typing import Any
14
+
15
+ from nac_test_pyats_common.common import BaseDeviceResolver
16
+
17
+ from .registry import register_iosxe_resolver
18
+
19
+
20
+ @register_iosxe_resolver("IOSXE")
21
+ class IOSXEResolver(BaseDeviceResolver):
22
+ """Placeholder resolver for direct IOS-XE device access.
23
+
24
+ This resolver will be implemented when IOSXE_URL-based direct device
25
+ access is supported. Currently a placeholder to reserve the IOSXE
26
+ registry slot.
27
+ """
28
+
29
+ def get_architecture_name(self) -> str:
30
+ """Return architecture name."""
31
+ return "iosxe"
32
+
33
+ def get_schema_root_key(self) -> str:
34
+ """Return data model root key."""
35
+ return "devices"
36
+
37
+ def navigate_to_devices(self) -> list[dict[str, Any]]:
38
+ """Navigate to devices in data model."""
39
+ raise NotImplementedError(
40
+ "IOSXEResolver is not yet implemented. "
41
+ "This placeholder reserves the IOSXE registry slot for future use."
42
+ )
43
+
44
+ def extract_device_id(self, device_data: dict[str, Any]) -> str:
45
+ """Extract device ID."""
46
+ raise NotImplementedError("IOSXEResolver is not yet implemented")
47
+
48
+ def extract_hostname(self, device_data: dict[str, Any]) -> str:
49
+ """Extract hostname."""
50
+ raise NotImplementedError("IOSXEResolver is not yet implemented")
51
+
52
+ def extract_host_ip(self, device_data: dict[str, Any]) -> str:
53
+ """Extract management IP."""
54
+ raise NotImplementedError("IOSXEResolver is not yet implemented")
55
+
56
+ def extract_os_type(self, device_data: dict[str, Any]) -> str:
57
+ """Extract OS type."""
58
+ raise NotImplementedError("IOSXEResolver is not yet implemented")
59
+
60
+ def get_credential_env_vars(self) -> tuple[str, str]:
61
+ """Return credential environment variable names."""
62
+ return ("IOSXE_USERNAME", "IOSXE_PASSWORD")
@@ -0,0 +1,153 @@
1
+ """Registry for IOS-XE device resolvers.
2
+
3
+ This module implements a plugin architecture for registering and retrieving
4
+ device resolvers for different controller types that manage IOS-XE devices.
5
+ Each architecture (SD-WAN, Meraki, Catalyst Center, etc.) can register its
6
+ own resolver while sharing common IOS-XE functionality.
7
+
8
+ The registry pattern allows for:
9
+ - Decoupled architecture implementations
10
+ - Runtime discovery of available resolvers
11
+ - Easy extension with new controller types
12
+ - Type-safe resolver registration and retrieval
13
+
14
+ Global Variables:
15
+ _IOSXE_RESOLVER_REGISTRY: Dictionary mapping controller types to resolver classes.
16
+
17
+ Functions:
18
+ register_iosxe_resolver: Decorator for registering a resolver class.
19
+ get_resolver_for_controller: Retrieve a resolver for a specific controller type.
20
+ get_supported_controllers: List all registered controller types.
21
+ """
22
+
23
+ import logging
24
+ from collections.abc import Callable
25
+ from typing import TypeVar
26
+
27
+ from nac_test_pyats_common.common import BaseDeviceResolver
28
+
29
+ # Type variable for the resolver class type
30
+ T = TypeVar("T", bound=BaseDeviceResolver)
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Global registry for IOS-XE device resolvers
35
+ _IOSXE_RESOLVER_REGISTRY: dict[str, type[BaseDeviceResolver]] = {}
36
+
37
+
38
+ def register_iosxe_resolver(controller_type: str) -> Callable[[type[T]], type[T]]:
39
+ """Decorator to register an IOS-XE device resolver for a controller type.
40
+
41
+ This decorator registers a device resolver class with the global registry,
42
+ associating it with a specific controller type. The resolver must extend
43
+ BaseDeviceResolver and implement all required abstract methods.
44
+
45
+ Args:
46
+ controller_type: The controller type identifier (e.g., "SDWAN", "MERAKI",
47
+ "CATALYST_CENTER"). Should be uppercase by convention.
48
+
49
+ Returns:
50
+ A decorator function that registers the class and returns it unchanged.
51
+
52
+ Raises:
53
+ ValueError: If a resolver is already registered for the controller type.
54
+ TypeError: If the class doesn't extend BaseDeviceResolver.
55
+
56
+ Example:
57
+ >>> @register_iosxe_resolver("SDWAN")
58
+ ... class SDWANDeviceResolver(BaseDeviceResolver):
59
+ ... def get_architecture_name(self) -> str:
60
+ ... return "sdwan"
61
+ ... # ... implement other abstract methods
62
+ >>>
63
+ >>> # The resolver is now registered and can be retrieved
64
+ >>> resolver_class = get_resolver_for_controller("SDWAN")
65
+ """
66
+
67
+ def decorator(cls: type[T]) -> type[T]:
68
+ """Register the resolver class and return it unchanged.
69
+
70
+ Args:
71
+ cls: The resolver class to register.
72
+
73
+ Returns:
74
+ The same class, unchanged.
75
+
76
+ Raises:
77
+ ValueError: If a resolver is already registered for the controller type.
78
+ TypeError: If the class doesn't extend BaseDeviceResolver.
79
+ """
80
+ # Validate the class extends BaseDeviceResolver
81
+ if not issubclass(cls, BaseDeviceResolver):
82
+ raise TypeError(f"Resolver class {cls.__name__} must extend BaseDeviceResolver")
83
+
84
+ # Check for duplicate registration
85
+ if controller_type in _IOSXE_RESOLVER_REGISTRY:
86
+ existing_class = _IOSXE_RESOLVER_REGISTRY[controller_type]
87
+ raise ValueError(
88
+ f"A resolver is already registered for controller type '{controller_type}': "
89
+ f"{existing_class.__module__}.{existing_class.__name__}"
90
+ )
91
+
92
+ # Register the resolver
93
+ _IOSXE_RESOLVER_REGISTRY[controller_type] = cls
94
+ logger.debug(
95
+ f"Registered IOS-XE resolver {cls.__name__} for controller type '{controller_type}'"
96
+ )
97
+
98
+ return cls
99
+
100
+ return decorator
101
+
102
+
103
+ def get_resolver_for_controller(controller_type: str) -> type[BaseDeviceResolver] | None:
104
+ """Get the device resolver class for a specific controller type.
105
+
106
+ Retrieves the registered resolver class for the specified controller type.
107
+ Returns None if no resolver is registered for that type.
108
+
109
+ Args:
110
+ controller_type: The controller type identifier (e.g., "SDWAN", "MERAKI").
111
+
112
+ Returns:
113
+ The resolver class if registered, None otherwise.
114
+
115
+ Example:
116
+ >>> resolver_class = get_resolver_for_controller("SDWAN")
117
+ >>> if resolver_class:
118
+ ... resolver = resolver_class(data_model)
119
+ ... devices = resolver.get_resolved_inventory()
120
+ ... else:
121
+ ... print("No resolver found for SDWAN")
122
+ """
123
+ resolver_class = _IOSXE_RESOLVER_REGISTRY.get(controller_type)
124
+
125
+ if resolver_class:
126
+ logger.debug(
127
+ f"Found resolver {resolver_class.__name__} for controller type '{controller_type}'"
128
+ )
129
+ else:
130
+ logger.debug(
131
+ f"No resolver registered for controller type '{controller_type}'. "
132
+ f"Available types: {', '.join(get_supported_controllers())}"
133
+ )
134
+
135
+ return resolver_class
136
+
137
+
138
+ def get_supported_controllers() -> list[str]:
139
+ """Get a list of all registered controller types.
140
+
141
+ Returns a sorted list of controller type identifiers for which
142
+ resolvers have been registered. This is useful for validation,
143
+ documentation, and user feedback.
144
+
145
+ Returns:
146
+ Sorted list of registered controller type strings.
147
+
148
+ Example:
149
+ >>> controllers = get_supported_controllers()
150
+ >>> print(f"Supported controllers: {', '.join(controllers)}")
151
+ Supported controllers: CATALYST_CENTER, MERAKI, SDWAN
152
+ """
153
+ return sorted(_IOSXE_RESOLVER_REGISTRY.keys())
@@ -0,0 +1,116 @@
1
+ """IOS-XE test base class for SSH/D2D testing."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ from nac_test.pyats_core.common.ssh_base_test import SSHTestBase
7
+ from nac_test.utils.controller import detect_controller_type
8
+
9
+ from .registry import get_resolver_for_controller
10
+
11
+
12
+ class IOSXETestBase(SSHTestBase): # type: ignore[misc]
13
+ """Base class for IOS-XE device testing via SSH.
14
+
15
+ Provides device inventory resolution for multiple architectures:
16
+ - SD-WAN (via vManage)
17
+ - Catalyst Center
18
+ - IOS-XE (direct device access via IOSXE_URL)
19
+ """
20
+
21
+ @classmethod
22
+ def get_ssh_device_inventory(cls, data_model: dict[str, Any]) -> list[dict[str, Any]]:
23
+ """Get the SSH device inventory for IOS-XE devices.
24
+
25
+ Main entry point that detects the architecture and returns
26
+ the resolved device inventory. Performs inline validation
27
+ of controller type and data model structure.
28
+
29
+ Args:
30
+ data_model: The merged data model from nac-test containing all
31
+ configuration data with resolved variables.
32
+
33
+ Returns:
34
+ List of device dictionaries with connection details.
35
+
36
+ Raises:
37
+ ValueError: If controller type is unsupported or data validation fails.
38
+ """
39
+ # Try to detect controller type from environment
40
+ controller_type = detect_controller_type()
41
+
42
+ # If no controller detected, infer from data model
43
+ if controller_type == "UNKNOWN":
44
+ controller_type = cls._infer_architecture_from_data_model(data_model)
45
+
46
+ # Inline validation: Check if controller supports IOS-XE
47
+ supported_controllers = {"SDWAN", "CC", "IOSXE"}
48
+ if controller_type not in supported_controllers:
49
+ raise ValueError(
50
+ f"Controller type '{controller_type}' does not support IOS-XE devices. "
51
+ f"Supported types: {', '.join(sorted(supported_controllers))}"
52
+ )
53
+
54
+ # Get resolver from registry
55
+ resolver_class = get_resolver_for_controller(controller_type)
56
+ if resolver_class is None:
57
+ raise ValueError(
58
+ f"No device resolver registered for controller type '{controller_type}'"
59
+ )
60
+ resolver = resolver_class(data_model)
61
+
62
+ # Inline validation: Check data model has expected root key
63
+ expected_keys = {
64
+ "SDWAN": "sdwan",
65
+ "CC": "catalyst_center",
66
+ "IOSXE": "devices",
67
+ }
68
+ expected_key = expected_keys.get(controller_type)
69
+ if expected_key and expected_key not in data_model:
70
+ raise ValueError(
71
+ f"Data model missing expected root key '{expected_key}' "
72
+ f"for {controller_type} architecture"
73
+ )
74
+
75
+ # Return resolved inventory
76
+ return resolver.get_resolved_inventory()
77
+
78
+ @classmethod
79
+ def _infer_architecture_from_data_model(cls, data_model: dict[str, Any]) -> str:
80
+ """Infer architecture from data model structure when no controller is present.
81
+
82
+ Examines the root keys in the data model to determine which
83
+ architecture is being used.
84
+
85
+ Args:
86
+ data_model: The merged data model to examine.
87
+
88
+ Returns:
89
+ Inferred controller type string.
90
+ """
91
+ # Check for architecture-specific root keys
92
+ if "sdwan" in data_model:
93
+ return "SDWAN"
94
+ elif "catalyst_center" in data_model:
95
+ return "CC"
96
+ elif "devices" in data_model:
97
+ return "IOSXE"
98
+ else:
99
+ # Default to IOS-XE if no recognized structure
100
+ return "IOSXE"
101
+
102
+ def get_device_credentials(self, device: dict[str, Any]) -> dict[str, str | None]:
103
+ """Get IOS-XE device credentials from environment.
104
+
105
+ Args:
106
+ device: Device dictionary (not used - all devices share credentials).
107
+
108
+ Returns:
109
+ Dictionary containing:
110
+ - username (str | None): SSH username from IOSXE_USERNAME
111
+ - password (str | None): SSH password from IOSXE_PASSWORD
112
+ """
113
+ return {
114
+ "username": os.environ.get("IOSXE_USERNAME"),
115
+ "password": os.environ.get("IOSXE_PASSWORD"),
116
+ }
@@ -0,0 +1 @@
1
+ # PEP 561 marker file - this package supports type checking
@@ -0,0 +1,47 @@
1
+ """SD-WAN (SDWAN Manager) adapter module for NAC PyATS testing.
2
+
3
+ This module provides SD-WAN-specific authentication, test base classes, and device
4
+ resolver implementations for use with the nac-test framework. It includes support
5
+ for both SDWAN Manager API testing and SSH-based device-to-device (D2D) testing.
6
+
7
+ Classes:
8
+ SDWANManagerAuth: SDWAN Manager authentication with JSESSIONID and XSRF token management.
9
+ SDWANManagerTestBase: Base class for SDWAN Manager API tests with tracking.
10
+ SDWANTestBase: Base class for SD-WAN SSH/D2D tests with device inventory.
11
+ SDWANDeviceResolver: Resolves device information from the SD-WAN data model.
12
+
13
+ Example:
14
+ For SDWAN Manager API testing:
15
+
16
+ >>> from nac_test_pyats_common.sdwan import SDWANManagerTestBase
17
+ >>>
18
+ >>> class VerifyDeviceList(SDWANManagerTestBase):
19
+ ... async def get_items_to_verify(self):
20
+ ... return ['device1', 'device2']
21
+ ...
22
+ ... async def verify_item(self, item):
23
+ ... response = await self.client.get(f"/dataservice/device/{item}")
24
+ ... return response.status_code == 200
25
+
26
+ For SSH/D2D testing:
27
+
28
+ >>> from nac_test_pyats_common.sdwan import SDWANTestBase
29
+ >>>
30
+ >>> class VerifyInterfaceStatus(SDWANTestBase):
31
+ ... @aetest.test
32
+ ... def verify_interfaces(self, steps, device):
33
+ ... # SSH-based verification
34
+ ... pass
35
+ """
36
+
37
+ from .api_test_base import SDWANManagerTestBase
38
+ from .auth import SDWANManagerAuth
39
+ from .device_resolver import SDWANDeviceResolver
40
+ from .ssh_test_base import SDWANTestBase
41
+
42
+ __all__ = [
43
+ "SDWANManagerAuth",
44
+ "SDWANManagerTestBase",
45
+ "SDWANTestBase",
46
+ "SDWANDeviceResolver",
47
+ ]
@@ -0,0 +1,164 @@
1
+ """SDWAN Manager-specific base test class for SD-WAN API testing.
2
+
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.
7
+
8
+ The class integrates with PyATS/Genie test frameworks and provides automatic
9
+ API call tracking for enhanced HTML reporting.
10
+ """
11
+
12
+ import asyncio
13
+ from typing import Any
14
+
15
+ import httpx
16
+ from nac_test.pyats_core.common.base_test import NACTestBase # type: ignore[import-untyped]
17
+ from pyats import aetest # type: ignore[import-untyped]
18
+
19
+ from .auth import SDWANManagerAuth
20
+
21
+
22
+ class SDWANManagerTestBase(NACTestBase): # type: ignore[misc]
23
+ """Base class for SDWAN Manager API tests with enhanced reporting.
24
+
25
+ This class extends the generic NACTestBase to provide SDWAN Manager-specific
26
+ functionality including session management (JSESSIONID and optional XSRF token),
27
+ API call tracking for HTML reports, and wrapped HTTP client for automatic
28
+ response capture. It serves as the foundation for all SD-WAN controller-specific
29
+ API test classes.
30
+
31
+ The class follows the same pattern as APICTestBase for consistency across
32
+ NAC architecture adapters. Token refresh is handled automatically by the
33
+ AuthCache TTL mechanism.
34
+
35
+ Attributes:
36
+ auth_data (dict): SDWAN Manager authentication data containing jsessionid and
37
+ optional xsrf_token obtained during setup.
38
+ client (httpx.AsyncClient | None): Wrapped async HTTP client configured for
39
+ SDWAN Manager. Initialized to None, set during setup().
40
+ controller_url (str): Base URL of the SDWAN Manager (inherited).
41
+ username (str): SDWAN Manager username for authentication (inherited).
42
+ password (str): SDWAN Manager password for authentication (inherited).
43
+
44
+ Methods:
45
+ setup(): Initialize SDWAN Manager authentication and client.
46
+ get_sdwan_manager_client(): Create and configure an SDWAN Manager-specific HTTP client.
47
+ run_async_verification_test(): Execute async verification tests with PyATS.
48
+
49
+ Example:
50
+ class MySDWANManagerTest(SDWANManagerTestBase):
51
+ async def get_items_to_verify(self):
52
+ return ['device1', 'device2']
53
+
54
+ async def verify_item(self, item):
55
+ # Custom verification logic here
56
+ pass
57
+
58
+ @aetest.test
59
+ def verify_devices(self, steps):
60
+ self.run_async_verification_test(steps)
61
+ """
62
+
63
+ client: httpx.AsyncClient | None = None # MUST declare at class level
64
+
65
+ @aetest.setup # type: ignore[misc, untyped-decorator]
66
+ def setup(self) -> None:
67
+ """Setup method that extends the generic base class setup.
68
+
69
+ Initializes the SDWAN Manager test environment by:
70
+ 1. Calling the parent class setup method
71
+ 2. Obtaining SDWAN Manager session data (jsessionid, xsrf_token) using cached auth
72
+ 3. Creating and storing an SDWAN Manager client for use in verification methods
73
+
74
+ The session data is obtained through the SDWANManagerAuth utility which
75
+ manages session lifecycle and prevents duplicate authentication requests
76
+ across parallel test execution.
77
+ """
78
+ super().setup()
79
+
80
+ # Get shared SDWAN Manager auth data (jsessionid, xsrf_token)
81
+ self.auth_data = SDWANManagerAuth.get_auth()
82
+
83
+ # Store the SDWAN Manager client for use in verification methods
84
+ self.client = self.get_sdwan_manager_client()
85
+
86
+ def get_sdwan_manager_client(self) -> httpx.AsyncClient:
87
+ """Get an httpx async client configured for SDWAN Manager with response tracking.
88
+
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.
93
+
94
+ The client includes:
95
+ - JSESSIONID cookie in all requests (via Cookie header)
96
+ - X-XSRF-TOKEN header when available (19.2+)
97
+ - Content-Type: application/json header
98
+ - Automatic API call tracking for reporting
99
+
100
+ 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.
104
+
105
+ Note:
106
+ SSL verification is disabled (verify=False) to support lab environments
107
+ with self-signed certificates. For production environments, consider
108
+ enabling SSL verification with proper certificate management.
109
+ """
110
+ # Build headers with Cookie header (following APIC pattern for consistency)
111
+ headers = {
112
+ "Cookie": f"JSESSIONID={self.auth_data['jsessionid']}",
113
+ "Content-Type": "application/json",
114
+ }
115
+
116
+ # Add XSRF token if available (19.2+ requires this for CSRF protection)
117
+ if self.auth_data.get("xsrf_token"):
118
+ headers["X-XSRF-TOKEN"] = self.auth_data["xsrf_token"]
119
+
120
+ # Get base client from pool with SSL verification disabled for lab compatibility
121
+ base_client = self.pool.get_client(
122
+ base_url=self.controller_url, headers=headers, verify=False
123
+ )
124
+
125
+ # Use the generic tracking wrapper from base class
126
+ return self.wrap_client_for_tracking(base_client, device_name="SDWAN Manager") # type: ignore[no-any-return]
127
+
128
+ def run_async_verification_test(self, steps: Any) -> None:
129
+ """Execute asynchronous verification tests with PyATS step tracking.
130
+
131
+ Simple entry point that uses base class orchestration to run async
132
+ verification tests. This thin wrapper:
133
+ 1. Creates and manages an event loop for async operations
134
+ 2. Calls NACTestBase.run_verification_async() to execute tests
135
+ 3. Passes results to NACTestBase.process_results_smart() for reporting
136
+ 4. Ensures proper cleanup of async resources
137
+
138
+ The actual verification logic is handled by:
139
+ - get_items_to_verify() - must be implemented by the test class
140
+ - verify_item() - must be implemented by the test class
141
+
142
+ Args:
143
+ steps: PyATS steps object for test reporting and step management.
144
+ Each verification item will be executed as a separate step
145
+ with automatic pass/fail tracking.
146
+
147
+ Note:
148
+ This method creates its own event loop to ensure compatibility
149
+ with PyATS synchronous test execution model. The loop and client
150
+ connections are properly closed after test completion.
151
+ """
152
+ loop = asyncio.new_event_loop()
153
+ asyncio.set_event_loop(loop)
154
+ try:
155
+ # Call the base class generic orchestration
156
+ results = loop.run_until_complete(self.run_verification_async())
157
+
158
+ # Process results using smart configuration-driven processing
159
+ self.process_results_smart(results, steps)
160
+ finally:
161
+ # Clean up the SDWAN Manager client connection
162
+ if self.client is not None: # MANDATORY: never use hasattr()
163
+ loop.run_until_complete(self.client.aclose())
164
+ loop.close()