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:
Christian Manivong
2026-06-22 22:57:33 +02:00
co-authored by Claude Sonnet 4.6
parent f3f70cbf74
commit 2be268a65e
2 changed files with 26 additions and 61 deletions
+8 -60
View File
@@ -27,7 +27,7 @@ from netmiko.exceptions import (
NetmikoAuthenticationException,
NetmikoTimeoutException,
)
from napalm_device_types import SwitchDriver
from napalm_device_types import ConfigLifecycleMixin, SwitchDriver
from napalm_device_types.models import InterfaceConfigDict, VlanConfigDict
from napalm.base import helpers as napalm_helpers
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.
Auto-detects transport on ``open()``. See module docstring for details.
@@ -425,6 +425,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 +1034,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 +1080,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: