Skip to content

Commit 81fe31b

Browse files
adityageshLiliDeng
authored andcommitted
Modprobe Tool: Improve failure debugging
If Modprobe fails to remove a module, run some additional commands to debug why it failed
1 parent 64963e6 commit 81fe31b

File tree

4 files changed

+195
-19
lines changed

4 files changed

+195
-19
lines changed

lisa/tools/dmesg.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Licensed under the MIT license.
33

44
import re
5-
from typing import List
5+
from typing import List, Optional
66

77
from semver import VersionInfo
88

@@ -49,11 +49,26 @@ def command(self) -> str:
4949
def _check_exists(self) -> bool:
5050
return True
5151

52-
def get_output(self, force_run: bool = False) -> str:
53-
command_output = self._run(force_run=force_run)
52+
def get_output(
53+
self,
54+
force_run: bool = False,
55+
no_error_log: bool = True,
56+
no_info_log: bool = True,
57+
no_debug_log: bool = False,
58+
tail_lines: Optional[int] = None,
59+
) -> str:
60+
command_output = self._run(
61+
force_run=force_run,
62+
no_error_log=no_error_log,
63+
no_info_log=no_info_log,
64+
no_debug_log=no_debug_log,
65+
)
5466

5567
# Remove the color code from stdout stream
5668
stdout = filter_ansi_escape(command_output.stdout)
69+
if tail_lines is not None:
70+
stdout_lines = stdout.splitlines()
71+
stdout = "\n".join(stdout_lines[-tail_lines:])
5772
return stdout
5873

5974
def check_kernel_errors(
@@ -104,12 +119,29 @@ def get_vmbus_version(self) -> VersionInfo:
104119
return VersionInfo(int(major), int(minor))
105120
raise LisaException("No find matched vmbus version in dmesg")
106121

107-
def _run(self, force_run: bool = False) -> ExecutableResult:
108-
# sometime it need sudo, we can retry
109-
# so no_error_log for first time
110-
result = self.run(force_run=force_run, no_error_log=True)
122+
def _run(
123+
self,
124+
force_run: bool = False,
125+
no_error_log: bool = True,
126+
no_info_log: bool = True,
127+
no_debug_log: bool = False,
128+
) -> ExecutableResult:
129+
# no_error_log is set to True for the first run because
130+
# it will fail for distros that require sudo to run dmesg.
131+
result = self.run(
132+
force_run=force_run,
133+
no_error_log=True,
134+
no_info_log=no_info_log,
135+
no_debug_log=no_debug_log,
136+
)
111137
if result.exit_code != 0:
112138
# may need sudo
113-
result = self.run(sudo=True, force_run=force_run)
139+
result = self.run(
140+
sudo=True,
141+
force_run=force_run,
142+
no_error_log=no_error_log,
143+
no_info_log=no_info_log,
144+
no_debug_log=no_debug_log,
145+
)
114146
self._cached_result = result
115147
return result

lisa/tools/journalctl.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,40 @@ def first_n_logs_from_boot(
5353
expected_exit_code=0,
5454
)
5555
return result.stdout
56+
57+
def filter_logs_by_pattern(
58+
self,
59+
pattern: str,
60+
tail_line_count: int = 1000,
61+
no_debug_log: bool = False,
62+
no_error_log: bool = False,
63+
no_info_log: bool = True,
64+
sudo: bool = True,
65+
) -> str:
66+
"""
67+
Filters journalctl logs by a given pattern using grep.
68+
69+
Args:
70+
pattern (str): Passed directly to grep. Use a valid grep pattern
71+
(string or regex).
72+
tail_line_count (int): Optionally limits the output to the last N lines.
73+
"""
74+
cmd = f"--no-pager | grep '{pattern}'"
75+
76+
if tail_line_count > 0:
77+
cmd = cmd + f" | tail -n {tail_line_count}"
78+
79+
result = self.run(
80+
cmd,
81+
force_run=True,
82+
shell=True,
83+
sudo=sudo,
84+
no_debug_log=no_debug_log,
85+
no_error_log=no_error_log,
86+
no_info_log=no_info_log,
87+
expected_exit_code=0,
88+
expected_exit_code_failure_message=(
89+
f"Failed to filter journalctl logs by pattern '{pattern}'"
90+
),
91+
)
92+
return result.stdout

lisa/tools/lsmod.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
# Licensed under the MIT license.
33

44
import re
5-
from typing import Any
5+
from typing import Any, Tuple
66

77
from lisa.executable import Tool
8-
from lisa.util import find_patterns_in_lines
8+
from lisa.util import find_patterns_groups_in_lines, find_patterns_in_lines
99

1010

1111
class Lsmod(Tool):
@@ -14,6 +14,7 @@ class Lsmod(Tool):
1414
# fuse 52176 3
1515
# cryptd 14125 0
1616
# aes_generic 32970 1 aes_i586
17+
# ip_tables 32768 3 iptable_filter,iptable_security,iptable_nat
1718
__output_pattern = re.compile(
1819
r"^(?P<name>[^\s]+)\s+(?P<size>[^\s]+)\s+(?P<usedby>.*)?$", re.MULTILINE
1920
)
@@ -34,12 +35,14 @@ def module_exists(
3435
force_run: bool = False,
3536
no_info_log: bool = True,
3637
no_error_log: bool = True,
38+
no_debug_log: bool = False,
3739
) -> bool:
3840
result = self.run(
3941
sudo=True,
4042
force_run=force_run,
4143
no_info_log=no_info_log,
4244
no_error_log=no_error_log,
45+
no_debug_log=no_debug_log,
4346
expected_exit_code=0,
4447
)
4548

@@ -48,3 +51,43 @@ def module_exists(
4851
return True
4952

5053
return False
54+
55+
def get_used_by_modules(
56+
self,
57+
mod_name: str,
58+
sudo: bool = True,
59+
force_run: bool = False,
60+
no_info_log: bool = True,
61+
no_error_log: bool = True,
62+
no_debug_log: bool = True,
63+
) -> Tuple[int, str]:
64+
"""
65+
Returns a list of modules that are using the given module name.
66+
"""
67+
result = self.run(
68+
sudo=sudo,
69+
force_run=force_run,
70+
no_info_log=no_info_log,
71+
no_error_log=no_error_log,
72+
no_debug_log=no_debug_log,
73+
expected_exit_code=0,
74+
)
75+
76+
module_info = find_patterns_groups_in_lines(
77+
result.stdout, [self.__output_pattern]
78+
)
79+
80+
target_module = next(
81+
(module for module in module_info[0] if module["name"] == mod_name), None
82+
)
83+
84+
# If the module is not found or has no 'usedby' information,
85+
# return 0 and empty string
86+
if target_module is None or not target_module.get("usedby"):
87+
return 0, ""
88+
89+
usedby_split = target_module["usedby"].split(maxsplit=1)
90+
usedby_count = int(usedby_split[0]) if usedby_split[0].isdigit() else 0
91+
usedby_modules = usedby_split[1] if len(usedby_split) > 1 else ""
92+
93+
return usedby_count, usedby_modules

lisa/tools/modprobe.py

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from typing import Any, List, Optional, Type, Union
44

55
from lisa.executable import ExecutableResult, Tool
6+
from lisa.tools.dmesg import Dmesg
7+
from lisa.tools.journalctl import Journalctl
68
from lisa.tools.kernel_config import KLDStat
9+
from lisa.tools.lsmod import Lsmod
710
from lisa.util import UnsupportedOperationException
811

912

@@ -74,15 +77,20 @@ def remove(self, mod_names: List[str], ignore_error: bool = False) -> None:
7477
self.node.execute(f"rmmod {mod_name}", sudo=True, shell=True)
7578
else:
7679
if self.is_module_loaded(mod_name, force_run=True):
77-
self.run(
78-
f"-r {mod_name}",
79-
force_run=True,
80-
sudo=True,
81-
shell=True,
82-
expected_exit_code=0,
83-
expected_exit_code_failure_message="Fail to remove module "
84-
f"{mod_name}",
85-
)
80+
try:
81+
self.run(
82+
f"-r {mod_name}",
83+
force_run=True,
84+
sudo=True,
85+
shell=True,
86+
expected_exit_code=0,
87+
expected_exit_code_failure_message=(
88+
f"Fail to remove module {mod_name}"
89+
),
90+
)
91+
except AssertionError as e:
92+
self._collect_logs(mod_name)
93+
raise e
8694

8795
def load(
8896
self,
@@ -154,6 +162,62 @@ def load_by_file(
154162
result.assert_exit_code(0, f"failed to load module {file_name}")
155163
return result
156164

165+
def _collect_logs(self, mod_name: str) -> None:
166+
self._log.info(
167+
f"Failed to remove module {mod_name}."
168+
" Collecting additional debug information."
169+
)
170+
self._log_lsmod_status(mod_name)
171+
self._tail_dmesg_log()
172+
self._tail_journalctl_for_module(mod_name)
173+
174+
def _log_lsmod_status(self, mod_name: str) -> None:
175+
lsmod_tool = self.node.tools[Lsmod]
176+
module_exists = lsmod_tool.module_exists(
177+
mod_name=mod_name,
178+
force_run=True,
179+
no_debug_log=True,
180+
)
181+
if not module_exists:
182+
self._log.info(f"Module {mod_name} does not exist in lsmod output.")
183+
else:
184+
self._log.info(f"Module {mod_name} exists in lsmod output.")
185+
usedby_count, usedby_modules = lsmod_tool.get_used_by_modules(
186+
mod_name, sudo=True, force_run=True
187+
)
188+
if usedby_count == 0:
189+
self._log.info(f"Module '{mod_name}' is not used by any other modules.")
190+
else:
191+
self._log.info(
192+
f"Module {mod_name} is used by {usedby_count} modules. "
193+
f"Module names: '{usedby_modules}'"
194+
)
195+
196+
def _tail_dmesg_log(self) -> None:
197+
dmesg_tool = self.node.tools[Dmesg]
198+
tail_lines = 20
199+
200+
dmesg_output = dmesg_tool.get_output(
201+
force_run=True, no_debug_log=True, tail_lines=tail_lines
202+
)
203+
204+
self._log.info(f"Last {tail_lines} dmesg lines: {dmesg_output}")
205+
206+
def _tail_journalctl_for_module(self, mod_name: str) -> None:
207+
tail_lines = 20
208+
journalctl_tool = self.node.tools[Journalctl]
209+
journalctl_out = journalctl_tool.filter_logs_by_pattern(
210+
mod_name,
211+
tail_line_count=tail_lines,
212+
no_debug_log=True,
213+
no_error_log=True,
214+
no_info_log=True,
215+
)
216+
self._log.info(
217+
f"Last {tail_lines} journalctl lines for module {mod_name}:\n"
218+
f"{journalctl_out}"
219+
)
220+
157221

158222
class ModprobeFreeBSD(Modprobe):
159223
def module_exists(self, modules: Union[str, List[str]]) -> bool:

0 commit comments

Comments
 (0)