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.
Files changed (197) hide show
  1. amd_node_scraper-0.0.1.dist-info/LICENSE +21 -0
  2. amd_node_scraper-0.0.1.dist-info/METADATA +424 -0
  3. amd_node_scraper-0.0.1.dist-info/RECORD +197 -0
  4. amd_node_scraper-0.0.1.dist-info/WHEEL +5 -0
  5. amd_node_scraper-0.0.1.dist-info/entry_points.txt +2 -0
  6. amd_node_scraper-0.0.1.dist-info/top_level.txt +1 -0
  7. nodescraper/__init__.py +32 -0
  8. nodescraper/base/__init__.py +34 -0
  9. nodescraper/base/inbandcollectortask.py +118 -0
  10. nodescraper/base/inbanddataplugin.py +39 -0
  11. nodescraper/base/regexanalyzer.py +120 -0
  12. nodescraper/cli/__init__.py +29 -0
  13. nodescraper/cli/cli.py +511 -0
  14. nodescraper/cli/constants.py +27 -0
  15. nodescraper/cli/dynamicparserbuilder.py +171 -0
  16. nodescraper/cli/helper.py +517 -0
  17. nodescraper/cli/inputargtypes.py +129 -0
  18. nodescraper/configbuilder.py +123 -0
  19. nodescraper/configregistry.py +66 -0
  20. nodescraper/configs/node_status.json +19 -0
  21. nodescraper/connection/__init__.py +25 -0
  22. nodescraper/connection/inband/__init__.py +46 -0
  23. nodescraper/connection/inband/inband.py +171 -0
  24. nodescraper/connection/inband/inbandlocal.py +93 -0
  25. nodescraper/connection/inband/inbandmanager.py +151 -0
  26. nodescraper/connection/inband/inbandremote.py +173 -0
  27. nodescraper/connection/inband/sshparams.py +43 -0
  28. nodescraper/constants.py +26 -0
  29. nodescraper/enums/__init__.py +40 -0
  30. nodescraper/enums/eventcategory.py +89 -0
  31. nodescraper/enums/eventpriority.py +42 -0
  32. nodescraper/enums/executionstatus.py +44 -0
  33. nodescraper/enums/osfamily.py +34 -0
  34. nodescraper/enums/systeminteraction.py +41 -0
  35. nodescraper/enums/systemlocation.py +33 -0
  36. nodescraper/generictypes.py +36 -0
  37. nodescraper/interfaces/__init__.py +44 -0
  38. nodescraper/interfaces/connectionmanager.py +143 -0
  39. nodescraper/interfaces/dataanalyzertask.py +138 -0
  40. nodescraper/interfaces/datacollectortask.py +185 -0
  41. nodescraper/interfaces/dataplugin.py +356 -0
  42. nodescraper/interfaces/plugin.py +127 -0
  43. nodescraper/interfaces/resultcollator.py +56 -0
  44. nodescraper/interfaces/task.py +164 -0
  45. nodescraper/interfaces/taskresulthook.py +39 -0
  46. nodescraper/models/__init__.py +48 -0
  47. nodescraper/models/analyzerargs.py +93 -0
  48. nodescraper/models/collectorargs.py +30 -0
  49. nodescraper/models/connectionconfig.py +34 -0
  50. nodescraper/models/datamodel.py +171 -0
  51. nodescraper/models/datapluginresult.py +39 -0
  52. nodescraper/models/event.py +158 -0
  53. nodescraper/models/pluginconfig.py +38 -0
  54. nodescraper/models/pluginresult.py +39 -0
  55. nodescraper/models/systeminfo.py +44 -0
  56. nodescraper/models/taskresult.py +185 -0
  57. nodescraper/models/timerangeargs.py +38 -0
  58. nodescraper/pluginexecutor.py +274 -0
  59. nodescraper/pluginregistry.py +152 -0
  60. nodescraper/plugins/__init__.py +25 -0
  61. nodescraper/plugins/inband/__init__.py +25 -0
  62. nodescraper/plugins/inband/amdsmi/__init__.py +28 -0
  63. nodescraper/plugins/inband/amdsmi/amdsmi_analyzer.py +821 -0
  64. nodescraper/plugins/inband/amdsmi/amdsmi_collector.py +1313 -0
  65. nodescraper/plugins/inband/amdsmi/amdsmi_plugin.py +43 -0
  66. nodescraper/plugins/inband/amdsmi/amdsmidata.py +1002 -0
  67. nodescraper/plugins/inband/amdsmi/analyzer_args.py +50 -0
  68. nodescraper/plugins/inband/amdsmi/cper.py +65 -0
  69. nodescraper/plugins/inband/bios/__init__.py +29 -0
  70. nodescraper/plugins/inband/bios/analyzer_args.py +64 -0
  71. nodescraper/plugins/inband/bios/bios_analyzer.py +93 -0
  72. nodescraper/plugins/inband/bios/bios_collector.py +93 -0
  73. nodescraper/plugins/inband/bios/bios_plugin.py +43 -0
  74. nodescraper/plugins/inband/bios/biosdata.py +30 -0
  75. nodescraper/plugins/inband/cmdline/__init__.py +25 -0
  76. nodescraper/plugins/inband/cmdline/analyzer_args.py +80 -0
  77. nodescraper/plugins/inband/cmdline/cmdline_analyzer.py +113 -0
  78. nodescraper/plugins/inband/cmdline/cmdline_collector.py +77 -0
  79. nodescraper/plugins/inband/cmdline/cmdline_plugin.py +43 -0
  80. nodescraper/plugins/inband/cmdline/cmdlinedata.py +30 -0
  81. nodescraper/plugins/inband/device_enumeration/__init__.py +29 -0
  82. nodescraper/plugins/inband/device_enumeration/analyzer_args.py +73 -0
  83. nodescraper/plugins/inband/device_enumeration/device_enumeration_analyzer.py +81 -0
  84. nodescraper/plugins/inband/device_enumeration/device_enumeration_collector.py +176 -0
  85. nodescraper/plugins/inband/device_enumeration/device_enumeration_plugin.py +45 -0
  86. nodescraper/plugins/inband/device_enumeration/deviceenumdata.py +36 -0
  87. nodescraper/plugins/inband/dimm/__init__.py +25 -0
  88. nodescraper/plugins/inband/dimm/collector_args.py +31 -0
  89. nodescraper/plugins/inband/dimm/dimm_collector.py +151 -0
  90. nodescraper/plugins/inband/dimm/dimm_plugin.py +40 -0
  91. nodescraper/plugins/inband/dimm/dimmdata.py +30 -0
  92. nodescraper/plugins/inband/dkms/__init__.py +25 -0
  93. nodescraper/plugins/inband/dkms/analyzer_args.py +85 -0
  94. nodescraper/plugins/inband/dkms/dkms_analyzer.py +106 -0
  95. nodescraper/plugins/inband/dkms/dkms_collector.py +76 -0
  96. nodescraper/plugins/inband/dkms/dkms_plugin.py +43 -0
  97. nodescraper/plugins/inband/dkms/dkmsdata.py +33 -0
  98. nodescraper/plugins/inband/dmesg/__init__.py +28 -0
  99. nodescraper/plugins/inband/dmesg/analyzer_args.py +33 -0
  100. nodescraper/plugins/inband/dmesg/collector_args.py +39 -0
  101. nodescraper/plugins/inband/dmesg/dmesg_analyzer.py +503 -0
  102. nodescraper/plugins/inband/dmesg/dmesg_collector.py +164 -0
  103. nodescraper/plugins/inband/dmesg/dmesg_plugin.py +44 -0
  104. nodescraper/plugins/inband/dmesg/dmesgdata.py +116 -0
  105. nodescraper/plugins/inband/fabrics/__init__.py +28 -0
  106. nodescraper/plugins/inband/fabrics/fabrics_collector.py +726 -0
  107. nodescraper/plugins/inband/fabrics/fabrics_plugin.py +37 -0
  108. nodescraper/plugins/inband/fabrics/fabricsdata.py +140 -0
  109. nodescraper/plugins/inband/journal/__init__.py +28 -0
  110. nodescraper/plugins/inband/journal/collector_args.py +33 -0
  111. nodescraper/plugins/inband/journal/journal_collector.py +107 -0
  112. nodescraper/plugins/inband/journal/journal_plugin.py +40 -0
  113. nodescraper/plugins/inband/journal/journaldata.py +44 -0
  114. nodescraper/plugins/inband/kernel/__init__.py +25 -0
  115. nodescraper/plugins/inband/kernel/analyzer_args.py +64 -0
  116. nodescraper/plugins/inband/kernel/kernel_analyzer.py +91 -0
  117. nodescraper/plugins/inband/kernel/kernel_collector.py +129 -0
  118. nodescraper/plugins/inband/kernel/kernel_plugin.py +43 -0
  119. nodescraper/plugins/inband/kernel/kerneldata.py +32 -0
  120. nodescraper/plugins/inband/kernel_module/__init__.py +25 -0
  121. nodescraper/plugins/inband/kernel_module/analyzer_args.py +59 -0
  122. nodescraper/plugins/inband/kernel_module/kernel_module_analyzer.py +211 -0
  123. nodescraper/plugins/inband/kernel_module/kernel_module_collector.py +264 -0
  124. nodescraper/plugins/inband/kernel_module/kernel_module_data.py +60 -0
  125. nodescraper/plugins/inband/kernel_module/kernel_module_plugin.py +43 -0
  126. nodescraper/plugins/inband/memory/__init__.py +25 -0
  127. nodescraper/plugins/inband/memory/analyzer_args.py +45 -0
  128. nodescraper/plugins/inband/memory/memory_analyzer.py +98 -0
  129. nodescraper/plugins/inband/memory/memory_collector.py +330 -0
  130. nodescraper/plugins/inband/memory/memory_plugin.py +43 -0
  131. nodescraper/plugins/inband/memory/memorydata.py +90 -0
  132. nodescraper/plugins/inband/network/__init__.py +28 -0
  133. nodescraper/plugins/inband/network/network_collector.py +1828 -0
  134. nodescraper/plugins/inband/network/network_plugin.py +37 -0
  135. nodescraper/plugins/inband/network/networkdata.py +319 -0
  136. nodescraper/plugins/inband/nvme/__init__.py +28 -0
  137. nodescraper/plugins/inband/nvme/nvme_collector.py +167 -0
  138. nodescraper/plugins/inband/nvme/nvme_plugin.py +37 -0
  139. nodescraper/plugins/inband/nvme/nvmedata.py +45 -0
  140. nodescraper/plugins/inband/os/__init__.py +25 -0
  141. nodescraper/plugins/inband/os/analyzer_args.py +64 -0
  142. nodescraper/plugins/inband/os/os_analyzer.py +73 -0
  143. nodescraper/plugins/inband/os/os_collector.py +131 -0
  144. nodescraper/plugins/inband/os/os_plugin.py +43 -0
  145. nodescraper/plugins/inband/os/osdata.py +31 -0
  146. nodescraper/plugins/inband/package/__init__.py +25 -0
  147. nodescraper/plugins/inband/package/analyzer_args.py +48 -0
  148. nodescraper/plugins/inband/package/package_analyzer.py +253 -0
  149. nodescraper/plugins/inband/package/package_collector.py +273 -0
  150. nodescraper/plugins/inband/package/package_plugin.py +43 -0
  151. nodescraper/plugins/inband/package/packagedata.py +41 -0
  152. nodescraper/plugins/inband/pcie/__init__.py +29 -0
  153. nodescraper/plugins/inband/pcie/analyzer_args.py +63 -0
  154. nodescraper/plugins/inband/pcie/pcie_analyzer.py +1081 -0
  155. nodescraper/plugins/inband/pcie/pcie_collector.py +690 -0
  156. nodescraper/plugins/inband/pcie/pcie_data.py +2017 -0
  157. nodescraper/plugins/inband/pcie/pcie_plugin.py +43 -0
  158. nodescraper/plugins/inband/process/__init__.py +25 -0
  159. nodescraper/plugins/inband/process/analyzer_args.py +45 -0
  160. nodescraper/plugins/inband/process/collector_args.py +31 -0
  161. nodescraper/plugins/inband/process/process_analyzer.py +91 -0
  162. nodescraper/plugins/inband/process/process_collector.py +115 -0
  163. nodescraper/plugins/inband/process/process_plugin.py +46 -0
  164. nodescraper/plugins/inband/process/processdata.py +34 -0
  165. nodescraper/plugins/inband/rocm/__init__.py +25 -0
  166. nodescraper/plugins/inband/rocm/analyzer_args.py +66 -0
  167. nodescraper/plugins/inband/rocm/rocm_analyzer.py +100 -0
  168. nodescraper/plugins/inband/rocm/rocm_collector.py +205 -0
  169. nodescraper/plugins/inband/rocm/rocm_plugin.py +43 -0
  170. nodescraper/plugins/inband/rocm/rocmdata.py +62 -0
  171. nodescraper/plugins/inband/storage/__init__.py +25 -0
  172. nodescraper/plugins/inband/storage/analyzer_args.py +38 -0
  173. nodescraper/plugins/inband/storage/collector_args.py +31 -0
  174. nodescraper/plugins/inband/storage/storage_analyzer.py +152 -0
  175. nodescraper/plugins/inband/storage/storage_collector.py +110 -0
  176. nodescraper/plugins/inband/storage/storage_plugin.py +44 -0
  177. nodescraper/plugins/inband/storage/storagedata.py +70 -0
  178. nodescraper/plugins/inband/sysctl/__init__.py +29 -0
  179. nodescraper/plugins/inband/sysctl/analyzer_args.py +67 -0
  180. nodescraper/plugins/inband/sysctl/sysctl_analyzer.py +81 -0
  181. nodescraper/plugins/inband/sysctl/sysctl_collector.py +101 -0
  182. nodescraper/plugins/inband/sysctl/sysctl_plugin.py +43 -0
  183. nodescraper/plugins/inband/sysctl/sysctldata.py +42 -0
  184. nodescraper/plugins/inband/syslog/__init__.py +28 -0
  185. nodescraper/plugins/inband/syslog/syslog_collector.py +121 -0
  186. nodescraper/plugins/inband/syslog/syslog_plugin.py +37 -0
  187. nodescraper/plugins/inband/syslog/syslogdata.py +46 -0
  188. nodescraper/plugins/inband/uptime/__init__.py +25 -0
  189. nodescraper/plugins/inband/uptime/uptime_collector.py +88 -0
  190. nodescraper/plugins/inband/uptime/uptime_plugin.py +37 -0
  191. nodescraper/plugins/inband/uptime/uptimedata.py +31 -0
  192. nodescraper/resultcollators/__init__.py +25 -0
  193. nodescraper/resultcollators/tablesummary.py +159 -0
  194. nodescraper/taskresulthooks/__init__.py +28 -0
  195. nodescraper/taskresulthooks/filesystemloghook.py +88 -0
  196. nodescraper/typeutils.py +171 -0
  197. 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