0x1949 Team - FAZEMRX - MANAGER
Edit File: checkrestart
#!/usr/bin/python3 # Copyright (C) 2001 Matt Zimmerman <mdz@debian.org> # Copyright (C) 2007,2010-2015 Javier Fernandez-Sanguino <jfs@debian.org> # - included patch from Justin Pryzby <justinpryzby_AT_users.sourceforge.net> # to work with the latest Lsof - modify to reduce false positives by not # complaining about deleted inodes/files under /tmp/, /var/log/, # /var/run or named /SYSV. # - introduced a verbose option # PENDING: # - included code from 'psdel' contributed by Sam Morris <sam_AT_robots.org.uk> to # make the program work even if lsof is not installed # (available at https://robots.org.uk/src/psdel) # - make it work with a whitelist of directories instead of a blacklist # (might make it less false positive prone) # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty 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 # # On Debian systems, a copy of the GNU General Public License may be # found in /usr/share/common-licenses/GPL. import sys import os, errno import re import pwd import sys import string import subprocess import getopt from stat import * def checkroot(): if os.getuid() != 0: sys.stderr.write('ERROR: This program must be run as root in order to obtain information\n') sys.stderr.write('about all open file descriptors in the system.\n') sys.exit(1) def find_cmd(cmd): dirs = [ '/', '/usr/', '/usr/local/', sys.prefix ] for d in dirs: for sd in ('bin', 'sbin'): location = os.path.join(d, sd, cmd) if os.path.exists(location): return location return False def checksystemd(): # Check if systemd is installed in the system if os.path.exists('/bin/systemd'): return 0 return 1 def usage(): sys.stderr.write('usage: checkrestart [-vhpam] [ -b blacklist_file ] [ -i package_name ] [ -e pid ]\n') def main(): global lc_all_c_env, file_query_check process = None toRestart = {} lc_all_c_env = os.environ lc_all_c_env['LC_ALL'] = 'C' file_query_check = {} is_systemd = False useLsof = True blacklistFiles = [] blacklist = [] ignorelist = [ 'screen', 'systemd', 'dbus' ] excludepidlist = [] # Process options try: opts, args = getopt.getopt(sys.argv[1:], "hvpamb:i:ne:t", ["help", "verbose", "packages", "all", "machine", "blacklist", "ignore", "nolsof", "excludepid", "terse"]) except getopt.GetoptError as err: # print help information and exit: print(err) # will print something like "option -x not recognized" usage() sys.exit(2) # Global variables set through the command line global verbose, onlyPackageFiles, allFiles verbose = False # Only look for deleted files that belong to packages onlyPackageFiles = False # Look for any deleted file allFiles = False # Generate terse output, disabled by default terseOutput = False # Generate machine parsable output, disabled by default machineOutput = False for o, a in opts: if o in ("-v", "--verbose"): verbose = True elif o in ("-h", "--help"): usage() sys.exit() elif o in ("-p", "--packages"): onlyPackageFiles = True elif o in ("-a", "--all"): allFiles = True onlyPackageFiles = False elif o in ("-e", "--excludepid"): excludepidlist.append(a) elif o in ("-m", "--machine"): machineOutput = True elif o in ("-b", "--blacklist"): blacklistFiles.append(a) onlyPackageFiles = False elif o in ("-i", "--ignore"): ignorelist.append(a) elif o in ("-n", "--nolsof"): useLsof = False elif o in ("-t", "--terse"): terseOutput = True else: assert False, "unhandled option" checkroot() for f in blacklistFiles: blacklistFile = open(f, 'r') for line in blacklistFile.readlines(): if line.startswith("#"): continue blacklist.append(re.compile(line.strip())) blacklistFile.close() # Start checking if checksystemd() == 0: is_systemd = True # if find_cmd('lsof') == 1: # sys.stderr.write('ERROR: This program needs lsof in order to run.\n') # sys.stderr.write('Please install the lsof package in your system.\n') # sys.exit(1) # Check if we have lsof, if not, use an alternative mechanism if not find_cmd('lsof') or not useLsof: if verbose and not find_cmd('lsof'): print("[DEBUG] Lsof is not available in the system. Using alternative mechanism.") toRestart = procfilescheck(blacklist = blacklist, excludepidlist = excludepidlist) else: toRestart = lsoffilescheck(blacklist = blacklist) if terseOutput: # Terse output and exit # Use Nagios exit codes: 0 OK, 1 warning, 2 critical, 3 unknown # we only care for 0 or 1 if len(toRestart): print("%d processes using old versions of upgraded files" % len(toRestart)) sys.exit(1) # Exit with no error if there is nothing to restart print("No processes found using old versions of upgraded files") sys.exit(0) if not machineOutput: print("Found %d processes using old versions of upgraded files" % len(toRestart)) else: print("PROCESSES: %d" % len(toRestart)) if len(toRestart) == 0: sys.exit(0) programs = {} for process in toRestart: programs.setdefault(process.program, []) programs[process.program].append(process) if not machineOutput: if len(programs) == 1: print("(%d distinct program)" % len(programs)) else: print("(%d distinct programs)" % len(programs)) else: print("PROGRAMS: %d" % len(programs)) #services Verbose information if verbose: for process in toRestart: if not machineOutput: print("[DEBUG] Process %s (PID: %d) " % (process.program, process.pid)) process.listDeleted() else: process.listDeletedMachine() packages = {} diverted = None dpkgQuery = ["dpkg-query", "--search"] + list(programs.keys()) if verbose: print("[DEBUG] Running: %s" % ' '.join(dpkgQuery)) dpkgProc = subprocess.Popen(dpkgQuery, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env = lc_all_c_env) while True: line = dpkgProc.stdout.readline().decode("utf-8") if not line: break if verbose: print("[DEBUG] Reading line from dpkg-query: %s" % line) if line.startswith('local diversion'): continue if not ':' in line: continue m = re.match('^diversion by (\S+) (from|to): (.*)$', line) if m: if m.group(2) == 'from': diverted = m.group(3) continue if not diverted: raise Exception('Weird error while handling diversion') packagename, program = m.group(1), diverted else: packagename, program = line[:-1].split(': ') if program == diverted: # dpkg prints a summary line after the diversion, name both # packages of the diversion, so ignore this line # mutt-patched, mutt: /usr/bin/mutt continue packages.setdefault(packagename,Package(packagename)) try: packages[packagename].processes.extend(programs[program]) if verbose: print("[DEBUG] Found package %s for program %s" % (packagename, program)) except KeyError: sys.stderr.write ('checkrestart (program not found): %s: %s\n' % (packagename, program)) sys.stdout.flush() # Close the pipe dpkgProc.stdout.close() # Remove the ignored packages from the list of packages if ignorelist: for i in ignorelist: if i in packages: if verbose: print("[DEBUG] Removing %s from the package list (ignored)" % (i)) try: del packages[i] except KeyError: continue if not machineOutput: print("(%d distinct packages)" % len(packages)) else: print("PACKAGES: %d" % len(packages)) if len(packages) == 0: if not machineOutput: print("No packages seem to need to be restarted.") print("(please read checkrestart(8))") sys.exit(0) for package in list(packages.values()): dpkgQuery = ["dpkg-query", "--listfiles", package.name] if verbose: print("[DEBUG] Running: %s" % ' '.join(dpkgQuery)) dpkgProc = subprocess.Popen(dpkgQuery, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env = lc_all_c_env) while True: line = dpkgProc.stdout.readline().decode("utf-8") if not line: break path = line[:-1] if path.startswith('/etc/init.d/'): if path.endswith('.sh'): continue package.initscripts.add(path[12:]) # If running on a systemd system, extract the systemd's service files from the package if is_systemd and path.startswith('/lib/systemd/system/') and path.endswith('.service') and path.find('.wants') == -1: if path.endswith('@.service'): continue # Read the service file and make sure it is not of type 'oneshot' servicefile = open (path) is_oneshot = False for line in servicefile.readlines(): if line.find ('Type=oneshot') > 0: is_oneshot = True continue servicefile.close () if not is_oneshot: package.systemdservice.add(path[20:]) sys.stdout.flush() dpkgProc.stdout.close() # Alternatively, find init.d scripts that match the process name if len(package.initscripts) == 0 and len(package.systemdservice) == 0: for process in package.processes: proc_name = os.path.basename(process.program) if os.path.exists('/etc/init.d/' + proc_name): package.initscripts.add(proc_name) restartable = [] nonrestartable = [] restartInitCommands = [] restartServiceCommands = [] for package in list(packages.values()): if len(package.initscripts) > 0: restartable.append(package) restartInitCommands.extend(['service ' + s + ' restart' for s in package.initscripts]) elif len(package.systemdservice) > 0: restartable.append(package) restartServiceCommands.extend(['systemctl restart ' + s for s in package.systemdservice]) else: nonrestartable.append(package) if len(restartable) > 0: if not machineOutput: print() print("Of these, %d seem to contain systemd service definitions or init scripts which can be used to restart them." % len(restartable)) # TODO - consider putting this in a --verbose option print("The following packages seem to have definitions that could be used\nto restart their services:") for package in restartable: print(package.name + ':') for process in package.processes: print("\t%s\t%s" % (process.pid,process.program)) if len(restartServiceCommands)>0: print() print("These are the systemd services:") print('\n'.join(restartServiceCommands)) print() if len(restartInitCommands)>0: print("These are the initd scripts:") print('\n'.join(restartInitCommands)) print() else: for package in restartable: for process in package.processes: print('SERVICE:%s,%s,%s' % (package.name, process.pid,process.program)) if len(nonrestartable) == 0: sys.exit(0) # TODO - consider putting this in a --verbose option if not machineOutput: print("These processes (%d) do not seem to have an associated init script to restart them:" %len(nonrestartable)) for package in nonrestartable: if not machineOutput: print(package.name + ':') for process in package.processes: print("\t%s\t%s" % (process.pid,process.program)) else: for process in package.processes: print('OTHER:%s,%s,%s' % (package.name,process.pid,process.program)) def lsoffilescheck(blacklist = None): # Use LSOF to extract the list of deleted files from subprocess import check_output processes = {} for line in check_output(['lsof', '+XL', '-F', 'nf']).decode(errors="ignore").splitlines(): field, data = line[0], line[1:] if field == 'p': process = processes.setdefault(data,Process(int(data))) elif field == 'k': process.links.append(data) elif field == 'n': # Remove the previous entry to check if this is something we should use if data.find('SYSV') >= 0: # If we find SYSV we discard the previous descriptor last = process.descriptors.pop() elif data.startswith('/') or data.startswith('(deleted)/') or data.startswith(' (deleted)/'): last = process.descriptors.pop() # If the data starts with (deleted) put it in the end of the # file name, this is used to workaround different behaviour in # OpenVZ systems, see # https://bugzilla.openvz.org/show_bug.cgi?id=2932 if data.startswith('(deleted)'): data = data[9:] + ' (deleted)' elif data.startswith(' (deleted)'): data = data[10:] + ' (deleted)' # Add it to the list of deleted files if the previous descriptor # was DEL or lsof marks it as deleted if re.compile("DEL").search(last) or re.compile("\(deleted\)").search(data) or re.compile("\(path inode=[0-9]+\)$").search(data): process.files.append(data) else: # We discard the previous descriptors and drop it last = process.descriptors.pop() elif field == 'f': # Save the descriptor for later comparison process.descriptors.append(data) toRestart = [process for process in list(processes.values()) if process.needsRestart(blacklist)] return toRestart def procfilescheck(blacklist = None, excludepidlist = None): # Use the underlying /proc file system to determine processes that # are using deleted files from subprocess import check_output processes = {} # Get a list of running processes pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] for pid in pids: if pid in excludepidlist: continue # Get the list of open files for this process from /proc # We can ignore failures over this block as links will # disappear as we run them foundfiles = [] try: for fd in os.listdir('/proc/' + pid + '/fd'): if os.path.islink('/proc/' + pid + '/fd/' + fd): fname = os.readlink('/proc/' + pid + '/fd/' + fd) if re.compile("\s\(deleted\)$").search(fname): foundfiles.append(fname) except: continue # Get the list of memory mapped files using system pmap for output in check_output(['pmap', pid]).decode(errors="ignore").splitlines(): data = re.split('\s+', output.strip('\n'), 3) if len(data) == 4: f = data[3] if re.compile("\s\(deleted\)$").search(f): foundfiles.append(f) if len(foundfiles) > 1: process = processes.setdefault(pid,Process(int(pid))) # print pid + ': ' + ', '.join(foundfiles) process.files = foundfiles toRestart = [process for process in list(processes.values()) if process.needsRestart(blacklist)] return toRestart # Tells if a given file is part of a package # Returns: # - False - file does not exist in the system or cannot be found when querying the package database # - True - file is found in an operating system package def ispackagedFile (f): file_in_package = False file_regexp = False if verbose: print("[DEBUG] Checking if file %s belongs to any package" % f) # First check if the file exists if not os.path.exists(f): if ( f.startswith('/lib/') or f.startswith('/usr/lib/') ) and re.compile("\.so[\d.]+$"): # For libraries that do not exist then try to use a regular expression with the # soname # In libraries, indent characters that could belong to a regular expression first f = re.compile("\+").sub("\+", f) f = re.compile(".so[\d.]+$").sub(".so.*", f) f = re.compile("\.").sub("\.", f) file_regexp = True else: # Do not call dpkg-query if the file simply does not exist in the file system # just assume it does not belong to any package return False # If it exists, run dpkg-query dpkgQuery = ["dpkg-query", "--search", f ] if verbose: print("[DEBUG] Running: %s" % ' '.join(dpkgQuery)) dpkgProc = subprocess.Popen(dpkgQuery, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env = lc_all_c_env, close_fds=True, universal_newlines=True) dpkgProc.wait() if verbose: print("[DEBUG] Running: %s" % ' '.join(dpkgQuery)) for line in dpkgProc.stdout.readlines(): line = line.strip() if line.find('no path found matching pattern ' + f) > 0: file_in_package = False break if line.endswith(f) or ( file_regexp and re.search(f, line)): package = re.compile(":.*$").sub("",line) file_in_package = True break if file_in_package and verbose: print("[DEBUG] YES: File belongs to package %s" % package) if not file_in_package and verbose: print("[DEBUG] NO: File does not belongs to any package") return file_in_package # Tells if a file has to be considered a deleted file # Returns: # - 0 (NO) for known locations of files which might be deleted # - 1 (YES) for valid deleted files we are interested in def isdeletedFile (f, blacklist = None): global lc_all_c_env, file_query_check if allFiles: return 1 if blacklist: for p in blacklist: if p.search(f): return 0 # We don't care about log files if f.startswith('/var/log/') or f.startswith('/var/local/log/'): return 0 # Or about files under temporary locations if f.startswith('/var/run/') or f.startswith('/var/local/run/'): return 0 # Or about files under /tmp if f.startswith('/tmp/'): return 0 # Or about files under /dev if f.startswith('/dev/'): return 0 # Or about files under /run if f.startswith('/run/'): return 0 # Or about files under /drm if f.startswith('/drm'): return 0 # Or about files under /i915 if f.startswith('/i915'): return 0 # Or about files under /var/tmp and /var/local/tmp if f.startswith('/var/tmp/') or f.startswith('/var/local/tmp/'): return 0 # Or /usr/lib/locale if f.startswith('/usr/lib/locale/'): return 0 # Skip files from the user's home directories # many processes hold temporary files there if f.startswith('/home/'): return 0 # Skip automatically generated files if f.endswith('icon-theme.cache'): return 0 # Skip font files if f.startswith('/var/cache/fontconfig/'): return 0 # Skip Nagios Spool if f.startswith('/var/lib/nagios3/spool/'): return 0 # Skip nagios spool files if f.startswith('/var/lib/nagios3/spool/checkresults/'): return 0 # Skip PostgreSQL files if f.startswith('/var/lib/postgresql/'): return 0 # Skip VDR lib files if f.startswith('/var/lib/vdr/'): return 0 # Skip Aio files found in MySQL servers if f.startswith('/[aio]'): return 0 # Skip memfd files if f.startswith('/memfd:'): return 0 # Skip dovecot mail indexes if f.endswith('/dovecot.index (deleted)') or f.endswith('/dovecot.index'): return 0 # Skip sssd cache if f.startswith('/var/lib/sss/mc/'): return 0 # Skip, if asked to, files that do not belong to any package if onlyPackageFiles: # Remove some lsof information from the file to ensure that it is # a proper filename file_name = re.sub(r'\(.*\)','', f) file_name = re.sub(r'\s+$','', file_name) # First check: have we checked this file before? If we have not then make the check if not file_name in file_query_check: file_query_check[file_name] = ispackagedFile(file_name) # Once we have the result then check if the file belongs to a package if not file_query_check[file_name]: return 0 # TODO: it should only care about library files (i.e. /lib, /usr/lib and the like) # build that check with a regexp to exclude others if f.endswith(' (deleted)'): return 1 if re.compile("\(path inode=[0-9]+\)$").search(f): return 1 # Default: it is a deleted file we are interested in return 1 def psdelcheck(): # TODO - Needs to be fixed to work here # Useful for seeing which processes need to be restarted after you upgrade # programs or shared libraries. Written to replace checkrestart(8) from the # debian-goodies, which often misses out processes due to bugs in lsof; see # <https://bugs.debian.org/264985> for more information. numeric = re.compile(r'\d+') toRestart = list(map (delmaps, list(map (string.atoi, list(filter (numeric.match, os.listdir('/proc'))))))) return toRestart def delmaps (pid): processes = {} process = processes.setdefault(pid,Process(int(pid))) deleted = re.compile(r'(.*) \(deleted\)$') boring = re.compile(r'/(dev/zero|SYSV([\da-f]{8}))|/usr/lib/locale') mapline = re.compile(r'^[\da-f]{8}-[\da-f]{8} [r-][w-][x-][sp-] ' r'[\da-f]{8} [\da-f]{2}:[\da-f]{2} (\d+) *(.+)( \(deleted\))?\n$') maps = open('/proc/%d/maps' % (pid)) for line in maps.readlines (): m = mapline.match (line) if (m): inode = string.atoi (m.group (1)) file = m.group (2) if inode == 0: continue # remove ' (deleted)' suffix if deleted.match (file): file = file [0:-10] if boring.match (file): continue # list file names whose inode numbers do not match their on-disk # values; or files that do not exist at all try: if os.stat (file)[stat.ST_INO] != inode: process = processes.setdefault(pid,Process(int(pid))) except OSError as e_tuple: (e, strerror) = e_tuple.args if e == errno.ENOENT: process = processes.setdefault(pid,Process(int(pid))) else: sys.stderr.write ('checkrestart (psdel): %s %s: %s\n' % (SysProcess.get(pid).info (), file, os.strerror (e))) else: print('checkrestart (psdel): Error parsing "%s"' % (line [0:-1])) maps.close () return process class SysProcess: re_name = re.compile('Name:\t(.*)$') re_uids = re.compile('Uid:\t(\d+)\t(\d+)\t(\d+)\t(\d+)$') processes = {} def get (pid): try: return Process.processes [pid] except KeyError: Process.processes [pid] = Process (pid) return Process.get (pid) # private def __init__ (self, pid): self.pid = pid status = open ('/proc/%d/status' % (self.pid)) for line in status.readlines (): m = self.re_name.match (line) if m: self.name = m.group (1) continue m = self.re_uids.match (line) if m: self.user = pwd.getpwuid (string.atoi (m.group (1)))[0] continue status.close () def info (self): return '%d %s %s' % (self.pid, self.name, self.user) class Process: def __init__(self, pid): self.pid = pid self.files = [] self.descriptors = [] self.links = [] self.program = '' try: self.program = os.readlink('/proc/%d/exe' % self.pid) # if the executable command is an interpreter such as perl/python/ruby/tclsh, # we want to find the real program m = re.match("^/usr/bin/(perl|python|ruby|tclsh)", self.program) if m: with open('/proc/%d/cmdline' % self.pid, 'r') as cmdline: # only match program in /usr (ex.: /usr/sbin/smokeping) # ignore child, etc. #m = re.search(r'^(([/]\w*){1,5})\s.*$', cmdline.read()) # Split by null-bytes, see proc(5) data = cmdline.read().split('\x00') # Last character should be null-byte, too, see proc(5) if not data[-1]: data.pop() # Spamd sets $0 wrongly, see # https://bugzilla.redhat.com/show_bug.cgi?id=755644 # i.e. the blank after spamd is relevant in case # this will be fixed in the future. m = re.match("^/usr/sbin/spamd |^spamd ", data[0]) if m: self.program = "/usr/sbin/spamd" else: # Strip first value, the interpreter data.pop(0) # Check if something's left after the interpreter, see #715000 if data: # Strip all options following the interpreter, e.g. python's -O m = re.match("^-", data[0]) while (m): data.pop(0) if not data: break m = re.match("^-", data[0]) if data and data[0]: data = self.which(data[0]) m = re.search(r'^(/usr/\S+)$', data) if m: # store the real full path of script as the program self.program = m.group(1) except OSError as e: if e.errno != errno.ENOENT: if self.pid == 1: sys.stderr.write("Found unreadable pid 1. Assuming we're under vserver and continuing.\n") else: sys.stderr.write('ERROR: Failed to read %d' % self.pid) raise self.program = self.cleanFile(self.program) def which(self, program): if os.path.isabs(program): return program path = os.environ.get("PATH", os.defpath).split(os.pathsep) seen = set() for dir in path: dir = os.path.normcase(os.path.abspath(dir)) if not dir in seen: seen.add(dir) name = os.path.join(dir, program) if os.path.exists(name) and os.access(name, os.F_OK|os.X_OK) and not os.path.isdir(name): return name return program def cleanFile(self, f): # /proc/pid/exe has all kinds of junk in it sometimes null = f.find('\0') if null != -1: f = f[:null] # Support symlinked /usr if f.startswith('/usr'): statinfo = os.lstat('/usr')[ST_MODE] # If /usr is a symlink then find where it points to if S_ISLNK(statinfo): newusr = os.readlink('/usr') if not newusr.startswith('/'): # If the symlink is relative, make it absolute newusr = os.path.join(os.path.dirname('/usr'), newusr) f = re.sub('^/usr',newusr, f) # print "Changing usr to " + newusr + " result:" +f; # Debugging return re.sub('( \(deleted\)|.dpkg-new).*$','',f) def listDeletedHelper(self): listfiles = [] listdescriptors = [] for f in self.files: if isdeletedFile(f): listfiles.append(f) return listfiles def listDeleted(self): listfiles = self.listDeletedHelper() if listfiles != []: print("List of deleted files in use:") for file in listfiles: print("\t" + file) def listDeletedMachine(self): listfiles = self.listDeletedHelper() for file in listfiles: print('file\t%s\t%s\t%s' % (self.pid, self.program, file)) # Check if a process needs to be restarted, previously we would # just check if it used libraries named '.dpkg-new' since that's # what dpkg would do. Now we need to be more contrieved. # Returns: # - 0 if there is no need to restart the process # - 1 if the process needs to be restarted def needsRestart(self, blacklist = None): for f in self.files: if isdeletedFile(f, blacklist): return 1 for f in self.links: if f == 0: return 1 return 0 class Package: def __init__(self, name): self.name = name # use a set, we don't need duplicates self.initscripts = set() self.systemdservice = set() self.processes = [] if __name__ == '__main__': main()