nac-test-pyats-common 0.1.1__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nac_test_pyats_common/__init__.py +8 -1
- nac_test_pyats_common/aci/auth.py +177 -53
- nac_test_pyats_common/aci/test_base.py +35 -12
- nac_test_pyats_common/catc/__init__.py +27 -9
- nac_test_pyats_common/catc/{test_base.py → api_test_base.py} +35 -11
- nac_test_pyats_common/catc/auth.py +116 -49
- nac_test_pyats_common/catc/device_resolver.py +171 -0
- nac_test_pyats_common/catc/ssh_test_base.py +115 -0
- nac_test_pyats_common/common/base_device_resolver.py +144 -199
- nac_test_pyats_common/iosxe/iosxe_resolver.py +2 -2
- nac_test_pyats_common/iosxe/test_base.py +6 -0
- nac_test_pyats_common/sdwan/api_test_base.py +28 -7
- nac_test_pyats_common/sdwan/auth.py +125 -64
- nac_test_pyats_common/sdwan/device_resolver.py +69 -22
- nac_test_pyats_common/sdwan/ssh_test_base.py +11 -9
- {nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/METADATA +3 -2
- nac_test_pyats_common-0.2.1.dist-info/RECORD +25 -0
- nac_test_pyats_common/iosxe/catc_resolver.py +0 -67
- nac_test_pyats_common-0.1.1.dist-info/RECORD +0 -24
- {nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/WHEEL +0 -0
- {nac_test_pyats_common-0.1.1.dist-info → nac_test_pyats_common-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,14 +14,21 @@ The module implements a two-tier API design:
|
|
|
14
14
|
|
|
15
15
|
This design ensures efficient token management by reusing valid tokens and only
|
|
16
16
|
re-authenticating when necessary, reducing unnecessary API calls to the controller.
|
|
17
|
+
|
|
18
|
+
Note on Fork Safety:
|
|
19
|
+
This module uses urllib instead of httpx for synchronous authentication requests.
|
|
20
|
+
httpx is NOT fork-safe on macOS - creating httpx.Client after fork() causes
|
|
21
|
+
silent crashes due to OpenSSL threading issues. urllib uses simpler primitives
|
|
22
|
+
that work correctly after fork().
|
|
17
23
|
"""
|
|
18
24
|
|
|
19
25
|
import os
|
|
20
26
|
from typing import Any
|
|
21
27
|
|
|
22
|
-
import
|
|
23
|
-
from nac_test.pyats_core.common.
|
|
24
|
-
|
|
28
|
+
from nac_test.pyats_core.common.auth_cache import AuthCache
|
|
29
|
+
from nac_test.pyats_core.common.subprocess_auth import (
|
|
30
|
+
SubprocessAuthError, # noqa: F401 - re-exported for callers to catch
|
|
31
|
+
execute_auth_subprocess,
|
|
25
32
|
)
|
|
26
33
|
|
|
27
34
|
# Default token lifetime for Catalyst Center authentication in seconds
|
|
@@ -74,6 +81,10 @@ class CatalystCenterAuth:
|
|
|
74
81
|
using Basic Auth. It tries the modern auth endpoint first, then falls back
|
|
75
82
|
to the legacy endpoint if needed for backward compatibility.
|
|
76
83
|
|
|
84
|
+
Note: On macOS, SSL operations in forked processes crash due to OpenSSL
|
|
85
|
+
threading issues. This method uses subprocess with spawn context to perform
|
|
86
|
+
authentication in a fresh process, avoiding the fork+SSL crash.
|
|
87
|
+
|
|
77
88
|
Args:
|
|
78
89
|
url: Base URL of the Catalyst Center (e.g., "https://catc.example.com").
|
|
79
90
|
Should not include trailing slashes or API paths.
|
|
@@ -90,56 +101,110 @@ class CatalystCenterAuth:
|
|
|
90
101
|
- expires_in (int): Token lifetime in seconds (typically 3600).
|
|
91
102
|
|
|
92
103
|
Raises:
|
|
93
|
-
|
|
94
|
-
on all
|
|
95
|
-
|
|
96
|
-
ValueError: If the token is not received in the response from any endpoint.
|
|
104
|
+
SubprocessAuthError: If authentication subprocess fails or authentication
|
|
105
|
+
fails on all available endpoints.
|
|
106
|
+
ValueError: If the authentication response is malformed.
|
|
97
107
|
|
|
98
108
|
Note:
|
|
99
109
|
SSL verification can be disabled via the verify_ssl parameter to handle
|
|
100
110
|
self-signed certificates commonly used in lab deployments. In production
|
|
101
111
|
environments, proper certificate validation should be enabled.
|
|
102
112
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
113
|
+
# Build auth parameters for subprocess
|
|
114
|
+
auth_params = {
|
|
115
|
+
"url": url,
|
|
116
|
+
"username": username,
|
|
117
|
+
"password": password,
|
|
118
|
+
"timeout": AUTH_REQUEST_TIMEOUT_SECONDS,
|
|
119
|
+
"verify_ssl": verify_ssl,
|
|
120
|
+
"endpoints": AUTH_ENDPOINTS,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Catalyst Center-specific authentication logic
|
|
124
|
+
# This script assumes `params` dict is already loaded by execute_auth_subprocess
|
|
125
|
+
auth_script_body = """
|
|
126
|
+
import base64
|
|
127
|
+
import json
|
|
128
|
+
import ssl
|
|
129
|
+
import urllib.request
|
|
130
|
+
import urllib.error
|
|
131
|
+
|
|
132
|
+
url = params["url"]
|
|
133
|
+
username = params["username"]
|
|
134
|
+
password = params["password"]
|
|
135
|
+
timeout = params["timeout"]
|
|
136
|
+
verify_ssl = params["verify_ssl"]
|
|
137
|
+
endpoints = params["endpoints"]
|
|
138
|
+
|
|
139
|
+
# Create SSL context
|
|
140
|
+
ssl_context = ssl.create_default_context()
|
|
141
|
+
if not verify_ssl:
|
|
142
|
+
ssl_context.check_hostname = False
|
|
143
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
144
|
+
|
|
145
|
+
# Create Basic Auth header
|
|
146
|
+
credentials = f"{username}:{password}"
|
|
147
|
+
b64_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
|
|
148
|
+
auth_header = f"Basic {b64_credentials}"
|
|
149
|
+
|
|
150
|
+
# Create HTTPS handler with SSL context
|
|
151
|
+
https_handler = urllib.request.HTTPSHandler(context=ssl_context)
|
|
152
|
+
opener = urllib.request.build_opener(https_handler)
|
|
153
|
+
|
|
154
|
+
last_error = None
|
|
155
|
+
|
|
156
|
+
for endpoint in endpoints:
|
|
157
|
+
try:
|
|
158
|
+
# Create request with Basic Auth and proper headers
|
|
159
|
+
request = urllib.request.Request(
|
|
160
|
+
f"{url}{endpoint}",
|
|
161
|
+
data=None,
|
|
162
|
+
headers={
|
|
163
|
+
"Content-Type": "application/json",
|
|
164
|
+
"Accept": "application/json",
|
|
165
|
+
"Authorization": auth_header,
|
|
166
|
+
},
|
|
167
|
+
method="POST"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Execute authentication request
|
|
171
|
+
response = opener.open(request, timeout=timeout)
|
|
172
|
+
response_body = response.read().decode("utf-8")
|
|
173
|
+
response_data = json.loads(response_body)
|
|
174
|
+
|
|
175
|
+
# Extract token from response
|
|
176
|
+
token = response_data.get("Token")
|
|
177
|
+
if not token:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"No 'Token' field in auth response from {endpoint}. "
|
|
180
|
+
f"Response keys: {list(response_data.keys())}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
result = {"token": str(token)}
|
|
184
|
+
break # Success - exit loop
|
|
185
|
+
|
|
186
|
+
except urllib.error.HTTPError as e:
|
|
187
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
188
|
+
err_snippet = error_body[:200]
|
|
189
|
+
last_error = (
|
|
190
|
+
f"HTTP {e.code} on {endpoint}: {e.reason}. {err_snippet}"
|
|
191
|
+
)
|
|
192
|
+
continue
|
|
193
|
+
except ValueError as e:
|
|
194
|
+
last_error = str(e)
|
|
195
|
+
continue
|
|
196
|
+
except Exception as e:
|
|
197
|
+
last_error = f"{endpoint}: {str(e)}"
|
|
198
|
+
continue
|
|
199
|
+
else:
|
|
200
|
+
# Loop completed without break - all endpoints failed
|
|
201
|
+
result = {"error": f"All endpoints failed. Last error: {last_error}"}
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
# Execute authentication in subprocess (fork-safe on macOS)
|
|
205
|
+
auth_result = execute_auth_subprocess(auth_params, auth_script_body)
|
|
206
|
+
|
|
207
|
+
return {"token": auth_result["token"]}, CATALYST_CENTER_TOKEN_LIFETIME_SECONDS
|
|
143
208
|
|
|
144
209
|
@classmethod
|
|
145
210
|
def get_auth(cls) -> dict[str, Any]:
|
|
@@ -168,8 +233,10 @@ class CatalystCenterAuth:
|
|
|
168
233
|
Raises:
|
|
169
234
|
ValueError: If any required environment variables (CC_URL, CC_USERNAME,
|
|
170
235
|
CC_PASSWORD) are not set.
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
SubprocessAuthError: If authentication fails due to invalid credentials,
|
|
237
|
+
network issues, connection timeouts, or Catalyst Center server errors.
|
|
238
|
+
The error message will contain details from the authentication
|
|
239
|
+
subprocess.
|
|
173
240
|
|
|
174
241
|
Example:
|
|
175
242
|
>>> # Set environment variables first
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
# Copyright (c) 2025 Daniel Schmidt
|
|
3
|
+
|
|
4
|
+
"""Catalyst Center-specific device resolver for parsing the NAC data model.
|
|
5
|
+
|
|
6
|
+
This module provides the CatalystCenterDeviceResolver class, which extends
|
|
7
|
+
BaseDeviceResolver to implement Catalyst Center schema navigation for D2D testing.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from nac_test_pyats_common.common import BaseDeviceResolver
|
|
14
|
+
from nac_test_pyats_common.iosxe.registry import register_iosxe_resolver
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_iosxe_resolver("CC")
|
|
20
|
+
class CatalystCenterDeviceResolver(BaseDeviceResolver):
|
|
21
|
+
"""Catalyst Center device resolver for D2D testing.
|
|
22
|
+
|
|
23
|
+
Navigates the Catalyst Center NAC schema (catalyst_center.inventory.devices[])
|
|
24
|
+
to extract device information for SSH testing.
|
|
25
|
+
|
|
26
|
+
Schema structure:
|
|
27
|
+
catalyst_center:
|
|
28
|
+
inventory:
|
|
29
|
+
devices:
|
|
30
|
+
- name: P3-BN1
|
|
31
|
+
fqdn_name: P3-BN1.cisco.eu
|
|
32
|
+
device_ip: 192.168.38.1
|
|
33
|
+
pid: C9300-24P
|
|
34
|
+
state: PROVISION
|
|
35
|
+
device_role: ACCESS
|
|
36
|
+
site: Global/MAX_AREA/MAX_BUILDING
|
|
37
|
+
|
|
38
|
+
Credentials:
|
|
39
|
+
Uses IOSXE_USERNAME and IOSXE_PASSWORD environment variables
|
|
40
|
+
for SSH access to managed IOS-XE devices.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> resolver = CatalystCenterDeviceResolver(data_model)
|
|
44
|
+
>>> devices = resolver.get_resolved_inventory()
|
|
45
|
+
>>> for device in devices:
|
|
46
|
+
... print(f"{device['hostname']}: {device['host']}")
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def get_architecture_name(self) -> str:
|
|
50
|
+
"""Return 'catalyst_center' as the architecture identifier.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Architecture name used in logging and error messages.
|
|
54
|
+
"""
|
|
55
|
+
return "catalyst_center"
|
|
56
|
+
|
|
57
|
+
def get_schema_root_key(self) -> str:
|
|
58
|
+
"""Return 'catalyst_center' as the root key in the data model.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Root key used when navigating the schema.
|
|
62
|
+
"""
|
|
63
|
+
return "catalyst_center"
|
|
64
|
+
|
|
65
|
+
def navigate_to_devices(self) -> list[dict[str, Any]]:
|
|
66
|
+
"""Navigate Catalyst Center schema: catalyst_center.inventory.devices[].
|
|
67
|
+
|
|
68
|
+
Traverses the Catalyst Center data model structure to find all
|
|
69
|
+
managed devices in the inventory.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of device dictionaries from the inventory.
|
|
73
|
+
"""
|
|
74
|
+
devices: list[dict[str, Any]] = []
|
|
75
|
+
cc_data = self.data_model.get("catalyst_center", {})
|
|
76
|
+
inventory = cc_data.get("inventory", {})
|
|
77
|
+
devices.extend(inventory.get("devices", []))
|
|
78
|
+
return devices
|
|
79
|
+
|
|
80
|
+
def validate_device_data(self, device_data: dict[str, Any]) -> None:
|
|
81
|
+
"""Validate device state before extraction.
|
|
82
|
+
|
|
83
|
+
Skip devices with INIT or PNP states as they are not fully provisioned.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
device_data: Device data dictionary from the data model.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If the device has an unsupported state (INIT, PNP).
|
|
90
|
+
"""
|
|
91
|
+
state = device_data.get("state", "").upper()
|
|
92
|
+
if state in ("INIT", "PNP"):
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Device has unsupported state '{state}' "
|
|
95
|
+
"(devices in INIT or PNP state are not fully provisioned)"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def extract_hostname(self, device_data: dict[str, Any]) -> str:
|
|
99
|
+
"""Extract hostname from the 'name' field.
|
|
100
|
+
|
|
101
|
+
Uses the device name as the hostname for SSH connections.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
device_data: Device data dictionary from the data model.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Device hostname string.
|
|
108
|
+
"""
|
|
109
|
+
name = device_data.get("name")
|
|
110
|
+
if not name:
|
|
111
|
+
raise ValueError("Device missing 'name' field")
|
|
112
|
+
return str(name)
|
|
113
|
+
|
|
114
|
+
def extract_host_ip(self, device_data: dict[str, Any]) -> str:
|
|
115
|
+
"""Extract management IP from device_ip field.
|
|
116
|
+
|
|
117
|
+
Reads the device_ip field directly from the device data.
|
|
118
|
+
Handles CIDR notation if present (e.g., "10.1.1.100/32" -> "10.1.1.100").
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
device_data: Device data dictionary from the data model.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
IP address string without CIDR notation (e.g., "192.168.38.1").
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If device_ip field is not found.
|
|
128
|
+
"""
|
|
129
|
+
device_ip = device_data.get("device_ip")
|
|
130
|
+
if not device_ip:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"Device missing 'device_ip' field. "
|
|
133
|
+
"Ensure device_ip is configured in the inventory."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
ip_value = str(device_ip)
|
|
137
|
+
|
|
138
|
+
# Strip CIDR notation if present
|
|
139
|
+
if "/" in ip_value:
|
|
140
|
+
ip_value = ip_value.split("/")[0]
|
|
141
|
+
|
|
142
|
+
return ip_value
|
|
143
|
+
|
|
144
|
+
def extract_os_platform_type(self, device_data: dict[str, Any]) -> dict[str, str]:
|
|
145
|
+
"""Return PyATS abstraction info for Catalyst Center managed devices.
|
|
146
|
+
|
|
147
|
+
All managed devices are currently IOS-XE based. Platform and model
|
|
148
|
+
could potentially be extracted from the 'pid' field in the future.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
device_data: Device data dictionary from the data model.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dictionary with 'os' key (and potentially platform/model in future).
|
|
155
|
+
"""
|
|
156
|
+
return {
|
|
157
|
+
"os": "iosxe",
|
|
158
|
+
# Future enhancement: extract platform/model from pid field
|
|
159
|
+
# e.g., "C9300-24P" -> platform="cat9k", model="c9300"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
def get_credential_env_vars(self) -> tuple[str, str]:
|
|
163
|
+
"""Return IOS-XE credential env vars for managed devices.
|
|
164
|
+
|
|
165
|
+
Catalyst Center D2D tests connect to IOS-XE devices,
|
|
166
|
+
NOT the Catalyst Center controller.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple of (username_env_var, password_env_var).
|
|
170
|
+
"""
|
|
171
|
+
return ("IOSXE_USERNAME", "IOSXE_PASSWORD")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
# Copyright (c) 2025 Daniel Schmidt
|
|
3
|
+
|
|
4
|
+
"""Catalyst Center specific base test class for SSH/Direct-to-Device testing.
|
|
5
|
+
|
|
6
|
+
This module provides the CatalystCenterSSHTestBase class, which extends the generic
|
|
7
|
+
SSHTestBase to add Catalyst Center-specific functionality for device-to-device (D2D)
|
|
8
|
+
testing.
|
|
9
|
+
|
|
10
|
+
The class delegates device inventory resolution to CatalystCenterDeviceResolver, which
|
|
11
|
+
handles all Catalyst Center schema navigation and credential injection.
|
|
12
|
+
|
|
13
|
+
Credentials:
|
|
14
|
+
Catalyst Center D2D tests connect to IOS-XE devices managed by Catalyst Center,
|
|
15
|
+
NOT the Catalyst Center controller. Set these environment variables:
|
|
16
|
+
- IOSXE_USERNAME: SSH username for managed devices
|
|
17
|
+
- IOSXE_PASSWORD: SSH password for managed devices
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from nac_test.pyats_core.common.ssh_base_test import (
|
|
25
|
+
SSHTestBase, # type: ignore[import-untyped]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .device_resolver import CatalystCenterDeviceResolver
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CatalystCenterSSHTestBase(SSHTestBase): # type: ignore[misc]
|
|
34
|
+
"""Catalyst Center-specific base test class for SSH/D2D testing.
|
|
35
|
+
|
|
36
|
+
This class extends SSHTestBase and implements the contract required by
|
|
37
|
+
nac-test's SSH execution engine. Device inventory resolution is fully
|
|
38
|
+
delegated to CatalystCenterDeviceResolver.
|
|
39
|
+
|
|
40
|
+
Credentials:
|
|
41
|
+
Catalyst Center D2D tests require IOSXE_USERNAME and IOSXE_PASSWORD
|
|
42
|
+
environment variables (NOT CC_* which are for the controller).
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
class MyCatalystCenterSSHTest(CatalystCenterSSHTestBase):
|
|
46
|
+
@aetest.test
|
|
47
|
+
def verify_device_connectivity(self, steps, device):
|
|
48
|
+
# SSH-based verification logic here
|
|
49
|
+
pass
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Class-level storage for the last resolver instance
|
|
53
|
+
# This allows nac-test to access skipped_devices after calling
|
|
54
|
+
# get_ssh_device_inventory()
|
|
55
|
+
_last_resolver: "CatalystCenterDeviceResolver | None" = None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_ssh_device_inventory(
|
|
59
|
+
cls, data_model: dict[str, Any]
|
|
60
|
+
) -> list[dict[str, Any]]:
|
|
61
|
+
"""Parse the Catalyst Center data model to retrieve the device inventory.
|
|
62
|
+
|
|
63
|
+
This method is the entry point called by nac-test's orchestrator.
|
|
64
|
+
All device inventory resolution is delegated to CatalystCenterDeviceResolver,
|
|
65
|
+
which handles:
|
|
66
|
+
- Schema navigation (catalyst_center.inventory.devices[])
|
|
67
|
+
- Device metadata extraction (name, device_ip, etc.)
|
|
68
|
+
- Credential injection (IOSXE_USERNAME, IOSXE_PASSWORD)
|
|
69
|
+
|
|
70
|
+
After calling this method, access cls._last_resolver.skipped_devices
|
|
71
|
+
to get information about devices that failed resolution.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
data_model: The merged data model from nac-test containing all
|
|
75
|
+
configuration data with resolved variables.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of device dictionaries, each containing:
|
|
79
|
+
- hostname (str): Device hostname
|
|
80
|
+
- host (str): Management IP address for SSH connection
|
|
81
|
+
- os (str): Operating system type (e.g., "iosxe")
|
|
82
|
+
- username (str): SSH username from IOSXE_USERNAME
|
|
83
|
+
- password (str): SSH password from IOSXE_PASSWORD
|
|
84
|
+
- device_id (str): Device identifier (name)
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If IOSXE_USERNAME or IOSXE_PASSWORD env vars are not set.
|
|
88
|
+
"""
|
|
89
|
+
logger.info(
|
|
90
|
+
"CatalystCenterSSHTestBase: Resolving device inventory via "
|
|
91
|
+
"CatalystCenterDeviceResolver"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
cls._last_resolver = CatalystCenterDeviceResolver(data_model)
|
|
95
|
+
return cls._last_resolver.get_resolved_inventory()
|
|
96
|
+
|
|
97
|
+
def get_device_credentials(self, device: dict[str, Any]) -> dict[str, str | None]:
|
|
98
|
+
"""Get Catalyst Center managed device SSH credentials from env vars.
|
|
99
|
+
|
|
100
|
+
Catalyst Center D2D tests connect to IOS-XE devices managed by
|
|
101
|
+
Catalyst Center, NOT the Catalyst Center controller. Use IOSXE_*
|
|
102
|
+
environment variables.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
device: Device dictionary (not used - all devices share credentials).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary containing:
|
|
109
|
+
- username (str | None): SSH username from IOSXE_USERNAME
|
|
110
|
+
- password (str | None): SSH password from IOSXE_PASSWORD
|
|
111
|
+
"""
|
|
112
|
+
return {
|
|
113
|
+
"username": os.environ.get("IOSXE_USERNAME"),
|
|
114
|
+
"password": os.environ.get("IOSXE_PASSWORD"),
|
|
115
|
+
}
|