feat!: fix templates with the caller's values replace run_device_action (0.2.0)
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 24s
CI / test (3.12) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 25s
CI / test (3.11) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 25s
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 24s
CI / test (3.12) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 25s
CI / test (3.11) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 25s
fix_snmp set no community at all and opened the management zone's udp/161 to every source, with a rule named after an application; a second fix_snmp in the system mixin was shadowed and never ran (#5). install_lldpd, install_auc and install_coreutils_base64 were actions of their own. The driver implements napalm-device-types 4.13's templates instead: - configure_snmp_agent(settings): snmpd installed when missing; community, contact and location in UCI with one com2sec (com2sec6) per allowed source; the zone-resolving firewall step of the old fix, now limited to the sources (src_ip) and named Allow-SNMP; snmpd enabled, restarted and listening; - get_management_peer() from SSH_CONNECTION; - ensure_package(name) from the base package, over install_package. A community, contact or location that would break UCI is refused. BREAKING CHANGE: run_device_action is removed. Requires napalm-device-types 4.13. Closes #5. Part of #13.
This commit is contained in:
1 parent
d569a289ba
commit
968d6583ba
5 files changed
+244
-300
No files matched your search
@@ -14,7 +14,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time as _time
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -258,156 +257,6 @@ class OpenWrtSystemMixin:
|
||||
|
||||
return SNMPConfigDict(running=True, community=community, port=161, version="2c")
|
||||
|
||||
def _action_fix_snmp(self) -> dict[str, Any]:
|
||||
"""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 the management zone
|
||||
try:
|
||||
# Use `ss` (always available on OpenWrt) to find the IP of the current SSH client
|
||||
raw_conn = self._send_command(
|
||||
"ss -tnp 2>/dev/null | awk '/ESTAB.*:22/{print $5}' | head -1 | sed 's/:[0-9]*$//'"
|
||||
).strip()
|
||||
# Strip IPv6-mapped prefix if present
|
||||
if raw_conn.startswith("::ffff:"):
|
||||
raw_conn = raw_conn[7:]
|
||||
peer_ip = raw_conn.splitlines()[-1].strip() if raw_conn else ""
|
||||
|
||||
if peer_ip and peer_ip not in ("", "0.0.0.0", "::"):
|
||||
# Determine which firewall zone owns the interface that routes to peer_ip.
|
||||
# Walk uci firewall zones: find the zone whose associated network interface
|
||||
# has an address in the same /24 as peer_ip.
|
||||
peer_prefix = peer_ip.rsplit(".", 1)[0] if "." in peer_ip else ""
|
||||
fw_out = self._send_command("uci show firewall 2>/dev/null || true")
|
||||
src_zone = "*"
|
||||
# Build map zone_name → zone_uci_key
|
||||
zone_map: dict[str, str] = {}
|
||||
for fw_line in fw_out.splitlines():
|
||||
m = re.match(r"firewall\.(\w+)\.name='([^']+)'", fw_line.strip())
|
||||
if m:
|
||||
zone_map[m.group(2)] = m.group(1)
|
||||
# For each zone find its network, then the interface IP
|
||||
for zone_name, uci_key in zone_map.items():
|
||||
net_line = self._send_command(
|
||||
f"uci -q get firewall.{uci_key}.network 2>/dev/null || true"
|
||||
).strip()
|
||||
if not net_line:
|
||||
continue
|
||||
for net_name in net_line.split():
|
||||
iface_ip = self._send_command(
|
||||
f"uci -q get network.{net_name}.ipaddr 2>/dev/null || true"
|
||||
).strip()
|
||||
if iface_ip and "." in iface_ip:
|
||||
iface_prefix = iface_ip.split("/")[0].rsplit(".", 1)[0]
|
||||
if peer_prefix and iface_prefix == peer_prefix:
|
||||
src_zone = zone_name
|
||||
break
|
||||
if src_zone != "*":
|
||||
break
|
||||
|
||||
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-from-mgmt'; "
|
||||
f"uci set firewall.snmp_netork.src='{src_zone}'; "
|
||||
f"uci set firewall.snmp_netork.dest_port='161'; "
|
||||
f"uci set firewall.snmp_netork.proto='udp'; "
|
||||
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 (src zone: {src_zone}).")
|
||||
else:
|
||||
lines.append("[firewall] Could not detect peer IP via ss — skipping firewall step.")
|
||||
except Exception as exc:
|
||||
lines.append(f"[firewall] skipped — {exc}")
|
||||
|
||||
# 4. Break any crash-loop, then start cleanly
|
||||
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)}
|
||||
|
||||
def get_users(self) -> dict[str, Any]:
|
||||
"""Return users configured on the device.
|
||||
|
||||
|
||||
Reference in new issue
Block a user