feat: SNMP support — get_snmp_config(), fix_snmp action
Install snmpd-nossl + luci-app-snmpd via opkg, configure via UCI with correct field names (group/viewname/context='none'), bare port 161, stop+pkill before start to break crash loops. get_snmp_config() reads current UCI snmpd state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1f0349aee9
commit
64964c1b33
@@ -1881,6 +1881,8 @@ class OpenWrtDriver(AccessPointDriver):
|
|||||||
return self._action_install_auc()
|
return self._action_install_auc()
|
||||||
if action == "install_coreutils_base64":
|
if action == "install_coreutils_base64":
|
||||||
return self._action_install_coreutils_base64()
|
return self._action_install_coreutils_base64()
|
||||||
|
if action == "fix_snmp":
|
||||||
|
return self._action_fix_snmp()
|
||||||
raise NotImplementedError(f"Unknown action: {action!r}")
|
raise NotImplementedError(f"Unknown action: {action!r}")
|
||||||
|
|
||||||
def _action_install_coreutils_base64(self) -> Dict:
|
def _action_install_coreutils_base64(self) -> Dict:
|
||||||
@@ -2602,3 +2604,148 @@ class OpenWrtDriver(AccessPointDriver):
|
|||||||
return {k: v for k, v in instances.items() if k == name}
|
return {k: v for k, v in instances.items() if k == name}
|
||||||
|
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
|
# ── SNMP / Health ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_snmp_config(self):
|
||||||
|
"""Return SNMP agent config if snmpd is installed and running on OpenWrt."""
|
||||||
|
try:
|
||||||
|
from napalm_device_types.models import SNMPConfigDict
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
running = (
|
||||||
|
self._send_command(
|
||||||
|
"/etc/init.d/snmpd running 2>/dev/null && echo active || echo inactive"
|
||||||
|
).strip() == "active"
|
||||||
|
)
|
||||||
|
if not running:
|
||||||
|
return None
|
||||||
|
|
||||||
|
community = "public"
|
||||||
|
try:
|
||||||
|
# UCI config (set by luci-app-snmpd)
|
||||||
|
uci_comm = self._send_command(
|
||||||
|
"uci -q get snmpd.public.community 2>/dev/null || "
|
||||||
|
"uci -q get snmpd.@com2sec[0].community 2>/dev/null || echo ''"
|
||||||
|
).strip()
|
||||||
|
if uci_comm:
|
||||||
|
community = uci_comm
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return SNMPConfigDict(running=True, community=community, port=161, version="2c")
|
||||||
|
|
||||||
|
def _action_fix_snmp(self) -> Dict:
|
||||||
|
"""Install and configure snmpd on OpenWrt.
|
||||||
|
|
||||||
|
Installs snmpd-nossl (the daemon) and luci-app-snmpd (UCI schema +
|
||||||
|
proper procd init script). Configures community 'public' via UCI.
|
||||||
|
"""
|
||||||
|
lines: list = []
|
||||||
|
|
||||||
|
# 1. Install packages — snmpd-nossl (daemon) + luci-app-snmpd (UCI init)
|
||||||
|
pm = self._pm_type()
|
||||||
|
if pm == "apk":
|
||||||
|
raw = self._send_command("apk add snmpd-nossl luci-app-snmpd 2>&1")
|
||||||
|
else:
|
||||||
|
self._send_command("opkg update 2>/dev/null || true")
|
||||||
|
raw = self._send_command("opkg install snmpd-nossl luci-app-snmpd 2>&1")
|
||||||
|
out = self._clean_pkg_output(raw)
|
||||||
|
low = out.lower()
|
||||||
|
installed = not any(kw in low for kw in ("error:", "failed"))
|
||||||
|
lines.append(f"[install] {out[-300:]}")
|
||||||
|
|
||||||
|
if not installed and "already installed" not in low:
|
||||||
|
return {"success": False, "output": "\n".join(lines)}
|
||||||
|
|
||||||
|
# 2. Configure via UCI — modify the existing default sections only.
|
||||||
|
# Do NOT create new named sections (causes duplicate directives in
|
||||||
|
# the generated /var/run/snmpd.conf which crashes snmpd).
|
||||||
|
# Also remove any stale named sections from previous fix attempts.
|
||||||
|
# The init script reads these UCI field names to generate /var/run/snmpd.conf:
|
||||||
|
# agent: agentaddress
|
||||||
|
# com2sec: secname, source, community
|
||||||
|
# group: group (name!), version, secname
|
||||||
|
# view: viewname (not name!), type, oid
|
||||||
|
# access: group, version, level, prefix, read, write, notify
|
||||||
|
# Default luci-app-snmpd schema uses different field names for group/view/access,
|
||||||
|
# so we patch all required fields explicitly.
|
||||||
|
uci_cmds = [
|
||||||
|
# Remove any stale named sections from previous runs
|
||||||
|
"uci -q delete snmpd.agent",
|
||||||
|
"uci -q delete snmpd.public",
|
||||||
|
# agent
|
||||||
|
"uci set snmpd.@agent[0].agentaddress='161'",
|
||||||
|
# com2sec
|
||||||
|
"uci set snmpd.@com2sec[0].secname='ro'",
|
||||||
|
"uci set snmpd.@com2sec[0].source='0.0.0.0/0'",
|
||||||
|
"uci set snmpd.@com2sec[0].community='public'",
|
||||||
|
# group — init script reads field 'group' (not 'name')
|
||||||
|
"uci set snmpd.@group[0].group='rogroup'",
|
||||||
|
"uci set snmpd.@group[0].version='v2c'",
|
||||||
|
"uci set snmpd.@group[0].secname='ro'",
|
||||||
|
# view — init script reads field 'viewname' (not 'name')
|
||||||
|
"uci set snmpd.@view[0].viewname='all'",
|
||||||
|
"uci set snmpd.@view[0].type='included'",
|
||||||
|
"uci set snmpd.@view[0].oid='.1'",
|
||||||
|
# access — init script needs write + notify or it returns early
|
||||||
|
"uci set snmpd.@access[0].group='rogroup'",
|
||||||
|
"uci set snmpd.@access[0].context='none'",
|
||||||
|
"uci set snmpd.@access[0].version='v2c'",
|
||||||
|
"uci set snmpd.@access[0].level='noAuthNoPriv'",
|
||||||
|
"uci set snmpd.@access[0].prefix='exact'",
|
||||||
|
"uci set snmpd.@access[0].read='all'",
|
||||||
|
"uci set snmpd.@access[0].write='none'",
|
||||||
|
"uci set snmpd.@access[0].notify='none'",
|
||||||
|
"uci commit snmpd",
|
||||||
|
]
|
||||||
|
for cmd in uci_cmds:
|
||||||
|
self._send_command(f"{cmd} 2>/dev/null || true")
|
||||||
|
lines.append("[config] Configured snmpd via UCI (all required fields set).")
|
||||||
|
|
||||||
|
# 3. Firewall: allow UDP 161 from netOrk subnet
|
||||||
|
try:
|
||||||
|
raw_conn = self._send_command(
|
||||||
|
"netstat -tn 2>/dev/null | awk '/ESTABLISHED.*:22/{print $5}' | head -1 | cut -d: -f1"
|
||||||
|
).strip()
|
||||||
|
if raw_conn and raw_conn not in ("", "0.0.0.0"):
|
||||||
|
subnet = raw_conn.rsplit(".", 1)[0] + ".0/24"
|
||||||
|
self._send_command(
|
||||||
|
f"uci -q delete firewall.snmp_netork 2>/dev/null; "
|
||||||
|
f"uci set firewall.snmp_netork=rule; "
|
||||||
|
f"uci set firewall.snmp_netork.name='Allow-SNMP-netOrk'; "
|
||||||
|
f"uci set firewall.snmp_netork.src='*'; "
|
||||||
|
f"uci set firewall.snmp_netork.dest_port='161'; "
|
||||||
|
f"uci set firewall.snmp_netork.proto='udp'; "
|
||||||
|
f"uci set firewall.snmp_netork.src_ip='{subnet}'; "
|
||||||
|
f"uci set firewall.snmp_netork.target='ACCEPT'; "
|
||||||
|
f"uci commit firewall; "
|
||||||
|
f"/etc/init.d/firewall reload 2>/dev/null || true"
|
||||||
|
)
|
||||||
|
lines.append(f"[firewall] Added UDP:161 allow rule for {subnet}.")
|
||||||
|
except Exception as exc:
|
||||||
|
lines.append(f"[firewall] skipped — {exc}")
|
||||||
|
|
||||||
|
# 4. Break any crash-loop, then start cleanly
|
||||||
|
import time as _time
|
||||||
|
self._send_command("/etc/init.d/snmpd stop 2>/dev/null; true")
|
||||||
|
_time.sleep(2)
|
||||||
|
self._send_command("pkill -9 snmpd 2>/dev/null; true") # kill crash-loop zombie
|
||||||
|
_time.sleep(3)
|
||||||
|
self._send_command("/etc/init.d/snmpd enable 2>/dev/null; true")
|
||||||
|
self._send_command("/etc/init.d/snmpd start 2>/dev/null; true")
|
||||||
|
_time.sleep(4)
|
||||||
|
lines.append("[service] snmpd started via procd.")
|
||||||
|
|
||||||
|
# 5. Check if snmpd is now active (no local snmpget on OpenWrt by default)
|
||||||
|
status = self._send_command(
|
||||||
|
"/etc/init.d/snmpd running 2>/dev/null && echo active || echo inactive"
|
||||||
|
).strip()
|
||||||
|
success = status == "active"
|
||||||
|
if success:
|
||||||
|
lines.append("[ok] snmpd is active.")
|
||||||
|
else:
|
||||||
|
lines.append(f"[warn] snmpd status: {status}")
|
||||||
|
|
||||||
|
return {"success": success, "output": "\n".join(lines)}
|
||||||
|
|||||||
Reference in New Issue
Block a user