Skip to content

Commit cb9c412

Browse files
richtjaxianglongfei
authored andcommitted
Fixed the issue in sysinfo where the dmidecode and fdisk -l commands were executed.
Signed-off-by: xianglongfei <xianglongfei@uniontech.com>
2 parents 50bcca0 + bbc65fa commit cb9c412

File tree

9 files changed

+532
-16
lines changed

9 files changed

+532
-16
lines changed

avocado/core/sysinfo.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# This code was inspired in the autotest project,
1212
# client/shared/settings.py
1313
# Author: John Admanski <jadmanski@google.com>
14+
import configparser
1415
import filecmp
1516
import logging
1617
import os
@@ -68,6 +69,47 @@ def __init__(self, basedir=None, log_packages=None, profiler=None):
6869
"""
6970
self.config = settings.as_dict()
7071

72+
# Retrieve the configured paths for sudo commands and distros from the settings dictionary
73+
sudo_commands_conf = self.config.get("sysinfo.sudo_commands", "")
74+
sudo_distros_conf = self.config.get("sysinfo.sudo_distros", "")
75+
76+
if sudo_commands_conf:
77+
log.info("sudo_commands loaded from config: %s", sudo_commands_conf)
78+
else:
79+
log.debug("sudo_commands config is empty or missing")
80+
81+
if sudo_distros_conf:
82+
log.info("sudo_distros loaded from config: %s", sudo_distros_conf)
83+
else:
84+
log.debug("sudo_distros config is empty or missing")
85+
86+
def _load_sudo_list(raw_value, key):
87+
# pylint: disable=wrong-spelling-in-docstring
88+
"""
89+
If `raw_value` is a path to an INI file, read `[sysinfo] / key`
90+
from it; otherwise, treat `raw_value` itself as a CSV list.
91+
"""
92+
if not raw_value:
93+
return ""
94+
if os.path.isfile(raw_value):
95+
parser = configparser.ConfigParser()
96+
parser.read(raw_value)
97+
return parser.get("sysinfo", key, fallback="")
98+
return raw_value
99+
100+
# Retrieve the actual sudo commands and distros values from the config files,
101+
# falling back to empty string if the keys are missing
102+
sudo_commands_value = _load_sudo_list(sudo_commands_conf, "sudo_commands")
103+
sudo_distros_value = _load_sudo_list(sudo_distros_conf, "sudo_distros")
104+
105+
self.sudo_commands = {
106+
cmd.strip().lower() for cmd in sudo_commands_value.split(",") if cmd.strip()
107+
}
108+
109+
self.sudo_distros = {
110+
dst.strip().lower() for dst in sudo_distros_value.split(",") if dst.strip()
111+
}
112+
71113
if basedir is None:
72114
basedir = utils_path.init_dir("sysinfo")
73115
self.basedir = basedir
@@ -136,15 +178,33 @@ def _set_collectibles(self):
136178

137179
for cmd in self.sysinfo_files["commands"]:
138180
self.start_collectibles.add(
139-
sysinfo.Command(cmd, timeout=timeout, locale=locale)
181+
sysinfo.Command(
182+
cmd,
183+
timeout=timeout,
184+
locale=locale,
185+
sudo_commands=self.sudo_commands,
186+
sudo_distros=self.sudo_distros,
187+
)
140188
)
141189
self.end_collectibles.add(
142-
sysinfo.Command(cmd, timeout=timeout, locale=locale)
190+
sysinfo.Command(
191+
cmd,
192+
timeout=timeout,
193+
locale=locale,
194+
sudo_commands=self.sudo_commands,
195+
sudo_distros=self.sudo_distros,
196+
)
143197
)
144198

145199
for fail_cmd in self.sysinfo_files["fail_commands"]:
146200
self.end_fail_collectibles.add(
147-
sysinfo.Command(fail_cmd, timeout=timeout, locale=locale)
201+
sysinfo.Command(
202+
fail_cmd,
203+
timeout=timeout,
204+
locale=locale,
205+
sudo_commands=self.sudo_commands,
206+
sudo_distros=self.sudo_distros,
207+
)
148208
)
149209

150210
for filename in self.sysinfo_files["files"]:

avocado/etc/avocado/sysinfo.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[sysinfo]
2+
sudo_commands = dmidecode,fdisk
3+
# Add any other commands that require sudo here, separated by commas.
4+
sudo_distros = uos,deepin
5+
# Add any other operating system that require sudo here, separated by commas.
6+
# Values of sudo_distros must match the ID= field from /etc/os-release (e.g. uos, deepin).

avocado/plugins/sysinfo.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,28 @@ def initialize(self):
169169
help_msg=help_msg,
170170
)
171171

172+
help_msg = "File with list of commands that require sudo"
173+
default = system_wide_or_base_path("etc/avocado/sysinfo.conf")
174+
settings.register_option(
175+
section="sysinfo",
176+
key="sudo_commands",
177+
key_type=prepend_base_path,
178+
default=default,
179+
help_msg=help_msg,
180+
)
181+
182+
help_msg = (
183+
"File with list of distributions (values matching ID= in /etc/os-release) "
184+
"that require sudo"
185+
)
186+
settings.register_option(
187+
section="sysinfo",
188+
key="sudo_distros",
189+
key_type=prepend_base_path,
190+
default=default,
191+
help_msg=help_msg,
192+
)
193+
172194

173195
class SysInfoJob(JobPreTests, JobPostTests):
174196

avocado/utils/sysinfo.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414
# John Admanski <jadmanski@google.com>
1515

1616
import json
17+
import logging
1718
import os
19+
import platform
1820
import shlex
1921
import subprocess
2022
import tempfile
2123
from abc import ABC, abstractmethod
2224

2325
from avocado.utils import astring, process
26+
from avocado.utils.process import can_sudo
2427

2528
DATA_SIZE = 200000
29+
log = logging.getLogger("avocado.sysinfo")
2630

2731

2832
class CollectibleException(Exception):
@@ -132,12 +136,23 @@ class Command(Collectible):
132136
:param locale: Force LANG for sysinfo collection
133137
"""
134138

135-
def __init__(self, cmd, timeout=-1, locale="C"):
139+
def __init__(
140+
self, cmd, timeout=-1, locale="C", sudo_commands=None, sudo_distros=None
141+
): # pylint: disable=R0913
136142
super().__init__(cmd)
137143
self._name = self.log_path
138144
self.cmd = cmd
139145
self.timeout = timeout
140146
self.locale = locale
147+
self.sudo_commands = sudo_commands
148+
self.sudo_distros = sudo_distros
149+
self._sysinfo_cmd = None
150+
151+
@property
152+
def _sudoer(self):
153+
if self._sysinfo_cmd is None and self.sudo_commands and self.sudo_distros:
154+
self._sysinfo_cmd = SysinfoCommand(self.sudo_commands, self.sudo_distros)
155+
return self._sysinfo_cmd
141156

142157
def __repr__(self):
143158
r = "Command(%r, %r)"
@@ -168,6 +183,13 @@ def collect(self):
168183
# but the avocado.utils.process APIs define no timeouts as "None"
169184
if int(self.timeout) <= 0:
170185
self.timeout = None
186+
187+
# Determine whether to run with sudo (do not mutate the command string)
188+
sudo_flag = False
189+
if self._sudoer:
190+
sudo_flag = self._sudoer.use_sudo() and self._sudoer.is_sudo_cmd(self.cmd)
191+
log.info("Executing Command%s: %s", " (sudo)" if sudo_flag else "", self.cmd)
192+
171193
try:
172194
result = process.run(
173195
self.cmd,
@@ -176,6 +198,7 @@ def collect(self):
176198
ignore_status=True,
177199
shell=True,
178200
env=env,
201+
sudo=sudo_flag,
179202
)
180203
yield result.stdout
181204
except FileNotFoundError as exc_fnf:
@@ -394,3 +417,55 @@ def collect(self):
394417
raise CollectibleException(
395418
f"Not logging {self.path} " f"(lack of permissions)"
396419
) from exc
420+
421+
422+
class SysinfoCommand:
423+
def __init__(self, sudo_commands=None, sudo_distros=None):
424+
self.sudo_cmds = sudo_commands if sudo_commands else set()
425+
self.sudo_distros = sudo_distros if sudo_distros else set()
426+
self.sudo_available = False
427+
# Only attempt sudo capability detection on Linux, where it is relevant.
428+
if platform.system().lower() == "linux":
429+
self.sudo_available = can_sudo()
430+
431+
def use_sudo(self):
432+
"""
433+
Determine if 'sudo' should be used based on the system type.
434+
435+
Returns:
436+
bool: True if 'sudo' should be used, False otherwise.
437+
"""
438+
if not self.sudo_available:
439+
return False
440+
system_name = platform.system().lower()
441+
if system_name == "linux":
442+
if hasattr(os, "geteuid") and not os.geteuid():
443+
return False
444+
try:
445+
with open("/etc/os-release", encoding="utf-8") as f:
446+
for line in f:
447+
if line.startswith("ID="):
448+
os_id = line.strip().split("=")[1].strip('"')
449+
return os_id.lower() in self.sudo_distros
450+
except FileNotFoundError:
451+
log.debug("/etc/os-release not found.")
452+
return False
453+
return False
454+
return False
455+
456+
def is_sudo_cmd(self, cmd):
457+
"""
458+
Determine if 'sudo' should be used for a specific command based on the configuration.
459+
460+
Args:
461+
cmd (str): The command to check.
462+
463+
Returns:
464+
bool: True if 'sudo' should be used, False otherwise.
465+
"""
466+
try:
467+
first = shlex.split(cmd or "")[0]
468+
except (ValueError, IndexError):
469+
return False
470+
base = os.path.basename(first).lower()
471+
return base in self.sudo_cmds

avocado/utils/wait.py

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
"""Utilities for waiting for conditions to be met.
2+
3+
This module provides utilities for polling functions until they return
4+
a truthy value or a timeout expires, useful for testing and development
5+
scenarios where you need to wait for system state changes.
6+
"""
7+
18
import logging
29
import time
310

@@ -6,18 +13,75 @@
613

714
# pylint: disable=R0913
815
def wait_for(func, timeout, first=0.0, step=1.0, text=None, args=None, kwargs=None):
9-
"""
10-
Wait until func() evaluates to True.
16+
"""Wait until a function returns a truthy value or timeout expires.
17+
18+
This function repeatedly calls a given function with optional arguments
19+
until it returns a truthy value (anything that evaluates to True in a
20+
boolean context) or until the specified timeout expires. It provides
21+
configurable delays before the first attempt and between subsequent
22+
attempts, making it useful for polling operations in testing and
23+
development scenarios.
24+
25+
The function uses time.monotonic() for reliable timeout calculation that
26+
is not affected by system clock adjustments. Note that the step sleep
27+
duration is not interrupted when timeout expires, so actual elapsed time
28+
may exceed the specified timeout by up to one step duration.
29+
30+
:param func: Callable to be executed repeatedly until it returns a truthy
31+
value. Can be any callable object (function, lambda, method,
32+
callable class instance).
33+
:type func: callable
34+
:param timeout: Maximum time in seconds to wait for func to return a
35+
truthy value. Must be a non-negative number. If timeout
36+
expires before func returns truthy, None is returned.
37+
:type timeout: float or int
38+
:param first: Time in seconds to sleep before the first attempt to call
39+
func. Useful when you know the condition won't be met
40+
immediately. Defaults to 0.0 (no initial delay).
41+
:type first: float or int
42+
:param step: Time in seconds to sleep between successive calls to func.
43+
The actual sleep happens after each failed attempt. Defaults
44+
to 1.0 second. Note that this sleep is not interrupted when
45+
timeout expires.
46+
:type step: float or int
47+
:param text: Optional debug message to log before each attempt. When
48+
provided, logs at DEBUG level with elapsed time since start.
49+
If None, no logging occurs. Useful for debugging wait
50+
operations.
51+
:type text: str or None
52+
:param args: Optional list or tuple of positional arguments to pass to
53+
func on each call. If None, defaults to empty list.
54+
:type args: list, tuple, or None
55+
:param kwargs: Optional dictionary of keyword arguments to pass to func on
56+
each call. If None, defaults to empty dict.
57+
:type kwargs: dict or None
58+
:return: The truthy return value from func if it succeeds within timeout,
59+
or None if timeout expires without func returning a truthy value.
60+
The actual return value from func is preserved (e.g., strings,
61+
numbers, lists, objects).
62+
:rtype: Any (return type of func) or None
63+
:raises: Any exception raised by func will be propagated to the caller.
64+
No exception handling is performed on func calls.
1165
12-
If func() evaluates to True before timeout expires, return the
13-
value of func(). Otherwise return None.
66+
Example::
1467
15-
:param timeout: Timeout in seconds
16-
:param first: Time to sleep before first attempt
17-
:param step: Time to sleep between attempts in seconds
18-
:param text: Text to print while waiting, for debug purposes
19-
:param args: Positional arguments to func
20-
:param kwargs: Keyword arguments to func
68+
>>> import os
69+
>>> # Wait for a file to exist
70+
>>> wait_for(lambda: os.path.exists("/tmp/myfile"), timeout=30, step=1)
71+
True
72+
>>> # Wait for a counter to reach threshold
73+
>>> counter = [0]
74+
>>> def check(): counter[0] += 1; return counter[0] >= 5
75+
>>> wait_for(check, timeout=10, step=0.5)
76+
True
77+
>>> # Wait with custom function and arguments
78+
>>> def check_value(expected, current):
79+
... return current >= expected
80+
>>> wait_for(check_value, timeout=5, step=0.1, args=[10, 15])
81+
True
82+
>>> # Wait with debug logging
83+
>>> wait_for(lambda: False, timeout=2, step=0.5, text="Waiting for condition")
84+
None
2185
"""
2286
args = args or []
2387
kwargs = kwargs or {}

selftests/check.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
"job-api-check-tmp-directory-exists": 1,
2828
"nrunner-interface": 90,
2929
"nrunner-requirement": 28,
30-
"unit": 874,
30+
"unit": 900,
3131
"jobs": 11,
32-
"functional-parallel": 342,
32+
"functional-parallel": 344,
3333
"functional-serial": 7,
3434
"optional-plugins": 0,
3535
"optional-plugins-golang": 2,

0 commit comments

Comments
 (0)