#!/usr/bin/env python import os, sys, glob, re, shutil, getopt, popen2, time, fnmatch import ConfigParser, urlparse, pwd, grp, stat, syslog import difflib, gzip, md5, sha VERSION = '0.5.1svn' enable = ('yes', 'on', 'true', '1') disable = ('no', 'off', 'false', '0') excludelist = 'CVS CVS.adm RCS RCSLOG SCCS TAGS cvslog.* tags .make.state .nse_depinfo *~ #* .#* ,* _$* *$ *.old *.bak *.BAK *.orig *.rej .del-* *.a *.olb *.o *.lo *.la *.obj *.so *.exe *.Z *.elc *.ln core core.[0-9]* .svn *.rpmorig *.rpmnew *.rpmsave .DS_Store' class Options: def __init__(self, args): self.configfile = '/etc/dconf.conf' self.dist = None self.output = None self.quiet = False self.verbose = 1 try: opts, args = getopt.getopt (args, 'c:ho:qv', ['config=', 'help', 'output=', 'quiet', 'verbose', 'version']) except getopt.error, exc: print 'dconf: %s, try dconf -h for a list of all the options' % str(exc) sys.exit(1) for opt, arg in opts: if opt in ['-c', '--config']: self.configfile = os.path.abspath(arg) elif opt in ['-h', '--help']: self.usage() self.help() sys.exit(0) elif opt in ('-o', '--output'): self.output=arg elif opt in ['-q', '--quiet']: self.quiet = True elif opt in ['-v', '--verbose']: self.verbose = self.verbose + 1 elif opt in ['--version']: self.version() sys.exit(0) if self.quiet: self.verbose = 0 if self.verbose >= 3: print 'Verbosity set to level %d' % (self.verbose - 1) print 'Using configfile %s' % self.configfile def version(self): print 'dconf %s' % VERSION print 'Written by Dag Wieers ' print print 'platform %s/%s' % (os.name, sys.platform) print 'python %s' % sys.version def usage(self): print 'usage: dconf [-q] [-v] [-c config] [-o output]' def help(self): print '''Create a system's hardware and software configuration snapshot Dconf options: -c, --config=file specify alternative configfile -o, --output=file write output to given file -q, --quiet minimal output -v, --verbose increase verbosity -vv, -vvv increase verbosity more ''' class Config: def __init__(self): self.sections = {} self.read(op.configfile) self.confdir = self.getoption('main', 'confdir', '/etc/dconf.d') self.logdir = self.getoption('main', 'logdir', '/var/log/dconf') if not op.output: op.output = self.getoption('main', 'output', None) self.quiet = not self.getoption('main', 'quiet', 'no') in disable if op.verbose == 1 and self.quiet: op.verbose = 0 def read(self, configfile): self.cfg = ConfigParser.ConfigParser() info(2, 'Reading config file %s' % (configfile)) (s,b,p,q,f,o) = urlparse.urlparse(configfile) if s in ('http', 'ftp', 'file'): configfh = urllib.urlopen(configfile) try: self.cfg.readfp(configfh) except ConfigParser.MissingSectionHeaderError, e: die(6, 'Error accessing URL: %s' % configfile) else: if os.access(configfile, os.R_OK): try: self.cfg.read(configfile) except: die(7, 'Syntax error reading file: %s' % configfile) else: die(6, 'Error accessing file: %s' % configfile) self.compression = self.getoption('main', 'compression', 'gzip') self.cron = self.getoption('main', 'cron', None) self.mailfrom = self.getoption('main', 'mailfrom', 'mrepo@%s' % os.uname()[1]) self.mailto = self.getoption('main', 'mailto', None) self.smtpserver = self.getoption('main', 'smtp-server', 'localhost') self.rpm = not self.getoption('main', 'rpm', 'no') in disable self.exclude = self.getoption('main', 'exclude', excludelist).split() sections = self.cfg.sections() sections.sort() for section in sections: if section in ('main', ): continue else: self.sections[section] = {} for option in self.cfg.options(section): if option in ('cmds', 'dirs', 'files'): self.sections[section][option] = self.getoption(section, option, '').split('\n') def getoption(self, section, option, var): "Get an option from a section from configfile" try: var = self.cfg.get(section, option) # info(3, 'Setting option %s in section [%s] to: %s' % (option, section, var)) except ConfigParser.NoSectionError, e: # info(4, 'Failed to find section [%s] in %s' % (section, op.configfile)) pass except ConfigParser.NoOptionError, e: # info(4, 'Setting option %s in section [%s] to: %s (default)' % (option, section, var)) pass return var def dzopen(filename, arg='r'): "Opens a file using compression based on file's extension" if fnmatch.fnmatch(filename, '*.gz'): return gzip.open(filename, arg) elif fnmatch.fnmatch(filename, '*.bz2'): return BZ2File.open(filename, arg) else: return open(filename, arg) def md5sum(filename): "Return md5 from file" md5o = md5.new(); f = dzopen(filename); md5o.update(f.read()); f.close() return md5o ### FIXME: Also check timestamps/owner/perms def md5check(h, filename): "Check if md5sum is the same as in rpmdb" if os.path.exists(filename): for file in h.fiFromHeader(): if file[0] == filename: if md5sum(filename).hexdigest() == file[12]: return True return False def sha1sum(filename): "Return sha1 from file" sha1o = sha.new(); f = dzopen(filename); sha1o.update(f.read()); f.close() return sha1o ### BOGUS function, rpmdb only has md5 ? def sha1check(h, filename): "Check if sha1sum is the same as in rpmdb" if os.path.exists(filename): for file in h.fiFromHeader(): if file[0] == filename: if sha1sum(filename).hexdigest() == file[12]: return True return False def fromrpmdb(filename): "Check if file is inside rpmdb" if not ts: return None mi = ts.dbMatch('basenames', filename) for h in mi: return h else: info(5, 'File %s not in rpmdb, including' % filename) return None def info(level, str): "Output info message" if level <= op.verbose: print str def die(ret, str): "Print error and exit with errorcode" info(0, str) sys.exit(ret) def cleanup(): "Clean up logfile when interrupted." logfile = os.path.join(cf.logdir, 'dconf-' + hostname + '-' + timestamp + '.log' + extension) if os.path.isfile(logfile): remove(logfile) def symlink(src, dst): "Create a symbolic link, force if dst exists" if not os.path.islink(dst) and os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) ### Not using filecmp increases speed with 15% # if os.path.isfile(dst) and filecmp.cmp(src, dst) == 0: if os.path.isfile(dst): os.unlink(dst) if os.path.islink(dst): os.unlink(dst) mkdir(os.path.dirname(dst)) if not os.path.exists(dst): os.symlink(src, dst) def remove(*files): "Remove files or directories" for file in files: if os.path.islink(file): os.unlink(file) elif os.path.isdir(file): try: os.rmdir(file) except: os.path.walk(file, removedir, ()) elif os.path.exists(file): os.unlink(file) def removedir(void, dir, files): for file in files: remove(os.path.join(dir, file)) def mkdir(path): "Create a directory, and parents if needed" if not os.path.exists(path): os.makedirs(path) def getowner(uid, gid): try: owner = pwd.getpwuid(uid)[0] except: owner = uid try: group = grp.getgrgid(gid)[0] except: group = gid return owner, group def sectiontitle(section): info(3, 'Processing section [%s]' % section) stitle = '== ' + section.upper() + ' ==' return stitle + '=' * (80 - len(stitle)) + '\n' def which(cmd): "Find executables in PATH environment" for path in os.environ.get('PATH','$PATH').split(':'): if os.path.isfile(os.path.join(path, cmd)): info(4, 'Found command %s in path %s' % (cmd, path)) return os.path.join(path, cmd) return None def diff(fromfile, tofile): "Create a unified diff from 2 files" msg = '' fromfd = dzopen(fromfile) tofd = dzopen(tofile) for line in difflib.unified_diff(fromfd.readlines(), tofd.readlines(), fromfile, tofile, os.stat(fromfile).st_mtime, os.stat(tofile).st_mtime): msg = msg + line tofd.close() fromfd.close() return msg def fnmatches(file): for entry in cf.exclude: if fnmatch.fnmatch(file, entry): # info(6, 'File %s matches against glob %s' % (file, entry)) return True return False def mail(subject, msg): info(2, 'Sending mail to: %s' % cf.mailto) try: import smtplib smtp = smtplib.SMTP(cf.smtpserver) # server.set_debuglevel(1) msg = 'Subject: [dconf] %s\n\nCurrent time:\n%s\nSystem information:\n%s\nUptime:\n%s\nCurrently logged on users:\n%s\n%s\n\n%s' % (subject, os.popen('date').read(), os.popen('uname -a').read(), os.popen('uptime').read(), os.popen('who').read(), subject, msg) for email in cf.mailto.split(): smtp.sendmail(cf.mailfrom, email, 'To: %s\n%s' % (email, msg)) smtp.quit() except: info(1, 'Sending mail via %s failed.' % cf.smtpserver) def readconfig(): cf = Config() if cf.confdir and os.path.isdir(cf.confdir): files = glob.glob(os.path.join(cf.confdir, '*.conf')) files.sort() for configfile in files: cf.read(configfile) # cf.update(configfile) return cf def main(): global extension, hostname, timestamp, ts # if cf.rpm or which('rpm'): try: import rpm ts = rpm.TransactionSet() # ts.setVSFlags(rpm.RPMVSF_NORSA | rpm.RPMVSF_NODSA) ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES | rpm.RPMVSF_NOHDRCHK | rpm._RPMVSF_NODIGESTS | rpm.RPMVSF_NEEDPAYLOAD) except: # else: info(2, 'Disabling RPM capability since the rpm-python bindings could not be loaded.') cf.rpm = False ts = None if cf.compression == 'gzip': extension = '.log.gz' elif cf.compression == 'bzip2': extension = '.log.bz2' else: extension = '.log' hostname = os.uname()[1].split('.')[0] # hostname = os.uname()[1] # hostname = socket.gethostbyaddr(socket.gethostname())[0] timestamp = time.strftime('%Y%m%d-%H%M%S', time.localtime()) os.umask(077) # os.setenv('LC_ALL', 'C') os.environ['LC_ALL'] = 'C' syslog.openlog('dconf[%d]' % os.getpid()) syslog.syslog('Dconf %s started.' % VERSION) ### Add to cron if cf.cron and not op.output: if cf.cron in ('hourly', 'daily', 'weekly', 'monthly'): cronfile = os.path.join('/etc', 'cron.%s' % cf.cron, 'dconf') if os.path.isdir(os.path.dirname(cronfile)): if (os.path.realpath(cronfile) != which('dconf')): info(2, 'Adding dconf to cron (%s).' % cf.cron) symlink(which('dconf'), cronfile) else: info(2, 'Path %s does not exist, ignoring.' % os.path.dirname(cronfile)) else: info(2, 'Option cron should be set to hourly, daily, weekly or monthly.') ### Remove from cron for item in ('hourly', 'daily', 'weekly', 'monthly'): if item != cf.cron: cronfile = os.path.join('/etc', 'cron.%s' % item, 'dconf') if os.path.isfile(cronfile): info(2, 'Removing dconf from cron (%s).' % item) remove(cronfile) if op.output == '-': logfile = '- (stdout)' log = sys.stdout op.quiet = True op.verbose = 0 elif op.output: logfile = op.output log = dzopen(logfile, 'w') else: logfile = os.path.join(cf.logdir, 'dconf-' + hostname + '-' + timestamp + extension) latestlog = os.path.join(cf.logdir, 'dconf-' + hostname + '-latest' + extension) previouslog = os.path.join(cf.logdir, 'dconf-' + hostname + '-previous' + extension) oldestlog = os.path.join(cf.logdir, 'dconf-' + hostname + '-oldest' + extension) mkdir(cf.logdir) os.chmod(cf.logdir, 0700) log = dzopen(logfile, 'w') info(2, 'Building file: %s' % logfile) sections = cf.sections.keys() sections.sort() for section in sections: stitle = False if cf.sections[section].has_key('cmds'): for line in cf.sections[section]['cmds']: cmdline = line.split() if not cmdline: continue cmd = which(cmdline[0].strip()) extra = ' '.join(cmdline[1:]) if not cmd: info(5, 'Cmd %s not found in PATH, excluding.' % cmdline[0].strip()) continue if not os.access(cmd, os.X_OK): info(1, 'Cmd %s cannot be executed, excluding.' % cmd) continue if not stitle: stitle = True log.write(sectiontitle(section)) info(4, 'Processing cmd %s' % cmd) (mode, t, t, t, uid, gid, size, t, t, t) = os.stat(cmd) owner, group = getowner(uid, gid) title = '--[ Cmd: %s %s ]--(%04o, %s, %s, %s)--' % (cmd, extra, stat.S_IMODE(mode), owner, group, size) log.write(title + '-' * (80 - len(title)) + '\n') (o, i) = popen2.popen4('%s %s' % (cmd, extra)) log.write(o.read() + '\n') if cf.sections[section].has_key('files'): for line in cf.sections[section]['files']: flist = line.split('|') if not flist: continue extra = '|'.join(flist[1:]) if extra: extra = extra + ' ' for file in glob.glob(flist[0].strip()): if not file or not os.path.isfile(file): continue if fnmatches(file): info(5, 'File %s is in the exclude list, excluding.' % file) continue (mode, t, t, t, uid, gid, size, t, t, t) = os.stat(file) if not size: continue if ts: h = fromrpmdb(file) if h and md5check(h, file): info(5, 'File %s has not been changed since installation, excluding.' % file) continue if not os.access(file, os.R_OK): info(1, 'File %s cannot be read, excluding.' % file) continue if not stitle: stitle = True log.write(sectiontitle(section)) info(4, 'Processing file %s' % file) owner, group = getowner(uid, gid) title = '--[ File: %s %s]--(%04o, %s, %s, %s)--' % (file, extra, stat.S_IMODE(mode), owner, group, size) log.write(title + '-' * (80 - len(title)) + '\n') (o, i) = popen2.popen4('cat %s %s' % (file, extra)) log.write(o.read() + '\n') log.close() syslog.syslog('Dconf %s ended succesfully.' % VERSION) syslog.closelog() if op.output: return if os.path.isfile(latestlog): if sha1sum(latestlog).digest() != sha1sum(logfile).digest(): info(2, 'New logfile is different than last logfile, keeping.') symlink(os.path.basename(os.path.realpath(latestlog)), previouslog) symlink(os.path.basename(logfile), latestlog) if cf.mailto: mail('changes to %s' % os.uname()[1], diff(os.path.realpath(previouslog), os.path.realpath(latestlog))) else: info(2, 'New logfile is identical to last logfile, removing.') remove(logfile) else: info(2, 'Marking this as a first time run, symlinking.') symlink(os.path.basename(logfile), oldestlog) symlink(os.path.basename(logfile), previouslog) symlink(os.path.basename(logfile), latestlog) if cf.mailto: mail('initial config for %s' % os.uname()[1], dzopen(logfile, 'r').read()) ### Workaround for python <= 2.2.1 try: True, False except NameError: True = 1 False = 0 ### Main entrance if __name__ == '__main__': op = Options(sys.argv[1:]) cf = readconfig() try: main() except KeyboardInterrupt, e: cleanup() die(6, 'Exiting on user request') except: cleanup() raise # vim:ts=4:sw=4:et