Skip to content

Commit 2e09839

Browse files
Add device passthrough support for hyperv platform
Implement Device Pool as per runbook Schema change for runbook to get the device pool details Add powershell script to get all assignable devices Set the node context as per requirement Signed-off-by: Smit Gardhariya <[email protected]>
1 parent f4958c4 commit 2e09839

File tree

5 files changed

+666
-4
lines changed

5 files changed

+666
-4
lines changed

Diff for: lisa/sut_orchestrator/hyperv/context.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT license.
33

4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, field
55
from pathlib import PurePath
6-
from typing import Optional
6+
from typing import List, Optional
77

88
from lisa import Node, RemoteNode
9+
from lisa.sut_orchestrator.hyperv.schema import DeviceAddressSchema
10+
from lisa.sut_orchestrator.util.schema import HostDevicePoolType
911
from lisa.util.process import Process
1012

1113

14+
@dataclass
15+
class DevicePassthroughContext:
16+
pool_type: HostDevicePoolType = HostDevicePoolType.PCI_NIC
17+
device_list: List[DeviceAddressSchema] = field(
18+
default_factory=list,
19+
)
20+
21+
1222
@dataclass
1323
class NodeContext:
1424
vm_name: str = ""
1525
host: Optional[RemoteNode] = None
1626
working_path = PurePath()
1727
serial_log_process: Optional[Process] = None
1828

29+
# Device pass through configuration
30+
passthrough_devices: List[DevicePassthroughContext] = field(
31+
default_factory=list,
32+
)
33+
1934
@property
2035
def console_log_path(self) -> PurePath:
2136
return self.working_path / f"{self.vm_name}-console.log"
+359
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
# Refer: https://learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/deploy/deploying-graphics-devices-using-dda # noqa E501
4+
import re
5+
from typing import Dict, List, Optional
6+
7+
from lisa.node import Node
8+
from lisa.tools import PowerShell
9+
from lisa.util import LisaException, find_group_in_lines, find_groups_in_lines
10+
from lisa.util.logger import Logger
11+
12+
from .schema import DeviceAddressSchema
13+
14+
15+
class HypervAssignableDevices:
16+
PKEY_DEVICE_TYPE = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 1"
17+
PKEY_BASE_CLASS = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 3"
18+
PKEY_REQUIRES_RESERVED_MEMORY_REGION = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 34" # noqa E501
19+
PKEY_ACS_COMPATIBLE_UP_HIERARCHY = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 31" # noqa E501
20+
PROP_DEVICE_TYPE_PCI_EXPRESS_ENDPOINT = "2"
21+
PROP_DEVICE_TYPE_PCI_EXPRESS_LEGACY_ENDPOINT = "3"
22+
PROP_DEVICE_TYPE_PCI_EXPRESS_ROOT_COMPLEX_INTEGRATED_ENDPOINT = "4"
23+
PROP_DEVICE_TYPE_PCI_EXPRESS_TREATED_AS_PCI = "5"
24+
PROP_ACS_COMPATIBLE_UP_HIERARCHY_NOT_SUPPORTED = "0"
25+
PROP_BASE_CLASS_DISPLAY_CTRL = "3"
26+
27+
def __init__(self, host_node: Node, log: Logger):
28+
self.host_node = host_node
29+
self.log = log
30+
self.pwsh = self.host_node.tools[PowerShell]
31+
self.pnp_allocated_resources: List[Dict[str, str]] = (
32+
self.__load_pnp_allocated_resources()
33+
)
34+
35+
def get_assignable_devices(
36+
self,
37+
vendor_id: str,
38+
device_id: str,
39+
) -> List[DeviceAddressSchema]:
40+
device_id_list = self.__get_devices_by_vendor_device_id(
41+
vendor_id=vendor_id, device_id=device_id
42+
)
43+
44+
devices: List[DeviceAddressSchema] = []
45+
for rec in device_id_list:
46+
device_id = rec["device_id"]
47+
result = self.__get_dda_properties(device_id=device_id)
48+
if result:
49+
result.friendly_name = rec["friendly_name"]
50+
devices.append(result)
51+
return devices
52+
53+
def __get_devices_by_vendor_device_id(
54+
self,
55+
vendor_id: str,
56+
device_id: str,
57+
) -> List[Dict[str, str]]:
58+
"""
59+
Get the device ID list for given vendor/device ID combination
60+
"""
61+
devices: List[Dict[str, str]] = []
62+
device_regex = re.compile(
63+
r"Description\s+:\s*(?P<desc>.+)\n.*DeviceID\s+:\s*(?P<device_id>.+)"
64+
)
65+
66+
cmd = (
67+
"Get-WmiObject Win32_PnPEntity -Filter "
68+
f"\"DeviceID LIKE 'PCI\\\\VEN_{vendor_id}&DEV_{device_id}%'\""
69+
)
70+
stdout = self.pwsh.run_cmdlet(
71+
cmdlet=cmd,
72+
force_run=True,
73+
sudo=True,
74+
)
75+
76+
devices_str = stdout.strip().split("\r\n\r\n")
77+
filtered_devices = [i.strip() for i in devices_str if i.strip() != ""]
78+
for device_properties in filtered_devices:
79+
res = find_group_in_lines(
80+
lines=device_properties,
81+
pattern=device_regex,
82+
single_line=False,
83+
)
84+
if not res:
85+
raise LisaException("Can not extract DeviceId/Description")
86+
87+
devices.append({
88+
"device_id": res["device_id"].strip(),
89+
"friendly_name": res["desc"].strip(),
90+
})
91+
return devices
92+
93+
def __get_pnp_device_property(self, device_id: str, property_name: str) -> str:
94+
"""
95+
Retrieve a PnP device property by instance ID and property key.
96+
"""
97+
cmd = (
98+
"(Get-PnpDeviceProperty -InstanceId "
99+
f"'{device_id}' '{property_name}').Data"
100+
)
101+
102+
output = self.pwsh.run_cmdlet(
103+
cmdlet=cmd,
104+
sudo=True,
105+
force_run=True,
106+
)
107+
return output.strip()
108+
109+
def __load_pnp_allocated_resources(self) -> List[Dict[str, str]]:
110+
# Command output result (just 2 device properties)
111+
# ========================================================
112+
# __GENUS : 2
113+
# __CLASS : Win32_PNPAllocatedResource
114+
# __SUPERCLASS : CIM_AllocatedResource
115+
# __DYNASTY : CIM_Dependency
116+
# __RELPATH : Win32_PNPAllocatedResource.Antecedent="\\\\WIN-2IDCNC2D5V
117+
# C\\root\\cimv2:Win32_DeviceMemoryAddress.StartingAddress=\
118+
# "2463203328\"",Dependent="\\\\WIN-2IDCNC2D5VC\\root\\cimv2:
119+
# Win32_PnPEntity.DeviceID=\"PCI\\\\VEN_8086&DEV_A1A3&
120+
# SUBSYS_07161028&REV_09\\\\3&11583659&0&FC\""
121+
# __PROPERTY_COUNT : 2
122+
# __DERIVATION : {CIM_AllocatedResource, CIM_Dependency}
123+
# __SERVER : WIN-2IDCNC2D5VC
124+
# __NAMESPACE : root\cimv2
125+
# __PATH : \\WIN-2IDCNC2D5VC\root\cimv2:Win32_PNPAllocatedResource.
126+
# Antecedent="\\\\WIN-2IDCNC2D5VC\\root\\cimv2:Win32_
127+
# DeviceMemoryAddress.StartingAddress=\"2463203328\"",
128+
# Dependent="\\\\WIN-2IDCNC2D5VC\\root\\cimv2:Win32_PnP
129+
# Entity.DeviceID=\"PCI\\\\VEN_8086&DEV_A1A3&SUBSYS_07161028&
130+
# REV_09\\\\3&11583659&0&FC\""
131+
# Antecedent : \\WIN-2IDCNC2D5VC\root\cimv2:Win32_DeviceMemoryAddress.
132+
# StartingAddress="2463203328"
133+
# Dependent : \\WIN-2IDCNC2D5VC\root\cimv2:Win32_PnPEntity.DeviceID=
134+
# "PCI\\VEN_8086&DEV_A1A3&SUBSYS_07161028&REV_09\\3
135+
# &11583659&0&FC"
136+
# PSComputerName : WIN-2IDCNC2D5VC
137+
138+
# __GENUS : 2
139+
# __CLASS : Win32_PNPAllocatedResource
140+
# __SUPERCLASS : CIM_AllocatedResource
141+
# __DYNASTY : CIM_Dependency
142+
# __RELPATH : Win32_PNPAllocatedResource.Antecedent="\\\\WIN-2IDCNC2D5VC
143+
# \\root\\cimv2:Win32_PortResource.StartingAddress=\"8192\"",
144+
# Dependent="\\\\WIN-2IDCNC2D5VC\\root\\cimv2:Win32_PnPEntity
145+
# .DeviceID=\"PCI\\\\VEN_8086&DEV_A1A3&SUBSYS_07161028&REV_09
146+
# \\\\3&11583659&0&FC\""
147+
# __PROPERTY_COUNT : 2
148+
# __DERIVATION : {CIM_AllocatedResource, CIM_Dependency}
149+
# __SERVER : WIN-2IDCNC2D5VC
150+
# __NAMESPACE : root\cimv2
151+
# __PATH : \\WIN-2IDCNC2D5VC\root\cimv2:Win32_PNPAllocatedResource.
152+
# Antecedent="\\\\WIN-2IDCNC2D5VC\\root\\cimv2:Win32_PortR
153+
# esource.StartingAddress=\"8192\"",Dependent="\\\\WIN-2ID
154+
# CNC2D5VC\\root\\cimv2:Win32_PnPEntity.DeviceID=\"PCI\\\\
155+
# VEN_8086&DEV_A1A3&SUBSYS_07161028&REV_09\\\\3&11
156+
# 583659&0&FC\""
157+
# Antecedent : \\WIN-2IDCNC2D5VC\root\cimv2:Win32_PortResource.
158+
# StartingAddress="8192"
159+
# Dependent : \\WIN-2IDCNC2D5VC\root\cimv2:Win32_PnPEntity.DeviceID=
160+
# "PCI\\VEN_8086&DEV_A1A3&SUBSYS_07161028&REV_09\\3&
161+
# 11583659&0&FC"
162+
# PSComputerName : WIN-2IDCNC2D5VC
163+
164+
stdout = self.pwsh.run_cmdlet(
165+
cmdlet="gwmi -query 'select * from Win32_PnPAllocatedResource'",
166+
sudo=True,
167+
force_run=True,
168+
)
169+
pnp_allocated_resources = stdout.strip().split("\r\n\r\n")
170+
result: List[Dict[str, str]] = []
171+
# Regular expression to match the key-value pairs
172+
pattern = re.compile(r'(?P<key>\S+)\s*:\s*(?P<value>.*?)(?=\n\S|\Z)', re.DOTALL)
173+
174+
for rec in pnp_allocated_resources:
175+
extract_val = {}
176+
matches = find_groups_in_lines(
177+
lines=rec.strip(),
178+
pattern=pattern,
179+
single_line=False,
180+
)
181+
if matches:
182+
for element in matches:
183+
key = element["key"]
184+
val = element["value"]
185+
val = val.replace(" ", "")
186+
val = val.replace("\r\n", "")
187+
extract_val[key] = val
188+
result.append(extract_val)
189+
return result
190+
191+
def __get_mmio_end_address(self, start_addr: str) -> Optional[str]:
192+
# MemoryType Name Status
193+
# ---------- ---- ------
194+
# WindowDecode 0xE1800000-0xE1BFFFFF OK
195+
# 0xE2000000-0xE2000FFF OK
196+
# WindowDecode 0xD4000000-0xD43FFFFF OK
197+
# 0xD4800000-0xD4800FFF OK
198+
# 0xFED1C000-0xFED3FFFF OK
199+
200+
device_mem_addr = self.pwsh.run_cmdlet(
201+
cmdlet="gwmi -query 'select * from Win32_DeviceMemoryAddress'",
202+
sudo=True,
203+
force_run=True,
204+
)
205+
end_addr_rec = None
206+
for rec in device_mem_addr.splitlines():
207+
rec = rec.strip()
208+
if rec.find(start_addr) >= 0:
209+
addr = rec.split("-")
210+
start_addr_rec = addr[0].split()[-1]
211+
end_addr_rec = addr[1].split()[0].strip()
212+
213+
err = "MMIO Starting address not matching"
214+
assert start_addr == start_addr_rec, err
215+
break
216+
return end_addr_rec
217+
218+
def __get_dda_properties(self, device_id: str) -> Optional[DeviceAddressSchema]:
219+
"""
220+
Determine if a PCI device is assignable using Discrete Device Assignment (DDA)
221+
If so, get DDA proerprties like locationpath, device-id, friendly name
222+
"""
223+
self.log.debug(f"PCI InstanceId: {device_id}")
224+
225+
rmrr = self.__get_pnp_device_property(
226+
device_id=device_id,
227+
property_name=self.PKEY_REQUIRES_RESERVED_MEMORY_REGION,
228+
)
229+
rmrr = rmrr.strip()
230+
if rmrr != "False":
231+
self.log.debug(
232+
"BIOS requires that this device remain attached to BIOS-owned memory."
233+
"Not assignable."
234+
)
235+
return None
236+
237+
acs_up = self.__get_pnp_device_property(
238+
device_id=device_id,
239+
property_name=self.PKEY_ACS_COMPATIBLE_UP_HIERARCHY,
240+
)
241+
acs_up = acs_up.strip()
242+
if acs_up == self.PROP_ACS_COMPATIBLE_UP_HIERARCHY_NOT_SUPPORTED:
243+
self.log.debug(
244+
"Traffic from this device may be redirected to other devices in "
245+
"the system. Not assignable."
246+
)
247+
return None
248+
249+
dev_type = self.__get_pnp_device_property(
250+
device_id=device_id,
251+
property_name=self.PKEY_DEVICE_TYPE
252+
)
253+
dev_type = dev_type.strip()
254+
if dev_type == self.PROP_DEVICE_TYPE_PCI_EXPRESS_ENDPOINT:
255+
self.log.debug("Express Endpoint -- more secure.")
256+
else:
257+
if dev_type == (
258+
self.PROP_DEVICE_TYPE_PCI_EXPRESS_ROOT_COMPLEX_INTEGRATED_ENDPOINT
259+
):
260+
self.log.debug("Embedded Endpoint -- less secure.")
261+
elif dev_type == self.PROP_DEVICE_TYPE_PCI_EXPRESS_LEGACY_ENDPOINT:
262+
dev_base_class = self.__get_pnp_device_property(
263+
device_id=device_id,
264+
property_name=self.PKEY_BASE_CLASS,
265+
)
266+
dev_base_class = dev_base_class.strip()
267+
if dev_base_class == self.PROP_BASE_CLASS_DISPLAY_CTRL:
268+
self.log.debug("Legacy Express Endpoint -- graphics controller.")
269+
else:
270+
self.log.debug("Legacy, non-VGA PCI device. Not assignable.")
271+
return None
272+
else:
273+
if dev_type == self.PROP_DEVICE_TYPE_PCI_EXPRESS_TREATED_AS_PCI:
274+
self.log.debug(
275+
"BIOS kept control of PCI Express for this device. "
276+
"Not assignable."
277+
)
278+
else:
279+
self.log.debug(
280+
"Old-style PCI device, switch port, etc. "
281+
"Not assignable."
282+
)
283+
return None
284+
285+
# Get the device location path
286+
location_path = self.__get_pnp_device_property(
287+
device_id=device_id,
288+
property_name="DEVPKEY_Device_LocationPaths",
289+
)
290+
location_path = location_path.strip().splitlines()[0]
291+
self.log.debug(f"Device locationpath: {location_path}")
292+
assert location_path.find("PCI") == 0, "Location path is wrong"
293+
294+
cmd = (
295+
"(Get-PnpDevice -PresentOnly -InstanceId "
296+
f"'{device_id}').ConfigManagerErrorCode"
297+
)
298+
conf_mng_err_code = self.pwsh.run_cmdlet(
299+
cmdlet=cmd,
300+
force_run=True,
301+
sudo=True,
302+
)
303+
conf_mng_err_code = conf_mng_err_code.strip()
304+
self.log.debug(f"ConfigManagerErrorCode: {conf_mng_err_code}")
305+
if conf_mng_err_code == "CM_PROB_DISABLED":
306+
self.log.debug(
307+
"Device is Disabled, unable to check resource requirements, "
308+
"it may be assignable."
309+
)
310+
self.log.debug("Enable the device and rerun this script to confirm.")
311+
return None
312+
313+
irq_assignements = [
314+
i for i in self.pnp_allocated_resources
315+
if i["Dependent"].find(device_id.replace("\\", "\\\\")) >= 0
316+
]
317+
if irq_assignements:
318+
msi_assignments = [
319+
i for i in self.pnp_allocated_resources
320+
if i["Antecedent"].find("IRQNumber=42949") >= 0
321+
]
322+
if not msi_assignments:
323+
self.log.debug(
324+
"All of the interrupts are line-based, no assignment can work."
325+
)
326+
return None
327+
else:
328+
self.log.debug("Its interrupts are message-based, assignment can work.")
329+
else:
330+
self.log.debug("It has no interrupts at all -- assignment can work.")
331+
332+
mmio_assignments = [
333+
i for i in self.pnp_allocated_resources
334+
if i["Dependent"].find(device_id.replace("\\", "\\\\")) >= 0
335+
and i["__RELPATH"].find("Win32_DeviceMemoryAddres") >= 0
336+
]
337+
mmio_total = 0
338+
if mmio_assignments:
339+
for rec in mmio_assignments:
340+
antecedent_val = rec["Antecedent"]
341+
addresses = antecedent_val.split('"')
342+
assert len(addresses) >= 2, "Antecedent: Can't get MMIO Start Address"
343+
start_address = hex(int(addresses[1].strip())).upper()
344+
start_address_hex = start_address.replace("X", "x")
345+
end_address = self.__get_mmio_end_address(start_address_hex)
346+
assert end_address, "Can not get MMIO End Address"
347+
348+
mmio = int(end_address, 16) - int(start_address, 16)
349+
mmio_total += mmio
350+
if mmio_total:
351+
mmio_total = round(mmio_total / (1024 * 1024))
352+
self.log.debug(f"Device '{device_id}', Total MMIO = {mmio_total}MB ")
353+
else:
354+
self.log.debug("It has no MMIO space")
355+
356+
device = DeviceAddressSchema()
357+
device.location_path = location_path
358+
device.instance_id = device_id
359+
return device

0 commit comments

Comments
 (0)