feat!: configure_snmp_agent with the caller's values replaces fix_snmp (0.3.0)
CI / test (3.10) (push) Successful in 24s
CI / test (3.11) (push) Successful in 23s
CI / test (3.12) (push) Successful in 25s
CI / test (3.10) (pull_request) Successful in 24s
CI / test (3.11) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 24s

fix_snmp hard-coded community "public" and sent its config-mode line
through _send_command, which waits for the exec prompt: in config mode
it ran into its timeout and left the session there (#6).

netgear_smart implements napalm-device-types 4.13's configure_snmp_agent
steps: community (read-only), contact and location from the caller
through _apply_config_lines, which waits for any prompt and reports a
refused line; config mode is left in all cases; the configuration is
saved and the community checked in show snmp. v7 firmware, which has
no SNMP commands in its CLI, says so. A value that would break the CLI
is refused.

napalm-device-types is now declared in pyproject.toml and
requirements.txt (#7).

BREAKING CHANGE: run_device_action is removed.

Closes #6. Part of #7 and #8.
This commit is contained in:
Christian Manivong committed 2026-10-17 08:38:07 +02:00
1 parent 16bd276e82
commit 34554eade9
4 files changed
+134 -41

No files matched your search

+54 -40
View File
@@ -36,7 +36,7 @@ import netaddr
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoAuthenticationException, NetmikoTimeoutException
from napalm_device_types import ConfigLifecycleMixin, FingerprintRule, SwitchDriver
from napalm_device_types import ConfigLifecycleMixin, FingerprintRule, SnmpAgentMixin, SwitchDriver
from napalm.base import helpers as napalm_helpers
from napalm.base.exceptions import (
CommandErrorException,
@@ -49,7 +49,7 @@ from napalm.base.netmiko_helpers import netmiko_args
import napalm.base.constants as C
class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver):
class NetgearSmartDriver(ConfigLifecycleMixin, SnmpAgentMixin, SwitchDriver):
"""NAPALM driver for Netgear Smart Managed switches (GS-series)."""
VENDOR = "Netgear"
@@ -74,7 +74,10 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver):
#: CPU is text ("5 Secs ( 3.2%) 60 Secs ( …"); the five-second figure counts.
SNMP_HEALTH_PROFILE: ClassVar[Dict[str, Any]] = {
"uptime": {"oids": ["1.3.6.1.2.1.1.3.0"]},
"cpu": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.9.0"], "pattern": r"5\s+Secs\s*\(\s*([\d.]+)\s*%\)"},
"cpu": {
"oids": ["1.3.6.1.4.1.4526.100.1.1.4.9.0"],
"pattern": r"5\s+Secs\s*\(\s*([\d.]+)\s*%\)",
},
"memory": {
"total": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.2.0"]},
"free": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.1.0"]},
@@ -1877,50 +1880,61 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver):
return SNMPConfigDict(running=True, community=community, port=161, version="2c")
def run_device_action(self, action: str) -> Dict:
"""Execute a named action on the switch."""
if action == "fix_snmp":
return self._action_fix_snmp()
raise NotImplementedError(f"Unknown action: {action!r}")
# ------------------------------------------------------------------
# SNMP agent (napalm-device-types SnmpAgentMixin): the caller's values
# ------------------------------------------------------------------
def _action_fix_snmp(self) -> Dict:
"""Enable SNMP with community 'public' (read-only) on the Netgear Smart switch."""
def _install_snmp_agent(self) -> Dict[str, Any]:
return {"ok": True, "output": "the SNMP agent is built in"}
def _write_snmp_agent_config(self, settings: Dict[str, Any]) -> Dict[str, Any]:
if self._detect_cli_v7():
# The "v7" Cisco-like CLI (e.g. GS110TPv3 7.x) has no
# "snmp-server"/"show snmp" commands at all — SNMP can only be
# enabled via the web UI on this firmware generation.
# The "v7" Cisco-like CLI (e.g. GS110TPv3 7.x) has no "snmp-server"
# commands at all: SNMP is set in the web UI on this firmware.
return {
"success": False,
"output": (
"SNMP cannot be configured via CLI on this switch's firmware "
"(no 'snmp-server' commands available). Enable SNMP manually "
"via the switch's web UI, or ignore this warning."
),
"ok": False,
"output": "this firmware has no SNMP commands in its CLI; "
"SNMP is set in its web UI",
}
lines: list = []
community = settings["community"]
if not re.match(r'^[^\s"#]+$', community):
raise ValueError("the community must be one word without quotes or #")
lines = [f"snmp-server community {community} ro"]
for key in ("contact", "location"):
value = settings.get(key, "")
if value:
if '"' in value or "\n" in value:
raise ValueError(f"{key} must be one line without double quotes")
lines.append(f'snmp-server {key} "{value}"')
self._enter_config_mode()
# Netgear Smart CLI: snmp-server community <name> ro
self._send_command("snmp-server community public ro")
lines.append("[config] SNMP community 'public' (read-only) configured.")
self._exit_config_mode()
self._save_config()
lines.append("[config] Configuration saved.")
# Verify
try:
out = self._send_command("show snmp")
success = "public" in out
except Exception:
success = False
out = ""
if success:
lines.append("[ok] SNMP active with community 'public'.")
else:
lines.append(f"[warn] Verification failed: {out[:100]}")
errors = self._apply_config_lines("\n".join(lines))
finally:
self._exit_config_mode()
if errors:
return {"ok": False, "output": "the switch refused:\n" + "\n".join(errors)}
return {"ok": True, "output": "community (read-only), contact and location set"}
return {"success": success, "output": "\n".join(lines)}
@staticmethod
def _allow_snmp_sources(settings: Dict[str, Any]) -> Dict[str, Any]:
sources = ", ".join(settings.get("allowed_sources", [])) or "none given"
note = f"no host firewall on the switch; sources not enforced ({sources})"
return {"ok": True, "output": note}
def _start_snmp_agent(self) -> Dict[str, Any]:
self._save_config()
return {"ok": True, "output": "configuration saved"}
def configure_snmp_agent(self, settings: Dict[str, Any]) -> Dict[str, Any]:
"""The template's steps, then a check that the switch lists the community."""
result = SnmpAgentMixin.configure_snmp_agent(self, settings)
if not result["success"]:
return result
listed = self._send_command("show snmp")
if re.search(rf"(?<!\S){re.escape(settings['community'])}(?!\S)", listed):
return result
missing = f"[verify] the switch does not list the community: {listed[:200]}"
return {**result, "success": False, "output": f"{result['output']}\n{missing}"}
# ------------------------------------------------------------------
# PoE