From 1408cbd8ca29d1a921a69e7f0f21597fe097e3c9 Mon Sep 17 00:00:00 2001 From: Bernhard Suttner Date: Tue, 2 Apr 2024 16:42:27 +0200 Subject: [PATCH] Fixes #36973 - Use dnf needs-restarting to collect tracer information (#149) * Fixes #36973 - Use dnf needs-restarting to collect tracer information Co-Authored-by: Ewoud Kohl van Wijngaarden * Update src/katello/tracer/dnf.py Co-authored-by: Ian Ballou --------- Co-authored-by: Ewoud Kohl van Wijngaarden Co-authored-by: Ian Ballou --- src/katello/tracer/SystemdDbus.py | 49 +++++++++++++ src/katello/tracer/__init__.py | 29 ++++---- src/katello/tracer/dnf.py | 118 ++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 src/katello/tracer/SystemdDbus.py create mode 100644 src/katello/tracer/dnf.py diff --git a/src/katello/tracer/SystemdDbus.py b/src/katello/tracer/SystemdDbus.py new file mode 100644 index 000000000..6062fb74c --- /dev/null +++ b/src/katello/tracer/SystemdDbus.py @@ -0,0 +1,49 @@ +# SystemdUnit.py +# Module for getting data from Systemd about Units + +# Copyright (C) 2017 Sean O'Keeffe +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +# + +import dbus + +class SystemdDbus(object): + def __init__(self): + self.__systemd = dbus.SystemBus().get_object('org.freedesktop.systemd1','/org/freedesktop/systemd1') + self.__manager = dbus.Interface(self.__systemd, dbus_interface='org.freedesktop.systemd1.Manager') + + def unit_path_from_pid(self, pid): + try: + return self.__manager.GetUnitByPID(pid) + except dbus.exceptions.DBusException: + return False + + def has_service_property_from_pid(self, pid, attr): + try: + unit = self.unit_path_from_pid(pid) + if not unit: + return False + + proxy = dbus.SystemBus().get_object('org.freedesktop.systemd1', unit) + propty = proxy.Get('org.freedesktop.systemd1.Service', attr, dbus_interface='org.freedesktop.DBus.Properties') + except dbus.exceptions.DBusException: + return False + return bool(propty) + + def get_unit_property_from_pid(self, pid, attr): + unit_path = self.unit_path_from_pid(pid) + if bool(unit_path): + proxy = dbus.SystemBus().get_object('org.freedesktop.systemd1', self.unit_path_from_pid(pid)) + return proxy.Get('org.freedesktop.systemd1.Unit', attr, dbus_interface='org.freedesktop.DBus.Properties') + else: + return False diff --git a/src/katello/tracer/__init__.py b/src/katello/tracer/__init__.py index bc7056712..cc3e0ce96 100644 --- a/src/katello/tracer/__init__.py +++ b/src/katello/tracer/__init__.py @@ -6,35 +6,34 @@ def collect_apps(): raise NotImplementedError("Couldn't detect package manager. Failed to query affected apps!") -try: - imp.find_module('apt') - from katello.tracer.deb import collect_apps -except ImportError: - pass - +# RHEL based systems try: imp.find_module('dnf') - tracer = True + from katello.tracer.dnf import collect_apps except ImportError: try: imp.find_module('yum') - tracer = True + from tracer.query import Query + def collect_apps(): + query = Query() + return query.affected_applications().get() except ImportError: - tracer = False + pass -if tracer: - from tracer.query import Query - def collect_apps(): - query = Query() - return query.affected_applications().get() +# debian based systems +try: + imp.find_module('apt') + from katello.tracer.deb import collect_apps +except ImportError: + pass +# SUSE based systems try: imp.find_module('zypp_plugin') from katello.tracer.zypper import collect_apps except ImportError: pass - def upload_tracer_profile(queryfunc, plugin=None): uep = get_uep() consumer_id = lookup_consumer_id() diff --git a/src/katello/tracer/dnf.py b/src/katello/tracer/dnf.py new file mode 100644 index 000000000..4e02d6894 --- /dev/null +++ b/src/katello/tracer/dnf.py @@ -0,0 +1,118 @@ +import os +import re +import subprocess +import psutil +from katello.tracer.SystemdDbus import SystemdDbus + +# these services need a reboot of the system +STATIC_SERVICES = [ + "systemd", + "dbus", +] + +# these apps are ignored and no tracer will be added +IGNORE_APPS = [ + "sudo", + "su", + "(sd-pam)", +] + +REBOOT_HELPER= 'You will have to reboot your computer' +SESSION_HELPER = 'You will have to log out & log in again' + +class DnfTracerApp: + def __init__(self, name, helper, app_type): + self.name = name + self.helper = helper + self.type = app_type + + +class Process: + def __init__(self, bus, pid): + self.bus = bus + self.pid = int(pid) + self.process = psutil.Process(self.pid) + self.name = self.detect_name() + + # special handling for ssh sessions. Thanks to + # https://github.com/FrostyX/tracer/blob/ff8fc924fcbe2f638dd88b50549813dab2b8595b/tracer/resources/processes.py#L79 + try: + if self.name == 'sshd': + exe = self.process.exe() + cmdline = self.process.cmdline() + if exe not in cmdline and len(cmdline) > 1: + self.name = 'ssh-{0}-session'.format(re.split(' |@',' '.join(cmdline))[1]) + except psutil.AccessDenied: + pass + + def detect_name(self): + if self.pid and self.bus.unit_path_from_pid(self.pid): + if not self.bus.has_service_property_from_pid(self.pid, 'PAMName'): + unit_id = self.bus.get_unit_property_from_pid(self.pid, 'Id') + if unit_id and unit_id.endswith('.service'): + return unit_id[:-8] + return self.process.name() + + def is_session(self): + if self.name.startswith("ssh-") and self.name.endswith("-session"): + return True + + terminal = self.process.terminal() + if terminal is not None: + parent = self.process.parent() + if parent is None or terminal != parent.terminal(): + return True + return False + + def is_reboot_required(self): + return self.name in STATIC_SERVICES + + +def collect_services_state(): + env = dict(os.environ, LANG='C') + process = subprocess.run(['dnf', 'needs-restarting'], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + if process.returncode != 0: + return [] + + lines = process.stdout.split('\n') + pids = [line.split(' : ')[0].strip() for line in lines if ' : ' in line] + + apps = set() + bus = SystemdDbus() + added_services = [] + + for pid in pids: + p = Process(bus, pid) + + if p.name in IGNORE_APPS: + continue + + app_type = 'daemon' + helper = "systemctl restart " + p.name + if p.is_session(): + app_type = 'session' + helper = SESSION_HELPER + if p.is_reboot_required(): + app_type = 'static' + helper = REBOOT_HELPER + + if p.name is not None and p.name not in added_services: + app = DnfTracerApp(p.name, helper, app_type) + apps.add(app) + added_services.append(p.name) + return list(apps) + + +def collect_restart(): + apps = [] + process = subprocess.run(["dnf", "needs-restarting", "-r"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if process.returncode == 1: + app = DnfTracerApp("kernel", REBOOT_HELPER, "static") + apps.append(app) + return apps + + +def collect_apps(plugin=None): + apps = collect_services_state() + reboot = collect_restart() + return apps + reboot