diff --git a/napalm_openwrt/openwrt.py b/napalm_openwrt/openwrt.py index 11f6f2c..84bf77d 100644 --- a/napalm_openwrt/openwrt.py +++ b/napalm_openwrt/openwrt.py @@ -1881,6 +1881,8 @@ class OpenWrtDriver(AccessPointDriver): return self._action_install_auc() if 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}") 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 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)}