Skip to content

Commit 2bb35de

Browse files
committed
CP-54481: support DMV RPU plugin
move DMV common code to python-libs because RPU plugin calls these functions as well Signed-off-by: Chunjie Zhu <[email protected]>
1 parent 3e0dd25 commit 2bb35de

File tree

1 file changed

+362
-0
lines changed

1 file changed

+362
-0
lines changed

xcp/dmv.py

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
# Copyright (c) 2025, Citrix Inc.
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# 1. Redistributions of source code must retain the above copyright notice, this
8+
# list of conditions and the following disclaimer.
9+
# 2. Redistributions in binary form must reproduce the above copyright notice,
10+
# this list of conditions and the following disclaimer in the documentation
11+
# and/or other materials provided with the distribution.
12+
#
13+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23+
24+
import os
25+
import subprocess
26+
import json
27+
import re
28+
import struct
29+
import glob
30+
import errno
31+
32+
from .compat import open_with_codec_handling
33+
34+
dmv_proto_ver = 0.1
35+
err_proto_ver = 0.1
36+
37+
def get_all_kabi_dirs():
38+
"""Return a list of (kabi_ver, updates_dir, dmv_dir) tuples for all kernel versions."""
39+
modules_root = "/lib/modules/"
40+
dirs = []
41+
for kabi_ver in os.listdir(modules_root):
42+
updates_dir = os.path.join(modules_root, kabi_ver, "updates")
43+
dmv_dir = os.path.join(modules_root, kabi_ver, "dmv")
44+
# not checking if updates_dir and dmv_dir exist here, will check later when use them
45+
dirs.append((kabi_ver, updates_dir, dmv_dir))
46+
return dirs
47+
48+
def note_offset(var_len):
49+
"""Note section has 4 bytes padding"""
50+
ret = (((var_len - 1) & ~3) + 4) - var_len
51+
return ret
52+
53+
def get_active_variant(modules):
54+
"""Check and report active driver"""
55+
# Check if any module in the modules is loaded
56+
for module in modules:
57+
# get 'module' from 'module.ko'
58+
module_name = os.path.splitext(module)[0]
59+
note_file = os.path.join("/sys/module", module_name, "notes/.note.XenServer")
60+
if not os.path.isfile(note_file):
61+
continue
62+
63+
note_struct_size = struct.calcsize('III')
64+
with open(note_file, "rb") as n_file:
65+
for _ in range(3):
66+
note_hdr = struct.unpack('III', n_file.read(note_struct_size))
67+
n_file.read(note_offset(note_struct_size))
68+
vendor = n_file.read(note_hdr[0])
69+
n_file.read(note_offset(note_hdr[0]))
70+
content = n_file.read(note_hdr[1])[:-1]
71+
n_file.read(note_offset(note_hdr[1]))
72+
note_type = note_hdr[2]
73+
if vendor == b'XenServer' and note_type == 1:
74+
variant = content.decode("ascii")
75+
return variant
76+
return None
77+
78+
def get_loaded_modules(modules):
79+
"""Return all loaded modules"""
80+
loaded_modules = []
81+
for module in modules:
82+
# get 'module' from 'module.ko'
83+
module_name = os.path.splitext(module)[0]
84+
note_file = os.path.join("/sys/module", module_name, "notes/.note.XenServer")
85+
if os.path.isfile(note_file):
86+
loaded_modules.append(module)
87+
return loaded_modules
88+
89+
def id_matches(id1, id2):
90+
if '*' in [id1, id2]:
91+
return True
92+
return id1 == id2
93+
94+
'''
95+
driver_pci_ids example:
96+
{
97+
"abc.ko": [
98+
{
99+
"vendor_id": "14e4",
100+
"device_id": "163c",
101+
"subvendor_id": "*",
102+
"subdevice_id": "*"
103+
},
104+
{
105+
"vendor_id": "14e4",
106+
"device_id": "163b",
107+
"subvendor_id": "*",
108+
"subdevice_id": "*"
109+
}],
110+
"de.ko": [
111+
{
112+
"vendor_id": "eees",
113+
"device_id": "163c",
114+
"subvendor_id": "*",
115+
"subdevice_id": "*"
116+
},
117+
{
118+
"vendor_id": "14f4",
119+
"device_id": "16db",
120+
"subvendor_id": "2123",
121+
"subdevice_id": "1123"
122+
}]
123+
}
124+
'''
125+
def pci_matches(present_pci_id, driver_pci_ids):
126+
"""Check if present PCI ID matches any of the driver PCI IDs."""
127+
merged_driver_pci_id_list = []
128+
for module_pci_list in driver_pci_ids.values():
129+
for item in module_pci_list:
130+
merged_driver_pci_id_list.append(item)
131+
132+
for pci_id in merged_driver_pci_id_list:
133+
if (id_matches(present_pci_id['vendor'], pci_id['vendor_id']) and
134+
id_matches(present_pci_id['device'], pci_id['device_id']) and
135+
id_matches(present_pci_id['subvendor'], pci_id['subvendor_id']) and
136+
id_matches(present_pci_id['subdevice'], pci_id['subdevice_id'])):
137+
return True
138+
return False
139+
140+
def hardware_present(lspci_out, pci_ids):
141+
"""Check if supported hardware is fitted"""
142+
if not pci_ids or not lspci_out:
143+
return False
144+
145+
# 'lspci -nm' output:
146+
# 00:15.3 "0604" "15ad" "07a0" -r01 -p00 "15ad" "07a0"
147+
# 00:01.0 "0604" "8086" "7191" -r01 -p00 "" ""
148+
lspci_expression = r'''
149+
^
150+
(?P<slot>\S+) # PCI slot (00:15.3)
151+
\s+
152+
"(?P<class>[^"]*)" # Device class (0604)
153+
\s+
154+
"(?P<vendor>[^"]*)" # Vendor (15ad)
155+
\s+
156+
"(?P<device>[^"]*)" # Device name (07a0)
157+
\s*
158+
(?:-(?P<revision>\S+))? # Optional revision (-r01)
159+
\s*
160+
(?:-(?P<progif>\S+))? # Optional programming interface (-p00)
161+
\s+
162+
"(?P<subvendor>[^"]*)" # Subvendor (15ad or empty)
163+
\s+
164+
"(?P<subdevice>[^"]*)" # Subdevice (07a0 or empty)
165+
$
166+
'''
167+
lscpi_pattern = re.compile(lspci_expression, re.VERBOSE | re.MULTILINE)
168+
for match in lscpi_pattern.finditer(lspci_out):
169+
if pci_matches(match.groupdict(), pci_ids):
170+
return True
171+
return False
172+
173+
def variant_selected(modules, updates_dir):
174+
"""Check and return which driver is selected"""
175+
# Check if any module in the modules is selected
176+
for module in modules:
177+
slink_file = os.path.join(updates_dir, module)
178+
if os.path.islink(slink_file):
179+
module_path = os.path.realpath(slink_file)
180+
module_dir = os.path.dirname(module_path)
181+
info_file = os.path.join(module_dir, "info.json")
182+
with open(info_file, "r", encoding="ascii") as json_file:
183+
json_data = json.load(json_file)
184+
variant = json_data["variant"]
185+
186+
return variant
187+
return None
188+
189+
class DriverMultiVersion(object):
190+
def __init__(self, updates_dir, lspci_out, runtime=False):
191+
self.updates_dir = updates_dir
192+
self.lspci_out = lspci_out
193+
self.runtime = runtime
194+
195+
def variant_selected(self, modules):
196+
"""Check and return which driver is selected"""
197+
# Check if any module in the modules is selected
198+
for module in modules:
199+
slink_file = os.path.join(self.updates_dir, module)
200+
if os.path.islink(slink_file):
201+
module_path = os.path.realpath(slink_file)
202+
module_dir = os.path.dirname(module_path)
203+
info_file = os.path.join(module_dir, "info.json")
204+
with open(info_file, "r", encoding="ascii") as json_file:
205+
json_data = json.load(json_file)
206+
variant = json_data["variant"]
207+
208+
return variant
209+
return None
210+
211+
def parse_dmv_info(self, fpath):
212+
"""Populate dmv list with information"""
213+
json_data = None
214+
with open_with_codec_handling(fpath, encoding="ascii") as json_file:
215+
json_data = json.load(json_file)
216+
json_formatted = {
217+
"type": json_data["category"],
218+
"friendly_name": json_data["name"],
219+
"description": json_data["description"],
220+
"info": json_data["name"],
221+
"variants": {
222+
json_data["variant"]: {
223+
"version": json_data["version"],
224+
"hardware_present": hardware_present(
225+
self.lspci_out.stdout,
226+
json_data["pci_ids"]),
227+
"priority": json_data["priority"],
228+
"status": json_data["status"]}}}
229+
if self.runtime:
230+
json_formatted["selected"] = self.variant_selected(
231+
json_data["pci_ids"].keys())
232+
json_formatted["active"] = get_active_variant(
233+
json_data["pci_ids"].keys())
234+
json_formatted["loaded modules"] = get_loaded_modules(
235+
json_data["pci_ids"].keys())
236+
return json_data, json_formatted
237+
238+
class DriverMultiVersionManager(object):
239+
def __init__(self, runtime=False):
240+
self.runtime = runtime
241+
self.dmv_list = {
242+
"protocol": {"version": dmv_proto_ver},
243+
"operation": {"reboot": False},
244+
"drivers": {}
245+
}
246+
self.errors_list = {
247+
"version": err_proto_ver,
248+
"exit_code": 0,
249+
"message": "Success"
250+
}
251+
252+
def merge_jsondata(self, oldone, newone):
253+
variants = oldone["variants"]
254+
for k, v in newone["variants"].items():
255+
variants[k] = v
256+
257+
json_formatted = {
258+
"type": oldone["type"],
259+
"friendly_name": oldone["friendly_name"],
260+
"description": oldone["description"],
261+
"info": oldone["info"],
262+
"variants": variants}
263+
264+
if self.runtime:
265+
selected = None
266+
if oldone["selected"] is not None:
267+
selected = oldone["selected"]
268+
elif newone["selected"] is not None:
269+
selected = newone["selected"]
270+
json_formatted["selected"] = selected
271+
272+
active = None
273+
if oldone["active"] is not None:
274+
active = oldone["active"]
275+
elif newone["active"] is not None:
276+
active = newone["active"]
277+
json_formatted["active"] = active
278+
279+
loaded = oldone["loaded modules"] + newone["loaded modules"]
280+
json_formatted["loaded modules"] = loaded
281+
282+
self.dmv_list["drivers"][oldone["info"]] = json_formatted
283+
284+
def process_dmv_data(self, json_data, json_formatted):
285+
if not json_data["name"] in self.dmv_list["drivers"]:
286+
self.dmv_list["drivers"][json_data["name"]] = json_formatted
287+
elif self.dmv_list["drivers"][json_data["name"]] is None:
288+
self.dmv_list["drivers"][json_data["name"]] = json_formatted
289+
else:
290+
self.merge_jsondata(self.dmv_list["drivers"][json_data["name"]], json_formatted)
291+
292+
def parse_dmv_list(self):
293+
lspci_out = subprocess.run(["lspci", '-nm'], stdout=subprocess.PIPE,
294+
stderr=subprocess.PIPE, universal_newlines=True,
295+
check=True)
296+
for _, updates_dir, dmv_dir in get_all_kabi_dirs():
297+
if not os.path.isdir(dmv_dir):
298+
continue
299+
300+
for path, _, files in os.walk(dmv_dir):
301+
if "info.json" not in files:
302+
continue
303+
304+
fpath = os.path.join(path, "info.json")
305+
d = DriverMultiVersion(updates_dir, lspci_out, self.runtime)
306+
json_data, json_formatted = d.parse_dmv_info(fpath)
307+
self.process_dmv_data(json_data, json_formatted)
308+
309+
def parse_dmv_file(self, fpath):
310+
lspci_out = subprocess.run(["lspci", '-nm'], stdout=subprocess.PIPE,
311+
stderr=subprocess.PIPE, universal_newlines=True,
312+
check=True)
313+
d = DriverMultiVersion("", lspci_out)
314+
json_data, json_formatted = d.parse_dmv_info(fpath)
315+
self.process_dmv_data(json_data, json_formatted)
316+
317+
def get_dmv_list(self):
318+
return self.dmv_list
319+
320+
def create_dmv_symlink(self, name, ver):
321+
created = False
322+
for _, updates_dir, dmv_dir in get_all_kabi_dirs():
323+
module_dir = os.path.join(dmv_dir, name, ver)
324+
module_files = glob.glob(os.path.join(module_dir, "**", "*.ko"), recursive=True)
325+
for module_file in module_files:
326+
# updates_dir may not exist
327+
os.makedirs(updates_dir, exist_ok=True)
328+
module_sym = os.path.join(updates_dir, os.path.basename(module_file))
329+
tmp_name = module_sym + ".tmp"
330+
try:
331+
os.unlink(tmp_name)
332+
except FileNotFoundError:
333+
pass
334+
os.symlink(module_file, tmp_name)
335+
os.rename(tmp_name, module_sym)
336+
created = True
337+
modules = [module_sym]
338+
input_data = "\n".join(modules) + "\n"
339+
subprocess.run(
340+
["/usr/sbin/weak-modules", "--no-initramfs", "--add-modules"],
341+
input=input_data,
342+
text=True,
343+
check=True
344+
)
345+
if created:
346+
subprocess.run(["/usr/sbin/depmod", "-a"], check=True)
347+
uname_r = subprocess.run(["uname", '-r'], stdout=subprocess.PIPE, text=True,
348+
check=True).stdout.strip()
349+
if os.path.exists("/usr/bin/dracut"):
350+
initrd_img = "/boot/initrd-" + uname_r + ".img"
351+
subprocess.run(["/usr/bin/dracut", "-f", initrd_img, uname_r], check=True)
352+
return True
353+
self.errors_list["exit_code"] = errno.ENOENT
354+
self.errors_list["message"] = os.strerror(errno.ENOENT)
355+
return False
356+
357+
def get_dmv_error(self):
358+
return self.errors_list
359+
360+
def set_dmv_error(self, errcode):
361+
self.errors_list["exit_code"] = errcode
362+
self.errors_list["message"] = os.strerror(errcode)

0 commit comments

Comments
 (0)