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,197 @@
|
|
|
1
|
+
"""WiFi / WifiBss template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for per-SSID / per-VAP configuration on a
|
|
4
|
+
WiFi-capable device: lifecycle, security, MAC ACL authorization,
|
|
5
|
+
broadcast suppression, VLAN binding, max-clients, DTIM, and captive-portal
|
|
6
|
+
admin.
|
|
7
|
+
|
|
8
|
+
A BSS is identified by a stable logical *name* the test supplies at
|
|
9
|
+
creation time (matching the RadiusClient.add_server pattern). The SSID
|
|
10
|
+
broadcast string can change later via set_ssid without affecting the
|
|
11
|
+
identity of the BSS in the contract.
|
|
12
|
+
|
|
13
|
+
802.1X / Enterprise security references RADIUS servers by *name* — the
|
|
14
|
+
referenced server must be registered via the RadiusClient template
|
|
15
|
+
composed on the same device. Drivers whose device does not compose
|
|
16
|
+
RadiusClient cannot satisfy EAP modes — they raise on those
|
|
17
|
+
*security_mode* values.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
from testprotocols.models.wifi import WifiAcl, WifiBssConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class WifiBss(Protocol):
|
|
29
|
+
"""Abstract contract for per-SSID / per-VAP WiFi configuration."""
|
|
30
|
+
|
|
31
|
+
# --- Lifecycle ---
|
|
32
|
+
|
|
33
|
+
def create_bss(
|
|
34
|
+
self,
|
|
35
|
+
name: str,
|
|
36
|
+
band: str,
|
|
37
|
+
ssid: str,
|
|
38
|
+
security_mode: str,
|
|
39
|
+
*,
|
|
40
|
+
passphrase: str | None = None,
|
|
41
|
+
radius_server_name: str | None = None,
|
|
42
|
+
mfp: str = "optional",
|
|
43
|
+
vlan_id: int | None = None,
|
|
44
|
+
max_clients: int | None = None,
|
|
45
|
+
broadcast_enabled: bool = True,
|
|
46
|
+
dtim_period: int = 2,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Create a new BSS on *band*, identified by *name*.
|
|
49
|
+
|
|
50
|
+
*security_mode* is one of: ``"Open"``, ``"OWE"``, ``"WPA2-PSK"``,
|
|
51
|
+
``"WPA2-EAP"``, ``"WPA3-SAE"``, ``"WPA3-EAP"``,
|
|
52
|
+
``"WPA2-WPA3-PSK-Mixed"``, ``"WPA2-WPA3-EAP-Mixed"``.
|
|
53
|
+
|
|
54
|
+
*mfp* is one of ``"off"``, ``"optional"``, ``"required"``.
|
|
55
|
+
|
|
56
|
+
Required arguments per security_mode:
|
|
57
|
+
- PSK / SAE / mixed-PSK modes: *passphrase* required
|
|
58
|
+
- EAP / mixed-EAP modes: *radius_server_name* required, must
|
|
59
|
+
reference a server registered via RadiusClient on this device
|
|
60
|
+
- Open / OWE: neither required
|
|
61
|
+
|
|
62
|
+
Raises ValueError on a duplicate *name* or on missing
|
|
63
|
+
mode-required arguments.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def delete_bss(self, name: str) -> None:
|
|
68
|
+
"""Delete the BSS identified by *name*. Raises KeyError if absent."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def list_bss(self) -> list[WifiBssConfig]:
|
|
72
|
+
"""Return all BSS configurations on the device (without secrets)."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def get_bss_config(self, name: str) -> WifiBssConfig:
|
|
76
|
+
"""Return the full configuration of the BSS identified by *name* (without secret).
|
|
77
|
+
|
|
78
|
+
Raises KeyError if absent.
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
# --- Admin state ---
|
|
83
|
+
|
|
84
|
+
def set_enabled(self, name: str, enabled: bool) -> None:
|
|
85
|
+
"""Enable or disable the BSS without deleting it.
|
|
86
|
+
|
|
87
|
+
The radio stays up (DFS continues, neighbour scanning continues);
|
|
88
|
+
only the BSS stops broadcasting and accepting clients.
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
# --- SSID broadcast ---
|
|
93
|
+
|
|
94
|
+
def set_ssid(self, name: str, ssid: str) -> None:
|
|
95
|
+
"""Change the broadcast SSID string of the BSS identified by *name*.
|
|
96
|
+
|
|
97
|
+
Does not change the logical *name* (which remains the contract handle).
|
|
98
|
+
"""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
def set_broadcast_enabled(self, name: str, enabled: bool) -> None:
|
|
102
|
+
"""Suppress or restore SSID broadcast in beacons (hidden SSID)."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
# --- Security ---
|
|
106
|
+
|
|
107
|
+
def set_security(
|
|
108
|
+
self,
|
|
109
|
+
name: str,
|
|
110
|
+
mode: str,
|
|
111
|
+
*,
|
|
112
|
+
passphrase: str | None = None,
|
|
113
|
+
radius_server_name: str | None = None,
|
|
114
|
+
mfp: str = "optional",
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Reconfigure the security of an existing BSS.
|
|
117
|
+
|
|
118
|
+
Same value space and required-argument rules as ``create_bss``.
|
|
119
|
+
Reconfiguration disconnects currently associated clients on most
|
|
120
|
+
drivers; tests should expect re-association.
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
# --- MAC ACL — per-BSS authorization scheme ---
|
|
125
|
+
|
|
126
|
+
def set_acl_mode(self, name: str, mode: str) -> None:
|
|
127
|
+
"""Set the per-BSS MAC ACL mode.
|
|
128
|
+
|
|
129
|
+
*mode* is one of:
|
|
130
|
+
- ``"disabled"`` — no MAC filtering; the BSS's ACL list is ignored
|
|
131
|
+
- ``"allow"`` — allow-list (whitelist); only MACs in the ACL may associate
|
|
132
|
+
- ``"deny"`` — deny-list (blacklist); MACs in the ACL are blocked
|
|
133
|
+
|
|
134
|
+
Raises KeyError if *name* is not registered.
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def add_acl_entry(self, name: str, mac: str) -> None:
|
|
139
|
+
"""Add *mac* to the BSS's ACL list. No-op if already present.
|
|
140
|
+
|
|
141
|
+
Effective filtering depends on the current ACL mode.
|
|
142
|
+
Raises KeyError if *name* is not registered.
|
|
143
|
+
"""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
def remove_acl_entry(self, name: str, mac: str) -> None:
|
|
147
|
+
"""Remove *mac* from the BSS's ACL list. No-op if absent.
|
|
148
|
+
|
|
149
|
+
Raises KeyError if *name* is not registered.
|
|
150
|
+
"""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
def clear_acl(self, name: str) -> None:
|
|
154
|
+
"""Remove all entries from the BSS's ACL list. Mode is unchanged.
|
|
155
|
+
|
|
156
|
+
Raises KeyError if *name* is not registered.
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
def get_acl(self, name: str) -> WifiAcl:
|
|
161
|
+
"""Return the BSS's MAC ACL state (mode + entries).
|
|
162
|
+
|
|
163
|
+
Raises KeyError if *name* is not registered.
|
|
164
|
+
"""
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
# --- VLAN binding ---
|
|
168
|
+
|
|
169
|
+
def set_vlan(self, name: str, vlan_id: int | None) -> None:
|
|
170
|
+
"""Bind the BSS to *vlan_id*, or pass None to remove the binding (untagged)."""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
# --- Capacity / timing ---
|
|
174
|
+
|
|
175
|
+
def set_max_clients(self, name: str, max_clients: int | None) -> None:
|
|
176
|
+
"""Cap concurrent associations at *max_clients*, or pass None to remove the cap."""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
def set_dtim_period(self, name: str, period: int) -> None:
|
|
180
|
+
"""Set the DTIM period (number of beacons between DTIM transmissions). Typical: 1-5."""
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
# --- Captive portal (minimal on-AP surface) ---
|
|
184
|
+
|
|
185
|
+
def set_captive_portal(
|
|
186
|
+
self,
|
|
187
|
+
name: str,
|
|
188
|
+
enabled: bool,
|
|
189
|
+
redirect_url: str | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Enable or disable the per-BSS captive portal.
|
|
192
|
+
|
|
193
|
+
When enabled, *redirect_url* is the splash page clients are
|
|
194
|
+
redirected to. Voucher / local-account / splash-HTML upload are
|
|
195
|
+
out of scope here — they belong to WifiController.
|
|
196
|
+
"""
|
|
197
|
+
...
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""WiFi / WifiClient template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for WiFi client operations including
|
|
4
|
+
association, scanning, and monitor-mode management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class WifiClient(Protocol):
|
|
14
|
+
"""Abstract contract for WiFi client operations."""
|
|
15
|
+
|
|
16
|
+
def reset_wifi_iface(self) -> None:
|
|
17
|
+
"""Reset the WiFi interface to its default state."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def disable_wifi(self) -> None:
|
|
21
|
+
"""Disable the WiFi radio."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def enable_wifi(self) -> None:
|
|
25
|
+
"""Enable the WiFi radio."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def wifi_client_connect(
|
|
29
|
+
self,
|
|
30
|
+
ssid_name: str,
|
|
31
|
+
password: str | None = None,
|
|
32
|
+
security_mode: str | None = None,
|
|
33
|
+
bssid: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Connect to *ssid_name* using the supplied credentials and security mode."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def wifi_disconnect(self) -> None:
|
|
39
|
+
"""Disconnect from the current WiFi network."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def is_wlan_connected(self) -> bool:
|
|
43
|
+
"""Return True if the WLAN interface is currently connected."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def list_wifi_ssids(self) -> list[str]:
|
|
47
|
+
"""Return a list of visible SSIDs from a scan."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def set_wlan_scan_channel(self, channel: str) -> None:
|
|
51
|
+
"""Set the WiFi scan channel to *channel*."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def iwlist_supported_channels(self, wifi_band: str) -> list[str]:
|
|
55
|
+
"""Return the list of channels supported by the adapter for *wifi_band*."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def change_wifi_region(self, country: str) -> None:
|
|
59
|
+
"""Change the regulatory domain to *country* (ISO 3166-1 alpha-2)."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def enable_monitor_mode(self) -> None:
|
|
63
|
+
"""Enable monitor mode on the WiFi interface."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def disable_monitor_mode(self) -> None:
|
|
67
|
+
"""Disable monitor mode and return the interface to managed mode."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
def is_monitor_mode_enabled(self) -> bool:
|
|
71
|
+
"""Return True if the WiFi interface is in monitor mode."""
|
|
72
|
+
...
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""WiFi / WifiMesh template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for WiFi mesh participation: role
|
|
4
|
+
inspection, mesh enable/disable, topology read, backhaul-link
|
|
5
|
+
control (band / channel), agent onboarding (configurator and enrollee
|
|
6
|
+
sides), agent removal, and cross-mesh client steering.
|
|
7
|
+
|
|
8
|
+
Composed onto any device that participates in or will participate in a
|
|
9
|
+
wireless mesh. A pre-onboarding device (about to be added as an agent)
|
|
10
|
+
composes WifiMesh with its role reading "uncommissioned"; after
|
|
11
|
+
onboarding via the controller's add_agent, role becomes "agent".
|
|
12
|
+
|
|
13
|
+
Vendor uniformity for mesh is low — enterprise stacks use proprietary
|
|
14
|
+
multi-AP protocols; EasyMesh R1/R2/R3 lives in prpl, OneWiFi (RDK-B),
|
|
15
|
+
OpenWrt prplMesh, and select carrier-grade SKUs. Each driver translates
|
|
16
|
+
the uniform contract here to its underlying vendor RPCs (EasyMesh M1/M2,
|
|
17
|
+
proprietary controller add-agent calls, vendor-specific steering APIs).
|
|
18
|
+
Drivers without support for specific operations raise NotImplementedError.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Protocol, runtime_checkable
|
|
24
|
+
|
|
25
|
+
from testprotocols.models.wifi import WifiMeshStatus, WifiMeshTopology
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class WifiMesh(Protocol):
|
|
30
|
+
"""Abstract contract for WiFi mesh participation."""
|
|
31
|
+
|
|
32
|
+
# --- Admin state ---
|
|
33
|
+
|
|
34
|
+
def set_enabled(self, enabled: bool) -> None:
|
|
35
|
+
"""Enable or disable this device's participation in the mesh.
|
|
36
|
+
|
|
37
|
+
When disabled, the device retains its role configuration but stops
|
|
38
|
+
forwarding mesh frames and stops responding to mesh control messages.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
# --- Status and topology ---
|
|
43
|
+
|
|
44
|
+
def get_mesh_status(self) -> WifiMeshStatus:
|
|
45
|
+
"""Return this device's local mesh status.
|
|
46
|
+
|
|
47
|
+
Includes role, enabled state, parent, hop count, and backhaul link.
|
|
48
|
+
Lightweight read — does not query other agents.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def get_topology(self) -> WifiMeshTopology:
|
|
53
|
+
"""Return the full mesh topology as known to this device.
|
|
54
|
+
|
|
55
|
+
Heavier read — for a controller, returns the full list of known
|
|
56
|
+
agents with their parents and roles. For an agent, returns the
|
|
57
|
+
agent's local view (typically just its parent and any peers it
|
|
58
|
+
directly observes).
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
# --- Backhaul control (agent-side) ---
|
|
63
|
+
|
|
64
|
+
def set_backhaul_band(self, band: str | None) -> None:
|
|
65
|
+
"""Force the backhaul radio to *band* (e.g. ``"5GHz"``), or pass None to
|
|
66
|
+
release the constraint.
|
|
67
|
+
|
|
68
|
+
Releasing returns the device to whatever band-selection policy
|
|
69
|
+
the mesh controller / driver default uses. Drivers without
|
|
70
|
+
backhaul-band override (typical of controller-only nodes that
|
|
71
|
+
have no uplink) raise NotImplementedError.
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def set_backhaul_channel(self, channel: int | None) -> None:
|
|
76
|
+
"""Force the backhaul radio to *channel*, or pass None to release the constraint.
|
|
77
|
+
|
|
78
|
+
Drivers without backhaul-channel override raise NotImplementedError.
|
|
79
|
+
Raises ValueError if *channel* is not supported on the current
|
|
80
|
+
backhaul band.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
# --- Agent management (typically controller-side) ---
|
|
85
|
+
|
|
86
|
+
def add_agent(
|
|
87
|
+
self,
|
|
88
|
+
agent_mac: str,
|
|
89
|
+
*,
|
|
90
|
+
dpp_uri: str | None = None,
|
|
91
|
+
psk: str | None = None,
|
|
92
|
+
wps_pin: str | None = None,
|
|
93
|
+
wps_pbc: bool = False,
|
|
94
|
+
timeout: float = 60.0,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Onboard a new agent identified by *agent_mac* into the mesh.
|
|
97
|
+
|
|
98
|
+
Exactly one onboarding credential must be passed:
|
|
99
|
+
- *dpp_uri* — the agent's Device Provisioning Protocol URI
|
|
100
|
+
- *psk* — pre-shared key the agent is preconfigured with
|
|
101
|
+
- *wps_pin* — WPS PIN displayed on or assigned to the agent
|
|
102
|
+
- *wps_pbc=True* — WPS Push-Button onboarding (controller starts PBC window)
|
|
103
|
+
|
|
104
|
+
Blocks until the agent has joined the mesh (M1/M2 exchange complete
|
|
105
|
+
and the agent appears in get_topology) or *timeout* seconds elapse.
|
|
106
|
+
|
|
107
|
+
Raises ValueError if zero or multiple credentials are passed.
|
|
108
|
+
Raises TimeoutError if onboarding does not complete within *timeout*.
|
|
109
|
+
Drivers that do not support an onboarding method raise
|
|
110
|
+
NotImplementedError when that credential is passed.
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def remove_agent(self, agent_mac: str) -> None:
|
|
115
|
+
"""Remove the agent identified by *agent_mac* from the mesh.
|
|
116
|
+
|
|
117
|
+
The agent is told to leave (mesh-leave message in EasyMesh, vendor
|
|
118
|
+
equivalent otherwise), and is removed from the controller's
|
|
119
|
+
topology.
|
|
120
|
+
|
|
121
|
+
Raises KeyError if no agent with that MAC is currently in the mesh.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
# --- DPP enrollee (typically agent-side, before onboarding) ---
|
|
126
|
+
|
|
127
|
+
def get_dpp_uri(self) -> str:
|
|
128
|
+
"""Return this device's DPP bootstrap URI for use by a configurator.
|
|
129
|
+
|
|
130
|
+
The URI encodes the device's DPP public key and supported channels
|
|
131
|
+
(e.g. ``DPP:C:81/1;K:MDkw...;;``). Tests fetch it from the new agent,
|
|
132
|
+
then pass it to the controller's ``add_agent(dpp_uri=...)``.
|
|
133
|
+
|
|
134
|
+
Drivers without DPP enrollee support raise NotImplementedError.
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def start_dpp_enrollee(self, timeout: float = 120.0) -> None:
|
|
139
|
+
"""Put the device into DPP-enrollee listening mode.
|
|
140
|
+
|
|
141
|
+
Returns immediately. The device listens for DPP authentication
|
|
142
|
+
from a configurator for *timeout* seconds (standard DPP window
|
|
143
|
+
is 120s); after that, the listener closes whether or not
|
|
144
|
+
onboarding completed.
|
|
145
|
+
|
|
146
|
+
Drivers without DPP enrollee support raise NotImplementedError.
|
|
147
|
+
Drivers where the enrollee listener is always-on (no explicit
|
|
148
|
+
trigger needed) implement this as a no-op.
|
|
149
|
+
"""
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
# --- WPS / PSK enrollee (agent-side) ---
|
|
153
|
+
|
|
154
|
+
def trigger_wps_enrollee(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
pin: str | None = None,
|
|
158
|
+
window_seconds: float = 120.0,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Put the device into WPS enrollee mode for mesh-agent onboarding.
|
|
161
|
+
|
|
162
|
+
Returns immediately. The device listens for WPS handshake from a
|
|
163
|
+
controller for *window_seconds* (standard WPS window is 120s).
|
|
164
|
+
|
|
165
|
+
When *pin* is None, the device opens a WPS-PBC window — a
|
|
166
|
+
controller in PBC mode within the same window completes the join.
|
|
167
|
+
When *pin* is provided, the device opens a WPS-PIN window expecting
|
|
168
|
+
a controller to present that PIN.
|
|
169
|
+
|
|
170
|
+
*pin* is 4 or 8 digits per WPS spec, as ``str`` (preserves leading zeros).
|
|
171
|
+
Drivers raise ValueError on malformed PINs.
|
|
172
|
+
|
|
173
|
+
Drivers without WPS enrollee support raise NotImplementedError.
|
|
174
|
+
"""
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
def set_mesh_psk_for_enrollment(self, psk: str | None) -> None:
|
|
178
|
+
"""Configure (or clear) the pre-shared key this device presents when joining a mesh.
|
|
179
|
+
|
|
180
|
+
Used for PSK-based mesh enrollment, where an agent is preconfigured
|
|
181
|
+
with the mesh PSK and the controller accepts agents presenting that
|
|
182
|
+
PSK. Pass None to clear a previously-set PSK.
|
|
183
|
+
|
|
184
|
+
Drivers without PSK-based mesh enrollment raise NotImplementedError.
|
|
185
|
+
Read-side intentionally omitted: PSK is write-only across the contract.
|
|
186
|
+
"""
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
# --- Cross-mesh client steering (typically controller-side) ---
|
|
190
|
+
|
|
191
|
+
def steer_client(
|
|
192
|
+
self,
|
|
193
|
+
client_mac: str,
|
|
194
|
+
target_agent_mac: str,
|
|
195
|
+
*,
|
|
196
|
+
disassoc_imminent: bool = False,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Steer the associated client *client_mac* toward agent *target_agent_mac*.
|
|
199
|
+
|
|
200
|
+
The controller looks up which agent the client is currently on,
|
|
201
|
+
sends an 802.11v BSS Transition Management Request from that agent
|
|
202
|
+
suggesting the target, and (when *disassoc_imminent* is True) sets
|
|
203
|
+
the disassoc-imminent bit so the client must roam or be disassociated.
|
|
204
|
+
|
|
205
|
+
Fire-and-forget: returns once the BTM Request has been sent. Tests
|
|
206
|
+
verify the outcome by polling ``WifiStations.get_station(client_mac).bss_name``
|
|
207
|
+
on the target agent (or reading the controller's topology / association
|
|
208
|
+
events).
|
|
209
|
+
|
|
210
|
+
Raises KeyError if the client is not currently associated to any agent
|
|
211
|
+
in the mesh, or if *target_agent_mac* is not a known agent.
|
|
212
|
+
"""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@runtime_checkable
|
|
217
|
+
class WifiMeshWhiteBox(WifiMesh, Protocol):
|
|
218
|
+
"""White-box extension of WifiMesh for raw 1905.1 / EasyMesh introspection.
|
|
219
|
+
|
|
220
|
+
Drivers running an open-source EasyMesh stack (prplMesh, hostapd-based
|
|
221
|
+
controllers, etc.) and able to shell into the controller / agent satisfy
|
|
222
|
+
this extension. Closed-source vendor controllers typically satisfy only
|
|
223
|
+
the base ``WifiMesh`` Protocol; tests requiring raw 1905 / EasyMesh state
|
|
224
|
+
verification should pin against ``WifiMeshWhiteBox`` and accept the
|
|
225
|
+
collection-skip on drivers that don't satisfy it (per the ``@white_box``
|
|
226
|
+
scenario tag rule).
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def get_raw_ieee1905_state(self) -> str:
|
|
230
|
+
"""Return the IEEE 1905.1 AL-entity state of this device verbatim.
|
|
231
|
+
|
|
232
|
+
Format is driver-dependent — typically the output of the
|
|
233
|
+
controller / agent's CLI dump command (e.g. ``prplmeshcli al-entity-info``,
|
|
234
|
+
vendor equivalents). Tests parse for AL MAC, neighbour AL MACs,
|
|
235
|
+
topology-discovery counters, and link metrics.
|
|
236
|
+
"""
|
|
237
|
+
...
|
|
238
|
+
|
|
239
|
+
def get_raw_easymesh_tlvs(self, message_type: str | None = None) -> str:
|
|
240
|
+
"""Return raw EasyMesh TLVs observed on the 1905 control plane.
|
|
241
|
+
|
|
242
|
+
With *message_type* None, returns all recently-observed TLVs (driver
|
|
243
|
+
chooses the window — typically the controller's transient cache).
|
|
244
|
+
With *message_type* set (e.g. ``"AP-Capability-Report"``,
|
|
245
|
+
``"Topology-Notification"``), returns only TLVs from messages of
|
|
246
|
+
that type. Format is driver-dependent (often hex-encoded TLV with
|
|
247
|
+
a parsed annotation per line).
|
|
248
|
+
"""
|
|
249
|
+
...
|
|
250
|
+
|
|
251
|
+
def get_controller_logs(self, since_seconds: float = 60.0) -> str:
|
|
252
|
+
"""Return recent controller-side log output covering EasyMesh activity.
|
|
253
|
+
|
|
254
|
+
*since_seconds* bounds the lookback window. Format is the controller's
|
|
255
|
+
native log format (typically a timestamped text stream). Tests grep
|
|
256
|
+
for specific event signatures: agent onboarding messages, channel-
|
|
257
|
+
selection decisions, steering events, link-metric updates.
|
|
258
|
+
"""
|
|
259
|
+
...
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""WiFi / WifiOnboarding template.
|
|
2
|
+
|
|
3
|
+
Defines the abstract contract for WPS and DPP (EasyConnect) client
|
|
4
|
+
onboarding on a WiFi-capable device: per-BSS WPS enable, PBC / PIN
|
|
5
|
+
trigger, device-PIN read, and DPP-based client enrollment.
|
|
6
|
+
|
|
7
|
+
Vendor uniformity is low-medium: residential / RDK-B / prpl / OpenWrt
|
|
8
|
+
stacks expose all of these as first-class operations; enterprise stacks
|
|
9
|
+
commonly disable WPS entirely and offer DPP only sporadically. Drivers
|
|
10
|
+
without support for a specific onboarding method raise
|
|
11
|
+
NotImplementedError.
|
|
12
|
+
|
|
13
|
+
Mesh agent onboarding (M1/M2 / DPP between APs) lives on WifiMesh;
|
|
14
|
+
this template is exclusively for STA-side (client) onboarding.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class WifiOnboarding(Protocol):
|
|
24
|
+
"""Abstract contract for WPS and DPP client onboarding."""
|
|
25
|
+
|
|
26
|
+
# --- WPS — per-BSS admin state ---
|
|
27
|
+
|
|
28
|
+
def set_wps_enabled(self, bss_name: str, enabled: bool) -> None:
|
|
29
|
+
"""Enable or disable WPS support on *bss_name*.
|
|
30
|
+
|
|
31
|
+
Raises KeyError if *bss_name* is not registered.
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def get_wps_enabled(self, bss_name: str) -> bool:
|
|
36
|
+
"""Return True if WPS is currently enabled on *bss_name*.
|
|
37
|
+
|
|
38
|
+
Raises KeyError if *bss_name* is not registered.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
# --- WPS — session triggers ---
|
|
43
|
+
|
|
44
|
+
def trigger_wps_pbc(self, bss_name: str, window_seconds: float = 120.0) -> None:
|
|
45
|
+
"""Open a WPS Push-Button Configuration window on *bss_name*.
|
|
46
|
+
|
|
47
|
+
Returns immediately after the window opens. The AP accepts WPS-PBC
|
|
48
|
+
joins for *window_seconds* (standard WPS window is 120s); after
|
|
49
|
+
that, the window closes whether or not a client joined. Tests
|
|
50
|
+
verify joins by polling ``WifiStations.list_associated_stations``.
|
|
51
|
+
|
|
52
|
+
Raises KeyError if *bss_name* is not registered.
|
|
53
|
+
Raises RuntimeError if WPS is currently disabled on *bss_name*.
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def trigger_wps_pin(self, bss_name: str, pin: str, window_seconds: float = 120.0) -> None:
|
|
58
|
+
"""Open a WPS PIN-mode window on *bss_name* expecting *pin*.
|
|
59
|
+
|
|
60
|
+
Returns immediately. The AP accepts a WPS-PIN join from any client
|
|
61
|
+
presenting *pin* for *window_seconds*. *pin* is 4 or 8 digits per
|
|
62
|
+
WPS spec; drivers raise ValueError on malformed PINs.
|
|
63
|
+
|
|
64
|
+
Raises KeyError if *bss_name* is not registered.
|
|
65
|
+
Raises RuntimeError if WPS is currently disabled on *bss_name*.
|
|
66
|
+
"""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def get_wps_device_pin(self, bss_name: str) -> str:
|
|
70
|
+
"""Return the AP's own WPS device PIN for *bss_name*.
|
|
71
|
+
|
|
72
|
+
The device PIN is the 8-digit PIN burned into the AP that a client
|
|
73
|
+
can use to enroll without an explicitly-set PIN session. Useful in
|
|
74
|
+
tests where the client drives WPS-PIN onboarding and needs to know
|
|
75
|
+
the AP's PIN.
|
|
76
|
+
|
|
77
|
+
Raises KeyError if *bss_name* is not registered.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
# --- DPP / EasyConnect — AP as configurator ---
|
|
82
|
+
|
|
83
|
+
def enroll_client_via_dpp(
|
|
84
|
+
self,
|
|
85
|
+
client_dpp_uri: str,
|
|
86
|
+
bss_name: str,
|
|
87
|
+
timeout: float = 60.0,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Provision a new client onto *bss_name* via DPP, acting as configurator.
|
|
90
|
+
|
|
91
|
+
*client_dpp_uri* is the client's DPP bootstrap URI (typically scanned
|
|
92
|
+
from the client's QR code or obtained via NFC / out-of-band channel).
|
|
93
|
+
The AP performs the DPP authentication and configuration exchange,
|
|
94
|
+
delivering the SSID + credentials for *bss_name* to the client.
|
|
95
|
+
|
|
96
|
+
Blocks until the DPP exchange completes (driver has confirmed
|
|
97
|
+
configuration delivery to the client) or *timeout* seconds elapse.
|
|
98
|
+
The client's subsequent association is asynchronous — tests verify
|
|
99
|
+
via ``WifiStations.list_associated_stations``.
|
|
100
|
+
|
|
101
|
+
Raises KeyError if *bss_name* is not registered.
|
|
102
|
+
Raises ValueError if *client_dpp_uri* is malformed.
|
|
103
|
+
Raises TimeoutError if the DPP exchange does not complete within *timeout*.
|
|
104
|
+
"""
|
|
105
|
+
...
|