fix: extract J-code part numbers from ProCurve model strings
parse_system_info() now extracts HPE/HP J-codes (e.g. J9298A, J9777A) from the System Model field and returns them as part_number separately. The model name is cleaned of the J-code and surrounding punctuation: "HP J9298A Switch 2520G-8-PoE" → model="HP 2520G-8-PoE Switch", pn="J9298A" "HP2530-8G Switch(J9777A)" → model="HP2530-8G Switch", pn="J9777A" get_facts() includes part_number in the returned dict so NetOrk can store it on Device.part_number and pass it to NetBox device types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
f3f70cbf74
commit
2be268a65e
@@ -89,7 +89,23 @@ def parse_system_info(output: str) -> Dict:
|
|||||||
)
|
)
|
||||||
serial = get("Serial Number") or get("Serial Num")
|
serial = get("Serial Number") or get("Serial Num")
|
||||||
base_mac = procurve_mac_to_std(get("Base MAC Addr") or get("MAC Address") or "")
|
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"
|
# Parse uptime — ProCurve formats: "5 days 3:14" or "0 days 0:05"
|
||||||
# Also: "5 day(s), 3:14:00"
|
# Also: "5 day(s), 3:14:00"
|
||||||
@@ -101,6 +117,7 @@ def parse_system_info(output: str) -> Dict:
|
|||||||
"serial_number": serial,
|
"serial_number": serial,
|
||||||
"base_mac": base_mac,
|
"base_mac": base_mac,
|
||||||
"model": model,
|
"model": model,
|
||||||
|
"part_number": part_number,
|
||||||
"uptime_seconds": uptime_seconds,
|
"uptime_seconds": uptime_seconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from netmiko.exceptions import (
|
|||||||
NetmikoAuthenticationException,
|
NetmikoAuthenticationException,
|
||||||
NetmikoTimeoutException,
|
NetmikoTimeoutException,
|
||||||
)
|
)
|
||||||
from napalm_device_types import SwitchDriver
|
from napalm_device_types import ConfigLifecycleMixin, SwitchDriver
|
||||||
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 import helpers as napalm_helpers
|
||||||
from napalm.base.exceptions import (
|
from napalm.base.exceptions import (
|
||||||
@@ -71,7 +71,7 @@ _SSH_DISABLED_LEGACY = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProcurveDriver(SwitchDriver):
|
class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
|
||||||
"""NAPALM driver for HPE/Aruba ProCurve switches.
|
"""NAPALM driver for HPE/Aruba ProCurve switches.
|
||||||
|
|
||||||
Auto-detects transport on ``open()``. See module docstring for details.
|
Auto-detects transport on ``open()``. See module docstring for details.
|
||||||
@@ -425,6 +425,7 @@ class ProcurveDriver(SwitchDriver):
|
|||||||
return {
|
return {
|
||||||
"vendor": self.VENDOR,
|
"vendor": self.VENDOR,
|
||||||
"model": parsed["model"],
|
"model": parsed["model"],
|
||||||
|
"part_number": parsed.get("part_number", ""),
|
||||||
"hostname": parsed["hostname"],
|
"hostname": parsed["hostname"],
|
||||||
"fqdn": parsed["hostname"],
|
"fqdn": parsed["hostname"],
|
||||||
"os_version": parsed["os_version"],
|
"os_version": parsed["os_version"],
|
||||||
@@ -1033,61 +1034,26 @@ class ProcurveDriver(SwitchDriver):
|
|||||||
# NAPALM: Configuration management (CLI only)
|
# NAPALM: Configuration management (CLI only)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _get_running_config(self) -> str:
|
||||||
|
return self._send_command("show running-config")
|
||||||
|
|
||||||
def load_merge_candidate(
|
def load_merge_candidate(
|
||||||
self, filename: Optional[str] = None, config: Optional[str] = None
|
self, filename: Optional[str] = None, config: Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load a candidate configuration for merging."""
|
|
||||||
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."
|
||||||
)
|
)
|
||||||
if filename and config:
|
super().load_merge_candidate(filename, 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"
|
|
||||||
|
|
||||||
def load_replace_candidate(
|
def load_replace_candidate(
|
||||||
self, filename: Optional[str] = None, config: Optional[str] = None
|
self, filename: Optional[str] = None, config: Optional[str] = None
|
||||||
) -> 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":
|
if self._transport == "api":
|
||||||
raise ReplaceConfigException(
|
raise ReplaceConfigException(
|
||||||
"Replace config is not supported via REST API transport. Use SSH."
|
"Replace config is not supported via REST API transport. Use SSH."
|
||||||
)
|
)
|
||||||
if filename and config:
|
super().load_replace_candidate(filename, 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)
|
|
||||||
|
|
||||||
def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None:
|
def commit_config(self, message: str = "", revert_in: Optional[int] = None) -> None:
|
||||||
"""Apply the candidate configuration to the device."""
|
"""Apply the candidate configuration to the device."""
|
||||||
@@ -1114,24 +1080,6 @@ class ProcurveDriver(SwitchDriver):
|
|||||||
self._candidate_config = None
|
self._candidate_config = None
|
||||||
self._candidate_mode = 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 ──────────────────────────────────────────────────────────
|
# ── SNMP / Health ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_device_warnings(self) -> list:
|
def get_device_warnings(self) -> list:
|
||||||
|
|||||||
Reference in New Issue
Block a user