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,49 @@
|
|
|
1
|
+
"""NAC PyATS Common - Architecture adapters for NAC PyATS testing.
|
|
2
|
+
|
|
3
|
+
This package provides architecture-specific authentication, test base classes,
|
|
4
|
+
and device resolver implementations for use with the nac-test framework. It
|
|
5
|
+
consolidates duplicated PyATS testing infrastructure from multiple NAC
|
|
6
|
+
architecture repositories (ACI, SD-WAN, Catalyst Center) into a single,
|
|
7
|
+
centralized, and maintainable package.
|
|
8
|
+
|
|
9
|
+
Supported Architectures:
|
|
10
|
+
- ACI (APIC): nac_test_pyats_common.aci
|
|
11
|
+
- SD-WAN (SDWAN Manager): nac_test_pyats_common.sdwan
|
|
12
|
+
- Catalyst Center: nac_test_pyats_common.catc
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> # For ACI/APIC testing
|
|
16
|
+
>>> from nac_test_pyats_common.aci import APICTestBase
|
|
17
|
+
>>>
|
|
18
|
+
>>> # For SD-WAN/SDWAN Manager testing
|
|
19
|
+
>>> from nac_test_pyats_common.sdwan import SDWANManagerTestBase, SDWANTestBase
|
|
20
|
+
>>>
|
|
21
|
+
>>> # For Catalyst Center testing
|
|
22
|
+
>>> from nac_test_pyats_common.catc import CatalystCenterTestBase
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
|
|
27
|
+
# Public API - Import from subpackages
|
|
28
|
+
from nac_test_pyats_common.aci import APICAuth, APICTestBase
|
|
29
|
+
from nac_test_pyats_common.catc import CatalystCenterAuth, CatalystCenterTestBase
|
|
30
|
+
from nac_test_pyats_common.sdwan import (
|
|
31
|
+
SDWANDeviceResolver,
|
|
32
|
+
SDWANManagerAuth,
|
|
33
|
+
SDWANManagerTestBase,
|
|
34
|
+
SDWANTestBase,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# ACI/APIC
|
|
39
|
+
"APICAuth",
|
|
40
|
+
"APICTestBase",
|
|
41
|
+
# SD-WAN/SDWAN Manager
|
|
42
|
+
"SDWANManagerAuth",
|
|
43
|
+
"SDWANManagerTestBase",
|
|
44
|
+
"SDWANTestBase",
|
|
45
|
+
"SDWANDeviceResolver",
|
|
46
|
+
# Catalyst Center
|
|
47
|
+
"CatalystCenterAuth",
|
|
48
|
+
"CatalystCenterTestBase",
|
|
49
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""ACI adapter module for PyATS common utilities.
|
|
2
|
+
|
|
3
|
+
This module provides the core ACI-specific components for PyATS testing, including
|
|
4
|
+
authentication handling and base test classes. It serves as the primary interface
|
|
5
|
+
for interacting with Cisco ACI (Application Centric Infrastructure) environments
|
|
6
|
+
within the PyATS testing framework.
|
|
7
|
+
|
|
8
|
+
The module exports two primary components:
|
|
9
|
+
1. APICAuth: Handles authentication and session management for APIC controllers
|
|
10
|
+
2. APICTestBase: Provides the base class for all ACI-specific PyATS tests
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
Basic usage of the ACI module components:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from nac_test_pyats_common.aci import APICAuth, APICTestBase
|
|
17
|
+
|
|
18
|
+
# Create authentication handler
|
|
19
|
+
auth = APICAuth(
|
|
20
|
+
controller_url="https://apic.example.com",
|
|
21
|
+
username="admin",
|
|
22
|
+
password="password123"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Use in a test class
|
|
26
|
+
class MyACITest(APICTestBase):
|
|
27
|
+
def setup(self):
|
|
28
|
+
self.auth = auth
|
|
29
|
+
super().setup()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
This module is specifically designed for testing ACI environments and requires
|
|
34
|
+
proper APIC controller access and credentials for full functionality.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .auth import APICAuth
|
|
38
|
+
from .test_base import APICTestBase
|
|
39
|
+
|
|
40
|
+
__all__ = ["APICAuth", "APICTestBase"]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""APIC authentication module for Cisco ACI (Application Centric Infrastructure).
|
|
2
|
+
|
|
3
|
+
This module provides authentication functionality for Cisco APIC (Application Policy
|
|
4
|
+
Infrastructure Controller), which is the central management and policy enforcement
|
|
5
|
+
point for ACI fabric. The authentication mechanism uses REST API calls to obtain
|
|
6
|
+
session tokens that are valid for a limited time period.
|
|
7
|
+
|
|
8
|
+
The module implements a two-tier API design:
|
|
9
|
+
1. authenticate() - Low-level method that performs direct APIC authentication
|
|
10
|
+
2. get_token() - High-level method that leverages caching for efficient token reuse
|
|
11
|
+
|
|
12
|
+
This design ensures efficient token management by reusing valid tokens and only
|
|
13
|
+
re-authenticating when necessary, reducing unnecessary API calls to the APIC controller.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from nac_test.pyats_core.common.auth_cache import AuthCache # type: ignore[import-untyped]
|
|
18
|
+
|
|
19
|
+
# Default token lifetime for APIC authentication tokens in seconds
|
|
20
|
+
# APIC tokens are typically valid for 10 minutes (600 seconds) by default
|
|
21
|
+
APIC_TOKEN_LIFETIME_SECONDS: int = 600
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APICAuth:
|
|
25
|
+
"""APIC-specific authentication implementation with token caching.
|
|
26
|
+
|
|
27
|
+
This class provides a two-tier API for APIC authentication:
|
|
28
|
+
|
|
29
|
+
1. Low-level authenticate() method: Directly authenticates with APIC and returns
|
|
30
|
+
a token along with its expiration time. This is typically used by the caching
|
|
31
|
+
layer and not called directly by consumers.
|
|
32
|
+
|
|
33
|
+
2. High-level get_token() method: Provides cached token management, automatically
|
|
34
|
+
handling token renewal when expired. This is the primary method that consumers
|
|
35
|
+
should use for obtaining APIC tokens.
|
|
36
|
+
|
|
37
|
+
The two-tier design ensures efficient token reuse across multiple API calls while
|
|
38
|
+
maintaining clean separation between authentication logic and caching concerns.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def authenticate(url: str, username: str, password: str) -> tuple[str, int]:
|
|
43
|
+
"""Perform direct APIC authentication and obtain a session token.
|
|
44
|
+
|
|
45
|
+
This method performs a direct authentication request to the APIC controller
|
|
46
|
+
using the provided credentials. It returns both the token and its lifetime
|
|
47
|
+
for proper cache management.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
url: Base URL of the APIC controller (e.g., "https://apic.example.com").
|
|
51
|
+
Should not include trailing slashes or API paths.
|
|
52
|
+
username: APIC username for authentication. This should be a valid user
|
|
53
|
+
configured in the APIC with appropriate permissions.
|
|
54
|
+
password: Password for the specified APIC user account.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A tuple containing:
|
|
58
|
+
- token (str): The APIC session token that should be included in
|
|
59
|
+
subsequent API requests as a cookie (APIC-cookie).
|
|
60
|
+
- expires_in (int): Token lifetime in seconds (typically 600 seconds).
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
httpx.HTTPStatusError: If the APIC returns a non-2xx status code,
|
|
64
|
+
typically indicating authentication failure (401) or server error.
|
|
65
|
+
httpx.RequestError: If the request fails due to network issues,
|
|
66
|
+
connection timeouts, or other transport-level problems.
|
|
67
|
+
KeyError: If the APIC response doesn't contain the expected JSON
|
|
68
|
+
structure with token information.
|
|
69
|
+
ValueError: If the APIC response contains malformed JSON that cannot
|
|
70
|
+
be parsed.
|
|
71
|
+
"""
|
|
72
|
+
# NOTE: SSL verification is disabled (verify=False) to handle self-signed
|
|
73
|
+
# certificates commonly used in lab and development APIC deployments.
|
|
74
|
+
# In production environments, proper certificate validation should be enabled
|
|
75
|
+
# by either installing the APIC certificate in the trust store or providing
|
|
76
|
+
# a custom CA bundle via the verify parameter.
|
|
77
|
+
with httpx.Client(verify=False) as client:
|
|
78
|
+
response = client.post(
|
|
79
|
+
f"{url}/api/aaaLogin.json",
|
|
80
|
+
json={"aaaUser": {"attributes": {"name": username, "pwd": password}}},
|
|
81
|
+
)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
|
|
84
|
+
# Parse the APIC response and extract the token
|
|
85
|
+
# Response structure: {"imdata": [{"aaaLogin": {"attributes": {"token": "..."}}}]}
|
|
86
|
+
try:
|
|
87
|
+
response_data = response.json()
|
|
88
|
+
token = response_data["imdata"][0]["aaaLogin"]["attributes"]["token"]
|
|
89
|
+
except (KeyError, IndexError) as e:
|
|
90
|
+
# Provide a more informative error message for malformed responses
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"APIC returned unexpected response structure. "
|
|
93
|
+
f"Expected JSON with 'imdata[0].aaaLogin.attributes.token' path. "
|
|
94
|
+
f"Actual response: {response.text[:500]}"
|
|
95
|
+
) from e
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
# Handle JSON parsing errors explicitly
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"APIC returned invalid JSON response: {response.text[:500]}"
|
|
100
|
+
) from e
|
|
101
|
+
|
|
102
|
+
return token, APIC_TOKEN_LIFETIME_SECONDS
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def get_token(cls, url: str, username: str, password: str) -> str:
|
|
106
|
+
"""Get APIC token with automatic caching and renewal.
|
|
107
|
+
|
|
108
|
+
This is the primary method that consumers should use to obtain APIC tokens.
|
|
109
|
+
It leverages the AuthCache to efficiently manage token lifecycle, reusing
|
|
110
|
+
valid tokens and automatically renewing expired ones. This significantly
|
|
111
|
+
reduces the number of authentication requests to the APIC controller.
|
|
112
|
+
|
|
113
|
+
The method uses a cache key based on the controller type ("ACI"), URL,
|
|
114
|
+
and username to ensure proper token isolation between different APIC
|
|
115
|
+
instances and user accounts.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
url: Base URL of the APIC controller (e.g., "https://apic.example.com").
|
|
119
|
+
Should not include trailing slashes or API paths.
|
|
120
|
+
username: APIC username for authentication. This should be a valid user
|
|
121
|
+
configured in the APIC with appropriate permissions.
|
|
122
|
+
password: Password for the specified APIC user account.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A valid APIC session token that can be used in API requests.
|
|
126
|
+
The token should be included as a cookie (APIC-cookie) in subsequent
|
|
127
|
+
API calls to the APIC controller.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
httpx.HTTPStatusError: If the APIC returns a non-2xx status code during
|
|
131
|
+
authentication, typically indicating invalid credentials (401) or
|
|
132
|
+
server issues (5xx).
|
|
133
|
+
httpx.RequestError: If the request fails due to network issues,
|
|
134
|
+
connection timeouts, or other transport-level problems.
|
|
135
|
+
ValueError: If the APIC response contains malformed or unexpected JSON
|
|
136
|
+
structure that cannot be properly parsed.
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
>>> # Get a token for APIC access
|
|
140
|
+
>>> token = APICAuth.get_token(
|
|
141
|
+
... url="https://apic.example.com",
|
|
142
|
+
... username="admin",
|
|
143
|
+
... password="password123"
|
|
144
|
+
... )
|
|
145
|
+
>>> # Use the token in subsequent API calls
|
|
146
|
+
>>> headers = {"Cookie": f"APIC-cookie={token}"}
|
|
147
|
+
"""
|
|
148
|
+
# AuthCache.get_or_create_token returns str, but mypy can't verify this
|
|
149
|
+
# because nac_test lacks py.typed marker. The return type is guaranteed
|
|
150
|
+
# by AuthCache's implementation which uses extract_token=True mode.
|
|
151
|
+
return AuthCache.get_or_create_token( # type: ignore[no-any-return]
|
|
152
|
+
controller_type="ACI",
|
|
153
|
+
url=url,
|
|
154
|
+
username=username,
|
|
155
|
+
password=password,
|
|
156
|
+
auth_func=cls.authenticate,
|
|
157
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""APIC-specific base test class for ACI API testing.
|
|
2
|
+
|
|
3
|
+
This module provides the APICTestBase class, which extends the generic NACTestBase
|
|
4
|
+
to add ACI-specific functionality for testing APIC controllers. It handles
|
|
5
|
+
authentication, client management, and provides a standardized interface for
|
|
6
|
+
running asynchronous verification tests against ACI fabrics.
|
|
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 APICAuth
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APICTestBase(NACTestBase): # type: ignore[misc]
|
|
23
|
+
"""Base class for APIC API tests with enhanced reporting.
|
|
24
|
+
|
|
25
|
+
This class extends the generic NACTestBase to provide APIC-specific
|
|
26
|
+
functionality including APIC authentication token management, API call
|
|
27
|
+
tracking for HTML reports, and wrapped HTTP client for automatic response
|
|
28
|
+
capture. It serves as the foundation for all ACI-specific test classes.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
token (str): APIC authentication token obtained during setup.
|
|
32
|
+
client (httpx.AsyncClient): Wrapped async HTTP client configured for APIC.
|
|
33
|
+
controller_url (str): Base URL of the APIC controller (inherited).
|
|
34
|
+
username (str): APIC username for authentication (inherited).
|
|
35
|
+
password (str): APIC password for authentication (inherited).
|
|
36
|
+
|
|
37
|
+
Methods:
|
|
38
|
+
setup(): Initialize APIC authentication and client.
|
|
39
|
+
get_apic_client(): Create and configure an APIC-specific HTTP client.
|
|
40
|
+
run_async_verification_test(): Execute async verification tests with PyATS.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
class MyAPICTest(APICTestBase):
|
|
44
|
+
async def get_items_to_verify(self):
|
|
45
|
+
return ['tenant1', 'tenant2']
|
|
46
|
+
|
|
47
|
+
async def verify_item(self, item):
|
|
48
|
+
# Custom verification logic here
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@aetest.test
|
|
52
|
+
def verify_tenants(self, steps):
|
|
53
|
+
self.run_async_verification_test(steps)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@aetest.setup # type: ignore[misc, untyped-decorator]
|
|
57
|
+
def setup(self) -> None:
|
|
58
|
+
"""Setup method that extends the generic base class setup.
|
|
59
|
+
|
|
60
|
+
Initializes the APIC test environment by:
|
|
61
|
+
1. Calling the parent class setup method
|
|
62
|
+
2. Obtaining an APIC authentication token using file-based locking
|
|
63
|
+
3. Creating and storing an APIC client for use in verification methods
|
|
64
|
+
|
|
65
|
+
The authentication token is obtained through the APICAuth utility which
|
|
66
|
+
manages token lifecycle and prevents duplicate authentication requests
|
|
67
|
+
across parallel test execution.
|
|
68
|
+
"""
|
|
69
|
+
super().setup()
|
|
70
|
+
|
|
71
|
+
# Get shared APIC token using file-based locking
|
|
72
|
+
self.token = APICAuth.get_token(self.controller_url, self.username, self.password)
|
|
73
|
+
|
|
74
|
+
# Store the APIC client for use in verification methods
|
|
75
|
+
self.client = self.get_apic_client()
|
|
76
|
+
|
|
77
|
+
def get_apic_client(self) -> httpx.AsyncClient:
|
|
78
|
+
"""Get an httpx async client configured for APIC with response tracking.
|
|
79
|
+
|
|
80
|
+
Creates an HTTP client specifically configured for APIC API communication
|
|
81
|
+
with authentication headers, base URL, and automatic response tracking
|
|
82
|
+
for HTML report generation. The client is wrapped to capture all API
|
|
83
|
+
interactions for detailed test reporting.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
httpx.AsyncClient: Configured client with APIC authentication, base URL,
|
|
87
|
+
and wrapped for automatic API call tracking. The client has SSL
|
|
88
|
+
verification disabled for lab environment compatibility.
|
|
89
|
+
|
|
90
|
+
Note:
|
|
91
|
+
SSL verification is disabled (verify=False) to support lab environments
|
|
92
|
+
with self-signed certificates. For production environments, consider
|
|
93
|
+
enabling SSL verification with proper certificate management.
|
|
94
|
+
"""
|
|
95
|
+
headers = {"Cookie": f"APIC-cookie={self.token}"}
|
|
96
|
+
# SSL verification disabled for lab environment compatibility
|
|
97
|
+
client = self.pool.get_client(base_url=self.controller_url, headers=headers, verify=False)
|
|
98
|
+
|
|
99
|
+
# Use the generic tracking wrapper from base class
|
|
100
|
+
return self.wrap_client_for_tracking(client, device_name="APIC") # type: ignore[no-any-return]
|
|
101
|
+
|
|
102
|
+
def run_async_verification_test(self, steps: Any) -> None:
|
|
103
|
+
"""Execute asynchronous verification tests with PyATS step tracking.
|
|
104
|
+
|
|
105
|
+
Simple entry point that uses base class orchestration to run async
|
|
106
|
+
verification tests. This thin wrapper:
|
|
107
|
+
1. Creates and manages an event loop for async operations
|
|
108
|
+
2. Calls NACTestBase.run_verification_async() to execute tests
|
|
109
|
+
3. Passes results to NACTestBase.process_results_smart() for reporting
|
|
110
|
+
4. Ensures proper cleanup of async resources
|
|
111
|
+
|
|
112
|
+
The actual verification logic is handled by:
|
|
113
|
+
- get_items_to_verify() - must be implemented by the test class
|
|
114
|
+
- verify_item() - must be implemented by the test class
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
steps: PyATS steps object for test reporting and step management.
|
|
118
|
+
Each verification item will be executed as a separate step
|
|
119
|
+
with automatic pass/fail tracking.
|
|
120
|
+
|
|
121
|
+
Note:
|
|
122
|
+
This method creates its own event loop to ensure compatibility
|
|
123
|
+
with PyATS synchronous test execution model. The loop and client
|
|
124
|
+
connections are properly closed after test completion.
|
|
125
|
+
"""
|
|
126
|
+
loop = asyncio.new_event_loop()
|
|
127
|
+
asyncio.set_event_loop(loop)
|
|
128
|
+
try:
|
|
129
|
+
# Call the base class generic orchestration
|
|
130
|
+
results = loop.run_until_complete(self.run_verification_async())
|
|
131
|
+
|
|
132
|
+
# Process results using smart configuration-driven processing
|
|
133
|
+
self.process_results_smart(results, steps)
|
|
134
|
+
finally:
|
|
135
|
+
# Clean up the APIC client connection
|
|
136
|
+
if hasattr(self, "client"):
|
|
137
|
+
loop.run_until_complete(self.client.aclose())
|
|
138
|
+
loop.close()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Catalyst Center adapter module for NAC PyATS testing.
|
|
2
|
+
|
|
3
|
+
This module provides Catalyst Center-specific authentication and test base class
|
|
4
|
+
implementations for use with the nac-test framework. Catalyst Center (formerly
|
|
5
|
+
DNA Center) is Cisco's enterprise network management platform.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
CatalystCenterAuth: Token-based authentication with automatic endpoint detection.
|
|
9
|
+
CatalystCenterTestBase: Base class for Catalyst Center API tests with tracking.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from nac_test_pyats_common.catc import CatalystCenterTestBase
|
|
13
|
+
>>>
|
|
14
|
+
>>> class VerifyNetworkDevices(CatalystCenterTestBase):
|
|
15
|
+
... async def get_items_to_verify(self):
|
|
16
|
+
... return ['device-uuid-1', 'device-uuid-2']
|
|
17
|
+
...
|
|
18
|
+
... async def verify_item(self, item):
|
|
19
|
+
... response = await self.client.get(
|
|
20
|
+
... f"/dna/intent/api/v1/network-device/{item}"
|
|
21
|
+
... )
|
|
22
|
+
... return response.status_code == 200
|
|
23
|
+
...
|
|
24
|
+
... @aetest.test
|
|
25
|
+
... def verify_devices(self, steps):
|
|
26
|
+
... self.run_async_verification_test(steps)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .auth import CatalystCenterAuth
|
|
30
|
+
from .test_base import CatalystCenterTestBase
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CatalystCenterAuth",
|
|
34
|
+
"CatalystCenterTestBase",
|
|
35
|
+
]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Catalyst Center-specific authentication implementation.
|
|
2
|
+
|
|
3
|
+
This module provides authentication functionality for Cisco Catalyst Center
|
|
4
|
+
(formerly DNA Center), which is the central management platform for enterprise
|
|
5
|
+
networks. The authentication mechanism uses token-based login with Basic Auth.
|
|
6
|
+
|
|
7
|
+
The module implements a two-tier API design:
|
|
8
|
+
1. _authenticate() - Low-level method that performs direct Catalyst Center authentication
|
|
9
|
+
2. get_auth() - High-level method that leverages caching for efficient token reuse
|
|
10
|
+
|
|
11
|
+
This design ensures efficient token management by reusing valid tokens and only
|
|
12
|
+
re-authenticating when necessary, reducing unnecessary API calls to the controller.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
from nac_test.pyats_core.common.auth_cache import AuthCache # type: ignore[import-untyped]
|
|
20
|
+
|
|
21
|
+
# Default token lifetime for Catalyst Center authentication in seconds
|
|
22
|
+
# Catalyst Center tokens are typically valid for 1 hour (3600 seconds) by default
|
|
23
|
+
CATALYST_CENTER_TOKEN_LIFETIME_SECONDS: int = 3600
|
|
24
|
+
|
|
25
|
+
# HTTP timeout for authentication request
|
|
26
|
+
AUTH_REQUEST_TIMEOUT_SECONDS: float = 30.0
|
|
27
|
+
|
|
28
|
+
# Auth endpoints (try modern first, fallback to legacy)
|
|
29
|
+
AUTH_ENDPOINTS: list[str] = [
|
|
30
|
+
"/api/system/v1/auth/token", # Modern (Catalyst Center 2.x)
|
|
31
|
+
"/dna/system/api/v1/auth/token", # Legacy (DNA Center 1.x/2.x)
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CatalystCenterAuth:
|
|
36
|
+
"""Catalyst Center-specific authentication implementation with token caching.
|
|
37
|
+
|
|
38
|
+
This class provides a two-tier API for Catalyst Center authentication:
|
|
39
|
+
|
|
40
|
+
1. Low-level _authenticate() method: Directly authenticates with Catalyst Center
|
|
41
|
+
using Basic Auth and returns token data along with expiration time. This is
|
|
42
|
+
typically used by the caching layer and not called directly by consumers.
|
|
43
|
+
|
|
44
|
+
2. High-level get_auth() method: Provides cached token management, automatically
|
|
45
|
+
handling token renewal when expired. This is the primary method that consumers
|
|
46
|
+
should use for obtaining Catalyst Center tokens.
|
|
47
|
+
|
|
48
|
+
The authentication flow supports both:
|
|
49
|
+
- Modern Catalyst Center 2.x: /api/system/v1/auth/token endpoint
|
|
50
|
+
- Legacy DNA Center 1.x/2.x: /dna/system/api/v1/auth/token endpoint
|
|
51
|
+
|
|
52
|
+
The class mirrors VManageAuth pattern for consistency across NAC adapters.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> # Get authentication data for Catalyst Center API calls
|
|
56
|
+
>>> auth_data = CatalystCenterAuth.get_auth()
|
|
57
|
+
>>> # Use in requests
|
|
58
|
+
>>> headers = {"X-Auth-Token": auth_data["token"]}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def _authenticate(
|
|
63
|
+
cls, url: str, username: str, password: str, verify_ssl: bool
|
|
64
|
+
) -> tuple[dict[str, Any], int]:
|
|
65
|
+
"""Perform direct Catalyst Center authentication and obtain token.
|
|
66
|
+
|
|
67
|
+
This method performs a direct authentication request to the Catalyst Center
|
|
68
|
+
using Basic Auth. It tries the modern auth endpoint first, then falls back
|
|
69
|
+
to the legacy endpoint if needed for backward compatibility.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
url: Base URL of the Catalyst Center (e.g., "https://catc.example.com").
|
|
73
|
+
Should not include trailing slashes or API paths.
|
|
74
|
+
username: Catalyst Center username for authentication. This should be
|
|
75
|
+
a valid user configured with appropriate permissions.
|
|
76
|
+
password: Password for the specified Catalyst Center user account.
|
|
77
|
+
verify_ssl: Whether to verify SSL certificates. Set to False for
|
|
78
|
+
lab environments with self-signed certificates.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A tuple containing:
|
|
82
|
+
- auth_dict (dict): Dictionary with 'token' (str) containing the
|
|
83
|
+
authentication token for API requests.
|
|
84
|
+
- expires_in (int): Token lifetime in seconds (typically 3600).
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
httpx.HTTPStatusError: If Catalyst Center returns a non-2xx status code
|
|
88
|
+
on all auth endpoints, typically indicating authentication failure.
|
|
89
|
+
RuntimeError: If authentication fails on all available endpoints.
|
|
90
|
+
ValueError: If the token is not received in the response from any endpoint.
|
|
91
|
+
|
|
92
|
+
Note:
|
|
93
|
+
SSL verification can be disabled via the verify_ssl parameter to handle
|
|
94
|
+
self-signed certificates commonly used in lab deployments. In production
|
|
95
|
+
environments, proper certificate validation should be enabled.
|
|
96
|
+
"""
|
|
97
|
+
last_error: Exception | None = None
|
|
98
|
+
|
|
99
|
+
with httpx.Client(verify=verify_ssl, timeout=AUTH_REQUEST_TIMEOUT_SECONDS) as client:
|
|
100
|
+
for endpoint in AUTH_ENDPOINTS:
|
|
101
|
+
try:
|
|
102
|
+
auth_response = client.post(
|
|
103
|
+
f"{url}{endpoint}",
|
|
104
|
+
auth=(username, password),
|
|
105
|
+
headers={
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
"Accept": "application/json",
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
auth_response.raise_for_status()
|
|
111
|
+
|
|
112
|
+
# Extract token from response
|
|
113
|
+
response_data = auth_response.json()
|
|
114
|
+
token = response_data.get("Token")
|
|
115
|
+
|
|
116
|
+
if not token:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"No 'Token' field in auth response from {endpoint}. "
|
|
119
|
+
f"Response keys: {list(response_data.keys())}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Return auth data with token lifetime
|
|
123
|
+
return {"token": str(token)}, CATALYST_CENTER_TOKEN_LIFETIME_SECONDS
|
|
124
|
+
|
|
125
|
+
except (httpx.HTTPError, ValueError) as e:
|
|
126
|
+
last_error = e
|
|
127
|
+
# Try next endpoint
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# All endpoints failed
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
f"Catalyst Center authentication failed on all endpoints. Last error: {last_error}"
|
|
133
|
+
) from last_error
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def get_auth(cls) -> dict[str, Any]:
|
|
137
|
+
"""Get Catalyst Center authentication data with automatic caching and renewal.
|
|
138
|
+
|
|
139
|
+
This is the primary method that consumers should use to obtain Catalyst Center
|
|
140
|
+
tokens. It leverages the AuthCache to efficiently manage token lifecycle,
|
|
141
|
+
reusing valid tokens and automatically renewing expired ones. This significantly
|
|
142
|
+
reduces the number of authentication requests to the Catalyst Center.
|
|
143
|
+
|
|
144
|
+
The method uses a cache key based on the controller type ("CC") and URL
|
|
145
|
+
to ensure proper token isolation between different Catalyst Center instances.
|
|
146
|
+
|
|
147
|
+
Environment Variables Required:
|
|
148
|
+
CC_URL: Base URL of the Catalyst Center
|
|
149
|
+
CC_USERNAME: Catalyst Center username for authentication
|
|
150
|
+
CC_PASSWORD: Catalyst Center password for authentication
|
|
151
|
+
CC_INSECURE: Optional. Set to "True" to disable SSL verification (default: True)
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A dictionary containing:
|
|
155
|
+
- token (str): The authentication token for API requests.
|
|
156
|
+
Should be included as X-Auth-Token header in subsequent calls.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If any required environment variables (CC_URL, CC_USERNAME,
|
|
160
|
+
CC_PASSWORD) are not set.
|
|
161
|
+
RuntimeError: If authentication fails on all available endpoints.
|
|
162
|
+
httpx.HTTPStatusError: If Catalyst Center returns authentication errors.
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
>>> # Set environment variables first
|
|
166
|
+
>>> import os
|
|
167
|
+
>>> os.environ["CC_URL"] = "https://catalyst.example.com"
|
|
168
|
+
>>> os.environ["CC_USERNAME"] = "admin"
|
|
169
|
+
>>> os.environ["CC_PASSWORD"] = "password123"
|
|
170
|
+
>>> os.environ["CC_INSECURE"] = "True" # For lab environments
|
|
171
|
+
>>> # Get authentication data
|
|
172
|
+
>>> auth_data = CatalystCenterAuth.get_auth()
|
|
173
|
+
>>> # Use in API requests
|
|
174
|
+
>>> headers = {"X-Auth-Token": auth_data["token"]}
|
|
175
|
+
"""
|
|
176
|
+
url = os.environ.get("CC_URL")
|
|
177
|
+
username = os.environ.get("CC_USERNAME")
|
|
178
|
+
password = os.environ.get("CC_PASSWORD")
|
|
179
|
+
insecure = os.environ.get("CC_INSECURE", "True").lower() in ("true", "1", "yes")
|
|
180
|
+
|
|
181
|
+
if not all([url, username, password]):
|
|
182
|
+
missing_vars: list[str] = []
|
|
183
|
+
if not url:
|
|
184
|
+
missing_vars.append("CC_URL")
|
|
185
|
+
if not username:
|
|
186
|
+
missing_vars.append("CC_USERNAME")
|
|
187
|
+
if not password:
|
|
188
|
+
missing_vars.append("CC_PASSWORD")
|
|
189
|
+
raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
|
|
190
|
+
|
|
191
|
+
# Normalize URL by removing trailing slash
|
|
192
|
+
url = url.rstrip("/") # type: ignore[union-attr]
|
|
193
|
+
verify_ssl = not insecure # CC_INSECURE=True means verify=False
|
|
194
|
+
|
|
195
|
+
def auth_wrapper() -> tuple[dict[str, Any], int]:
|
|
196
|
+
"""Wrapper for authentication that captures closure variables."""
|
|
197
|
+
return cls._authenticate(url, username, password, verify_ssl) # type: ignore[arg-type]
|
|
198
|
+
|
|
199
|
+
# AuthCache.get_or_create returns dict[str, Any], but mypy can't verify this
|
|
200
|
+
# because nac_test lacks py.typed marker.
|
|
201
|
+
return AuthCache.get_or_create( # type: ignore[no-any-return]
|
|
202
|
+
controller_type="CC",
|
|
203
|
+
url=url,
|
|
204
|
+
auth_func=auth_wrapper,
|
|
205
|
+
)
|