feat(system): report remote syslog, LuCI state and bridge STP
get_system_config() already parsed the whole system section but returned
only a slice of it, so netOrk had no IST side for three AP-profile fields
and could not compare them at all — the fields were editable, stored and
silently ineffective.
Adds:
* syslog_remote / syslog_ip / syslog_port / syslog_proto — read from the
system section that was already being parsed. log_remote gates the
others: OpenWrt ships nothing without it, so a leftover log_ip must
not read as an active target.
* luci_enabled — from the uhttpd init script rather than its listener
config, so the answer stays "is the web UI served" and re-enabling
restores whatever was configured before.
* bridge_stp — None when there is no br-ap at all, which is a different
statement from "bridge without STP" and has to stay distinguishable.
netOrk #164
This commit is contained in:
@@ -75,9 +75,17 @@ class OpenWrtSystemMixin:
|
|||||||
* dropbear_port (int) — SSH port
|
* dropbear_port (int) — SSH port
|
||||||
* dropbear_password_auth (bool) — whether password login is allowed
|
* dropbear_password_auth (bool) — whether password login is allowed
|
||||||
* dropbear_root_password_auth (bool)
|
* dropbear_root_password_auth (bool)
|
||||||
|
* syslog_remote (bool) — whether logs are shipped off the device
|
||||||
|
* syslog_ip (str), syslog_port (int), syslog_proto (str)
|
||||||
|
* luci_enabled (bool) — whether the uhttpd service serving LuCI is enabled
|
||||||
|
* bridge_stp (bool | None) — STP on br-ap; None when there is no br-ap
|
||||||
"""
|
"""
|
||||||
sys_out = self._send_command("uci show system 2>/dev/null || true")
|
sys_out = self._send_command("uci show system 2>/dev/null || true")
|
||||||
db_out = self._send_command("uci show dropbear 2>/dev/null || true")
|
db_out = self._send_command("uci show dropbear 2>/dev/null || true")
|
||||||
|
luci_out = self._send_command(
|
||||||
|
"/etc/init.d/uhttpd enabled 2>/dev/null && echo 1 || echo 0"
|
||||||
|
)
|
||||||
|
net_out = self._send_command("uci show network 2>/dev/null || true")
|
||||||
|
|
||||||
sys_cfg: dict[str, str] = {}
|
sys_cfg: dict[str, str] = {}
|
||||||
for line in sys_out.splitlines():
|
for line in sys_out.splitlines():
|
||||||
@@ -109,6 +117,31 @@ class OpenWrtSystemMixin:
|
|||||||
def _bool_uci(val: str, default: bool = True) -> bool:
|
def _bool_uci(val: str, default: bool = True) -> bool:
|
||||||
return val.lower() not in ("0", "off", "false", "no") if val else default
|
return val.lower() not in ("0", "off", "false", "no") if val else default
|
||||||
|
|
||||||
|
# Remote syslog. OpenWrt only ships logs when log_remote is set, so a
|
||||||
|
# leftover log_ip without it means nothing is being sent.
|
||||||
|
syslog_remote = _bool_uci(sys_cfg.get("log_remote", ""), default=False)
|
||||||
|
try:
|
||||||
|
syslog_port = int(sys_cfg.get("log_port", "514") or "514")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
syslog_port = 514
|
||||||
|
|
||||||
|
# STP on the AP bridge. None when there is no br-ap device section at
|
||||||
|
# all — "no bridge" is a different statement from "bridge without STP".
|
||||||
|
bridge_stp: bool | None = None
|
||||||
|
br_section: str | None = None
|
||||||
|
for line in net_out.splitlines():
|
||||||
|
m = re.match(r"network\.(\w+)\.name='br-ap'", line.strip())
|
||||||
|
if m:
|
||||||
|
br_section = m.group(1)
|
||||||
|
break
|
||||||
|
if br_section:
|
||||||
|
bridge_stp = False
|
||||||
|
for line in net_out.splitlines():
|
||||||
|
m = re.match(rf"network\.{br_section}\.stp='([^']*)'", line.strip())
|
||||||
|
if m:
|
||||||
|
bridge_stp = _bool_uci(m.group(1), default=False)
|
||||||
|
break
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"hostname": sys_cfg.get("hostname", ""),
|
"hostname": sys_cfg.get("hostname", ""),
|
||||||
"timezone": sys_cfg.get("timezone", ""),
|
"timezone": sys_cfg.get("timezone", ""),
|
||||||
@@ -117,6 +150,12 @@ class OpenWrtSystemMixin:
|
|||||||
"dropbear_port": ssh_port,
|
"dropbear_port": ssh_port,
|
||||||
"dropbear_password_auth": _bool_uci(db_cfg.get("PasswordAuth", "on")),
|
"dropbear_password_auth": _bool_uci(db_cfg.get("PasswordAuth", "on")),
|
||||||
"dropbear_root_password_auth": _bool_uci(db_cfg.get("RootPasswordAuth", "on")),
|
"dropbear_root_password_auth": _bool_uci(db_cfg.get("RootPasswordAuth", "on")),
|
||||||
|
"syslog_remote": syslog_remote,
|
||||||
|
"syslog_ip": sys_cfg.get("log_ip", "") if syslog_remote else "",
|
||||||
|
"syslog_port": syslog_port,
|
||||||
|
"syslog_proto": sys_cfg.get("log_proto", "udp"),
|
||||||
|
"luci_enabled": luci_out.strip().endswith("1"),
|
||||||
|
"bridge_stp": bridge_stp,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_snmp_information(self) -> dict[str, Any]:
|
def get_snmp_information(self) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -1734,3 +1734,136 @@ class TestUciSectionParser:
|
|||||||
def test_blank_and_malformed_lines_ignored(self, driver):
|
def test_blank_and_malformed_lines_ignored(self, driver):
|
||||||
parsed = driver._parse_uci_sections("\n\nnot a uci line\nfirewall.x=rule\n")
|
parsed = driver._parse_uci_sections("\n\nnot a uci line\nfirewall.x=rule\n")
|
||||||
assert list(parsed) == ["x"]
|
assert list(parsed) == ["x"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# get_system_config — remote syslog, LuCI and bridge STP (netOrk #164)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCI_SYSTEM_WITH_SYSLOG = """\
|
||||||
|
system.@system[0]=system
|
||||||
|
system.@system[0].hostname='ap-eze-Garten'
|
||||||
|
system.@system[0].timezone='CET-1CEST,M3.5.0,M10.5.0/3'
|
||||||
|
system.@system[0].zonename='Europe/Berlin'
|
||||||
|
system.@system[0].log_remote='1'
|
||||||
|
system.@system[0].log_ip='10.10.40.2'
|
||||||
|
system.@system[0].log_port='5514'
|
||||||
|
system.@system[0].log_proto='udp'
|
||||||
|
system.ntp=timeserver
|
||||||
|
system.ntp.server='0.openwrt.pool.ntp.org' '1.openwrt.pool.ntp.org'
|
||||||
|
"""
|
||||||
|
|
||||||
|
UCI_SYSTEM_NO_SYSLOG = """\
|
||||||
|
system.@system[0]=system
|
||||||
|
system.@system[0].hostname='ap-eze-Parkplatz'
|
||||||
|
system.@system[0].timezone='GMT0'
|
||||||
|
system.@system[0].zonename='UTC'
|
||||||
|
system.ntp=timeserver
|
||||||
|
system.ntp.server='0.openwrt.pool.ntp.org'
|
||||||
|
"""
|
||||||
|
|
||||||
|
UCI_DROPBEAR_SYS = """\
|
||||||
|
dropbear.@dropbear[0]=dropbear
|
||||||
|
dropbear.@dropbear[0].Port='22'
|
||||||
|
dropbear.@dropbear[0].PasswordAuth='on'
|
||||||
|
dropbear.@dropbear[0].RootPasswordAuth='on'
|
||||||
|
"""
|
||||||
|
|
||||||
|
UCI_NETWORK_STP_ON = """\
|
||||||
|
network.ap_bridge=device
|
||||||
|
network.ap_bridge.name='br-ap'
|
||||||
|
network.ap_bridge.type='bridge'
|
||||||
|
network.ap_bridge.stp='1'
|
||||||
|
network.lan=interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
UCI_NETWORK_STP_OFF = """\
|
||||||
|
network.ap_bridge=device
|
||||||
|
network.ap_bridge.name='br-ap'
|
||||||
|
network.ap_bridge.type='bridge'
|
||||||
|
network.lan=interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _system_send(system_out=UCI_SYSTEM_WITH_SYSLOG, luci_out="1", network_out=UCI_NETWORK_STP_ON):
|
||||||
|
"""Dispatch _send_command by the command it receives."""
|
||||||
|
|
||||||
|
def _send(cmd, **kw):
|
||||||
|
if "uci show system" in cmd:
|
||||||
|
return system_out
|
||||||
|
if "uci show dropbear" in cmd:
|
||||||
|
return UCI_DROPBEAR_SYS
|
||||||
|
if "uci show network" in cmd:
|
||||||
|
return network_out
|
||||||
|
if "uhttpd" in cmd:
|
||||||
|
return luci_out
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return _send
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSystemConfigSyslog:
|
||||||
|
def test_remote_syslog_target_is_reported(self, driver):
|
||||||
|
driver._send_command = _system_send()
|
||||||
|
cfg = driver.get_system_config()
|
||||||
|
assert cfg["syslog_remote"] is True
|
||||||
|
assert cfg["syslog_ip"] == "10.10.40.2"
|
||||||
|
assert cfg["syslog_port"] == 5514
|
||||||
|
|
||||||
|
def test_absent_syslog_reports_as_disabled(self, driver):
|
||||||
|
driver._send_command = _system_send(UCI_SYSTEM_NO_SYSLOG)
|
||||||
|
cfg = driver.get_system_config()
|
||||||
|
assert cfg["syslog_remote"] is False
|
||||||
|
assert cfg["syslog_ip"] == ""
|
||||||
|
|
||||||
|
def test_log_ip_without_log_remote_is_not_active(self, driver):
|
||||||
|
# OpenWrt only ships logs when log_remote is set, whatever log_ip says.
|
||||||
|
out = UCI_SYSTEM_NO_SYSLOG + "system.@system[0].log_ip='10.10.40.2'\n"
|
||||||
|
driver._send_command = _system_send(out)
|
||||||
|
assert driver.get_system_config()["syslog_remote"] is False
|
||||||
|
|
||||||
|
def test_port_falls_back_to_the_openwrt_default(self, driver):
|
||||||
|
out = UCI_SYSTEM_NO_SYSLOG + (
|
||||||
|
"system.@system[0].log_remote='1'\nsystem.@system[0].log_ip='10.10.40.2'\n"
|
||||||
|
)
|
||||||
|
driver._send_command = _system_send(out)
|
||||||
|
assert driver.get_system_config()["syslog_port"] == 514
|
||||||
|
|
||||||
|
def test_existing_fields_are_untouched(self, driver):
|
||||||
|
driver._send_command = _system_send()
|
||||||
|
cfg = driver.get_system_config()
|
||||||
|
assert cfg["timezone"] == "CET-1CEST,M3.5.0,M10.5.0/3"
|
||||||
|
assert cfg["zonename"] == "Europe/Berlin"
|
||||||
|
assert cfg["dropbear_port"] == 22
|
||||||
|
assert cfg["ntp_servers"] == ["0.openwrt.pool.ntp.org", "1.openwrt.pool.ntp.org"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSystemConfigLuci:
|
||||||
|
def test_enabled_uhttpd_reports_luci_as_reachable(self, driver):
|
||||||
|
driver._send_command = _system_send(luci_out="1")
|
||||||
|
assert driver.get_system_config()["luci_enabled"] is True
|
||||||
|
|
||||||
|
def test_disabled_uhttpd_reports_luci_as_unreachable(self, driver):
|
||||||
|
driver._send_command = _system_send(luci_out="0")
|
||||||
|
assert driver.get_system_config()["luci_enabled"] is False
|
||||||
|
|
||||||
|
def test_missing_uhttpd_reports_as_unreachable(self, driver):
|
||||||
|
# No uhttpd installed at all — LuCI cannot be served.
|
||||||
|
driver._send_command = _system_send(luci_out="")
|
||||||
|
assert driver.get_system_config()["luci_enabled"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSystemConfigBridgeStp:
|
||||||
|
def test_stp_enabled_is_reported(self, driver):
|
||||||
|
driver._send_command = _system_send(network_out=UCI_NETWORK_STP_ON)
|
||||||
|
assert driver.get_system_config()["bridge_stp"] is True
|
||||||
|
|
||||||
|
def test_absent_stp_option_means_disabled(self, driver):
|
||||||
|
# UCI defaults stp to 0 when the option is not present.
|
||||||
|
driver._send_command = _system_send(network_out=UCI_NETWORK_STP_OFF)
|
||||||
|
assert driver.get_system_config()["bridge_stp"] is False
|
||||||
|
|
||||||
|
def test_no_ap_bridge_reports_none(self, driver):
|
||||||
|
# Nothing to have an opinion about — distinct from "STP is off".
|
||||||
|
driver._send_command = _system_send(network_out="network.lan=interface\n")
|
||||||
|
assert driver.get_system_config()["bridge_stp"] is None
|
||||||
|
|||||||
Reference in New Issue
Block a user