fix: fix_snmp SSH fallback when driver is connected via REST API

The REST API has no writable config endpoint for SNMP communities.
When _action_fix_snmp() is called on a REST-connected driver, it now
opens a temporary Netmiko SSH session (standard KEX, then legacy KEX
fallback), runs the config-mode CLI commands, and restores the REST
transport state on exit. SSH errors are surfaced verbatim instead of
being silently swallowed.

Also:
- _save_config: guard against REST transport (was calling self._device
  directly → NoneType AttributeError when transport == "api")
- _action_fix_snmp: extracted CLI logic into _action_fix_snmp_cli()
  so it is reused for both the SSH-fallback and native SSH/Telnet paths
- api_client: add get_snmp_communities() + configure_snmp_community()
  for future use; improve GET error logging (warn vs debug on non-404)
- _try_api: probe timeout capped at 15 s; SSL retry loop for
  self-signed certificates; improved log levels
- _api_set_interface / _cli_set_interface: handle enabled/description
  fields that were previously ignored
- probe(): broaden accepted status codes (301/302/303/403)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-09 23:12:32 +02:00
co-authored by Claude Sonnet 4.6
parent aa55eb2bb9
commit 0cab237b1d
2 changed files with 153 additions and 37 deletions
+38 -3
View File
@@ -79,7 +79,9 @@ class ProcurveApiClient:
# 401 = API exists but not authenticated (most common)
# 200 = API exists and accessible without auth (unusual)
# 405 = wrong HTTP verb but API is there
if resp.status_code in (200, 401, 405):
# 302/301/303 = redirect to login page (older firmware)
# 403 = API exists but access forbidden
if resp.status_code in (200, 301, 302, 303, 401, 403, 405):
logger.debug(
"API detected at %s (HTTP %s, %s %s)",
hostname, resp.status_code, proto, ver,
@@ -169,9 +171,12 @@ class ProcurveApiClient:
resp = self._session.get(url, timeout=self.timeout)
if resp.ok:
return resp.json()
logger.debug("GET %s returned HTTP %s", url, resp.status_code)
if resp.status_code == 404:
logger.debug("GET %s returned HTTP 404", url)
else:
logger.warning("GET %s returned HTTP %s: %s", url, resp.status_code, resp.text[:200])
except Exception as exc:
logger.debug("GET %s error: %s", url, exc)
logger.warning("GET %s error: %s", url, exc)
return {}
def post(self, endpoint: str, **kwargs: Any) -> requests.Response:
@@ -481,6 +486,36 @@ class ProcurveApiClient:
f"delete_vlan({vlan_id}): REST API returned HTTP {resp.status_code}"
)
def get_snmp_communities(self) -> Dict[str, Dict]:
"""Return configured SNMP communities from REST API."""
data = self.get("snmp-server/community")
result: Dict[str, Dict] = {}
for entry in data.get("snmp_community_element", []):
name = entry.get("community_name", "")
if name:
result[name] = {"access_type": entry.get("access_type", "")}
return result
def configure_snmp_community(self, community: str = "public") -> Tuple[bool, str]:
"""Ensure an SNMP read-only community exists via REST resource endpoint.
Returns (success, message).
"""
existing = self.get_snmp_communities()
if community in existing:
return True, f"SNMP community '{community}' already exists."
resp = self.post("snmp-server/community", json={
"community_name": community,
"access_type": "MIB2",
})
if resp.ok:
return True, f"SNMP community '{community}' configured via REST API."
if resp.status_code == 409:
return True, f"SNMP community '{community}' already exists."
return False, f"REST API returned HTTP {resp.status_code}: {resp.text[:200]}"
def ping(
self,
destination: str,