Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/manpages/qvm-device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 40 additions & 2 deletions qubesadmin/tests/tools/qvm_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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[(
Expand Down
46 changes: 36 additions & 10 deletions qubesadmin/tools/qvm_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -53,21 +53,35 @@

: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")]

Check warning on line 63 in qubesadmin/tools/qvm_device.py

View check run for this annotation

Codecov / codecov/patch

qubesadmin/tools/qvm_device.py#L62-L63

Added lines #L62 - L63 were not covered by tests
else:
header += [("BACKEND:DEVID", "DESCRIPTION", "USED BY")]

Check warning on line 65 in qubesadmin/tools/qvm_device.py

View check run for this annotation

Codecov / codecov/patch

qubesadmin/tools/qvm_device.py#L65

Added line #L65 was not covered by tests

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)

Expand All @@ -81,6 +95,7 @@
self.description = device.description
self.assignment = assignment
self.frontends = []
self.sbdf = getattr(device, "data", {}).get("sbdf")

@property
def assignments(self):
Expand All @@ -96,6 +111,8 @@
"""
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
Expand All @@ -107,7 +124,9 @@
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):
Expand Down Expand Up @@ -469,6 +488,13 @@
"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,
Expand Down