nac-test-pyats-common 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (39) hide show
  1. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/PKG-INFO +2 -1
  2. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/pyproject.toml +6 -2
  3. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/__init__.py +11 -1
  4. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/aci/__init__.py +3 -0
  5. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/aci/auth.py +8 -2
  6. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/aci/test_base.py +12 -3
  7. nac_test_pyats_common-0.2.0/src/nac_test_pyats_common/catc/__init__.py +56 -0
  8. nac_test_pyats_common-0.1.0/src/nac_test_pyats_common/catc/test_base.py → nac_test_pyats_common-0.2.0/src/nac_test_pyats_common/catc/api_test_base.py +12 -3
  9. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/catc/auth.py +18 -6
  10. nac_test_pyats_common-0.2.0/src/nac_test_pyats_common/catc/device_resolver.py +164 -0
  11. nac_test_pyats_common-0.2.0/src/nac_test_pyats_common/catc/ssh_test_base.py +115 -0
  12. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/common/__init__.py +3 -0
  13. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/common/base_device_resolver.py +83 -176
  14. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/iosxe/__init__.py +3 -0
  15. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/iosxe/iosxe_resolver.py +3 -0
  16. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/iosxe/registry.py +15 -5
  17. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/iosxe/test_base.py +12 -1
  18. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/sdwan/__init__.py +5 -1
  19. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/sdwan/api_test_base.py +25 -15
  20. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/sdwan/auth.py +17 -6
  21. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/sdwan/device_resolver.py +59 -22
  22. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/sdwan/ssh_test_base.py +20 -11
  23. nac_test_pyats_common-0.2.0/tests/__init__.py +4 -0
  24. nac_test_pyats_common-0.2.0/tests/unit/__init__.py +4 -0
  25. nac_test_pyats_common-0.2.0/tests/unit/catc/__init__.py +4 -0
  26. nac_test_pyats_common-0.2.0/tests/unit/catc/test_auth.py +344 -0
  27. nac_test_pyats_common-0.2.0/tests/unit/catc/test_device_resolver.py +533 -0
  28. nac_test_pyats_common-0.2.0/tests/unit/iosxe/__init__.py +4 -0
  29. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/tests/unit/iosxe/test_registry.py +3 -0
  30. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/tests/unit/test_base_device_resolver.py +590 -371
  31. nac_test_pyats_common-0.1.0/src/nac_test_pyats_common/catc/__init__.py +0 -35
  32. nac_test_pyats_common-0.1.0/src/nac_test_pyats_common/iosxe/catc_resolver.py +0 -64
  33. nac_test_pyats_common-0.1.0/tests/__init__.py +0 -1
  34. nac_test_pyats_common-0.1.0/tests/unit/__init__.py +0 -1
  35. nac_test_pyats_common-0.1.0/tests/unit/iosxe/__init__.py +0 -1
  36. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/.gitignore +0 -0
  37. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/LICENSE +0 -0
  38. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/README.md +0 -0
  39. {nac_test_pyats_common-0.1.0 → nac_test_pyats_common-0.2.0}/src/nac_test_pyats_common/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nac-test-pyats-common
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Architecture adapters for Network as Code (NaC) PyATS testing - auth classes, test base classes, and device resolvers
5
5
  Project-URL: Homepage, https://github.com/netascode/nac-test-pyats-common
6
6
  Project-URL: Documentation, https://github.com/netascode/nac-test-pyats-common
@@ -24,6 +24,7 @@ Classifier: Topic :: Software Development :: Testing
24
24
  Classifier: Topic :: System :: Networking
25
25
  Requires-Python: >=3.10
26
26
  Requires-Dist: httpx>=0.28
27
+ Requires-Dist: nac-test==1.1.0b2
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: bandit[toml]>=1.8.6; extra == 'dev'
29
30
  Requires-Dist: mypy>=1.10; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nac-test-pyats-common"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Architecture adapters for Network as Code (NaC) PyATS testing - auth classes, test base classes, and device resolvers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -28,6 +28,7 @@ keywords = ["cisco", "network", "testing", "pyats", "nac", "aci", "sdwan", "cata
28
28
 
29
29
  dependencies = [
30
30
  "httpx>=0.28", # HTTP client for auth
31
+ "nac-test==1.1.0b2", # Pinned until nac-test stable release includes pyats functionality
31
32
  ]
32
33
 
33
34
  [project.optional-dependencies]
@@ -53,6 +54,9 @@ Issues = "https://github.com/netascode/nac-test-pyats-common/issues"
53
54
  requires = ["hatchling"]
54
55
  build-backend = "hatchling.build"
55
56
 
57
+ [tool.hatch.metadata]
58
+ allow-direct-references = true
59
+
56
60
  [tool.hatch.build.targets.wheel]
57
61
  packages = ["src/nac_test_pyats_common"]
58
62
 
@@ -128,7 +132,7 @@ markers = [
128
132
 
129
133
  [tool.bandit]
130
134
  exclude_dirs = ["tests"]
131
- skips = ["B101"]
135
+ skips = ["B101", "B501"]
132
136
 
133
137
  [tool.coverage.run]
134
138
  source = ["src/nac_test_pyats_common"]
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """NAC PyATS Common - Architecture adapters for NAC PyATS testing.
2
5
 
3
6
  This package provides architecture-specific authentication, test base classes,
@@ -26,7 +29,12 @@ __version__ = "1.0.0"
26
29
 
27
30
  # Public API - Import from subpackages
28
31
  from nac_test_pyats_common.aci import APICAuth, APICTestBase
29
- from nac_test_pyats_common.catc import CatalystCenterAuth, CatalystCenterTestBase
32
+ from nac_test_pyats_common.catc import (
33
+ CatalystCenterAuth,
34
+ CatalystCenterDeviceResolver,
35
+ CatalystCenterSSHTestBase,
36
+ CatalystCenterTestBase,
37
+ )
30
38
  from nac_test_pyats_common.sdwan import (
31
39
  SDWANDeviceResolver,
32
40
  SDWANManagerAuth,
@@ -46,4 +54,6 @@ __all__ = [
46
54
  # Catalyst Center
47
55
  "CatalystCenterAuth",
48
56
  "CatalystCenterTestBase",
57
+ "CatalystCenterSSHTestBase",
58
+ "CatalystCenterDeviceResolver",
49
59
  ]
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """ACI adapter module for PyATS common utilities.
2
5
 
3
6
  This module provides the core ACI-specific components for PyATS testing, including
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """APIC authentication module for Cisco ACI (Application Centric Infrastructure).
2
5
 
3
6
  This module provides authentication functionality for Cisco APIC (Application Policy
@@ -14,7 +17,9 @@ re-authenticating when necessary, reducing unnecessary API calls to the APIC con
14
17
  """
15
18
 
16
19
  import httpx
17
- from nac_test.pyats_core.common.auth_cache import AuthCache # type: ignore[import-untyped]
20
+ from nac_test.pyats_core.common.auth_cache import (
21
+ AuthCache, # type: ignore[import-untyped]
22
+ )
18
23
 
19
24
  # Default token lifetime for APIC authentication tokens in seconds
20
25
  # APIC tokens are typically valid for 10 minutes (600 seconds) by default
@@ -82,7 +87,8 @@ class APICAuth:
82
87
  response.raise_for_status()
83
88
 
84
89
  # Parse the APIC response and extract the token
85
- # Response structure: {"imdata": [{"aaaLogin": {"attributes": {"token": "..."}}}]}
90
+ # Response structure:
91
+ # {"imdata": [{"aaaLogin": {"attributes": {"token": "..."}}}]}
86
92
  try:
87
93
  response_data = response.json()
88
94
  token = response_data["imdata"][0]["aaaLogin"]["attributes"]["token"]
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """APIC-specific base test class for ACI API testing.
2
5
 
3
6
  This module provides the APICTestBase class, which extends the generic NACTestBase
@@ -13,7 +16,9 @@ import asyncio
13
16
  from typing import Any
14
17
 
15
18
  import httpx
16
- from nac_test.pyats_core.common.base_test import NACTestBase # type: ignore[import-untyped]
19
+ from nac_test.pyats_core.common.base_test import (
20
+ NACTestBase, # type: ignore[import-untyped]
21
+ )
17
22
  from pyats import aetest # type: ignore[import-untyped]
18
23
 
19
24
  from .auth import APICAuth
@@ -69,7 +74,9 @@ class APICTestBase(NACTestBase): # type: ignore[misc]
69
74
  super().setup()
70
75
 
71
76
  # Get shared APIC token using file-based locking
72
- self.token = APICAuth.get_token(self.controller_url, self.username, self.password)
77
+ self.token = APICAuth.get_token(
78
+ self.controller_url, self.username, self.password
79
+ )
73
80
 
74
81
  # Store the APIC client for use in verification methods
75
82
  self.client = self.get_apic_client()
@@ -94,7 +101,9 @@ class APICTestBase(NACTestBase): # type: ignore[misc]
94
101
  """
95
102
  headers = {"Cookie": f"APIC-cookie={self.token}"}
96
103
  # SSL verification disabled for lab environment compatibility
97
- client = self.pool.get_client(base_url=self.controller_url, headers=headers, verify=False)
104
+ client = self.pool.get_client(
105
+ base_url=self.controller_url, headers=headers, verify=False
106
+ )
98
107
 
99
108
  # Use the generic tracking wrapper from base class
100
109
  return self.wrap_client_for_tracking(client, device_name="APIC") # type: ignore[no-any-return]
@@ -0,0 +1,56 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
4
+ """Catalyst Center adapter module for NAC PyATS testing.
5
+
6
+ This module provides Catalyst Center-specific authentication, test base classes, and
7
+ device resolver implementations for use with the nac-test framework. It includes support
8
+ for both Catalyst Center API testing and SSH-based device-to-device (D2D) testing.
9
+
10
+ Classes:
11
+ CatalystCenterAuth: Token-based authentication with automatic endpoint
12
+ detection.
13
+ CatalystCenterTestBase: Base class for Catalyst Center API tests with
14
+ tracking.
15
+ CatalystCenterSSHTestBase: Base class for Catalyst Center SSH/D2D tests
16
+ with device inventory.
17
+ CatalystCenterDeviceResolver: Resolves device information from the
18
+ Catalyst Center data model.
19
+
20
+ Example:
21
+ For Catalyst Center API testing:
22
+
23
+ >>> from nac_test_pyats_common.catc import CatalystCenterTestBase
24
+ >>>
25
+ >>> class VerifyNetworkDevices(CatalystCenterTestBase):
26
+ ... async def get_items_to_verify(self):
27
+ ... return ['device-uuid-1', 'device-uuid-2']
28
+ ...
29
+ ... async def verify_item(self, item):
30
+ ... response = await self.client.get(
31
+ ... f"/dna/intent/api/v1/network-device/{item}"
32
+ ... )
33
+ ... return response.status_code == 200
34
+
35
+ For SSH/D2D testing:
36
+
37
+ >>> from nac_test_pyats_common.catc import CatalystCenterSSHTestBase
38
+ >>>
39
+ >>> class VerifyInterfaceStatus(CatalystCenterSSHTestBase):
40
+ ... @aetest.test
41
+ ... def verify_interfaces(self, steps, device):
42
+ ... # SSH-based verification
43
+ ... pass
44
+ """
45
+
46
+ from .api_test_base import CatalystCenterTestBase
47
+ from .auth import CatalystCenterAuth
48
+ from .device_resolver import CatalystCenterDeviceResolver
49
+ from .ssh_test_base import CatalystCenterSSHTestBase
50
+
51
+ __all__ = [
52
+ "CatalystCenterAuth",
53
+ "CatalystCenterTestBase",
54
+ "CatalystCenterSSHTestBase",
55
+ "CatalystCenterDeviceResolver",
56
+ ]
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """Catalyst Center-specific base test class for API testing.
2
5
 
3
6
  This module provides the CatalystCenterTestBase class, which extends the generic
@@ -14,7 +17,9 @@ import os
14
17
  from typing import Any
15
18
 
16
19
  import httpx
17
- from nac_test.pyats_core.common.base_test import NACTestBase # type: ignore[import-untyped]
20
+ from nac_test.pyats_core.common.base_test import (
21
+ NACTestBase, # type: ignore[import-untyped]
22
+ )
18
23
  from pyats import aetest # type: ignore[import-untyped]
19
24
 
20
25
  from .auth import CatalystCenterAuth
@@ -52,7 +57,9 @@ class CatalystCenterTestBase(NACTestBase): # type: ignore[misc]
52
57
  return ['device1', 'device2']
53
58
 
54
59
  async def verify_item(self, item):
55
- response = await self.client.get(f"/dna/intent/api/v1/network-device/{item}")
60
+ response = await self.client.get(
61
+ f"/dna/intent/api/v1/network-device/{item}"
62
+ )
56
63
  return response.status_code == 200
57
64
 
58
65
  @aetest.test
@@ -90,7 +97,9 @@ class CatalystCenterTestBase(NACTestBase): # type: ignore[misc]
90
97
  self.client = self.get_catc_client()
91
98
 
92
99
  def get_catc_client(self) -> httpx.AsyncClient:
93
- """Get an httpx async client configured for Catalyst Center with response tracking.
100
+ """Get an httpx async client configured for Catalyst Center.
101
+
102
+ Configured with response tracking.
94
103
 
95
104
  Creates an HTTP client specifically configured for Catalyst Center API
96
105
  communication with authentication headers, base URL, and automatic response
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """Catalyst Center-specific authentication implementation.
2
5
 
3
6
  This module provides authentication functionality for Cisco Catalyst Center
@@ -5,7 +8,8 @@ This module provides authentication functionality for Cisco Catalyst Center
5
8
  networks. The authentication mechanism uses token-based login with Basic Auth.
6
9
 
7
10
  The module implements a two-tier API design:
8
- 1. _authenticate() - Low-level method that performs direct Catalyst Center authentication
11
+ 1. _authenticate() - Low-level method that performs direct Catalyst Center
12
+ authentication
9
13
  2. get_auth() - High-level method that leverages caching for efficient token reuse
10
14
 
11
15
  This design ensures efficient token management by reusing valid tokens and only
@@ -16,7 +20,9 @@ import os
16
20
  from typing import Any
17
21
 
18
22
  import httpx
19
- from nac_test.pyats_core.common.auth_cache import AuthCache # type: ignore[import-untyped]
23
+ from nac_test.pyats_core.common.auth_cache import (
24
+ AuthCache, # type: ignore[import-untyped]
25
+ )
20
26
 
21
27
  # Default token lifetime for Catalyst Center authentication in seconds
22
28
  # Catalyst Center tokens are typically valid for 1 hour (3600 seconds) by default
@@ -96,7 +102,9 @@ class CatalystCenterAuth:
96
102
  """
97
103
  last_error: Exception | None = None
98
104
 
99
- with httpx.Client(verify=verify_ssl, timeout=AUTH_REQUEST_TIMEOUT_SECONDS) as client:
105
+ with httpx.Client(
106
+ verify=verify_ssl, timeout=AUTH_REQUEST_TIMEOUT_SECONDS
107
+ ) as client:
100
108
  for endpoint in AUTH_ENDPOINTS:
101
109
  try:
102
110
  auth_response = client.post(
@@ -129,7 +137,8 @@ class CatalystCenterAuth:
129
137
 
130
138
  # All endpoints failed
131
139
  raise RuntimeError(
132
- f"Catalyst Center authentication failed on all endpoints. Last error: {last_error}"
140
+ f"Catalyst Center authentication failed on all endpoints. "
141
+ f"Last error: {last_error}"
133
142
  ) from last_error
134
143
 
135
144
  @classmethod
@@ -148,7 +157,8 @@ class CatalystCenterAuth:
148
157
  CC_URL: Base URL of the Catalyst Center
149
158
  CC_USERNAME: Catalyst Center username for authentication
150
159
  CC_PASSWORD: Catalyst Center password for authentication
151
- CC_INSECURE: Optional. Set to "True" to disable SSL verification (default: True)
160
+ CC_INSECURE: Optional. Set to "True" to disable SSL verification
161
+ (default: True)
152
162
 
153
163
  Returns:
154
164
  A dictionary containing:
@@ -186,7 +196,9 @@ class CatalystCenterAuth:
186
196
  missing_vars.append("CC_USERNAME")
187
197
  if not password:
188
198
  missing_vars.append("CC_PASSWORD")
189
- raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
199
+ raise ValueError(
200
+ f"Missing required environment variables: {', '.join(missing_vars)}"
201
+ )
190
202
 
191
203
  # Normalize URL by removing trailing slash
192
204
  url = url.rstrip("/") # type: ignore[union-attr]
@@ -0,0 +1,164 @@
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_type(self, device_data: dict[str, Any]) -> str:
145
+ """Return 'iosxe' as all managed devices are IOS-XE based.
146
+
147
+ Args:
148
+ device_data: Device data dictionary (unused, OS is hardcoded).
149
+
150
+ Returns:
151
+ Always returns 'iosxe'.
152
+ """
153
+ return "iosxe"
154
+
155
+ def get_credential_env_vars(self) -> tuple[str, str]:
156
+ """Return IOS-XE credential env vars for managed devices.
157
+
158
+ Catalyst Center D2D tests connect to IOS-XE devices,
159
+ NOT the Catalyst Center controller.
160
+
161
+ Returns:
162
+ Tuple of (username_env_var, password_env_var).
163
+ """
164
+ 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
+ }
@@ -1,3 +1,6 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright (c) 2025 Daniel Schmidt
3
+
1
4
  """Common base classes for nac-test-pyats-common.
2
5
 
3
6
  This module provides architecture-agnostic base classes that can be extended