From b202a5e22732b76d57a54ea79a810ea6ced7f57f Mon Sep 17 00:00:00 2001 From: jolibran Date: Tue, 4 Aug 2026 11:11:18 -0400 Subject: [PATCH 1/8] DCERR-699: add bcmcli support and expand niccli read-only commands --- .../plugins/inband/nic/collector_args.py | 12 +- .../plugins/inband/nic/nic_collector.py | 896 ++++++++++++++---- nodescraper/plugins/inband/nic/nic_data.py | 5 + 3 files changed, 741 insertions(+), 172 deletions(-) diff --git a/nodescraper/plugins/inband/nic/collector_args.py b/nodescraper/plugins/inband/nic/collector_args.py index 99b4c7fa..09c7d5a7 100644 --- a/nodescraper/plugins/inband/nic/collector_args.py +++ b/nodescraper/plugins/inband/nic/collector_args.py @@ -31,11 +31,11 @@ class NicCollectorArgs(CollectorArgs): - """Collector arguments for NicPlugin (niccli/nicctl).""" + """Collector arguments for NicPlugin (niccli/nicctl/bcmcli).""" commands: Optional[List[str]] = Field( default=None, - description="Optional list of niccli/nicctl commands to run. When None, default command set is used.", + description="Optional list of niccli/nicctl/bcmcli commands to run. When None, default command set is used.", ) use_sudo_niccli: bool = Field( default=True, @@ -45,3 +45,11 @@ class NicCollectorArgs(CollectorArgs): default=True, description="If True, run nicctl commands with sudo when required.", ) + use_sudo_bcmcli: bool = Field( + default=True, + description="If True, run bcmcli commands with sudo when required.", + ) + broadcom_cli_override: Optional[str] = Field( + default=None, + description="Force 'niccli' or 'bcmcli' instead of auto-detecting which Broadcom CLI is present.", + ) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index c9c0b606..10982a35 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -323,6 +323,12 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_LIST = "niccli --list" CMD_NICCLI_LIST_DEVICES = "niccli --list_devices" # new (> v233) CMD_NICCLI_LIST_DEVICES_LEGACY = "niccli --listdev" # legacy (<= v233) + CMD_NICCLI_DEVID = "niccli devid" + CMD_NICCLI_VERIFY = "niccli verify" + CMD_NICCLI_GLOBAL = [ + CMD_NICCLI_DEVID, + CMD_NICCLI_VERIFY, + ] CMD_NICCLI_DISCOVERY_LEGACY = [ CMD_NICCLI_LIST_DEVICES_LEGACY, CMD_NICCLI_LIST, @@ -364,11 +370,99 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): "niccli --dev {device_num} nvm --getoption pcie_relaxed_ordering" ) CMD_NICCLI_QOS_TEMPLATE_NEW = "niccli --dev {device_num} qos --ets --show" + # Show / device info + CMD_NICCLI_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} show" + CMD_NICCLI_SHOW_ALL_TEMPLATE_NEW = "niccli --dev {device_num} show --all" + CMD_NICCLI_SHOW_HEALTH_TEMPLATE_NEW = "niccli --dev {device_num} show --health" + CMD_NICCLI_SHOW_DEVICE_INFO_TEMPLATE_NEW = "niccli --dev {device_num} show --device_info" + CMD_NICCLI_SHOW_DEVICE_PCI_IDS_TEMPLATE_NEW = "niccli --dev {device_num} show --device_pci_ids" + CMD_NICCLI_SHOW_CERTIFICATE_TEMPLATE_NEW = "niccli --dev {device_num} show --certificate" + CMD_NICCLI_SHOW_PKG_VER_TEMPLATE_NEW = "niccli --dev {device_num} show --pkg_ver" + # Link + CMD_NICCLI_LINK_STATUS_TEMPLATE_NEW = "niccli --dev {device_num} link --status" + CMD_NICCLI_LINK_COUNTERS_TEMPLATE_NEW = "niccli --dev {device_num} link --counters --show" + # Linkdiag (read-only) + CMD_NICCLI_LINKDIAG_LOOPBACK_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} linkdiag --loopback --show" + CMD_NICCLI_LINKDIAG_DSCDUMP_TEMPLATE_NEW = "niccli --dev {device_num} linkdiag --dscdump --lane 0" + CMD_NICCLI_LINKDIAG_TXFIR_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} linkdiag --txfir --show --modulation_type NRZ --lane 0" + # QoS additional show commands + CMD_NICCLI_QOS_EGRESS_COSQ_TEMPLATE_NEW = "niccli --dev {device_num} qos --egress --cosq --show" + CMD_NICCLI_QOS_INGRESS_COSQ_TEMPLATE_NEW = "niccli --dev {device_num} qos --ingress --cosq --show" + CMD_NICCLI_QOS_RX_RATE_LIMIT_TEMPLATE_NEW = "niccli --dev {device_num} qos --rx_rate_limit --show" + CMD_NICCLI_QOS_TX_EP_RATE_LIMIT_TEMPLATE_NEW = "niccli --dev {device_num} qos --tx_ep_rate_limit --port 0 --show" + CMD_NICCLI_QOS_DSCP2PRIO_TEMPLATE_NEW = "niccli --dev {device_num} qos --dscp2prio" + CMD_NICCLI_QOS_LISTMAP_TEMPLATE_NEW = "niccli --dev {device_num} qos --listmap --pri2cos" + # NVM read commands + CMD_NICCLI_NVM_LIST_TEMPLATE_NEW = "niccli --dev {device_num} nvm --list" + CMD_NICCLI_NVM_LISTOPTIONS_TEMPLATE_NEW = "niccli --dev {device_num} nvm --listoptions" + CMD_NICCLI_NVM_VIEW_TEMPLATE_NEW = "niccli --dev {device_num} nvm --view" + CMD_NICCLI_NVM_VERIFY_TEMPLATE_NEW = "niccli --dev {device_num} nvm --verify" + # Firmware read + CMD_NICCLI_FW_LIVEPATCH_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} fw --livepatch --show" + # MSIX read + CMD_NICCLI_MSIX_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} msix --max_vectors --show --pf 0" + # Timesync read + CMD_NICCLI_TIMESYNC_PTP_TEMPLATE_NEW = "niccli --dev {device_num} timesync --ptp --show" + CMD_NICCLI_TIMESYNC_SYNCE_TEMPLATE_NEW = "niccli --dev {device_num} timesync --synce --show" + CMD_NICCLI_TIMESYNC_TSIO_TEMPLATE_NEW = "niccli --dev {device_num} timesync --tsio --show" + # Tunnel read + CMD_NICCLI_TUNNEL_RSS_TEMPLATE_NEW = "niccli --dev {device_num} tunnel --cfg --rss --show" + CMD_NICCLI_TUNNEL_VXLAN_IPV4_TEMPLATE_NEW = "niccli --dev {device_num} tunnel --cfg --vxlan --type ipv4 --show" + # PCIe counters + CMD_NICCLI_COUNTERS_PCIE_TEMPLATE_NEW = "niccli --dev {device_num} counters --pcie" + # Resource management + CMD_NICCLI_RESMGMT_PROFILE_TEMPLATE_NEW = "niccli --dev {device_num} resmgmt --all --profile --show" + # Cable / transceiver info + CMD_NICCLI_CABLE_MODULE_INFO_TEMPLATE_NEW = "niccli --dev {device_num} cable --module_info --show" CMD_NICCLI_PER_DEVICE_NEW = [ CMD_NICCLI_SUPPORT_RDMA_TEMPLATE_NEW, CMD_NICCLI_PERFORMANCE_PROFILE_TEMPLATE_NEW, CMD_NICCLI_PCIE_RELAXED_ORDERING_TEMPLATE_NEW, CMD_NICCLI_QOS_TEMPLATE_NEW, + # Show / device info + CMD_NICCLI_SHOW_TEMPLATE_NEW, + CMD_NICCLI_SHOW_ALL_TEMPLATE_NEW, + CMD_NICCLI_SHOW_HEALTH_TEMPLATE_NEW, + CMD_NICCLI_SHOW_DEVICE_INFO_TEMPLATE_NEW, + CMD_NICCLI_SHOW_DEVICE_PCI_IDS_TEMPLATE_NEW, + CMD_NICCLI_SHOW_CERTIFICATE_TEMPLATE_NEW, + CMD_NICCLI_SHOW_PKG_VER_TEMPLATE_NEW, + # Link + CMD_NICCLI_LINK_STATUS_TEMPLATE_NEW, + CMD_NICCLI_LINK_COUNTERS_TEMPLATE_NEW, + # Linkdiag (read-only) + CMD_NICCLI_LINKDIAG_LOOPBACK_SHOW_TEMPLATE_NEW, + CMD_NICCLI_LINKDIAG_DSCDUMP_TEMPLATE_NEW, + CMD_NICCLI_LINKDIAG_TXFIR_SHOW_TEMPLATE_NEW, + # QoS additional show + CMD_NICCLI_QOS_EGRESS_COSQ_TEMPLATE_NEW, + CMD_NICCLI_QOS_INGRESS_COSQ_TEMPLATE_NEW, + CMD_NICCLI_QOS_RX_RATE_LIMIT_TEMPLATE_NEW, + CMD_NICCLI_QOS_TX_EP_RATE_LIMIT_TEMPLATE_NEW, + CMD_NICCLI_QOS_DSCP2PRIO_TEMPLATE_NEW, + CMD_NICCLI_QOS_LISTMAP_TEMPLATE_NEW, + # NVM read + CMD_NICCLI_NVM_LIST_TEMPLATE_NEW, + CMD_NICCLI_NVM_LISTOPTIONS_TEMPLATE_NEW, + CMD_NICCLI_NVM_VIEW_TEMPLATE_NEW, + CMD_NICCLI_NVM_VERIFY_TEMPLATE_NEW, + # Firmware read + CMD_NICCLI_FW_LIVEPATCH_SHOW_TEMPLATE_NEW, + # MSIX read + CMD_NICCLI_MSIX_SHOW_TEMPLATE_NEW, + # Timesync read + CMD_NICCLI_TIMESYNC_PTP_TEMPLATE_NEW, + CMD_NICCLI_TIMESYNC_SYNCE_TEMPLATE_NEW, + CMD_NICCLI_TIMESYNC_TSIO_TEMPLATE_NEW, + # Tunnel read + CMD_NICCLI_TUNNEL_RSS_TEMPLATE_NEW, + CMD_NICCLI_TUNNEL_VXLAN_IPV4_TEMPLATE_NEW, + # PCIe counters + CMD_NICCLI_COUNTERS_PCIE_TEMPLATE_NEW, + # Resource management + CMD_NICCLI_RESMGMT_PROFILE_TEMPLATE_NEW, + # Cable / transceiver + CMD_NICCLI_CABLE_MODULE_INFO_TEMPLATE_NEW, ] # Backward compatibility: default to legacy templates CMD_NICCLI_SUPPORT_RDMA_TEMPLATE = CMD_NICCLI_SUPPORT_RDMA_TEMPLATE_LEGACY @@ -419,189 +513,482 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): "nicctl show version host-software", ] + # bcmcli (next-gen Broadcom, Thor Ultra): separate binaries, device targeted with -d suffix. + # Read-only show/query commands only — no fw/update/reset/set/loopback/coredump operations. + CMD_BCMCLI_VERSION = "bcmcli_show version" + CMD_BCMCLI_LIST = "bcmcli_show device_list" + + CMD_BCMCLI_GLOBAL = [ + "bcmcli_show version", + "bcmcli_show device_list", + ] + + # Per-device templates — {device_id} expanded at runtime from bcmcli_show device_list output + # NVM config queries + CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE = "bcmcli_config query support_rdma -d {device_id}" + CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE = "bcmcli_config query performance_profile -d {device_id}" + CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE = "bcmcli_config query pcie_relaxed_ordering -d {device_id}" + CMD_BCMCLI_AN_PROTOCOL_TEMPLATE = "bcmcli_config query AN_PROTOCOL -d {device_id}" + CMD_BCMCLI_NVM_SHOW_CONFS_TEMPLATE = "bcmcli_config show_confs -d {device_id}" + CMD_BCMCLI_NVM_SHOW_TEMPLATE = "bcmcli_nvm show -d {device_id}" + # QoS show commands + CMD_BCMCLI_QOS_TEMPLATE = "bcmcli_qos show qos -d {device_id}" + CMD_BCMCLI_QOS_HW_MAPS_TEMPLATE = "bcmcli_qos show hw-maps -d {device_id}" + CMD_BCMCLI_QOS_RX_PORT_RATELIMIT_TEMPLATE = "bcmcli_qos show rx-port-ratelimit -d {device_id}" + CMD_BCMCLI_QOS_TX_EP_RATELIMIT_TEMPLATE = "bcmcli_qos show tx-ep-ratelimit --port 0 -d {device_id}" + CMD_BCMCLI_QOS_INGRESS_COSQ_TEMPLATE = "bcmcli_qos show ingress --cosq -d {device_id}" + CMD_BCMCLI_QOS_EGRESS_COSQ_TEMPLATE = "bcmcli_qos show egress --cosq -d {device_id}" + # Device info / health + CMD_BCMCLI_HEALTH_TEMPLATE = "bcmcli_show health -d {device_id}" + CMD_BCMCLI_STATUS_TEMPLATE = "bcmcli_show status -d {device_id}" + CMD_BCMCLI_TEMPERATURE_TEMPLATE = "bcmcli_show temperature -d {device_id}" + CMD_BCMCLI_PCI_TEMPLATE = "bcmcli_show pci -d {device_id}" + CMD_BCMCLI_PHY_TEMPLATE = "bcmcli_show phy -d {device_id}" + CMD_BCMCLI_RESOURCE_COUNTS_TEMPLATE = "bcmcli_show resource_counts -d {device_id}" + # Link + CMD_BCMCLI_LINK_STATUS_TEMPLATE = "bcmcli_link show -d {device_id}" + # Hardware debug / diagnostics (read-only) + CMD_BCMCLI_TX_COUNTERS_TEMPLATE = "bcmcli_hwdbg dump_tx_counters -d {device_id}" + CMD_BCMCLI_RX_COUNTERS_TEMPLATE = "bcmcli_hwdbg dump_rx_counters -d {device_id}" + CMD_BCMCLI_PCIE_COUNTERS_TEMPLATE = "bcmcli_debug pcie counters -d {device_id}" + CMD_BCMCLI_VERIFY_TEMPLATE = "bcmcli_debug verify -d {device_id}" + CMD_BCMCLI_LOOPBACK_SHOW_TEMPLATE = "bcmcli_debug loopback -d {device_id}" + CMD_BCMCLI_DSCDUMP_TEMPLATE = "bcmcli_debug dscdump --lane 0 -d {device_id}" + CMD_BCMCLI_SERDES_TX_GET_TEMPLATE = "bcmcli_debug serdes_tx --get --modtype NRZ --lane 0 -d {device_id}" + # Firmware + CMD_BCMCLI_FW_VERSION_TEMPLATE = "bcmcli_fwmanager show fwpackage -d {device_id}" + CMD_BCMCLI_FW_CERTIFICATE_TEMPLATE = "bcmcli_fwmanager show certificate -d {device_id}" + CMD_BCMCLI_LIVEPATCH_SHOW_TEMPLATE = "bcmcli_livepatch show -d {device_id}" + # Tunnel + CMD_BCMCLI_TUNNEL_RSS_TEMPLATE = "bcmcli_tunnel show rss -d {device_id}" + CMD_BCMCLI_TUNNEL_VXLAN_IPV4_TEMPLATE = "bcmcli_tunnel show vxlan --type ipv4 -d {device_id}" + CMD_BCMCLI_TUNNEL_VXLAN_IPV6_TEMPLATE = "bcmcli_tunnel show vxlan --type ipv6 -d {device_id}" + # Keep old name as alias for backward compat + CMD_BCMCLI_TUNNEL_VXLAN_TEMPLATE = CMD_BCMCLI_TUNNEL_VXLAN_IPV4_TEMPLATE + # Timesync + CMD_BCMCLI_TIMESYNC_PTP_TEMPLATE = "bcmcli_timesync show ptp -d {device_id}" + CMD_BCMCLI_TIMESYNC_SYNCE_TEMPLATE = "bcmcli_timesync show synce -d {device_id}" + CMD_BCMCLI_TIMESYNC_TSIO_TEMPLATE = "bcmcli_timesync show tsio -d {device_id}" + # MSIX / dump + CMD_BCMCLI_MSIX_TEMPLATE = "bcmcli_config msixmv query --pf all -d {device_id}" + CMD_BCMCLI_SNAPDUMP_TEMPLATE = "bcmcli_dump snap_dump -d {device_id}" + + CMD_BCMCLI_PER_DEVICE = [ + # NVM config queries + CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE, + CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE, + CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE, + CMD_BCMCLI_AN_PROTOCOL_TEMPLATE, + CMD_BCMCLI_NVM_SHOW_CONFS_TEMPLATE, + CMD_BCMCLI_NVM_SHOW_TEMPLATE, + # QoS show + CMD_BCMCLI_QOS_TEMPLATE, + CMD_BCMCLI_QOS_HW_MAPS_TEMPLATE, + CMD_BCMCLI_QOS_RX_PORT_RATELIMIT_TEMPLATE, + CMD_BCMCLI_QOS_TX_EP_RATELIMIT_TEMPLATE, + CMD_BCMCLI_QOS_INGRESS_COSQ_TEMPLATE, + CMD_BCMCLI_QOS_EGRESS_COSQ_TEMPLATE, + # Device info / health + CMD_BCMCLI_HEALTH_TEMPLATE, + CMD_BCMCLI_STATUS_TEMPLATE, + CMD_BCMCLI_TEMPERATURE_TEMPLATE, + CMD_BCMCLI_PCI_TEMPLATE, + CMD_BCMCLI_PHY_TEMPLATE, + CMD_BCMCLI_RESOURCE_COUNTS_TEMPLATE, + # Link + CMD_BCMCLI_LINK_STATUS_TEMPLATE, + # Hardware debug / diagnostics + CMD_BCMCLI_TX_COUNTERS_TEMPLATE, + CMD_BCMCLI_RX_COUNTERS_TEMPLATE, + CMD_BCMCLI_PCIE_COUNTERS_TEMPLATE, + CMD_BCMCLI_VERIFY_TEMPLATE, + CMD_BCMCLI_LOOPBACK_SHOW_TEMPLATE, + CMD_BCMCLI_DSCDUMP_TEMPLATE, + CMD_BCMCLI_SERDES_TX_GET_TEMPLATE, + # Firmware + CMD_BCMCLI_FW_VERSION_TEMPLATE, + CMD_BCMCLI_FW_CERTIFICATE_TEMPLATE, + CMD_BCMCLI_LIVEPATCH_SHOW_TEMPLATE, + # Tunnel + CMD_BCMCLI_TUNNEL_RSS_TEMPLATE, + CMD_BCMCLI_TUNNEL_VXLAN_IPV4_TEMPLATE, + CMD_BCMCLI_TUNNEL_VXLAN_IPV6_TEMPLATE, + # Timesync + CMD_BCMCLI_TIMESYNC_PTP_TEMPLATE, + CMD_BCMCLI_TIMESYNC_SYNCE_TEMPLATE, + CMD_BCMCLI_TIMESYNC_TSIO_TEMPLATE, + # MSIX / dump + CMD_BCMCLI_MSIX_TEMPLATE, + CMD_BCMCLI_SNAPDUMP_TEMPLATE, + ] + def collect_data( self, args: Optional[NicCollectorArgs] = None, ) -> Tuple[TaskResult, Optional[NicDataModel]]: - """Run niccli/nicctl commands and store stdout/stderr/exit_code per command.""" + """Run niccli/nicctl/bcmcli commands and store stdout/stderr/exit_code per command.""" use_sudo_niccli = args.use_sudo_niccli if args else True use_sudo_nicctl = args.use_sudo_nicctl if args else True + use_sudo_bcmcli = args.use_sudo_bcmcli if args else True custom_commands = args.commands if args and args.commands else None results: dict[str, NicCommandResult] = {} - # Detect niccli version to choose command set (legacy <= v233 vs new > v233) - niccli_version: Optional[int] = None - res_version = self._run_sut_cmd(NicCollector.CMD_NICCLI_VERSION, sudo=use_sudo_niccli) - if res_version.exit_code == 0 and res_version.stdout: - niccli_version = _parse_niccli_version(res_version.stdout) - results[NicCollector.CMD_NICCLI_VERSION] = NicCommandResult( - command=NicCollector.CMD_NICCLI_VERSION, - stdout=res_version.stdout or "", - stderr=res_version.stderr or "", - exit_code=res_version.exit_code, - ) - - # Discovery: device numbers from niccli - device_nums: List[int] = [] - discovery_cmds = _get_niccli_discovery_commands(niccli_version) - for list_cmd in discovery_cmds: - res = self._run_sut_cmd(list_cmd, sudo=use_sudo_niccli) - results[list_cmd] = NicCommandResult( - command=list_cmd, - stdout=res.stdout or "", - stderr=res.stderr or "", - exit_code=res.exit_code, + # Detect which Broadcom CLI is present (bcmcli takes priority over niccli). + broadcom_cli = self._detect_broadcom_cli(args, results) + + # --- bcmcli path (next-gen Broadcom, Thor Ultra) --- + if broadcom_cli == "bcmcli": + device_ids: List[str] = [] + res_list = self._run_sut_cmd(NicCollector.CMD_BCMCLI_LIST, sudo=use_sudo_bcmcli) + results[NicCollector.CMD_BCMCLI_LIST] = NicCommandResult( + command=NicCollector.CMD_BCMCLI_LIST, + stdout=res_list.stdout or "", + stderr=res_list.stderr or "", + exit_code=res_list.exit_code, ) - if res.exit_code == 0 and res.stdout: - device_nums = _parse_niccli_device_numbers(res.stdout) - if device_nums: - break - - # Discovery: card IDs from nicctl show card (text); same output used for pensando_nic_cards - card_ids: List[str] = [] - card_list_from_text: List[Dict[str, Any]] = [] - res_card = self._run_sut_cmd(NicCollector.CMD_NICCTL_CARD_TEXT, sudo=use_sudo_nicctl) - results[NicCollector.CMD_NICCTL_CARD_TEXT] = NicCommandResult( - command=NicCollector.CMD_NICCTL_CARD_TEXT, - stdout=res_card.stdout or "", - stderr=res_card.stderr or "", - exit_code=res_card.exit_code, - ) - if res_card.exit_code == 0 and res_card.stdout: - legacy_cards = self._parse_nicctl_card(res_card.stdout) - card_ids = [c.id for c in legacy_cards] - card_list_from_text = [c.model_dump() for c in legacy_cards] - - if custom_commands is None and not device_nums and not card_ids: - self._log_event( - category=EventCategory.NETWORK, - description="No Broadcom (niccli) or Pensando (nicctl) NIC hardware detected", - priority=EventPriority.INFO, - ) - self.result.status = ExecutionStatus.NOT_RAN - self.result.message = ( - "No Broadcom (niccli) or Pensando (nicctl) NIC hardware detected; " - "NIC collection skipped" + if res_list.exit_code == 0 and res_list.stdout: + device_ids = _parse_bcmcli_device_list(res_list.stdout) + + # Discovery: card IDs from nicctl (Pensando may still coexist) + card_ids: List[str] = [] + card_list_from_text: List[Dict[str, Any]] = [] + res_card = self._run_sut_cmd(NicCollector.CMD_NICCTL_CARD_TEXT, sudo=use_sudo_nicctl) + results[NicCollector.CMD_NICCTL_CARD_TEXT] = NicCommandResult( + command=NicCollector.CMD_NICCTL_CARD_TEXT, + stdout=res_card.stdout or "", + stderr=res_card.stderr or "", + exit_code=res_card.exit_code, ) - return self.result, None - - # Build full command list (expand placeholders) - if custom_commands is not None: - commands_to_run: List[str] = [] - for tpl in custom_commands: - if "{device_num}" in tpl: - for d in device_nums: - commands_to_run.append(tpl.format(device_num=d)) - elif "{card_id}" in tpl: - for c in card_ids: - commands_to_run.append(tpl.format(card_id=c)) - else: - commands_to_run.append(tpl) - else: - commands_to_run = [] - # niccli list already stored - if device_nums: - per_device_templates = _get_niccli_per_device_templates(niccli_version) - for tpl in per_device_templates: - for d in device_nums: - commands_to_run.append(tpl.format(device_num=d)) - if card_ids: - for c in NicCollector.CMD_NICCTL_GLOBAL: - commands_to_run.append(c) - for tpl in NicCollector.CMD_NICCTL_PER_CARD: - for cid in card_ids: - commands_to_run.append(tpl.format(card_id=cid)) - for cmd in NicCollector.CMD_NICCTL_LEGACY_TEXT: - commands_to_run.append(cmd) + if res_card.exit_code == 0 and res_card.stdout: + legacy_cards = self._parse_nicctl_card(res_card.stdout) + card_ids = [c.id for c in legacy_cards] + card_list_from_text = [c.model_dump() for c in legacy_cards] - # Run each command and store (artifact-only commands are not added to results / data model). - for cmd in commands_to_run: - if cmd in results: - continue - is_niccli = cmd.strip().startswith("niccli") - sudo = use_sudo_niccli if is_niccli else use_sudo_nicctl - res = self._run_sut_cmd(cmd, sudo=sudo) - has_error_output = has_command_error_output(res.stderr or "", res.stdout or "") - if _is_artifact_only_command(cmd): + if custom_commands is None and not device_ids and not card_ids: + self._log_event( + category=EventCategory.NETWORK, + description="No bcmcli or Pensando (nicctl) NIC hardware detected", + priority=EventPriority.INFO, + ) + self.result.status = ExecutionStatus.NOT_RAN + self.result.message = ( + "No bcmcli or Pensando (nicctl) NIC hardware detected; NIC collection skipped" + ) + return self.result, None + + # Build bcmcli command list + if custom_commands is not None: + commands_to_run: List[str] = [] + for tpl in custom_commands: + if "{device_id}" in tpl: + for d in device_ids: + commands_to_run.append(tpl.format(device_id=d)) + elif "{card_id}" in tpl: + for c in card_ids: + commands_to_run.append(tpl.format(card_id=c)) + else: + commands_to_run.append(tpl) + else: + commands_to_run = [] + if device_ids: + for tpl in NicCollector.CMD_BCMCLI_PER_DEVICE: + for d in device_ids: + commands_to_run.append(tpl.format(device_id=d)) + if card_ids: + for c in NicCollector.CMD_NICCTL_GLOBAL: + commands_to_run.append(c) + for tpl in NicCollector.CMD_NICCTL_PER_CARD: + for cid in card_ids: + commands_to_run.append(tpl.format(card_id=cid)) + for cmd in NicCollector.CMD_NICCTL_LEGACY_TEXT: + commands_to_run.append(cmd) + + # Run bcmcli commands + for cmd in commands_to_run: + if cmd in results: + continue + is_bcmcli = cmd.strip().startswith("bcmcli_") + is_niccli_cmd = cmd.strip().startswith("niccli") + sudo = use_sudo_bcmcli if is_bcmcli else (use_sudo_niccli if is_niccli_cmd else use_sudo_nicctl) + res = self._run_sut_cmd(cmd, sudo=sudo) + has_error_output = has_command_error_output(res.stderr or "", res.stdout or "") + if _is_artifact_only_command(cmd): + if res.exit_code != 0: + self._log_event( + category=EventCategory.NETWORK, + description=f"bcmcli/nicctl command failed: {cmd}", + data=command_result_event_data(res), + priority=EventPriority.WARNING, + ) + elif has_error_output: + self._log_event( + category=EventCategory.NETWORK, + description=f"bcmcli/nicctl reported errors (exit 0): {cmd}", + data=command_result_event_data(res), + priority=EventPriority.WARNING, + ) + continue + results[cmd] = NicCommandResult( + command=cmd, + stdout=res.stdout or "", + stderr=res.stderr or "", + exit_code=res.exit_code, + ) if res.exit_code != 0: self._log_event( category=EventCategory.NETWORK, - description=f"niccli/nicctl command failed: {cmd}", + description=f"bcmcli/nicctl command failed: {cmd}", data=command_result_event_data(res), priority=EventPriority.WARNING, ) elif has_error_output: self._log_event( category=EventCategory.NETWORK, - description=f"niccli/nicctl reported errors (exit 0): {cmd}", + description=f"bcmcli/nicctl reported errors (exit 0): {cmd}", data=command_result_event_data(res), priority=EventPriority.WARNING, ) - continue - results[cmd] = NicCommandResult( - command=cmd, - stdout=res.stdout or "", - stderr=res.stderr or "", - exit_code=res.exit_code, + + # Populate broadcom_nic_* fields from bcmcli results keyed by device_id + broadcom_support_rdma: Dict[int, str] = {} + broadcom_performance_profile: Dict[int, str] = {} + broadcom_pcie_relaxed_ordering: Dict[int, str] = {} + broadcom_qos_data: Dict[int, NicCliQos] = {} + for idx, dev_id in enumerate(device_ids): + sr_cmd = NicCollector.CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE.format(device_id=dev_id) + r_sr = results.get(sr_cmd) + if r_sr and r_sr.exit_code == 0 and (r_sr.stdout or "").strip(): + broadcom_support_rdma[idx] = r_sr.stdout.strip() + pp_cmd = NicCollector.CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE.format(device_id=dev_id) + r_pp = results.get(pp_cmd) + if r_pp and r_pp.exit_code == 0 and (r_pp.stdout or "").strip(): + broadcom_performance_profile[idx] = r_pp.stdout.strip() + ro_cmd = NicCollector.CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE.format(device_id=dev_id) + r_ro = results.get(ro_cmd) + if r_ro and r_ro.exit_code == 0 and (r_ro.stdout or "").strip(): + broadcom_pcie_relaxed_ordering[idx] = r_ro.stdout.strip() + qos_cmd = NicCollector.CMD_BCMCLI_QOS_TEMPLATE.format(device_id=dev_id) + r_qos = results.get(qos_cmd) + if r_qos and r_qos.exit_code == 0 and (r_qos.stdout or "").strip(): + broadcom_qos_data[idx] = _parse_bcmcli_qos(dev_id, r_qos.stdout) + + broadcom_devices: List[NicCliDevice] = [] + + # Pensando structured data (if nicctl also present) + ( + pensando_cards, + pensando_dcqcn, + pensando_environment, + pensando_lif, + pensando_pcie_ats, + pensando_ports, + pensando_qos, + pensando_rdma_statistics, + pensando_version_host_software, + pensando_version_firmware, + ) = self._collect_pensando_nic_structured(results) + + # nicctl card_show (not applicable in bcmcli path) + card_show = None + cards: list = [] + port = lif = qos = rdma = dcqcn = environment = version = None + + else: + # --- niccli path (current Broadcom, Thor2) --- + niccli_version: Optional[int] = None + res_version = self._run_sut_cmd(NicCollector.CMD_NICCLI_VERSION, sudo=use_sudo_niccli) + if res_version.exit_code == 0 and res_version.stdout: + niccli_version = _parse_niccli_version(res_version.stdout) + results[NicCollector.CMD_NICCLI_VERSION] = NicCommandResult( + command=NicCollector.CMD_NICCLI_VERSION, + stdout=res_version.stdout or "", + stderr=res_version.stderr or "", + exit_code=res_version.exit_code, ) - if res.exit_code != 0: - self._log_event( - category=EventCategory.NETWORK, - description=f"niccli/nicctl command failed: {cmd}", - data=command_result_event_data(res), - priority=EventPriority.WARNING, + + # Discovery: device numbers from niccli + device_nums: List[int] = [] + discovery_cmds = _get_niccli_discovery_commands(niccli_version) + for list_cmd in discovery_cmds: + res = self._run_sut_cmd(list_cmd, sudo=use_sudo_niccli) + results[list_cmd] = NicCommandResult( + command=list_cmd, + stdout=res.stdout or "", + stderr=res.stderr or "", + exit_code=res.exit_code, ) - elif has_error_output: + if res.exit_code == 0 and res.stdout: + device_nums = _parse_niccli_device_numbers(res.stdout) + if device_nums: + break + + # Discovery: card IDs from nicctl show card (text); same output used for pensando_nic_cards + card_ids = [] + card_list_from_text = [] + res_card = self._run_sut_cmd(NicCollector.CMD_NICCTL_CARD_TEXT, sudo=use_sudo_nicctl) + results[NicCollector.CMD_NICCTL_CARD_TEXT] = NicCommandResult( + command=NicCollector.CMD_NICCTL_CARD_TEXT, + stdout=res_card.stdout or "", + stderr=res_card.stderr or "", + exit_code=res_card.exit_code, + ) + if res_card.exit_code == 0 and res_card.stdout: + legacy_cards = self._parse_nicctl_card(res_card.stdout) + card_ids = [c.id for c in legacy_cards] + card_list_from_text = [c.model_dump() for c in legacy_cards] + + if custom_commands is None and not device_nums and not card_ids: self._log_event( category=EventCategory.NETWORK, - description=f"niccli/nicctl reported errors (exit 0): {cmd}", - data=command_result_event_data(res), - priority=EventPriority.WARNING, + description="No Broadcom (niccli) or Pensando (nicctl) NIC hardware detected", + priority=EventPriority.INFO, + ) + self.result.status = ExecutionStatus.NOT_RAN + self.result.message = ( + "No Broadcom (niccli) or Pensando (nicctl) NIC hardware detected; " + "NIC collection skipped" + ) + return self.result, None + + # Build full command list (expand placeholders) + if custom_commands is not None: + commands_to_run = [] + for tpl in custom_commands: + if "{device_num}" in tpl: + for d in device_nums: + commands_to_run.append(tpl.format(device_num=d)) + elif "{card_id}" in tpl: + for c in card_ids: + commands_to_run.append(tpl.format(card_id=c)) + else: + commands_to_run.append(tpl) + else: + commands_to_run = [] + for cmd in NicCollector.CMD_NICCLI_GLOBAL: + commands_to_run.append(cmd) + if device_nums: + per_device_templates = _get_niccli_per_device_templates(niccli_version) + for tpl in per_device_templates: + for d in device_nums: + commands_to_run.append(tpl.format(device_num=d)) + if card_ids: + for c in NicCollector.CMD_NICCTL_GLOBAL: + commands_to_run.append(c) + for tpl in NicCollector.CMD_NICCTL_PER_CARD: + for cid in card_ids: + commands_to_run.append(tpl.format(card_id=cid)) + for cmd in NicCollector.CMD_NICCTL_LEGACY_TEXT: + commands_to_run.append(cmd) + + # Run each command and store (artifact-only commands are not added to results / data model). + for cmd in commands_to_run: + if cmd in results: + continue + is_niccli_cmd = cmd.strip().startswith("niccli") + sudo = use_sudo_niccli if is_niccli_cmd else use_sudo_nicctl + res = self._run_sut_cmd(cmd, sudo=sudo) + has_error_output = has_command_error_output(res.stderr or "", res.stdout or "") + if _is_artifact_only_command(cmd): + if res.exit_code != 0: + self._log_event( + category=EventCategory.NETWORK, + description=f"niccli/nicctl command failed: {cmd}", + data=command_result_event_data(res), + priority=EventPriority.WARNING, + ) + elif has_error_output: + self._log_event( + category=EventCategory.NETWORK, + description=f"niccli/nicctl reported errors (exit 0): {cmd}", + data=command_result_event_data(res), + priority=EventPriority.WARNING, + ) + continue + results[cmd] = NicCommandResult( + command=cmd, + stdout=res.stdout or "", + stderr=res.stderr or "", + exit_code=res.exit_code, ) + if res.exit_code != 0: + self._log_event( + category=EventCategory.NETWORK, + description=f"niccli/nicctl command failed: {cmd}", + data=command_result_event_data(res), + priority=EventPriority.WARNING, + ) + elif has_error_output: + self._log_event( + category=EventCategory.NETWORK, + description=f"niccli/nicctl reported errors (exit 0): {cmd}", + data=command_result_event_data(res), + priority=EventPriority.WARNING, + ) - # Parse JSON for building structured domain objects (artifact-only commands have no stdout, so not in parsed). - parsed: Dict[str, Any] = {} - for cmd, r in results.items(): - if r.exit_code != 0 or not (r.stdout or "").strip(): - continue - try: - parsed[cmd] = json.loads(r.stdout.strip()) - except (ValueError, TypeError): - pass + # Parse JSON for building structured domain objects (artifact-only commands have no stdout, so not in parsed). + parsed: Dict[str, Any] = {} + for cmd, r in results.items(): + if r.exit_code != 0 or not (r.stdout or "").strip(): + continue + try: + parsed[cmd] = json.loads(r.stdout.strip()) + except (ValueError, TypeError): + pass - # Build structured domain objects from JSON/raw output (card_show/cards from text when present). - ( - card_show, - cards, - port, - lif, - qos, - rdma, - dcqcn, - environment, - version, - ) = _build_structured( - results, parsed, card_ids, card_list_override=card_list_from_text or None - ) + # Build structured domain objects from JSON/raw output (card_show/cards from text when present). + ( + card_show, + cards, + port, + lif, + qos, + rdma, + dcqcn, + environment, + version, + ) = _build_structured( + results, parsed, card_ids, card_list_override=card_list_from_text or None + ) - # card_show and cards (can be large) go to TextFileArtifacts; excluded from datamodel. - if card_show is not None: - self.result.artifacts.append( - TextFileArtifact( - filename="niccli_card_show.json", - contents=card_show.model_dump_json(indent=2), + # card_show and cards (can be large) go to TextFileArtifacts; excluded from datamodel. + if card_show is not None: + self.result.artifacts.append( + TextFileArtifact( + filename="niccli_card_show.json", + contents=card_show.model_dump_json(indent=2), + ) ) - ) - if cards: - self.result.artifacts.append( - TextFileArtifact( - filename="niccli_cards.json", - contents=json.dumps([c.model_dump(mode="json") for c in cards], indent=2), + if cards: + self.result.artifacts.append( + TextFileArtifact( + filename="niccli_cards.json", + contents=json.dumps([c.model_dump(mode="json") for c in cards], indent=2), + ) ) - ) - # Serialized nicclidatamodel.json: no stdout in results, truncated command/stderr (keeps file small). + # Legacy text parsers: populate broadcom_nic_* and pensando_nic_* for the datamodel. + ( + broadcom_devices, + broadcom_qos_data, + broadcom_support_rdma, + broadcom_performance_profile, + broadcom_pcie_relaxed_ordering, + ) = self._collect_broadcom_nic_structured(results, niccli_version=niccli_version) + ( + pensando_cards, + pensando_dcqcn, + pensando_environment, + pensando_lif, + pensando_pcie_ats, + pensando_ports, + pensando_qos, + pensando_rdma_statistics, + pensando_version_host_software, + pensando_version_firmware, + ) = self._collect_pensando_nic_structured(results) + + # Serialized datamodel: no stdout in results, truncated command/stderr (keeps file small). # Command output lives on disk from _run_sut_cmd; model keeps only command identity and status. def _truncate(s: str, max_len: int) -> str: if not s or len(s) <= max_len: @@ -618,33 +1005,13 @@ def _truncate(s: str, max_len: int) -> str: for cmd, r in results.items() } - # Legacy text parsers: populate broadcom_nic_* and pensando_nic_* for the datamodel. - ( - broadcom_devices, - broadcom_qos_data, - broadcom_support_rdma, - broadcom_performance_profile, - broadcom_pcie_relaxed_ordering, - ) = self._collect_broadcom_nic_structured(results, niccli_version=niccli_version) - ( - pensando_cards, - pensando_dcqcn, - pensando_environment, - pensando_lif, - pensando_pcie_ats, - pensando_ports, - pensando_qos, - pensando_rdma_statistics, - pensando_version_host_software, - pensando_version_firmware, - ) = self._collect_pensando_nic_structured(results) - + cli_label = "bcmcli" if broadcom_cli == "bcmcli" else "niccli/nicctl" if not results or all(r.exit_code != 0 for r in results.values()): self.result.status = ExecutionStatus.EXECUTION_FAILURE - self.result.message = "All niccli/nicctl commands failed or no commands were run" + self.result.message = f"All {cli_label} commands failed or no commands were run" else: self.result.status = ExecutionStatus.OK - self.result.message = f"Collected {len(results)} niccli/nicctl command results" + self.result.message = f"Collected {len(results)} {cli_label} command results" nicctl_card_logs = None if card_show is not None: @@ -666,6 +1033,7 @@ def _truncate(s: str, max_len: int) -> str: dcqcn=dcqcn, environment=environment, version=version, + broadcom_cli_type=broadcom_cli, broadcom_nic_devices=broadcom_devices, broadcom_nic_qos=broadcom_qos_data, broadcom_nic_support_rdma=broadcom_support_rdma, @@ -683,6 +1051,36 @@ def _truncate(s: str, max_len: int) -> str: pensando_nic_version_firmware=pensando_version_firmware, ) + def _detect_broadcom_cli( + self, + args: Optional[NicCollectorArgs], + results: Dict[str, "NicCommandResult"], + ) -> str: + """Return 'bcmcli' if bcmcli_show version exits 0, otherwise 'niccli'. + + Respects args.broadcom_cli_override ('niccli' or 'bcmcli') when set. + Stores the bcmcli version probe result in results so it appears in the datamodel. + """ + override = ( + args.broadcom_cli_override.strip().lower() + if (args and args.broadcom_cli_override) + else None + ) + if override in ("bcmcli", "niccli"): + return override + + use_sudo = args.use_sudo_bcmcli if args else True + res = self._run_sut_cmd(NicCollector.CMD_BCMCLI_VERSION, sudo=use_sudo) + results[NicCollector.CMD_BCMCLI_VERSION] = NicCommandResult( + command=NicCollector.CMD_BCMCLI_VERSION, + stdout=res.stdout or "", + stderr=res.stderr or "", + exit_code=res.exit_code, + ) + if res.exit_code == 0: + return "bcmcli" + return "niccli" + def _collect_broadcom_nic_structured( self, results: Dict[str, NicCommandResult], @@ -1292,6 +1690,164 @@ def _get_niccli_discovery_commands(version: Optional[int]) -> List[str]: return NicCollector.CMD_NICCLI_DISCOVERY_LEGACY.copy() +def _parse_bcmcli_qos(device_id: str, stdout: str) -> NicCliQos: + """Parse bcmcli_qos show qos output into NicCliQos. + + Real Thor Ultra format: + ETS Configuration: + TC TX_BW RX_BW TSA RateLimit Priority + ----------------------------------------------------------------------- + 0 50 0 ETS 100 0 1 2 4 5 6 + 1 50 0 ETS 100 3 + 2 0 0 Strict 100 7 + + PFC configuration: + priority 0 1 2 3 4 5 6 7 + enabled 0 0 0 1 0 0 0 0 + + APP TLV Configuration: + Index Selector Priority DSCP/Protocol + ----------------------------------------------------------------------- + 0 5 7 48 + + Maps into NicCliQos fields: + prio_map — priority→TC from the Priority column of the ETS table + tc_bandwidth — TX_BW per TC + tsa_map — TSA string per TC + tc_rate_limit — RateLimit per TC + pfc_enabled — integer bitmask from the 'enabled' row (bit 0 = priority 0) + app_entries — APP TLV rows as NicCliQosAppEntry + """ + prio_map: Dict[int, int] = {} + tc_bandwidth: List[int] = [] + tsa_map: Dict[int, str] = {} + tc_rate_limit: List[int] = [] + pfc_enabled: Optional[int] = None + app_entries: List[NicCliQosAppEntry] = [] + + section = None + pfc_priority_order: List[int] = [] + + for line in stdout.splitlines(): + stripped = line.strip() + if not stripped: + continue + + if stripped.startswith("ETS Configuration"): + section = "ets" + continue + if stripped.startswith("PFC configuration"): + section = "pfc" + continue + if stripped.startswith("APP TLV Configuration"): + section = "app" + continue + + # Skip separator and column header lines + if re.match(r"^-{5,}", stripped): + continue + + if section == "ets": + # Header line: TC TX_BW RX_BW TSA RateLimit Priority + if stripped.startswith("TC"): + continue + parts = stripped.split() + if not parts or not parts[0].isdigit(): + continue + tc = int(parts[0]) + # Ensure lists are long enough for this TC index + while len(tc_bandwidth) <= tc: + tc_bandwidth.append(0) + while len(tsa_map) <= tc: + tsa_map[len(tsa_map)] = "" + while len(tc_rate_limit) <= tc: + tc_rate_limit.append(0) + if len(parts) >= 2 and parts[1].isdigit(): + tc_bandwidth[tc] = int(parts[1]) + if len(parts) >= 4: + tsa_map[tc] = parts[3] + if len(parts) >= 5 and parts[4].isdigit(): + tc_rate_limit[tc] = int(parts[4]) + # Priority column: remaining tokens are space-separated priority values + for pri_str in parts[5:]: + if pri_str.isdigit(): + prio_map[int(pri_str)] = tc + + elif section == "pfc": + if stripped.startswith("priority"): + # Header: "priority 0 1 2 3 4 5 6 7" + pfc_priority_order = [int(x) for x in stripped.split()[1:] if x.isdigit()] + elif stripped.startswith("enabled"): + vals = [int(x) for x in stripped.split()[1:] if x.isdigit()] + mask = 0 + for i, v in enumerate(vals): + if v and i < len(pfc_priority_order): + mask |= (1 << pfc_priority_order[i]) + pfc_enabled = mask + + elif section == "app": + # Header: "Index Selector Priority DSCP/Protocol" + if stripped.startswith("Index") or stripped.startswith("DSCP"): + continue + parts = stripped.split() + if not parts or not parts[0].isdigit(): + continue + entry = NicCliQosAppEntry() + if len(parts) >= 2 and parts[1].isdigit(): + entry.sel = int(parts[1]) + if len(parts) >= 3 and parts[2].isdigit(): + entry.priority = int(parts[2]) + if len(parts) >= 4 and parts[3].isdigit(): + entry.port = int(parts[3]) + app_entries.append(entry) + + return NicCliQos( + device_num=0, + raw_output=stdout, + prio_map=prio_map, + tc_bandwidth=tc_bandwidth, + tsa_map=tsa_map, + pfc_enabled=pfc_enabled, + app_entries=app_entries, + tc_rate_limit=tc_rate_limit, + ) + + +def _parse_bcmcli_device_list(stdout: str) -> List[str]: + """Parse bcmcli_show device_list output into a list of PCI address device identifiers. + + Real Thor Ultra output is a columnar table where the PCI address is the second column: + Device Type PCI Address RDMA NET NUMA + ThorUltra(A0) 0000:81:00.0 bng_re0 enp129s0np0 0 + + Scans each non-header data line for a PCI address pattern anywhere in the line. + Also handles the unlikely integer-index format ('0) BCM...') for robustness. + """ + device_ids: List[str] = [] + pci_re = re.compile(r"\b([0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])\b") + for line in stdout.splitlines(): + line = line.strip() + if not line: + continue + # Skip header lines + if "PCI Address" in line or "Device Type" in line: + continue + # Integer index: lines like "0) BCM..." (fallback for alternate formats) + idx_match = re.match(r"^(\d+)\s*\)", line) + if idx_match: + device_ids.append(idx_match.group(1)) + continue + # PCI address anywhere on the line (covers second-column table format) + pci_match = pci_re.search(line) + if pci_match: + device_ids.append(pci_match.group(1)) + seen: List[str] = [] + for d in device_ids: + if d not in seen: + seen.append(d) + return seen + + # Commands whose output is very long; store only as file artifacts, not in data model. def _is_artifact_only_command(cmd: str) -> bool: c = cmd.strip() diff --git a/nodescraper/plugins/inband/nic/nic_data.py b/nodescraper/plugins/inband/nic/nic_data.py index 40e16216..9720c4c1 100644 --- a/nodescraper/plugins/inband/nic/nic_data.py +++ b/nodescraper/plugins/inband/nic/nic_data.py @@ -392,6 +392,11 @@ class NicDataModel(DataModel): pensando_nic_version_host_software: Optional[PensandoNicVersionHostSoftware] = None pensando_nic_version_firmware: List[PensandoNicVersionFirmware] = Field(default_factory=list) + broadcom_cli_type: Optional[str] = Field( + default=None, + description="'niccli' or 'bcmcli', whichever Broadcom CLI was detected and used for collection.", + ) + # Raw nicctl card log output for regex-based error detection nicctl_card_logs: Optional[Dict[str, str]] = Field( default=None, From a37ee5c060abbd957bba25caec298079a5497d61 Mon Sep 17 00:00:00 2001 From: jolibran Date: Thu, 6 Aug 2026 15:11:57 -0400 Subject: [PATCH 2/8] DCERR-699: add bcmcli support and expand niccli read-only commands --- .../plugins/inband/nic/nic_collector.py | 127 ++++++++---------- 1 file changed, 56 insertions(+), 71 deletions(-) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index 10982a35..119c36d1 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -71,8 +71,6 @@ NICCLI_VERSION_LEGACY_MAX = 233 # Commands use -dev/-getoption/getqos; for version > this use --dev/--getoption/qos --ets --show # Max lengths for fields included in the serialized datamodel (keeps nicclidatamodel.json small). -MAX_COMMAND_LENGTH_IN_DATAMODEL = 256 -MAX_STDERR_LENGTH_IN_DATAMODEL = 512 def _parse_niccli_version(stdout: str) -> Optional[int]: @@ -327,7 +325,6 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_VERIFY = "niccli verify" CMD_NICCLI_GLOBAL = [ CMD_NICCLI_DEVID, - CMD_NICCLI_VERIFY, ] CMD_NICCLI_DISCOVERY_LEGACY = [ CMD_NICCLI_LIST_DEVICES_LEGACY, @@ -420,39 +417,25 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_PCIE_RELAXED_ORDERING_TEMPLATE_NEW, CMD_NICCLI_QOS_TEMPLATE_NEW, # Show / device info - CMD_NICCLI_SHOW_TEMPLATE_NEW, CMD_NICCLI_SHOW_ALL_TEMPLATE_NEW, CMD_NICCLI_SHOW_HEALTH_TEMPLATE_NEW, - CMD_NICCLI_SHOW_DEVICE_INFO_TEMPLATE_NEW, - CMD_NICCLI_SHOW_DEVICE_PCI_IDS_TEMPLATE_NEW, CMD_NICCLI_SHOW_CERTIFICATE_TEMPLATE_NEW, CMD_NICCLI_SHOW_PKG_VER_TEMPLATE_NEW, # Link CMD_NICCLI_LINK_STATUS_TEMPLATE_NEW, - CMD_NICCLI_LINK_COUNTERS_TEMPLATE_NEW, - # Linkdiag (read-only) + # Linkdiag (read-only) — dscdump omitted (~5s per device) CMD_NICCLI_LINKDIAG_LOOPBACK_SHOW_TEMPLATE_NEW, - CMD_NICCLI_LINKDIAG_DSCDUMP_TEMPLATE_NEW, CMD_NICCLI_LINKDIAG_TXFIR_SHOW_TEMPLATE_NEW, # QoS additional show CMD_NICCLI_QOS_EGRESS_COSQ_TEMPLATE_NEW, CMD_NICCLI_QOS_INGRESS_COSQ_TEMPLATE_NEW, CMD_NICCLI_QOS_RX_RATE_LIMIT_TEMPLATE_NEW, - CMD_NICCLI_QOS_TX_EP_RATE_LIMIT_TEMPLATE_NEW, - CMD_NICCLI_QOS_DSCP2PRIO_TEMPLATE_NEW, CMD_NICCLI_QOS_LISTMAP_TEMPLATE_NEW, - # NVM read - CMD_NICCLI_NVM_LIST_TEMPLATE_NEW, - CMD_NICCLI_NVM_LISTOPTIONS_TEMPLATE_NEW, - CMD_NICCLI_NVM_VIEW_TEMPLATE_NEW, - CMD_NICCLI_NVM_VERIFY_TEMPLATE_NEW, - # Firmware read - CMD_NICCLI_FW_LIVEPATCH_SHOW_TEMPLATE_NEW, + # NVM read — list/listoptions/view/verify omitted (20-120s per device) # MSIX read CMD_NICCLI_MSIX_SHOW_TEMPLATE_NEW, # Timesync read CMD_NICCLI_TIMESYNC_PTP_TEMPLATE_NEW, - CMD_NICCLI_TIMESYNC_SYNCE_TEMPLATE_NEW, CMD_NICCLI_TIMESYNC_TSIO_TEMPLATE_NEW, # Tunnel read CMD_NICCLI_TUNNEL_RSS_TEMPLATE_NEW, @@ -461,8 +444,6 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_COUNTERS_PCIE_TEMPLATE_NEW, # Resource management CMD_NICCLI_RESMGMT_PROFILE_TEMPLATE_NEW, - # Cable / transceiver - CMD_NICCLI_CABLE_MODULE_INFO_TEMPLATE_NEW, ] # Backward compatibility: default to legacy templates CMD_NICCLI_SUPPORT_RDMA_TEMPLATE = CMD_NICCLI_SUPPORT_RDMA_TEMPLATE_LEGACY @@ -569,15 +550,15 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_BCMCLI_TIMESYNC_PTP_TEMPLATE = "bcmcli_timesync show ptp -d {device_id}" CMD_BCMCLI_TIMESYNC_SYNCE_TEMPLATE = "bcmcli_timesync show synce -d {device_id}" CMD_BCMCLI_TIMESYNC_TSIO_TEMPLATE = "bcmcli_timesync show tsio -d {device_id}" + CMD_BCMCLI_TIMESYNC_DLL_TEMPLATE = "bcmcli_timesync show dll -d {device_id}" + CMD_BCMCLI_TIMESYNC_DUTY_CYCLE_TEMPLATE = "bcmcli_timesync show duty-cycle -d {device_id}" + CMD_BCMCLI_TIMESYNC_TS_PIN_TEMPLATE = "bcmcli_timesync show ts-pin -d {device_id}" # MSIX / dump CMD_BCMCLI_MSIX_TEMPLATE = "bcmcli_config msixmv query --pf all -d {device_id}" CMD_BCMCLI_SNAPDUMP_TEMPLATE = "bcmcli_dump snap_dump -d {device_id}" CMD_BCMCLI_PER_DEVICE = [ # NVM config queries - CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE, - CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE, - CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE, CMD_BCMCLI_AN_PROTOCOL_TEMPLATE, CMD_BCMCLI_NVM_SHOW_CONFS_TEMPLATE, CMD_BCMCLI_NVM_SHOW_TEMPLATE, @@ -614,9 +595,9 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_BCMCLI_TUNNEL_VXLAN_IPV4_TEMPLATE, CMD_BCMCLI_TUNNEL_VXLAN_IPV6_TEMPLATE, # Timesync - CMD_BCMCLI_TIMESYNC_PTP_TEMPLATE, - CMD_BCMCLI_TIMESYNC_SYNCE_TEMPLATE, - CMD_BCMCLI_TIMESYNC_TSIO_TEMPLATE, + CMD_BCMCLI_TIMESYNC_DLL_TEMPLATE, + CMD_BCMCLI_TIMESYNC_DUTY_CYCLE_TEMPLATE, + CMD_BCMCLI_TIMESYNC_TS_PIN_TEMPLATE, # MSIX / dump CMD_BCMCLI_MSIX_TEMPLATE, CMD_BCMCLI_SNAPDUMP_TEMPLATE, @@ -639,16 +620,31 @@ def collect_data( # --- bcmcli path (next-gen Broadcom, Thor Ultra) --- if broadcom_cli == "bcmcli": + bcmcli_bin_dir = self._resolve_bcmcli_bin_dir() if use_sudo_bcmcli else "" + + def _bcmcli_cmd(cmd: str) -> str: + if not bcmcli_bin_dir: + return cmd + parts = cmd.split() + if parts and parts[0].startswith("bcmcli_") and "/" not in parts[0]: + parts[0] = f"{bcmcli_bin_dir}/{parts[0]}" + return " ".join(parts) + return cmd + device_ids: List[str] = [] - res_list = self._run_sut_cmd(NicCollector.CMD_BCMCLI_LIST, sudo=use_sudo_bcmcli) - results[NicCollector.CMD_BCMCLI_LIST] = NicCommandResult( - command=NicCollector.CMD_BCMCLI_LIST, - stdout=res_list.stdout or "", - stderr=res_list.stderr or "", - exit_code=res_list.exit_code, - ) - if res_list.exit_code == 0 and res_list.stdout: - device_ids = _parse_bcmcli_device_list(res_list.stdout) + # CMD_BCMCLI_LIST was already run (and stored) by _detect_broadcom_cli; reuse it. + existing_list = results.get(NicCollector.CMD_BCMCLI_LIST) + if existing_list is None: + res_list = self._run_sut_cmd(NicCollector.CMD_BCMCLI_LIST, sudo=use_sudo_bcmcli) + results[NicCollector.CMD_BCMCLI_LIST] = NicCommandResult( + command=NicCollector.CMD_BCMCLI_LIST, + stdout=res_list.stdout or "", + stderr=res_list.stderr or "", + exit_code=res_list.exit_code, + ) + existing_list = results[NicCollector.CMD_BCMCLI_LIST] + if existing_list.exit_code == 0 and existing_list.stdout: + device_ids = _parse_bcmcli_device_list(existing_list.stdout) # Discovery: card IDs from nicctl (Pensando may still coexist) card_ids: List[str] = [] @@ -711,7 +707,8 @@ def collect_data( is_bcmcli = cmd.strip().startswith("bcmcli_") is_niccli_cmd = cmd.strip().startswith("niccli") sudo = use_sudo_bcmcli if is_bcmcli else (use_sudo_niccli if is_niccli_cmd else use_sudo_nicctl) - res = self._run_sut_cmd(cmd, sudo=sudo) + run_cmd = _bcmcli_cmd(cmd) if is_bcmcli else cmd + res = self._run_sut_cmd(run_cmd, sudo=sudo) has_error_output = has_command_error_output(res.stderr or "", res.stdout or "") if _is_artifact_only_command(cmd): if res.exit_code != 0: @@ -756,18 +753,6 @@ def collect_data( broadcom_pcie_relaxed_ordering: Dict[int, str] = {} broadcom_qos_data: Dict[int, NicCliQos] = {} for idx, dev_id in enumerate(device_ids): - sr_cmd = NicCollector.CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE.format(device_id=dev_id) - r_sr = results.get(sr_cmd) - if r_sr and r_sr.exit_code == 0 and (r_sr.stdout or "").strip(): - broadcom_support_rdma[idx] = r_sr.stdout.strip() - pp_cmd = NicCollector.CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE.format(device_id=dev_id) - r_pp = results.get(pp_cmd) - if r_pp and r_pp.exit_code == 0 and (r_pp.stdout or "").strip(): - broadcom_performance_profile[idx] = r_pp.stdout.strip() - ro_cmd = NicCollector.CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE.format(device_id=dev_id) - r_ro = results.get(ro_cmd) - if r_ro and r_ro.exit_code == 0 and (r_ro.stdout or "").strip(): - broadcom_pcie_relaxed_ordering[idx] = r_ro.stdout.strip() qos_cmd = NicCollector.CMD_BCMCLI_QOS_TEMPLATE.format(device_id=dev_id) r_qos = results.get(qos_cmd) if r_qos and r_qos.exit_code == 0 and (r_qos.stdout or "").strip(): @@ -988,23 +973,6 @@ def collect_data( pensando_version_firmware, ) = self._collect_pensando_nic_structured(results) - # Serialized datamodel: no stdout in results, truncated command/stderr (keeps file small). - # Command output lives on disk from _run_sut_cmd; model keeps only command identity and status. - def _truncate(s: str, max_len: int) -> str: - if not s or len(s) <= max_len: - return s or "" - return s[: max_len - 3] + "..." - - results_for_model = { - cmd: NicCommandResult( - command=_truncate(r.command, MAX_COMMAND_LENGTH_IN_DATAMODEL), - stdout="", - stderr=_truncate(r.stderr or "", MAX_STDERR_LENGTH_IN_DATAMODEL), - exit_code=r.exit_code, - ) - for cmd, r in results.items() - } - cli_label = "bcmcli" if broadcom_cli == "bcmcli" else "niccli/nicctl" if not results or all(r.exit_code != 0 for r in results.values()): self.result.status = ExecutionStatus.EXECUTION_FAILURE @@ -1022,7 +990,7 @@ def _truncate(s: str, max_len: int) -> str: } return self.result, NicDataModel( - results=results_for_model, + results=results, card_show=None, cards=[], nicctl_card_logs=nicctl_card_logs, @@ -1051,15 +1019,22 @@ def _truncate(s: str, max_len: int) -> str: pensando_nic_version_firmware=pensando_version_firmware, ) + def _resolve_bcmcli_bin_dir(self) -> str: + res = self._run_sut_cmd("which bcmcli_show", sudo=False, log_artifact=False) + if res.exit_code == 0 and (res.stdout or "").strip(): + import os + return os.path.dirname(res.stdout.strip()) + return "" + def _detect_broadcom_cli( self, args: Optional[NicCollectorArgs], results: Dict[str, "NicCommandResult"], ) -> str: - """Return 'bcmcli' if bcmcli_show version exits 0, otherwise 'niccli'. + """Return 'bcmcli' if bcmcli_show version exits 0 and device_list finds devices, otherwise 'niccli'. - Respects args.broadcom_cli_override ('niccli' or 'bcmcli') when set. - Stores the bcmcli version probe result in results so it appears in the datamodel. + Respects args.broadcom_cli_override ('niccli' or 'bcmcli') to skip detection entirely. + Stores both probe results in results so they appear in the datamodel. """ override = ( args.broadcom_cli_override.strip().lower() @@ -1077,7 +1052,17 @@ def _detect_broadcom_cli( stderr=res.stderr or "", exit_code=res.exit_code, ) - if res.exit_code == 0: + if res.exit_code != 0: + return "niccli" + + res_list = self._run_sut_cmd(NicCollector.CMD_BCMCLI_LIST, sudo=use_sudo) + results[NicCollector.CMD_BCMCLI_LIST] = NicCommandResult( + command=NicCollector.CMD_BCMCLI_LIST, + stdout=res_list.stdout or "", + stderr=res_list.stderr or "", + exit_code=res_list.exit_code, + ) + if res_list.exit_code == 0 and _parse_bcmcli_device_list(res_list.stdout or ""): return "bcmcli" return "niccli" From b2229ec220e42c1d64fbadf3a54ba10d36b3aee1 Mon Sep 17 00:00:00 2001 From: jolibran Date: Thu, 6 Aug 2026 16:12:12 -0400 Subject: [PATCH 3/8] DCERR-699: restore _truncate and results_for_model in datamodel serialization --- .../plugins/inband/nic/nic_collector.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index 119c36d1..417cd796 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -71,6 +71,8 @@ NICCLI_VERSION_LEGACY_MAX = 233 # Commands use -dev/-getoption/getqos; for version > this use --dev/--getoption/qos --ets --show # Max lengths for fields included in the serialized datamodel (keeps nicclidatamodel.json small). +MAX_COMMAND_LENGTH_IN_DATAMODEL = 256 +MAX_STDERR_LENGTH_IN_DATAMODEL = 512 def _parse_niccli_version(stdout: str) -> Optional[int]: @@ -973,6 +975,23 @@ def _bcmcli_cmd(cmd: str) -> str: pensando_version_firmware, ) = self._collect_pensando_nic_structured(results) + # Serialized datamodel: no stdout in results, truncated command/stderr (keeps file small). + # Command output lives on disk from _run_sut_cmd; model keeps only command identity and status. + def _truncate(s: str, max_len: int) -> str: + if not s or len(s) <= max_len: + return s or "" + return s[: max_len - 3] + "..." + + results_for_model = { + cmd: NicCommandResult( + command=_truncate(r.command, MAX_COMMAND_LENGTH_IN_DATAMODEL), + stdout="", + stderr=_truncate(r.stderr or "", MAX_STDERR_LENGTH_IN_DATAMODEL), + exit_code=r.exit_code, + ) + for cmd, r in results.items() + } + cli_label = "bcmcli" if broadcom_cli == "bcmcli" else "niccli/nicctl" if not results or all(r.exit_code != 0 for r in results.values()): self.result.status = ExecutionStatus.EXECUTION_FAILURE @@ -990,7 +1009,7 @@ def _bcmcli_cmd(cmd: str) -> str: } return self.result, NicDataModel( - results=results, + results=results_for_model, card_show=None, cards=[], nicctl_card_logs=nicctl_card_logs, From 6095b04fc0165f1910644c2b43f823d24f317867 Mon Sep 17 00:00:00 2001 From: jolibran Date: Thu, 6 Aug 2026 16:22:07 -0400 Subject: [PATCH 4/8] DCERR-699: remove unnecessary comments --- nodescraper/plugins/inband/nic/nic_collector.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index 417cd796..89b49e7e 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -425,7 +425,7 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_SHOW_PKG_VER_TEMPLATE_NEW, # Link CMD_NICCLI_LINK_STATUS_TEMPLATE_NEW, - # Linkdiag (read-only) — dscdump omitted (~5s per device) + # Linkdiag (read-only) CMD_NICCLI_LINKDIAG_LOOPBACK_SHOW_TEMPLATE_NEW, CMD_NICCLI_LINKDIAG_TXFIR_SHOW_TEMPLATE_NEW, # QoS additional show @@ -433,7 +433,6 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_QOS_INGRESS_COSQ_TEMPLATE_NEW, CMD_NICCLI_QOS_RX_RATE_LIMIT_TEMPLATE_NEW, CMD_NICCLI_QOS_LISTMAP_TEMPLATE_NEW, - # NVM read — list/listoptions/view/verify omitted (20-120s per device) # MSIX read CMD_NICCLI_MSIX_SHOW_TEMPLATE_NEW, # Timesync read From 5d66c26edf839fc818cc351dda788df8944c16e3 Mon Sep 17 00:00:00 2001 From: jolibran Date: Thu, 6 Aug 2026 16:31:55 -0400 Subject: [PATCH 5/8] DCERR-699: comment formatting --- .../plugins/inband/nic/nic_collector.py | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index 89b49e7e..b56f7627 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -496,7 +496,7 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): ] # bcmcli (next-gen Broadcom, Thor Ultra): separate binaries, device targeted with -d suffix. - # Read-only show/query commands only — no fw/update/reset/set/loopback/coredump operations. + # Read-only show/query commands only; no fw/update/reset/set/loopback/coredump operations. CMD_BCMCLI_VERSION = "bcmcli_show version" CMD_BCMCLI_LIST = "bcmcli_show device_list" @@ -505,7 +505,7 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): "bcmcli_show device_list", ] - # Per-device templates — {device_id} expanded at runtime from bcmcli_show device_list output + # Per-device templates; {device_id} expanded at runtime from bcmcli_show device_list output # NVM config queries CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE = "bcmcli_config query support_rdma -d {device_id}" CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE = "bcmcli_config query performance_profile -d {device_id}" @@ -1696,30 +1696,13 @@ def _get_niccli_discovery_commands(version: Optional[int]) -> List[str]: def _parse_bcmcli_qos(device_id: str, stdout: str) -> NicCliQos: """Parse bcmcli_qos show qos output into NicCliQos. - Real Thor Ultra format: - ETS Configuration: - TC TX_BW RX_BW TSA RateLimit Priority - ----------------------------------------------------------------------- - 0 50 0 ETS 100 0 1 2 4 5 6 - 1 50 0 ETS 100 3 - 2 0 0 Strict 100 7 - - PFC configuration: - priority 0 1 2 3 4 5 6 7 - enabled 0 0 0 1 0 0 0 0 - - APP TLV Configuration: - Index Selector Priority DSCP/Protocol - ----------------------------------------------------------------------- - 0 5 7 48 - Maps into NicCliQos fields: - prio_map — priority→TC from the Priority column of the ETS table - tc_bandwidth — TX_BW per TC - tsa_map — TSA string per TC - tc_rate_limit — RateLimit per TC - pfc_enabled — integer bitmask from the 'enabled' row (bit 0 = priority 0) - app_entries — APP TLV rows as NicCliQosAppEntry + prio_map - priority to TC from the Priority column of the ETS table + tc_bandwidth - TX_BW per TC + tsa_map - TSA string per TC + tc_rate_limit - RateLimit per TC + pfc_enabled - integer bitmask from the 'enabled' row (bit 0 = priority 0) + app_entries - APP TLV rows as NicCliQosAppEntry """ prio_map: Dict[int, int] = {} tc_bandwidth: List[int] = [] @@ -1817,15 +1800,7 @@ def _parse_bcmcli_qos(device_id: str, stdout: str) -> NicCliQos: def _parse_bcmcli_device_list(stdout: str) -> List[str]: - """Parse bcmcli_show device_list output into a list of PCI address device identifiers. - - Real Thor Ultra output is a columnar table where the PCI address is the second column: - Device Type PCI Address RDMA NET NUMA - ThorUltra(A0) 0000:81:00.0 bng_re0 enp129s0np0 0 - - Scans each non-header data line for a PCI address pattern anywhere in the line. - Also handles the unlikely integer-index format ('0) BCM...') for robustness. - """ + """Parse bcmcli_show device_list output into a list of PCI address device identifiers.""" device_ids: List[str] = [] pci_re = re.compile(r"\b([0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])\b") for line in stdout.splitlines(): From be5fb812ac697f55bf656751388c5483fe9a36e5 Mon Sep 17 00:00:00 2001 From: jolibran Date: Thu, 6 Aug 2026 16:37:38 -0400 Subject: [PATCH 6/8] DCERR-699: comment formatting --- nodescraper/plugins/inband/nic/nic_collector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index b56f7627..b9dc34b8 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -495,7 +495,7 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): "nicctl show version host-software", ] - # bcmcli (next-gen Broadcom, Thor Ultra): separate binaries, device targeted with -d suffix. + # bcmcli (Thor Ultra, Thor3): separate binaries, device targeted with -d suffix. # Read-only show/query commands only; no fw/update/reset/set/loopback/coredump operations. CMD_BCMCLI_VERSION = "bcmcli_show version" CMD_BCMCLI_LIST = "bcmcli_show device_list" @@ -619,7 +619,7 @@ def collect_data( # Detect which Broadcom CLI is present (bcmcli takes priority over niccli). broadcom_cli = self._detect_broadcom_cli(args, results) - # --- bcmcli path (next-gen Broadcom, Thor Ultra) --- + # --- bcmcli path (Thor Ultra, Thor3) --- if broadcom_cli == "bcmcli": bcmcli_bin_dir = self._resolve_bcmcli_bin_dir() if use_sudo_bcmcli else "" From 40d9f3479e368b4efbfe9dfe07b2cecf40526a2c Mon Sep 17 00:00:00 2001 From: jolibran Date: Fri, 7 Aug 2026 14:07:42 -0400 Subject: [PATCH 7/8] add unit tests for bcmcli/niccli NicCollector changes --- test/unit/plugin/test_niccli_collector.py | 143 +++++++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/test/unit/plugin/test_niccli_collector.py b/test/unit/plugin/test_niccli_collector.py index 709ed8a3..c31133a6 100644 --- a/test/unit/plugin/test_niccli_collector.py +++ b/test/unit/plugin/test_niccli_collector.py @@ -12,7 +12,12 @@ from nodescraper.enums.executionstatus import ExecutionStatus from nodescraper.enums.systeminteraction import SystemInteractionLevel from nodescraper.models.systeminfo import OSFamily -from nodescraper.plugins.inband.nic.nic_collector import NicCollector +from nodescraper.plugins.inband.nic.nic_collector import ( + NicCollector, + _parse_bcmcli_device_list, + _parse_bcmcli_qos, + _parse_niccli_version, +) from nodescraper.plugins.inband.nic.nic_data import ( NicCliDevice, NicCliQos, @@ -254,7 +259,7 @@ def test_collect_data_not_ran_when_no_nic_hardware(collector, conn_mock): assert result.status == ExecutionStatus.NOT_RAN assert data is None assert "skipped" in result.message.lower() - assert collector._run_sut_cmd.call_count <= 4 + assert collector._run_sut_cmd.call_count <= 6 def test_collect_data_skips_nicctl_commands_when_no_pensando_cards(collector, conn_mock): @@ -306,3 +311,137 @@ def run_sut_cmd_side_effect(cmd, **kwargs): assert data is not None assert isinstance(data, NicDataModel) assert len(data.results) >= 1 + + +BCMCLI_DEVICE_LIST_OUTPUT = """\ +PCI Address Device Type Firmware Version +-------- ----------- ---------------- +0000:03:00.0 BCM957608 1.140.0 +0000:04:00.0 BCM957608 1.140.0 +""" + +BCMCLI_QOS_OUTPUT = """\ +ETS Configuration +----------------- +TC TX_BW RX_BW TSA RateLimit Priority +0 50 50 ETS 0 0 1 2 +1 50 50 ETS 0 3 4 +2 0 0 strict 0 5 6 7 +PFC configuration +----------------- +priority 0 1 2 3 4 5 6 7 +enabled 0 0 0 1 0 0 0 0 +APP TLV Configuration +--------------------- +Index Selector Priority DSCP/Protocol +0 5 7 48 +""" + + +def test_parse_niccli_version(): + assert _parse_niccli_version("niccli v234") == 234 + assert _parse_niccli_version("") is None + + +def test_parse_bcmcli_device_list(): + """PCI addresses parsed from table; empty input returns empty list.""" + ids = _parse_bcmcli_device_list(BCMCLI_DEVICE_LIST_OUTPUT) + assert ids == ["0000:03:00.0", "0000:04:00.0"] + assert _parse_bcmcli_device_list("") == [] + + +def test_parse_bcmcli_qos(): + """ETS rows, PFC bitmask, and APP entries all parsed correctly.""" + qos = _parse_bcmcli_qos("0000:03:00.0", BCMCLI_QOS_OUTPUT) + assert qos.prio_map[0] == 0 and qos.prio_map[3] == 1 and qos.prio_map[5] == 2 + assert qos.tc_bandwidth == [50, 50, 0] + assert qos.tsa_map[0] == "ETS" and qos.tsa_map[2] == "strict" + assert qos.pfc_enabled == 8 # only priority 3 set (1 << 3) + assert len(qos.app_entries) == 1 and qos.app_entries[0].priority == 7 + + +def test_collect_data_uses_bcmcli_when_detected(collector): + """Both bcmcli probes succeed; bcmcli path used, broadcom_cli_type='bcmcli'.""" + commands_run: list[str] = [] + + def side_effect(cmd, **kwargs): + commands_run.append(cmd) + if "bcmcli_show version" in cmd or "which bcmcli_show" in cmd: + return MagicMock(exit_code=0, stdout="bcmcli v1.140", stderr="", command=cmd) + if "bcmcli_show device_list" in cmd: + return MagicMock(exit_code=0, stdout=BCMCLI_DEVICE_LIST_OUTPUT, stderr="", command=cmd) + if "nicctl show card" in cmd: + return MagicMock(exit_code=1, stdout="", stderr="", command=cmd) + return MagicMock(exit_code=0, stdout="", stderr="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=side_effect) + result, data = collector.collect_data() + + assert result.status == ExecutionStatus.OK + assert data.broadcom_cli_type == "bcmcli" + assert any("bcmcli_" in c for c in commands_run) + + +def test_detect_broadcom_cli_falls_back_when_device_list_empty(collector): + """bcmcli_show version exits 0 but device_list empty; falls back to niccli.""" + def side_effect(cmd, **kwargs): + if "bcmcli_show version" in cmd: + return MagicMock(exit_code=0, stdout="bcmcli v1.140", stderr="", command=cmd) + if "bcmcli_show device_list" in cmd: + return MagicMock(exit_code=0, stdout="PCI Address Device Type\n", stderr="", command=cmd) + if "niccli --version" in cmd: + return MagicMock(exit_code=0, stdout="niccli v234", stderr="", command=cmd) + if "--list_devices" in cmd or "--listdev" in cmd or "--list" in cmd: + return MagicMock(exit_code=0, stdout=NICCLI_LISTDEV_OUTPUT, stderr="", command=cmd) + if "nicctl show card" in cmd: + return MagicMock(exit_code=1, stdout="", stderr="", command=cmd) + return MagicMock(exit_code=0, stdout="", stderr="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=side_effect) + _, data = collector.collect_data() + + assert data.broadcom_cli_type == "niccli" + + +def test_broadcom_cli_override_skips_detection(collector): + """broadcom_cli_override='niccli' bypasses bcmcli probes entirely.""" + from nodescraper.plugins.inband.nic.collector_args import NicCollectorArgs + + commands_run: list[str] = [] + + def side_effect(cmd, **kwargs): + commands_run.append(cmd) + if "niccli --version" in cmd: + return MagicMock(exit_code=0, stdout="niccli v234", stderr="", command=cmd) + if "--list_devices" in cmd or "--listdev" in cmd or "--list" in cmd: + return MagicMock(exit_code=0, stdout=NICCLI_LISTDEV_OUTPUT, stderr="", command=cmd) + if "nicctl show card" in cmd: + return MagicMock(exit_code=1, stdout="", stderr="", command=cmd) + return MagicMock(exit_code=0, stdout="", stderr="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=side_effect) + _, data = collector.collect_data(args=NicCollectorArgs(broadcom_cli_override="niccli")) + + assert data.broadcom_cli_type == "niccli" + assert not any("bcmcli_show version" in c for c in commands_run) + + +def test_collect_data_niccli_version_routing(collector): + """niccli v234 uses --dev/--getoption syntax; legacy -dev/-getoption not run.""" + commands_run: list[str] = [] + + def side_effect(cmd, **kwargs): + commands_run.append(cmd) + if cmd == "niccli --version": + return MagicMock(exit_code=0, stdout="niccli v234", stderr="", command=cmd) + if "--list_devices" in cmd or "--listdev" in cmd or "--list" in cmd: + return MagicMock(exit_code=0, stdout=NICCLI_LISTDEV_OUTPUT, stderr="", command=cmd) + if "nicctl show card" in cmd: + return MagicMock(exit_code=1, stdout="", stderr="", command=cmd) + return MagicMock(exit_code=0, stdout="", stderr="", command=cmd) + + collector._run_sut_cmd = MagicMock(side_effect=side_effect) + collector.collect_data() + + assert any("--dev" in c and "--getoption" in c for c in commands_run) + assert not any(" -dev " in c for c in commands_run) From 386442ae699fc46d2a3293a0d99303b675a709bc Mon Sep 17 00:00:00 2001 From: jolibran Date: Fri, 7 Aug 2026 15:28:05 -0400 Subject: [PATCH 8/8] add unit tests for bcmcli/niccli NicCollector changes --- .../plugins/inband/nic/nic_collector.py | 61 ++++++++++++++----- test/unit/plugin/test_niccli_collector.py | 5 +- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/nodescraper/plugins/inband/nic/nic_collector.py b/nodescraper/plugins/inband/nic/nic_collector.py index b9dc34b8..8d68f731 100644 --- a/nodescraper/plugins/inband/nic/nic_collector.py +++ b/nodescraper/plugins/inband/nic/nic_collector.py @@ -381,14 +381,26 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_LINK_STATUS_TEMPLATE_NEW = "niccli --dev {device_num} link --status" CMD_NICCLI_LINK_COUNTERS_TEMPLATE_NEW = "niccli --dev {device_num} link --counters --show" # Linkdiag (read-only) - CMD_NICCLI_LINKDIAG_LOOPBACK_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} linkdiag --loopback --show" - CMD_NICCLI_LINKDIAG_DSCDUMP_TEMPLATE_NEW = "niccli --dev {device_num} linkdiag --dscdump --lane 0" - CMD_NICCLI_LINKDIAG_TXFIR_SHOW_TEMPLATE_NEW = "niccli --dev {device_num} linkdiag --txfir --show --modulation_type NRZ --lane 0" + CMD_NICCLI_LINKDIAG_LOOPBACK_SHOW_TEMPLATE_NEW = ( + "niccli --dev {device_num} linkdiag --loopback --show" + ) + CMD_NICCLI_LINKDIAG_DSCDUMP_TEMPLATE_NEW = ( + "niccli --dev {device_num} linkdiag --dscdump --lane 0" + ) + CMD_NICCLI_LINKDIAG_TXFIR_SHOW_TEMPLATE_NEW = ( + "niccli --dev {device_num} linkdiag --txfir --show --modulation_type NRZ --lane 0" + ) # QoS additional show commands CMD_NICCLI_QOS_EGRESS_COSQ_TEMPLATE_NEW = "niccli --dev {device_num} qos --egress --cosq --show" - CMD_NICCLI_QOS_INGRESS_COSQ_TEMPLATE_NEW = "niccli --dev {device_num} qos --ingress --cosq --show" - CMD_NICCLI_QOS_RX_RATE_LIMIT_TEMPLATE_NEW = "niccli --dev {device_num} qos --rx_rate_limit --show" - CMD_NICCLI_QOS_TX_EP_RATE_LIMIT_TEMPLATE_NEW = "niccli --dev {device_num} qos --tx_ep_rate_limit --port 0 --show" + CMD_NICCLI_QOS_INGRESS_COSQ_TEMPLATE_NEW = ( + "niccli --dev {device_num} qos --ingress --cosq --show" + ) + CMD_NICCLI_QOS_RX_RATE_LIMIT_TEMPLATE_NEW = ( + "niccli --dev {device_num} qos --rx_rate_limit --show" + ) + CMD_NICCLI_QOS_TX_EP_RATE_LIMIT_TEMPLATE_NEW = ( + "niccli --dev {device_num} qos --tx_ep_rate_limit --port 0 --show" + ) CMD_NICCLI_QOS_DSCP2PRIO_TEMPLATE_NEW = "niccli --dev {device_num} qos --dscp2prio" CMD_NICCLI_QOS_LISTMAP_TEMPLATE_NEW = "niccli --dev {device_num} qos --listmap --pri2cos" # NVM read commands @@ -406,13 +418,19 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_NICCLI_TIMESYNC_TSIO_TEMPLATE_NEW = "niccli --dev {device_num} timesync --tsio --show" # Tunnel read CMD_NICCLI_TUNNEL_RSS_TEMPLATE_NEW = "niccli --dev {device_num} tunnel --cfg --rss --show" - CMD_NICCLI_TUNNEL_VXLAN_IPV4_TEMPLATE_NEW = "niccli --dev {device_num} tunnel --cfg --vxlan --type ipv4 --show" + CMD_NICCLI_TUNNEL_VXLAN_IPV4_TEMPLATE_NEW = ( + "niccli --dev {device_num} tunnel --cfg --vxlan --type ipv4 --show" + ) # PCIe counters CMD_NICCLI_COUNTERS_PCIE_TEMPLATE_NEW = "niccli --dev {device_num} counters --pcie" # Resource management - CMD_NICCLI_RESMGMT_PROFILE_TEMPLATE_NEW = "niccli --dev {device_num} resmgmt --all --profile --show" + CMD_NICCLI_RESMGMT_PROFILE_TEMPLATE_NEW = ( + "niccli --dev {device_num} resmgmt --all --profile --show" + ) # Cable / transceiver info - CMD_NICCLI_CABLE_MODULE_INFO_TEMPLATE_NEW = "niccli --dev {device_num} cable --module_info --show" + CMD_NICCLI_CABLE_MODULE_INFO_TEMPLATE_NEW = ( + "niccli --dev {device_num} cable --module_info --show" + ) CMD_NICCLI_PER_DEVICE_NEW = [ CMD_NICCLI_SUPPORT_RDMA_TEMPLATE_NEW, CMD_NICCLI_PERFORMANCE_PROFILE_TEMPLATE_NEW, @@ -508,8 +526,12 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): # Per-device templates; {device_id} expanded at runtime from bcmcli_show device_list output # NVM config queries CMD_BCMCLI_SUPPORT_RDMA_TEMPLATE = "bcmcli_config query support_rdma -d {device_id}" - CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE = "bcmcli_config query performance_profile -d {device_id}" - CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE = "bcmcli_config query pcie_relaxed_ordering -d {device_id}" + CMD_BCMCLI_PERFORMANCE_PROFILE_TEMPLATE = ( + "bcmcli_config query performance_profile -d {device_id}" + ) + CMD_BCMCLI_PCIE_RELAXED_ORDERING_TEMPLATE = ( + "bcmcli_config query pcie_relaxed_ordering -d {device_id}" + ) CMD_BCMCLI_AN_PROTOCOL_TEMPLATE = "bcmcli_config query AN_PROTOCOL -d {device_id}" CMD_BCMCLI_NVM_SHOW_CONFS_TEMPLATE = "bcmcli_config show_confs -d {device_id}" CMD_BCMCLI_NVM_SHOW_TEMPLATE = "bcmcli_nvm show -d {device_id}" @@ -517,7 +539,9 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_BCMCLI_QOS_TEMPLATE = "bcmcli_qos show qos -d {device_id}" CMD_BCMCLI_QOS_HW_MAPS_TEMPLATE = "bcmcli_qos show hw-maps -d {device_id}" CMD_BCMCLI_QOS_RX_PORT_RATELIMIT_TEMPLATE = "bcmcli_qos show rx-port-ratelimit -d {device_id}" - CMD_BCMCLI_QOS_TX_EP_RATELIMIT_TEMPLATE = "bcmcli_qos show tx-ep-ratelimit --port 0 -d {device_id}" + CMD_BCMCLI_QOS_TX_EP_RATELIMIT_TEMPLATE = ( + "bcmcli_qos show tx-ep-ratelimit --port 0 -d {device_id}" + ) CMD_BCMCLI_QOS_INGRESS_COSQ_TEMPLATE = "bcmcli_qos show ingress --cosq -d {device_id}" CMD_BCMCLI_QOS_EGRESS_COSQ_TEMPLATE = "bcmcli_qos show egress --cosq -d {device_id}" # Device info / health @@ -536,7 +560,9 @@ class NicCollector(InBandDataCollector[NicDataModel, NicCollectorArgs]): CMD_BCMCLI_VERIFY_TEMPLATE = "bcmcli_debug verify -d {device_id}" CMD_BCMCLI_LOOPBACK_SHOW_TEMPLATE = "bcmcli_debug loopback -d {device_id}" CMD_BCMCLI_DSCDUMP_TEMPLATE = "bcmcli_debug dscdump --lane 0 -d {device_id}" - CMD_BCMCLI_SERDES_TX_GET_TEMPLATE = "bcmcli_debug serdes_tx --get --modtype NRZ --lane 0 -d {device_id}" + CMD_BCMCLI_SERDES_TX_GET_TEMPLATE = ( + "bcmcli_debug serdes_tx --get --modtype NRZ --lane 0 -d {device_id}" + ) # Firmware CMD_BCMCLI_FW_VERSION_TEMPLATE = "bcmcli_fwmanager show fwpackage -d {device_id}" CMD_BCMCLI_FW_CERTIFICATE_TEMPLATE = "bcmcli_fwmanager show certificate -d {device_id}" @@ -707,7 +733,11 @@ def _bcmcli_cmd(cmd: str) -> str: continue is_bcmcli = cmd.strip().startswith("bcmcli_") is_niccli_cmd = cmd.strip().startswith("niccli") - sudo = use_sudo_bcmcli if is_bcmcli else (use_sudo_niccli if is_niccli_cmd else use_sudo_nicctl) + sudo = ( + use_sudo_bcmcli + if is_bcmcli + else (use_sudo_niccli if is_niccli_cmd else use_sudo_nicctl) + ) run_cmd = _bcmcli_cmd(cmd) if is_bcmcli else cmd res = self._run_sut_cmd(run_cmd, sudo=sudo) has_error_output = has_command_error_output(res.stderr or "", res.stdout or "") @@ -1041,6 +1071,7 @@ def _resolve_bcmcli_bin_dir(self) -> str: res = self._run_sut_cmd("which bcmcli_show", sudo=False, log_artifact=False) if res.exit_code == 0 and (res.stdout or "").strip(): import os + return os.path.dirname(res.stdout.strip()) return "" @@ -1768,7 +1799,7 @@ def _parse_bcmcli_qos(device_id: str, stdout: str) -> NicCliQos: mask = 0 for i, v in enumerate(vals): if v and i < len(pfc_priority_order): - mask |= (1 << pfc_priority_order[i]) + mask |= 1 << pfc_priority_order[i] pfc_enabled = mask elif section == "app": diff --git a/test/unit/plugin/test_niccli_collector.py b/test/unit/plugin/test_niccli_collector.py index c31133a6..46a2fcff 100644 --- a/test/unit/plugin/test_niccli_collector.py +++ b/test/unit/plugin/test_niccli_collector.py @@ -384,11 +384,14 @@ def side_effect(cmd, **kwargs): def test_detect_broadcom_cli_falls_back_when_device_list_empty(collector): """bcmcli_show version exits 0 but device_list empty; falls back to niccli.""" + def side_effect(cmd, **kwargs): if "bcmcli_show version" in cmd: return MagicMock(exit_code=0, stdout="bcmcli v1.140", stderr="", command=cmd) if "bcmcli_show device_list" in cmd: - return MagicMock(exit_code=0, stdout="PCI Address Device Type\n", stderr="", command=cmd) + return MagicMock( + exit_code=0, stdout="PCI Address Device Type\n", stderr="", command=cmd + ) if "niccli --version" in cmd: return MagicMock(exit_code=0, stdout="niccli v234", stderr="", command=cmd) if "--list_devices" in cmd or "--listdev" in cmd or "--list" in cmd: