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,230 @@
1
+ """Voice / SipPhone template.
2
+
3
+ Defines the abstract contract for SIP phone operations including call
4
+ state management, dialling, and feature codes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Protocol, runtime_checkable
10
+
11
+
12
+ @runtime_checkable
13
+ class SipPhone(Protocol):
14
+ """Abstract contract for SIP phone operations."""
15
+
16
+ def phone_start(self) -> None:
17
+ """Start the SIP phone software."""
18
+ ...
19
+
20
+ def phone_config(self, ipv6_flag: bool, sipserver_fqdn: str = "") -> None:
21
+ """Configure the SIP phone with the given server FQDN and IP-version flag."""
22
+ ...
23
+
24
+ def phone_kill(self) -> None:
25
+ """Terminate the SIP phone software."""
26
+ ...
27
+
28
+ def on_hook(self) -> None:
29
+ """Place the phone on-hook (hang up)."""
30
+ ...
31
+
32
+ def off_hook(self) -> None:
33
+ """Take the phone off-hook."""
34
+ ...
35
+
36
+ def answer(self) -> bool:
37
+ """Answer an incoming call. Returns True if successful."""
38
+ ...
39
+
40
+ def dial(self, sequence: str) -> None:
41
+ """Dial a DTMF *sequence*."""
42
+ ...
43
+
44
+ def is_idle(self) -> bool:
45
+ """Return True if the phone is in the idle state."""
46
+ ...
47
+
48
+ def is_dialing(self) -> bool:
49
+ """Return True if the phone is currently dialling."""
50
+ ...
51
+
52
+ def is_incall_dialing(self) -> bool:
53
+ """Return True if the phone is dialling while connected to an existing call
54
+ (e.g. entering a transfer target during hold).
55
+ """
56
+ ...
57
+
58
+ def is_ringing(self) -> bool:
59
+ """Return True if the phone is ringing."""
60
+ ...
61
+
62
+ def is_connected(self) -> bool:
63
+ """Return True if the phone has an active call connection."""
64
+ ...
65
+
66
+ def is_incall_connected(self) -> bool:
67
+ """Return True if the phone has an in-progress second leg connected during a
68
+ transfer or consultation.
69
+ """
70
+ ...
71
+
72
+ def is_onhold(self) -> bool:
73
+ """Return True if the current primary call is in a hold
74
+ (re-INVITE sendonly / inactive) state.
75
+ """
76
+ ...
77
+
78
+ def is_playing_dialtone(self) -> bool:
79
+ """Return True if the phone is currently playing a dial tone."""
80
+ ...
81
+
82
+ def is_incall_playing_dialtone(self) -> bool:
83
+ """Return True if the phone is emitting a dial tone during an in-call consultation."""
84
+ ...
85
+
86
+ def is_call_ended(self) -> bool:
87
+ """Return True if the last call has ended."""
88
+ ...
89
+
90
+ def is_code_ended(self) -> bool:
91
+ """Return True if the last feature-code dial sequence has finished
92
+ (no more DTMF digits will be sent).
93
+ """
94
+ ...
95
+
96
+ def is_call_waiting(self) -> bool:
97
+ """Return True if a second incoming call is currently waiting while the
98
+ primary call is connected.
99
+ """
100
+ ...
101
+
102
+ def is_in_conference(self) -> bool:
103
+ """Return True if this phone is currently a participant in a three-way or
104
+ larger conference bridge.
105
+ """
106
+ ...
107
+
108
+ def has_off_hook_warning(self) -> bool:
109
+ """Return True if the phone is playing an off-hook warning tone
110
+ (handset left off-hook without dialling).
111
+ """
112
+ ...
113
+
114
+ def detect_dialtone(self) -> bool:
115
+ """Return True if a dial tone is detected on the line."""
116
+ ...
117
+
118
+ def is_line_busy(self) -> bool:
119
+ """Return True if the line is busy."""
120
+ ...
121
+
122
+ def reply_with_code(self, code: int) -> None:
123
+ """Reply to an incoming call or event with the given SIP response *code*."""
124
+ ...
125
+
126
+ def is_call_not_answered(self) -> bool:
127
+ """Return True if the outgoing call was not answered."""
128
+ ...
129
+
130
+ def answer_waiting_call(self) -> None:
131
+ """Answer the waiting call and place the primary call on hold."""
132
+ ...
133
+
134
+ def toggle_call(self) -> None:
135
+ """Swap which call is active: place the currently-active call on hold and
136
+ resume the held call.
137
+ """
138
+ ...
139
+
140
+ def merge_two_calls(self) -> None:
141
+ """Merge the active and held calls into a single conference bridge."""
142
+ ...
143
+
144
+ def reject_waiting_call(self) -> None:
145
+ """Reject the waiting call with 486 Busy Here; the primary call remains connected."""
146
+ ...
147
+
148
+ def place_call_onhold(self) -> None:
149
+ """Place the active call on hold."""
150
+ ...
151
+
152
+ def place_call_offhold(self) -> None:
153
+ """Resume the held call."""
154
+ ...
155
+
156
+ def press_R_button(self) -> None:
157
+ """Press the R (recall/flash) button on the phone."""
158
+ ...
159
+
160
+ def hook_flash(self) -> None:
161
+ """Send a hook-flash signal."""
162
+ ...
163
+
164
+ def wait_for_state(self, state: str, timeout: int = 10) -> bool:
165
+ """Wait up to *timeout* seconds for the phone to reach *state*.
166
+
167
+ Returns True if the state was reached within the timeout.
168
+ """
169
+ ...
170
+
171
+ def press_buttons(self, buttons: str) -> None:
172
+ """Press the DTMF *buttons* sequence on the phone keypad."""
173
+ ...
174
+
175
+ # ------------------------------------------------------------------
176
+ # MWI / voicemail indicators (v0.2.0+)
177
+ # ------------------------------------------------------------------
178
+
179
+ def has_mwi_indicator(self) -> bool:
180
+ """Return True if the phone's MWI indicator (lamp/display) is active.
181
+
182
+ A conformant driver observes the phone's SIP NOTIFY stream for
183
+ ``message-summary`` events and exposes the current waiting flag here.
184
+ """
185
+ ...
186
+
187
+ def check_voicemail(self) -> None:
188
+ """Dial the voicemail-access feature code (e.g. ``*97``)."""
189
+ ...
190
+
191
+ # ------------------------------------------------------------------
192
+ # Presence (v0.2.0+)
193
+ # ------------------------------------------------------------------
194
+
195
+ def is_away(self) -> bool:
196
+ """Return True if the phone is currently in an away/unavailable presence state."""
197
+ ...
198
+
199
+ def set_presence(self, status: str) -> None:
200
+ """Publish the local presence *status* for this phone.
201
+
202
+ Typical values: ``"online"``, ``"busy"``, ``"away"``, ``"offline"``.
203
+ Implementers emit a SIP PUBLISH with a ``presence`` event package.
204
+ """
205
+ ...
206
+
207
+ # ------------------------------------------------------------------
208
+ # Offline SIP MESSAGE (v0.2.0+)
209
+ # ------------------------------------------------------------------
210
+
211
+ def has_pending_offline_message(self) -> bool:
212
+ """Return True if a stored-while-offline SIP MESSAGE is pending delivery.
213
+
214
+ Expected to flip True after a REGISTER that flushes msilo-stored
215
+ messages, and back to False once the UA has acknowledged them.
216
+ """
217
+ ...
218
+
219
+
220
+ @runtime_checkable
221
+ class SipPhoneWhiteBox(SipPhone, Protocol):
222
+ """Deep introspection capabilities for SIP phones."""
223
+
224
+ def has_rtp_udp_bindings(self) -> bool:
225
+ """Return True if the underlying OS has active UDP sockets in the RTP port range.
226
+
227
+ This is a diagnostic white-box check, typically used when testing in
228
+ --null-audio simulated environments where real media analysis is impossible.
229
+ """
230
+ ...
@@ -0,0 +1,205 @@
1
+ """Voice / SipServer template.
2
+
3
+ Defines the abstract contract for SIP server operations including user
4
+ management, call tracking and SIP message verification.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Protocol, runtime_checkable
10
+
11
+
12
+ @runtime_checkable
13
+ class SipServer(Protocol):
14
+ """Abstract contract for SIP server operations."""
15
+
16
+ # ------------------------------------------------------------------
17
+ # Abstract properties
18
+ # ------------------------------------------------------------------
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ """Human-readable name of this SIP server instance."""
23
+ ...
24
+
25
+ @property
26
+ def ipv4_addr(self) -> str | None:
27
+ """IPv4 address of the SIP server, or None if not configured."""
28
+ ...
29
+
30
+ @property
31
+ def ipv6_addr(self) -> str | None:
32
+ """IPv6 address of the SIP server, or None if not configured."""
33
+ ...
34
+
35
+ # ------------------------------------------------------------------
36
+ # Abstract methods
37
+ # ------------------------------------------------------------------
38
+
39
+ def start(self) -> None:
40
+ """Start the SIP server process."""
41
+ ...
42
+
43
+ def stop(self) -> None:
44
+ """Stop the SIP server process."""
45
+ ...
46
+
47
+ def restart(self) -> None:
48
+ """Restart the SIP server process."""
49
+ ...
50
+
51
+ def get_status(self) -> str:
52
+ """Return a string describing the current SIP server status."""
53
+ ...
54
+
55
+ def get_online_users(self) -> str:
56
+ """Return a string listing currently registered/online SIP users."""
57
+ ...
58
+
59
+ def add_user(self, user: str, password: str | None = None) -> None:
60
+ """Add a SIP *user* account, optionally with a *password*."""
61
+ ...
62
+
63
+ def remove_endpoint(self, endpoint: str) -> None:
64
+ """Remove the SIP *endpoint* (user or device) from the server."""
65
+ ...
66
+
67
+ def allocate_number(self, number: str | None = None) -> str:
68
+ """Allocate a DID *number* on the server and return the allocated number."""
69
+ ...
70
+
71
+ def get_expire_timer(self) -> int:
72
+ """Return the current SIP registration expire timer value (seconds)."""
73
+ ...
74
+
75
+ def set_expire_timer(self, to_timer: int = 60) -> None:
76
+ """Set the SIP registration expire timer to *to_timer* seconds."""
77
+ ...
78
+
79
+ def get_active_calls(self) -> int:
80
+ """Return the exact number of currently active dialogs on the server.
81
+
82
+ Implementations MUST use a dialog-module backed query (e.g. kamailio
83
+ ``kamcmd dlg.stats_active``). The pre-v0.2.0 "best-effort heuristic"
84
+ based on counting registered contacts is no longer acceptable;
85
+ a driver paired with a v1.3.0+ sipcenter image has access to real
86
+ dialog state and must use it.
87
+ """
88
+ ...
89
+
90
+ def get_rtpengine_stats(self) -> dict[str, Any]:
91
+ """Return RTPEngine statistics as a dictionary."""
92
+ ...
93
+
94
+ def verify_sip_message(
95
+ self,
96
+ message_type: str,
97
+ since: Any = None,
98
+ timeout: int = 5,
99
+ ) -> bool:
100
+ """Verify that a SIP message of *message_type* was received.
101
+
102
+ Implementations MUST consult an authoritative log channel (e.g.
103
+ the sipcenter's ``/var/log/kamailio/kamailio.log`` written by
104
+ rsyslog, or the merged testbed log at ``raikou/logs/sip-testbed.log``).
105
+ The pre-v0.2.0 ``journalctl``-based probe is dead — the image does
106
+ not run systemd — and any driver still relying on it must switch
107
+ to the authoritative file path.
108
+
109
+ Parameters
110
+ ----------
111
+ message_type:
112
+ The SIP method (``INVITE``, ``MESSAGE``, ``NOTIFY``, ...) or
113
+ response code (``200``, ``486``, ...) to match.
114
+ since:
115
+ Optional timestamp or marker; only messages after this point
116
+ are considered.
117
+ timeout:
118
+ Seconds to wait for the expected message.
119
+ """
120
+ ...
121
+
122
+ # ------------------------------------------------------------------
123
+ # Voicemail (v0.2.0+)
124
+ # ------------------------------------------------------------------
125
+
126
+ def get_voicemail_count(self, user: str) -> int:
127
+ """Return the number of unheard voicemail messages waiting for *user*.
128
+
129
+ Returns 0 if the user exists but has no messages. Raises when *user*
130
+ has no mailbox provisioned on the server.
131
+ """
132
+ ...
133
+
134
+ def clear_voicemail(self, user: str) -> None:
135
+ """Delete all voicemail messages (heard and unheard) for *user*."""
136
+ ...
137
+
138
+ # ------------------------------------------------------------------
139
+ # MWI — Message Waiting Indication (v0.2.0+)
140
+ # ------------------------------------------------------------------
141
+
142
+ def get_mwi_status(self, user: str) -> dict[str, Any]:
143
+ """Return the current MWI status for *user*.
144
+
145
+ Returns a dict with keys:
146
+ - ``waiting`` (bool): True if the waiting flag is set.
147
+ - ``new`` (int): count of new (unheard) messages.
148
+ - ``old`` (int): count of old (heard but retained) messages.
149
+ """
150
+ ...
151
+
152
+ def set_mwi_status(self, user: str, waiting: bool) -> None:
153
+ """Set the MWI waiting flag for *user*.
154
+
155
+ Implementers should emit a ``message-summary`` NOTIFY to any
156
+ subscribed UA so the phone's indicator updates immediately.
157
+ """
158
+ ...
159
+
160
+ # ------------------------------------------------------------------
161
+ # Presence (v0.2.0+)
162
+ # ------------------------------------------------------------------
163
+
164
+ def get_user_presence(self, user: str) -> str:
165
+ """Return the current presence status string for *user*.
166
+
167
+ Typical values: ``"online"``, ``"busy"``, ``"away"``, ``"offline"``.
168
+ Implementations may return provider-specific extensions.
169
+ """
170
+ ...
171
+
172
+ def subscribe_to_user(self, watcher: str, watched: str) -> None:
173
+ """Create a presence subscription from *watcher* to *watched*."""
174
+ ...
175
+
176
+ def notify_presence(self, user: str, status: str) -> None:
177
+ """Publish presence *status* for *user* to all current subscribers."""
178
+ ...
179
+
180
+ # ------------------------------------------------------------------
181
+ # Offline SIP MESSAGE (v0.2.0+)
182
+ # ------------------------------------------------------------------
183
+
184
+ def send_offline_message(self, from_user: str, to_user: str, body: str) -> bool:
185
+ """Store a SIP MESSAGE from *from_user* to the offline *to_user*.
186
+
187
+ Returns True if the message was stored for later delivery. The
188
+ message is expected to flush on the next successful REGISTER from
189
+ *to_user*.
190
+ """
191
+ ...
192
+
193
+ def get_offline_messages(self, user: str) -> list[dict[str, Any]]:
194
+ """Return the list of pending offline messages addressed to *user*.
195
+
196
+ Each entry is a dict with keys:
197
+ - ``from`` (str): sender URI.
198
+ - ``body`` (str): message body.
199
+ - ``timestamp`` (str): ISO-8601 timestamp of when stored.
200
+ """
201
+ ...
202
+
203
+ def clear_offline_messages(self, user: str) -> None:
204
+ """Delete all pending offline messages addressed to *user*."""
205
+ ...
@@ -0,0 +1,61 @@
1
+ """Site-to-site VPN template — managed SD-WAN appliance.
2
+
3
+ Defines the abstract contract for an appliance's participation in the
4
+ site-to-site VPN overlay: its role (hub / spoke / disabled), the hubs a
5
+ spoke connects to (including whether the default route points into the
6
+ overlay), the local subnets advertised into the overlay, and a read of peer
7
+ reachability.
8
+
9
+ The configuration is one ``SiteToSiteVpnConfig`` read and replaced whole —
10
+ role and hubs are semantically coupled (hubs are only meaningful for a
11
+ spoke), and a managed appliance exposes overlay participation as a single
12
+ configuration surface. "Point the default route into the overlay" is a
13
+ config edit: get, flip ``use_default_route`` on a hub entry, set.
14
+
15
+ Mapping pattern — relational role models: on some management planes the
16
+ role is not stored on the device. Hub-ness exists only relationally (an
17
+ edge *is* a hub because other sites' configs reference it), and the
18
+ default-route intent is a backhaul designation rather than a per-hub flag.
19
+ A driver for such a product synthesizes the role on read (referenced as
20
+ hub anywhere → ``HUB``; overlay enabled with hub list → ``SPOKE``) and on
21
+ write registers/dereferences the device in the relevant site configs. The
22
+ round-trip is intent-preserving even where the stored shape differs.
23
+
24
+ In scope: overlay participation (role, hubs + default route, subnets) and
25
+ peer status.
26
+
27
+ Out of scope: VPN-scoped firewall rules (see ``l3_firewall``), IPsec crypto
28
+ parameters (no driving test; highly vendor-divergent — add on evidence),
29
+ path steering across the overlay (see ``sdwan_policy_manager``), and
30
+ third-party / non-overlay tunnels (add on evidence).
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from typing import Protocol, runtime_checkable
36
+
37
+ from testprotocols.models.sdwan_appliance import SiteToSiteVpnConfig, VpnPeerStatus
38
+
39
+
40
+ @runtime_checkable
41
+ class SiteToSiteVpn(Protocol):
42
+ """Abstract contract for an appliance's site-to-site VPN overlay."""
43
+
44
+ def set_vpn_config(self, config: SiteToSiteVpnConfig) -> None:
45
+ """Replace the appliance's overlay participation with *config*.
46
+
47
+ The config is complete — role, hubs (spoke only, priority order),
48
+ and subnet advertisement — and replaces the previous state whole.
49
+ """
50
+ ...
51
+
52
+ def get_vpn_config(self) -> SiteToSiteVpnConfig:
53
+ """Return the current overlay-participation configuration."""
54
+ ...
55
+
56
+ def get_vpn_peers(self) -> list[VpnPeerStatus]:
57
+ """Return the observed status of every site-to-site VPN peer.
58
+
59
+ Empty list when the device participates in no overlay.
60
+ """
61
+ ...
@@ -0,0 +1,17 @@
1
+ """SNMP / Client template.
2
+
3
+ Defines the abstract contract for SNMP client operations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Protocol, runtime_checkable
9
+
10
+
11
+ @runtime_checkable
12
+ class SnmpClient(Protocol):
13
+ """Abstract contract for SNMP client operations."""
14
+
15
+ def execute_snmp_command(self, snmp_command: str, timeout: int = 30) -> str:
16
+ """Execute an SNMP command and return the output string."""
17
+ ...
@@ -0,0 +1,37 @@
1
+ """Spanning-tree configuration — global mode/priority + per-port guards."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ from testprotocols.models.l2_common import StpMode, StpPortState
8
+ from testprotocols.models.switch import StpPortConfig
9
+
10
+
11
+ @runtime_checkable
12
+ class SpanningTree(Protocol):
13
+ """Abstract contract for spanning-tree configuration and read."""
14
+
15
+ def set_mode(self, mode: StpMode) -> None:
16
+ """Set the global STP mode. RSTP-only products raise unsupported-capability on MSTP."""
17
+ ...
18
+
19
+ def get_mode(self) -> StpMode:
20
+ """Return the active STP mode."""
21
+ ...
22
+
23
+ def set_bridge_priority(self, priority: int) -> None:
24
+ """Set the bridge priority (0–61440, in steps of 4096 per IEEE 802.1D)."""
25
+ ...
26
+
27
+ def set_port_config(self, config: StpPortConfig) -> None:
28
+ """Apply per-port guard/edge/path-cost/priority for ``config.port``."""
29
+ ...
30
+
31
+ def get_port_config(self, port: str) -> StpPortConfig:
32
+ """Return the per-port STP configuration for *port*."""
33
+ ...
34
+
35
+ def get_port_state(self, port: str) -> StpPortState:
36
+ """Return the observed STP state for *port*."""
37
+ ...
@@ -0,0 +1,47 @@
1
+ """Static-route template — WAN edge (twin and managed appliance).
2
+
3
+ Defines the abstract contract for testbed-managed static routes: per-entry
4
+ CRUD keyed by ``StaticRoute.name``, plus a config-view read-back. All
5
+ reviewed management planes store static routes as individual objects, so the
6
+ contract is per-entry CRUD — deliberately not the whole-list-replace shape
7
+ used by the firewall/steering policies.
8
+
9
+ ``list_static_routes`` returns the *configured* entries (config view);
10
+ the *operational* routing table (RIB) read stays on
11
+ ``router.Router.get_routing_table``.
12
+
13
+ In scope: add/update, remove, and list testbed-managed static routes.
14
+
15
+ Out of scope: RIB reads (see ``router``), dynamic routing — BGP — (deferred
16
+ in GAPS.md), and policy-based routing / steering (see
17
+ ``sdwan_policy_manager``).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Protocol, runtime_checkable
23
+
24
+ from testprotocols.models.sdwan_appliance import StaticRoute
25
+
26
+
27
+ @runtime_checkable
28
+ class StaticRoutes(Protocol):
29
+ """Abstract contract for a WAN edge's testbed-managed static routes."""
30
+
31
+ def add_static_route(self, route: StaticRoute) -> None:
32
+ """Create the route, or update it in place if ``route.name`` exists.
33
+
34
+ Idempotent by name — repeating a call converges to the same state.
35
+ """
36
+ ...
37
+
38
+ def remove_static_route(self, name: str) -> None:
39
+ """Remove the route named *name*.
40
+
41
+ Raises KeyError if no route with that name exists.
42
+ """
43
+ ...
44
+
45
+ def list_static_routes(self) -> list[StaticRoute]:
46
+ """Return the configured static routes (config view, not the RIB)."""
47
+ ...
@@ -0,0 +1,24 @@
1
+ """Per-port broadcast/multicast/unknown-unicast storm-control thresholds."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ from testprotocols.models.switch import StormControlConfig
8
+
9
+
10
+ @runtime_checkable
11
+ class StormControl(Protocol):
12
+ """Abstract contract for per-port storm control.
13
+
14
+ A product that exposes storm control only in a controller UI (no API) raises
15
+ unsupported-capability.
16
+ """
17
+
18
+ def set_config(self, config: StormControlConfig) -> None:
19
+ """Apply storm-control thresholds for ``config.port``."""
20
+ ...
21
+
22
+ def get_config(self, port: str) -> StormControlConfig:
23
+ """Return storm-control thresholds for *port*."""
24
+ ...
@@ -0,0 +1,32 @@
1
+ """Streaming origin (HLS/DASH) server template.
2
+
3
+ Defines the abstract contract for content-origin servers used by
4
+ streaming/QoE test scenarios. Implementations are responsible for
5
+ ensuring named assets are present and serveable; richer surface
6
+ (bitrate enumeration, log inspection, etc.) is intentionally deferred
7
+ until a concrete testbed needs it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Protocol, runtime_checkable
13
+
14
+
15
+ @runtime_checkable
16
+ class StreamingServer(Protocol):
17
+ """Abstract contract for HLS/DASH content origins used in test scenarios."""
18
+
19
+ def ensure_content_available(self, video_id: str = "default") -> None:
20
+ """Ensure the named asset is present in the origin.
21
+
22
+ Idempotent: returns immediately if the asset is already seeded;
23
+ otherwise generates and uploads it. Raises if the operation fails.
24
+
25
+ Parameters
26
+ ----------
27
+ video_id:
28
+ Identifier of the asset to seed. Implementation decides how
29
+ this maps to actual content (bucket key, file path, stream
30
+ name, etc.).
31
+ """
32
+ ...
@@ -0,0 +1,29 @@
1
+ """Unified L2 + L3/L4 switch ACL — one ordered rule set per binding + direction.
2
+
3
+ The reviewed switches use one ACL engine matching MAC/VLAN and IP 5-tuple
4
+ fields, so this is one capability (not separate L2/L3 protocols). Rules reuse
5
+ ``RuleAction`` / ``RuleProtocol``; the record is ``SwitchAclRule``. Bindings are
6
+ by port or VLAN, ingress or egress, applied as an ordered whole-list replace.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Protocol, runtime_checkable
12
+
13
+ from testprotocols.models.switch import AclDirection, SwitchAclRule
14
+
15
+
16
+ @runtime_checkable
17
+ class SwitchAcl(Protocol):
18
+ """Abstract contract for the switch ACL."""
19
+
20
+ def set_acl(
21
+ self, binding: str, direction: AclDirection, rules: list[SwitchAclRule]
22
+ ) -> None:
23
+ """Replace the ordered ACL bound to *binding* (a port or ``vlan:<id>``)
24
+ in *direction*."""
25
+ ...
26
+
27
+ def get_acl(self, binding: str, direction: AclDirection) -> list[SwitchAclRule]:
28
+ """Return the ordered ACL for *binding* / *direction*."""
29
+ ...