From 2be268a65e824182a920802f1e49774e708e1e77 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 22 Jun 2026 22:57:33 +0200 Subject: [PATCH] fix: extract J-code part numbers from ProCurve model strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- napalm_procurve/parsers.py | 19 ++++++++++- napalm_procurve/procurve.py | 68 +++++-------------------------------- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/napalm_procurve/parsers.py b/napalm_procurve/parsers.py index 1430e37..bacba07 100644 --- a/napalm_procurve/parsers.py +++ b/napalm_procurve/parsers.py @@ -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, } diff --git a/napalm_procurve/procurve.py b/napalm_procurve/procurve.py index 7934f98..479fcee 100644 --- a/napalm_procurve/procurve.py +++ b/napalm_procurve/procurve.py @@ -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: