Skip to content

Commit dfc5566

Browse files
Fix nvme device path fetch logic
1 parent a65190f commit dfc5566

File tree

1 file changed

+102
-2
lines changed

1 file changed

+102
-2
lines changed

lisa/tools/nvmecli.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,108 @@ def get_devices(self, force_run: bool = False) -> Any:
125125
return nvme_devices["Devices"]
126126

127127
def get_disks(self, force_run: bool = False) -> List[str]:
128-
nvme_devices = self.get_devices(force_run=force_run)
129-
return [device["DevicePath"] for device in nvme_devices]
128+
"""
129+
Return NVMe device nodes/paths (`/dev/...`) robustly across nvme-cli schemas.
130+
131+
Upstream change context:
132+
- nvme-cli reworked `nvme list -o json` around v2.11, removing the
133+
legacy top-level `.Devices[].DevicePath` and nesting device info under:
134+
Subsystems → Controllers → Namespaces.
135+
- Reference discussion and breakage report:
136+
https://github.com/linux-nvme/nvme-cli/issues/2749
137+
(thread points to commit 929f461 as the change introducing the new JSON)
138+
- Some distro builds (e.g., certain RHEL package revisions) may still
139+
emit `DevicePath`. This logic supports both.
140+
141+
- jq option is implemented to simplify parsing, but a pure-Python
142+
fallback is also provided if `jq` is not available on the target system.
143+
jq option is more efficient and robust, so it is preferred when possible.
144+
Returns:
145+
List[str]: device nodes/paths like `/dev/nvme0n1`
146+
"""
147+
148+
check = self.node.execute(
149+
"command -v jq", shell=True, sudo=True, no_error_log=True
150+
)
151+
# a new tool for jq can also be written if used frequently
152+
if check.exit_code != 0 or not (check.stdout or "").strip():
153+
use_jq = False
154+
else:
155+
use_jq = True
156+
157+
if use_jq:
158+
# -------------------------------
159+
# jq pipeline (more efficient)
160+
# -------------------------------
161+
# Rationale: First prefer legacy `.DevicePath` if present, else build
162+
# `/dev/<NameSpace>` from the new nested schema.
163+
# Reference: https://github.com/linux-nvme/nvme-cli/issues/2749
164+
165+
cmd = r"""list -o json 2>/dev/null | jq -r '
166+
.Devices[]? as $d |
167+
if ($d | has("DevicePath")) and ($d.DevicePath != null) then
168+
# Legacy / RHEL-patched builds: use the flat field directly
169+
$d.DevicePath
170+
else
171+
# Newer schema: Subsystems → Controllers → Namespaces → NameSpace
172+
[$d.Subsystems[]? | .Controllers[]? | .Namespaces[]?
173+
| "/dev/" + (.NameSpace // "")]
174+
| map(select(length > 5)) # drop empties like "/dev/"
175+
| .[]
176+
end
177+
'"""
178+
179+
result = self.run(
180+
cmd,
181+
shell=True,
182+
sudo=True,
183+
force_run=force_run,
184+
no_error_log=True,
185+
)
186+
187+
device_paths = [
188+
ln.strip() for ln in (result.stdout or "").splitlines() if ln.strip()
189+
]
190+
if not device_paths:
191+
raise LisaException(
192+
"No NVMe devices found. The jq-based pipeline returned no paths."
193+
)
194+
return device_paths
195+
else:
196+
# -------------------------------
197+
# Pure Python (when jq is not available)
198+
# -------------------------------
199+
nvme_devices = self.get_devices(force_run=force_run) # raw ["Devices"]
200+
device_paths = []
201+
202+
def _add(path: str) -> None:
203+
if isinstance(path, str) and path.startswith("/dev/") and len(path) > 5:
204+
device_paths.append(path)
205+
206+
for nvme_device in nvme_devices or []:
207+
# Legacy schema (flat fields):
208+
_add(nvme_device.get("DevicePath"))
209+
_add(nvme_device.get("GenericPath"))
210+
211+
# New schema: Subsystems → Controllers → Namespaces
212+
for subsystem in nvme_device.get("Subsystems") or []:
213+
for controller in (subsystem or {}).get("Controllers") or []:
214+
for namespace in (controller or {}).get("Namespaces") or []:
215+
namespace_name = namespace.get(
216+
"NameSpace"
217+
) # e.g., "nvme0n1"
218+
generic_name = namespace.get("Generic") # e.g., "ng0n1"
219+
if isinstance(namespace_name, str) and namespace_name:
220+
_add(f"/dev/{namespace_name}")
221+
if isinstance(generic_name, str) and generic_name:
222+
_add(f"/dev/{generic_name}")
223+
224+
device_paths = sorted(set(device_paths))
225+
if not device_paths:
226+
raise LisaException(
227+
"No NVMe device nodes could be derived from 'nvme list -o json'."
228+
)
229+
return device_paths
130230

131231
# NVME namespace ids are unique for each disk under any NVME controller.
132232
# These are useful in detecting the lun id of the remote azure disk disks.

0 commit comments

Comments
 (0)