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,