2323# SOFTWARE.
2424#
2525###############################################################################
26+ import json
2627import os
2728import re
2829from typing import Optional
3233from nodescraper .base import InBandDataCollector
3334from nodescraper .enums import EventCategory , EventPriority , ExecutionStatus , OSFamily
3435from nodescraper .models import TaskResult
36+ from nodescraper .utils import bytes_to_human_readable , str_or_none
3537
36- from .nvmedata import NvmeDataModel
38+ from .nvmedata import NvmeDataModel , NvmeListEntry
3739
3840
3941class NvmeCollector (InBandDataCollector [NvmeDataModel , None ]):
4042 """Collect NVMe details from the system."""
4143
4244 DATA_MODEL = NvmeDataModel
45+ CMD_LINUX_LIST_JSON = "nvme list -o json"
4346 CMD_LINUX = {
4447 "smart_log" : "nvme smart-log {dev}" ,
4548 "error_log" : "nvme error-log {dev} --log-entries=256" ,
@@ -54,6 +57,15 @@ class NvmeCollector(InBandDataCollector[NvmeDataModel, None]):
5457
5558 TELEMETRY_FILENAME = "telemetry_log.bin"
5659
60+ def _check_nvme_cli_installed (self ) -> bool :
61+ """Check if the nvme CLI is installed on the system.
62+
63+ Returns:
64+ bool: True if nvme is available, False otherwise.
65+ """
66+ res = self ._run_sut_cmd ("which nvme" )
67+ return res .exit_code == 0 and bool (res .stdout .strip ())
68+
5769 def collect_data (
5870 self ,
5971 args = None ,
@@ -73,6 +85,18 @@ def collect_data(
7385 self .result .status = ExecutionStatus .NOT_RAN
7486 return self .result , None
7587
88+ if not self ._check_nvme_cli_installed ():
89+ self ._log_event (
90+ category = EventCategory .SW_DRIVER ,
91+ description = "nvme CLI not found; install nvme-cli to collect NVMe data" ,
92+ priority = EventPriority .WARNING ,
93+ )
94+ self .result .message = "nvme CLI not found; NVMe collection skipped"
95+ self .result .status = ExecutionStatus .NOT_RAN
96+ return self .result , None
97+
98+ nvme_list_entries = self ._collect_nvme_list_entries ()
99+
76100 nvme_devices = self ._get_nvme_devices ()
77101 if not nvme_devices :
78102 self ._log_event (
@@ -115,7 +139,7 @@ def collect_data(
115139
116140 if all_device_data :
117141 try :
118- nvme_data = NvmeDataModel (devices = all_device_data )
142+ nvme_data = NvmeDataModel (nvme_list = nvme_list_entries , devices = all_device_data )
119143 except ValidationError as exp :
120144 self ._log_event (
121145 category = EventCategory .SW_DRIVER ,
@@ -130,7 +154,10 @@ def collect_data(
130154 self ._log_event (
131155 category = EventCategory .SW_DRIVER ,
132156 description = "Collected NVMe data" ,
133- data = nvme_data .model_dump (),
157+ data = {
158+ "devices" : list (nvme_data .devices .keys ()),
159+ "nvme_list_entries" : len (nvme_data .nvme_list or []),
160+ },
134161 priority = EventPriority .INFO ,
135162 )
136163 self .result .message = "NVMe data successfully collected"
@@ -147,6 +174,140 @@ def collect_data(
147174 self .result .status = ExecutionStatus .ERROR
148175 return self .result , None
149176
177+ def _collect_nvme_list_entries (self ) -> Optional [list [NvmeListEntry ]]:
178+ """Run 'nvme list -o json' and parse output into list of NvmeListEntry."""
179+ res = self ._run_sut_cmd (self .CMD_LINUX_LIST_JSON , sudo = False )
180+ if res .exit_code == 0 and res .stdout :
181+ entries = self ._parse_nvme_list_json (res .stdout .strip ())
182+ if not entries :
183+ self ._log_event (
184+ category = EventCategory .SW_DRIVER ,
185+ description = "Parsing of 'nvme list -o json' output failed (no entries from nested or flat format)" ,
186+ priority = EventPriority .WARNING ,
187+ )
188+ return entries
189+ return None
190+
191+ def _parse_nvme_list_json (self , raw : str ) -> list [NvmeListEntry ]:
192+ """Parse 'nvme list -o json' output into NvmeListEntry list.
193+
194+ Supports two formats:
195+ - Nested: Devices[] -> Subsystems[] -> Controllers[] -> Namespaces[].
196+ - Flat: Devices[] where each element has DevicePath, SerialNumber, ModelNumber, etc.
197+ """
198+ try :
199+ data = json .loads (raw )
200+ except json .JSONDecodeError :
201+ return []
202+ devices = data .get ("Devices" , []) if isinstance (data , dict ) else []
203+ if not isinstance (devices , list ):
204+ return []
205+ entries = self ._parse_nvme_list_nested (devices )
206+ if not entries and devices :
207+ entries = self ._parse_nvme_list_flat (devices )
208+ return entries
209+
210+ def _parse_nvme_list_flat (self , devices : list ) -> list [NvmeListEntry ]:
211+ """Parse flat 'nvme list -o json' format (one object per namespace in Devices[])."""
212+ entries = []
213+ for dev in devices :
214+ if not isinstance (dev , dict ):
215+ continue
216+ if dev .get ("DevicePath" ) is None and dev .get ("SerialNumber" ) is None :
217+ continue
218+ node = str_or_none (dev .get ("DevicePath" ))
219+ generic_path = str_or_none (dev .get ("GenericPath" ))
220+ serial_number = str_or_none (dev .get ("SerialNumber" ))
221+ model = str_or_none (dev .get ("ModelNumber" ))
222+ fw_rev = str_or_none (dev .get ("Firmware" ))
223+ name_space = dev .get ("NameSpace" ) or dev .get ("NameSpaceId" )
224+ nsid = name_space if name_space is not None else dev .get ("NSID" )
225+ namespace_id = (
226+ f"0x{ int (nsid ):x} " if isinstance (nsid , (int , float )) else str_or_none (nsid )
227+ )
228+ used_bytes = dev .get ("UsedBytes" )
229+ physical_size = dev .get ("PhysicalSize" )
230+ sector_size = dev .get ("SectorSize" )
231+ if isinstance (used_bytes , (int , float )) and isinstance (physical_size , (int , float )):
232+ usage = (
233+ f"{ bytes_to_human_readable (int (used_bytes ))} / "
234+ f"{ bytes_to_human_readable (int (physical_size ))} "
235+ )
236+ else :
237+ usage = None
238+ format_lba = f"{ sector_size } B + 0 B" if sector_size is not None else None
239+ entries .append (
240+ NvmeListEntry (
241+ node = node ,
242+ generic = generic_path ,
243+ serial_number = serial_number ,
244+ model = model ,
245+ namespace_id = namespace_id ,
246+ usage = usage ,
247+ format_lba = format_lba ,
248+ fw_rev = fw_rev ,
249+ )
250+ )
251+ return entries
252+
253+ def _parse_nvme_list_nested (self , devices : list ) -> list [NvmeListEntry ]:
254+ """Parse nested 'nvme list -o json' format (Devices -> Subsystems -> Controllers -> Namespaces)."""
255+ entries = []
256+ for dev in devices :
257+ if not isinstance (dev , dict ):
258+ continue
259+ subsystems = dev .get ("Subsystems" ) or []
260+ for subsys in subsystems :
261+ if not isinstance (subsys , dict ):
262+ continue
263+ controllers = subsys .get ("Controllers" ) or []
264+ for ctrl in controllers :
265+ if not isinstance (ctrl , dict ):
266+ continue
267+ serial_number = str_or_none (ctrl .get ("SerialNumber" ))
268+ model = str_or_none (ctrl .get ("ModelNumber" ))
269+ fw_rev = str_or_none (ctrl .get ("Firmware" ))
270+ namespaces = ctrl .get ("Namespaces" ) or []
271+ for ns in namespaces :
272+ if not isinstance (ns , dict ):
273+ continue
274+ name_space = ns .get ("NameSpace" ) or ns .get ("NameSpaceId" )
275+ generic = ns .get ("Generic" )
276+ nsid = ns .get ("NSID" )
277+ used_bytes = ns .get ("UsedBytes" )
278+ physical_size = ns .get ("PhysicalSize" )
279+ sector_size = ns .get ("SectorSize" )
280+ node = f"/dev/{ name_space } " if name_space else None
281+ generic_path = (
282+ f"/dev/{ generic } " if (generic and str (generic ).strip ()) else None
283+ )
284+ namespace_id = f"0x{ nsid :x} " if isinstance (nsid , int ) else str_or_none (nsid )
285+ if isinstance (used_bytes , (int , float )) and isinstance (
286+ physical_size , (int , float )
287+ ):
288+ usage = (
289+ f"{ bytes_to_human_readable (int (used_bytes ))} / "
290+ f"{ bytes_to_human_readable (int (physical_size ))} "
291+ )
292+ else :
293+ usage = None
294+ format_lba = (
295+ f"{ sector_size } B + 0 B" if sector_size is not None else None
296+ )
297+ entries .append (
298+ NvmeListEntry (
299+ node = str_or_none (node ),
300+ generic = str_or_none (generic_path ),
301+ serial_number = serial_number ,
302+ model = model ,
303+ namespace_id = namespace_id ,
304+ usage = usage ,
305+ format_lba = format_lba ,
306+ fw_rev = fw_rev ,
307+ )
308+ )
309+ return entries
310+
150311 def _get_nvme_devices (self ) -> list [str ]:
151312 nvme_devs = []
152313
0 commit comments