Skip to content

Commit 8e2a28e

Browse files
committed
ext/pci: implement PCI path for device identification
PCI SBDF may change if _another_ device is added/removed from the system. Use PCI device path to avoid this issue. The PCI device path is more or less sysfs path, but with bus number behind bridges replaced with relative bus number to bridge's secondary bus number. Each component of the path is separated with '-', as that is allowed in qrexec arguments. For example device 0000:c1:00.0 behind bridge at 0000:00:08.1 will get path 0000_00_08.1-00_00.0 (assuming bridge 0000:00:08.1 has secondary bus at 0xc1). Resolving device path requires access to the system's sysfs. As a side effect, this adds also support for multi-segment systems, PCI segment is no longer hardcoded to 0000. PCIDevice object will automatically resolve old device identifiers, which partially covers migration path. The other part is converting BDF to device path when loading device assignments. Tests include subset of sysfs from a real system that has multiple bridges. TODO: - consider still allowing attaching via SBDF, or otherwise expose function to translate SBDF to PCI path (an Admin API call?) - fix salt listing/attaching devices to sys-net/sys-usb - depending on the above Fixes QubesOS/qubes-issues#8681
1 parent a1778ad commit 8e2a28e

24 files changed

Lines changed: 299 additions & 24 deletions

File tree

qubes/ext/pci.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import qubes.devices
3636
import qubes.ext
3737
from qubes.device_protocol import Port
38+
from qubes.utils import sbdf_to_path, path_to_sbdf, is_pci_path
3839

3940
#: cache of PCI device classes
4041
pci_classes = None
@@ -138,22 +139,20 @@ def _device_desc(hostdev_xml):
138139
class PCIDevice(qubes.device_protocol.DeviceInfo):
139140
# pylint: disable=too-few-public-methods
140141
regex = re.compile(
141-
r"\A(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)\."
142-
r"(?P<function>[0-9a-f]+)\Z"
142+
r"\A((?P<segment>[0-9a-f]{4})[_:])?(?P<bus>[0-9a-f]{2})[_:]"
143+
r"(?P<device>[0-9a-f]{2})\.(?P<function>[0-9a-f])\Z"
143144
)
144145
_libvirt_regex = re.compile(
145-
r"\Apci_0000_(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)_"
146-
r"(?P<function>[0-9a-f]+)\Z"
146+
r"\Apci_(?P<segment>[0-9a-f]{4})_(?P<bus>[0-9a-f]{2})_"
147+
r"(?P<device>[0-9a-f]{2})_(?P<function>[0-9a-f])\Z"
147148
)
148149

149150
def __init__(self, port: Port, libvirt_name=None):
150151
if libvirt_name:
151152
dev_match = self._libvirt_regex.match(libvirt_name)
152153
if not dev_match:
153154
raise UnsupportedDevice(libvirt_name)
154-
port_id = "{bus}_{device}.{function}".format(
155-
**dev_match.groupdict()
156-
)
155+
port_id = sbdf_to_path(libvirt_name)
157156
port = Port(
158157
backend_domain=port.backend_domain,
159158
port_id=port_id,
@@ -162,14 +161,24 @@ def __init__(self, port: Port, libvirt_name=None):
162161

163162
super().__init__(port)
164163

165-
dev_match = self.regex.match(port.port_id)
164+
if is_pci_path(port.port_id):
165+
sbdf = path_to_sbdf(port.port_id)
166+
else:
167+
sbdf = port.port_id
168+
dev_match = self.regex.match(sbdf)
166169
if not dev_match:
167170
raise ValueError(
168-
"Invalid device identifier: {!r}".format(port.port_id)
171+
"Invalid device identifier: {!r} (sbdf: {!r})".format(
172+
port.port_id, sbdf
173+
)
169174
)
170175

176+
self.sbdf = sbdf
177+
171178
for group in self.regex.groupindex:
172179
setattr(self, group, dev_match.group(group))
180+
if getattr(self, "segment") is None:
181+
self.segment = "0000"
173182

174183
# lazy loading
175184
self._description: Optional[str] = None
@@ -249,7 +258,7 @@ def parent_device(self) -> Optional[qubes.device_protocol.DeviceInfo]:
249258
def libvirt_name(self):
250259
# pylint: disable=no-member
251260
# noinspection PyUnresolvedReferences
252-
return f"pci_0000_{self.bus}_{self.device}_{self.function}"
261+
return f"pci_{self.segment}_{self.bus}_{self.device}_{self.function}"
253262

254263
@property
255264
def description(self):
@@ -380,31 +389,31 @@ def on_device_list_attached(self, vm, event, **kwargs):
380389
if hostdev.get("type") != "pci":
381390
continue
382391
address = hostdev.find("source/address")
392+
segment = address.get("domain")[2:]
383393
bus = address.get("bus")[2:]
384394
device = address.get("slot")[2:]
385395
function = address.get("function")[2:]
386396

387-
port_id = "{bus}_{device}.{function}".format(
397+
libvirt_name = "pci_{segment}_{bus}_{device}_{function}".format(
398+
segment=segment,
388399
bus=bus,
389400
device=device,
390401
function=function,
391402
)
392403
yield PCIDevice(
393404
Port(
394405
backend_domain=vm.app.domains[0],
395-
port_id=port_id,
406+
port_id=None,
396407
devclass="pci",
397-
)
408+
),
409+
libvirt_name=libvirt_name,
398410
), {}
399411

400412
@qubes.ext.handler("device-pre-attach:pci")
401413
def on_device_pre_attached_pci(self, vm, event, device, options):
402414
# pylint: disable=unused-argument
403-
if not os.path.exists(
404-
"/sys/bus/pci/devices/0000:{}".format(
405-
device.port_id.replace("_", ":")
406-
)
407-
):
415+
sbdf = path_to_sbdf(device.port_id)
416+
if sbdf is None or not os.path.exists(f"/sys/bus/pci/devices/{sbdf}"):
408417
raise qubes.exc.QubesException(
409418
"Invalid PCI device: {}".format(device.port_id)
410419
)
@@ -458,7 +467,7 @@ def on_device_pre_detached_pci(self, vm, event, port):
458467
) as p:
459468
result = p.communicate()[0].decode()
460469
m = re.search(
461-
r"^(\d+.\d+)\s+0000:{}$".format(device.port_id.replace("_", ":")),
470+
r"^(\d+.\d+)\s+{}$".format(device.sbdf),
462471
result,
463472
flags=re.MULTILINE,
464473
)

qubes/tests/devices_pci.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
#
1818
# You should have received a copy of the GNU Lesser General Public
1919
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
20+
import os.path
21+
import unittest
2022
from unittest import mock
2123

2224
import qubes.tests
2325
import qubes.ext.pci
2426
from qubes.device_protocol import DeviceInterface
27+
from qubes.utils import sbdf_to_path, path_to_sbdf
28+
29+
orig_open = open
2530

2631

2732
class TestVM(object):
@@ -107,17 +112,55 @@ def mock_file_open(filename: str, *_args, **_kwargs):
107112
\t09 CANBUS
108113
\t80 Serial bus controller
109114
"""
110-
elif filename.startswith("/sys/devices/pci"):
111-
content = "0x0c0330"
112115
else:
113-
raise OSError()
116+
return orig_open(filename, *_args, **_kwargs)
114117

115118
file_object = mock.mock_open(read_data=content).return_value
116119
file_object.__iter__.return_value = content
117120
return file_object
118121

119122

120-
class TC_00_Block(qubes.tests.QubesTestCase):
123+
# prefer location in git checkout
124+
tests_sysfs_path = (
125+
os.path.dirname(__file__) + "/../../tests-data/sysfs/sys/bus/pci/devices"
126+
)
127+
if not os.path.exists(tests_sysfs_path):
128+
# but if not there, look for package installed one
129+
tests_sysfs_path = "/usr/share/qubes/tests-data/sysfs/sys/bus/pci/devices"
130+
131+
132+
@mock.patch("qubes.utils.SYSFS_PCI_DEVS_BASE", tests_sysfs_path)
133+
class TC_00_helpers(qubes.tests.QubesTestCase):
134+
def test_000_sbdf_to_path1(self):
135+
path = sbdf_to_path("0000:c6:00.0")
136+
self.assertEqual(path, "c0_03.5-00_00.0-00_00.0")
137+
138+
def test_001_sbdf_to_path2(self):
139+
path = sbdf_to_path("0000:00:18.4")
140+
self.assertEqual(path, "00_18.4")
141+
142+
def test_002_sbdf_to_path_libvirt(self):
143+
path = sbdf_to_path("pci_0000_00_18_4")
144+
self.assertEqual(path, "00_18.4")
145+
146+
def test_003_sbdf_to_path_default_segment1(self):
147+
path = sbdf_to_path("00:18.4")
148+
self.assertEqual(path, "00_18.4")
149+
150+
def test_004_sbdf_to_path_default_segment2(self):
151+
path = sbdf_to_path("0000:00:18.4")
152+
self.assertEqual(path, "00_18.4")
153+
154+
def test_010_path_to_sbdf1(self):
155+
path = path_to_sbdf("0000_c0_03.5-00_00.0-00_00.0")
156+
self.assertEqual(path, "0000:c6:00.0")
157+
158+
def test_011_path_to_sbdf2(self):
159+
path = path_to_sbdf("0000_00_18.4")
160+
self.assertEqual(path, "0000:00:18.4")
161+
162+
163+
class TC_10_PCI(qubes.tests.QubesTestCase):
121164
def setUp(self):
122165
super().setUp()
123166
self.ext = qubes.ext.pci.PCIDeviceExtension()

qubes/tests/vm/qubesvm.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@
4747
import qubes.tests
4848
import qubes.tests.vm
4949

50+
# prefer location in git checkout
51+
tests_sysfs_path = (
52+
os.path.dirname(__file__) + "/../../../tests-data/sysfs/sys/bus/pci/devices"
53+
)
54+
if not os.path.exists(tests_sysfs_path):
55+
# but if not there, look for package installed one
56+
tests_sysfs_path = "/usr/share/qubes/tests-data/sysfs/sys/bus/pci/devices"
57+
5058

5159
class TestApp(object):
5260
labels = {
@@ -928,6 +936,55 @@ def test_500_property_migrate_virt_mode(self):
928936
with self.assertRaises(AttributeError):
929937
vm.hvm
930938

939+
@unittest.mock.patch("qubes.utils.SYSFS_PCI_DEVS_BASE", tests_sysfs_path)
940+
def test_510_migrate_pci_assignments(self):
941+
vm = qubes.vm.adminvm.AdminVM(self.app, None)
942+
dom0 = self.get_vm(vm=vm)
943+
xml_template = """
944+
<domain class="QubesVM" id="domain-1">
945+
<properties>
946+
<property name="qid">1</property>
947+
<property name="name">testvm</property>
948+
<property name="label" ref="label-1" />
949+
<property name="virt_mode">hvm</property>
950+
</properties>
951+
<devices class="pci">
952+
<device backend-domain="dom0" id="02_00.2"/>
953+
</devices>
954+
</domain>
955+
"""
956+
xml = lxml.etree.XML(xml_template)
957+
vm = qubes.vm.qubesvm.QubesVM(self.app, xml)
958+
vm.load_properties()
959+
vm.load_extras()
960+
dev_ass = list(vm.devices["pci"].get_assigned_devices())
961+
self.assertEqual(len(dev_ass), 1)
962+
self.assertEqual(dev_ass[0].port_id, "00_08.1-00_00.2")
963+
964+
def test_511_migrate_pci_assignments_non_existing(self):
965+
vm = qubes.vm.adminvm.AdminVM(self.app, None)
966+
dom0 = self.get_vm(vm=vm)
967+
xml_template = """
968+
<domain class="QubesVM" id="domain-1">
969+
<properties>
970+
<property name="qid">1</property>
971+
<property name="name">testvm</property>
972+
<property name="label" ref="label-1" />
973+
<property name="virt_mode">hvm</property>
974+
</properties>
975+
<devices class="pci">
976+
<device backend-domain="dom0" id="02_00.7"/>
977+
</devices>
978+
</domain>
979+
"""
980+
xml = lxml.etree.XML(xml_template)
981+
vm = qubes.vm.qubesvm.QubesVM(self.app, xml)
982+
vm.load_properties()
983+
vm.load_extras()
984+
dev_ass = list(vm.devices["pci"].get_assigned_devices())
985+
self.assertEqual(len(dev_ass), 1)
986+
self.assertEqual(dev_ass[0].port_id, "02_00.7")
987+
931988
def test_600_libvirt_xml_pv(self):
932989
my_uuid = "7db78950-c467-4863-94d1-af59806384ea"
933990
expected = f"""<domain type="xen">
@@ -1527,6 +1584,7 @@ def test_600_libvirt_xml_hvm_pcidev(self):
15271584
<hostdev type="pci" managed="yes">
15281585
<source>
15291586
<address
1587+
domain="0x0000"
15301588
bus="0x00"
15311589
slot="0x00"
15321590
function="0x0" />
@@ -1637,6 +1695,7 @@ def test_600_libvirt_xml_hvm_pcidev_s0ix(self):
16371695
<source
16381696
powerManagementFiltering="no">
16391697
<address
1698+
domain="0x0000"
16401699
bus="0x00"
16411700
slot="0x00"
16421701
function="0x0" />

0 commit comments

Comments
 (0)