diff --git a/avocado/core/sysinfo.py b/avocado/core/sysinfo.py index cba1bfe51a..745dcf9a44 100644 --- a/avocado/core/sysinfo.py +++ b/avocado/core/sysinfo.py @@ -11,6 +11,7 @@ # This code was inspired in the autotest project, # client/shared/settings.py # Author: John Admanski +import configparser import filecmp import logging import os @@ -68,6 +69,47 @@ def __init__(self, basedir=None, log_packages=None, profiler=None): """ self.config = settings.as_dict() + # Retrieve the configured paths for sudo commands and distros from the settings dictionary + sudo_commands_conf = self.config.get("sysinfo.sudo_commands", "") + sudo_distros_conf = self.config.get("sysinfo.sudo_distros", "") + + if sudo_commands_conf: + log.info("sudo_commands loaded from config: %s", sudo_commands_conf) + else: + log.debug("sudo_commands config is empty or missing") + + if sudo_distros_conf: + log.info("sudo_distros loaded from config: %s", sudo_distros_conf) + else: + log.debug("sudo_distros config is empty or missing") + + def _load_sudo_list(raw_value, key): + # pylint: disable=wrong-spelling-in-docstring + """ + If `raw_value` is a path to an INI file, read `[sysinfo] / key` + from it; otherwise, treat `raw_value` itself as a CSV list. + """ + if not raw_value: + return "" + if os.path.isfile(raw_value): + parser = configparser.ConfigParser() + parser.read(raw_value) + return parser.get("sysinfo", key, fallback="") + return raw_value + + # Retrieve the actual sudo commands and distros values from the config files, + # falling back to empty string if the keys are missing + sudo_commands_value = _load_sudo_list(sudo_commands_conf, "sudo_commands") + sudo_distros_value = _load_sudo_list(sudo_distros_conf, "sudo_distros") + + self.sudo_commands = { + cmd.strip().lower() for cmd in sudo_commands_value.split(",") if cmd.strip() + } + + self.sudo_distros = { + dst.strip().lower() for dst in sudo_distros_value.split(",") if dst.strip() + } + if basedir is None: basedir = utils_path.init_dir("sysinfo") self.basedir = basedir @@ -136,15 +178,33 @@ def _set_collectibles(self): for cmd in self.sysinfo_files["commands"]: self.start_collectibles.add( - sysinfo.Command(cmd, timeout=timeout, locale=locale) + sysinfo.Command( + cmd, + timeout=timeout, + locale=locale, + sudo_commands=self.sudo_commands, + sudo_distros=self.sudo_distros, + ) ) self.end_collectibles.add( - sysinfo.Command(cmd, timeout=timeout, locale=locale) + sysinfo.Command( + cmd, + timeout=timeout, + locale=locale, + sudo_commands=self.sudo_commands, + sudo_distros=self.sudo_distros, + ) ) for fail_cmd in self.sysinfo_files["fail_commands"]: self.end_fail_collectibles.add( - sysinfo.Command(fail_cmd, timeout=timeout, locale=locale) + sysinfo.Command( + fail_cmd, + timeout=timeout, + locale=locale, + sudo_commands=self.sudo_commands, + sudo_distros=self.sudo_distros, + ) ) for filename in self.sysinfo_files["files"]: diff --git a/avocado/etc/avocado/sysinfo.conf b/avocado/etc/avocado/sysinfo.conf new file mode 100644 index 0000000000..8b2477d947 --- /dev/null +++ b/avocado/etc/avocado/sysinfo.conf @@ -0,0 +1,6 @@ +[sysinfo] +sudo_commands = dmidecode,fdisk +# Add any other commands that require sudo here, separated by commas. +sudo_distros = uos,deepin +# Add any other operating system that require sudo here, separated by commas. +# Values of sudo_distros must match the ID= field from /etc/os-release (e.g. uos, deepin). diff --git a/avocado/plugins/sysinfo.py b/avocado/plugins/sysinfo.py index a6ee797cfc..a1b4d1445e 100644 --- a/avocado/plugins/sysinfo.py +++ b/avocado/plugins/sysinfo.py @@ -169,6 +169,28 @@ def initialize(self): help_msg=help_msg, ) + help_msg = "File with list of commands that require sudo" + default = system_wide_or_base_path("etc/avocado/sysinfo.conf") + settings.register_option( + section="sysinfo", + key="sudo_commands", + key_type=prepend_base_path, + default=default, + help_msg=help_msg, + ) + + help_msg = ( + "File with list of distributions (values matching ID= in /etc/os-release) " + "that require sudo" + ) + settings.register_option( + section="sysinfo", + key="sudo_distros", + key_type=prepend_base_path, + default=default, + help_msg=help_msg, + ) + class SysInfoJob(JobPreTests, JobPostTests): diff --git a/avocado/utils/sysinfo.py b/avocado/utils/sysinfo.py index 4e62462ce2..9061fe377b 100644 --- a/avocado/utils/sysinfo.py +++ b/avocado/utils/sysinfo.py @@ -14,15 +14,19 @@ # John Admanski import json +import logging import os +import platform import shlex import subprocess import tempfile from abc import ABC, abstractmethod from avocado.utils import astring, process +from avocado.utils.process import can_sudo DATA_SIZE = 200000 +log = logging.getLogger("avocado.sysinfo") class CollectibleException(Exception): @@ -132,12 +136,23 @@ class Command(Collectible): :param locale: Force LANG for sysinfo collection """ - def __init__(self, cmd, timeout=-1, locale="C"): + def __init__( + self, cmd, timeout=-1, locale="C", sudo_commands=None, sudo_distros=None + ): # pylint: disable=R0913 super().__init__(cmd) self._name = self.log_path self.cmd = cmd self.timeout = timeout self.locale = locale + self.sudo_commands = sudo_commands + self.sudo_distros = sudo_distros + self._sysinfo_cmd = None + + @property + def _sudoer(self): + if self._sysinfo_cmd is None and self.sudo_commands and self.sudo_distros: + self._sysinfo_cmd = SysinfoCommand(self.sudo_commands, self.sudo_distros) + return self._sysinfo_cmd def __repr__(self): r = "Command(%r, %r)" @@ -168,6 +183,13 @@ def collect(self): # but the avocado.utils.process APIs define no timeouts as "None" if int(self.timeout) <= 0: self.timeout = None + + # Determine whether to run with sudo (do not mutate the command string) + sudo_flag = False + if self._sudoer: + sudo_flag = self._sudoer.use_sudo() and self._sudoer.is_sudo_cmd(self.cmd) + log.info("Executing Command%s: %s", " (sudo)" if sudo_flag else "", self.cmd) + try: result = process.run( self.cmd, @@ -176,6 +198,7 @@ def collect(self): ignore_status=True, shell=True, env=env, + sudo=sudo_flag, ) yield result.stdout except FileNotFoundError as exc_fnf: @@ -394,3 +417,55 @@ def collect(self): raise CollectibleException( f"Not logging {self.path} " f"(lack of permissions)" ) from exc + + +class SysinfoCommand: + def __init__(self, sudo_commands=None, sudo_distros=None): + self.sudo_cmds = sudo_commands if sudo_commands else set() + self.sudo_distros = sudo_distros if sudo_distros else set() + self.sudo_available = False + # Only attempt sudo capability detection on Linux, where it is relevant. + if platform.system().lower() == "linux": + self.sudo_available = can_sudo() + + def use_sudo(self): + """ + Determine if 'sudo' should be used based on the system type. + + Returns: + bool: True if 'sudo' should be used, False otherwise. + """ + if not self.sudo_available: + return False + system_name = platform.system().lower() + if system_name == "linux": + if hasattr(os, "geteuid") and not os.geteuid(): + return False + try: + with open("/etc/os-release", encoding="utf-8") as f: + for line in f: + if line.startswith("ID="): + os_id = line.strip().split("=")[1].strip('"') + return os_id.lower() in self.sudo_distros + except FileNotFoundError: + log.debug("/etc/os-release not found.") + return False + return False + return False + + def is_sudo_cmd(self, cmd): + """ + Determine if 'sudo' should be used for a specific command based on the configuration. + + Args: + cmd (str): The command to check. + + Returns: + bool: True if 'sudo' should be used, False otherwise. + """ + try: + first = shlex.split(cmd or "")[0] + except (ValueError, IndexError): + return False + base = os.path.basename(first).lower() + return base in self.sudo_cmds diff --git a/python-avocado.spec b/python-avocado.spec index 12c1e51665..7c1685f5ae 100644 --- a/python-avocado.spec +++ b/python-avocado.spec @@ -290,6 +290,7 @@ Common files (such as configuration) for the Avocado Testing Framework. %dir %{_datarootdir}/avocado/schemas %{_datarootdir}/avocado/schemas/* %config(noreplace)%{_sysconfdir}/avocado/sysinfo/commands +%config(noreplace)%{_sysconfdir}/avocado/sysinfo.conf %config(noreplace)%{_sysconfdir}/avocado/sysinfo/files %config(noreplace)%{_sysconfdir}/avocado/sysinfo/profilers %config(noreplace)%{_sysconfdir}/avocado/scripts/job/pre.d/README