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.
Files changed (119) hide show
  1. testprotocols/__init__.py +217 -0
  2. testprotocols/aftr_gateway.py +22 -0
  3. testprotocols/appliance_nat.py +52 -0
  4. testprotocols/appliance_uplinks.py +32 -0
  5. testprotocols/appliance_vlans.py +50 -0
  6. testprotocols/arp_client.py +26 -0
  7. testprotocols/bgp.py +55 -0
  8. testprotocols/conntrack.py +147 -0
  9. testprotocols/content_filtering.py +47 -0
  10. testprotocols/device_lifecycle.py +49 -0
  11. testprotocols/device_management.py +50 -0
  12. testprotocols/devices/__init__.py +46 -0
  13. testprotocols/devices/base.py +40 -0
  14. testprotocols/devices/client.py +133 -0
  15. testprotocols/devices/cpe.py +66 -0
  16. testprotocols/devices/infra.py +62 -0
  17. testprotocols/devices/sdwan.py +97 -0
  18. testprotocols/devices/switch.py +115 -0
  19. testprotocols/devices/traffic.py +53 -0
  20. testprotocols/devices/voice.py +69 -0
  21. testprotocols/devices/wan.py +60 -0
  22. testprotocols/dhcp_client.py +30 -0
  23. testprotocols/dhcp_server.py +23 -0
  24. testprotocols/discovery.py +20 -0
  25. testprotocols/dns_client.py +23 -0
  26. testprotocols/file_transfer.py +22 -0
  27. testprotocols/firewall.py +121 -0
  28. testprotocols/firewall_zones.py +133 -0
  29. testprotocols/first_hop_security.py +52 -0
  30. testprotocols/gateway_redundancy.py +29 -0
  31. testprotocols/http_client.py +36 -0
  32. testprotocols/http_server.py +22 -0
  33. testprotocols/hw_console.py +48 -0
  34. testprotocols/infra_controller.py +28 -0
  35. testprotocols/interface_dhcp.py +30 -0
  36. testprotocols/ip_interface.py +62 -0
  37. testprotocols/ip_routing.py +57 -0
  38. testprotocols/iperf_client.py +47 -0
  39. testprotocols/iperf_generator.py +42 -0
  40. testprotocols/iperf_server.py +41 -0
  41. testprotocols/l3_firewall.py +74 -0
  42. testprotocols/l7_firewall.py +32 -0
  43. testprotocols/link_aggregation.py +24 -0
  44. testprotocols/mac_table.py +20 -0
  45. testprotocols/models/__init__.py +304 -0
  46. testprotocols/models/dhcp.py +28 -0
  47. testprotocols/models/firewall.py +197 -0
  48. testprotocols/models/impairment.py +18 -0
  49. testprotocols/models/l2_common.py +53 -0
  50. testprotocols/models/multicast.py +22 -0
  51. testprotocols/models/networking.py +50 -0
  52. testprotocols/models/packets.py +21 -0
  53. testprotocols/models/qoe.py +31 -0
  54. testprotocols/models/radius.py +63 -0
  55. testprotocols/models/sdwan_appliance.py +637 -0
  56. testprotocols/models/switch.py +297 -0
  57. testprotocols/models/switch_routing.py +122 -0
  58. testprotocols/models/tr069.py +35 -0
  59. testprotocols/models/traffic.py +29 -0
  60. testprotocols/models/wan_edge.py +116 -0
  61. testprotocols/models/wifi.py +183 -0
  62. testprotocols/multicast_client.py +20 -0
  63. testprotocols/nat.py +87 -0
  64. testprotocols/netem_controller.py +42 -0
  65. testprotocols/network_endpoint.py +32 -0
  66. testprotocols/network_probe.py +27 -0
  67. testprotocols/nmap_scanner.py +27 -0
  68. testprotocols/ntp_client.py +26 -0
  69. testprotocols/ntp_config.py +25 -0
  70. testprotocols/ospf.py +24 -0
  71. testprotocols/packet_filter.py +144 -0
  72. testprotocols/pcap_capture.py +39 -0
  73. testprotocols/pdu_controller.py +26 -0
  74. testprotocols/port_poe.py +25 -0
  75. testprotocols/port_security.py +25 -0
  76. testprotocols/port_status.py +23 -0
  77. testprotocols/py.typed +0 -0
  78. testprotocols/qoe_browser.py +62 -0
  79. testprotocols/radius_client.py +78 -0
  80. testprotocols/radius_server.py +130 -0
  81. testprotocols/routed_interfaces.py +29 -0
  82. testprotocols/router.py +53 -0
  83. testprotocols/routing_read.py +22 -0
  84. testprotocols/sdwan_policy_manager.py +64 -0
  85. testprotocols/sip_phone.py +230 -0
  86. testprotocols/sip_server.py +205 -0
  87. testprotocols/site_to_site_vpn.py +61 -0
  88. testprotocols/snmp_client.py +17 -0
  89. testprotocols/spanning_tree.py +37 -0
  90. testprotocols/static_routes.py +47 -0
  91. testprotocols/storm_control.py +24 -0
  92. testprotocols/streaming_server.py +32 -0
  93. testprotocols/switch_acl.py +29 -0
  94. testprotocols/switch_ports.py +28 -0
  95. testprotocols/switch_qos.py +33 -0
  96. testprotocols/switch_vlans.py +34 -0
  97. testprotocols/syslog_config.py +31 -0
  98. testprotocols/tftp_server.py +22 -0
  99. testprotocols/threat_prevention.py +60 -0
  100. testprotocols/tr069_client.py +47 -0
  101. testprotocols/tr069_server.py +151 -0
  102. testprotocols/traffic_shaping.py +54 -0
  103. testprotocols/upnp_client.py +37 -0
  104. testprotocols/vlan_client.py +22 -0
  105. testprotocols/wan_link_admin.py +34 -0
  106. testprotocols/wifi_bss.py +197 -0
  107. testprotocols/wifi_client.py +72 -0
  108. testprotocols/wifi_mesh.py +259 -0
  109. testprotocols/wifi_onboarding.py +105 -0
  110. testprotocols/wifi_radio.py +153 -0
  111. testprotocols/wifi_rf.py +78 -0
  112. testprotocols/wifi_stations.py +59 -0
  113. testprotocols/wifi_transitions.py +112 -0
  114. testprotocols-0.1.0.dist-info/METADATA +29 -0
  115. testprotocols-0.1.0.dist-info/RECORD +119 -0
  116. testprotocols-0.1.0.dist-info/WHEEL +5 -0
  117. testprotocols-0.1.0.dist-info/licenses/LICENSE +201 -0
  118. testprotocols-0.1.0.dist-info/licenses/NOTICE +11 -0
  119. 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
+ ...
@@ -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
+ ...