Skip to content

Commit cbea52d

Browse files
authored
Merge pull request avocado-framework#6225 from dhsrivas/master
utils: Add PCI device, drivers, modules, dmesg related avocado utilities
2 parents fdf8aee + e51a2bf commit cbea52d

File tree

7 files changed

+283
-1
lines changed

7 files changed

+283
-1
lines changed

avocado/utils/dmesg.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,22 @@ def filter_strings(line):
182182
return not any(string in line for string in skip_messages)
183183

184184
return "\n".join(filter(None, filter(filter_strings, dmesg_stdout.splitlines())))
185+
186+
187+
def check_kernel_logs(pattern):
188+
"""
189+
Check if "pattern" is present in kernel logs.
190+
191+
:param pattern: string to check in kernel logs
192+
:return: True if pattern found, False otherwise
193+
"""
194+
out = process.run("dmesg", ignore_status=True, verbose=False, sudo=True)
195+
if pattern in out.stdout_text:
196+
return True
197+
198+
cmd = "journalctl -k -b"
199+
out = process.run(cmd, ignore_status=True, verbose=False, sudo=True)
200+
if pattern in out.stdout_text:
201+
return True
202+
203+
return False

avocado/utils/linux_modules.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,31 @@ def parse_kernel_config(kernel_config):
264264
return parse_kernel_config(kernel_config)
265265

266266

267+
def configure_module(module, config):
268+
"""
269+
Check if 'config' is not set, builtin or module.
270+
Load 'module' if 'config' is set as module.
271+
272+
:param module: kernel module to configure
273+
:param config: kernel config to check and validate
274+
:return: True if module is builtin or loaded successfully. Else False.
275+
:rtype: boolean
276+
"""
277+
config_status = check_kernel_config(config)
278+
if config_status == ModuleConfig.BUILTIN:
279+
LOG.info("%s is built-in. %s already loaded.", config, module)
280+
return True
281+
282+
if config_status == ModuleConfig.MODULE:
283+
if load_module(module) and module_is_loaded(module):
284+
LOG.info("Module %s loaded successfully", module)
285+
return True
286+
LOG.error("Failed to load module %s", module)
287+
288+
LOG.error("%s is not set. %s cannot be loaded.", config, module)
289+
return False
290+
291+
267292
def get_modules_dir():
268293
"""
269294
Return the modules dir for the running kernel version

avocado/utils/pci.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"""
2222

2323

24+
import errno
2425
import os
2526
import re
2627

@@ -377,6 +378,94 @@ def get_vendor_id(full_pci_address):
377378
return out.split(" ")[2].strip()
378379

379380

381+
def add_vendor_id(full_pci_address, driver):
382+
"""
383+
Retrieve and add the vendor ID of a PCI device to the specified driver.
384+
385+
:param full_pci_address: Full PCI device address, including domain
386+
(e.g., 0000:03:00.0).
387+
:param driver: Driver to associate with the vendor ID of the PCI device.
388+
"""
389+
# Get vendor id of pci device
390+
output = get_vendor_id(full_pci_address)
391+
vid = output.replace(":", " ")
392+
393+
# Add device vendor id to the driver
394+
try:
395+
genio.write_file(f"/sys/bus/pci/drivers/{driver}/new_id", f"{vid}\n")
396+
except OSError as e:
397+
if e.errno != errno.EEXIST:
398+
raise ValueError(
399+
f"Failed to add vendor ID '{vid}' to driver '{driver}': {e}"
400+
) from e
401+
402+
403+
def attach_driver(full_pci_address, driver):
404+
"""
405+
Unbind the device from its existing driver and bind it to the given driver.
406+
407+
:param full_pci_address: Full PCI device address (e.g., 0000:03:00.0)
408+
:param driver: Driver to be attached to the specified PCI device
409+
:raises ValueError: If `driver` or `full_pci_address` is None, or if the driver
410+
could not be attached successfully due to an OS error.
411+
:warning: This function may unbind the device from its current driver, which can
412+
temporarily make the device unavailable until it is reattached.
413+
"""
414+
415+
try:
416+
if not driver or not full_pci_address:
417+
raise ValueError("'driver' or 'full_pci_address' inputs are None")
418+
419+
# add vendor id of device to driver
420+
add_vendor_id(full_pci_address, driver)
421+
422+
# unbind the device from its initial driver
423+
cur_driver = get_driver(full_pci_address)
424+
if cur_driver is not None:
425+
unbind(cur_driver, full_pci_address)
426+
427+
# Bind device to driver
428+
bind(driver, full_pci_address)
429+
430+
except (OSError, ValueError, genio.GenIOError) as e:
431+
raise ValueError(
432+
f"Not able to attach {driver} to {full_pci_address}. Reason: {e}"
433+
) from e
434+
435+
436+
def check_msix_capability(full_pci_address):
437+
"""
438+
Check whether the PCI device supports Extended Message Signaled Interrupts.
439+
440+
:param full_pci_address: Full PCI address including domain (0000:03:00.0)
441+
:return: True if supported, False otherwise
442+
"""
443+
out = process.run(f"lspci -vvs {full_pci_address}", ignore_status=True, shell=True)
444+
if out.exit_status:
445+
raise ValueError(f"lspci failed for {full_pci_address}: {out.stderr_text}")
446+
return any("MSI-X:" in line for line in out.stdout_text.splitlines())
447+
448+
449+
def device_supports_irqs(full_pci_address, count):
450+
"""
451+
Check if the device supports at least the specified number of interrupts.
452+
453+
:param full_pci_address: Full PCI device address including domain (e.g., 0000:03:00.0)
454+
:param count: Number of IRQs the device should support
455+
:return: True if supported, False otherwise
456+
"""
457+
out = process.run(
458+
f"lspci -vv -s {full_pci_address}", ignore_status=True, shell=True
459+
)
460+
if out.exit_status:
461+
raise ValueError(f"lspci failed for {full_pci_address}: {out.stderr_text}")
462+
match = re.search(r"MSI-X:.*?Count=(\d+)", out.stdout_text, re.S)
463+
if match:
464+
nirq = int(match.group(1))
465+
return nirq >= count
466+
return False
467+
468+
380469
def reset_check(full_pci_address):
381470
"""
382471
Check if reset for "full_pci_address" is successful

selftests/check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"job-api-check-tmp-directory-exists": 1,
2828
"nrunner-interface": 90,
2929
"nrunner-requirement": 28,
30-
"unit": 950,
30+
"unit": 965,
3131
"jobs": 11,
3232
"functional-parallel": 366,
3333
"functional-serial": 7,

selftests/unit/utils/dmesg.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import unittest
2+
from unittest import mock
3+
4+
from avocado import Test
5+
from avocado.utils import dmesg
6+
7+
8+
class TestCheckKernelLogs(Test):
9+
@mock.patch("avocado.utils.dmesg.process.run")
10+
def test_pattern_found_in_dmesg(self, mock_run):
11+
# Simulate `dmesg` output containing the pattern
12+
mock_run.return_value.stdout_text = "kernel: test_pattern detected"
13+
mock_run.return_value.exit_status = 0
14+
15+
result = dmesg.check_kernel_logs("test_pattern")
16+
self.assertTrue(result)
17+
mock_run.assert_called_with(
18+
"dmesg", ignore_status=True, verbose=False, sudo=True
19+
)
20+
21+
@mock.patch("avocado.utils.dmesg.process.run")
22+
def test_pattern_found_in_journalctl(self, mock_run):
23+
dmesg_out = mock.Mock()
24+
dmesg_out.stdout_text = "kernel: something else"
25+
dmesg_out.exit_status = 0
26+
27+
journal_out = mock.Mock()
28+
journal_out.stdout_text = "kernel: test_pattern here"
29+
journal_out.exit_status = 0
30+
31+
mock_run.side_effect = [dmesg_out, journal_out]
32+
33+
result = dmesg.check_kernel_logs("test_pattern")
34+
self.assertTrue(result)
35+
36+
@mock.patch("avocado.utils.dmesg.process.run")
37+
def test_pattern_not_found(self, mock_run):
38+
dmesg_out = mock.Mock()
39+
dmesg_out.stdout_text = "kernel: no match"
40+
dmesg_out.exit_status = 0
41+
42+
journal_out = mock.Mock()
43+
journal_out.stdout_text = "kernel: still no match"
44+
journal_out.exit_status = 0
45+
46+
mock_run.side_effect = [dmesg_out, journal_out]
47+
48+
result = dmesg.check_kernel_logs("test_pattern")
49+
self.assertFalse(result)
50+
51+
52+
if __name__ == "__main__":
53+
unittest.main()

selftests/unit/utils/linux_modules.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,38 @@ def test_is_module_loaded(self):
6363
self.assertTrue(linux_modules.module_is_loaded("rfcomm"))
6464
self.assertFalse(linux_modules.module_is_loaded("unknown_module"))
6565

66+
@unittest.mock.patch("avocado.utils.linux_modules.check_kernel_config")
67+
def test_configure_module_1(self, mock_check_kernel_config):
68+
mock_check_kernel_config.return_value = linux_modules.ModuleConfig.NOT_SET
69+
self.assertFalse(linux_modules.configure_module("mod", "CONFIG_MOD"))
70+
71+
@unittest.mock.patch("avocado.utils.linux_modules.module_is_loaded")
72+
@unittest.mock.patch("avocado.utils.linux_modules.load_module")
73+
@unittest.mock.patch("avocado.utils.linux_modules.check_kernel_config")
74+
def test_configure_module_2(self, mock_check, mock_load, mock_loaded):
75+
mock_check.return_value = linux_modules.ModuleConfig.BUILTIN
76+
self.assertTrue(linux_modules.configure_module("mod", "CONFIG_MOD"))
77+
mock_load.assert_not_called()
78+
mock_loaded.assert_not_called()
79+
80+
@unittest.mock.patch("avocado.utils.linux_modules.module_is_loaded")
81+
@unittest.mock.patch("avocado.utils.linux_modules.load_module")
82+
@unittest.mock.patch("avocado.utils.linux_modules.check_kernel_config")
83+
def test_configure_module_3(self, mock_check, mock_load, mock_loaded):
84+
mock_check.return_value = linux_modules.ModuleConfig.MODULE
85+
mock_load.return_value = True
86+
mock_loaded.return_value = True
87+
self.assertTrue(linux_modules.configure_module("mod", "CONFIG_MOD"))
88+
89+
@unittest.mock.patch("avocado.utils.linux_modules.module_is_loaded")
90+
@unittest.mock.patch("avocado.utils.linux_modules.load_module")
91+
@unittest.mock.patch("avocado.utils.linux_modules.check_kernel_config")
92+
def test_configure_module_4(self, mock_check, mock_load, mock_loaded):
93+
mock_check.return_value = linux_modules.ModuleConfig.MODULE
94+
mock_load.return_value = False
95+
mock_loaded.return_value = False
96+
self.assertFalse(linux_modules.configure_module("mod", "CONFIG_MOD"))
97+
6698

6799
if __name__ == "__main__":
68100
unittest.main()

selftests/unit/utils/pci.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import errno
12
import unittest.mock
23

34
from avocado.utils import pci
@@ -42,6 +43,69 @@ def test_get_slot_from_sysfs_negative(self):
4243
):
4344
self.assertRaises(ValueError, pci.get_slot_from_sysfs, "0002:01:00.1")
4445

46+
@unittest.mock.patch("avocado.utils.pci.get_vendor_id")
47+
@unittest.mock.patch("avocado.utils.pci.genio.write_file")
48+
def test_add_vendor_id_success(self, mock_write, mock_get_vid):
49+
mock_get_vid.return_value = "1234:abcd"
50+
pci.add_vendor_id("0000:03:00.0", "driver")
51+
mock_get_vid.assert_called_once_with("0000:03:00.0")
52+
mock_write.assert_called_once_with(
53+
"/sys/bus/pci/drivers/driver/new_id", "1234 abcd\n"
54+
)
55+
56+
@unittest.mock.patch("avocado.utils.pci.get_vendor_id", return_value="1234:abcd")
57+
@unittest.mock.patch(
58+
"avocado.utils.pci.genio.write_file",
59+
side_effect=OSError(errno.EEXIST, "File exists"),
60+
)
61+
def test_add_vendor_id_already_exists(self, mock_write, mock_get_vid):
62+
pci.add_vendor_id("0000:03:00.0", "driver")
63+
mock_get_vid.assert_called_once_with("0000:03:00.0")
64+
mock_write.assert_called_once_with(
65+
"/sys/bus/pci/drivers/driver/new_id", "1234 abcd\n"
66+
)
67+
68+
@unittest.mock.patch("avocado.utils.pci.bind")
69+
@unittest.mock.patch("avocado.utils.pci.unbind")
70+
@unittest.mock.patch("avocado.utils.pci.get_driver")
71+
@unittest.mock.patch("avocado.utils.pci.add_vendor_id")
72+
def test_attach_driver(self, mock_add_vid, mock_get_driver, mock_unbind, mock_bind):
73+
mock_get_driver.return_value = "old_driver"
74+
pci.attach_driver("0000:03:00.0", "new_driver")
75+
mock_add_vid.assert_called_once()
76+
mock_unbind.assert_called_once_with("old_driver", "0000:03:00.0")
77+
mock_bind.assert_called_once_with("new_driver", "0000:03:00.0")
78+
79+
@unittest.mock.patch("avocado.utils.pci.process.run")
80+
def test_check_msix_capability_supported(self, mock_run):
81+
mock_run.return_value.exit_status = 0
82+
mock_run.return_value.stdout_text = "Capabilities: [90] MSI-X: Enable+ Count=16"
83+
self.assertTrue(pci.check_msix_capability("0000:03:00.0"))
84+
85+
@unittest.mock.patch("avocado.utils.pci.process.run")
86+
def test_check_msix_capability_unsupported(self, mock_run):
87+
mock_run.return_value.exit_status = 0
88+
mock_run.return_value.stdout_text = "Capabilities: [90] MSI: Enable+ Count=16"
89+
self.assertFalse(pci.check_msix_capability("0000:03:00.0"))
90+
91+
@unittest.mock.patch("avocado.utils.pci.process.run")
92+
def test_device_supports_irqs_enough(self, mock_run):
93+
mock_run.return_value.exit_status = 0
94+
mock_run.return_value.stdout_text = "MSI-X: Enable+ Count=64"
95+
self.assertTrue(pci.device_supports_irqs("0000:03:00.0", 32))
96+
97+
@unittest.mock.patch("avocado.utils.pci.process.run")
98+
def test_device_supports_irqs_insufficient(self, mock_run):
99+
mock_run.return_value.exit_status = 0
100+
mock_run.return_value.stdout_text = "MSI-X: Enable+ Count=4"
101+
self.assertFalse(pci.device_supports_irqs("0000:03:00.0", 16))
102+
103+
@unittest.mock.patch("avocado.utils.pci.process.run")
104+
def test_device_supports_irqs_no_msix(self, mock_run):
105+
mock_run.return_value.exit_status = 0
106+
mock_run.return_value.stdout_text = "Capabilities: [90] MSI: Enable+ Count=16"
107+
self.assertFalse(pci.device_supports_irqs("0000:03:00.0", 8))
108+
45109

46110
if __name__ == "__main__":
47111
unittest.main()

0 commit comments

Comments
 (0)