nac-test-pyats-common 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nac_test_pyats_common/__init__.py +49 -0
- nac_test_pyats_common/aci/__init__.py +40 -0
- nac_test_pyats_common/aci/auth.py +157 -0
- nac_test_pyats_common/aci/test_base.py +138 -0
- nac_test_pyats_common/catc/__init__.py +35 -0
- nac_test_pyats_common/catc/auth.py +205 -0
- nac_test_pyats_common/catc/test_base.py +168 -0
- nac_test_pyats_common/common/__init__.py +29 -0
- nac_test_pyats_common/common/base_device_resolver.py +492 -0
- nac_test_pyats_common/iosxe/__init__.py +58 -0
- nac_test_pyats_common/iosxe/catc_resolver.py +64 -0
- nac_test_pyats_common/iosxe/iosxe_resolver.py +62 -0
- nac_test_pyats_common/iosxe/registry.py +153 -0
- nac_test_pyats_common/iosxe/test_base.py +116 -0
- nac_test_pyats_common/py.typed +1 -0
- nac_test_pyats_common/sdwan/__init__.py +47 -0
- nac_test_pyats_common/sdwan/api_test_base.py +164 -0
- nac_test_pyats_common/sdwan/auth.py +215 -0
- nac_test_pyats_common/sdwan/device_resolver.py +179 -0
- nac_test_pyats_common/sdwan/ssh_test_base.py +101 -0
- nac_test_pyats_common-0.1.0.dist-info/METADATA +304 -0
- nac_test_pyats_common-0.1.0.dist-info/RECORD +24 -0
- nac_test_pyats_common-0.1.0.dist-info/WHEEL +4 -0
- nac_test_pyats_common-0.1.0.dist-info/licenses/LICENSE +385 -0
|
@@ -0,0 +1,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()
|