From d7fe806eec047e0a9e77d873d207a3b184743563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 3 Apr 2025 04:33:15 +0200 Subject: [PATCH] qvm-pci: optionally list SBDF of listed devices SBDF aka resolved path is what most interfaces use, including lspci, sysfs etc. Include that in listing too, to ease user's life. Note that 'qvm-pci info' already includes that info. QubesOS/qubes-issues#8681 --- doc/manpages/qvm-device.rst | 5 +++ qubesadmin/tests/tools/qvm_device.py | 42 +++++++++++++++++++++++-- qubesadmin/tools/qvm_device.py | 46 ++++++++++++++++++++++------ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/doc/manpages/qvm-device.rst b/doc/manpages/qvm-device.rst index b015c1d8..cbd2f404 100644 --- a/doc/manpages/qvm-device.rst +++ b/doc/manpages/qvm-device.rst @@ -53,6 +53,11 @@ List devices. Include info about device assignments, indicated by '*' before qube name. +.. option:: --with-sbdf, --resolve-paths + + For PCI devices list also resolved device path (SBDF). This eases looking up the device with other tools like lspci. + The option is ignored when listing non-PCI devices. + .. option:: --all List devices from all qubes. You can use :option:`--exclude` to limit the diff --git a/qubesadmin/tests/tools/qvm_device.py b/qubesadmin/tests/tools/qvm_device.py index b6c88be6..65fd18f2 100644 --- a/qubesadmin/tests/tools/qvm_device.py +++ b/qubesadmin/tests/tools/qvm_device.py @@ -35,9 +35,12 @@ class TC_00_qvm_device(qubesadmin.tests.QubesTestCase): """ Tests the output logic of the qvm-device tool """ - def expected_device_call(self, vm, action, returned=b"0\0"): + def expected_device_call( + self, vm, action, returned=b"0\0", klass="testclass" + ): self.app.expected_calls[ - (vm, f'admin.vm.device.testclass.{action}', None, None)] = returned + (vm, f"admin.vm.device.{klass}.{action}", None, None) + ] = returned def setUp(self): super().setUp() @@ -174,6 +177,41 @@ def test_003_list_device_classes(self): 'pci\nusb\n' ) + def test_004_list_pci_with_sbdf(self): + """ + List PCI devices with SBDF info. + """ + self.app.expected_calls[("dom0", "admin.vm.List", None, None)] = ( + b"0\0dom0 class=AdminVM state=Running\n" + ) + self.app.domains.clear_cache() + self.expected_device_call( + "dom0", + "Available", + b"0\00000_14.0:0x8086:0xa0ed::p0c0330 " + b"device_id='0x8086:0xa0ed::p0c0330' port_id='00_14.0' " + b"devclass='pci' backend_domain='dom0' product='p1' vendor='v' " + b"interfaces='p0c0330' _sbdf='0000:00:14.0'\n" + b"00_1d.0-00_00.0:0x8086:0x2725::p028000 " + b"device_id='0x8086:0x2725::p028000' port_id='00_1d.0-00_00.0' " + b"devclass='pci' backend_domain='dom0' product='p2' vendor='v' " + b"interfaces='p028000' _sbdf='0000:aa:00.0'\n", + klass="pci", + ) + self.expected_device_call("dom0", "Attached", b"0\0", klass="pci") + + with qubesadmin.tests.tools.StdoutBuffer() as buf: + qubesadmin.tools.qvm_device.main( + ["pci", "list", "--with-sbdf", "dom0"], app=self.app + ) + self.assertEqual( + [x.rstrip() for x in buf.getvalue().splitlines()], + [ + "dom0:00_14.0 0000:00:14.0 PCI_USB: v p1", + "dom0:00_1d.0-00_00.0 0000:aa:00.0 Network: v p2", + ], + ) + def test_010_attach(self): """ Test attach action """ self.app.expected_calls[( diff --git a/qubesadmin/tools/qvm_device.py b/qubesadmin/tools/qvm_device.py index a661b734..04059110 100644 --- a/qubesadmin/tools/qvm_device.py +++ b/qubesadmin/tools/qvm_device.py @@ -44,7 +44,7 @@ from qubesadmin.devices import DEVICE_DENY_LIST -def prepare_table(dev_list): +def prepare_table(dev_list, with_sbdf=False): """Converts a list of :py:class:`qubes.devices.DeviceInfo` objects to a list of tuples for the :py:func:`qubes.tools.print_table`. @@ -53,21 +53,35 @@ def prepare_table(dev_list): :param iterable dev_list: List of :py:class:`qubes.devices.DeviceInfo` objects. + :param bool with_sbdf: when True, include SBDF identifier of PCI device :returns: list of tuples """ output = [] header = [] if sys.stdout.isatty(): - header += [("BACKEND:DEVID", "DESCRIPTION", "USED BY")] # NOQA + if with_sbdf: + header += [("BACKEND:DEVID", "SBDF", "DESCRIPTION", "USED BY")] + else: + header += [("BACKEND:DEVID", "DESCRIPTION", "USED BY")] for line in dev_list: - output += [ - ( - line.ident, - line.description, - str(line.assignments), - ) - ] + if with_sbdf: + output += [ + ( + line.ident, + line.sbdf, + line.description, + str(line.assignments), + ) + ] + else: + output += [ + ( + line.ident, + line.description, + str(line.assignments), + ) + ] return header + sorted(output) @@ -81,6 +95,7 @@ def __init__(self, device: DeviceInfo, assignment=False): self.description = device.description self.assignment = assignment self.frontends = [] + self.sbdf = getattr(device, "data", {}).get("sbdf") @property def assignments(self): @@ -96,6 +111,8 @@ def list_devices(args): """ Called by the parser to execute the qubes-devices list subcommand.""" domains = args.domains if hasattr(args, "domains") else None + if args.devclass != "pci": + args.with_sbdf = False lines = _load_lines(args.app, domains, args.devclass, actual_devices=True) lines = list(lines.values()) # short command without (list/ls) should print just existing devices @@ -107,7 +124,9 @@ def list_devices(args): args.app, [], args.devclass, actual_devices=False ) lines += list(extra_lines.values()) - qubesadmin.tools.print_table(prepare_table(lines)) + qubesadmin.tools.print_table( + prepare_table(lines, with_sbdf=getattr(args, "with_sbdf")) + ) def _load_lines(app, domains, devclass, actual_devices: bool): @@ -469,6 +488,13 @@ def init_list_parser(sub_parsers): "indicated by '*' before qube name.", ) + list_parser.add_argument( + "--with-sbdf", + "--resolve-paths", + action="store_true", + help="Include resolved PCI path (SBDF) identifier of the PCI " + "devices; ignored for non-PCI devices", + ) vm_name_group = qubesadmin.tools.VmNameGroup( list_parser, required=False,