Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 53 additions & 18 deletions macfleet/pool/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from macfleet.pool.network import LinkType, NetworkLink

_MAX_SERIALIZED_LINKS = 8
_MAX_MTU = 65_535

_LINK_PRIORITY = {
LinkType.LOOPBACK: 50,
Expand Down Expand Up @@ -44,6 +45,47 @@ def _is_dialable_address(value: str) -> bool:
return not (parsed.version == 6 and parsed.is_link_local)


def _telemetry_number(
item: dict[object, object],
short_name: str,
long_name: str,
*,
maximum: float | None = None,
) -> float:
raw = item[short_name] if short_name in item else item.get(long_name, 0.0)
if not _number_in_range(raw, maximum=maximum):
raise ValueError(f"{long_name} must be a number")
assert isinstance(raw, (int, float))
value = float(raw)
return value


def _number_in_range(value: object, *, maximum: float | None = None) -> bool:
return (
not isinstance(value, bool)
and isinstance(value, (int, float))
and math.isfinite(float(value))
and value >= 0
and (maximum is None or value <= maximum)
)


def _valid_mtu(value: object) -> bool:
return (
not isinstance(value, bool)
and isinstance(value, int)
and 1 <= value <= _MAX_MTU
)


def _telemetry_mtu(item: dict[object, object]) -> int:
raw = item["m"] if "m" in item else item.get("mtu", 1500)
if not _valid_mtu(raw):
raise ValueError("mtu must be an integer between 1 and 65535")
assert isinstance(raw, int)
return raw


def _same_lan_score(left: str, right: str) -> int:
left_ip = _parsed_address(left)
right_ip = _parsed_address(right)
Expand Down Expand Up @@ -179,18 +221,18 @@ def serialize_network_links(links: Sequence[NetworkLink]) -> str:
"t": link.link_type.value,
"ip": link.ip_address,
}
if link.bandwidth_mbps:
if link.bandwidth_mbps and _number_in_range(link.bandwidth_mbps):
item["b"] = link.bandwidth_mbps
if link.latency_ms:
if link.latency_ms and _number_in_range(link.latency_ms):
item["l"] = link.latency_ms
if link.loss_rate:
if link.loss_rate and _number_in_range(link.loss_rate, maximum=1.0):
item["r"] = link.loss_rate
if link.mtu != 1500:
if link.mtu != 1500 and _valid_mtu(link.mtu):
item["m"] = link.mtu
payload.append(item)
if len(payload) >= _MAX_SERIALIZED_LINKS:
break
return json.dumps(payload, separators=(",", ":"))
return json.dumps(payload, separators=(",", ":"), allow_nan=False)


def deserialize_network_links(payload: str) -> tuple[NetworkLink, ...]:
Expand All @@ -201,6 +243,8 @@ def deserialize_network_links(payload: str) -> tuple[NetworkLink, ...]:
return ()
if not isinstance(raw, list):
return ()
if len(raw) > _MAX_SERIALIZED_LINKS:
return ()

links: list[NetworkLink] = []
for item in raw:
Expand All @@ -218,19 +262,10 @@ def deserialize_network_links(payload: str) -> tuple[NetworkLink, ...]:
or link_type == LinkType.LOOPBACK
):
continue
bandwidth_mbps = float(item.get("b") or item.get("bandwidth_mbps") or 0.0)
latency_ms = float(item.get("l") or item.get("latency_ms") or 0.0)
loss_rate = float(item.get("r") or item.get("loss_rate") or 0.0)
mtu = int(item.get("m") or item.get("mtu") or 1500)
if (
not all(math.isfinite(value) and value >= 0 for value in (
bandwidth_mbps,
latency_ms,
loss_rate,
))
or mtu < 1
):
continue
bandwidth_mbps = _telemetry_number(item, "b", "bandwidth_mbps")
latency_ms = _telemetry_number(item, "l", "latency_ms")
loss_rate = _telemetry_number(item, "r", "loss_rate", maximum=1.0)
mtu = _telemetry_mtu(item)
links.append(
NetworkLink(
interface=interface,
Expand Down
75 changes: 75 additions & 0 deletions tests/test_pool/test_topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,78 @@ def test_deserialize_network_links_rejects_non_finite_telemetry():
payload = '[{"i":"en0","t":"ethernet","ip":"192.168.1.2","b":NaN}]'

assert deserialize_network_links(payload) == ()


@pytest.mark.parametrize(
("field", "value"),
[
("b", True),
("b", "100"),
("b", -1),
("l", True),
("l", "5"),
("l", -1),
("r", True),
("r", "0.1"),
("r", -0.1),
("r", 1.01),
("m", True),
("m", 1500.5),
("m", "1500"),
("m", 0),
("m", 65536),
],
)
def test_deserialize_network_links_rejects_invalid_telemetry_types_and_bounds(
field, value
):
item = {"i": "en0", "t": "ethernet", "ip": "192.168.1.2", field: value}

assert deserialize_network_links(json.dumps([item])) == ()


def test_deserialize_network_links_accepts_loss_and_mtu_boundaries():
payload = json.dumps(
[
{
"i": "en0",
"t": "ethernet",
"ip": "192.168.1.2",
"r": 1,
"m": 65535,
}
]
)

restored = deserialize_network_links(payload)

assert len(restored) == 1
assert restored[0].loss_rate == 1.0
assert restored[0].mtu == 65535


def test_serialize_network_links_omits_invalid_local_telemetry():
link = NetworkLink(
"en0",
LinkType.ETHERNET,
"192.168.1.2",
bandwidth_mbps=float("nan"),
latency_ms=-1,
loss_rate=2,
mtu=1500.5, # type: ignore[arg-type]
)

payload = json.loads(serialize_network_links([link]))

assert payload == [{"i": "en0", "t": "ethernet", "ip": "192.168.1.2"}]


def test_deserialize_network_links_rejects_too_many_advertisements():
payload = json.dumps(
[
{"i": f"en{index}", "t": "ethernet", "ip": f"192.168.1.{index + 1}"}
for index in range(9)
]
)

assert deserialize_network_links(payload) == ()
Loading