Skip to content

Commit

Permalink
Default to "modern" parsing, falling back to legacy on errors
Browse files Browse the repository at this point in the history
  • Loading branch information
smsearcy committed Jul 5, 2024
1 parent a3dc730 commit 4cd5877
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 73 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pre-commit:
pdm run pre-commit run --all-files

lint:
pdm run ruff check . --fix
pdm run ruff format .
pdm run ruff check . --fix

mypy:
pdm run mypy meshinfo
Expand Down
138 changes: 70 additions & 68 deletions meshinfo/aredn.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,6 @@ def __attrs_post_init__(self):
if self.ip_address == "none":
self.ip_address = None

@classmethod
def from_json(cls, raw_data: dict[str, str]) -> Interface:
return cls(
name=raw_data["name"],
# some tunnel interfaces lack a MAC address
mac_address=raw_data.get("mac", ""),
ip_address=raw_data.get("ip"),
)


@attrs.define
class Service:
Expand Down Expand Up @@ -229,58 +220,14 @@ class LinkInfo:
rx_rate: float | None = None
olsr_cost: float | None = None

@classmethod
def from_json(
cls, raw_data: dict[str, Any], *, source: str, ip_address: str
) -> LinkInfo:
"""Construct the `Link` dataclass from the AREDN JSON information.
Needs the name of the source node passed in as well.
Args:
source: Hostname of source node (lowercase, no domain)
ip_address: IP address of link destination
"""
# fix example of a DTD link that wasn't properly identified as such
missing_dtd = (
raw_data["linkType"] == "" and raw_data["olsrInterface"] == "br-dtdlink"
)
type_ = "DTD" if missing_dtd else raw_data["linkType"]
try:
link_type = getattr(LinkType, type_)
except AttributeError as exc:
logger.warning("Unknown link type", error=str(exc))
link_type = LinkType.UNKNOWN

# ensure consistent node names
node_name = raw_data["hostname"].replace(".local.mesh", "").lstrip(".").lower()
if (link_cost := raw_data.get("linkCost")) is not None and link_cost > 99.99:
link_cost = 99.99

return LinkInfo(
source=source,
destination=node_name,
destination_ip=ip_address,
type=link_type,
interface=raw_data["olsrInterface"],
quality=raw_data["linkQuality"],
neighbor_quality=raw_data["neighborLinkQuality"],
signal=raw_data.get("signal"),
noise=raw_data.get("noise"),
tx_rate=raw_data.get("tx_rate"),
rx_rate=raw_data.get("rx_rate"),
olsr_cost=link_cost,
)


@attrs.define(kw_only=True)
class SystemInfo:
"""Data class to represent the node data from 'sysinfo.json'.
Data that is directly retrieved from the node is stored in this class
and "derived" data is then determined at runtime via property attributes
(e.g. the wireless adaptor and band information).
(e.g. band information).
The network interfaces are represented by a dictionary,
indexed by the interface name.
Expand All @@ -289,7 +236,7 @@ class SystemInfo:
particularly if an empty string would not be a valid value (e.g. SSID).
If there is a situation in which missing/unknown values need to be distinguished
from empty strings then `None` would be appropriate.
In a case like node description it is an optional value
In a case like node description it is an optional value,
so I see no need for "Unknown"/`None`.
"""
Expand Down Expand Up @@ -334,7 +281,9 @@ def __attrs_post_init__(self) -> None:
self.mac_address = iface.mac_address
break
else:
logger.warning("Unable to identify wireless interface")
logger.warning(
"Unable to identify wireless interface", interfaces=self.interfaces
)

@property
def lan_ip_address(self) -> str:
Expand Down Expand Up @@ -416,20 +365,30 @@ def load_system_info(json_data: dict[str, Any], *, ip_address: str = "") -> Syst
Args:
json_data: Python dictionary loaded from the JSON data.
ip_address: IP address used to connect to this node
(used in case we cannot identify the primary interface).
Returns:
Data class with information about the node.
"""
api_version = tuple(int(value) for value in json_data["api_version"].split("."))
if api_version < (1, 5):
# pre-1.5 the JSON was much flatter, so treat separately
logger.debug("Parsing legacy sysinfo.json", version=api_version)
return _load_legacy_system_info(json_data)
# The vast majority of nodes at this time are on the newer API
# (and some "custom" software doesn't report its API version correctly),
# so we're just going to try the "modern" parser, falling back in case of errors.
try:
node_info = _load_system_info(json_data)
except Exception as exc:
logger.warning("JSON parse error, falling back to legacy version", exc=exc)
node_info = _load_legacy_system_info(json_data)

# handle issue of failing to identify the main wireless interface
if not node_info.ip_address:
node_info.ip_address = ip_address
return node_info

# since 1.5, stuff has been added but the format was consistent,
# so handling here (at least until there is a 2.x API)

def _load_system_info(json_data: dict[str, Any]) -> SystemInfo:
"""Load "modern" `sysinfo.json` (aka API >= 1.5)."""
rf_info = json_data["meshrf"]
details = json_data["node_details"]
link_info = json_data.get("link_info") or {}
Expand Down Expand Up @@ -457,20 +416,18 @@ def load_system_info(json_data: dict[str, Any], *, ip_address: str = "") -> Syst
board_id=details["board_id"],
active_tunnel_count=int(json_data["tunnels"]["active_tunnel_count"]),
links=[
LinkInfo.from_json(
link_info, source=json_data["node"].lower(), ip_address=ip_address
_load_link_info(
link_info, source=json_data["node"], destination_ip=ip_address
)
for ip_address, link_info in link_info.items()
],
source_json=json_data,
)
# handle issue of failing to identify the main wireless interface
if not node_info.ip_address:
node_info.ip_address = ip_address
return node_info


def _load_legacy_system_info(json_data: dict[str, Any]) -> SystemInfo:
"""Load `sysinfo.json` in older format (API version < 1.5)."""
node_info = SystemInfo(
node_name=json_data["node"],
display_name=json_data["node"],
Expand Down Expand Up @@ -574,3 +531,48 @@ def _load_interfaces(values: list[dict]) -> dict[str, Interface]:
for obj in values
)
return {iface.name: iface for iface in interfaces}


def _load_link_info(
json_data: dict[str, Any], *, source: str, destination_ip: str
) -> LinkInfo:
"""Construct the `LinkInfo` dataclass from the AREDN JSON information.
Needs the name of the source node and I passed in as well.
Args:
json_data: JSON link information from `sysinfo.json`
source: Hostname of source node (no domain)
destination_ip: IP address of link destination
"""
# fix example of a DTD link that wasn't properly identified as such
missing_dtd = (
json_data["linkType"] == "" and json_data["olsrInterface"] == "br-dtdlink"
)
type_ = "DTD" if missing_dtd else json_data["linkType"]
try:
link_type = getattr(LinkType, type_)
except AttributeError as exc:
logger.warning("Unknown link type", error=str(exc))
link_type = LinkType.UNKNOWN

# ensure consistent node names
node_name = json_data["hostname"].replace(".local.mesh", "").lstrip(".").lower()
if (link_cost := json_data.get("linkCost")) is not None and link_cost > 99.99:
link_cost = 99.99

return LinkInfo(
source=source,
destination=node_name,
destination_ip=destination_ip,
type=link_type,
interface=json_data["olsrInterface"],
quality=json_data["linkQuality"],
neighbor_quality=json_data["neighborLinkQuality"],
signal=json_data.get("signal"),
noise=json_data.get("noise"),
tx_rate=json_data.get("tx_rate"),
rx_rate=json_data.get("rx_rate"),
olsr_cost=link_cost,
)
8 changes: 4 additions & 4 deletions tests/test_aredn.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,8 @@ def test_invalid_link_json():
"olsrInterface": "eth.0",
"linkType": "foobar",
}
link_info = aredn.LinkInfo.from_json(
link_json, source="n0call-nsm1", ip_address="10.1.1.1"
link_info = aredn._load_link_info(
link_json, source="n0call-nsm1", destination_ip="10.1.1.1"
)
expected = aredn.LinkInfo(
source="n0call-nsm1",
Expand Down Expand Up @@ -381,8 +381,8 @@ def test_link_cost_cap():
"lastHelloTime": 0,
"signal": -83,
}
link_info = aredn.LinkInfo.from_json(
link_json, source="N0CALL-hAP-AC-4", ip_address="10.84.160.54"
link_info = aredn._load_link_info(
link_json, source="N0CALL-hAP-AC-4", destination_ip="10.84.160.54"
)
expected = aredn.LinkInfo(
source="N0CALL-hAP-AC-4",
Expand Down

0 comments on commit 4cd5877

Please sign in to comment.