Merge pull request #29669 from jirikotlin/develop

checkrestart functionality
This commit is contained in:
Mike Place 2015-12-15 12:17:00 -07:00
commit 5a476c5dc3

View File

@ -0,0 +1,385 @@
# -*- coding: utf-8 -*-
'''
checkrestart functionality for Debian and Red Hat Based systems
Identifies services (processes) that are linked against deleted files (for example after downloading an updated
binary of a shared library).
Based on checkrestart script from debian-goodies (written by Matt Zimmerman for the Debian GNU/Linux distribution,
https://packages.debian.org/debian-goodies) and psdel by Sam Morris.
:codeauthor: Jiri Kotlin <jiri.kotlin@ultimum.io>
'''
from __future__ import absolute_import
# Import python libs
import os
import re
import subprocess
import sys
# Import salt libs
import salt.utils
HAS_PSUTIL = False
try:
import psutil
HAS_PSUTIL = True
except ImportError:
pass
def __virtual__():
'''
Only run this module if the psutil python module is installed (package python-psutil).
'''
return HAS_PSUTIL
def _valid_deleted_file(path):
'''
Filters file path against unwanted directories and decides whether file is marked as deleted.
Returns:
True if file is desired deleted file, else False.
Args:
path: A string - path to file
'''
ret = False
if path.endswith(' (deleted)'):
ret = True
if re.compile(r"\(path inode=[0-9]+\)$").search(path):
ret = True
# We don't care about log files
if path.startswith('/var/log/') or path.startswith('/var/local/log/'):
ret = False
# Or about files under temporary locations
if path.startswith('/var/run/') or path.startswith('/var/local/run/'):
ret = False
# Or about files under /tmp
if path.startswith('/tmp/'):
ret = False
# Or about files under /dev/shm
if path.startswith('/dev/shm/'):
ret = False
# Or about files under /run
if path.startswith('/run/'):
ret = False
# Or about files under /drm
if path.startswith('/drm'):
ret = False
# Or about files under /var/tmp and /var/local/tmp
if path.startswith('/var/tmp/') or path.startswith('/var/local/tmp/'):
ret = False
# Or /dev/zero
if path.startswith('/dev/zero'):
ret = False
# Or /dev/pts (used by gpm)
if path.startswith('/dev/pts/'):
ret = False
# Or /usr/lib/locale
if path.startswith('/usr/lib/locale/'):
ret = False
# Skip files from the user's home directories
# many processes hold temporafy files there
if path.startswith('/home/'):
ret = False
# Skip automatically generated files
if path.endswith('icon-theme.cache'):
ret = False
# Skip font files
if path.startswith('/var/cache/fontconfig/'):
ret = False
# Skip Nagios Spool
if path.startswith('/var/lib/nagios3/spool/'):
ret = False
# Skip nagios spool files
if path.startswith('/var/lib/nagios3/spool/checkresults/'):
ret = False
# Skip Postgresql files
if path.startswith('/var/lib/postgresql/'):
ret = False
# Skip VDR lib files
if path.startswith('/var/lib/vdr/'):
ret = False
# Skip Aio files found in MySQL servers
if path.startswith('/[aio]'):
ret = False
return ret
def _deleted_files():
'''
Iterates over /proc/PID/maps and /proc/PID/fd links and returns list of desired deleted files.
Returns:
List of deleted files to analyze, False on failure.
'''
deleted_files = []
for proc in psutil.process_iter():
try:
pinfo = proc.as_dict(attrs=['pid', 'name'])
try:
maps = salt.utils.fopen('/proc/{0}/maps'.format(pinfo['pid']))
dirpath = '/proc/' + str(pinfo['pid']) + '/fd/'
listdir = os.listdir(dirpath)
except (OSError, IOError):
return False
# /proc/PID/maps
maplines = maps.readlines()
maps.close()
mapline = re.compile(r'^[\da-f]+-[\da-f]+ [r-][w-][x-][sp-] '
r'[\da-f]+ [\da-f]{2}:[\da-f]{2} (\d+) *(.+)( \(deleted\))?\n$')
for line in maplines:
matched = mapline.match(line)
if matched:
path = matched.group(2)
if file:
if _valid_deleted_file(path):
val = (pinfo['name'], pinfo['pid'], path[0:-10])
if val not in deleted_files:
deleted_files.append(val)
# /proc/PID/fd
try:
for link in listdir:
path = dirpath + link
readlink = os.readlink(path)
filenames = []
if os.path.isfile(readlink):
filenames.append(readlink)
elif os.path.isdir(readlink) and readlink != '/':
for root, dummy_dirs, files in os.walk(readlink, followlinks=True):
for name in files:
filenames.append(os.path.join(root, name))
for filename in filenames:
if _valid_deleted_file(filename):
val = (pinfo['name'], pinfo['pid'], filename)
if val not in deleted_files:
deleted_files.append(val)
except OSError:
pass
except psutil.NoSuchProcess:
pass
return deleted_files
def _format_output(kernel_restart, packages, verbose, restartable, nonrestartable, restartservicecommands,
restartinitcommands):
'''
Formats the output of the restartcheck module.
Returns:
String - formatted output.
Args:
kernel_restart: indicates that newer kernel is instaled
packages: list of packages that should be restarted
verbose: enables extensive output
restartable: list of restartable packages
nonrestartable: list of non-restartable packages
restartservicecommands: list of commands to restart services
restartinitcommands: list of commands to restart init.d scripts
'''
ret = ''
if kernel_restart:
ret = str(kernel_restart) + '\n\n'
ret += "Found {0} processes using old versions of upgraded files.\n".format(len(packages))
if not verbose:
ret += "These are the packages:\n"
packages = restartable + nonrestartable
for package in packages:
ret += package + '\n'
else:
if len(restartable) > 0:
ret += "Of these, {0} seem to contain systemd service definitions or init scripts " \
"which can be used to restart them:\n".format(len(restartable))
for package in restartable:
ret += package + ':\n'
for program in packages[package]['processes']:
ret += program + '\n'
if len(restartservicecommands) > 0:
ret += "\n\nThese are the systemd services:\n"
ret += '\n'.join(restartservicecommands)
if len(restartinitcommands) > 0:
ret += "\n\nThese are the initd scripts:\n"
ret += '\n'.join(restartinitcommands)
if len(nonrestartable) > 0:
ret += "\n\nThese processes {0} do not seem to have an associated init script " \
"to restart them:\n".format(len(nonrestartable))
for package in nonrestartable:
ret += package + ':\n'
for program in packages[package]['processes']:
ret += program + '\n'
return ret
def restartcheck(ignorelist=None, blacklist=None, excludepid=None, verbose=True):
'''
Analyzes files openeded by running processes and seeks for packages which need to be restarted.
Args:
ignorelist: string or list of packages to be ignored
blacklist: string or list of file paths to be ignored
excludepid: string or list of process IDs to be ignored
verbose: boolean, enables extensive output
Returns:
True if no packages for restart found.
False on failure.
String with checkrestart output if some package seems to need to be restarted.
.. versionadded:: 2015.8.3
CLI Example:
.. code-block:: bash
salt '*' restartcheck.restartcheck
'''
if __grains__.get('os_family') == 'Debian':
cmd_pkg_query = 'dpkg-query --listfiles '
systemd_folder = '/lib/systemd/system/'
systemd = '/bin/systemd'
kernel = """dpkg --get-selections | grep linux-image | """ \
"""perl -pe 's/^linux-image-(\\S+).*/$1/' | tail -2 | head -1"""
elif __grains__.get('os_family') == 'RedHat':
cmd_pkg_query = 'repoquery -l '
systemd_folder = '/usr/lib/systemd/system/'
systemd = '/usr/bin/systemctl'
kernel = """rpm -q --last kernel | perl -pe 's/^kernel-(\\S+).*/$1/' | head -1"""
else:
return {'result': False, 'comment': 'Only available on Debian and Red Hat based systems.'}
# Check kernel versions
kernel_last = __salt__['cmd.run'](kernel, python_shell=True)
kernel_current = __salt__['cmd.run']('uname -r')
if kernel_current != kernel_last:
kernel_restart = 'Kernel outdated - current: {0}, last installed: {1}'.format(kernel_current, kernel_last)
else:
kernel_restart = False
packages = {}
if ignorelist:
if not isinstance(ignorelist, list):
ignorelist = [ignorelist]
else:
ignorelist = ['screen', 'systemd']
if blacklist:
if not isinstance(blacklist, list):
blacklist = [blacklist]
else:
blacklist = []
if excludepid:
if not isinstance(excludepid, list):
excludepid = [excludepid]
else:
excludepid = []
deleted_files = _deleted_files()
if not isinstance(deleted_files, list):
return {'result': False, 'comment': 'Could not get list of processes. '
'(Do you have root access?)'}
owners_cache = {}
for deleted_file in deleted_files:
name, pid, path = deleted_file[0], deleted_file[1], deleted_file[2]
if path in blacklist or pid in excludepid:
continue
readlink = os.readlink('/proc/{0}/exe'.format(pid))
try:
packagename = owners_cache[readlink]
except KeyError:
packagename = __salt__['pkg.owner'](readlink)
owners_cache[readlink] = packagename
if packagename and packagename not in ignorelist:
program = '\t' + str(pid) + ' ' + readlink + ' (file: ' + str(path) + ')'
if packagename not in packages:
packages[packagename] = {'initscripts': [], 'systemdservice': [], 'processes': [program],
'process_name': name}
else:
if program not in packages[packagename]['processes']:
packages[packagename]['processes'].append(program)
if len(packages) == 0 and not kernel_restart:
return 'No packages seem to need to be restarted.'
for package in packages.keys():
cmd = cmd_pkg_query + package
paths = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
while True:
line = paths.stdout.readline()
if not line:
break
pth = line[:-1]
if pth.startswith('/etc/init.d/') and not pth.endswith('.sh'):
packages[package]['initscripts'].append(pth[12:])
if os.path.exists(systemd) and pth.startswith(systemd_folder) and pth.endswith('.service') and \
pth.find('.wants') == -1:
is_oneshot = False
servicefile = salt.utils.fopen(pth)
sysfold_len = len(systemd_folder)
for line in servicefile.readlines():
if line.find('Type=oneshot') > 0:
# scripts that does a single job and then exit
is_oneshot = True
continue
servicefile.close()
if not is_oneshot:
packages[package]['systemdservice'].append(pth[sysfold_len:])
sys.stdout.flush()
paths.stdout.close()
# Alternatively, find init.d script or service that match the process name
for package in packages.keys():
if len(packages[package]['systemdservice']) == 0 and len(packages[package]['initscripts']) == 0:
service = __salt__['service.available'](packages[package]['process_name'])
if service:
packages[package]['systemdservice'].append(packages[package]['process_name'])
else:
if os.path.exists('/etc/init.d/' + packages[package]['process_name']):
packages[package]['initscripts'].append(packages[package]['process_name'])
restartable = []
nonrestartable = []
restartinitcommands = []
restartservicecommands = []
for package in packages.keys():
if len(packages[package]['initscripts']) > 0:
restartable.append(package)
restartinitcommands.extend(['service ' + s + ' restart' for s in packages[package]['initscripts']])
elif len(packages[package]['systemdservice']) > 0:
restartable.append(package)
restartservicecommands.extend(['systemctl restart ' + s for s in packages[package]['systemdservice']])
else:
nonrestartable.append(package)
ret = _format_output(kernel_restart, packages, verbose, restartable, nonrestartable,
restartservicecommands, restartinitcommands)
return ret