amd-node-scraper 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amd_node_scraper-0.0.1.dist-info/LICENSE +21 -0
- amd_node_scraper-0.0.1.dist-info/METADATA +424 -0
- amd_node_scraper-0.0.1.dist-info/RECORD +197 -0
- amd_node_scraper-0.0.1.dist-info/WHEEL +5 -0
- amd_node_scraper-0.0.1.dist-info/entry_points.txt +2 -0
- amd_node_scraper-0.0.1.dist-info/top_level.txt +1 -0
- nodescraper/__init__.py +32 -0
- nodescraper/base/__init__.py +34 -0
- nodescraper/base/inbandcollectortask.py +118 -0
- nodescraper/base/inbanddataplugin.py +39 -0
- nodescraper/base/regexanalyzer.py +120 -0
- nodescraper/cli/__init__.py +29 -0
- nodescraper/cli/cli.py +511 -0
- nodescraper/cli/constants.py +27 -0
- nodescraper/cli/dynamicparserbuilder.py +171 -0
- nodescraper/cli/helper.py +517 -0
- nodescraper/cli/inputargtypes.py +129 -0
- nodescraper/configbuilder.py +123 -0
- nodescraper/configregistry.py +66 -0
- nodescraper/configs/node_status.json +19 -0
- nodescraper/connection/__init__.py +25 -0
- nodescraper/connection/inband/__init__.py +46 -0
- nodescraper/connection/inband/inband.py +171 -0
- nodescraper/connection/inband/inbandlocal.py +93 -0
- nodescraper/connection/inband/inbandmanager.py +151 -0
- nodescraper/connection/inband/inbandremote.py +173 -0
- nodescraper/connection/inband/sshparams.py +43 -0
- nodescraper/constants.py +26 -0
- nodescraper/enums/__init__.py +40 -0
- nodescraper/enums/eventcategory.py +89 -0
- nodescraper/enums/eventpriority.py +42 -0
- nodescraper/enums/executionstatus.py +44 -0
- nodescraper/enums/osfamily.py +34 -0
- nodescraper/enums/systeminteraction.py +41 -0
- nodescraper/enums/systemlocation.py +33 -0
- nodescraper/generictypes.py +36 -0
- nodescraper/interfaces/__init__.py +44 -0
- nodescraper/interfaces/connectionmanager.py +143 -0
- nodescraper/interfaces/dataanalyzertask.py +138 -0
- nodescraper/interfaces/datacollectortask.py +185 -0
- nodescraper/interfaces/dataplugin.py +356 -0
- nodescraper/interfaces/plugin.py +127 -0
- nodescraper/interfaces/resultcollator.py +56 -0
- nodescraper/interfaces/task.py +164 -0
- nodescraper/interfaces/taskresulthook.py +39 -0
- nodescraper/models/__init__.py +48 -0
- nodescraper/models/analyzerargs.py +93 -0
- nodescraper/models/collectorargs.py +30 -0
- nodescraper/models/connectionconfig.py +34 -0
- nodescraper/models/datamodel.py +171 -0
- nodescraper/models/datapluginresult.py +39 -0
- nodescraper/models/event.py +158 -0
- nodescraper/models/pluginconfig.py +38 -0
- nodescraper/models/pluginresult.py +39 -0
- nodescraper/models/systeminfo.py +44 -0
- nodescraper/models/taskresult.py +185 -0
- nodescraper/models/timerangeargs.py +38 -0
- nodescraper/pluginexecutor.py +274 -0
- nodescraper/pluginregistry.py +152 -0
- nodescraper/plugins/__init__.py +25 -0
- nodescraper/plugins/inband/__init__.py +25 -0
- nodescraper/plugins/inband/amdsmi/__init__.py +28 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_analyzer.py +821 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_collector.py +1313 -0
- nodescraper/plugins/inband/amdsmi/amdsmi_plugin.py +43 -0
- nodescraper/plugins/inband/amdsmi/amdsmidata.py +1002 -0
- nodescraper/plugins/inband/amdsmi/analyzer_args.py +50 -0
- nodescraper/plugins/inband/amdsmi/cper.py +65 -0
- nodescraper/plugins/inband/bios/__init__.py +29 -0
- nodescraper/plugins/inband/bios/analyzer_args.py +64 -0
- nodescraper/plugins/inband/bios/bios_analyzer.py +93 -0
- nodescraper/plugins/inband/bios/bios_collector.py +93 -0
- nodescraper/plugins/inband/bios/bios_plugin.py +43 -0
- nodescraper/plugins/inband/bios/biosdata.py +30 -0
- nodescraper/plugins/inband/cmdline/__init__.py +25 -0
- nodescraper/plugins/inband/cmdline/analyzer_args.py +80 -0
- nodescraper/plugins/inband/cmdline/cmdline_analyzer.py +113 -0
- nodescraper/plugins/inband/cmdline/cmdline_collector.py +77 -0
- nodescraper/plugins/inband/cmdline/cmdline_plugin.py +43 -0
- nodescraper/plugins/inband/cmdline/cmdlinedata.py +30 -0
- nodescraper/plugins/inband/device_enumeration/__init__.py +29 -0
- nodescraper/plugins/inband/device_enumeration/analyzer_args.py +73 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py +81 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py +176 -0
- nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py +45 -0
- nodescraper/plugins/inband/device_enumeration/deviceenumdata.py +36 -0
- nodescraper/plugins/inband/dimm/__init__.py +25 -0
- nodescraper/plugins/inband/dimm/collector_args.py +31 -0
- nodescraper/plugins/inband/dimm/dimm_collector.py +151 -0
- nodescraper/plugins/inband/dimm/dimm_plugin.py +40 -0
- nodescraper/plugins/inband/dimm/dimmdata.py +30 -0
- nodescraper/plugins/inband/dkms/__init__.py +25 -0
- nodescraper/plugins/inband/dkms/analyzer_args.py +85 -0
- nodescraper/plugins/inband/dkms/dkms_analyzer.py +106 -0
- nodescraper/plugins/inband/dkms/dkms_collector.py +76 -0
- nodescraper/plugins/inband/dkms/dkms_plugin.py +43 -0
- nodescraper/plugins/inband/dkms/dkmsdata.py +33 -0
- nodescraper/plugins/inband/dmesg/__init__.py +28 -0
- nodescraper/plugins/inband/dmesg/analyzer_args.py +33 -0
- nodescraper/plugins/inband/dmesg/collector_args.py +39 -0
- nodescraper/plugins/inband/dmesg/dmesg_analyzer.py +503 -0
- nodescraper/plugins/inband/dmesg/dmesg_collector.py +164 -0
- nodescraper/plugins/inband/dmesg/dmesg_plugin.py +44 -0
- nodescraper/plugins/inband/dmesg/dmesgdata.py +116 -0
- nodescraper/plugins/inband/fabrics/__init__.py +28 -0
- nodescraper/plugins/inband/fabrics/fabrics_collector.py +726 -0
- nodescraper/plugins/inband/fabrics/fabrics_plugin.py +37 -0
- nodescraper/plugins/inband/fabrics/fabricsdata.py +140 -0
- nodescraper/plugins/inband/journal/__init__.py +28 -0
- nodescraper/plugins/inband/journal/collector_args.py +33 -0
- nodescraper/plugins/inband/journal/journal_collector.py +107 -0
- nodescraper/plugins/inband/journal/journal_plugin.py +40 -0
- nodescraper/plugins/inband/journal/journaldata.py +44 -0
- nodescraper/plugins/inband/kernel/__init__.py +25 -0
- nodescraper/plugins/inband/kernel/analyzer_args.py +64 -0
- nodescraper/plugins/inband/kernel/kernel_analyzer.py +91 -0
- nodescraper/plugins/inband/kernel/kernel_collector.py +129 -0
- nodescraper/plugins/inband/kernel/kernel_plugin.py +43 -0
- nodescraper/plugins/inband/kernel/kerneldata.py +32 -0
- nodescraper/plugins/inband/kernel_module/__init__.py +25 -0
- nodescraper/plugins/inband/kernel_module/analyzer_args.py +59 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py +211 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_collector.py +264 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_data.py +60 -0
- nodescraper/plugins/inband/kernel_module/kernel_module_plugin.py +43 -0
- nodescraper/plugins/inband/memory/__init__.py +25 -0
- nodescraper/plugins/inband/memory/analyzer_args.py +45 -0
- nodescraper/plugins/inband/memory/memory_analyzer.py +98 -0
- nodescraper/plugins/inband/memory/memory_collector.py +330 -0
- nodescraper/plugins/inband/memory/memory_plugin.py +43 -0
- nodescraper/plugins/inband/memory/memorydata.py +90 -0
- nodescraper/plugins/inband/network/__init__.py +28 -0
- nodescraper/plugins/inband/network/network_collector.py +1828 -0
- nodescraper/plugins/inband/network/network_plugin.py +37 -0
- nodescraper/plugins/inband/network/networkdata.py +319 -0
- nodescraper/plugins/inband/nvme/__init__.py +28 -0
- nodescraper/plugins/inband/nvme/nvme_collector.py +167 -0
- nodescraper/plugins/inband/nvme/nvme_plugin.py +37 -0
- nodescraper/plugins/inband/nvme/nvmedata.py +45 -0
- nodescraper/plugins/inband/os/__init__.py +25 -0
- nodescraper/plugins/inband/os/analyzer_args.py +64 -0
- nodescraper/plugins/inband/os/os_analyzer.py +73 -0
- nodescraper/plugins/inband/os/os_collector.py +131 -0
- nodescraper/plugins/inband/os/os_plugin.py +43 -0
- nodescraper/plugins/inband/os/osdata.py +31 -0
- nodescraper/plugins/inband/package/__init__.py +25 -0
- nodescraper/plugins/inband/package/analyzer_args.py +48 -0
- nodescraper/plugins/inband/package/package_analyzer.py +253 -0
- nodescraper/plugins/inband/package/package_collector.py +273 -0
- nodescraper/plugins/inband/package/package_plugin.py +43 -0
- nodescraper/plugins/inband/package/packagedata.py +41 -0
- nodescraper/plugins/inband/pcie/__init__.py +29 -0
- nodescraper/plugins/inband/pcie/analyzer_args.py +63 -0
- nodescraper/plugins/inband/pcie/pcie_analyzer.py +1081 -0
- nodescraper/plugins/inband/pcie/pcie_collector.py +690 -0
- nodescraper/plugins/inband/pcie/pcie_data.py +2017 -0
- nodescraper/plugins/inband/pcie/pcie_plugin.py +43 -0
- nodescraper/plugins/inband/process/__init__.py +25 -0
- nodescraper/plugins/inband/process/analyzer_args.py +45 -0
- nodescraper/plugins/inband/process/collector_args.py +31 -0
- nodescraper/plugins/inband/process/process_analyzer.py +91 -0
- nodescraper/plugins/inband/process/process_collector.py +115 -0
- nodescraper/plugins/inband/process/process_plugin.py +46 -0
- nodescraper/plugins/inband/process/processdata.py +34 -0
- nodescraper/plugins/inband/rocm/__init__.py +25 -0
- nodescraper/plugins/inband/rocm/analyzer_args.py +66 -0
- nodescraper/plugins/inband/rocm/rocm_analyzer.py +100 -0
- nodescraper/plugins/inband/rocm/rocm_collector.py +205 -0
- nodescraper/plugins/inband/rocm/rocm_plugin.py +43 -0
- nodescraper/plugins/inband/rocm/rocmdata.py +62 -0
- nodescraper/plugins/inband/storage/__init__.py +25 -0
- nodescraper/plugins/inband/storage/analyzer_args.py +38 -0
- nodescraper/plugins/inband/storage/collector_args.py +31 -0
- nodescraper/plugins/inband/storage/storage_analyzer.py +152 -0
- nodescraper/plugins/inband/storage/storage_collector.py +110 -0
- nodescraper/plugins/inband/storage/storage_plugin.py +44 -0
- nodescraper/plugins/inband/storage/storagedata.py +70 -0
- nodescraper/plugins/inband/sysctl/__init__.py +29 -0
- nodescraper/plugins/inband/sysctl/analyzer_args.py +67 -0
- nodescraper/plugins/inband/sysctl/sysctl_analyzer.py +81 -0
- nodescraper/plugins/inband/sysctl/sysctl_collector.py +101 -0
- nodescraper/plugins/inband/sysctl/sysctl_plugin.py +43 -0
- nodescraper/plugins/inband/sysctl/sysctldata.py +42 -0
- nodescraper/plugins/inband/syslog/__init__.py +28 -0
- nodescraper/plugins/inband/syslog/syslog_collector.py +121 -0
- nodescraper/plugins/inband/syslog/syslog_plugin.py +37 -0
- nodescraper/plugins/inband/syslog/syslogdata.py +46 -0
- nodescraper/plugins/inband/uptime/__init__.py +25 -0
- nodescraper/plugins/inband/uptime/uptime_collector.py +88 -0
- nodescraper/plugins/inband/uptime/uptime_plugin.py +37 -0
- nodescraper/plugins/inband/uptime/uptimedata.py +31 -0
- nodescraper/resultcollators/__init__.py +25 -0
- nodescraper/resultcollators/tablesummary.py +159 -0
- nodescraper/taskresulthooks/__init__.py +28 -0
- nodescraper/taskresulthooks/filesystemloghook.py +88 -0
- nodescraper/typeutils.py +171 -0
- nodescraper/utils.py +412 -0
|
@@ -0,0 +1,1828 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# MIT License
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2025 Advanced Micro Devices, Inc.
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
# copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
# SOFTWARE.
|
|
24
|
+
#
|
|
25
|
+
###############################################################################
|
|
26
|
+
import re
|
|
27
|
+
from typing import Dict, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
from nodescraper.base import InBandDataCollector
|
|
30
|
+
from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus
|
|
31
|
+
from nodescraper.models import TaskResult
|
|
32
|
+
|
|
33
|
+
from .networkdata import (
|
|
34
|
+
BroadcomNicDevice,
|
|
35
|
+
BroadcomNicQos,
|
|
36
|
+
BroadcomNicQosAppEntry,
|
|
37
|
+
EthtoolInfo,
|
|
38
|
+
IpAddress,
|
|
39
|
+
Neighbor,
|
|
40
|
+
NetworkDataModel,
|
|
41
|
+
NetworkInterface,
|
|
42
|
+
PensandoNicCard,
|
|
43
|
+
PensandoNicDcqcn,
|
|
44
|
+
PensandoNicEnvironment,
|
|
45
|
+
PensandoNicPcieAts,
|
|
46
|
+
PensandoNicPort,
|
|
47
|
+
PensandoNicQos,
|
|
48
|
+
PensandoNicQosScheduling,
|
|
49
|
+
PensandoNicRdmaStatistic,
|
|
50
|
+
PensandoNicRdmaStatistics,
|
|
51
|
+
PensandoNicVersionFirmware,
|
|
52
|
+
PensandoNicVersionHostSoftware,
|
|
53
|
+
Route,
|
|
54
|
+
RoutingRule,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NetworkCollector(InBandDataCollector[NetworkDataModel, None]):
|
|
59
|
+
"""Collect network configuration details using ip command"""
|
|
60
|
+
|
|
61
|
+
DATA_MODEL = NetworkDataModel
|
|
62
|
+
CMD_ADDR = "ip addr show"
|
|
63
|
+
CMD_ROUTE = "ip route show"
|
|
64
|
+
CMD_RULE = "ip rule show"
|
|
65
|
+
CMD_NEIGHBOR = "ip neighbor show"
|
|
66
|
+
CMD_ETHTOOL_TEMPLATE = "ethtool {interface}"
|
|
67
|
+
|
|
68
|
+
# LLDP commands
|
|
69
|
+
CMD_LLDPCLI_NEIGHBOR = "lldpcli show neighbor"
|
|
70
|
+
CMD_LLDPCTL = "lldpctl"
|
|
71
|
+
|
|
72
|
+
# Broadcom NIC commands
|
|
73
|
+
CMD_NICCLI_LISTDEV = "niccli --list_devices"
|
|
74
|
+
CMD_NICCLI_GETQOS_TEMPLATE = "niccli --dev {device_num} qos --ets --show"
|
|
75
|
+
|
|
76
|
+
# Pensando NIC commands
|
|
77
|
+
CMD_NICCTL_CARD = "nicctl show card"
|
|
78
|
+
CMD_NICCTL_DCQCN = "nicctl show dcqcn"
|
|
79
|
+
CMD_NICCTL_ENVIRONMENT = "nicctl show environment"
|
|
80
|
+
CMD_NICCTL_PCIE_ATS = "nicctl show pcie ats"
|
|
81
|
+
CMD_NICCTL_PORT = "nicctl show port"
|
|
82
|
+
CMD_NICCTL_QOS = "nicctl show qos"
|
|
83
|
+
CMD_NICCTL_RDMA_STATISTICS = "nicctl show rdma statistics"
|
|
84
|
+
CMD_NICCTL_VERSION_HOST_SOFTWARE = "nicctl show version host-software"
|
|
85
|
+
CMD_NICCTL_VERSION_FIRMWARE = "nicctl show version firmware"
|
|
86
|
+
|
|
87
|
+
def _parse_ip_addr(self, output: str) -> List[NetworkInterface]:
|
|
88
|
+
"""Parse 'ip addr show' output into NetworkInterface objects.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
output: Raw output from 'ip addr show' command
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of NetworkInterface objects
|
|
95
|
+
"""
|
|
96
|
+
interfaces = {}
|
|
97
|
+
current_interface = None
|
|
98
|
+
|
|
99
|
+
for line in output.splitlines():
|
|
100
|
+
# Check if this is an interface header line
|
|
101
|
+
# Format: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN ...
|
|
102
|
+
if re.match(r"^\d+:", line):
|
|
103
|
+
parts = line.split()
|
|
104
|
+
|
|
105
|
+
# Extract interface index and name
|
|
106
|
+
idx_str = parts[0].rstrip(":")
|
|
107
|
+
try:
|
|
108
|
+
index = int(idx_str)
|
|
109
|
+
except ValueError:
|
|
110
|
+
index = None
|
|
111
|
+
|
|
112
|
+
ifname = parts[1].rstrip(":")
|
|
113
|
+
current_interface = ifname
|
|
114
|
+
|
|
115
|
+
# Extract flags
|
|
116
|
+
flags: List[str] = []
|
|
117
|
+
if "<" in line:
|
|
118
|
+
flag_match = re.search(r"<([^>]+)>", line)
|
|
119
|
+
if flag_match:
|
|
120
|
+
flags = flag_match.group(1).split(",")
|
|
121
|
+
|
|
122
|
+
# Extract other attributes
|
|
123
|
+
mtu = None
|
|
124
|
+
qdisc = None
|
|
125
|
+
state = None
|
|
126
|
+
|
|
127
|
+
# Known keyword-value pairs
|
|
128
|
+
keyword_value_pairs = ["mtu", "qdisc", "state"]
|
|
129
|
+
|
|
130
|
+
for i, part in enumerate(parts):
|
|
131
|
+
if part in keyword_value_pairs and i + 1 < len(parts):
|
|
132
|
+
if part == "mtu":
|
|
133
|
+
try:
|
|
134
|
+
mtu = int(parts[i + 1])
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
elif part == "qdisc":
|
|
138
|
+
qdisc = parts[i + 1]
|
|
139
|
+
elif part == "state":
|
|
140
|
+
state = parts[i + 1]
|
|
141
|
+
|
|
142
|
+
interfaces[ifname] = NetworkInterface(
|
|
143
|
+
name=ifname,
|
|
144
|
+
index=index,
|
|
145
|
+
state=state,
|
|
146
|
+
mtu=mtu,
|
|
147
|
+
qdisc=qdisc,
|
|
148
|
+
flags=flags,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Check if this is a link line (contains MAC address)
|
|
152
|
+
# Format: link/ether 00:40:a6:96:d7:5a brd ff:ff:ff:ff:ff:ff
|
|
153
|
+
elif "link/" in line and current_interface:
|
|
154
|
+
parts = line.split()
|
|
155
|
+
if "link/ether" in parts:
|
|
156
|
+
idx = parts.index("link/ether")
|
|
157
|
+
if idx + 1 < len(parts):
|
|
158
|
+
interfaces[current_interface].mac_address = parts[idx + 1]
|
|
159
|
+
elif "link/loopback" in parts:
|
|
160
|
+
# Loopback interface
|
|
161
|
+
if len(parts) > 1:
|
|
162
|
+
interfaces[current_interface].mac_address = parts[1]
|
|
163
|
+
|
|
164
|
+
# Check if this is an inet/inet6 address line
|
|
165
|
+
# Format: inet 10.228.152.67/22 brd 10.228.155.255 scope global noprefixroute enp129s0
|
|
166
|
+
elif any(x in line for x in ["inet ", "inet6 "]) and current_interface:
|
|
167
|
+
parts = line.split()
|
|
168
|
+
|
|
169
|
+
# Parse the IP address
|
|
170
|
+
family = None
|
|
171
|
+
address = None
|
|
172
|
+
prefix_len = None
|
|
173
|
+
scope = None
|
|
174
|
+
broadcast = None
|
|
175
|
+
|
|
176
|
+
for i, part in enumerate(parts):
|
|
177
|
+
if part in ["inet", "inet6"]:
|
|
178
|
+
family = part
|
|
179
|
+
if i + 1 < len(parts):
|
|
180
|
+
addr_part = parts[i + 1]
|
|
181
|
+
if "/" in addr_part:
|
|
182
|
+
address, prefix = addr_part.split("/")
|
|
183
|
+
try:
|
|
184
|
+
prefix_len = int(prefix)
|
|
185
|
+
except ValueError:
|
|
186
|
+
pass
|
|
187
|
+
else:
|
|
188
|
+
address = addr_part
|
|
189
|
+
elif part == "scope" and i + 1 < len(parts):
|
|
190
|
+
scope = parts[i + 1]
|
|
191
|
+
elif part in ["brd", "broadcast"] and i + 1 < len(parts):
|
|
192
|
+
broadcast = parts[i + 1]
|
|
193
|
+
|
|
194
|
+
if address and current_interface in interfaces:
|
|
195
|
+
ip_addr = IpAddress(
|
|
196
|
+
address=address,
|
|
197
|
+
prefix_len=prefix_len,
|
|
198
|
+
family=family,
|
|
199
|
+
scope=scope,
|
|
200
|
+
broadcast=broadcast,
|
|
201
|
+
label=current_interface,
|
|
202
|
+
)
|
|
203
|
+
interfaces[current_interface].addresses.append(ip_addr)
|
|
204
|
+
|
|
205
|
+
return list(interfaces.values())
|
|
206
|
+
|
|
207
|
+
def _parse_ip_route(self, output: str) -> List[Route]:
|
|
208
|
+
"""Parse 'ip route show' output into Route objects.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
output: Raw output from 'ip route show' command
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of Route objects
|
|
215
|
+
"""
|
|
216
|
+
routes = []
|
|
217
|
+
|
|
218
|
+
for line in output.splitlines():
|
|
219
|
+
line = line.strip()
|
|
220
|
+
if not line:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
parts = line.split()
|
|
224
|
+
if not parts:
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# First part is destination (can be "default" or a network)
|
|
228
|
+
destination = parts[0]
|
|
229
|
+
|
|
230
|
+
route = Route(destination=destination)
|
|
231
|
+
|
|
232
|
+
# Known keyword-value pairs
|
|
233
|
+
keyword_value_pairs = ["via", "dev", "proto", "scope", "metric", "src", "table"]
|
|
234
|
+
|
|
235
|
+
# Parse route attributes
|
|
236
|
+
i = 1
|
|
237
|
+
while i < len(parts):
|
|
238
|
+
if parts[i] in keyword_value_pairs and i + 1 < len(parts):
|
|
239
|
+
keyword = parts[i]
|
|
240
|
+
value = parts[i + 1]
|
|
241
|
+
|
|
242
|
+
if keyword == "via":
|
|
243
|
+
route.gateway = value
|
|
244
|
+
elif keyword == "dev":
|
|
245
|
+
route.device = value
|
|
246
|
+
elif keyword == "proto":
|
|
247
|
+
route.protocol = value
|
|
248
|
+
elif keyword == "scope":
|
|
249
|
+
route.scope = value
|
|
250
|
+
elif keyword == "metric":
|
|
251
|
+
try:
|
|
252
|
+
route.metric = int(value)
|
|
253
|
+
except ValueError:
|
|
254
|
+
pass
|
|
255
|
+
elif keyword == "src":
|
|
256
|
+
route.source = value
|
|
257
|
+
elif keyword == "table":
|
|
258
|
+
route.table = value
|
|
259
|
+
i += 2
|
|
260
|
+
else:
|
|
261
|
+
i += 1
|
|
262
|
+
|
|
263
|
+
routes.append(route)
|
|
264
|
+
|
|
265
|
+
return routes
|
|
266
|
+
|
|
267
|
+
def _parse_ip_rule(self, output: str) -> List[RoutingRule]:
|
|
268
|
+
"""Parse 'ip rule show' output into RoutingRule objects.
|
|
269
|
+
Example ip rule: 200: from 172.16.0.0/12 to 8.8.8.8 iif wlan0 oif eth0 fwmark 0x20 table vpn_table
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
output: Raw output from 'ip rule show' command
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
List of RoutingRule objects
|
|
276
|
+
"""
|
|
277
|
+
rules = []
|
|
278
|
+
|
|
279
|
+
for line in output.splitlines():
|
|
280
|
+
line = line.strip()
|
|
281
|
+
if not line:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
parts = line.split()
|
|
285
|
+
if not parts:
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# First part is priority followed by ":"
|
|
289
|
+
priority_str = parts[0].rstrip(":")
|
|
290
|
+
try:
|
|
291
|
+
priority = int(priority_str)
|
|
292
|
+
except ValueError:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
rule = RoutingRule(priority=priority)
|
|
296
|
+
|
|
297
|
+
# Parse rule attributes
|
|
298
|
+
i = 1
|
|
299
|
+
while i < len(parts):
|
|
300
|
+
if parts[i] == "from" and i + 1 < len(parts):
|
|
301
|
+
if parts[i + 1] != "all":
|
|
302
|
+
rule.source = parts[i + 1]
|
|
303
|
+
i += 2
|
|
304
|
+
elif parts[i] == "to" and i + 1 < len(parts):
|
|
305
|
+
if parts[i + 1] != "all":
|
|
306
|
+
rule.destination = parts[i + 1]
|
|
307
|
+
i += 2
|
|
308
|
+
elif parts[i] in ["lookup", "table"] and i + 1 < len(parts):
|
|
309
|
+
rule.table = parts[i + 1]
|
|
310
|
+
if parts[i] == "lookup":
|
|
311
|
+
rule.action = "lookup"
|
|
312
|
+
i += 2
|
|
313
|
+
elif parts[i] == "iif" and i + 1 < len(parts):
|
|
314
|
+
rule.iif = parts[i + 1]
|
|
315
|
+
i += 2
|
|
316
|
+
elif parts[i] == "oif" and i + 1 < len(parts):
|
|
317
|
+
rule.oif = parts[i + 1]
|
|
318
|
+
i += 2
|
|
319
|
+
elif parts[i] == "fwmark" and i + 1 < len(parts):
|
|
320
|
+
rule.fwmark = parts[i + 1]
|
|
321
|
+
i += 2
|
|
322
|
+
elif parts[i] in ["unreachable", "prohibit", "blackhole"]:
|
|
323
|
+
rule.action = parts[i]
|
|
324
|
+
i += 1
|
|
325
|
+
else:
|
|
326
|
+
i += 1
|
|
327
|
+
|
|
328
|
+
rules.append(rule)
|
|
329
|
+
|
|
330
|
+
return rules
|
|
331
|
+
|
|
332
|
+
def _parse_ip_neighbor(self, output: str) -> List[Neighbor]:
|
|
333
|
+
"""Parse 'ip neighbor show' output into Neighbor objects.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
output: Raw output from 'ip neighbor show' command
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
List of Neighbor objects
|
|
340
|
+
"""
|
|
341
|
+
neighbors = []
|
|
342
|
+
|
|
343
|
+
# Known keyword-value pairs (keyword takes next element as value)
|
|
344
|
+
keyword_value_pairs = ["dev", "lladdr", "nud", "vlan", "via"]
|
|
345
|
+
|
|
346
|
+
for line in output.splitlines():
|
|
347
|
+
line = line.strip()
|
|
348
|
+
if not line:
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
parts = line.split()
|
|
352
|
+
if not parts:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# First part is the IP address
|
|
356
|
+
ip_address = parts[0]
|
|
357
|
+
|
|
358
|
+
neighbor = Neighbor(ip_address=ip_address)
|
|
359
|
+
|
|
360
|
+
# Parse neighbor attributes
|
|
361
|
+
i = 1
|
|
362
|
+
while i < len(parts):
|
|
363
|
+
current = parts[i]
|
|
364
|
+
|
|
365
|
+
# Check for known keyword-value pairs
|
|
366
|
+
if current in keyword_value_pairs and i + 1 < len(parts):
|
|
367
|
+
if current == "dev":
|
|
368
|
+
neighbor.device = parts[i + 1]
|
|
369
|
+
elif current == "lladdr":
|
|
370
|
+
neighbor.mac_address = parts[i + 1]
|
|
371
|
+
# Other keyword-value pairs can be added here as needed
|
|
372
|
+
i += 2
|
|
373
|
+
|
|
374
|
+
# Check if it's a state (all uppercase, typically single word)
|
|
375
|
+
elif current.isupper() and current.isalpha():
|
|
376
|
+
# States: REACHABLE, STALE, DELAY, PROBE, FAILED, INCOMPLETE, PERMANENT, NOARP
|
|
377
|
+
# Future states will also be captured
|
|
378
|
+
neighbor.state = current
|
|
379
|
+
i += 1
|
|
380
|
+
|
|
381
|
+
# Check if it looks like a MAC address (contains colons)
|
|
382
|
+
elif ":" in current and not current.startswith("http"):
|
|
383
|
+
# Already handled by lladdr, but in case it appears standalone
|
|
384
|
+
if not neighbor.mac_address:
|
|
385
|
+
neighbor.mac_address = current
|
|
386
|
+
i += 1
|
|
387
|
+
|
|
388
|
+
# Check if it looks like an IP address (has dots or is IPv6)
|
|
389
|
+
elif "." in current or ("::" in current):
|
|
390
|
+
# Skip IP addresses that might appear (already captured as first element)
|
|
391
|
+
i += 1
|
|
392
|
+
|
|
393
|
+
# Anything else that's a simple lowercase word is likely a flag
|
|
394
|
+
elif current.isalpha() and current.islower():
|
|
395
|
+
# Flags: router, proxy, extern_learn, offload, managed, etc.
|
|
396
|
+
# Captures both known and future flags
|
|
397
|
+
neighbor.flags.append(current)
|
|
398
|
+
i += 1
|
|
399
|
+
|
|
400
|
+
else:
|
|
401
|
+
# Unknown format, skip it
|
|
402
|
+
i += 1
|
|
403
|
+
|
|
404
|
+
neighbors.append(neighbor)
|
|
405
|
+
|
|
406
|
+
return neighbors
|
|
407
|
+
|
|
408
|
+
def _parse_ethtool(self, interface: str, output: str) -> EthtoolInfo:
|
|
409
|
+
"""Parse 'ethtool <interface>' output into EthtoolInfo object.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
interface: Name of the network interface
|
|
413
|
+
output: Raw output from 'ethtool <interface>' command
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
EthtoolInfo object with parsed data
|
|
417
|
+
"""
|
|
418
|
+
ethtool_info = EthtoolInfo(interface=interface, raw_output=output)
|
|
419
|
+
|
|
420
|
+
# Parse line by line
|
|
421
|
+
current_section = None
|
|
422
|
+
for line in output.splitlines():
|
|
423
|
+
line_stripped = line.strip()
|
|
424
|
+
if not line_stripped:
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
# Detect sections (lines ending with colon and no tab prefix)
|
|
428
|
+
if line_stripped.endswith(":") and not line.startswith("\t"):
|
|
429
|
+
current_section = line_stripped.rstrip(":")
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
# Parse key-value pairs (lines with colon in the middle)
|
|
433
|
+
if ":" in line_stripped:
|
|
434
|
+
# Split on first colon
|
|
435
|
+
parts = line_stripped.split(":", 1)
|
|
436
|
+
if len(parts) == 2:
|
|
437
|
+
key = parts[0].strip()
|
|
438
|
+
value = parts[1].strip()
|
|
439
|
+
|
|
440
|
+
# Store in settings dict
|
|
441
|
+
ethtool_info.settings[key] = value
|
|
442
|
+
|
|
443
|
+
# Extract specific important fields
|
|
444
|
+
if key == "Speed":
|
|
445
|
+
ethtool_info.speed = value
|
|
446
|
+
elif key == "Duplex":
|
|
447
|
+
ethtool_info.duplex = value
|
|
448
|
+
elif key == "Port":
|
|
449
|
+
ethtool_info.port = value
|
|
450
|
+
elif key == "Auto-negotiation":
|
|
451
|
+
ethtool_info.auto_negotiation = value
|
|
452
|
+
elif key == "Link detected":
|
|
453
|
+
ethtool_info.link_detected = value
|
|
454
|
+
|
|
455
|
+
# Parse supported/advertised link modes (typically indented list items)
|
|
456
|
+
elif current_section in ["Supported link modes", "Advertised link modes"]:
|
|
457
|
+
# These are typically list items, possibly with speeds like "10baseT/Half"
|
|
458
|
+
if line.startswith("\t") or line.startswith(" "):
|
|
459
|
+
mode = line_stripped
|
|
460
|
+
if current_section == "Supported link modes":
|
|
461
|
+
ethtool_info.supported_link_modes.append(mode)
|
|
462
|
+
elif current_section == "Advertised link modes":
|
|
463
|
+
ethtool_info.advertised_link_modes.append(mode)
|
|
464
|
+
|
|
465
|
+
return ethtool_info
|
|
466
|
+
|
|
467
|
+
def _parse_niccli_listdev(self, output: str) -> List[BroadcomNicDevice]:
|
|
468
|
+
"""Parse 'niccli --list_devices' output into BroadcomNicDevice objects.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
output: Raw output from 'niccli --list_devices' command
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
List of BroadcomNicDevice objects
|
|
475
|
+
"""
|
|
476
|
+
devices = []
|
|
477
|
+
current_device = None
|
|
478
|
+
|
|
479
|
+
for line in output.splitlines():
|
|
480
|
+
line_stripped = line.strip()
|
|
481
|
+
if not line_stripped:
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
# Check if this is a device header line
|
|
485
|
+
match = re.match(r"^(\d+)\s*\)\s*(.+?)(?:\s+\((.+?)\))?$", line_stripped)
|
|
486
|
+
if match:
|
|
487
|
+
device_num_str = match.group(1)
|
|
488
|
+
model = match.group(2).strip() if match.group(2) else None
|
|
489
|
+
adapter_port = match.group(3).strip() if match.group(3) else None
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
device_num = int(device_num_str)
|
|
493
|
+
except ValueError:
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
current_device = BroadcomNicDevice(
|
|
497
|
+
device_num=device_num,
|
|
498
|
+
model=model,
|
|
499
|
+
adapter_port=adapter_port,
|
|
500
|
+
)
|
|
501
|
+
devices.append(current_device)
|
|
502
|
+
|
|
503
|
+
# Check for Device Interface Name line
|
|
504
|
+
elif "Device Interface Name" in line and current_device:
|
|
505
|
+
parts = line_stripped.split(":")
|
|
506
|
+
if len(parts) >= 2:
|
|
507
|
+
current_device.interface_name = parts[1].strip()
|
|
508
|
+
|
|
509
|
+
# Check for MAC Address line
|
|
510
|
+
elif "MAC Address" in line and current_device:
|
|
511
|
+
parts = line_stripped.split(":")
|
|
512
|
+
if len(parts) >= 2:
|
|
513
|
+
# MAC address has colons, so rejoin the parts after first split
|
|
514
|
+
mac = ":".join(parts[1:]).strip()
|
|
515
|
+
current_device.mac_address = mac
|
|
516
|
+
|
|
517
|
+
# Check for PCI Address line
|
|
518
|
+
elif "PCI Address" in line and current_device:
|
|
519
|
+
parts = line_stripped.split(":")
|
|
520
|
+
if len(parts) >= 2:
|
|
521
|
+
# PCI address also has colons, rejoin
|
|
522
|
+
pci = ":".join(parts[1:]).strip()
|
|
523
|
+
current_device.pci_address = pci
|
|
524
|
+
|
|
525
|
+
return devices
|
|
526
|
+
|
|
527
|
+
def _parse_nicctl_card(self, output: str) -> List[PensandoNicCard]:
|
|
528
|
+
"""Parse 'nicctl show card' output into PensandoNicCard objects.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
output: Raw output from 'nicctl show card' command
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
List of PensandoNicCard objects
|
|
535
|
+
"""
|
|
536
|
+
cards = []
|
|
537
|
+
|
|
538
|
+
# Skip header lines and separator lines
|
|
539
|
+
in_data_section = False
|
|
540
|
+
|
|
541
|
+
for line in output.splitlines():
|
|
542
|
+
line_stripped = line.strip()
|
|
543
|
+
if not line_stripped:
|
|
544
|
+
continue
|
|
545
|
+
|
|
546
|
+
# Skip header line (starts with "Id")
|
|
547
|
+
if line_stripped.startswith("Id"):
|
|
548
|
+
in_data_section = True
|
|
549
|
+
continue
|
|
550
|
+
|
|
551
|
+
# Skip separator lines (mostly dashes)
|
|
552
|
+
if re.match(r"^-+$", line_stripped):
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
# Parse data lines after header
|
|
556
|
+
if in_data_section:
|
|
557
|
+
# Split by whitespace
|
|
558
|
+
parts = line_stripped.split()
|
|
559
|
+
|
|
560
|
+
# Expected format: Id PCIe_BDF ASIC F/W_partition Serial_number
|
|
561
|
+
if len(parts) >= 2:
|
|
562
|
+
card = PensandoNicCard(
|
|
563
|
+
id=parts[0],
|
|
564
|
+
pcie_bdf=parts[1],
|
|
565
|
+
asic=parts[2] if len(parts) > 2 else None,
|
|
566
|
+
fw_partition=parts[3] if len(parts) > 3 else None,
|
|
567
|
+
serial_number=parts[4] if len(parts) > 4 else None,
|
|
568
|
+
)
|
|
569
|
+
cards.append(card)
|
|
570
|
+
|
|
571
|
+
return cards
|
|
572
|
+
|
|
573
|
+
def _parse_nicctl_dcqcn(self, output: str) -> List[PensandoNicDcqcn]:
|
|
574
|
+
"""Parse 'nicctl show dcqcn' output into PensandoNicDcqcn objects.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
output: Raw output from 'nicctl show dcqcn' command
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
List of PensandoNicDcqcn objects
|
|
581
|
+
"""
|
|
582
|
+
dcqcn_entries = []
|
|
583
|
+
current_entry = None
|
|
584
|
+
|
|
585
|
+
for line in output.splitlines():
|
|
586
|
+
line_stripped = line.strip()
|
|
587
|
+
if not line_stripped:
|
|
588
|
+
continue
|
|
589
|
+
|
|
590
|
+
# Check for NIC line
|
|
591
|
+
if line_stripped.startswith("NIC :"):
|
|
592
|
+
# Save previous entry if exists
|
|
593
|
+
if current_entry:
|
|
594
|
+
dcqcn_entries.append(current_entry)
|
|
595
|
+
|
|
596
|
+
# Parse NIC ID and PCIe BDF
|
|
597
|
+
# Format: "NIC : <id> (<pcie_bdf>)"
|
|
598
|
+
match = re.match(
|
|
599
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)", line_stripped, re.IGNORECASE
|
|
600
|
+
)
|
|
601
|
+
if match:
|
|
602
|
+
nic_id = match.group(1)
|
|
603
|
+
pcie_bdf = match.group(2)
|
|
604
|
+
current_entry = PensandoNicDcqcn(
|
|
605
|
+
nic_id=nic_id,
|
|
606
|
+
pcie_bdf=pcie_bdf,
|
|
607
|
+
)
|
|
608
|
+
continue
|
|
609
|
+
|
|
610
|
+
# Skip separator lines (dashes or asterisks)
|
|
611
|
+
if re.match(r"^[-*]+$", line_stripped):
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
# Parse fields within current entry
|
|
615
|
+
if current_entry and ":" in line_stripped:
|
|
616
|
+
parts = line_stripped.split(":", 1)
|
|
617
|
+
if len(parts) == 2:
|
|
618
|
+
key = parts[0].strip()
|
|
619
|
+
value = parts[1].strip()
|
|
620
|
+
|
|
621
|
+
if key == "Lif id":
|
|
622
|
+
current_entry.lif_id = value
|
|
623
|
+
elif key == "ROCE device":
|
|
624
|
+
current_entry.roce_device = value
|
|
625
|
+
elif key == "DCQCN profile id":
|
|
626
|
+
current_entry.dcqcn_profile_id = value
|
|
627
|
+
elif key == "Status":
|
|
628
|
+
current_entry.status = value
|
|
629
|
+
|
|
630
|
+
# Add the last entry if exists
|
|
631
|
+
if current_entry:
|
|
632
|
+
dcqcn_entries.append(current_entry)
|
|
633
|
+
|
|
634
|
+
return dcqcn_entries
|
|
635
|
+
|
|
636
|
+
def _parse_nicctl_environment(self, output: str) -> List[PensandoNicEnvironment]:
|
|
637
|
+
"""Parse 'nicctl show environment' output into PensandoNicEnvironment objects.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
output: Raw output from 'nicctl show environment' command
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
List of PensandoNicEnvironment objects
|
|
644
|
+
"""
|
|
645
|
+
environment_entries = []
|
|
646
|
+
current_entry = None
|
|
647
|
+
|
|
648
|
+
for line in output.splitlines():
|
|
649
|
+
line_stripped = line.strip()
|
|
650
|
+
if not line_stripped:
|
|
651
|
+
continue
|
|
652
|
+
|
|
653
|
+
# Check for NIC line
|
|
654
|
+
if line_stripped.startswith("NIC :"):
|
|
655
|
+
# Save previous entry if exists
|
|
656
|
+
if current_entry:
|
|
657
|
+
environment_entries.append(current_entry)
|
|
658
|
+
|
|
659
|
+
# Parse NIC ID and PCIe BDF
|
|
660
|
+
# Format: "NIC : <id> (<pcie_bdf>)"
|
|
661
|
+
match = re.match(
|
|
662
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)", line_stripped, re.IGNORECASE
|
|
663
|
+
)
|
|
664
|
+
if match:
|
|
665
|
+
nic_id = match.group(1)
|
|
666
|
+
pcie_bdf = match.group(2)
|
|
667
|
+
current_entry = PensandoNicEnvironment(
|
|
668
|
+
nic_id=nic_id,
|
|
669
|
+
pcie_bdf=pcie_bdf,
|
|
670
|
+
)
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
# Skip separator lines (dashes)
|
|
674
|
+
if re.match(r"^-+$", line_stripped):
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
# Skip section headers (Power(W):, Temperature(C):, etc.)
|
|
678
|
+
if line_stripped.endswith("):"):
|
|
679
|
+
continue
|
|
680
|
+
|
|
681
|
+
# Parse fields within current entry
|
|
682
|
+
if current_entry and ":" in line_stripped:
|
|
683
|
+
parts = line_stripped.split(":", 1)
|
|
684
|
+
if len(parts) == 2:
|
|
685
|
+
key = parts[0].strip()
|
|
686
|
+
value_str = parts[1].strip()
|
|
687
|
+
|
|
688
|
+
# Try to parse the value as float
|
|
689
|
+
try:
|
|
690
|
+
value = float(value_str)
|
|
691
|
+
except ValueError:
|
|
692
|
+
continue
|
|
693
|
+
|
|
694
|
+
# Map keys to fields
|
|
695
|
+
if key == "Total power drawn (pin)" or key == "Total power drawn":
|
|
696
|
+
current_entry.total_power_drawn = value
|
|
697
|
+
elif key == "Core power (pout1)" or key == "Core power":
|
|
698
|
+
current_entry.core_power = value
|
|
699
|
+
elif key == "ARM power (pout2)" or key == "ARM power":
|
|
700
|
+
current_entry.arm_power = value
|
|
701
|
+
elif key == "Local board temperature":
|
|
702
|
+
current_entry.local_board_temperature = value
|
|
703
|
+
elif key == "Die temperature":
|
|
704
|
+
current_entry.die_temperature = value
|
|
705
|
+
elif key == "Input voltage":
|
|
706
|
+
current_entry.input_voltage = value
|
|
707
|
+
elif key == "Core voltage":
|
|
708
|
+
current_entry.core_voltage = value
|
|
709
|
+
elif key == "Core frequency":
|
|
710
|
+
current_entry.core_frequency = value
|
|
711
|
+
elif key == "CPU frequency":
|
|
712
|
+
current_entry.cpu_frequency = value
|
|
713
|
+
elif key == "P4 stage frequency":
|
|
714
|
+
current_entry.p4_stage_frequency = value
|
|
715
|
+
|
|
716
|
+
# Add the last entry if exists
|
|
717
|
+
if current_entry:
|
|
718
|
+
environment_entries.append(current_entry)
|
|
719
|
+
|
|
720
|
+
return environment_entries
|
|
721
|
+
|
|
722
|
+
def _parse_nicctl_pcie_ats(self, output: str) -> List[PensandoNicPcieAts]:
|
|
723
|
+
"""Parse 'nicctl show pcie ats' output into PensandoNicPcieAts objects.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
output: Raw output from 'nicctl show pcie ats' command
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
List of PensandoNicPcieAts objects
|
|
730
|
+
"""
|
|
731
|
+
pcie_ats_entries = []
|
|
732
|
+
|
|
733
|
+
for line in output.splitlines():
|
|
734
|
+
line_stripped = line.strip()
|
|
735
|
+
if not line_stripped:
|
|
736
|
+
continue
|
|
737
|
+
|
|
738
|
+
# Parse line format: "NIC : <id> (<pcie_bdf>) : <status>"
|
|
739
|
+
if line_stripped.startswith("NIC :"):
|
|
740
|
+
match = re.match(
|
|
741
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)\s*:\s*(\w+)",
|
|
742
|
+
line_stripped,
|
|
743
|
+
re.IGNORECASE,
|
|
744
|
+
)
|
|
745
|
+
if match:
|
|
746
|
+
nic_id = match.group(1)
|
|
747
|
+
pcie_bdf = match.group(2)
|
|
748
|
+
status = match.group(3)
|
|
749
|
+
entry = PensandoNicPcieAts(
|
|
750
|
+
nic_id=nic_id,
|
|
751
|
+
pcie_bdf=pcie_bdf,
|
|
752
|
+
status=status,
|
|
753
|
+
)
|
|
754
|
+
pcie_ats_entries.append(entry)
|
|
755
|
+
|
|
756
|
+
return pcie_ats_entries
|
|
757
|
+
|
|
758
|
+
def _parse_nicctl_port(self, output: str) -> List[PensandoNicPort]:
|
|
759
|
+
"""Parse 'nicctl show port' output into PensandoNicPort objects.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
output: Raw output from 'nicctl show port' command
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
List of PensandoNicPort objects
|
|
766
|
+
"""
|
|
767
|
+
port_entries = []
|
|
768
|
+
current_entry = None
|
|
769
|
+
current_section = None # 'spec' or 'status'
|
|
770
|
+
current_nic_id = None
|
|
771
|
+
current_pcie_bdf = None
|
|
772
|
+
|
|
773
|
+
for line in output.splitlines():
|
|
774
|
+
line_stripped = line.strip()
|
|
775
|
+
if not line_stripped:
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
# Check for NIC line
|
|
779
|
+
if line_stripped.startswith("NIC") and ":" in line_stripped:
|
|
780
|
+
# Save previous entry if exists
|
|
781
|
+
if current_entry:
|
|
782
|
+
port_entries.append(current_entry)
|
|
783
|
+
current_entry = None
|
|
784
|
+
|
|
785
|
+
# Parse NIC ID and PCIe BDF
|
|
786
|
+
match = re.match(
|
|
787
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)", line_stripped, re.IGNORECASE
|
|
788
|
+
)
|
|
789
|
+
if match:
|
|
790
|
+
current_nic_id = match.group(1)
|
|
791
|
+
current_pcie_bdf = match.group(2)
|
|
792
|
+
continue
|
|
793
|
+
|
|
794
|
+
# Check for Port line
|
|
795
|
+
if (
|
|
796
|
+
line_stripped.startswith("Port")
|
|
797
|
+
and ":" in line_stripped
|
|
798
|
+
and current_nic_id
|
|
799
|
+
and current_pcie_bdf
|
|
800
|
+
):
|
|
801
|
+
# Save previous entry if exists
|
|
802
|
+
if current_entry:
|
|
803
|
+
port_entries.append(current_entry)
|
|
804
|
+
|
|
805
|
+
# Parse Port ID and Port name
|
|
806
|
+
match = re.match(
|
|
807
|
+
r"Port\s*:\s*([a-f0-9\-]+)\s*\(([^\)]+)\)", line_stripped, re.IGNORECASE
|
|
808
|
+
)
|
|
809
|
+
if match:
|
|
810
|
+
port_id = match.group(1)
|
|
811
|
+
port_name = match.group(2)
|
|
812
|
+
current_entry = PensandoNicPort(
|
|
813
|
+
nic_id=current_nic_id,
|
|
814
|
+
pcie_bdf=current_pcie_bdf,
|
|
815
|
+
port_id=port_id,
|
|
816
|
+
port_name=port_name,
|
|
817
|
+
)
|
|
818
|
+
continue
|
|
819
|
+
|
|
820
|
+
# Skip separator lines (dashes)
|
|
821
|
+
if re.match(r"^-+$", line_stripped):
|
|
822
|
+
continue
|
|
823
|
+
|
|
824
|
+
# Check for section headers
|
|
825
|
+
if line_stripped.endswith(":"):
|
|
826
|
+
if line_stripped == "Spec:":
|
|
827
|
+
current_section = "spec"
|
|
828
|
+
elif line_stripped == "Status:":
|
|
829
|
+
current_section = "status"
|
|
830
|
+
continue
|
|
831
|
+
|
|
832
|
+
# Parse fields within current entry and section
|
|
833
|
+
if current_entry and current_section and ":" in line_stripped:
|
|
834
|
+
parts = line_stripped.split(":", 1)
|
|
835
|
+
if len(parts) == 2:
|
|
836
|
+
key = parts[0].strip()
|
|
837
|
+
value = parts[1].strip()
|
|
838
|
+
|
|
839
|
+
if current_section == "spec":
|
|
840
|
+
if key == "Ifindex":
|
|
841
|
+
current_entry.spec_ifindex = value
|
|
842
|
+
elif key == "Type":
|
|
843
|
+
current_entry.spec_type = value
|
|
844
|
+
elif key == "speed":
|
|
845
|
+
current_entry.spec_speed = value
|
|
846
|
+
elif key == "Admin state":
|
|
847
|
+
current_entry.spec_admin_state = value
|
|
848
|
+
elif key == "FEC type":
|
|
849
|
+
current_entry.spec_fec_type = value
|
|
850
|
+
elif key == "Pause type":
|
|
851
|
+
current_entry.spec_pause_type = value
|
|
852
|
+
elif key == "Number of lanes":
|
|
853
|
+
try:
|
|
854
|
+
current_entry.spec_num_lanes = int(value)
|
|
855
|
+
except ValueError:
|
|
856
|
+
pass
|
|
857
|
+
elif key == "MTU":
|
|
858
|
+
try:
|
|
859
|
+
current_entry.spec_mtu = int(value)
|
|
860
|
+
except ValueError:
|
|
861
|
+
pass
|
|
862
|
+
elif key == "TX pause":
|
|
863
|
+
current_entry.spec_tx_pause = value
|
|
864
|
+
elif key == "RX pause":
|
|
865
|
+
current_entry.spec_rx_pause = value
|
|
866
|
+
elif key == "Auto negotiation":
|
|
867
|
+
current_entry.spec_auto_negotiation = value
|
|
868
|
+
elif current_section == "status":
|
|
869
|
+
if key == "Physical port":
|
|
870
|
+
try:
|
|
871
|
+
current_entry.status_physical_port = int(value)
|
|
872
|
+
except ValueError:
|
|
873
|
+
pass
|
|
874
|
+
elif key == "Operational status":
|
|
875
|
+
current_entry.status_operational_status = value
|
|
876
|
+
elif key == "Link FSM state":
|
|
877
|
+
current_entry.status_link_fsm_state = value
|
|
878
|
+
elif key == "FEC type":
|
|
879
|
+
current_entry.status_fec_type = value
|
|
880
|
+
elif key == "Cable type":
|
|
881
|
+
current_entry.status_cable_type = value
|
|
882
|
+
elif key == "Number of lanes":
|
|
883
|
+
try:
|
|
884
|
+
current_entry.status_num_lanes = int(value)
|
|
885
|
+
except ValueError:
|
|
886
|
+
pass
|
|
887
|
+
elif key == "speed":
|
|
888
|
+
current_entry.status_speed = value
|
|
889
|
+
elif key == "Auto negotiation":
|
|
890
|
+
current_entry.status_auto_negotiation = value
|
|
891
|
+
elif key == "MAC ID":
|
|
892
|
+
try:
|
|
893
|
+
current_entry.status_mac_id = int(value)
|
|
894
|
+
except ValueError:
|
|
895
|
+
pass
|
|
896
|
+
elif key == "MAC channel":
|
|
897
|
+
try:
|
|
898
|
+
current_entry.status_mac_channel = int(value)
|
|
899
|
+
except ValueError:
|
|
900
|
+
pass
|
|
901
|
+
elif key == "MAC address":
|
|
902
|
+
current_entry.status_mac_address = value
|
|
903
|
+
elif key == "Transceiver type":
|
|
904
|
+
current_entry.status_transceiver_type = value
|
|
905
|
+
elif key == "Transceiver state":
|
|
906
|
+
current_entry.status_transceiver_state = value
|
|
907
|
+
elif key == "Transceiver PID":
|
|
908
|
+
current_entry.status_transceiver_pid = value
|
|
909
|
+
|
|
910
|
+
# Add the last entry if exists
|
|
911
|
+
if current_entry:
|
|
912
|
+
port_entries.append(current_entry)
|
|
913
|
+
|
|
914
|
+
return port_entries
|
|
915
|
+
|
|
916
|
+
def _parse_nicctl_qos(self, output: str) -> List[PensandoNicQos]:
|
|
917
|
+
"""Parse 'nicctl show qos' output into PensandoNicQos objects.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
output: Raw output from 'nicctl show qos' command
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
List of PensandoNicQos objects
|
|
924
|
+
"""
|
|
925
|
+
qos_entries = []
|
|
926
|
+
current_entry = None
|
|
927
|
+
current_nic_id = None
|
|
928
|
+
current_pcie_bdf = None
|
|
929
|
+
in_scheduling_table = False
|
|
930
|
+
|
|
931
|
+
for line in output.splitlines():
|
|
932
|
+
line_stripped = line.strip()
|
|
933
|
+
if not line_stripped:
|
|
934
|
+
continue
|
|
935
|
+
|
|
936
|
+
# Check for NIC line: "NIC : 42424650-4c32-3533-3330-323934000000 (0000:06:00.0)"
|
|
937
|
+
if line_stripped.startswith("NIC") and ":" in line_stripped:
|
|
938
|
+
# Save previous entry if exists
|
|
939
|
+
if current_entry:
|
|
940
|
+
qos_entries.append(current_entry)
|
|
941
|
+
current_entry = None
|
|
942
|
+
|
|
943
|
+
# Parse NIC ID and PCIe BDF
|
|
944
|
+
match = re.match(
|
|
945
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)", line_stripped, re.IGNORECASE
|
|
946
|
+
)
|
|
947
|
+
if match:
|
|
948
|
+
current_nic_id = match.group(1)
|
|
949
|
+
current_pcie_bdf = match.group(2)
|
|
950
|
+
in_scheduling_table = False
|
|
951
|
+
continue
|
|
952
|
+
|
|
953
|
+
# Check for Port line: "Port : 0490814a-6c40-4242-4242-000011010000"
|
|
954
|
+
if (
|
|
955
|
+
line_stripped.startswith("Port")
|
|
956
|
+
and ":" in line_stripped
|
|
957
|
+
and current_nic_id
|
|
958
|
+
and current_pcie_bdf
|
|
959
|
+
):
|
|
960
|
+
# Save previous entry if exists
|
|
961
|
+
if current_entry:
|
|
962
|
+
qos_entries.append(current_entry)
|
|
963
|
+
|
|
964
|
+
# Parse Port ID
|
|
965
|
+
parts = line_stripped.split(":")
|
|
966
|
+
if len(parts) >= 2:
|
|
967
|
+
port_id = parts[1].strip()
|
|
968
|
+
current_entry = PensandoNicQos(
|
|
969
|
+
nic_id=current_nic_id,
|
|
970
|
+
pcie_bdf=current_pcie_bdf,
|
|
971
|
+
port_id=port_id,
|
|
972
|
+
)
|
|
973
|
+
in_scheduling_table = False
|
|
974
|
+
continue
|
|
975
|
+
|
|
976
|
+
# Skip separator lines (dashes) but don't reset scheduling table flag
|
|
977
|
+
if re.match(r"^-+$", line_stripped):
|
|
978
|
+
continue
|
|
979
|
+
|
|
980
|
+
# Check for section headers
|
|
981
|
+
if current_entry:
|
|
982
|
+
# Classification type
|
|
983
|
+
if "Classification type" in line:
|
|
984
|
+
parts = line_stripped.split(":")
|
|
985
|
+
if len(parts) >= 2:
|
|
986
|
+
current_entry.classification_type = parts[1].strip()
|
|
987
|
+
|
|
988
|
+
# DSCP bitmap
|
|
989
|
+
elif "DSCP bitmap" in line and "==>" in line:
|
|
990
|
+
parts = line_stripped.split("==>")
|
|
991
|
+
if len(parts) >= 2:
|
|
992
|
+
bitmap_part = parts[0].split(":")
|
|
993
|
+
if len(bitmap_part) >= 2:
|
|
994
|
+
current_entry.dscp_bitmap = bitmap_part[1].strip()
|
|
995
|
+
priority_part = parts[1].split(":")
|
|
996
|
+
if len(priority_part) >= 2:
|
|
997
|
+
try:
|
|
998
|
+
current_entry.dscp_priority = int(priority_part[1].strip())
|
|
999
|
+
except ValueError:
|
|
1000
|
+
pass
|
|
1001
|
+
|
|
1002
|
+
# DSCP range
|
|
1003
|
+
elif line_stripped.startswith("DSCP") and "==>" in line and "bitmap" not in line:
|
|
1004
|
+
parts = line_stripped.split("==>")
|
|
1005
|
+
if len(parts) >= 2:
|
|
1006
|
+
dscp_part = parts[0].split(":")
|
|
1007
|
+
if len(dscp_part) >= 2:
|
|
1008
|
+
current_entry.dscp_range = dscp_part[1].strip()
|
|
1009
|
+
priority_part = parts[1].split(":")
|
|
1010
|
+
if len(priority_part) >= 2:
|
|
1011
|
+
try:
|
|
1012
|
+
current_entry.dscp_priority = int(priority_part[1].strip())
|
|
1013
|
+
except ValueError:
|
|
1014
|
+
pass
|
|
1015
|
+
|
|
1016
|
+
# PFC priority bitmap
|
|
1017
|
+
elif "PFC priority bitmap" in line:
|
|
1018
|
+
parts = line_stripped.split(":")
|
|
1019
|
+
if len(parts) >= 2:
|
|
1020
|
+
current_entry.pfc_priority_bitmap = parts[1].strip()
|
|
1021
|
+
|
|
1022
|
+
# PFC no-drop priorities
|
|
1023
|
+
elif "PFC no-drop priorities" in line:
|
|
1024
|
+
parts = line_stripped.split(":")
|
|
1025
|
+
if len(parts) >= 2:
|
|
1026
|
+
current_entry.pfc_no_drop_priorities = parts[1].strip()
|
|
1027
|
+
|
|
1028
|
+
# Scheduling table header
|
|
1029
|
+
elif "Priority" in line and "Scheduling" in line:
|
|
1030
|
+
in_scheduling_table = True
|
|
1031
|
+
continue
|
|
1032
|
+
|
|
1033
|
+
# Parse scheduling table entries
|
|
1034
|
+
elif in_scheduling_table and not line_stripped.startswith("---"):
|
|
1035
|
+
# Try to parse scheduling entry
|
|
1036
|
+
# Format: "0 DWRR 0 N/A"
|
|
1037
|
+
parts = line_stripped.split()
|
|
1038
|
+
if len(parts) >= 2:
|
|
1039
|
+
try:
|
|
1040
|
+
priority = int(parts[0])
|
|
1041
|
+
scheduling_type = parts[1] if len(parts) > 1 else None
|
|
1042
|
+
bandwidth = None
|
|
1043
|
+
rate_limit = None
|
|
1044
|
+
if len(parts) > 2:
|
|
1045
|
+
try:
|
|
1046
|
+
bandwidth = int(parts[2])
|
|
1047
|
+
except ValueError:
|
|
1048
|
+
pass
|
|
1049
|
+
if len(parts) > 3:
|
|
1050
|
+
rate_limit = parts[3]
|
|
1051
|
+
|
|
1052
|
+
sched_entry = PensandoNicQosScheduling(
|
|
1053
|
+
priority=priority,
|
|
1054
|
+
scheduling_type=scheduling_type,
|
|
1055
|
+
bandwidth=bandwidth,
|
|
1056
|
+
rate_limit=rate_limit,
|
|
1057
|
+
)
|
|
1058
|
+
current_entry.scheduling.append(sched_entry)
|
|
1059
|
+
except (ValueError, IndexError):
|
|
1060
|
+
pass
|
|
1061
|
+
|
|
1062
|
+
# Add the last entry if exists
|
|
1063
|
+
if current_entry:
|
|
1064
|
+
qos_entries.append(current_entry)
|
|
1065
|
+
|
|
1066
|
+
return qos_entries
|
|
1067
|
+
|
|
1068
|
+
def _parse_nicctl_rdma_statistics(self, output: str) -> List[PensandoNicRdmaStatistics]:
|
|
1069
|
+
"""Parse 'nicctl show rdma statistics' output into PensandoNicRdmaStatistics objects.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
output: Raw output from 'nicctl show rdma statistics' command
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
List of PensandoNicRdmaStatistics objects
|
|
1076
|
+
"""
|
|
1077
|
+
rdma_stats_entries = []
|
|
1078
|
+
current_entry = None
|
|
1079
|
+
in_statistics_table = False
|
|
1080
|
+
|
|
1081
|
+
for line in output.splitlines():
|
|
1082
|
+
line_stripped = line.strip()
|
|
1083
|
+
if not line_stripped:
|
|
1084
|
+
continue
|
|
1085
|
+
|
|
1086
|
+
# Check for NIC line: "NIC : 42424650-4c32-3533-3330-323934000000 (0000:06:00.0)"
|
|
1087
|
+
if line_stripped.startswith("NIC") and ":" in line_stripped:
|
|
1088
|
+
# Save previous entry if exists
|
|
1089
|
+
if current_entry:
|
|
1090
|
+
rdma_stats_entries.append(current_entry)
|
|
1091
|
+
|
|
1092
|
+
# Parse NIC ID and PCIe BDF
|
|
1093
|
+
match = re.match(
|
|
1094
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)", line_stripped, re.IGNORECASE
|
|
1095
|
+
)
|
|
1096
|
+
if match:
|
|
1097
|
+
nic_id = match.group(1)
|
|
1098
|
+
pcie_bdf = match.group(2)
|
|
1099
|
+
current_entry = PensandoNicRdmaStatistics(
|
|
1100
|
+
nic_id=nic_id,
|
|
1101
|
+
pcie_bdf=pcie_bdf,
|
|
1102
|
+
)
|
|
1103
|
+
in_statistics_table = False
|
|
1104
|
+
continue
|
|
1105
|
+
|
|
1106
|
+
# Skip separator lines (dashes)
|
|
1107
|
+
if re.match(r"^-+$", line_stripped):
|
|
1108
|
+
continue
|
|
1109
|
+
|
|
1110
|
+
# Check for table header
|
|
1111
|
+
if "Name" in line and "Count" in line:
|
|
1112
|
+
in_statistics_table = True
|
|
1113
|
+
continue
|
|
1114
|
+
|
|
1115
|
+
# Parse statistics entries
|
|
1116
|
+
if current_entry and in_statistics_table:
|
|
1117
|
+
# The format is: "Queue pair create 1"
|
|
1118
|
+
# We need to split from the right to get the count
|
|
1119
|
+
parts = line_stripped.rsplit(None, 1) # Split from right, max 1 split
|
|
1120
|
+
if len(parts) == 2:
|
|
1121
|
+
name = parts[0].strip()
|
|
1122
|
+
count_str = parts[1].strip()
|
|
1123
|
+
try:
|
|
1124
|
+
count = int(count_str)
|
|
1125
|
+
stat_entry = PensandoNicRdmaStatistic(
|
|
1126
|
+
name=name,
|
|
1127
|
+
count=count,
|
|
1128
|
+
)
|
|
1129
|
+
current_entry.statistics.append(stat_entry)
|
|
1130
|
+
except ValueError:
|
|
1131
|
+
pass
|
|
1132
|
+
|
|
1133
|
+
# Add the last entry if exists
|
|
1134
|
+
if current_entry:
|
|
1135
|
+
rdma_stats_entries.append(current_entry)
|
|
1136
|
+
|
|
1137
|
+
return rdma_stats_entries
|
|
1138
|
+
|
|
1139
|
+
def _parse_nicctl_version_host_software(
|
|
1140
|
+
self, output: str
|
|
1141
|
+
) -> Optional[PensandoNicVersionHostSoftware]:
|
|
1142
|
+
"""Parse 'nicctl show version host-software' output into PensandoNicVersionHostSoftware object.
|
|
1143
|
+
|
|
1144
|
+
Args:
|
|
1145
|
+
output: Raw output from 'nicctl show version host-software' command
|
|
1146
|
+
|
|
1147
|
+
Returns:
|
|
1148
|
+
PensandoNicVersionHostSoftware object or None if no data found
|
|
1149
|
+
"""
|
|
1150
|
+
version_info = PensandoNicVersionHostSoftware()
|
|
1151
|
+
found_data = False
|
|
1152
|
+
|
|
1153
|
+
for line in output.splitlines():
|
|
1154
|
+
line_stripped = line.strip()
|
|
1155
|
+
if not line_stripped or ":" not in line_stripped:
|
|
1156
|
+
continue
|
|
1157
|
+
|
|
1158
|
+
# Split on the first colon to get key and value
|
|
1159
|
+
parts = line_stripped.split(":", 1)
|
|
1160
|
+
if len(parts) != 2:
|
|
1161
|
+
continue
|
|
1162
|
+
|
|
1163
|
+
key = parts[0].strip().lower()
|
|
1164
|
+
value = parts[1].strip()
|
|
1165
|
+
|
|
1166
|
+
if "nicctl" in key:
|
|
1167
|
+
version_info.nicctl = value
|
|
1168
|
+
found_data = True
|
|
1169
|
+
elif "ipc driver" in key or "ipc_driver" in key:
|
|
1170
|
+
version_info.ipc_driver = value
|
|
1171
|
+
found_data = True
|
|
1172
|
+
elif "ionic driver" in key or "ionic_driver" in key:
|
|
1173
|
+
version_info.ionic_driver = value
|
|
1174
|
+
found_data = True
|
|
1175
|
+
|
|
1176
|
+
return version_info if found_data else None
|
|
1177
|
+
|
|
1178
|
+
def _parse_nicctl_version_firmware(self, output: str) -> List[PensandoNicVersionFirmware]:
|
|
1179
|
+
"""Parse 'nicctl show version firmware' output into PensandoNicVersionFirmware objects.
|
|
1180
|
+
|
|
1181
|
+
Args:
|
|
1182
|
+
output: Raw output from 'nicctl show version firmware' command
|
|
1183
|
+
|
|
1184
|
+
Returns:
|
|
1185
|
+
List of PensandoNicVersionFirmware objects
|
|
1186
|
+
"""
|
|
1187
|
+
firmware_entries = []
|
|
1188
|
+
current_entry = None
|
|
1189
|
+
|
|
1190
|
+
for line in output.splitlines():
|
|
1191
|
+
line_stripped = line.strip()
|
|
1192
|
+
if not line_stripped:
|
|
1193
|
+
continue
|
|
1194
|
+
|
|
1195
|
+
# Skip separator lines (dashes)
|
|
1196
|
+
if re.match(r"^-+$", line_stripped):
|
|
1197
|
+
# Save previous entry when we hit a separator
|
|
1198
|
+
if current_entry:
|
|
1199
|
+
firmware_entries.append(current_entry)
|
|
1200
|
+
current_entry = None
|
|
1201
|
+
continue
|
|
1202
|
+
|
|
1203
|
+
# Check for NIC line
|
|
1204
|
+
if line_stripped.startswith("NIC") and ":" in line_stripped:
|
|
1205
|
+
# Save previous entry if exists
|
|
1206
|
+
if current_entry:
|
|
1207
|
+
firmware_entries.append(current_entry)
|
|
1208
|
+
|
|
1209
|
+
# Parse NIC ID and PCIe BDF
|
|
1210
|
+
match = re.match(
|
|
1211
|
+
r"NIC\s*:\s*([a-f0-9\-]+)\s*\(([0-9a-f:\.]+)\)", line_stripped, re.IGNORECASE
|
|
1212
|
+
)
|
|
1213
|
+
if match:
|
|
1214
|
+
nic_id = match.group(1)
|
|
1215
|
+
pcie_bdf = match.group(2)
|
|
1216
|
+
current_entry = PensandoNicVersionFirmware(
|
|
1217
|
+
nic_id=nic_id,
|
|
1218
|
+
pcie_bdf=pcie_bdf,
|
|
1219
|
+
)
|
|
1220
|
+
continue
|
|
1221
|
+
|
|
1222
|
+
# Parse version fields
|
|
1223
|
+
if current_entry and ":" in line_stripped:
|
|
1224
|
+
parts = line_stripped.split(":", 1)
|
|
1225
|
+
if len(parts) == 2:
|
|
1226
|
+
key = parts[0].strip().lower()
|
|
1227
|
+
value = parts[1].strip()
|
|
1228
|
+
|
|
1229
|
+
if "cpld" in key:
|
|
1230
|
+
current_entry.cpld = value
|
|
1231
|
+
elif "boot0" in key:
|
|
1232
|
+
current_entry.boot0 = value
|
|
1233
|
+
elif "uboot-a" in key or "uboot_a" in key:
|
|
1234
|
+
current_entry.uboot_a = value
|
|
1235
|
+
elif "firmware-a" in key or "firmware_a" in key:
|
|
1236
|
+
current_entry.firmware_a = value
|
|
1237
|
+
elif (
|
|
1238
|
+
"device config-a" in key
|
|
1239
|
+
or "device_config_a" in key
|
|
1240
|
+
or "device config" in key
|
|
1241
|
+
):
|
|
1242
|
+
current_entry.device_config_a = value
|
|
1243
|
+
|
|
1244
|
+
# Add the last entry if exists
|
|
1245
|
+
if current_entry:
|
|
1246
|
+
firmware_entries.append(current_entry)
|
|
1247
|
+
|
|
1248
|
+
return firmware_entries
|
|
1249
|
+
|
|
1250
|
+
def _parse_niccli_qos(self, device_num: int, output: str) -> BroadcomNicQos:
|
|
1251
|
+
"""Parse 'niccli --dev X qos --ets --show' output into BroadcomNicQos object.
|
|
1252
|
+
|
|
1253
|
+
Args:
|
|
1254
|
+
device_num: Device number
|
|
1255
|
+
output: Raw output from 'niccli --dev X qos --ets --show' command
|
|
1256
|
+
|
|
1257
|
+
Returns:
|
|
1258
|
+
BroadcomNicQos object with parsed data
|
|
1259
|
+
"""
|
|
1260
|
+
qos_info = BroadcomNicQos(device_num=device_num, raw_output=output)
|
|
1261
|
+
|
|
1262
|
+
current_app_entry = None
|
|
1263
|
+
|
|
1264
|
+
for line in output.splitlines():
|
|
1265
|
+
line_stripped = line.strip()
|
|
1266
|
+
if not line_stripped:
|
|
1267
|
+
continue
|
|
1268
|
+
|
|
1269
|
+
# Parse PRIO_MAP: "PRIO_MAP: 0:0 1:0 2:0 3:1 4:0 5:0 6:0 7:2"
|
|
1270
|
+
if "PRIO_MAP:" in line:
|
|
1271
|
+
parts = line.split("PRIO_MAP:")
|
|
1272
|
+
if len(parts) >= 2:
|
|
1273
|
+
prio_entries = parts[1].strip().split()
|
|
1274
|
+
for entry in prio_entries:
|
|
1275
|
+
if ":" in entry:
|
|
1276
|
+
prio, tc = entry.split(":")
|
|
1277
|
+
try:
|
|
1278
|
+
qos_info.prio_map[int(prio)] = int(tc)
|
|
1279
|
+
except ValueError:
|
|
1280
|
+
pass
|
|
1281
|
+
|
|
1282
|
+
# Parse TC Bandwidth: "TC Bandwidth: 50% 50% 0%"
|
|
1283
|
+
elif "TC Bandwidth:" in line:
|
|
1284
|
+
parts = line.split("TC Bandwidth:")
|
|
1285
|
+
if len(parts) >= 2:
|
|
1286
|
+
bandwidth_entries = parts[1].strip().split()
|
|
1287
|
+
for bw in bandwidth_entries:
|
|
1288
|
+
bw_clean = bw.rstrip("%")
|
|
1289
|
+
try:
|
|
1290
|
+
qos_info.tc_bandwidth.append(int(bw_clean))
|
|
1291
|
+
except ValueError:
|
|
1292
|
+
pass
|
|
1293
|
+
|
|
1294
|
+
# Parse TSA_MAP: "TSA_MAP: 0:ets 1:ets 2:strict"
|
|
1295
|
+
elif "TSA_MAP:" in line:
|
|
1296
|
+
parts = line.split("TSA_MAP:")
|
|
1297
|
+
if len(parts) >= 2:
|
|
1298
|
+
tsa_entries = parts[1].strip().split()
|
|
1299
|
+
for entry in tsa_entries:
|
|
1300
|
+
if ":" in entry:
|
|
1301
|
+
tc, tsa = entry.split(":", 1)
|
|
1302
|
+
try:
|
|
1303
|
+
qos_info.tsa_map[int(tc)] = tsa
|
|
1304
|
+
except ValueError:
|
|
1305
|
+
pass
|
|
1306
|
+
|
|
1307
|
+
# Parse PFC enabled: "PFC enabled: 3"
|
|
1308
|
+
elif "PFC enabled:" in line:
|
|
1309
|
+
parts = line.split("PFC enabled:")
|
|
1310
|
+
if len(parts) >= 2:
|
|
1311
|
+
try:
|
|
1312
|
+
qos_info.pfc_enabled = int(parts[1].strip())
|
|
1313
|
+
except ValueError:
|
|
1314
|
+
pass
|
|
1315
|
+
|
|
1316
|
+
# Parse APP entries - detect start of new APP entry
|
|
1317
|
+
elif line_stripped.startswith("APP#"):
|
|
1318
|
+
# Save previous entry if exists
|
|
1319
|
+
if current_app_entry:
|
|
1320
|
+
qos_info.app_entries.append(current_app_entry)
|
|
1321
|
+
current_app_entry = BroadcomNicQosAppEntry()
|
|
1322
|
+
|
|
1323
|
+
# Parse Priority within APP entry
|
|
1324
|
+
elif "Priority:" in line and current_app_entry is not None:
|
|
1325
|
+
parts = line.split("Priority:")
|
|
1326
|
+
if len(parts) >= 2:
|
|
1327
|
+
try:
|
|
1328
|
+
current_app_entry.priority = int(parts[1].strip())
|
|
1329
|
+
except ValueError:
|
|
1330
|
+
pass
|
|
1331
|
+
|
|
1332
|
+
# Parse Sel within APP entry
|
|
1333
|
+
elif "Sel:" in line and current_app_entry is not None:
|
|
1334
|
+
parts = line.split("Sel:")
|
|
1335
|
+
if len(parts) >= 2:
|
|
1336
|
+
try:
|
|
1337
|
+
current_app_entry.sel = int(parts[1].strip())
|
|
1338
|
+
except ValueError:
|
|
1339
|
+
pass
|
|
1340
|
+
|
|
1341
|
+
# Parse DSCP within APP entry
|
|
1342
|
+
elif "DSCP:" in line and current_app_entry is not None:
|
|
1343
|
+
parts = line.split("DSCP:")
|
|
1344
|
+
if len(parts) >= 2:
|
|
1345
|
+
try:
|
|
1346
|
+
current_app_entry.dscp = int(parts[1].strip())
|
|
1347
|
+
except ValueError:
|
|
1348
|
+
pass
|
|
1349
|
+
|
|
1350
|
+
# Parse protocol and port (e.g., "UDP or DCCP: 4791")
|
|
1351
|
+
elif (
|
|
1352
|
+
"UDP" in line or "TCP" in line or "DCCP" in line
|
|
1353
|
+
) and current_app_entry is not None:
|
|
1354
|
+
if ":" in line:
|
|
1355
|
+
parts = line.split(":")
|
|
1356
|
+
if len(parts) >= 2:
|
|
1357
|
+
current_app_entry.protocol = parts[0].strip()
|
|
1358
|
+
try:
|
|
1359
|
+
current_app_entry.port = int(parts[1].strip())
|
|
1360
|
+
except ValueError:
|
|
1361
|
+
pass
|
|
1362
|
+
|
|
1363
|
+
# Parse TC Rate Limit: "TC Rate Limit: 100% 100% 100% 0% 0% 0% 0% 0%"
|
|
1364
|
+
elif "TC Rate Limit:" in line:
|
|
1365
|
+
parts = line.split("TC Rate Limit:")
|
|
1366
|
+
if len(parts) >= 2:
|
|
1367
|
+
rate_entries = parts[1].strip().split()
|
|
1368
|
+
for rate in rate_entries:
|
|
1369
|
+
rate_clean = rate.rstrip("%")
|
|
1370
|
+
try:
|
|
1371
|
+
qos_info.tc_rate_limit.append(int(rate_clean))
|
|
1372
|
+
except ValueError:
|
|
1373
|
+
pass
|
|
1374
|
+
|
|
1375
|
+
# Add the last APP entry if exists
|
|
1376
|
+
if current_app_entry:
|
|
1377
|
+
qos_info.app_entries.append(current_app_entry)
|
|
1378
|
+
|
|
1379
|
+
return qos_info
|
|
1380
|
+
|
|
1381
|
+
def _collect_ethtool_info(self, interfaces: List[NetworkInterface]) -> Dict[str, EthtoolInfo]:
|
|
1382
|
+
"""Collect ethtool information for all network interfaces.
|
|
1383
|
+
|
|
1384
|
+
Args:
|
|
1385
|
+
interfaces: List of NetworkInterface objects to collect ethtool info for
|
|
1386
|
+
|
|
1387
|
+
Returns:
|
|
1388
|
+
Dictionary mapping interface name to EthtoolInfo
|
|
1389
|
+
"""
|
|
1390
|
+
ethtool_data = {}
|
|
1391
|
+
|
|
1392
|
+
for iface in interfaces:
|
|
1393
|
+
cmd = self.CMD_ETHTOOL_TEMPLATE.format(interface=iface.name)
|
|
1394
|
+
res_ethtool = self._run_sut_cmd(cmd, sudo=True)
|
|
1395
|
+
|
|
1396
|
+
if res_ethtool.exit_code == 0:
|
|
1397
|
+
ethtool_info = self._parse_ethtool(iface.name, res_ethtool.stdout)
|
|
1398
|
+
ethtool_data[iface.name] = ethtool_info
|
|
1399
|
+
self._log_event(
|
|
1400
|
+
category=EventCategory.NETWORK,
|
|
1401
|
+
description=f"Collected ethtool info for interface: {iface.name}",
|
|
1402
|
+
priority=EventPriority.INFO,
|
|
1403
|
+
)
|
|
1404
|
+
else:
|
|
1405
|
+
self._log_event(
|
|
1406
|
+
category=EventCategory.NETWORK,
|
|
1407
|
+
description=f"Error collecting ethtool info for interface: {iface.name}",
|
|
1408
|
+
data={"command": res_ethtool.command, "exit_code": res_ethtool.exit_code},
|
|
1409
|
+
priority=EventPriority.WARNING,
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
return ethtool_data
|
|
1413
|
+
|
|
1414
|
+
def _collect_lldp_info(self) -> None:
|
|
1415
|
+
"""Collect LLDP information using lldpcli and lldpctl commands."""
|
|
1416
|
+
# Run lldpcli show neighbor
|
|
1417
|
+
res_lldpcli = self._run_sut_cmd(self.CMD_LLDPCLI_NEIGHBOR, sudo=True)
|
|
1418
|
+
if res_lldpcli.exit_code == 0:
|
|
1419
|
+
self._log_event(
|
|
1420
|
+
category=EventCategory.NETWORK,
|
|
1421
|
+
description="Collected LLDP neighbor information (lldpcli)",
|
|
1422
|
+
priority=EventPriority.INFO,
|
|
1423
|
+
)
|
|
1424
|
+
else:
|
|
1425
|
+
self._log_event(
|
|
1426
|
+
category=EventCategory.NETWORK,
|
|
1427
|
+
description="LLDP neighbor collection failed or lldpcli not available",
|
|
1428
|
+
data={"command": res_lldpcli.command, "exit_code": res_lldpcli.exit_code},
|
|
1429
|
+
priority=EventPriority.INFO,
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
# Run lldpctl
|
|
1433
|
+
res_lldpctl = self._run_sut_cmd(self.CMD_LLDPCTL, sudo=True)
|
|
1434
|
+
if res_lldpctl.exit_code == 0:
|
|
1435
|
+
self._log_event(
|
|
1436
|
+
category=EventCategory.NETWORK,
|
|
1437
|
+
description="Collected LLDP information (lldpctl)",
|
|
1438
|
+
priority=EventPriority.INFO,
|
|
1439
|
+
)
|
|
1440
|
+
else:
|
|
1441
|
+
self._log_event(
|
|
1442
|
+
category=EventCategory.NETWORK,
|
|
1443
|
+
description="LLDP collection failed or lldpctl not available",
|
|
1444
|
+
data={"command": res_lldpctl.command, "exit_code": res_lldpctl.exit_code},
|
|
1445
|
+
priority=EventPriority.INFO,
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
def _collect_broadcom_nic_info(
|
|
1449
|
+
self,
|
|
1450
|
+
) -> Tuple[List[BroadcomNicDevice], Dict[int, BroadcomNicQos]]:
|
|
1451
|
+
"""Collect Broadcom NIC information using niccli commands.
|
|
1452
|
+
|
|
1453
|
+
Returns:
|
|
1454
|
+
Tuple of (list of BroadcomNicDevice, dict mapping device number to BroadcomNicQos)
|
|
1455
|
+
"""
|
|
1456
|
+
devices = []
|
|
1457
|
+
qos_data = {}
|
|
1458
|
+
|
|
1459
|
+
# First, list devices
|
|
1460
|
+
res_listdev = self._run_sut_cmd(self.CMD_NICCLI_LISTDEV, sudo=True)
|
|
1461
|
+
if res_listdev.exit_code == 0:
|
|
1462
|
+
# Parse device list
|
|
1463
|
+
devices = self._parse_niccli_listdev(res_listdev.stdout)
|
|
1464
|
+
self._log_event(
|
|
1465
|
+
category=EventCategory.NETWORK,
|
|
1466
|
+
description=f"Collected Broadcom NIC device list: {len(devices)} devices",
|
|
1467
|
+
priority=EventPriority.INFO,
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
# Collect QoS info for each device
|
|
1471
|
+
for device in devices:
|
|
1472
|
+
cmd = self.CMD_NICCLI_GETQOS_TEMPLATE.format(device_num=device.device_num)
|
|
1473
|
+
res_qos = self._run_sut_cmd(cmd, sudo=True)
|
|
1474
|
+
if res_qos.exit_code == 0:
|
|
1475
|
+
qos_info = self._parse_niccli_qos(device.device_num, res_qos.stdout)
|
|
1476
|
+
qos_data[device.device_num] = qos_info
|
|
1477
|
+
self._log_event(
|
|
1478
|
+
category=EventCategory.NETWORK,
|
|
1479
|
+
description=f"Collected Broadcom NIC QoS info for device {device.device_num}",
|
|
1480
|
+
priority=EventPriority.INFO,
|
|
1481
|
+
)
|
|
1482
|
+
else:
|
|
1483
|
+
self._log_event(
|
|
1484
|
+
category=EventCategory.NETWORK,
|
|
1485
|
+
description=f"Failed to collect QoS info for device {device.device_num}",
|
|
1486
|
+
data={"command": res_qos.command, "exit_code": res_qos.exit_code},
|
|
1487
|
+
priority=EventPriority.WARNING,
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
if qos_data:
|
|
1491
|
+
self._log_event(
|
|
1492
|
+
category=EventCategory.NETWORK,
|
|
1493
|
+
description=f"Collected Broadcom NIC QoS info for {len(qos_data)} devices",
|
|
1494
|
+
priority=EventPriority.INFO,
|
|
1495
|
+
)
|
|
1496
|
+
else:
|
|
1497
|
+
self._log_event(
|
|
1498
|
+
category=EventCategory.NETWORK,
|
|
1499
|
+
description="Broadcom NIC collection failed or niccli not available",
|
|
1500
|
+
data={"command": res_listdev.command, "exit_code": res_listdev.exit_code},
|
|
1501
|
+
priority=EventPriority.INFO,
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
return devices, qos_data
|
|
1505
|
+
|
|
1506
|
+
def _collect_pensando_nic_info(
|
|
1507
|
+
self,
|
|
1508
|
+
) -> Tuple[
|
|
1509
|
+
List[PensandoNicCard],
|
|
1510
|
+
List[PensandoNicDcqcn],
|
|
1511
|
+
List[PensandoNicEnvironment],
|
|
1512
|
+
List[PensandoNicPcieAts],
|
|
1513
|
+
List[PensandoNicPort],
|
|
1514
|
+
List[PensandoNicQos],
|
|
1515
|
+
List[PensandoNicRdmaStatistics],
|
|
1516
|
+
Optional[PensandoNicVersionHostSoftware],
|
|
1517
|
+
List[PensandoNicVersionFirmware],
|
|
1518
|
+
List[str],
|
|
1519
|
+
]:
|
|
1520
|
+
"""Collect Pensando NIC information using nicctl commands.
|
|
1521
|
+
|
|
1522
|
+
Returns:
|
|
1523
|
+
Tuple of (list of PensandoNicCard, list of PensandoNicDcqcn,
|
|
1524
|
+
list of PensandoNicEnvironment, list of PensandoNicPcieAts,
|
|
1525
|
+
list of PensandoNicPort, list of PensandoNicQos,
|
|
1526
|
+
list of PensandoNicRdmaStatistics,
|
|
1527
|
+
PensandoNicVersionHostSoftware object,
|
|
1528
|
+
list of PensandoNicVersionFirmware,
|
|
1529
|
+
list of uncollected command names)
|
|
1530
|
+
"""
|
|
1531
|
+
cards = []
|
|
1532
|
+
dcqcn_entries = []
|
|
1533
|
+
environment_entries = []
|
|
1534
|
+
pcie_ats_entries = []
|
|
1535
|
+
port_entries = []
|
|
1536
|
+
qos_entries = []
|
|
1537
|
+
rdma_statistics_entries = []
|
|
1538
|
+
version_host_software = None
|
|
1539
|
+
version_firmware_entries = []
|
|
1540
|
+
|
|
1541
|
+
# Track which commands failed
|
|
1542
|
+
uncollected_commands = []
|
|
1543
|
+
|
|
1544
|
+
# Parse nicctl show card output
|
|
1545
|
+
res_card = self._run_sut_cmd(self.CMD_NICCTL_CARD, sudo=True)
|
|
1546
|
+
if res_card.exit_code == 0:
|
|
1547
|
+
cards = self._parse_nicctl_card(res_card.stdout)
|
|
1548
|
+
self._log_event(
|
|
1549
|
+
category=EventCategory.NETWORK,
|
|
1550
|
+
description=f"Collected Pensando NIC card list: {len(cards)} cards",
|
|
1551
|
+
priority=EventPriority.INFO,
|
|
1552
|
+
)
|
|
1553
|
+
else:
|
|
1554
|
+
uncollected_commands.append(self.CMD_NICCTL_CARD)
|
|
1555
|
+
|
|
1556
|
+
# Parse nicctl show dcqcn output
|
|
1557
|
+
res_dcqcn = self._run_sut_cmd(self.CMD_NICCTL_DCQCN, sudo=True)
|
|
1558
|
+
if res_dcqcn.exit_code == 0:
|
|
1559
|
+
dcqcn_entries = self._parse_nicctl_dcqcn(res_dcqcn.stdout)
|
|
1560
|
+
self._log_event(
|
|
1561
|
+
category=EventCategory.NETWORK,
|
|
1562
|
+
description=f"Collected Pensando NIC DCQCN info: {len(dcqcn_entries)} entries",
|
|
1563
|
+
priority=EventPriority.INFO,
|
|
1564
|
+
)
|
|
1565
|
+
else:
|
|
1566
|
+
uncollected_commands.append(self.CMD_NICCTL_DCQCN)
|
|
1567
|
+
|
|
1568
|
+
# Parse nicctl show environment output
|
|
1569
|
+
res_environment = self._run_sut_cmd(self.CMD_NICCTL_ENVIRONMENT, sudo=True)
|
|
1570
|
+
if res_environment.exit_code == 0:
|
|
1571
|
+
environment_entries = self._parse_nicctl_environment(res_environment.stdout)
|
|
1572
|
+
self._log_event(
|
|
1573
|
+
category=EventCategory.NETWORK,
|
|
1574
|
+
description=f"Collected Pensando NIC environment info: {len(environment_entries)} entries",
|
|
1575
|
+
priority=EventPriority.INFO,
|
|
1576
|
+
)
|
|
1577
|
+
else:
|
|
1578
|
+
uncollected_commands.append(self.CMD_NICCTL_ENVIRONMENT)
|
|
1579
|
+
|
|
1580
|
+
# Parse nicctl show pcie ats output
|
|
1581
|
+
res_pcie_ats = self._run_sut_cmd(self.CMD_NICCTL_PCIE_ATS, sudo=True)
|
|
1582
|
+
if res_pcie_ats.exit_code == 0:
|
|
1583
|
+
pcie_ats_entries = self._parse_nicctl_pcie_ats(res_pcie_ats.stdout)
|
|
1584
|
+
self._log_event(
|
|
1585
|
+
category=EventCategory.NETWORK,
|
|
1586
|
+
description=f"Collected Pensando NIC PCIe ATS info: {len(pcie_ats_entries)} entries",
|
|
1587
|
+
priority=EventPriority.INFO,
|
|
1588
|
+
)
|
|
1589
|
+
else:
|
|
1590
|
+
uncollected_commands.append(self.CMD_NICCTL_PCIE_ATS)
|
|
1591
|
+
|
|
1592
|
+
# Parse nicctl show port output
|
|
1593
|
+
res_port = self._run_sut_cmd(self.CMD_NICCTL_PORT, sudo=True)
|
|
1594
|
+
if res_port.exit_code == 0:
|
|
1595
|
+
port_entries = self._parse_nicctl_port(res_port.stdout)
|
|
1596
|
+
self._log_event(
|
|
1597
|
+
category=EventCategory.NETWORK,
|
|
1598
|
+
description=f"Collected Pensando NIC port info: {len(port_entries)} ports",
|
|
1599
|
+
priority=EventPriority.INFO,
|
|
1600
|
+
)
|
|
1601
|
+
else:
|
|
1602
|
+
uncollected_commands.append(self.CMD_NICCTL_PORT)
|
|
1603
|
+
|
|
1604
|
+
# Parse nicctl show qos output
|
|
1605
|
+
res_qos = self._run_sut_cmd(self.CMD_NICCTL_QOS, sudo=True)
|
|
1606
|
+
if res_qos.exit_code == 0:
|
|
1607
|
+
qos_entries = self._parse_nicctl_qos(res_qos.stdout)
|
|
1608
|
+
self._log_event(
|
|
1609
|
+
category=EventCategory.NETWORK,
|
|
1610
|
+
description=f"Collected Pensando NIC QoS info: {len(qos_entries)} entries",
|
|
1611
|
+
priority=EventPriority.INFO,
|
|
1612
|
+
)
|
|
1613
|
+
else:
|
|
1614
|
+
uncollected_commands.append(self.CMD_NICCTL_QOS)
|
|
1615
|
+
|
|
1616
|
+
# Parse nicctl show rdma statistics output
|
|
1617
|
+
res_rdma_stats = self._run_sut_cmd(self.CMD_NICCTL_RDMA_STATISTICS, sudo=True)
|
|
1618
|
+
if res_rdma_stats.exit_code == 0:
|
|
1619
|
+
rdma_statistics_entries = self._parse_nicctl_rdma_statistics(res_rdma_stats.stdout)
|
|
1620
|
+
self._log_event(
|
|
1621
|
+
category=EventCategory.NETWORK,
|
|
1622
|
+
description=f"Collected Pensando NIC RDMA statistics: {len(rdma_statistics_entries)} entries",
|
|
1623
|
+
priority=EventPriority.INFO,
|
|
1624
|
+
)
|
|
1625
|
+
else:
|
|
1626
|
+
uncollected_commands.append(self.CMD_NICCTL_RDMA_STATISTICS)
|
|
1627
|
+
|
|
1628
|
+
# Parse nicctl show version host-software output
|
|
1629
|
+
res_version_host = self._run_sut_cmd(self.CMD_NICCTL_VERSION_HOST_SOFTWARE, sudo=True)
|
|
1630
|
+
if res_version_host.exit_code == 0:
|
|
1631
|
+
version_host_software = self._parse_nicctl_version_host_software(
|
|
1632
|
+
res_version_host.stdout
|
|
1633
|
+
)
|
|
1634
|
+
if version_host_software:
|
|
1635
|
+
self._log_event(
|
|
1636
|
+
category=EventCategory.NETWORK,
|
|
1637
|
+
description="Collected Pensando NIC host software version",
|
|
1638
|
+
priority=EventPriority.INFO,
|
|
1639
|
+
)
|
|
1640
|
+
else:
|
|
1641
|
+
uncollected_commands.append(self.CMD_NICCTL_VERSION_HOST_SOFTWARE)
|
|
1642
|
+
else:
|
|
1643
|
+
uncollected_commands.append(self.CMD_NICCTL_VERSION_HOST_SOFTWARE)
|
|
1644
|
+
|
|
1645
|
+
# Parse nicctl show version firmware output
|
|
1646
|
+
res_version_firmware = self._run_sut_cmd(self.CMD_NICCTL_VERSION_FIRMWARE, sudo=True)
|
|
1647
|
+
if res_version_firmware.exit_code == 0:
|
|
1648
|
+
version_firmware_entries = self._parse_nicctl_version_firmware(
|
|
1649
|
+
res_version_firmware.stdout
|
|
1650
|
+
)
|
|
1651
|
+
self._log_event(
|
|
1652
|
+
category=EventCategory.NETWORK,
|
|
1653
|
+
description=f"Collected Pensando NIC firmware versions: {len(version_firmware_entries)} entries",
|
|
1654
|
+
priority=EventPriority.INFO,
|
|
1655
|
+
)
|
|
1656
|
+
else:
|
|
1657
|
+
uncollected_commands.append(self.CMD_NICCTL_VERSION_FIRMWARE)
|
|
1658
|
+
|
|
1659
|
+
return (
|
|
1660
|
+
cards,
|
|
1661
|
+
dcqcn_entries,
|
|
1662
|
+
environment_entries,
|
|
1663
|
+
pcie_ats_entries,
|
|
1664
|
+
port_entries,
|
|
1665
|
+
qos_entries,
|
|
1666
|
+
rdma_statistics_entries,
|
|
1667
|
+
version_host_software,
|
|
1668
|
+
version_firmware_entries,
|
|
1669
|
+
uncollected_commands,
|
|
1670
|
+
)
|
|
1671
|
+
|
|
1672
|
+
def collect_data(
|
|
1673
|
+
self,
|
|
1674
|
+
args=None,
|
|
1675
|
+
) -> Tuple[TaskResult, Optional[NetworkDataModel]]:
|
|
1676
|
+
"""Collect network configuration from the system.
|
|
1677
|
+
|
|
1678
|
+
Returns:
|
|
1679
|
+
Tuple[TaskResult, Optional[NetworkDataModel]]: tuple containing the task result
|
|
1680
|
+
and an instance of NetworkDataModel or None if collection failed.
|
|
1681
|
+
"""
|
|
1682
|
+
interfaces = []
|
|
1683
|
+
routes = []
|
|
1684
|
+
rules = []
|
|
1685
|
+
neighbors = []
|
|
1686
|
+
ethtool_data = {}
|
|
1687
|
+
broadcom_devices: List[BroadcomNicDevice] = []
|
|
1688
|
+
broadcom_qos_data: Dict[int, BroadcomNicQos] = {}
|
|
1689
|
+
pensando_cards: List[PensandoNicCard] = []
|
|
1690
|
+
pensando_dcqcn: List[PensandoNicDcqcn] = []
|
|
1691
|
+
pensando_environment: List[PensandoNicEnvironment] = []
|
|
1692
|
+
pensando_pcie_ats: List[PensandoNicPcieAts] = []
|
|
1693
|
+
pensando_ports: List[PensandoNicPort] = []
|
|
1694
|
+
pensando_qos: List[PensandoNicQos] = []
|
|
1695
|
+
pensando_rdma_statistics: List[PensandoNicRdmaStatistics] = []
|
|
1696
|
+
pensando_version_host_software: Optional[PensandoNicVersionHostSoftware] = None
|
|
1697
|
+
pensando_version_firmware: List[PensandoNicVersionFirmware] = []
|
|
1698
|
+
|
|
1699
|
+
# Collect interface/address information
|
|
1700
|
+
res_addr = self._run_sut_cmd(self.CMD_ADDR)
|
|
1701
|
+
if res_addr.exit_code == 0:
|
|
1702
|
+
interfaces = self._parse_ip_addr(res_addr.stdout)
|
|
1703
|
+
self._log_event(
|
|
1704
|
+
category=EventCategory.NETWORK,
|
|
1705
|
+
description=f"Collected {len(interfaces)} network interfaces",
|
|
1706
|
+
priority=EventPriority.INFO,
|
|
1707
|
+
)
|
|
1708
|
+
else:
|
|
1709
|
+
self._log_event(
|
|
1710
|
+
category=EventCategory.NETWORK,
|
|
1711
|
+
description="Error collecting network interfaces",
|
|
1712
|
+
data={"command": res_addr.command, "exit_code": res_addr.exit_code},
|
|
1713
|
+
priority=EventPriority.ERROR,
|
|
1714
|
+
console_log=True,
|
|
1715
|
+
)
|
|
1716
|
+
|
|
1717
|
+
# Collect ethtool information for interfaces
|
|
1718
|
+
if interfaces:
|
|
1719
|
+
ethtool_data = self._collect_ethtool_info(interfaces)
|
|
1720
|
+
self._log_event(
|
|
1721
|
+
category=EventCategory.NETWORK,
|
|
1722
|
+
description=f"Collected ethtool info for {len(ethtool_data)} interfaces",
|
|
1723
|
+
priority=EventPriority.INFO,
|
|
1724
|
+
)
|
|
1725
|
+
|
|
1726
|
+
# Collect routing table
|
|
1727
|
+
res_route = self._run_sut_cmd(self.CMD_ROUTE)
|
|
1728
|
+
if res_route.exit_code == 0:
|
|
1729
|
+
routes = self._parse_ip_route(res_route.stdout)
|
|
1730
|
+
self._log_event(
|
|
1731
|
+
category=EventCategory.NETWORK,
|
|
1732
|
+
description=f"Collected {len(routes)} routes",
|
|
1733
|
+
priority=EventPriority.INFO,
|
|
1734
|
+
)
|
|
1735
|
+
else:
|
|
1736
|
+
self._log_event(
|
|
1737
|
+
category=EventCategory.NETWORK,
|
|
1738
|
+
description="Error collecting routes",
|
|
1739
|
+
data={"command": res_route.command, "exit_code": res_route.exit_code},
|
|
1740
|
+
priority=EventPriority.WARNING,
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
# Collect routing rules
|
|
1744
|
+
res_rule = self._run_sut_cmd(self.CMD_RULE)
|
|
1745
|
+
if res_rule.exit_code == 0:
|
|
1746
|
+
rules = self._parse_ip_rule(res_rule.stdout)
|
|
1747
|
+
self._log_event(
|
|
1748
|
+
category=EventCategory.NETWORK,
|
|
1749
|
+
description=f"Collected {len(rules)} routing rules",
|
|
1750
|
+
priority=EventPriority.INFO,
|
|
1751
|
+
)
|
|
1752
|
+
else:
|
|
1753
|
+
self._log_event(
|
|
1754
|
+
category=EventCategory.NETWORK,
|
|
1755
|
+
description="Error collecting routing rules",
|
|
1756
|
+
data={"command": res_rule.command, "exit_code": res_rule.exit_code},
|
|
1757
|
+
priority=EventPriority.WARNING,
|
|
1758
|
+
)
|
|
1759
|
+
|
|
1760
|
+
# Collect neighbor table (ARP/NDP)
|
|
1761
|
+
res_neighbor = self._run_sut_cmd(self.CMD_NEIGHBOR)
|
|
1762
|
+
if res_neighbor.exit_code == 0:
|
|
1763
|
+
neighbors = self._parse_ip_neighbor(res_neighbor.stdout)
|
|
1764
|
+
self._log_event(
|
|
1765
|
+
category=EventCategory.NETWORK,
|
|
1766
|
+
description=f"Collected {len(neighbors)} neighbor entries",
|
|
1767
|
+
priority=EventPriority.INFO,
|
|
1768
|
+
)
|
|
1769
|
+
else:
|
|
1770
|
+
self._log_event(
|
|
1771
|
+
category=EventCategory.NETWORK,
|
|
1772
|
+
description="Error collecting neighbor table",
|
|
1773
|
+
data={"command": res_neighbor.command, "exit_code": res_neighbor.exit_code},
|
|
1774
|
+
priority=EventPriority.WARNING,
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
# Collect LLDP information
|
|
1778
|
+
self._collect_lldp_info()
|
|
1779
|
+
|
|
1780
|
+
# Collect Broadcom NIC information
|
|
1781
|
+
broadcom_devices, broadcom_qos_data = self._collect_broadcom_nic_info()
|
|
1782
|
+
|
|
1783
|
+
# Collect Pensando NIC information
|
|
1784
|
+
(
|
|
1785
|
+
pensando_cards,
|
|
1786
|
+
pensando_dcqcn,
|
|
1787
|
+
pensando_environment,
|
|
1788
|
+
pensando_pcie_ats,
|
|
1789
|
+
pensando_ports,
|
|
1790
|
+
pensando_qos,
|
|
1791
|
+
pensando_rdma_statistics,
|
|
1792
|
+
pensando_version_host_software,
|
|
1793
|
+
pensando_version_firmware,
|
|
1794
|
+
uncollected_commands,
|
|
1795
|
+
) = self._collect_pensando_nic_info()
|
|
1796
|
+
|
|
1797
|
+
# Log summary of uncollected commands or success
|
|
1798
|
+
if uncollected_commands:
|
|
1799
|
+
self.result.message = "Network data collection failed"
|
|
1800
|
+
self._log_event(
|
|
1801
|
+
category=EventCategory.NETWORK,
|
|
1802
|
+
description=f"Failed to collect {len(uncollected_commands)} nicctl commands: {', '.join(uncollected_commands)}",
|
|
1803
|
+
priority=EventPriority.WARNING,
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
else:
|
|
1807
|
+
self.result.message = "Network data collected successfully"
|
|
1808
|
+
|
|
1809
|
+
network_data = NetworkDataModel(
|
|
1810
|
+
interfaces=interfaces,
|
|
1811
|
+
routes=routes,
|
|
1812
|
+
rules=rules,
|
|
1813
|
+
neighbors=neighbors,
|
|
1814
|
+
ethtool_info=ethtool_data,
|
|
1815
|
+
broadcom_nic_devices=broadcom_devices,
|
|
1816
|
+
broadcom_nic_qos=broadcom_qos_data,
|
|
1817
|
+
pensando_nic_cards=pensando_cards,
|
|
1818
|
+
pensando_nic_dcqcn=pensando_dcqcn,
|
|
1819
|
+
pensando_nic_environment=pensando_environment,
|
|
1820
|
+
pensando_nic_pcie_ats=pensando_pcie_ats,
|
|
1821
|
+
pensando_nic_ports=pensando_ports,
|
|
1822
|
+
pensando_nic_qos=pensando_qos,
|
|
1823
|
+
pensando_nic_rdma_statistics=pensando_rdma_statistics,
|
|
1824
|
+
pensando_nic_version_host_software=pensando_version_host_software,
|
|
1825
|
+
pensando_nic_version_firmware=pensando_version_firmware,
|
|
1826
|
+
)
|
|
1827
|
+
self.result.status = ExecutionStatus.OK
|
|
1828
|
+
return self.result, network_data
|