diff --git a/napalm_procurve/__pycache__/api_client.cpython-312.pyc b/napalm_procurve/__pycache__/api_client.cpython-312.pyc new file mode 100644 index 0000000..2de6d5c Binary files /dev/null and b/napalm_procurve/__pycache__/api_client.cpython-312.pyc differ diff --git a/napalm_procurve/__pycache__/procurve.cpython-312.pyc b/napalm_procurve/__pycache__/procurve.cpython-312.pyc new file mode 100644 index 0000000..caa1f9b Binary files /dev/null and b/napalm_procurve/__pycache__/procurve.cpython-312.pyc differ diff --git a/napalm_procurve/api_client.py b/napalm_procurve/api_client.py index f2ac1a8..015f380 100644 --- a/napalm_procurve/api_client.py +++ b/napalm_procurve/api_client.py @@ -242,8 +242,33 @@ class ProcurveApiClient: stats_data = self.get("port-statistics") output: Dict[str, Dict] = {} + trunk_groups: Dict[str, list] = {} + trunk_modes: Dict[str, str] = {} - for port in ports_data.get("port_element", []): + port_elements = list(ports_data.get("port_element", [])) + + # The /ports collection can under-report the switch's true port + # count (observed: a 2530-48G reports 51 of its 52 ports). Fill in + # any ports known to system/status/switch but missing from /ports. + seen_ids = {p.get("id") for p in port_elements} + try: + sw_status = self.get("system/status/switch") + for blade in sw_status.get("blades", []): + for p in blade.get("data_ports", []): + pid = p.get("port_name", "") + if pid and pid not in seen_ids: + port_elements.append({ + "id": pid, + "name": "", + "is_port_up": p.get("operStatus") == "OPER_UP", + "is_port_enabled": p.get("adminStatus") == "ADMIN_UP", + "trunk_group": "", + }) + seen_ids.add(pid) + except Exception: + pass + + for port in port_elements: pid = port.get("id", "") output[pid] = { "is_up": bool(port.get("is_port_up", False)), @@ -254,12 +279,33 @@ class ProcurveApiClient: "mtu": -1, "mac_address": "", } + group = port.get("trunk_group") or "" + if group: + output[pid]["trunk_group"] = group + trunk_groups.setdefault(group, []).append(pid) + if port.get("trunk_mode"): + trunk_modes[group] = port["trunk_mode"] for stat in stats_data.get("port_statistics_element", []): pid = stat.get("id", "") if pid in output: output[pid]["speed"] = float(stat.get("port_speed_mbps", 0)) + # Synthesize a logical interface entry for each configured LAG/trunk + # group so it shows up as its own row alongside its member ports. + for group, members in trunk_groups.items(): + output[group] = { + "is_up": any(output[m]["is_up"] for m in members), + "is_enabled": any(output[m]["is_enabled"] for m in members), + "description": f"LAG ({', '.join(sorted(members, key=lambda s: int(s) if s.isdigit() else 0))})", + "last_flapped": -1.0, + "speed": sum(output[m]["speed"] for m in members), + "mtu": -1, + "mac_address": "", + "lag_members": members, + "lag_mode": "lacp" if trunk_modes.get(group) == "PTT_LACP" else "trunk", + } + return output def get_interfaces_ip(self) -> Dict[str, Dict]: diff --git a/napalm_procurve/procurve.py b/napalm_procurve/procurve.py index 1e8ff39..7934f98 100644 --- a/napalm_procurve/procurve.py +++ b/napalm_procurve/procurve.py @@ -796,6 +796,55 @@ class ProcurveDriver(SwitchDriver): self._exit_config_mode() self._save_config() + # ------------------------------------------------------------------ + # LAG / trunk membership + # ------------------------------------------------------------------ + + def set_lag_members(self, lag_name: str, members: List[str]) -> None: + """Set the full member-port list of a LAG/trunk group. + + Diffs ``members`` against the trunk's current members (as reported + by ``get_interfaces()``) and issues ``trunk``/``no trunk`` commands + for the difference. Available on both REST API and CLI transports — + ArubaOS-Switch only exposes trunk membership via the CLI, including + through the REST API's ``/cli`` passthrough endpoint (``PUT /ports`` + does not apply ``trunk_group``/``trunk_mode`` changes). + """ + current = self.get_interfaces().get(lag_name, {}) + current_members = set(current.get("lag_members") or []) + desired = set(members) + to_remove = sorted(current_members - desired, key=lambda s: int(s) if s.isdigit() else 0) + to_add = sorted(desired - current_members, key=lambda s: int(s) if s.isdigit() else 0) + + if not to_remove and not to_add: + return + + mode = current.get("lag_mode") or "trunk" + trk_name = lag_name.lower() + + commands: List[str] = [] + if to_remove: + commands.append(f"no trunk {','.join(to_remove)}") + if to_add: + commands.append(f"trunk {','.join(to_add)} {trk_name} {mode}") + + if self._transport == "api": + for cmd in ["configure terminal", *commands]: + resp = self._api.post("cli", json={"cmd": cmd}) + if not resp.ok or resp.json().get("status") == "CCS_FAILURE": + raise CommandErrorException( + f"set_lag_members({lag_name}): '{cmd}' -> {resp.text[:200]}" + ) + else: + self._enter_config_mode() + try: + errors = self._apply_config_lines("\n".join(commands)) + if errors: + raise CommandErrorException(f"set_lag_members({lag_name}) errors: {errors}") + finally: + self._exit_config_mode() + self._save_config() + def set_interface(self, interface: str, config: InterfaceConfigDict) -> None: """Configure a switch interface (mode, VLAN membership).""" if self._transport == "api":