Compare commits
10
Commits
576752da23
...
d1db7a452d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1db7a452d | ||
|
|
ef5e453f5d | ||
|
|
247964178a | ||
|
|
232936162e | ||
|
|
55fe8b28f9 | ||
|
|
ff37f3c59f | ||
|
|
da5920a9bc | ||
|
|
d2915f5829 | ||
|
|
2be268a65e | ||
|
|
f3f70cbf74 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,6 +13,7 @@ from typing import Any, Dict, Optional, Tuple
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
from napalm.base import helpers as napalm_helpers
|
||||
from napalm.base.exceptions import ConnectionException, ConnectAuthError
|
||||
|
||||
logger = logging.getLogger("napalm_procurve.api")
|
||||
@@ -225,9 +226,24 @@ class ProcurveApiClient:
|
||||
for port in blade.get("data_ports", []):
|
||||
iface_list.append(port.get("port_name", ""))
|
||||
|
||||
model_raw = system.get("product_model", "")
|
||||
# Extract J-code part numbers (e.g. J9777A) from the model string
|
||||
import re as _re
|
||||
_PN_RE = _re.compile(r"\b(J\d{4}[A-Z]{1,2})\b")
|
||||
pn_match = _PN_RE.search(model_raw)
|
||||
part_number = pn_match.group(1) if pn_match else ""
|
||||
if pn_match:
|
||||
clean = _PN_RE.sub("", model_raw)
|
||||
clean = _re.sub(r"\(\s*\)", "", clean)
|
||||
clean = _re.sub(r"\s{2,}", " ", clean).strip(" -()")
|
||||
model = clean if clean else model_raw
|
||||
else:
|
||||
model = model_raw
|
||||
|
||||
return {
|
||||
"vendor": "HPE Aruba",
|
||||
"model": system.get("product_model", ""),
|
||||
"model": model,
|
||||
"part_number": part_number,
|
||||
"hostname": hostname,
|
||||
"fqdn": fqdn,
|
||||
"os_version": system.get("firmware_version", ""),
|
||||
@@ -366,9 +382,14 @@ class ProcurveApiClient:
|
||||
mac_data = self.get("mac-table")
|
||||
table = []
|
||||
for entry in mac_data.get("mac_table_entry_element", []):
|
||||
raw_mac = entry.get("mac_address", "")
|
||||
try:
|
||||
mac = napalm_helpers.mac(raw_mac)
|
||||
except Exception:
|
||||
mac = raw_mac
|
||||
table.append(
|
||||
{
|
||||
"mac": entry.get("mac_address", ""),
|
||||
"mac": mac,
|
||||
"interface": entry.get("port_id", ""),
|
||||
"vlan": int(entry.get("vlan_id", 0)),
|
||||
"static": entry.get("mac_addr_type", "").lower() == "static",
|
||||
|
||||
+50
-13
@@ -89,7 +89,23 @@ def parse_system_info(output: str) -> Dict:
|
||||
)
|
||||
serial = get("Serial Number") or get("Serial Num")
|
||||
base_mac = procurve_mac_to_std(get("Base MAC Addr") or get("MAC Address") or "")
|
||||
model = get("System Model") or get("product_model") or ""
|
||||
model_raw = get("System Model") or get("product_model") or ""
|
||||
|
||||
# Extract HPE/HP J-code part numbers (e.g. "J9298A") from the model string.
|
||||
# The J-code is the PN; the remaining text is the clean model name.
|
||||
# Examples:
|
||||
# "HP J9298A Switch 2520G-8-PoE" → model="HP 2520G-8-PoE Switch", pn="J9298A"
|
||||
# "HP2530-8G Switch(J9777A)" → model="HP2530-8G Switch", pn="J9777A"
|
||||
_PN_RE = re.compile(r"\b(J\d{4}[A-Z]{1,2})\b")
|
||||
pn_match = _PN_RE.search(model_raw)
|
||||
part_number = pn_match.group(1) if pn_match else ""
|
||||
if pn_match:
|
||||
clean = _PN_RE.sub("", model_raw)
|
||||
clean = re.sub(r"\(\s*\)", "", clean)
|
||||
clean = re.sub(r"\s{2,}", " ", clean).strip(" -()")
|
||||
model = clean if clean else model_raw
|
||||
else:
|
||||
model = model_raw
|
||||
|
||||
# Parse uptime — ProCurve formats: "5 days 3:14" or "0 days 0:05"
|
||||
# Also: "5 day(s), 3:14:00"
|
||||
@@ -101,6 +117,7 @@ def parse_system_info(output: str) -> Dict:
|
||||
"serial_number": serial,
|
||||
"base_mac": base_mac,
|
||||
"model": model,
|
||||
"part_number": part_number,
|
||||
"uptime_seconds": uptime_seconds,
|
||||
}
|
||||
|
||||
@@ -171,20 +188,40 @@ def parse_version(output: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def parse_model_from_version(output: str) -> str:
|
||||
"""Extract model from ``show version`` first line.
|
||||
def parse_model_from_version(output: str) -> tuple[str, str]:
|
||||
"""Extract (model, part_number) from ``show version`` first line.
|
||||
|
||||
Example: ``HP J9565A 2520G-8-PoE Switch`` → ``2520G-8-PoE``
|
||||
Handles both orderings of the HP banner:
|
||||
``HP J9565A 2520G-8-PoE Switch`` → ("2520G-8-PoE", "J9565A")
|
||||
``HP J9298A Switch 2520G-8-PoE`` → ("2520G-8-PoE", "J9298A")
|
||||
``Aruba 2530-8-PoE+ Switch`` → ("2530-8-PoE+", "")
|
||||
|
||||
Returns ("", "") when the first line is not an HP/Aruba device banner
|
||||
(e.g. when show version starts with "Image stamp:" on newer Aruba).
|
||||
"""
|
||||
first = output.strip().splitlines()[0] if output.strip() else ""
|
||||
m = re.search(r"HP\s+\S+\s+(\S+)\s+Switch", first, re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
# Aruba format: "Aruba 2530-8-PoE+ Switch"
|
||||
m = re.search(r"(?:Aruba|HP)\s+(\d\S+)\s+(?:Switch|)", first, re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
# Find the HP/Aruba product banner line — may not be the first line
|
||||
# (older ProCurve shows "Image stamp:" first, banner follows later)
|
||||
banner = ""
|
||||
for line in output.splitlines():
|
||||
if re.match(r"\s*(?:HP|HPE|Aruba)\b", line, re.I):
|
||||
banner = line.strip()
|
||||
break
|
||||
if not banner:
|
||||
return "", ""
|
||||
first = banner
|
||||
|
||||
# Extract J-code part number
|
||||
_PN_RE_V = re.compile(r"\b(J\d{4}[A-Z]{1,2})\b")
|
||||
pn_match = _PN_RE_V.search(first)
|
||||
part_number = pn_match.group(1) if pn_match else ""
|
||||
|
||||
# Strip vendor prefix, J-code and "Switch" keyword to get the product name
|
||||
cleaned = re.sub(r"\b(?:HP|HPE|Aruba)\b", "", first, flags=re.I)
|
||||
cleaned = _PN_RE_V.sub("", cleaned)
|
||||
cleaned = re.sub(r"\bSwitch\b", "", cleaned, flags=re.I)
|
||||
cleaned = re.sub(r"\s{2,}", " ", cleaned).strip(" -()")
|
||||
|
||||
return cleaned, part_number
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+31
-63
@@ -27,7 +27,7 @@ from netmiko.exceptions import (
|
||||
NetmikoAuthenticationException,
|
||||
NetmikoTimeoutException,
|
||||
)
|
||||
from napalm_device_types import SwitchDriver
|
||||
from napalm_device_types import ConfigLifecycleMixin, FingerprintRule, SwitchDriver
|
||||
from napalm_device_types.models import InterfaceConfigDict, VlanConfigDict
|
||||
from napalm.base import helpers as napalm_helpers
|
||||
from napalm.base.exceptions import (
|
||||
@@ -71,13 +71,31 @@ _SSH_DISABLED_LEGACY = {
|
||||
}
|
||||
|
||||
|
||||
class ProcurveDriver(SwitchDriver):
|
||||
class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
||||
"""NAPALM driver for HPE/Aruba ProCurve switches.
|
||||
|
||||
Auto-detects transport on ``open()``. See module docstring for details.
|
||||
"""
|
||||
|
||||
VENDOR = "HPE"
|
||||
DRIVER_NAME = "procurve"
|
||||
SNMP_OBJECT_ID_PREFIX = "1.3.6.1.4.1.11"
|
||||
OUI_PREFIXES = [
|
||||
"3C:D9:2B", # Hewlett Packard — IEEE
|
||||
"3C:52:82", # Hewlett Packard — IEEE
|
||||
"44:31:92", # Hewlett Packard — IEEE
|
||||
"00:17:A4", # Hewlett Packard — IEEE
|
||||
]
|
||||
SSH_FINGERPRINT = [
|
||||
FingerprintRule("hp", weight=5.0),
|
||||
FingerprintRule("aruba", weight=4.0),
|
||||
]
|
||||
HTTP_FINGERPRINT = [
|
||||
FingerprintRule("hp procurve", weight=8.0, mandatory=True),
|
||||
FingerprintRule("procurve", weight=6.0),
|
||||
FingerprintRule("ehttp", weight=5.0),
|
||||
FingerprintRule("comware", weight=5.0),
|
||||
]
|
||||
NETMIKO_DEVICE_TYPE = "hp_procurve"
|
||||
NETMIKO_DEVICE_TYPE_TELNET = "hp_procurve_telnet"
|
||||
|
||||
@@ -409,14 +427,16 @@ class ProcurveDriver(SwitchDriver):
|
||||
|
||||
# CLI path
|
||||
sysinfo = self._send_command(
|
||||
["show system-information", "show system information"]
|
||||
["show system-information", "show system information", "show system"]
|
||||
)
|
||||
parsed = parsers.parse_system_info(sysinfo)
|
||||
|
||||
# get model from show version if not in system-info
|
||||
# get model (and part_number) from show version if not in system-info
|
||||
if not parsed["model"]:
|
||||
ver_out = self._send_command("show version")
|
||||
parsed["model"] = parsers.parse_model_from_version(ver_out)
|
||||
parsed["model"], ver_pn = parsers.parse_model_from_version(ver_out)
|
||||
if ver_pn and not parsed.get("part_number"):
|
||||
parsed["part_number"] = ver_pn
|
||||
if not parsed["os_version"]:
|
||||
parsed["os_version"] = parsers.parse_version(ver_out)
|
||||
|
||||
@@ -425,6 +445,7 @@ class ProcurveDriver(SwitchDriver):
|
||||
return {
|
||||
"vendor": self.VENDOR,
|
||||
"model": parsed["model"],
|
||||
"part_number": parsed.get("part_number", ""),
|
||||
"hostname": parsed["hostname"],
|
||||
"fqdn": parsed["hostname"],
|
||||
"os_version": parsed["os_version"],
|
||||
@@ -1033,61 +1054,26 @@ class ProcurveDriver(SwitchDriver):
|
||||
# NAPALM: Configuration management (CLI only)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_running_config(self) -> str:
|
||||
return self._send_command("show running-config")
|
||||
|
||||
def load_merge_candidate(
|
||||
self, filename: Optional[str] = None, config: Optional[str] = None
|
||||
) -> None:
|
||||
"""Load a candidate configuration for merging."""
|
||||
if self._transport == "api":
|
||||
raise MergeConfigException(
|
||||
"Merge config is not supported via REST API transport. Use SSH."
|
||||
)
|
||||
if filename and config:
|
||||
raise MergeConfigException("Provide either filename or config, not both.")
|
||||
if filename:
|
||||
with open(filename) as f:
|
||||
config = f.read()
|
||||
if config is None:
|
||||
raise MergeConfigException("No configuration provided.")
|
||||
self._candidate_config = config
|
||||
self._candidate_mode = "merge"
|
||||
super().load_merge_candidate(filename, config)
|
||||
|
||||
def load_replace_candidate(
|
||||
self, filename: Optional[str] = None, config: Optional[str] = None
|
||||
) -> None:
|
||||
"""Load a candidate configuration for replacing.
|
||||
|
||||
Note: ProCurve does not natively support atomic config replace.
|
||||
The candidate is applied line-by-line (same as merge) when committed.
|
||||
"""
|
||||
if self._transport == "api":
|
||||
raise ReplaceConfigException(
|
||||
"Replace config is not supported via REST API transport. Use SSH."
|
||||
)
|
||||
if filename and config:
|
||||
raise ReplaceConfigException("Provide either filename or config, not both.")
|
||||
if filename:
|
||||
with open(filename) as f:
|
||||
config = f.read()
|
||||
if config is None:
|
||||
raise ReplaceConfigException("No configuration provided.")
|
||||
self._candidate_config = config
|
||||
self._candidate_mode = "replace"
|
||||
|
||||
def compare_config(self) -> str:
|
||||
"""Return a diff between the running config and the candidate config."""
|
||||
if self._candidate_config is None:
|
||||
return ""
|
||||
import difflib
|
||||
|
||||
running = self._send_command("show running-config")
|
||||
diff = difflib.unified_diff(
|
||||
running.splitlines(),
|
||||
self._candidate_config.splitlines(),
|
||||
fromfile="running-config",
|
||||
tofile="candidate-config",
|
||||
lineterm="",
|
||||
)
|
||||
return "\n".join(diff)
|
||||
super().load_replace_candidate(filename, config)
|
||||
|
||||
def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None:
|
||||
"""Apply the candidate configuration to the device."""
|
||||
@@ -1114,24 +1100,6 @@ class ProcurveDriver(SwitchDriver):
|
||||
self._candidate_config = None
|
||||
self._candidate_mode = None
|
||||
|
||||
def discard_config(self) -> None:
|
||||
"""Discard the loaded candidate configuration."""
|
||||
self._candidate_config = None
|
||||
self._candidate_mode = None
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""Re-apply the backup configuration saved before the last commit."""
|
||||
if self._backup_config is None:
|
||||
raise MergeConfigException("No backup config available for rollback.")
|
||||
self._candidate_config = self._backup_config
|
||||
self._candidate_mode = "merge"
|
||||
self.commit_config()
|
||||
self._backup_config = None
|
||||
|
||||
def has_pending_commit(self) -> bool:
|
||||
"""Return True if a candidate config is loaded."""
|
||||
return self._candidate_config is not None
|
||||
|
||||
# ── SNMP / Health ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_device_warnings(self) -> list:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user