testprotocols 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.
- testprotocols/__init__.py +217 -0
- testprotocols/aftr_gateway.py +22 -0
- testprotocols/appliance_nat.py +52 -0
- testprotocols/appliance_uplinks.py +32 -0
- testprotocols/appliance_vlans.py +50 -0
- testprotocols/arp_client.py +26 -0
- testprotocols/bgp.py +55 -0
- testprotocols/conntrack.py +147 -0
- testprotocols/content_filtering.py +47 -0
- testprotocols/device_lifecycle.py +49 -0
- testprotocols/device_management.py +50 -0
- testprotocols/devices/__init__.py +46 -0
- testprotocols/devices/base.py +40 -0
- testprotocols/devices/client.py +133 -0
- testprotocols/devices/cpe.py +66 -0
- testprotocols/devices/infra.py +62 -0
- testprotocols/devices/sdwan.py +97 -0
- testprotocols/devices/switch.py +115 -0
- testprotocols/devices/traffic.py +53 -0
- testprotocols/devices/voice.py +69 -0
- testprotocols/devices/wan.py +60 -0
- testprotocols/dhcp_client.py +30 -0
- testprotocols/dhcp_server.py +23 -0
- testprotocols/discovery.py +20 -0
- testprotocols/dns_client.py +23 -0
- testprotocols/file_transfer.py +22 -0
- testprotocols/firewall.py +121 -0
- testprotocols/firewall_zones.py +133 -0
- testprotocols/first_hop_security.py +52 -0
- testprotocols/gateway_redundancy.py +29 -0
- testprotocols/http_client.py +36 -0
- testprotocols/http_server.py +22 -0
- testprotocols/hw_console.py +48 -0
- testprotocols/infra_controller.py +28 -0
- testprotocols/interface_dhcp.py +30 -0
- testprotocols/ip_interface.py +62 -0
- testprotocols/ip_routing.py +57 -0
- testprotocols/iperf_client.py +47 -0
- testprotocols/iperf_generator.py +42 -0
- testprotocols/iperf_server.py +41 -0
- testprotocols/l3_firewall.py +74 -0
- testprotocols/l7_firewall.py +32 -0
- testprotocols/link_aggregation.py +24 -0
- testprotocols/mac_table.py +20 -0
- testprotocols/models/__init__.py +304 -0
- testprotocols/models/dhcp.py +28 -0
- testprotocols/models/firewall.py +197 -0
- testprotocols/models/impairment.py +18 -0
- testprotocols/models/l2_common.py +53 -0
- testprotocols/models/multicast.py +22 -0
- testprotocols/models/networking.py +50 -0
- testprotocols/models/packets.py +21 -0
- testprotocols/models/qoe.py +31 -0
- testprotocols/models/radius.py +63 -0
- testprotocols/models/sdwan_appliance.py +637 -0
- testprotocols/models/switch.py +297 -0
- testprotocols/models/switch_routing.py +122 -0
- testprotocols/models/tr069.py +35 -0
- testprotocols/models/traffic.py +29 -0
- testprotocols/models/wan_edge.py +116 -0
- testprotocols/models/wifi.py +183 -0
- testprotocols/multicast_client.py +20 -0
- testprotocols/nat.py +87 -0
- testprotocols/netem_controller.py +42 -0
- testprotocols/network_endpoint.py +32 -0
- testprotocols/network_probe.py +27 -0
- testprotocols/nmap_scanner.py +27 -0
- testprotocols/ntp_client.py +26 -0
- testprotocols/ntp_config.py +25 -0
- testprotocols/ospf.py +24 -0
- testprotocols/packet_filter.py +144 -0
- testprotocols/pcap_capture.py +39 -0
- testprotocols/pdu_controller.py +26 -0
- testprotocols/port_poe.py +25 -0
- testprotocols/port_security.py +25 -0
- testprotocols/port_status.py +23 -0
- testprotocols/py.typed +0 -0
- testprotocols/qoe_browser.py +62 -0
- testprotocols/radius_client.py +78 -0
- testprotocols/radius_server.py +130 -0
- testprotocols/routed_interfaces.py +29 -0
- testprotocols/router.py +53 -0
- testprotocols/routing_read.py +22 -0
- testprotocols/sdwan_policy_manager.py +64 -0
- testprotocols/sip_phone.py +230 -0
- testprotocols/sip_server.py +205 -0
- testprotocols/site_to_site_vpn.py +61 -0
- testprotocols/snmp_client.py +17 -0
- testprotocols/spanning_tree.py +37 -0
- testprotocols/static_routes.py +47 -0
- testprotocols/storm_control.py +24 -0
- testprotocols/streaming_server.py +32 -0
- testprotocols/switch_acl.py +29 -0
- testprotocols/switch_ports.py +28 -0
- testprotocols/switch_qos.py +33 -0
- testprotocols/switch_vlans.py +34 -0
- testprotocols/syslog_config.py +31 -0
- testprotocols/tftp_server.py +22 -0
- testprotocols/threat_prevention.py +60 -0
- testprotocols/tr069_client.py +47 -0
- testprotocols/tr069_server.py +151 -0
- testprotocols/traffic_shaping.py +54 -0
- testprotocols/upnp_client.py +37 -0
- testprotocols/vlan_client.py +22 -0
- testprotocols/wan_link_admin.py +34 -0
- testprotocols/wifi_bss.py +197 -0
- testprotocols/wifi_client.py +72 -0
- testprotocols/wifi_mesh.py +259 -0
- testprotocols/wifi_onboarding.py +105 -0
- testprotocols/wifi_radio.py +153 -0
- testprotocols/wifi_rf.py +78 -0
- testprotocols/wifi_stations.py +59 -0
- testprotocols/wifi_transitions.py +112 -0
- testprotocols-0.1.0.dist-info/METADATA +29 -0
- testprotocols-0.1.0.dist-info/RECORD +119 -0
- testprotocols-0.1.0.dist-info/WHEEL +5 -0
- testprotocols-0.1.0.dist-info/licenses/LICENSE +201 -0
- testprotocols-0.1.0.dist-info/licenses/NOTICE +11 -0
- testprotocols-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""PDU / Controller template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for Power Distribution Unit controller
|
|
4
|
+
operations including power cycling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class PduController(Protocol):
|
|
14
|
+
"""Abstract contract for PDU controller operations."""
|
|
15
|
+
|
|
16
|
+
def power_on(self) -> bool:
|
|
17
|
+
"""Power on the outlet and return True on success."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def power_off(self) -> bool:
|
|
21
|
+
"""Power off the outlet and return True on success."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def power_cycle(self) -> bool:
|
|
25
|
+
"""Power cycle the outlet and return True on success."""
|
|
26
|
+
...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Per-port PoE configuration and status read."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from testprotocols.models.switch import PoePortStatus, PoePriority
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class PortPoe(Protocol):
|
|
12
|
+
"""Abstract contract for per-port PoE."""
|
|
13
|
+
|
|
14
|
+
def set_enabled(self, port: str, enabled: bool) -> None:
|
|
15
|
+
"""Enable or disable PoE on *port*."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
def set_priority(self, port: str, priority: PoePriority) -> None:
|
|
19
|
+
"""Set PoE priority on *port*. Products without a priority knob raise
|
|
20
|
+
unsupported-capability."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def get_status(self, port: str) -> PoePortStatus:
|
|
24
|
+
"""Return live PoE status/draw for *port*."""
|
|
25
|
+
...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Per-port access policy — 802.1X / MAB / MAC limit / sticky.
|
|
2
|
+
|
|
3
|
+
The access policy references RADIUS servers *by name* from the composed
|
|
4
|
+
``radius`` (``RadiusClient``) registry; address/port/secret resolution is the
|
|
5
|
+
driver's concern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
from testprotocols.models.switch import AccessPolicy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class PortSecurity(Protocol):
|
|
17
|
+
"""Abstract contract for per-port access policy."""
|
|
18
|
+
|
|
19
|
+
def set_access_policy(self, policy: AccessPolicy) -> None:
|
|
20
|
+
"""Apply the access policy for ``policy.port``."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def get_access_policy(self, port: str) -> AccessPolicy:
|
|
24
|
+
"""Return the access policy for *port*."""
|
|
25
|
+
...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Read-only per-port link state, speed/duplex, and counters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from testprotocols.models.switch import PortStatusEntry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class PortStatus(Protocol):
|
|
12
|
+
"""Abstract contract for per-port status read.
|
|
13
|
+
|
|
14
|
+
Replaces the host-shaped ``ip_interface`` read for this archetype.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def list_port_status(self) -> list[PortStatusEntry]:
|
|
18
|
+
"""Return status for every port."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def get_port_status(self, name: str) -> PortStatusEntry:
|
|
22
|
+
"""Return status for port *name*. Raises KeyError if absent."""
|
|
23
|
+
...
|
testprotocols/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Traffic / QoeBrowser template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for browser-based Quality of Experience (QoE)
|
|
4
|
+
measurement operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from testprotocols.models.qoe import MeasurementSpec, QoEResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class QoeBrowser(Protocol):
|
|
16
|
+
"""Abstract contract for browser-based QoE measurement."""
|
|
17
|
+
|
|
18
|
+
def measure(self, url: str, spec: MeasurementSpec) -> QoEResult:
|
|
19
|
+
"""Run a generic QoE measurement against *url* using *spec*."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def measure_productivity(
|
|
23
|
+
self,
|
|
24
|
+
url: str,
|
|
25
|
+
*,
|
|
26
|
+
spec: MeasurementSpec | None = None,
|
|
27
|
+
scenario: str = "page_load",
|
|
28
|
+
wait_until: str = "networkidle",
|
|
29
|
+
timeout_ms: int = 30000,
|
|
30
|
+
) -> QoEResult:
|
|
31
|
+
"""Measure productivity-app QoE for *url* (e.g., page-load time)."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def measure_streaming(
|
|
35
|
+
self,
|
|
36
|
+
stream_url: str,
|
|
37
|
+
*,
|
|
38
|
+
spec: MeasurementSpec | None = None,
|
|
39
|
+
duration_s: int = 30,
|
|
40
|
+
) -> QoEResult:
|
|
41
|
+
"""Measure streaming QoE for *stream_url* over *duration_s* seconds."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def measure_conferencing(
|
|
45
|
+
self,
|
|
46
|
+
session_url: str,
|
|
47
|
+
*,
|
|
48
|
+
spec: MeasurementSpec | None = None,
|
|
49
|
+
duration_s: int = 60,
|
|
50
|
+
) -> QoEResult:
|
|
51
|
+
"""Measure conferencing QoE for *session_url* over *duration_s* seconds."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def attempt_outbound_connection(
|
|
55
|
+
self,
|
|
56
|
+
host: str,
|
|
57
|
+
port: int,
|
|
58
|
+
*,
|
|
59
|
+
timeout_s: float = 5.0,
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Attempt a TCP connection to *host*:*port* and return True if successful."""
|
|
62
|
+
...
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""RADIUS / RadiusClient template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for a device that authenticates upstream
|
|
4
|
+
to one or more RADIUS servers (e.g. a WiFi AP doing 802.1X, a wired
|
|
5
|
+
switch doing port-based authentication, a VPN concentrator).
|
|
6
|
+
|
|
7
|
+
The device maintains a registry of known servers indexed by a logical
|
|
8
|
+
*name*. Consumers (e.g. WifiBss) reference servers by name only — the
|
|
9
|
+
underlying address/port/secret resolution is the driver's responsibility.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from testprotocols.models.radius import RadiusServerConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class RadiusClient(Protocol):
|
|
21
|
+
"""Abstract contract for a RADIUS-authenticating device."""
|
|
22
|
+
|
|
23
|
+
def add_server(
|
|
24
|
+
self,
|
|
25
|
+
name: str,
|
|
26
|
+
address: str,
|
|
27
|
+
secret: str,
|
|
28
|
+
port: int = 1812,
|
|
29
|
+
acct_port: int | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Register a RADIUS server under *name*.
|
|
32
|
+
|
|
33
|
+
*port* is the auth port (default 1812). *acct_port* (typically 1813)
|
|
34
|
+
may be None to disable accounting on this server. Raises ValueError
|
|
35
|
+
if *name* is already registered.
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def update_server(
|
|
40
|
+
self,
|
|
41
|
+
name: str,
|
|
42
|
+
*,
|
|
43
|
+
address: str | None = None,
|
|
44
|
+
secret: str | None = None,
|
|
45
|
+
port: int | None = None,
|
|
46
|
+
acct_port: int | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Update one or more fields of an already-registered server.
|
|
49
|
+
|
|
50
|
+
Only fields passed (non-None) are changed. Raises KeyError if *name*
|
|
51
|
+
is not registered.
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def remove_server(self, name: str) -> None:
|
|
56
|
+
"""Unregister the RADIUS server identified by *name*. Raises KeyError if absent."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def list_servers(self) -> list[RadiusServerConfig]:
|
|
60
|
+
"""Return all registered RADIUS servers (without secrets)."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def get_server(self, name: str) -> RadiusServerConfig:
|
|
64
|
+
"""Return the registered RADIUS server identified by *name* (without secret).
|
|
65
|
+
|
|
66
|
+
Raises KeyError if *name* is not registered.
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
def test_server_reachable(self, name: str, timeout: float = 5.0) -> bool:
|
|
71
|
+
"""Probe the registered server: return True if it responds to an Access-Request
|
|
72
|
+
within *timeout* seconds.
|
|
73
|
+
|
|
74
|
+
Drivers typically send an Access-Request with a sentinel user; the
|
|
75
|
+
server's response (Access-Accept, Access-Reject, or any RADIUS reply)
|
|
76
|
+
counts as reachable. No reply within timeout returns False.
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""RADIUS / RadiusServer template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for a controllable RADIUS server used as
|
|
4
|
+
the AAA backend in a testbed (e.g. FreeRADIUS in a container, exercised
|
|
5
|
+
by WPA-Enterprise / 802.1X / VPN scenarios).
|
|
6
|
+
|
|
7
|
+
Tests use this template to provision EAP-capable users, inspect active
|
|
8
|
+
sessions, drive RFC 5176 dynamic-authorization (CoA / Disconnect) flows,
|
|
9
|
+
and read accounting records produced by NAS devices (APs, switches).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from testprotocols.models.radius import (
|
|
17
|
+
RadiusAccountingRecord,
|
|
18
|
+
RadiusSession,
|
|
19
|
+
RadiusUser,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class RadiusServer(Protocol):
|
|
25
|
+
"""Abstract contract for a controllable RADIUS server."""
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
# Lifecycle
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def start(self) -> None:
|
|
32
|
+
"""Start the RADIUS daemon."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def stop(self) -> None:
|
|
36
|
+
"""Stop the RADIUS daemon."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def get_status(self) -> str:
|
|
40
|
+
"""Return the daemon status. Typical values: ``"running"``, ``"stopped"``, ``"error"``."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
# User provisioning
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def add_user(
|
|
48
|
+
self,
|
|
49
|
+
username: str,
|
|
50
|
+
password: str,
|
|
51
|
+
eap_methods: list[str] | None = None,
|
|
52
|
+
attributes: dict[str, str] | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Provision a user authorised to authenticate.
|
|
55
|
+
|
|
56
|
+
*eap_methods* lists the EAP methods this user may use (e.g.
|
|
57
|
+
``["PEAP-MSCHAPv2", "TTLS-PAP"]``); None means the server's default
|
|
58
|
+
method set. *attributes* is a dict of RADIUS attributes returned in
|
|
59
|
+
the Access-Accept reply (e.g. ``{"Tunnel-Private-Group-Id": "42"}``
|
|
60
|
+
for VLAN assignment, ``{"Session-Timeout": "3600"}``).
|
|
61
|
+
|
|
62
|
+
Raises ValueError if *username* is already provisioned.
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def remove_user(self, username: str) -> None:
|
|
67
|
+
"""Remove the provisioned user. Raises KeyError if absent."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
def list_users(self) -> list[RadiusUser]:
|
|
71
|
+
"""Return all provisioned users (without passwords)."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def get_user(self, username: str) -> RadiusUser:
|
|
75
|
+
"""Return the provisioned user (without password). Raises KeyError if absent."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# Active sessions
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def get_active_sessions(self) -> list[RadiusSession]:
|
|
83
|
+
"""Return currently authenticated NAS sessions tracked by the server."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# Dynamic authorization (RFC 5176)
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def send_coa(self, session_id: str, attributes: dict[str, str]) -> bool:
|
|
91
|
+
"""Send a Change-of-Authorization request to the NAS for *session_id*.
|
|
92
|
+
|
|
93
|
+
*attributes* are the RADIUS attributes to apply (e.g. a new VLAN
|
|
94
|
+
assignment, a new Filter-Id). Returns True on CoA-ACK, False on
|
|
95
|
+
CoA-NAK or no response within the driver's timeout.
|
|
96
|
+
"""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
def send_disconnect(self, session_id: str) -> bool:
|
|
100
|
+
"""Send a Disconnect-Message to the NAS for *session_id*.
|
|
101
|
+
|
|
102
|
+
Returns True on Disconnect-ACK, False on Disconnect-NAK or no
|
|
103
|
+
response within the driver's timeout.
|
|
104
|
+
"""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Accounting
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def get_accounting_records(
|
|
112
|
+
self,
|
|
113
|
+
username: str | None = None,
|
|
114
|
+
nas_address: str | None = None,
|
|
115
|
+
since: float | None = None,
|
|
116
|
+
) -> list[RadiusAccountingRecord]:
|
|
117
|
+
"""Return accounting records, optionally filtered.
|
|
118
|
+
|
|
119
|
+
*since* is a Unix timestamp; only records at or after that time are
|
|
120
|
+
returned. Filters compose (AND).
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
def clear_accounting(self) -> None:
|
|
125
|
+
"""Discard all stored accounting records.
|
|
126
|
+
|
|
127
|
+
Tests typically call this at scenario start so subsequent
|
|
128
|
+
``get_accounting_records()`` returns only what the scenario produced.
|
|
129
|
+
"""
|
|
130
|
+
...
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Routed-interface (SVI / routed port / loopback) configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from testprotocols.models.switch_routing import RoutedInterface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class RoutedInterfaces(Protocol):
|
|
12
|
+
"""Abstract contract for L3 interface configuration.
|
|
13
|
+
|
|
14
|
+
Scope is the default VRF; multi-VRF is deferred (GAPS.md). A product whose
|
|
15
|
+
SVIs are gateway-anchored or standalone-only raises unsupported-capability on
|
|
16
|
+
the unsupported facets.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def list_interfaces(self) -> list[RoutedInterface]:
|
|
20
|
+
"""Return every routed interface."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def get_interface(self, name: str) -> RoutedInterface:
|
|
24
|
+
"""Return the routed interface *name*. Raises KeyError if absent."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def set_interface(self, interface: RoutedInterface) -> None:
|
|
28
|
+
"""Create or replace the routed interface ``interface.name``."""
|
|
29
|
+
...
|
testprotocols/router.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Router / WAN edge template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for **reading** a WAN edge router's state:
|
|
4
|
+
interface status, path metrics, link health, telemetry, and the routing
|
|
5
|
+
table. The surface is read-only so it holds for every WAN-edge archetype —
|
|
6
|
+
including API-managed appliances, whose management planes expose reads but
|
|
7
|
+
no link administration. Forced link-down lives on ``wan_link_admin``
|
|
8
|
+
(host-substrate only; see SPLITS.md).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from testprotocols.models.wan_edge import (
|
|
16
|
+
LinkHealthReport,
|
|
17
|
+
LinkStatus,
|
|
18
|
+
PathMetrics,
|
|
19
|
+
RouteEntry,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class Router(Protocol):
|
|
25
|
+
"""Abstract contract for reading WAN edge router state."""
|
|
26
|
+
|
|
27
|
+
def get_active_wan_interface(self, flow_dst: str | None = None) -> str | None:
|
|
28
|
+
"""Return the name of the active WAN interface, or ``None`` if no uplink
|
|
29
|
+
is currently active (for a given ``flow_dst``, if there is no active path
|
|
30
|
+
to it). "No active uplink" is an expected operational state — e.g. while
|
|
31
|
+
a failover test has every uplink impaired — not an error; use
|
|
32
|
+
``get_wan_interface_status`` for per-uplink detail."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def get_wan_interface_status(self) -> dict[str, LinkStatus]:
|
|
36
|
+
"""Return a mapping of WAN interface names to their current link status."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def get_wan_path_metrics(self) -> dict[str, PathMetrics]:
|
|
40
|
+
"""Return a mapping of WAN interface names to measured path metrics."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def get_link_health(self, wan_label: str) -> LinkHealthReport:
|
|
44
|
+
"""Return a comprehensive health report for the named WAN link."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def get_telemetry(self) -> dict[str, Any]:
|
|
48
|
+
"""Return a dict of current device telemetry data."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def get_routing_table(self) -> list[RouteEntry]:
|
|
52
|
+
"""Return the current routing table as a list of RouteEntry records."""
|
|
53
|
+
...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Read-only routing-table (RIB) read for a switch — RouteEntry, shared with Router."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from testprotocols.models.wan_edge import RouteEntry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class RoutingRead(Protocol):
|
|
12
|
+
"""Abstract contract for the switch routing-table read.
|
|
13
|
+
|
|
14
|
+
The config-view read (connected/static/configured routes) is universal; the
|
|
15
|
+
dynamic-learned RIB facet is best-effort — a product that is config-only on
|
|
16
|
+
routing state raises unsupported-capability for the learned routes.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def get_routing_table(self) -> list[RouteEntry]:
|
|
20
|
+
"""Return the routing table as RouteEntry records (origin-classified
|
|
21
|
+
where the product exposes it)."""
|
|
22
|
+
...
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""SD-WAN policy manager template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for SD-WAN path/policy management: generic
|
|
4
|
+
policy application, SLA policies, and application-flow visibility.
|
|
5
|
+
|
|
6
|
+
Firewall-rule administration is **not** here — it moved to the dedicated
|
|
7
|
+
``l3_firewall`` / ``l7_firewall`` capabilities (coherent-domain split; see
|
|
8
|
+
SPLITS.md). Typed path steering (``set_uplink_selection`` /
|
|
9
|
+
``get_uplink_selection`` over ordered ``UplinkSelectionRule``s; performance
|
|
10
|
+
classes reuse ``SLAPolicy`` by name) landed 2026-06-12; ``apply_policy``
|
|
11
|
+
remains the generic escape hatch for vendor-shaped policies beyond that
|
|
12
|
+
surface. Application-match steering grows on evidence.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
from testprotocols.models.sdwan_appliance import UplinkSelectionRule
|
|
20
|
+
from testprotocols.models.wan_edge import AppFlow, SLAPolicy
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class SdwanPolicyManager(Protocol):
|
|
25
|
+
"""Abstract contract for SD-WAN policy management operations."""
|
|
26
|
+
|
|
27
|
+
def apply_policy(self, policy: dict[str, Any]) -> None:
|
|
28
|
+
"""Apply a generic SD-WAN policy specified as a dict."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def remove_policy(self, name: str) -> None:
|
|
32
|
+
"""Remove the SD-WAN policy with the given name."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def configure_sla_policy(self, policy: SLAPolicy) -> None:
|
|
36
|
+
"""Configure an SLA policy on the SD-WAN device."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def remove_sla_policy(self, name: str) -> None:
|
|
40
|
+
"""Remove the SLA policy with the given name."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def get_application_flows(
|
|
44
|
+
self,
|
|
45
|
+
since_s: int = 60,
|
|
46
|
+
app_filter: str | None = None,
|
|
47
|
+
) -> list[AppFlow]:
|
|
48
|
+
"""Return application flows observed in the last *since_s* seconds."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def set_uplink_selection(self, rules: list[UplinkSelectionRule]) -> None:
|
|
52
|
+
"""Replace the ordered uplink-selection rule list with *rules*.
|
|
53
|
+
|
|
54
|
+
The list is the complete steering policy in evaluation order. A rule
|
|
55
|
+
referencing a ``performance_class`` requires the named ``SLAPolicy``
|
|
56
|
+
to be configured (``configure_sla_policy``); products that cannot
|
|
57
|
+
express arbitrary performance thresholds raise unsupported-capability
|
|
58
|
+
for such rules rather than approximating.
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def get_uplink_selection(self) -> list[UplinkSelectionRule]:
|
|
63
|
+
"""Return the uplink-selection rules in evaluation order."""
|
|
64
|
+
...
|