style: ruff format and the lint rules of the driver standard
Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM driver uses (E, F, I, UP, B at line length 100, Python 3.10). What ruff cannot fix comes in the next commit.
This commit is contained in:
1 parent
739283e76b
commit
c2a788fa7a
6 files changed
+275
-246
No files matched your search
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Licensed under the Apache License, Version 2.0
|
# Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
"""REST API client for HPE/Aruba ProCurve switches (ArubaOS REST interface).
|
"""REST API client for HPE/Aruba ProCurve switches (ArubaOS REST interface).
|
||||||
@@ -8,13 +7,12 @@ Used automatically by ProcurveDriver when the REST API is reachable.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
from napalm.base import helpers as napalm_helpers
|
from napalm.base import helpers as napalm_helpers
|
||||||
from napalm.base.exceptions import ConnectionException, ConnectAuthError
|
from napalm.base.exceptions import ConnectAuthError, ConnectionException
|
||||||
from napalm_device_types import add_lag_interfaces
|
from napalm_device_types import add_lag_interfaces
|
||||||
|
|
||||||
logger = logging.getLogger("napalm_procurve.api")
|
logger = logging.getLogger("napalm_procurve.api")
|
||||||
@@ -37,7 +35,7 @@ class ProcurveApiClient:
|
|||||||
password: str,
|
password: str,
|
||||||
timeout: int = 60,
|
timeout: int = 60,
|
||||||
ssl_verify: bool = False,
|
ssl_verify: bool = False,
|
||||||
api_version: Optional[str] = None,
|
api_version: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -47,7 +45,7 @@ class ProcurveApiClient:
|
|||||||
self._api_version = api_version # None = auto-detect
|
self._api_version = api_version # None = auto-detect
|
||||||
self._proto: str = "https"
|
self._proto: str = "https"
|
||||||
self._base_url: str = ""
|
self._base_url: str = ""
|
||||||
self._session: Optional[requests.Session] = None
|
self._session: requests.Session | None = None
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Detection
|
# Detection
|
||||||
@@ -59,7 +57,7 @@ class ProcurveApiClient:
|
|||||||
hostname: str,
|
hostname: str,
|
||||||
timeout: int = 5,
|
timeout: int = 5,
|
||||||
ssl_verify: bool = False,
|
ssl_verify: bool = False,
|
||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> tuple[str | None, str | None]:
|
||||||
"""Probe the device for an ArubaOS REST API.
|
"""Probe the device for an ArubaOS REST API.
|
||||||
|
|
||||||
Returns ``(api_version, proto)`` on success, ``(None, None)`` if the
|
Returns ``(api_version, proto)`` on success, ``(None, None)`` if the
|
||||||
@@ -86,7 +84,10 @@ class ProcurveApiClient:
|
|||||||
if resp.status_code in (200, 301, 302, 303, 401, 403, 405):
|
if resp.status_code in (200, 301, 302, 303, 401, 403, 405):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"API detected at %s (HTTP %s, %s %s)",
|
"API detected at %s (HTTP %s, %s %s)",
|
||||||
hostname, resp.status_code, proto, ver,
|
hostname,
|
||||||
|
resp.status_code,
|
||||||
|
proto,
|
||||||
|
ver,
|
||||||
)
|
)
|
||||||
return ver, proto
|
return ver, proto
|
||||||
except requests.exceptions.SSLError:
|
except requests.exceptions.SSLError:
|
||||||
@@ -115,9 +116,7 @@ class ProcurveApiClient:
|
|||||||
|
|
||||||
self._session = requests.Session()
|
self._session = requests.Session()
|
||||||
self._session.verify = self.ssl_verify
|
self._session.verify = self.ssl_verify
|
||||||
self._session.headers.update(
|
self._session.headers.update({"Content-Type": "application/json", "Connection": "close"})
|
||||||
{"Content-Type": "application/json", "Connection": "close"}
|
|
||||||
)
|
|
||||||
|
|
||||||
url = self._base_url + "login-sessions"
|
url = self._base_url + "login-sessions"
|
||||||
resp = self._session.post(
|
resp = self._session.post(
|
||||||
@@ -148,9 +147,7 @@ class ProcurveApiClient:
|
|||||||
"""Log out from the REST API session."""
|
"""Log out from the REST API session."""
|
||||||
if self._session and self._base_url:
|
if self._session and self._base_url:
|
||||||
try:
|
try:
|
||||||
self._session.delete(
|
self._session.delete(self._base_url + "login-sessions", timeout=self.timeout)
|
||||||
self._base_url + "login-sessions", timeout=self.timeout
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if self._session:
|
if self._session:
|
||||||
@@ -162,9 +159,7 @@ class ProcurveApiClient:
|
|||||||
if not self._session:
|
if not self._session:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
resp = self._session.get(
|
resp = self._session.get(self._base_url + "system/status", timeout=5)
|
||||||
self._base_url + "system/status", timeout=5
|
|
||||||
)
|
|
||||||
return resp.status_code == 200
|
return resp.status_code == 200
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
@@ -173,7 +168,7 @@ class ProcurveApiClient:
|
|||||||
# HTTP helpers
|
# HTTP helpers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get(self, endpoint: str) -> Dict[str, Any]:
|
def get(self, endpoint: str) -> dict[str, Any]:
|
||||||
"""GET ``{base_url}{endpoint}`` and return parsed JSON (or empty dict)."""
|
"""GET ``{base_url}{endpoint}`` and return parsed JSON (or empty dict)."""
|
||||||
url = self._base_url + endpoint
|
url = self._base_url + endpoint
|
||||||
try:
|
try:
|
||||||
@@ -183,7 +178,9 @@ class ProcurveApiClient:
|
|||||||
if resp.status_code == 404:
|
if resp.status_code == 404:
|
||||||
logger.debug("GET %s returned HTTP 404", url)
|
logger.debug("GET %s returned HTTP 404", url)
|
||||||
else:
|
else:
|
||||||
logger.warning("GET %s returned HTTP %s: %s", url, resp.status_code, resp.text[:200])
|
logger.warning(
|
||||||
|
"GET %s returned HTTP %s: %s", url, resp.status_code, resp.text[:200]
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("GET %s error: %s", url, exc)
|
logger.warning("GET %s error: %s", url, exc)
|
||||||
return {}
|
return {}
|
||||||
@@ -204,7 +201,7 @@ class ProcurveApiClient:
|
|||||||
# NAPALM data getters (API-backed)
|
# NAPALM data getters (API-backed)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_facts(self) -> Dict:
|
def get_facts(self) -> dict:
|
||||||
"""Return NAPALM facts from the REST API."""
|
"""Return NAPALM facts from the REST API."""
|
||||||
system = self.get("system/status")
|
system = self.get("system/status")
|
||||||
if not system:
|
if not system:
|
||||||
@@ -230,6 +227,7 @@ class ProcurveApiClient:
|
|||||||
model_raw = system.get("product_model", "")
|
model_raw = system.get("product_model", "")
|
||||||
# Extract J-code part numbers (e.g. J9777A) from the model string
|
# Extract J-code part numbers (e.g. J9777A) from the model string
|
||||||
import re as _re
|
import re as _re
|
||||||
|
|
||||||
_PN_RE = _re.compile(r"\b(J\d{4}[A-Z]{1,2})\b")
|
_PN_RE = _re.compile(r"\b(J\d{4}[A-Z]{1,2})\b")
|
||||||
pn_match = _PN_RE.search(model_raw)
|
pn_match = _PN_RE.search(model_raw)
|
||||||
part_number = pn_match.group(1) if pn_match else ""
|
part_number = pn_match.group(1) if pn_match else ""
|
||||||
@@ -253,14 +251,14 @@ class ProcurveApiClient:
|
|||||||
"interface_list": iface_list,
|
"interface_list": iface_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_interfaces(self) -> Dict[str, Dict]:
|
def get_interfaces(self) -> dict[str, dict]:
|
||||||
"""Return NAPALM interfaces from the REST API."""
|
"""Return NAPALM interfaces from the REST API."""
|
||||||
ports_data = self.get("ports")
|
ports_data = self.get("ports")
|
||||||
stats_data = self.get("port-statistics")
|
stats_data = self.get("port-statistics")
|
||||||
|
|
||||||
output: Dict[str, Dict] = {}
|
output: dict[str, dict] = {}
|
||||||
trunk_groups: Dict[str, list] = {}
|
trunk_groups: dict[str, list] = {}
|
||||||
trunk_modes: Dict[str, str] = {}
|
trunk_modes: dict[str, str] = {}
|
||||||
|
|
||||||
port_elements = list(ports_data.get("port_element", []))
|
port_elements = list(ports_data.get("port_element", []))
|
||||||
|
|
||||||
@@ -274,13 +272,15 @@ class ProcurveApiClient:
|
|||||||
for p in blade.get("data_ports", []):
|
for p in blade.get("data_ports", []):
|
||||||
pid = p.get("port_name", "")
|
pid = p.get("port_name", "")
|
||||||
if pid and pid not in seen_ids:
|
if pid and pid not in seen_ids:
|
||||||
port_elements.append({
|
port_elements.append(
|
||||||
|
{
|
||||||
"id": pid,
|
"id": pid,
|
||||||
"name": "",
|
"name": "",
|
||||||
"is_port_up": p.get("operStatus") == "OPER_UP",
|
"is_port_up": p.get("operStatus") == "OPER_UP",
|
||||||
"is_port_enabled": p.get("adminStatus") == "ADMIN_UP",
|
"is_port_enabled": p.get("adminStatus") == "ADMIN_UP",
|
||||||
"trunk_group": "",
|
"trunk_group": "",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
seen_ids.add(pid)
|
seen_ids.add(pid)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -309,15 +309,18 @@ class ProcurveApiClient:
|
|||||||
output[pid]["speed"] = float(stat.get("port_speed_mbps", 0))
|
output[pid]["speed"] = float(stat.get("port_speed_mbps", 0))
|
||||||
|
|
||||||
# One row per LAG/trunk group alongside its member ports.
|
# One row per LAG/trunk group alongside its member ports.
|
||||||
return add_lag_interfaces(output, {
|
return add_lag_interfaces(
|
||||||
|
output,
|
||||||
|
{
|
||||||
group: "lacp" if trunk_modes.get(group) == "PTT_LACP" else "trunk"
|
group: "lacp" if trunk_modes.get(group) == "PTT_LACP" else "trunk"
|
||||||
for group in trunk_groups
|
for group in trunk_groups
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_interfaces_ip(self) -> Dict[str, Dict]:
|
def get_interfaces_ip(self) -> dict[str, dict]:
|
||||||
"""Return NAPALM interfaces IP from the REST API."""
|
"""Return NAPALM interfaces IP from the REST API."""
|
||||||
vlans_data = self.get("vlans")
|
vlans_data = self.get("vlans")
|
||||||
result: Dict[str, Dict] = {}
|
result: dict[str, dict] = {}
|
||||||
|
|
||||||
for vlan in vlans_data.get("vlan_element", []):
|
for vlan in vlans_data.get("vlan_element", []):
|
||||||
vlan_id = str(vlan.get("vlan_id", ""))
|
vlan_id = str(vlan.get("vlan_id", ""))
|
||||||
@@ -331,6 +334,7 @@ class ProcurveApiClient:
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
plen = netaddr.IPNetwork(f"{addr}/{mask}").prefixlen
|
plen = netaddr.IPNetwork(f"{addr}/{mask}").prefixlen
|
||||||
except Exception:
|
except Exception:
|
||||||
plen = 24
|
plen = 24
|
||||||
@@ -343,12 +347,12 @@ class ProcurveApiClient:
|
|||||||
# PoE
|
# PoE
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_poe_ports(self) -> Dict[str, Dict]:
|
def get_poe_ports(self) -> dict[str, dict]:
|
||||||
"""Return PoE configuration for all ports, keyed by port id."""
|
"""Return PoE configuration for all ports, keyed by port id."""
|
||||||
data = self.get("poe/ports")
|
data = self.get("poe/ports")
|
||||||
return {p["port_id"]: p for p in data.get("port_poe", []) if p.get("port_id")}
|
return {p["port_id"]: p for p in data.get("port_poe", []) if p.get("port_id")}
|
||||||
|
|
||||||
def set_port_poe(self, port_id: str, payload: Dict[str, Any]) -> requests.Response:
|
def set_port_poe(self, port_id: str, payload: dict[str, Any]) -> requests.Response:
|
||||||
"""PUT PoE configuration changes for a single port."""
|
"""PUT PoE configuration changes for a single port."""
|
||||||
return self.put(f"ports/{port_id}/poe", json=payload)
|
return self.put(f"ports/{port_id}/poe", json=payload)
|
||||||
|
|
||||||
@@ -390,9 +394,9 @@ class ProcurveApiClient:
|
|||||||
)
|
)
|
||||||
return table
|
return table
|
||||||
|
|
||||||
def get_lldp_neighbors(self) -> Dict[str, list]:
|
def get_lldp_neighbors(self) -> dict[str, list]:
|
||||||
"""Return NAPALM LLDP neighbors from the REST API."""
|
"""Return NAPALM LLDP neighbors from the REST API."""
|
||||||
result: Dict[str, list] = {}
|
result: dict[str, list] = {}
|
||||||
data = self.get("lldp/remote-device")
|
data = self.get("lldp/remote-device")
|
||||||
for nbr in data.get("lldp_remote_device_element", []):
|
for nbr in data.get("lldp_remote_device_element", []):
|
||||||
local_port = nbr.get("local_port", "")
|
local_port = nbr.get("local_port", "")
|
||||||
@@ -404,9 +408,9 @@ class ProcurveApiClient:
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_lldp_neighbors_detail(self) -> Dict[str, list]:
|
def get_lldp_neighbors_detail(self) -> dict[str, list]:
|
||||||
"""Return NAPALM LLDP neighbor details from the REST API."""
|
"""Return NAPALM LLDP neighbor details from the REST API."""
|
||||||
result: Dict[str, list] = {}
|
result: dict[str, list] = {}
|
||||||
data = self.get("lldp/remote-device")
|
data = self.get("lldp/remote-device")
|
||||||
for nbr in data.get("lldp_remote_device_element", []):
|
for nbr in data.get("lldp_remote_device_element", []):
|
||||||
local_port = nbr.get("local_port", "")
|
local_port = nbr.get("local_port", "")
|
||||||
@@ -424,7 +428,7 @@ class ProcurveApiClient:
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_config(self) -> Dict[str, str]:
|
def get_config(self) -> dict[str, str]:
|
||||||
"""Return running and startup configuration via REST API.
|
"""Return running and startup configuration via REST API.
|
||||||
|
|
||||||
AOS-Switch returns the config as plain text, not JSON.
|
AOS-Switch returns the config as plain text, not JSON.
|
||||||
@@ -447,17 +451,17 @@ class ProcurveApiClient:
|
|||||||
logger.warning("GET %s error: %s", url, exc)
|
logger.warning("GET %s error: %s", url, exc)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_ntp_servers(self) -> Dict[str, Dict]:
|
def get_ntp_servers(self) -> dict[str, dict]:
|
||||||
"""Return NTP servers from REST API."""
|
"""Return NTP servers from REST API."""
|
||||||
data = self.get("ntp/server")
|
data = self.get("ntp/server")
|
||||||
servers: Dict[str, Dict] = {}
|
servers: dict[str, dict] = {}
|
||||||
for entry in data.get("ntp_server_element", []):
|
for entry in data.get("ntp_server_element", []):
|
||||||
addr = entry.get("server_address", "")
|
addr = entry.get("server_address", "")
|
||||||
if addr:
|
if addr:
|
||||||
servers[addr] = {}
|
servers[addr] = {}
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
def get_vlans(self) -> Dict[int, Dict]:
|
def get_vlans(self) -> dict[int, dict]:
|
||||||
"""Return VLAN information with proper tagged/untagged separation.
|
"""Return VLAN information with proper tagged/untagged separation.
|
||||||
|
|
||||||
Fetches ``/vlans`` for VLAN names and ``/vlans-ports`` for port membership.
|
Fetches ``/vlans`` for VLAN names and ``/vlans-ports`` for port membership.
|
||||||
@@ -491,7 +495,7 @@ class ProcurveApiClient:
|
|||||||
ports_data = self.get("vlans-ports")
|
ports_data = self.get("vlans-ports")
|
||||||
|
|
||||||
# Build VLAN skeleton from /vlans
|
# Build VLAN skeleton from /vlans
|
||||||
result: Dict[int, Dict] = {}
|
result: dict[int, dict] = {}
|
||||||
for vlan in vlans_data.get("vlan_element", []):
|
for vlan in vlans_data.get("vlan_element", []):
|
||||||
try:
|
try:
|
||||||
vid = int(vlan["vlan_id"])
|
vid = int(vlan["vlan_id"])
|
||||||
@@ -522,8 +526,7 @@ class ProcurveApiClient:
|
|||||||
|
|
||||||
# Untagged variants: POM_UNTAGGED, POM_NATIVE_UNTAGGED, "Untagged"
|
# Untagged variants: POM_UNTAGGED, POM_NATIVE_UNTAGGED, "Untagged"
|
||||||
is_untagged = any(
|
is_untagged = any(
|
||||||
kw in mode_low
|
kw in mode_low for kw in ("untagged", "native_untagged", "pom_untagged")
|
||||||
for kw in ("untagged", "native_untagged", "pom_untagged")
|
|
||||||
) and "tagged" not in mode_low.replace("untagged", "")
|
) and "tagged" not in mode_low.replace("untagged", "")
|
||||||
|
|
||||||
# More precise: contains "untagged" anywhere → untagged
|
# More precise: contains "untagged" anywhere → untagged
|
||||||
@@ -576,17 +579,17 @@ class ProcurveApiClient:
|
|||||||
f"delete_vlan({vlan_id}): REST API returned HTTP {resp.status_code}"
|
f"delete_vlan({vlan_id}): REST API returned HTTP {resp.status_code}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_snmp_communities(self) -> Dict[str, Dict]:
|
def get_snmp_communities(self) -> dict[str, dict]:
|
||||||
"""Return configured SNMP communities from REST API."""
|
"""Return configured SNMP communities from REST API."""
|
||||||
data = self.get("snmp-server/community")
|
data = self.get("snmp-server/community")
|
||||||
result: Dict[str, Dict] = {}
|
result: dict[str, dict] = {}
|
||||||
for entry in data.get("snmp_community_element", []):
|
for entry in data.get("snmp_community_element", []):
|
||||||
name = entry.get("community_name", "")
|
name = entry.get("community_name", "")
|
||||||
if name:
|
if name:
|
||||||
result[name] = {"access_type": entry.get("access_type", "")}
|
result[name] = {"access_type": entry.get("access_type", "")}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def configure_snmp_community(self, community: str = "public") -> Tuple[bool, str]:
|
def configure_snmp_community(self, community: str = "public") -> tuple[bool, str]:
|
||||||
"""Ensure an SNMP read-only community exists via REST resource endpoint.
|
"""Ensure an SNMP read-only community exists via REST resource endpoint.
|
||||||
|
|
||||||
Returns (success, message).
|
Returns (success, message).
|
||||||
@@ -595,10 +598,13 @@ class ProcurveApiClient:
|
|||||||
if community in existing:
|
if community in existing:
|
||||||
return True, f"SNMP community '{community}' already exists."
|
return True, f"SNMP community '{community}' already exists."
|
||||||
|
|
||||||
resp = self.post("snmp-server/community", json={
|
resp = self.post(
|
||||||
|
"snmp-server/community",
|
||||||
|
json={
|
||||||
"community_name": community,
|
"community_name": community,
|
||||||
"access_type": "MIB2",
|
"access_type": "MIB2",
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if resp.ok:
|
if resp.ok:
|
||||||
return True, f"SNMP community '{community}' configured via REST API."
|
return True, f"SNMP community '{community}' configured via REST API."
|
||||||
if resp.status_code == 409:
|
if resp.status_code == 409:
|
||||||
@@ -615,9 +621,9 @@ class ProcurveApiClient:
|
|||||||
size: int = 100,
|
size: int = 100,
|
||||||
count: int = 5,
|
count: int = 5,
|
||||||
vrf: str = "",
|
vrf: str = "",
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
"""Execute ping via REST API (if supported) or return not-implemented."""
|
"""Execute ping via REST API (if supported) or return not-implemented."""
|
||||||
payload: Dict = {"destination": destination, "count": count, "timeout": timeout}
|
payload: dict = {"destination": destination, "count": count, "timeout": timeout}
|
||||||
if source:
|
if source:
|
||||||
payload["source"] = source
|
payload["source"] = source
|
||||||
|
|
||||||
|
|||||||
+59
-60
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Licensed under the Apache License, Version 2.0
|
# Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
"""CLI output parsers for HPE/Aruba ProCurve switches.
|
"""CLI output parsers for HPE/Aruba ProCurve switches.
|
||||||
@@ -9,13 +8,12 @@ backends.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# MAC address helpers
|
# MAC address helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def procurve_mac_to_std(mac: str) -> str:
|
def procurve_mac_to_std(mac: str) -> str:
|
||||||
"""Convert ProCurve MAC format ``a1b2c3-d4e5f6`` to ``a1:b2:c3:d4:e5:f6``."""
|
"""Convert ProCurve MAC format ``a1b2c3-d4e5f6`` to ``a1:b2:c3:d4:e5:f6``."""
|
||||||
clean = mac.replace("-", "").replace(":", "").replace(".", "").lower()
|
clean = mac.replace("-", "").replace(":", "").replace(".", "").lower()
|
||||||
@@ -38,7 +36,8 @@ def _kv(output: str, key: str) -> str:
|
|||||||
# show system-information
|
# show system-information
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_system_info(output: str) -> Dict:
|
|
||||||
|
def parse_system_info(output: str) -> dict:
|
||||||
"""Parse ``show system-information`` output.
|
"""Parse ``show system-information`` output.
|
||||||
|
|
||||||
Returns a dict with keys: hostname, os_version, serial_number, model,
|
Returns a dict with keys: hostname, os_version, serial_number, model,
|
||||||
@@ -63,10 +62,8 @@ def parse_system_info(output: str) -> Dict:
|
|||||||
# between them, e.g.:
|
# between them, e.g.:
|
||||||
# " Software revision : R.11.27 Base MAC Addr : a1b2c3-d4e5f6"
|
# " Software revision : R.11.27 Base MAC Addr : a1b2c3-d4e5f6"
|
||||||
# We capture all k:v pairs on every line.
|
# We capture all k:v pairs on every line.
|
||||||
_KV_RE = re.compile(
|
_KV_RE = re.compile(r"([A-Za-z][A-Za-z0-9 ()/#-]{1,40}?)\s*:\s*(\S.*?)(?=\s{3,}[A-Za-z]|$)")
|
||||||
r"([A-Za-z][A-Za-z0-9 ()/#-]{1,40}?)\s*:\s*(\S.*?)(?=\s{3,}[A-Za-z]|$)"
|
kv_map: dict[str, str] = {}
|
||||||
)
|
|
||||||
kv_map: Dict[str, str] = {}
|
|
||||||
for raw_line in output.splitlines():
|
for raw_line in output.splitlines():
|
||||||
for m in _KV_RE.finditer(raw_line):
|
for m in _KV_RE.finditer(raw_line):
|
||||||
k = m.group(1).strip()
|
k = m.group(1).strip()
|
||||||
@@ -163,6 +160,7 @@ def _parse_procurve_uptime(uptime_str: str) -> float:
|
|||||||
# show version
|
# show version
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def parse_version(output: str) -> str:
|
def parse_version(output: str) -> str:
|
||||||
"""Extract firmware version from ``show version`` or ``show system-information``.
|
"""Extract firmware version from ``show version`` or ``show system-information``.
|
||||||
|
|
||||||
@@ -245,13 +243,13 @@ _INTF_BRIEF_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_interfaces_brief(output: str) -> Dict[str, Dict]:
|
def parse_interfaces_brief(output: str) -> dict[str, dict]:
|
||||||
"""Parse ``show interfaces brief`` tabular output.
|
"""Parse ``show interfaces brief`` tabular output.
|
||||||
|
|
||||||
Returns dict keyed by port name with sub-keys:
|
Returns dict keyed by port name with sub-keys:
|
||||||
is_up, is_enabled, speed, duplex, description.
|
is_up, is_enabled, speed, duplex, description.
|
||||||
"""
|
"""
|
||||||
interfaces: Dict[str, Dict] = {}
|
interfaces: dict[str, dict] = {}
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
m = _INTF_BRIEF_RE.match(line)
|
m = _INTF_BRIEF_RE.match(line)
|
||||||
if not m:
|
if not m:
|
||||||
@@ -260,9 +258,7 @@ def parse_interfaces_brief(output: str) -> Dict[str, Dict]:
|
|||||||
speed = 0.0
|
speed = 0.0
|
||||||
duplex = ""
|
duplex = ""
|
||||||
# Mode examples: "1000FDx", "100HDx", "Unknown"; MDI is "Auto", "MDI", "MDIX", "NA"
|
# Mode examples: "1000FDx", "100HDx", "Unknown"; MDI is "Auto", "MDI", "MDIX", "NA"
|
||||||
sm = next(
|
sm = next(filter(None, (re.match(r"(\d+)(FDx|HDx)?", t, re.I) for t in mode_or_mdi)), None)
|
||||||
filter(None, (re.match(r"(\d+)(FDx|HDx)?", t, re.I) for t in mode_or_mdi)), None
|
|
||||||
)
|
|
||||||
if sm:
|
if sm:
|
||||||
speed = float(sm.group(1))
|
speed = float(sm.group(1))
|
||||||
duplex = "full" if (sm.group(2) or "").lower() == "fdx" else "half"
|
duplex = "full" if (sm.group(2) or "").lower() == "fdx" else "half"
|
||||||
@@ -286,7 +282,8 @@ def parse_interfaces_brief(output: str) -> Dict[str, Dict]:
|
|||||||
# show interfaces (per-port detail for MAC / description)
|
# show interfaces (per-port detail for MAC / description)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_interface_detail(output: str, port: str) -> Dict:
|
|
||||||
|
def parse_interface_detail(output: str, port: str) -> dict:
|
||||||
"""Parse ``show interfaces <port>`` for MAC address and description."""
|
"""Parse ``show interfaces <port>`` for MAC address and description."""
|
||||||
mac = procurve_mac_to_std(_kv(output, "MAC address"))
|
mac = procurve_mac_to_std(_kv(output, "MAC address"))
|
||||||
name = _kv(output, "Name") or _kv(output, "Description")
|
name = _kv(output, "Name") or _kv(output, "Description")
|
||||||
@@ -297,7 +294,8 @@ def parse_interface_detail(output: str, port: str) -> Dict:
|
|||||||
# show ip / show ip interface
|
# show ip / show ip interface
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_ip_interfaces(output: str) -> Dict[str, Dict]:
|
|
||||||
|
def parse_ip_interfaces(output: str) -> dict[str, dict]:
|
||||||
"""Parse ``show ip`` or ``show ip interface brief`` output.
|
"""Parse ``show ip`` or ``show ip interface brief`` output.
|
||||||
|
|
||||||
Returns NAPALM-compatible dict::
|
Returns NAPALM-compatible dict::
|
||||||
@@ -314,7 +312,7 @@ def parse_ip_interfaces(output: str) -> Dict[str, Dict]:
|
|||||||
IP Configuration for VLAN 99:
|
IP Configuration for VLAN 99:
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
result: Dict[str, Dict] = {}
|
result: dict[str, dict] = {}
|
||||||
current_vlan = ""
|
current_vlan = ""
|
||||||
current_ip = ""
|
current_ip = ""
|
||||||
|
|
||||||
@@ -343,6 +341,7 @@ def parse_ip_interfaces(output: str) -> Dict[str, Dict]:
|
|||||||
mask = line.split(":", 1)[-1].strip()
|
mask = line.split(":", 1)[-1].strip()
|
||||||
try:
|
try:
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
prefix_len = netaddr.IPNetwork(f"{current_ip}/{mask}").prefixlen
|
prefix_len = netaddr.IPNetwork(f"{current_ip}/{mask}").prefixlen
|
||||||
except Exception:
|
except Exception:
|
||||||
prefix_len = 24
|
prefix_len = 24
|
||||||
@@ -369,16 +368,14 @@ _ARP_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_arp_table(output: str) -> List[Dict]:
|
def parse_arp_table(output: str) -> list[dict]:
|
||||||
"""Parse ``show arp`` output into NAPALM ARP table format."""
|
"""Parse ``show arp`` output into NAPALM ARP table format."""
|
||||||
table = []
|
table = []
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
m = _ARP_RE.search(line)
|
m = _ARP_RE.search(line)
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
ip, mac, entry_type, port = (
|
ip, mac, entry_type, port = (m.group(1), m.group(2), m.group(3), m.group(4) or "")
|
||||||
m.group(1), m.group(2), m.group(3), m.group(4) or ""
|
|
||||||
)
|
|
||||||
table.append(
|
table.append(
|
||||||
{
|
{
|
||||||
"interface": port,
|
"interface": port,
|
||||||
@@ -402,7 +399,7 @@ _MAC_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_mac_table(output: str) -> List[Dict]:
|
def parse_mac_table(output: str) -> list[dict]:
|
||||||
"""Parse ``show mac-address`` into NAPALM MAC table format."""
|
"""Parse ``show mac-address`` into NAPALM MAC table format."""
|
||||||
table = []
|
table = []
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
@@ -428,7 +425,8 @@ def parse_mac_table(output: str) -> List[Dict]:
|
|||||||
# show lldp info remote-device
|
# show lldp info remote-device
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_lldp_neighbors(output: str) -> Dict[str, List[Dict]]:
|
|
||||||
|
def parse_lldp_neighbors(output: str) -> dict[str, list[dict]]:
|
||||||
"""Parse ``show lldp info remote-device`` into NAPALM neighbors dict.
|
"""Parse ``show lldp info remote-device`` into NAPALM neighbors dict.
|
||||||
|
|
||||||
Output format::
|
Output format::
|
||||||
@@ -437,7 +435,7 @@ def parse_lldp_neighbors(output: str) -> Dict[str, List[Dict]]:
|
|||||||
--------- + ------------------- ------------ ------------ ...
|
--------- + ------------------- ------------ ------------ ...
|
||||||
1 | a1 b2 c3 d4 e5 f6 Gi 0/1 otherswitch ...
|
1 | a1 b2 c3 d4 e5 f6 Gi 0/1 otherswitch ...
|
||||||
"""
|
"""
|
||||||
neighbors: Dict[str, List[Dict]] = {}
|
neighbors: dict[str, list[dict]] = {}
|
||||||
in_table = False
|
in_table = False
|
||||||
|
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
@@ -472,21 +470,19 @@ def parse_lldp_neighbors(output: str) -> Dict[str, List[Dict]]:
|
|||||||
if re.match(r"^[0-9a-fA-F]{12}$", chassis):
|
if re.match(r"^[0-9a-fA-F]{12}$", chassis):
|
||||||
chassis = ":".join(chassis[i : i + 2] for i in range(0, 12, 2))
|
chassis = ":".join(chassis[i : i + 2] for i in range(0, 12, 2))
|
||||||
|
|
||||||
neighbors.setdefault(local_port, []).append(
|
neighbors.setdefault(local_port, []).append({"hostname": sys_name, "port": port_id})
|
||||||
{"hostname": sys_name, "port": port_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
return neighbors
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
def parse_lldp_neighbors_detail(output: str) -> Dict[str, List[Dict]]:
|
def parse_lldp_neighbors_detail(output: str) -> dict[str, list[dict]]:
|
||||||
"""Parse ``show lldp info remote-device detail`` output.
|
"""Parse ``show lldp info remote-device detail`` output.
|
||||||
|
|
||||||
Returns NAPALM lldp_neighbors_detail format.
|
Returns NAPALM lldp_neighbors_detail format.
|
||||||
"""
|
"""
|
||||||
neighbors: Dict[str, List[Dict]] = {}
|
neighbors: dict[str, list[dict]] = {}
|
||||||
current_port = ""
|
current_port = ""
|
||||||
entry: Dict = {}
|
entry: dict = {}
|
||||||
|
|
||||||
def _save():
|
def _save():
|
||||||
if current_port and entry:
|
if current_port and entry:
|
||||||
@@ -516,9 +512,11 @@ def parse_lldp_neighbors_detail(output: str) -> Dict[str, List[Dict]]:
|
|||||||
low = line.lower()
|
low = line.lower()
|
||||||
if "chassis id" in low:
|
if "chassis id" in low:
|
||||||
val = line.split(":", 1)[-1].strip()
|
val = line.split(":", 1)[-1].strip()
|
||||||
entry["remote_chassis_id"] = procurve_mac_to_std(
|
entry["remote_chassis_id"] = (
|
||||||
re.sub(r"\s+", "", val)
|
procurve_mac_to_std(re.sub(r"\s+", "", val))
|
||||||
) if re.search(r"[0-9a-fA-F ]{14,}", val) else val
|
if re.search(r"[0-9a-fA-F ]{14,}", val)
|
||||||
|
else val
|
||||||
|
)
|
||||||
elif "port id" in low and "descr" not in low:
|
elif "port id" in low and "descr" not in low:
|
||||||
entry["remote_port"] = line.split(":", 1)[-1].strip()
|
entry["remote_port"] = line.split(":", 1)[-1].strip()
|
||||||
elif "port description" in low:
|
elif "port description" in low:
|
||||||
@@ -528,9 +526,7 @@ def parse_lldp_neighbors_detail(output: str) -> Dict[str, List[Dict]]:
|
|||||||
elif "system description" in low:
|
elif "system description" in low:
|
||||||
entry["remote_system_description"] = line.split(":", 1)[-1].strip()
|
entry["remote_system_description"] = line.split(":", 1)[-1].strip()
|
||||||
elif "system capabilities" in low and "enabled" not in low:
|
elif "system capabilities" in low and "enabled" not in low:
|
||||||
entry["remote_system_capab"] = _parse_capabilities(
|
entry["remote_system_capab"] = _parse_capabilities(line.split(":", 1)[-1].strip())
|
||||||
line.split(":", 1)[-1].strip()
|
|
||||||
)
|
|
||||||
elif "enabled capabilities" in low or ("system capabilities" in low and "enabled" in low):
|
elif "enabled capabilities" in low or ("system capabilities" in low and "enabled" in low):
|
||||||
entry["remote_system_enable_capab"] = _parse_capabilities(
|
entry["remote_system_enable_capab"] = _parse_capabilities(
|
||||||
line.split(":", 1)[-1].strip()
|
line.split(":", 1)[-1].strip()
|
||||||
@@ -540,7 +536,7 @@ def parse_lldp_neighbors_detail(output: str) -> Dict[str, List[Dict]]:
|
|||||||
return neighbors
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
def _parse_capabilities(cap_str: str) -> List[str]:
|
def _parse_capabilities(cap_str: str) -> list[str]:
|
||||||
"""Convert ProCurve capability abbreviations to NAPALM strings."""
|
"""Convert ProCurve capability abbreviations to NAPALM strings."""
|
||||||
_MAP = {
|
_MAP = {
|
||||||
"B": "bridge",
|
"B": "bridge",
|
||||||
@@ -566,12 +562,13 @@ def _parse_capabilities(cap_str: str) -> List[str]:
|
|||||||
# show ntp associations / show timesync
|
# show ntp associations / show timesync
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_ntp_servers(output: str) -> Dict[str, Dict]:
|
|
||||||
|
def parse_ntp_servers(output: str) -> dict[str, dict]:
|
||||||
"""Parse ``show ntp associations`` or ``show ntp status`` output.
|
"""Parse ``show ntp associations`` or ``show ntp status`` output.
|
||||||
|
|
||||||
Returns dict of {server_ip: {}} as per NAPALM get_ntp_servers.
|
Returns dict of {server_ip: {}} as per NAPALM get_ntp_servers.
|
||||||
"""
|
"""
|
||||||
servers: Dict[str, Dict] = {}
|
servers: dict[str, dict] = {}
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
# " 10.0.0.1 sys.peer 2 ..." or "NTP server: 10.0.0.1"
|
# " 10.0.0.1 sys.peer 2 ..." or "NTP server: 10.0.0.1"
|
||||||
m = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", line)
|
m = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", line)
|
||||||
@@ -588,9 +585,10 @@ def parse_ntp_servers(output: str) -> Dict[str, Dict]:
|
|||||||
# show environment
|
# show environment
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_environment(output: str) -> Dict:
|
|
||||||
|
def parse_environment(output: str) -> dict:
|
||||||
"""Parse ``show environment`` output into NAPALM environment dict."""
|
"""Parse ``show environment`` output into NAPALM environment dict."""
|
||||||
env: Dict = {
|
env: dict = {
|
||||||
"fans": {},
|
"fans": {},
|
||||||
"temperature": {},
|
"temperature": {},
|
||||||
"power": {},
|
"power": {},
|
||||||
@@ -629,9 +627,7 @@ def parse_environment(output: str) -> Dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Fan status
|
# Fan status
|
||||||
for m in re.finditer(
|
for m in re.finditer(r"Fan\s*(\d+)\s*[:\s]+(\S+)", output, re.I):
|
||||||
r"Fan\s*(\d+)\s*[:\s]+(\S+)", output, re.I
|
|
||||||
):
|
|
||||||
fan_id = f"fan_{m.group(1)}"
|
fan_id = f"fan_{m.group(1)}"
|
||||||
status = m.group(2).lower()
|
status = m.group(2).lower()
|
||||||
env["fans"][fan_id] = {"status": status in ("ok", "good", "normal", "operating")}
|
env["fans"][fan_id] = {"status": status in ("ok", "good", "normal", "operating")}
|
||||||
@@ -643,12 +639,13 @@ def parse_environment(output: str) -> Dict:
|
|||||||
# show local-users
|
# show local-users
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_users(output: str) -> Dict[str, Dict]:
|
|
||||||
|
def parse_users(output: str) -> dict[str, dict]:
|
||||||
"""Parse ``show local-users`` or ``show users`` output.
|
"""Parse ``show local-users`` or ``show users`` output.
|
||||||
|
|
||||||
Returns NAPALM users dict.
|
Returns NAPALM users dict.
|
||||||
"""
|
"""
|
||||||
users: Dict[str, Dict] = {}
|
users: dict[str, dict] = {}
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
# " manager Operator Enabled ..." or " manager Manager ..."
|
# " manager Operator Enabled ..." or " manager Manager ..."
|
||||||
m = re.match(
|
m = re.match(
|
||||||
@@ -678,9 +675,10 @@ def parse_users(output: str) -> Dict[str, Dict]:
|
|||||||
# show snmp-server / show snmp
|
# show snmp-server / show snmp
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def parse_snmp_info(output: str) -> Dict:
|
|
||||||
|
def parse_snmp_info(output: str) -> dict:
|
||||||
"""Parse ``show snmp-server`` output into NAPALM snmp_information dict."""
|
"""Parse ``show snmp-server`` output into NAPALM snmp_information dict."""
|
||||||
communities: Dict[str, Dict] = {}
|
communities: dict[str, dict] = {}
|
||||||
contact = _kv(output, "System Contact") or _kv(output, "Contact")
|
contact = _kv(output, "System Contact") or _kv(output, "Contact")
|
||||||
location = _kv(output, "System Location") or _kv(output, "Location")
|
location = _kv(output, "System Location") or _kv(output, "Location")
|
||||||
chassis_id = ""
|
chassis_id = ""
|
||||||
@@ -708,7 +706,8 @@ def parse_snmp_info(output: str) -> Dict:
|
|||||||
# VLAN helpers
|
# VLAN helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def expand_procurve_ports(port_str: str) -> List[str]:
|
|
||||||
|
def expand_procurve_ports(port_str: str) -> list[str]:
|
||||||
"""Expand a ProCurve port-list string into a flat list of port names.
|
"""Expand a ProCurve port-list string into a flat list of port names.
|
||||||
|
|
||||||
Handles:
|
Handles:
|
||||||
@@ -717,7 +716,7 @@ def expand_procurve_ports(port_str: str) -> List[str]:
|
|||||||
- Trunk ports: ``Trk1,Trk2`` → ``["Trk1","Trk2"]``
|
- Trunk ports: ``Trk1,Trk2`` → ``["Trk1","Trk2"]``
|
||||||
- Mixed: ``1-3,Trk1,A1`` → ``["1","2","3","Trk1","A1"]``
|
- Mixed: ``1-3,Trk1,A1`` → ``["1","2","3","Trk1","A1"]``
|
||||||
"""
|
"""
|
||||||
ports: List[str] = []
|
ports: list[str] = []
|
||||||
for token in port_str.split(","):
|
for token in port_str.split(","):
|
||||||
token = token.strip()
|
token = token.strip()
|
||||||
if not token:
|
if not token:
|
||||||
@@ -746,7 +745,7 @@ def expand_procurve_ports(port_str: str) -> List[str]:
|
|||||||
return ports
|
return ports
|
||||||
|
|
||||||
|
|
||||||
def parse_vlans_from_running_config(config_text: str) -> Dict[int, Dict]:
|
def parse_vlans_from_running_config(config_text: str) -> dict[int, dict]:
|
||||||
"""Parse VLAN blocks from ProCurve ``show running-config`` output.
|
"""Parse VLAN blocks from ProCurve ``show running-config`` output.
|
||||||
|
|
||||||
Extracts ``name``, ``tagged``, and ``untagged`` port lists per VLAN.
|
Extracts ``name``, ``tagged``, and ``untagged`` port lists per VLAN.
|
||||||
@@ -777,8 +776,8 @@ def parse_vlans_from_running_config(config_text: str) -> Dict[int, Dict]:
|
|||||||
"untagged": []},
|
"untagged": []},
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
vlans: Dict[int, Dict] = {}
|
vlans: dict[int, dict] = {}
|
||||||
current_id: Optional[int] = None
|
current_id: int | None = None
|
||||||
|
|
||||||
for line in config_text.splitlines():
|
for line in config_text.splitlines():
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
@@ -818,7 +817,7 @@ def parse_vlans_from_running_config(config_text: str) -> Dict[int, Dict]:
|
|||||||
|
|
||||||
# Populate "interfaces" as union of tagged + untagged (NAPALM standard key)
|
# Populate "interfaces" as union of tagged + untagged (NAPALM standard key)
|
||||||
for vdata in vlans.values():
|
for vdata in vlans.values():
|
||||||
seen: List[str] = []
|
seen: list[str] = []
|
||||||
for p in vdata["untagged"] + vdata["tagged"]:
|
for p in vdata["untagged"] + vdata["tagged"]:
|
||||||
if p not in seen:
|
if p not in seen:
|
||||||
seen.append(p)
|
seen.append(p)
|
||||||
@@ -827,7 +826,7 @@ def parse_vlans_from_running_config(config_text: str) -> Dict[int, Dict]:
|
|||||||
return vlans
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
def parse_vlans_brief(output: str) -> Dict[int, str]:
|
def parse_vlans_brief(output: str) -> dict[int, str]:
|
||||||
"""Parse ``show vlans`` brief table for VLAN IDs and names only.
|
"""Parse ``show vlans`` brief table for VLAN IDs and names only.
|
||||||
|
|
||||||
Used as a discovery step when running-config is unavailable.
|
Used as a discovery step when running-config is unavailable.
|
||||||
@@ -841,7 +840,7 @@ def parse_vlans_brief(output: str) -> Dict[int, str]:
|
|||||||
|
|
||||||
Returns ``{vlan_id: name}`` dict.
|
Returns ``{vlan_id: name}`` dict.
|
||||||
"""
|
"""
|
||||||
vlans: Dict[int, str] = {}
|
vlans: dict[int, str] = {}
|
||||||
in_table = False
|
in_table = False
|
||||||
|
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
@@ -857,7 +856,7 @@ def parse_vlans_brief(output: str) -> Dict[int, str]:
|
|||||||
return vlans
|
return vlans
|
||||||
|
|
||||||
|
|
||||||
def parse_vlan_ports_detail(output: str) -> Dict[str, List[str]]:
|
def parse_vlan_ports_detail(output: str) -> dict[str, list[str]]:
|
||||||
"""Parse ``show vlans <id>`` per-VLAN port detail output.
|
"""Parse ``show vlans <id>`` per-VLAN port detail output.
|
||||||
|
|
||||||
Returns ``{"tagged": [...], "untagged": [...]}`` for ONE vlan.
|
Returns ``{"tagged": [...], "untagged": [...]}`` for ONE vlan.
|
||||||
@@ -870,8 +869,8 @@ def parse_vlan_ports_detail(output: str) -> Dict[str, List[str]]:
|
|||||||
2 Tagged Learn Up
|
2 Tagged Learn Up
|
||||||
Trk1 Tagged Learn Up
|
Trk1 Tagged Learn Up
|
||||||
"""
|
"""
|
||||||
tagged: List[str] = []
|
tagged: list[str] = []
|
||||||
untagged: List[str] = []
|
untagged: list[str] = []
|
||||||
in_table = False
|
in_table = False
|
||||||
|
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
@@ -892,7 +891,7 @@ def parse_vlan_ports_detail(output: str) -> Dict[str, List[str]]:
|
|||||||
return {"tagged": tagged, "untagged": untagged}
|
return {"tagged": tagged, "untagged": untagged}
|
||||||
|
|
||||||
|
|
||||||
def derive_port_pvids(vlans: Dict[int, Dict]) -> Dict[str, int]:
|
def derive_port_pvids(vlans: dict[int, dict]) -> dict[str, int]:
|
||||||
"""Derive PVID (native/access VLAN) for each port from the ``get_vlans`` result.
|
"""Derive PVID (native/access VLAN) for each port from the ``get_vlans`` result.
|
||||||
|
|
||||||
On ProCurve a port is untagged in exactly ONE VLAN — that is its PVID.
|
On ProCurve a port is untagged in exactly ONE VLAN — that is its PVID.
|
||||||
@@ -910,7 +909,7 @@ def derive_port_pvids(vlans: Dict[int, Dict]) -> Dict[str, int]:
|
|||||||
})
|
})
|
||||||
# → {"1": 1, "2": 1, "3": 1, "5": 1}
|
# → {"1": 1, "2": 1, "3": 1, "5": 1}
|
||||||
"""
|
"""
|
||||||
pvids: Dict[str, int] = {}
|
pvids: dict[str, int] = {}
|
||||||
|
|
||||||
# First pass: assign PVID from untagged membership
|
# First pass: assign PVID from untagged membership
|
||||||
for vid, vdata in sorted(vlans.items()):
|
for vid, vdata in sorted(vlans.items()):
|
||||||
|
|||||||
+114
-120
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Licensed under the Apache License, Version 2.0
|
# Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
"""NAPALM driver for HPE/Aruba ProCurve switches.
|
"""NAPALM driver for HPE/Aruba ProCurve switches.
|
||||||
@@ -18,15 +17,16 @@ Netmiko device type: ``hp_procurve`` / ``hp_procurve_telnet``
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import socket
|
from typing import Any, ClassVar
|
||||||
from typing import Any, ClassVar, Dict, List, Optional, Union
|
|
||||||
|
|
||||||
import netaddr
|
from napalm.base.exceptions import (
|
||||||
from netmiko import ConnectHandler
|
CommandErrorException,
|
||||||
from netmiko.exceptions import (
|
ConnectionClosedException,
|
||||||
NetmikoAuthenticationException,
|
ConnectionException,
|
||||||
NetmikoTimeoutException,
|
MergeConfigException,
|
||||||
|
ReplaceConfigException,
|
||||||
)
|
)
|
||||||
|
from napalm.base.netmiko_helpers import netmiko_args
|
||||||
from napalm_device_types import (
|
from napalm_device_types import (
|
||||||
ConfigLifecycleMixin,
|
ConfigLifecycleMixin,
|
||||||
FingerprintRule,
|
FingerprintRule,
|
||||||
@@ -35,24 +35,20 @@ from napalm_device_types import (
|
|||||||
run_fix_steps,
|
run_fix_steps,
|
||||||
)
|
)
|
||||||
from napalm_device_types.models import InterfaceConfigDict, VlanConfigDict
|
from napalm_device_types.models import InterfaceConfigDict, VlanConfigDict
|
||||||
from napalm.base import helpers as napalm_helpers
|
|
||||||
from napalm.base.exceptions import (
|
|
||||||
CommandErrorException,
|
|
||||||
ConnectionException,
|
|
||||||
ConnectionClosedException,
|
|
||||||
MergeConfigException,
|
|
||||||
ReplaceConfigException,
|
|
||||||
)
|
|
||||||
from napalm.base.netmiko_helpers import netmiko_args
|
|
||||||
from napalm_device_types.ssh_host_keys import (
|
from napalm_device_types.ssh_host_keys import (
|
||||||
KNOWN_KEYS_ARG,
|
KNOWN_KEYS_ARG,
|
||||||
OBSERVER_ARG,
|
OBSERVER_ARG,
|
||||||
HostKeyCheck,
|
HostKeyCheck,
|
||||||
SshHostKeyMismatch,
|
SshHostKeyMismatch,
|
||||||
)
|
)
|
||||||
|
from netmiko import ConnectHandler
|
||||||
|
from netmiko.exceptions import (
|
||||||
|
NetmikoAuthenticationException,
|
||||||
|
NetmikoTimeoutException,
|
||||||
|
)
|
||||||
|
|
||||||
from napalm_procurve.api_client import ProcurveApiClient
|
|
||||||
from napalm_procurve import parsers
|
from napalm_procurve import parsers
|
||||||
|
from napalm_procurve.api_client import ProcurveApiClient
|
||||||
|
|
||||||
logger = logging.getLogger("napalm_procurve")
|
logger = logging.getLogger("napalm_procurve")
|
||||||
|
|
||||||
@@ -118,7 +114,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
#: Where the switch publishes its health values over SNMP
|
#: Where the switch publishes its health values over SNMP
|
||||||
#: (napalm-device-types ``SnmpHealthProfile``); the application polls.
|
#: (napalm-device-types ``SnmpHealthProfile``); the application polls.
|
||||||
#: Memory is NETSWITCH-MIB ``hpLocalMemEntry``: .5 total, .6 free, .7 allocated.
|
#: Memory is NETSWITCH-MIB ``hpLocalMemEntry``: .5 total, .6 free, .7 allocated.
|
||||||
SNMP_HEALTH_PROFILE: ClassVar[Dict[str, Any]] = {
|
SNMP_HEALTH_PROFILE: ClassVar[dict[str, Any]] = {
|
||||||
"uptime": {"oids": ["1.3.6.1.4.1.11.2.14.11.1.2.4.1.5.1"]},
|
"uptime": {"oids": ["1.3.6.1.4.1.11.2.14.11.1.2.4.1.5.1"]},
|
||||||
"cpu": {"oids": ["1.3.6.1.4.1.11.2.14.11.5.1.9.6.1.0", "1.3.6.1.4.1.11.2.3.7.8.6.1.0"]},
|
"cpu": {"oids": ["1.3.6.1.4.1.11.2.14.11.5.1.9.6.1.0", "1.3.6.1.4.1.11.2.3.7.8.6.1.0"]},
|
||||||
"memory": {
|
"memory": {
|
||||||
@@ -134,7 +130,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
timeout: int = 60,
|
timeout: int = 60,
|
||||||
optional_args: Optional[Dict] = None,
|
optional_args: dict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -147,11 +143,11 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# --- optional_args ---
|
# --- optional_args ---
|
||||||
self.port: int = optional_args.get("port", 22)
|
self.port: int = optional_args.get("port", 22)
|
||||||
self.telnet_port: int = optional_args.get("telnet_port", 23)
|
self.telnet_port: int = optional_args.get("telnet_port", 23)
|
||||||
self.force_transport: Optional[str] = optional_args.get("transport")
|
self.force_transport: str | None = optional_args.get("transport")
|
||||||
self.ssl_verify: bool = optional_args.get(
|
self.ssl_verify: bool = optional_args.get(
|
||||||
"ssl_verify", optional_args.get("verify_ssl", optional_args.get("verify", False))
|
"ssl_verify", optional_args.get("verify_ssl", optional_args.get("verify", False))
|
||||||
)
|
)
|
||||||
self.api_version_hint: Optional[str] = optional_args.get("api_version")
|
self.api_version_hint: str | None = optional_args.get("api_version")
|
||||||
# Enable password for CLI (defaults to login password)
|
# Enable password for CLI (defaults to login password)
|
||||||
self._secret: str = optional_args.get("secret", password)
|
self._secret: str = optional_args.get("secret", password)
|
||||||
|
|
||||||
@@ -160,24 +156,24 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
|
|
||||||
self.netmiko_optional_args = netmiko_args(optional_args)
|
self.netmiko_optional_args = netmiko_args(optional_args)
|
||||||
# The host keys to expect and where the one seen is reported (netork#1167).
|
# The host keys to expect and where the one seen is reported (netork#1167).
|
||||||
self._host_key_args: Dict[str, Any] = {
|
self._host_key_args: dict[str, Any] = {
|
||||||
k: optional_args[k] for k in (KNOWN_KEYS_ARG, OBSERVER_ARG) if k in optional_args
|
k: optional_args[k] for k in (KNOWN_KEYS_ARG, OBSERVER_ARG) if k in optional_args
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- runtime state ---
|
# --- runtime state ---
|
||||||
# Active transport: "api" | "ssh" | "ssh_legacy" | "telnet"
|
# Active transport: "api" | "ssh" | "ssh_legacy" | "telnet"
|
||||||
self._transport: Optional[str] = None
|
self._transport: str | None = None
|
||||||
# CLI backend (SSH or Telnet)
|
# CLI backend (SSH or Telnet)
|
||||||
self._device: Optional[ConnectHandler] = None
|
self._device: ConnectHandler | None = None
|
||||||
# REST API backend
|
# REST API backend
|
||||||
self._api: Optional[ProcurveApiClient] = None
|
self._api: ProcurveApiClient | None = None
|
||||||
# Why each transport failed during the current open(), as "<transport>: <reason>"
|
# Why each transport failed during the current open(), as "<transport>: <reason>"
|
||||||
self._attempts: List[str] = []
|
self._attempts: list[str] = []
|
||||||
|
|
||||||
# Config management state (CLI only)
|
# Config management state (CLI only)
|
||||||
self._candidate_config: Optional[str] = None
|
self._candidate_config: str | None = None
|
||||||
self._candidate_mode: Optional[str] = None # "merge" | "replace"
|
self._candidate_mode: str | None = None # "merge" | "replace"
|
||||||
self._backup_config: Optional[str] = None
|
self._backup_config: str | None = None
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Connection management
|
# Connection management
|
||||||
@@ -246,14 +242,14 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
self._device = None
|
self._device = None
|
||||||
self._transport = None
|
self._transport = None
|
||||||
|
|
||||||
def is_alive(self) -> Dict[str, bool]:
|
def is_alive(self) -> dict[str, bool]:
|
||||||
"""Return transport-level liveness without writing to the channel."""
|
"""Return transport-level liveness without writing to the channel."""
|
||||||
if self._api:
|
if self._api:
|
||||||
return {"is_alive": self._api.is_alive()}
|
return {"is_alive": self._api.is_alive()}
|
||||||
if self._device:
|
if self._device:
|
||||||
try:
|
try:
|
||||||
return {"is_alive": self._device.remote_conn.transport.is_active()}
|
return {"is_alive": self._device.remote_conn.transport.is_active()}
|
||||||
except (AttributeError, socket.error, EOFError):
|
except (OSError, AttributeError, EOFError):
|
||||||
return {"is_alive": False}
|
return {"is_alive": False}
|
||||||
return {"is_alive": False}
|
return {"is_alive": False}
|
||||||
|
|
||||||
@@ -267,9 +263,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
logger.info("Probing REST API for %s (timeout=%ds)", self.hostname, probe_timeout)
|
logger.info("Probing REST API for %s (timeout=%ds)", self.hostname, probe_timeout)
|
||||||
# Probe without SSL verification — no credentials are sent during probing,
|
# Probe without SSL verification — no credentials are sent during probing,
|
||||||
# so this is safe and avoids failing on self-signed certificates.
|
# so this is safe and avoids failing on self-signed certificates.
|
||||||
ver, proto = ProcurveApiClient.probe(
|
ver, proto = ProcurveApiClient.probe(self.hostname, timeout=probe_timeout, ssl_verify=False)
|
||||||
self.hostname, timeout=probe_timeout, ssl_verify=False
|
|
||||||
)
|
|
||||||
# Allow hint override (e.g. user knows the API version)
|
# Allow hint override (e.g. user knows the API version)
|
||||||
if self.api_version_hint and proto:
|
if self.api_version_hint and proto:
|
||||||
ver = self.api_version_hint
|
ver = self.api_version_hint
|
||||||
@@ -281,8 +275,8 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
|
|
||||||
# Try connecting with the requested SSL setting first; if it fails due to a
|
# Try connecting with the requested SSL setting first; if it fails due to a
|
||||||
# self-signed certificate (ssl_verify=True), transparently retry unverified.
|
# self-signed certificate (ssl_verify=True), transparently retry unverified.
|
||||||
error: Optional[Exception] = None
|
error: Exception | None = None
|
||||||
for ssl_verify in ([self.ssl_verify] if not self.ssl_verify else [True, False]):
|
for ssl_verify in [self.ssl_verify] if not self.ssl_verify else [True, False]:
|
||||||
client = ProcurveApiClient(
|
client = ProcurveApiClient(
|
||||||
hostname=self.hostname,
|
hostname=self.hostname,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
@@ -300,8 +294,13 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
continue
|
continue
|
||||||
self._api = client
|
self._api = client
|
||||||
self._transport = "api"
|
self._transport = "api"
|
||||||
logger.info("Connected to %s via REST API (%s %s, ssl_verify=%s)",
|
logger.info(
|
||||||
self.hostname, proto, ver, ssl_verify)
|
"Connected to %s via REST API (%s %s, ssl_verify=%s)",
|
||||||
|
self.hostname,
|
||||||
|
proto,
|
||||||
|
ver,
|
||||||
|
ssl_verify,
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.warning("REST API connect failed for %s — falling back to CLI", self.hostname)
|
logger.warning("REST API connect failed for %s — falling back to CLI", self.hostname)
|
||||||
@@ -314,8 +313,11 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
Explicit fields (port, timeout, disabled_algorithms) take precedence and are
|
Explicit fields (port, timeout, disabled_algorithms) take precedence and are
|
||||||
excluded from the spread to avoid 'multiple values for keyword argument' errors.
|
excluded from the spread to avoid 'multiple values for keyword argument' errors.
|
||||||
"""
|
"""
|
||||||
extra = {k: v for k, v in self.netmiko_optional_args.items()
|
extra = {
|
||||||
if k not in ("port", "timeout", "disabled_algorithms")}
|
k: v
|
||||||
|
for k, v in self.netmiko_optional_args.items()
|
||||||
|
if k not in ("port", "timeout", "disabled_algorithms")
|
||||||
|
}
|
||||||
return dict(
|
return dict(
|
||||||
device_type=self.NETMIKO_DEVICE_TYPE,
|
device_type=self.NETMIKO_DEVICE_TYPE,
|
||||||
host=self.hostname,
|
host=self.hostname,
|
||||||
@@ -328,7 +330,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
**extra,
|
**extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ssh_connect(self, legacy: bool = False, port: Optional[int] = None) -> ConnectHandler:
|
def _ssh_connect(self, legacy: bool = False, port: int | None = None) -> ConnectHandler:
|
||||||
"""A netmiko SSH session, its host key verified and reported (netork#1167).
|
"""A netmiko SSH session, its host key verified and reported (netork#1167).
|
||||||
|
|
||||||
Every SSH session of this driver opens here. A refused key raises
|
Every SSH session of this driver opens here. A refused key raises
|
||||||
@@ -397,7 +399,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# CLI helpers
|
# CLI helpers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _send_command(self, command: Union[str, List[str]]) -> str:
|
def _send_command(self, command: str | list[str]) -> str:
|
||||||
"""Send a CLI command and return stripped output.
|
"""Send a CLI command and return stripped output.
|
||||||
|
|
||||||
When a list is supplied, commands are tried in order and the first
|
When a list is supplied, commands are tried in order and the first
|
||||||
@@ -421,11 +423,15 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
for cmd in command:
|
for cmd in command:
|
||||||
last = _do(cmd)
|
last = _do(cmd)
|
||||||
# ProCurve says "Invalid input: …"; other firmware "% Invalid …"
|
# ProCurve says "Invalid input: …"; other firmware "% Invalid …"
|
||||||
if "Invalid input" not in last and "% Invalid" not in last and "Error" not in last:
|
if (
|
||||||
|
"Invalid input" not in last
|
||||||
|
and "% Invalid" not in last
|
||||||
|
and "Error" not in last
|
||||||
|
):
|
||||||
return last
|
return last
|
||||||
return last
|
return last
|
||||||
return _do(command)
|
return _do(command)
|
||||||
except (socket.error, EOFError) as exc:
|
except (OSError, EOFError) as exc:
|
||||||
raise ConnectionClosedException(str(exc)) from exc
|
raise ConnectionClosedException(str(exc)) from exc
|
||||||
|
|
||||||
def _exec_prompt(self) -> str:
|
def _exec_prompt(self) -> str:
|
||||||
@@ -462,13 +468,13 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
read_timeout=self.timeout,
|
read_timeout=self.timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _apply_config_lines(self, config_text: str) -> List[str]:
|
def _apply_config_lines(self, config_text: str) -> list[str]:
|
||||||
"""Push config lines to the device while in config mode.
|
"""Push config lines to the device while in config mode.
|
||||||
|
|
||||||
Returns a list of error messages for rejected lines.
|
Returns a list of error messages for rejected lines.
|
||||||
"""
|
"""
|
||||||
ep = self._any_prompt()
|
ep = self._any_prompt()
|
||||||
errors: List[str] = []
|
errors: list[str] = []
|
||||||
for line in config_text.splitlines():
|
for line in config_text.splitlines():
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
if not stripped or stripped.startswith(("!", "#", ";")):
|
if not stripped or stripped.startswith(("!", "#", ";")):
|
||||||
@@ -486,7 +492,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_facts
|
# NAPALM: get_facts
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_facts(self) -> Dict:
|
def get_facts(self) -> dict:
|
||||||
"""Return general device information."""
|
"""Return general device information."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_facts()
|
return self._api.get_facts()
|
||||||
@@ -520,22 +526,20 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
"interface_list": iface_list,
|
"interface_list": iface_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_interface_list(self) -> List[str]:
|
def _get_interface_list(self) -> list[str]:
|
||||||
"""Return sorted list of interface names from ``show interfaces brief``."""
|
"""Return sorted list of interface names from ``show interfaces brief``."""
|
||||||
out = self._send_command("show interfaces brief")
|
out = self._send_command("show interfaces brief")
|
||||||
ifaces = parsers.parse_interfaces_brief(out)
|
ifaces = parsers.parse_interfaces_brief(out)
|
||||||
return sorted(
|
return sorted(
|
||||||
ifaces.keys(),
|
ifaces.keys(),
|
||||||
key=lambda s: [
|
key=lambda s: [int(x) if x.isdigit() else x for x in re.split(r"(\d+)", s)],
|
||||||
int(x) if x.isdigit() else x for x in re.split(r"(\d+)", s)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# NAPALM: get_interfaces
|
# NAPALM: get_interfaces
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_interfaces(self) -> Dict[str, Dict]:
|
def get_interfaces(self) -> dict[str, dict]:
|
||||||
"""Return a dictionary of interface details."""
|
"""Return a dictionary of interface details."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_interfaces()
|
return self._api.get_interfaces()
|
||||||
@@ -561,7 +565,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_interfaces_ip
|
# NAPALM: get_interfaces_ip
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_interfaces_ip(self) -> Dict[str, Dict]:
|
def get_interfaces_ip(self) -> dict[str, dict]:
|
||||||
"""Return IP addresses assigned to interfaces."""
|
"""Return IP addresses assigned to interfaces."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_interfaces_ip()
|
return self._api.get_interfaces_ip()
|
||||||
@@ -573,7 +577,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_arp_table
|
# NAPALM: get_arp_table
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_arp_table(self, vrf: str = "") -> List[Dict]:
|
def get_arp_table(self, vrf: str = "") -> list[dict]:
|
||||||
"""Return the ARP table."""
|
"""Return the ARP table."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_arp_table(vrf)
|
return self._api.get_arp_table(vrf)
|
||||||
@@ -585,7 +589,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_mac_address_table
|
# NAPALM: get_mac_address_table
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_mac_address_table(self) -> List[Dict]:
|
def get_mac_address_table(self) -> list[dict]:
|
||||||
"""Return the MAC address table."""
|
"""Return the MAC address table."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_mac_address_table()
|
return self._api.get_mac_address_table()
|
||||||
@@ -597,21 +601,19 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_lldp_neighbors
|
# NAPALM: get_lldp_neighbors
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_lldp_neighbors(self) -> Dict[str, List[Dict]]:
|
def get_lldp_neighbors(self) -> dict[str, list[dict]]:
|
||||||
"""Return LLDP neighbor information."""
|
"""Return LLDP neighbor information."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_lldp_neighbors()
|
return self._api.get_lldp_neighbors()
|
||||||
|
|
||||||
out = self._send_command(
|
out = self._send_command(["show lldp info remote-device", "show lldp neighbors"])
|
||||||
["show lldp info remote-device", "show lldp neighbors"]
|
|
||||||
)
|
|
||||||
return parsers.parse_lldp_neighbors(out)
|
return parsers.parse_lldp_neighbors(out)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# NAPALM: get_lldp_neighbors_detail
|
# NAPALM: get_lldp_neighbors_detail
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_lldp_neighbors_detail(self, interface: str = "") -> Dict[str, List[Dict]]:
|
def get_lldp_neighbors_detail(self, interface: str = "") -> dict[str, list[dict]]:
|
||||||
"""Return detailed LLDP neighbor information."""
|
"""Return detailed LLDP neighbor information."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_lldp_neighbors_detail()
|
return self._api.get_lldp_neighbors_detail()
|
||||||
@@ -629,7 +631,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
|
|
||||||
def get_config(
|
def get_config(
|
||||||
self, retrieve: str = "all", full: bool = False, sanitized: bool = False
|
self, retrieve: str = "all", full: bool = False, sanitized: bool = False
|
||||||
) -> Dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Return device configuration."""
|
"""Return device configuration."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
result = self._api.get_config()
|
result = self._api.get_config()
|
||||||
@@ -652,9 +654,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
if retrieve in ("all", "running"):
|
if retrieve in ("all", "running"):
|
||||||
running = self._send_command("show running-config")
|
running = self._send_command("show running-config")
|
||||||
if retrieve in ("all", "startup"):
|
if retrieve in ("all", "startup"):
|
||||||
startup = self._send_command(
|
startup = self._send_command(["show startup-config", "show config"])
|
||||||
["show startup-config", "show config"]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"running": running, "startup": startup, "candidate": candidate}
|
return {"running": running, "startup": startup, "candidate": candidate}
|
||||||
|
|
||||||
@@ -680,14 +680,12 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_environment
|
# NAPALM: get_environment
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_environment(self) -> Dict:
|
def get_environment(self) -> dict:
|
||||||
"""Return environment information (temperature, fans, CPU, memory)."""
|
"""Return environment information (temperature, fans, CPU, memory)."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
raise NotImplementedError("Environment data not available via REST API")
|
raise NotImplementedError("Environment data not available via REST API")
|
||||||
|
|
||||||
sysinfo = self._send_command(
|
sysinfo = self._send_command(["show system-information", "show system information"])
|
||||||
["show system-information", "show system information"]
|
|
||||||
)
|
|
||||||
env_out = self._send_command(["show environment", "show system"])
|
env_out = self._send_command(["show environment", "show system"])
|
||||||
combined = sysinfo + "\n" + env_out
|
combined = sysinfo + "\n" + env_out
|
||||||
return parsers.parse_environment(combined)
|
return parsers.parse_environment(combined)
|
||||||
@@ -696,25 +694,21 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_ntp_servers
|
# NAPALM: get_ntp_servers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_ntp_servers(self) -> Dict[str, Dict]:
|
def get_ntp_servers(self) -> dict[str, dict]:
|
||||||
"""Return configured NTP servers."""
|
"""Return configured NTP servers."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.get_ntp_servers()
|
return self._api.get_ntp_servers()
|
||||||
|
|
||||||
out = self._send_command(
|
out = self._send_command(["show ntp associations", "show ntp status", "show timesync"])
|
||||||
["show ntp associations", "show ntp status", "show timesync"]
|
|
||||||
)
|
|
||||||
return parsers.parse_ntp_servers(out)
|
return parsers.parse_ntp_servers(out)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# NAPALM: get_ntp_stats
|
# NAPALM: get_ntp_stats
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_ntp_stats(self) -> List[Dict]:
|
def get_ntp_stats(self) -> list[dict]:
|
||||||
"""Return NTP peer statistics."""
|
"""Return NTP peer statistics."""
|
||||||
out = self._send_command(
|
out = self._send_command(["show ntp associations", "show ntp status"])
|
||||||
["show ntp associations", "show ntp status"]
|
|
||||||
)
|
|
||||||
stats = []
|
stats = []
|
||||||
for line in out.splitlines():
|
for line in out.splitlines():
|
||||||
m = re.match(
|
m = re.match(
|
||||||
@@ -746,7 +740,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_users
|
# NAPALM: get_users
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_users(self) -> Dict[str, Dict]:
|
def get_users(self) -> dict[str, dict]:
|
||||||
"""Return local users."""
|
"""Return local users."""
|
||||||
out = self._send_command(["show local-users", "show users"])
|
out = self._send_command(["show local-users", "show users"])
|
||||||
return parsers.parse_users(out)
|
return parsers.parse_users(out)
|
||||||
@@ -755,7 +749,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_snmp_information
|
# NAPALM: get_snmp_information
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_snmp_information(self) -> Dict:
|
def get_snmp_information(self) -> dict:
|
||||||
"""Return SNMP configuration."""
|
"""Return SNMP configuration."""
|
||||||
out = self._send_command(["show snmp-server", "show snmp"])
|
out = self._send_command(["show snmp-server", "show snmp"])
|
||||||
return parsers.parse_snmp_info(out)
|
return parsers.parse_snmp_info(out)
|
||||||
@@ -764,7 +758,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: get_vlans (extended: tagged + untagged per VLAN)
|
# NAPALM: get_vlans (extended: tagged + untagged per VLAN)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_vlans(self) -> Dict[int, Dict]:
|
def get_vlans(self) -> dict[int, dict]:
|
||||||
"""Return VLAN information with tagged/untagged port separation.
|
"""Return VLAN information with tagged/untagged port separation.
|
||||||
|
|
||||||
This method extends the standard NAPALM ``get_vlans()`` return with
|
This method extends the standard NAPALM ``get_vlans()`` return with
|
||||||
@@ -811,18 +805,18 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# Fallback: show vlans (name discovery) + show vlans <id> per VLAN
|
# Fallback: show vlans (name discovery) + show vlans <id> per VLAN
|
||||||
return self._get_vlans_via_show_vlans()
|
return self._get_vlans_via_show_vlans()
|
||||||
|
|
||||||
def _get_vlans_via_show_vlans(self) -> Dict[int, Dict]:
|
def _get_vlans_via_show_vlans(self) -> dict[int, dict]:
|
||||||
"""Fallback VLAN discovery via ``show vlans`` + per-VLAN detail queries."""
|
"""Fallback VLAN discovery via ``show vlans`` + per-VLAN detail queries."""
|
||||||
brief_out = self._send_command("show vlans")
|
brief_out = self._send_command("show vlans")
|
||||||
vlan_names = parsers.parse_vlans_brief(brief_out)
|
vlan_names = parsers.parse_vlans_brief(brief_out)
|
||||||
|
|
||||||
result: Dict[int, Dict] = {}
|
result: dict[int, dict] = {}
|
||||||
for vid, name in vlan_names.items():
|
for vid, name in vlan_names.items():
|
||||||
detail_out = self._send_command(f"show vlans {vid}")
|
detail_out = self._send_command(f"show vlans {vid}")
|
||||||
ports = parsers.parse_vlan_ports_detail(detail_out)
|
ports = parsers.parse_vlan_ports_detail(detail_out)
|
||||||
tagged = ports["tagged"]
|
tagged = ports["tagged"]
|
||||||
untagged = ports["untagged"]
|
untagged = ports["untagged"]
|
||||||
interfaces: List[str] = []
|
interfaces: list[str] = []
|
||||||
for p in untagged + tagged:
|
for p in untagged + tagged:
|
||||||
if p not in interfaces:
|
if p not in interfaces:
|
||||||
interfaces.append(p)
|
interfaces.append(p)
|
||||||
@@ -839,7 +833,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# Custom: get_port_pvids (PVID per port, derived from get_vlans)
|
# Custom: get_port_pvids (PVID per port, derived from get_vlans)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_port_pvids(self) -> Dict[str, int]:
|
def get_port_pvids(self) -> dict[str, int]:
|
||||||
"""Return the PVID (native / access VLAN) for every switch port.
|
"""Return the PVID (native / access VLAN) for every switch port.
|
||||||
|
|
||||||
On ProCurve, a port is untagged in exactly **one** VLAN — that VLAN's
|
On ProCurve, a port is untagged in exactly **one** VLAN — that VLAN's
|
||||||
@@ -906,9 +900,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
lines.append("exit")
|
lines.append("exit")
|
||||||
errors = self._apply_config_lines("\n".join(lines))
|
errors = self._apply_config_lines("\n".join(lines))
|
||||||
if errors:
|
if errors:
|
||||||
raise CommandErrorException(
|
raise CommandErrorException(f"set_vlan({vlan_id}) errors: {errors}")
|
||||||
f"set_vlan({vlan_id}) errors: {errors}"
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
self._exit_config_mode()
|
self._exit_config_mode()
|
||||||
self._save_config()
|
self._save_config()
|
||||||
@@ -917,7 +909,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# LAG / trunk membership
|
# LAG / trunk membership
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def set_lag_members(self, lag_name: str, members: List[str]) -> None:
|
def set_lag_members(self, lag_name: str, members: list[str]) -> None:
|
||||||
"""Set the full member-port list of a LAG/trunk group.
|
"""Set the full member-port list of a LAG/trunk group.
|
||||||
|
|
||||||
Diffs ``members`` against the trunk's current members (as reported
|
Diffs ``members`` against the trunk's current members (as reported
|
||||||
@@ -939,7 +931,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
mode = current.get("lag_mode") or "trunk"
|
mode = current.get("lag_mode") or "trunk"
|
||||||
trk_name = lag_name.lower()
|
trk_name = lag_name.lower()
|
||||||
|
|
||||||
commands: List[str] = []
|
commands: list[str] = []
|
||||||
if to_remove:
|
if to_remove:
|
||||||
commands.append(f"no trunk {','.join(to_remove)}")
|
commands.append(f"no trunk {','.join(to_remove)}")
|
||||||
if to_add:
|
if to_add:
|
||||||
@@ -1024,7 +1016,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
mode = config.get("mode")
|
mode = config.get("mode")
|
||||||
self._enter_config_mode()
|
self._enter_config_mode()
|
||||||
try:
|
try:
|
||||||
lines: List[str] = []
|
lines: list[str] = []
|
||||||
if "enabled" in config or "description" in config:
|
if "enabled" in config or "description" in config:
|
||||||
lines.append(f"interface {interface}")
|
lines.append(f"interface {interface}")
|
||||||
if "enabled" in config:
|
if "enabled" in config:
|
||||||
@@ -1049,9 +1041,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
if lines:
|
if lines:
|
||||||
errors = self._apply_config_lines("\n".join(lines))
|
errors = self._apply_config_lines("\n".join(lines))
|
||||||
if errors:
|
if errors:
|
||||||
raise CommandErrorException(
|
raise CommandErrorException(f"set_interface({interface}) errors: {errors}")
|
||||||
f"set_interface({interface}) errors: {errors}"
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
self._exit_config_mode()
|
self._exit_config_mode()
|
||||||
self._save_config()
|
self._save_config()
|
||||||
@@ -1068,9 +1058,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
try:
|
try:
|
||||||
errors = self._apply_config_lines(f"no vlan {vlan_id}")
|
errors = self._apply_config_lines(f"no vlan {vlan_id}")
|
||||||
if errors:
|
if errors:
|
||||||
raise CommandErrorException(
|
raise CommandErrorException(f"delete_vlan({vlan_id}) errors: {errors}")
|
||||||
f"delete_vlan({vlan_id}) errors: {errors}"
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
self._exit_config_mode()
|
self._exit_config_mode()
|
||||||
self._save_config()
|
self._save_config()
|
||||||
@@ -1079,7 +1067,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# PoE
|
# PoE
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get_poe_status(self) -> Dict[str, Dict]:
|
def get_poe_status(self) -> dict[str, dict]:
|
||||||
"""Return PoE configuration for all ports, keyed by interface name.
|
"""Return PoE configuration for all ports, keyed by interface name.
|
||||||
|
|
||||||
Only available via the REST API transport.
|
Only available via the REST API transport.
|
||||||
@@ -1088,7 +1076,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
return self._api.get_poe_ports()
|
return self._api.get_poe_ports()
|
||||||
raise NotImplementedError("PoE status is only available via the REST API")
|
raise NotImplementedError("PoE status is only available via the REST API")
|
||||||
|
|
||||||
def set_poe(self, interface: str, config: Dict) -> None:
|
def set_poe(self, interface: str, config: dict) -> None:
|
||||||
"""Update PoE configuration for a single interface."""
|
"""Update PoE configuration for a single interface."""
|
||||||
if self._transport != "api":
|
if self._transport != "api":
|
||||||
raise NotImplementedError("PoE configuration is only available via the REST API")
|
raise NotImplementedError("PoE configuration is only available via the REST API")
|
||||||
@@ -1125,12 +1113,17 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
size: int = 100,
|
size: int = 100,
|
||||||
count: int = 5,
|
count: int = 5,
|
||||||
vrf: str = "",
|
vrf: str = "",
|
||||||
) -> Dict:
|
) -> dict:
|
||||||
"""Execute a ping from the device."""
|
"""Execute a ping from the device."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
return self._api.ping(
|
return self._api.ping(
|
||||||
destination, source=source, ttl=ttl,
|
destination,
|
||||||
timeout=timeout, size=size, count=count, vrf=vrf,
|
source=source,
|
||||||
|
ttl=ttl,
|
||||||
|
timeout=timeout,
|
||||||
|
size=size,
|
||||||
|
count=count,
|
||||||
|
vrf=vrf,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ProCurve CLI ping: "ping <ip> repetitions <n>"
|
# ProCurve CLI ping: "ping <ip> repetitions <n>"
|
||||||
@@ -1144,11 +1137,11 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# NAPALM: cli
|
# NAPALM: cli
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def cli(self, commands: List[str], encoding: str = "text") -> Dict[str, str]:
|
def cli(self, commands: list[str], encoding: str = "text") -> dict[str, str]:
|
||||||
"""Execute raw CLI commands and return output dict."""
|
"""Execute raw CLI commands and return output dict."""
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
# ArubaOS REST API supports CLI via POST /rest/<ver>/cli
|
# ArubaOS REST API supports CLI via POST /rest/<ver>/cli
|
||||||
results: Dict[str, str] = {}
|
results: dict[str, str] = {}
|
||||||
for cmd in commands:
|
for cmd in commands:
|
||||||
resp = self._api.post("cli", json={"cmd": cmd})
|
resp = self._api.post("cli", json={"cmd": cmd})
|
||||||
if resp.ok:
|
if resp.ok:
|
||||||
@@ -1166,9 +1159,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
def _get_running_config(self) -> str:
|
def _get_running_config(self) -> str:
|
||||||
return self._send_command("show running-config")
|
return self._send_command("show running-config")
|
||||||
|
|
||||||
def load_merge_candidate(
|
def load_merge_candidate(self, filename: str | None = None, config: str | None = None) -> None:
|
||||||
self, filename: Optional[str] = None, config: Optional[str] = None
|
|
||||||
) -> None:
|
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
raise MergeConfigException(
|
raise MergeConfigException(
|
||||||
"Merge config is not supported via REST API transport. Use SSH."
|
"Merge config is not supported via REST API transport. Use SSH."
|
||||||
@@ -1176,7 +1167,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
super().load_merge_candidate(filename, config)
|
super().load_merge_candidate(filename, config)
|
||||||
|
|
||||||
def load_replace_candidate(
|
def load_replace_candidate(
|
||||||
self, filename: Optional[str] = None, config: Optional[str] = None
|
self, filename: str | None = None, config: str | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
if self._transport == "api":
|
if self._transport == "api":
|
||||||
raise ReplaceConfigException(
|
raise ReplaceConfigException(
|
||||||
@@ -1184,7 +1175,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
)
|
)
|
||||||
super().load_replace_candidate(filename, config)
|
super().load_replace_candidate(filename, config)
|
||||||
|
|
||||||
def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None:
|
def commit_config(self, message: str = "", revert_in: int | None = None) -> None:
|
||||||
"""Apply the candidate configuration to the device."""
|
"""Apply the candidate configuration to the device."""
|
||||||
if self._candidate_config is None:
|
if self._candidate_config is None:
|
||||||
raise MergeConfigException("No candidate config loaded.")
|
raise MergeConfigException("No candidate config loaded.")
|
||||||
@@ -1201,9 +1192,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
raise MergeConfigException(f"Error applying config: {exc}") from exc
|
raise MergeConfigException(f"Error applying config: {exc}") from exc
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
raise MergeConfigException(
|
raise MergeConfigException("The following lines were rejected:\n" + "\n".join(errors))
|
||||||
"The following lines were rejected:\n" + "\n".join(errors)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._save_config()
|
self._save_config()
|
||||||
self._candidate_config = None
|
self._candidate_config = None
|
||||||
@@ -1224,11 +1213,15 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
|
|
||||||
# Use a single command with short read_timeout to avoid blocking the poll.
|
# Use a single command with short read_timeout to avoid blocking the poll.
|
||||||
try:
|
try:
|
||||||
out = self._device.send_command(
|
out = (
|
||||||
|
self._device.send_command(
|
||||||
"show snmp-server",
|
"show snmp-server",
|
||||||
expect_string=rf"{re.escape(self._device.base_prompt)}[>#]",
|
expect_string=rf"{re.escape(self._device.base_prompt)}[>#]",
|
||||||
read_timeout=10,
|
read_timeout=10,
|
||||||
).strip() if self._device else ""
|
).strip()
|
||||||
|
if self._device
|
||||||
|
else ""
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
if not out:
|
if not out:
|
||||||
@@ -1256,7 +1249,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# SNMP agent (napalm-device-types SnmpAgentMixin): the caller's values
|
# SNMP agent (napalm-device-types SnmpAgentMixin): the caller's values
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def configure_snmp_agent(self, settings: Dict[str, Any]) -> Dict[str, Any]:
|
def configure_snmp_agent(self, settings: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""The agent is built in; configure community, contact and location, then save.
|
"""The agent is built in; configure community, contact and location, then save.
|
||||||
|
|
||||||
The REST API has no writable SNMP endpoint, so over REST a temporary SSH
|
The REST API has no writable SNMP endpoint, so over REST a temporary SSH
|
||||||
@@ -1296,7 +1289,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
pass
|
pass
|
||||||
self._api, self._device, self._transport = old_api, old_device, old_transport
|
self._api, self._device, self._transport = old_api, old_device, old_transport
|
||||||
|
|
||||||
def _snmp_configure_cli(self, settings: Dict[str, Any]) -> Dict[str, Any]:
|
def _snmp_configure_cli(self, settings: dict[str, Any]) -> dict[str, Any]:
|
||||||
community = settings["community"]
|
community = settings["community"]
|
||||||
if not _CLI_WORD.match(community):
|
if not _CLI_WORD.match(community):
|
||||||
raise ValueError("the community must be one word without quotes or #")
|
raise ValueError("the community must be one word without quotes or #")
|
||||||
@@ -1317,12 +1310,12 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
return {"ok": True, "output": "community (read-only), contact and location set"}
|
return {"ok": True, "output": "community (read-only), contact and location set"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _snmp_sources_note(settings: Dict[str, Any]) -> Dict[str, Any]:
|
def _snmp_sources_note(settings: dict[str, Any]) -> dict[str, Any]:
|
||||||
sources = ", ".join(settings.get("allowed_sources", [])) or "none given"
|
sources = ", ".join(settings.get("allowed_sources", [])) or "none given"
|
||||||
note = f"no host firewall on the switch; sources not enforced ({sources})"
|
note = f"no host firewall on the switch; sources not enforced ({sources})"
|
||||||
return {"ok": True, "output": note}
|
return {"ok": True, "output": note}
|
||||||
|
|
||||||
def _snmp_save_and_verify(self, settings: Dict[str, Any]) -> Dict[str, Any]:
|
def _snmp_save_and_verify(self, settings: dict[str, Any]) -> dict[str, Any]:
|
||||||
self._save_config()
|
self._save_config()
|
||||||
shown = self._send_command("show snmp-server")
|
shown = self._send_command("show snmp-server")
|
||||||
if re.search(rf"^\s*{re.escape(settings['community'])}\s", shown, re.M):
|
if re.search(rf"^\s*{re.escape(settings['community'])}\s", shown, re.M):
|
||||||
@@ -1334,6 +1327,7 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _is_float(s: str) -> bool:
|
def _is_float(s: str) -> bool:
|
||||||
try:
|
try:
|
||||||
float(s)
|
float(s)
|
||||||
@@ -1342,7 +1336,7 @@ def _is_float(s: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _parse_ping_output(output: str, count: int) -> Dict:
|
def _parse_ping_output(output: str, count: int) -> dict:
|
||||||
"""Parse ProCurve CLI ping output into NAPALM ping result dict.
|
"""Parse ProCurve CLI ping output into NAPALM ping result dict.
|
||||||
|
|
||||||
ProCurve ping output::
|
ProCurve ping output::
|
||||||
@@ -1356,7 +1350,8 @@ def _parse_ping_output(output: str, count: int) -> Dict:
|
|||||||
# Packet statistics line
|
# Packet statistics line
|
||||||
m = re.search(
|
m = re.search(
|
||||||
r"(\d+)\s+packets\s+transmitted,\s+(\d+)\s+packets\s+received",
|
r"(\d+)\s+packets\s+transmitted,\s+(\d+)\s+packets\s+received",
|
||||||
output, re.I,
|
output,
|
||||||
|
re.I,
|
||||||
)
|
)
|
||||||
if not m:
|
if not m:
|
||||||
# Check for total failure
|
# Check for total failure
|
||||||
@@ -1399,4 +1394,3 @@ def _parse_ping_output(output: str, count: int) -> Dict:
|
|||||||
"results": results,
|
"results": results,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+8
-1
@@ -54,4 +54,11 @@ include = ["napalm_procurve*"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py38"
|
target-version = "py310"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# Recorded device output keeps its lines.
|
||||||
|
"tests/**" = ["E501"]
|
||||||
+35
-14
@@ -1,13 +1,12 @@
|
|||||||
"""Unit tests for ProcurveDriver and parsers — no real device required."""
|
"""Unit tests for ProcurveDriver and parsers — no real device required."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from napalm.base.exceptions import ConnectionException
|
from napalm.base.exceptions import ConnectionException
|
||||||
|
|
||||||
from napalm_procurve.procurve import _SSH_DISABLED_STANDARD, ProcurveDriver, _parse_ping_output
|
|
||||||
from napalm_procurve import parsers
|
from napalm_procurve import parsers
|
||||||
|
from napalm_procurve.procurve import _SSH_DISABLED_STANDARD, ProcurveDriver, _parse_ping_output
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Sample CLI output fixtures (captured from real ProCurve devices)
|
# Sample CLI output fixtures (captured from real ProCurve devices)
|
||||||
@@ -510,7 +509,9 @@ class TestDriverTransportDetection:
|
|||||||
with patch("napalm_procurve.procurve.ConnectHandler") as mock_conn:
|
with patch("napalm_procurve.procurve.ConnectHandler") as mock_conn:
|
||||||
mock_conn.return_value = MagicMock()
|
mock_conn.return_value = MagicMock()
|
||||||
drv = ProcurveDriver(
|
drv = ProcurveDriver(
|
||||||
"192.168.0.1", "manager", "secret",
|
"192.168.0.1",
|
||||||
|
"manager",
|
||||||
|
"secret",
|
||||||
optional_args={"transport": "ssh"},
|
optional_args={"transport": "ssh"},
|
||||||
)
|
)
|
||||||
drv.open()
|
drv.open()
|
||||||
@@ -521,7 +522,9 @@ class TestDriverTransportDetection:
|
|||||||
with patch("napalm_procurve.procurve.ConnectHandler") as mock_conn:
|
with patch("napalm_procurve.procurve.ConnectHandler") as mock_conn:
|
||||||
mock_conn.return_value = MagicMock()
|
mock_conn.return_value = MagicMock()
|
||||||
drv = ProcurveDriver(
|
drv = ProcurveDriver(
|
||||||
"192.168.0.1", "manager", "secret",
|
"192.168.0.1",
|
||||||
|
"manager",
|
||||||
|
"secret",
|
||||||
optional_args={"transport": "telnet"},
|
optional_args={"transport": "telnet"},
|
||||||
)
|
)
|
||||||
drv.open()
|
drv.open()
|
||||||
@@ -565,7 +568,9 @@ class TestDriverTransportDetection:
|
|||||||
def test_all_transports_fail_names_each_reason(self):
|
def test_all_transports_fail_names_each_reason(self):
|
||||||
"""The final error gives a reason per transport, not just its name."""
|
"""The final error gives a reason per transport, not just its name."""
|
||||||
with patch("napalm_procurve.procurve.ProcurveApiClient.probe", return_value=(None, None)):
|
with patch("napalm_procurve.procurve.ProcurveApiClient.probe", return_value=(None, None)):
|
||||||
with patch("napalm_procurve.procurve.ConnectHandler", side_effect=OSError("no route to host")):
|
with patch(
|
||||||
|
"napalm_procurve.procurve.ConnectHandler", side_effect=OSError("no route to host")
|
||||||
|
):
|
||||||
drv = ProcurveDriver("192.168.0.1", "manager", "secret")
|
drv = ProcurveDriver("192.168.0.1", "manager", "secret")
|
||||||
with pytest.raises(ConnectionException) as exc_info:
|
with pytest.raises(ConnectionException) as exc_info:
|
||||||
drv.open()
|
drv.open()
|
||||||
@@ -900,22 +905,36 @@ class TestApiGetInterfacesLag:
|
|||||||
client = ProcurveApiClient(hostname="192.168.0.1", username="manager", password="secret")
|
client = ProcurveApiClient(hostname="192.168.0.1", username="manager", password="secret")
|
||||||
responses = {
|
responses = {
|
||||||
"ports": {"port_element": ports},
|
"ports": {"port_element": ports},
|
||||||
"port-statistics": {"port_statistics_element": [
|
"port-statistics": {
|
||||||
{"id": p["id"], "port_speed_mbps": 1000} for p in ports
|
"port_statistics_element": [{"id": p["id"], "port_speed_mbps": 1000} for p in ports]
|
||||||
]},
|
},
|
||||||
"system/status/switch": {},
|
"system/status/switch": {},
|
||||||
}
|
}
|
||||||
client.get = MagicMock(side_effect=lambda endpoint, **kw: responses[endpoint])
|
client.get = MagicMock(side_effect=lambda endpoint, **kw: responses[endpoint])
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def test_trunk_rows_with_mode(self):
|
def test_trunk_rows_with_mode(self):
|
||||||
client = self._client([
|
client = self._client(
|
||||||
|
[
|
||||||
{"id": "1", "is_port_up": True, "is_port_enabled": True, "trunk_group": ""},
|
{"id": "1", "is_port_up": True, "is_port_enabled": True, "trunk_group": ""},
|
||||||
{"id": "3", "is_port_up": False, "is_port_enabled": True, "trunk_group": "trk3", "trunk_mode": "PTT_LACP"},
|
{
|
||||||
{"id": "4", "is_port_up": True, "is_port_enabled": True, "trunk_group": "trk3", "trunk_mode": "PTT_LACP"},
|
"id": "3",
|
||||||
|
"is_port_up": False,
|
||||||
|
"is_port_enabled": True,
|
||||||
|
"trunk_group": "trk3",
|
||||||
|
"trunk_mode": "PTT_LACP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"is_port_up": True,
|
||||||
|
"is_port_enabled": True,
|
||||||
|
"trunk_group": "trk3",
|
||||||
|
"trunk_mode": "PTT_LACP",
|
||||||
|
},
|
||||||
{"id": "10", "is_port_up": False, "is_port_enabled": True, "trunk_group": "trk6"},
|
{"id": "10", "is_port_up": False, "is_port_enabled": True, "trunk_group": "trk6"},
|
||||||
{"id": "7", "is_port_up": False, "is_port_enabled": True, "trunk_group": "trk6"},
|
{"id": "7", "is_port_up": False, "is_port_enabled": True, "trunk_group": "trk6"},
|
||||||
])
|
]
|
||||||
|
)
|
||||||
ifaces = client.get_interfaces()
|
ifaces = client.get_interfaces()
|
||||||
|
|
||||||
assert ifaces["trk3"]["lag_members"] == ["3", "4"]
|
assert ifaces["trk3"]["lag_members"] == ["3", "4"]
|
||||||
@@ -931,7 +950,9 @@ class TestApiGetInterfacesLag:
|
|||||||
class TestCliGetInterfacesLag:
|
class TestCliGetInterfacesLag:
|
||||||
def test_trunk_rows_from_member_ports(self, driver):
|
def test_trunk_rows_from_member_ports(self, driver):
|
||||||
driver._send_command = MagicMock(
|
driver._send_command = MagicMock(
|
||||||
side_effect=lambda cmd: SHOW_INTERFACES_BRIEF_INTRUSION if cmd == "show interfaces brief" else ""
|
side_effect=lambda cmd: (
|
||||||
|
SHOW_INTERFACES_BRIEF_INTRUSION if cmd == "show interfaces brief" else ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
ifaces = driver.get_interfaces()
|
ifaces = driver.get_interfaces()
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ HANDLER = "napalm_procurve.procurve.ConnectHandler"
|
|||||||
|
|
||||||
def _device() -> MagicMock:
|
def _device() -> MagicMock:
|
||||||
device = MagicMock()
|
device = MagicMock()
|
||||||
device.remote_conn_pre.get_transport.return_value.get_remote_server_key.return_value = SERVER_KEY
|
device.remote_conn_pre.get_transport.return_value.get_remote_server_key.return_value = (
|
||||||
|
SERVER_KEY
|
||||||
|
)
|
||||||
device.send_command.return_value = "hostname x"
|
device.send_command.return_value = "hostname x"
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user