Blame plugins/needs_restarting.py

Packit 3a9065
# needs_restarting.py
Packit 3a9065
# DNF plugin to check for running binaries in a need of restarting.
Packit 3a9065
#
Packit 3a9065
# Copyright (C) 2014 Red Hat, Inc.
Packit 3a9065
#
Packit 3a9065
# This copyrighted material is made available to anyone wishing to use,
Packit 3a9065
# modify, copy, or redistribute it subject to the terms and conditions of
Packit 3a9065
# the GNU General Public License v.2, or (at your option) any later version.
Packit 3a9065
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit 3a9065
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit 3a9065
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit 3a9065
# Public License for more details.  You should have received a copy of the
Packit 3a9065
# GNU General Public License along with this program; if not, write to the
Packit 3a9065
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 3a9065
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit 3a9065
# source code or documentation are not subject to the GNU General Public
Packit 3a9065
# License and may only be used or replicated with the express permission of
Packit 3a9065
# Red Hat, Inc.
Packit 3a9065
#
Packit 3a9065
# the mechanism of scanning smaps for opened files and matching them back to
Packit 3a9065
# packages is heavily inspired by the original needs-restarting.py:
Packit 3a9065
# http://yum.baseurl.org/gitweb?p=yum-utils.git;a=blob;f=needs-restarting.py
Packit 3a9065
Packit 3a9065
from __future__ import absolute_import
Packit 3a9065
from __future__ import division
Packit 3a9065
from __future__ import print_function
Packit 3a9065
from __future__ import unicode_literals
Packit 3a9065
from dnfpluginscore import logger, _
Packit 3a9065
Packit 3a9065
import dnf
Packit 3a9065
import dnf.cli
Packit Service 92acb9
import dbus
Packit 3a9065
import functools
Packit 3a9065
import os
Packit 3a9065
import re
Packit 3a9065
import stat
Packit 3a9065
Packit 3a9065
Packit 3a9065
# For which package updates we should recommend a reboot
Packit 3a9065
# Mostly taken from https://access.redhat.com/solutions/27943
Packit Service 6746a6
NEED_REBOOT = ['kernel', 'kernel-rt', 'glibc', 'linux-firmware',
Packit Service 6746a6
               'systemd', 'dbus', 'dbus-broker', 'dbus-daemon']
Packit 3a9065
Packit 3a9065
def get_options_from_dir(filepath, base):
Packit 3a9065
    """
Packit 3a9065
    Provide filepath as string if single dir or list of strings
Packit 3a9065
    Return set of package names contained in files under filepath
Packit 3a9065
    """
Packit 3a9065
Packit Service 6746a6
    if not os.path.exists(filepath):
Packit Service 6746a6
        return set()
Packit 3a9065
    options = set()
Packit 3a9065
    for file in os.listdir(filepath):
Packit 3a9065
        if os.path.isdir(file) or not file.endswith('.conf'):
Packit 3a9065
            continue
Packit 3a9065
Packit 3a9065
        with open(os.path.join(filepath, file)) as fp:
Packit 3a9065
            for line in fp:
Packit 3a9065
                options.add((line.rstrip(), file))
Packit 3a9065
Packit 3a9065
    packages = set()
Packit 3a9065
    for pkg in base.sack.query().installed().filter(name={x[0] for x in options}):
Packit 3a9065
        packages.add(pkg.name)
Packit Service 6746a6
    for name, file in {x for x in options if x[0] not in packages}:
Packit 3a9065
        logger.warning(
Packit Service 6746a6
            _('No installed package found for package name "{pkg}" '
Packit 3a9065
                'specified in needs-restarting file "{file}".'.format(pkg=name, file=file)))
Packit 3a9065
    return packages
Packit 3a9065
Packit 3a9065
Packit 3a9065
def list_opened_files(uid):
Packit 3a9065
    for (pid, smaps) in list_smaps():
Packit 3a9065
        try:
Packit 3a9065
            if uid is not None and uid != owner_uid(smaps):
Packit 3a9065
                continue
Packit 3a9065
            with open(smaps, 'r') as smaps_file:
Packit 3a9065
                lines = smaps_file.readlines()
Packit 3a9065
        except EnvironmentError:
Packit 3a9065
            logger.warning("Failed to read PID %d's smaps.", pid)
Packit 3a9065
            continue
Packit 3a9065
Packit 3a9065
        for line in lines:
Packit 3a9065
            ofile = smap2opened_file(pid, line)
Packit 3a9065
            if ofile is not None:
Packit 3a9065
                yield ofile
Packit 3a9065
Packit 3a9065
Packit 3a9065
def list_smaps():
Packit 3a9065
    for dir_ in os.listdir('/proc'):
Packit 3a9065
        try:
Packit 3a9065
            pid = int(dir_)
Packit 3a9065
        except ValueError:
Packit 3a9065
            continue
Packit 3a9065
        smaps = '/proc/%d/smaps' % pid
Packit 3a9065
        yield (pid, smaps)
Packit 3a9065
Packit 3a9065
Packit 3a9065
def memoize(func):
Packit 3a9065
    sentinel = object()
Packit 3a9065
    cache = {}
Packit 3a9065
    def wrapper(param):
Packit 3a9065
        val = cache.get(param, sentinel)
Packit 3a9065
        if val is not sentinel:
Packit 3a9065
            return val
Packit 3a9065
        val = func(param)
Packit 3a9065
        cache[param] = val
Packit 3a9065
        return val
Packit 3a9065
    return wrapper
Packit 3a9065
Packit 3a9065
Packit 3a9065
def owner_uid(fname):
Packit 3a9065
    return os.stat(fname)[stat.ST_UID]
Packit 3a9065
Packit 3a9065
Packit 3a9065
def owning_package(sack, fname):
Packit 3a9065
    matches = sack.query().filter(file=fname).run()
Packit 3a9065
    if matches:
Packit 3a9065
        return matches[0]
Packit 3a9065
    return None
Packit 3a9065
Packit 3a9065
Packit 3a9065
def print_cmd(pid):
Packit 3a9065
    cmdline = '/proc/%d/cmdline' % pid
Packit 3a9065
    with open(cmdline) as cmdline_file:
Packit 3a9065
        command = dnf.i18n.ucd(cmdline_file.read())
Packit 3a9065
    command = ' '.join(command.split('\000'))
Packit 3a9065
    print('%d : %s' % (pid, command))
Packit 3a9065
Packit 3a9065
Packit Service 92acb9
def get_service_dbus(pid):
Packit Service 92acb9
    bus = dbus.SystemBus()
Packit Service 92acb9
    systemd_manager_object = bus.get_object(
Packit Service 92acb9
        'org.freedesktop.systemd1',
Packit Service 92acb9
        '/org/freedesktop/systemd1'
Packit Service 92acb9
    )
Packit Service 92acb9
    systemd_manager_interface = dbus.Interface(
Packit Service 92acb9
        systemd_manager_object,
Packit Service 92acb9
        'org.freedesktop.systemd1.Manager'
Packit Service 92acb9
    )
Packit Service 92acb9
    service_proxy = bus.get_object(
Packit Service 92acb9
        'org.freedesktop.systemd1',
Packit Service 92acb9
        systemd_manager_interface.GetUnitByPID(pid)
Packit Service 92acb9
    )
Packit Service 92acb9
    service_properties = dbus.Interface(
Packit Service 92acb9
        service_proxy, dbus_interface="org.freedesktop.DBus.Properties")
Packit Service 92acb9
    name = service_properties.Get(
Packit Service 92acb9
        "org.freedesktop.systemd1.Unit",
Packit Service 92acb9
        'Id'
Packit Service 92acb9
    )
Packit Service 92acb9
    if name.endswith(".service"):
Packit Service 92acb9
        return name
Packit Service 92acb9
    return
Packit Service 92acb9
Packit 3a9065
def smap2opened_file(pid, line):
Packit 3a9065
    slash = line.find('/')
Packit 3a9065
    if slash < 0:
Packit 3a9065
        return None
Packit 3a9065
    if line.find('00:') >= 0:
Packit 3a9065
        # not a regular file
Packit 3a9065
        return None
Packit 3a9065
    fn = line[slash:].strip()
Packit 3a9065
    suffix_index = fn.rfind(' (deleted)')
Packit 3a9065
    if suffix_index < 0:
Packit 3a9065
        return OpenedFile(pid, fn, False)
Packit 3a9065
    else:
Packit 3a9065
        return OpenedFile(pid, fn[:suffix_index], True)
Packit 3a9065
Packit 3a9065
Packit 3a9065
class OpenedFile(object):
Packit 3a9065
    RE_TRANSACTION_FILE = re.compile('^(.+);[0-9A-Fa-f]{8,}$')
Packit 3a9065
Packit 3a9065
    def __init__(self, pid, name, deleted):
Packit 3a9065
        self.deleted = deleted
Packit 3a9065
        self.name = name
Packit 3a9065
        self.pid = pid
Packit 3a9065
Packit 3a9065
    @property
Packit 3a9065
    def presumed_name(self):
Packit 3a9065
        """Calculate the name of the file pre-transaction.
Packit 3a9065
Packit 3a9065
        In case of a file that got deleted during the transactionm, possibly
Packit 3a9065
        just because of an upgrade to a newer version of the same file, RPM
Packit 3a9065
        renames the old file to the same name with a hexadecimal suffix just
Packit 3a9065
        before delting it.
Packit 3a9065
Packit 3a9065
        """
Packit 3a9065
Packit 3a9065
        if self.deleted:
Packit 3a9065
            match = self.RE_TRANSACTION_FILE.match(self.name)
Packit 3a9065
            if match:
Packit 3a9065
                return match.group(1)
Packit 3a9065
        return self.name
Packit 3a9065
Packit 3a9065
Packit 3a9065
class ProcessStart(object):
Packit 3a9065
    def __init__(self):
Packit 3a9065
        self.boot_time = self.get_boot_time()
Packit 3a9065
        self.sc_clk_tck = self.get_sc_clk_tck()
Packit 3a9065
Packit 3a9065
    @staticmethod
Packit 3a9065
    def get_boot_time():
Packit 3a9065
        with open('/proc/stat') as stat_file:
Packit 3a9065
            for line in stat_file.readlines():
Packit 3a9065
                if not line.startswith('btime '):
Packit 3a9065
                    continue
Packit 3a9065
                return int(line[len('btime '):].strip())
Packit 3a9065
Packit 3a9065
    @staticmethod
Packit 3a9065
    def get_sc_clk_tck():
Packit 3a9065
        return os.sysconf(os.sysconf_names['SC_CLK_TCK'])
Packit 3a9065
Packit 3a9065
    def __call__(self, pid):
Packit 3a9065
        stat_fn = '/proc/%d/stat' % pid
Packit 3a9065
        with open(stat_fn) as stat_file:
Packit 3a9065
            stats = stat_file.read().strip().split()
Packit 3a9065
        ticks_after_boot = int(stats[21])
Packit 3a9065
        secs_after_boot = ticks_after_boot // self.sc_clk_tck
Packit 3a9065
        return self.boot_time + secs_after_boot
Packit 3a9065
Packit 3a9065
Packit 3a9065
@dnf.plugin.register_command
Packit 3a9065
class NeedsRestartingCommand(dnf.cli.Command):
Packit 3a9065
    aliases = ('needs-restarting',)
Packit 3a9065
    summary = _('determine updated binaries that need restarting')
Packit 3a9065
Packit 3a9065
    @staticmethod
Packit 3a9065
    def set_argparser(parser):
Packit 3a9065
        parser.add_argument('-u', '--useronly', action='store_true',
Packit 3a9065
                            help=_("only consider this user's processes"))
Packit 3a9065
        parser.add_argument('-r', '--reboothint', action='store_true',
Packit 3a9065
                            help=_("only report whether a reboot is required "
Packit 3a9065
                                   "(exit code 1) or not (exit code 0)"))
Packit Service 92acb9
        parser.add_argument('-s', '--services', action='store_true',
Packit Service 92acb9
                            help=_("only report affected systemd services"))
Packit 3a9065
Packit 3a9065
    def configure(self):
Packit 3a9065
        demands = self.cli.demands
Packit 3a9065
        demands.sack_activation = True
Packit 3a9065
Packit 3a9065
    def run(self):
Packit 3a9065
        process_start = ProcessStart()
Packit 3a9065
        owning_pkg_fn = functools.partial(owning_package, self.base.sack)
Packit 3a9065
        owning_pkg_fn = memoize(owning_pkg_fn)
Packit 3a9065
Packit 3a9065
        opt = get_options_from_dir(os.path.join(
Packit 3a9065
            self.base.conf.installroot,
Packit 3a9065
            "etc/dnf/plugins/needs-restarting.d/"),
Packit 3a9065
            self.base)
Packit 3a9065
        NEED_REBOOT.extend(opt)
Packit 3a9065
        if self.opts.reboothint:
Packit 3a9065
            need_reboot = set()
Packit 3a9065
            installed = self.base.sack.query().installed()
Packit 3a9065
            for pkg in installed.filter(name=NEED_REBOOT):
Packit 3a9065
                if pkg.installtime > process_start.boot_time:
Packit 3a9065
                    need_reboot.add(pkg.name)
Packit 3a9065
            if need_reboot:
Packit 3a9065
                print(_('Core libraries or services have been updated '
Packit 3a9065
                        'since boot-up:'))
Packit 3a9065
                for name in sorted(need_reboot):
Packit 3a9065
                    print('  * %s' % name)
Packit 3a9065
                print()
Packit 3a9065
                print(_('Reboot is required to fully utilize these updates.'))
Packit 3a9065
                print(_('More information:'),
Packit 3a9065
                      'https://access.redhat.com/solutions/27943')
Packit 3a9065
                raise dnf.exceptions.Error()  # Sets exit code 1
Packit 3a9065
            else:
Packit 3a9065
                print(_('No core libraries or services have been updated '
Packit 3a9065
                        'since boot-up.'))
Packit 3a9065
                print(_('Reboot should not be necessary.'))
Packit 3a9065
                return None
Packit 3a9065
Packit 3a9065
        stale_pids = set()
Packit 3a9065
        uid = os.geteuid() if self.opts.useronly else None
Packit 3a9065
        for ofile in list_opened_files(uid):
Packit 3a9065
            pkg = owning_pkg_fn(ofile.presumed_name)
Packit 3a9065
            if pkg is None:
Packit 3a9065
                continue
Packit 3a9065
            if pkg.installtime > process_start(ofile.pid):
Packit 3a9065
                stale_pids.add(ofile.pid)
Packit 3a9065
Packit Service 92acb9
        if self.opts.services:
Packit Service 92acb9
            names = set([get_service_dbus(pid) for pid in sorted(stale_pids)])
Packit Service 92acb9
            for name in names:
Packit Service 92acb9
                if name is not None:
Packit Service 92acb9
                    print(name)
Packit Service 92acb9
            return 0
Packit 3a9065
        for pid in sorted(stale_pids):
Packit 3a9065
            print_cmd(pid)