A hint: This file contains one or more very long lines, so maybe it is better readable using the pure text view mode that shows the contents as wrapped lines within the browser window.
1 #!/usr/bin/env python 2 3 import os, sys, glob, re, shutil, getopt, popen2, time, fnmatch 4 import ConfigParser, urlparse, pwd, grp, stat, syslog 5 import difflib, smtplib, gzip, md5, sha 6 7 VERSION = '0.5.1' 8 9 enable = ('yes', 'on', 'true', '1') 10 disable = ('no', 'off', 'false', '0') 11 12 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' 13 14 class Options: 15 def __init__(self, args): 16 self.configfile = '/etc/dconf.conf' 17 self.dist = None 18 self.output = None 19 self.quiet = False 20 self.verbose = 1 21 22 try: 23 opts, args = getopt.getopt (args, 'c:ho:qv', 24 ['config=', 'help', 'output=', 'quiet', 'verbose', 'version']) 25 except getopt.error, exc: 26 print 'dconf: %s, try dconf -h for a list of all the options' % str(exc) 27 sys.exit(1) 28 29 for opt, arg in opts: 30 if opt in ['-c', '--config']: 31 self.configfile = os.path.abspath(arg) 32 elif opt in ['-h', '--help']: 33 self.usage() 34 self.help() 35 sys.exit(0) 36 elif opt in ('-o', '--output'): 37 self.output=arg 38 elif opt in ['-q', '--quiet']: 39 self.quiet = True 40 elif opt in ['-v', '--verbose']: 41 self.verbose = self.verbose + 1 42 elif opt in ['--version']: 43 self.version() 44 sys.exit(0) 45 46 if self.quiet: 47 self.verbose = 0 48 49 if self.verbose >= 3: 50 print 'Verbosity set to level %d' % (self.verbose - 1) 51 print 'Using configfile %s' % self.configfile 52 53 def version(self): 54 print 'dconf %s' % VERSION 55 print 'Written by Dag Wieers <dag@wieers.com>' 56 print 57 print 'platform %s/%s' % (os.name, sys.platform) 58 print 'python %s' % sys.version 59 60 def usage(self): 61 print 'usage: dconf [-q] [-v] [-c config] [-o output]' 62 63 def help(self): 64 print '''Create a system's hardware and software configuration snapshot 65 66 Dconf options: 67 -c, --config=file specify alternative configfile 68 -o, --output=file write output to given file 69 -q, --quiet minimal output 70 -v, --verbose increase verbosity 71 -vv, -vvv increase verbosity more 72 ''' 73 74 class Config: 75 def __init__(self): 76 self.sections = {} 77 78 self.includefile(op.configfile) 79 self.include = self.getoption('main', 'include', None) 80 81 def includefile(self, configfile): 82 self.cfg = ConfigParser.ConfigParser() 83 84 (s,b,p,q,f,o) = urlparse.urlparse(configfile) 85 if s in ('http', 'ftp', 'file'): 86 configfh = urllib.urlopen(configfile) 87 try: 88 self.cfg.readfp(configfh) 89 except ConfigParser.MissingSectionHeaderError, e: 90 die(6, 'Error accessing URL: %s' % configfile) 91 else: 92 if os.access(configfile, os.R_OK): 93 try: 94 self.cfg.read(configfile) 95 except: 96 die(7, 'Syntax error reading file: %s' % configfile) 97 else: 98 die(6, 'Error accessing file: %s' % configfile) 99 100 self.compression = self.getoption('main', 'compression', 'gzip') 101 self.cron = self.getoption('main', 'cron', None) 102 self.logdir = self.getoption('main', 'logdir', '/var/log/dconf') 103 self.mailto = self.getoption('main', 'mailto', None) 104 self.smtpserver = self.getoption('main', 'smtp-server', 'localhost') 105 self.rpm = not self.getoption('main', 'rpm', 'no') in disable 106 self.exclude = self.getoption('main', 'exclude', excludelist).split() 107 108 if not op.output: 109 op.output = self.getoption('main', 'output', None) 110 111 self.quiet = not self.getoption('main', 'quiet', 'no') in disable 112 if op.verbose == 1 and self.quiet: 113 op.verbose = 0 114 115 sections = self.cfg.sections() 116 sections.sort() 117 for section in sections: 118 if section in ['main']: 119 continue 120 else: 121 self.sections[section] = {} 122 for option in self.cfg.options(section): 123 if option in ('cmds', 'dirs', 'files'): 124 self.sections[section][option] = self.getoption(section, option, '').split('\n') 125 126 def getoption(self, section, option, var): 127 "Get an option from a section from configfile" 128 try: 129 var = self.cfg.get(section, option) 130 # info(3, 'Setting option %s in section [%s] to: %s' % (option, section, var)) 131 except ConfigParser.NoSectionError, e: 132 # info(4, 'Failed to find section [%s] in %s' % (section, op.configfile)) 133 pass 134 except ConfigParser.NoOptionError, e: 135 # info(4, 'Setting option %s in section [%s] to: %s (default)' % (option, section, var)) 136 pass 137 return var 138 139 def dzopen(filename, arg='r'): 140 "Opens a file using compression based on file's extension" 141 if fnmatch.fnmatch(filename, '*.gz'): 142 return gzip.open(filename, arg) 143 elif fnmatch.fnmatch(filename, '*.bz2'): 144 return BZ2File.open(filename, arg) 145 else: 146 return open(filename, arg) 147 148 def md5sum(filename): 149 "Return md5 from file" 150 md5o = md5.new(); f = dzopen(filename); md5o.update(f.read()); f.close() 151 return md5o 152 153 ### FIXME: Also check timestamps/owner/perms 154 def md5check(h, filename): 155 "Check if md5sum is the same as in rpmdb" 156 if os.path.exists(filename): 157 for file in h.fiFromHeader(): 158 if file[0] == filename: 159 if md5sum(filename).hexdigest() == file[12]: 160 return True 161 return False 162 163 def sha1sum(filename): 164 "Return sha1 from file" 165 sha1o = sha.new(); f = dzopen(filename); sha1o.update(f.read()); f.close() 166 return sha1o 167 168 ### BOGUS function, rpmdb only has md5 ? 169 def sha1check(h, filename): 170 "Check if sha1sum is the same as in rpmdb" 171 if os.path.exists(filename): 172 for file in h.fiFromHeader(): 173 if file[0] == filename: 174 if sha1sum(filename).hexdigest() == file[12]: 175 return True 176 return False 177 178 def fromrpmdb(filename): 179 "Check if file is inside rpmdb" 180 if not ts: return None 181 mi = ts.dbMatch('basenames', filename) 182 for h in mi: 183 return h 184 else: 185 info(5, 'File %s not in rpmdb, including' % filename) 186 return None 187 188 def info(level, str): 189 "Output info message" 190 if level <= op.verbose: 191 print str 192 193 def die(ret, str): 194 "Print error and exit with errorcode" 195 info(0, str) 196 sys.exit(ret) 197 198 def cleanup(): 199 "Clean up logfile when interrupted." 200 logfile = os.path.join(cf.logdir, 'dconf-' + hostname + '-' + timestamp + '.log' + extension) 201 if os.path.isfile(logfile): 202 remove(logfile) 203 204 def symlink(src, dst): 205 "Create a symbolic link, force if dst exists" 206 if not os.path.islink(dst) and os.path.isdir(dst): 207 dst = os.path.join(dst, os.path.basename(src)) 208 ### Not using filecmp increases speed with 15% 209 # if os.path.isfile(dst) and filecmp.cmp(src, dst) == 0: 210 if os.path.isfile(dst): 211 os.unlink(dst) 212 if os.path.islink(dst): 213 os.unlink(dst) 214 mkdir(os.path.dirname(dst)) 215 if not os.path.exists(dst): 216 os.symlink(src, dst) 217 218 def remove(*files): 219 "Remove files or directories" 220 for file in files: 221 if os.path.islink(file): 222 os.unlink(file) 223 elif os.path.isdir(file): 224 try: 225 os.rmdir(file) 226 except: 227 os.path.walk(file, removedir, ()) 228 elif os.path.exists(file): 229 os.unlink(file) 230 231 def removedir(void, dir, files): 232 for file in files: 233 remove(os.path.join(dir, file)) 234 235 def mkdir(path): 236 "Create a directory, and parents if needed" 237 if not os.path.exists(path): 238 os.makedirs(path) 239 240 def getowner(uid, gid): 241 try: 242 owner = pwd.getpwuid(uid)[0] 243 except: 244 owner = uid 245 try: 246 group = grp.getgrgid(gid)[0] 247 except: 248 group = gid 249 return owner, group 250 251 def sectiontitle(section): 252 info(3, 'Processing section [%s]' % section) 253 stitle = '== ' + section.upper() + ' ==' 254 return stitle + '=' * (80 - len(stitle)) + '\n' 255 256 def which(cmd): 257 "Find executables in PATH environment" 258 for path in os.environ.get('PATH','$PATH').split(':'): 259 if os.path.isfile(os.path.join(path, cmd)): 260 info(4, 'Found command %s in path %s' % (cmd, path)) 261 return os.path.join(path, cmd) 262 return None 263 264 def diff(fromfile, tofile): 265 "Create a unified diff from 2 files" 266 msg = '' 267 fromfd = dzopen(fromfile) 268 tofd = dzopen(tofile) 269 for line in difflib.unified_diff(fromfd.readlines(), tofd.readlines(), fromfile, tofile, os.stat(fromfile).st_mtime, os.stat(tofile).st_mtime): 270 msg = msg + line 271 tofd.close() 272 fromfd.close() 273 return msg 274 275 def fnmatches(file): 276 for entry in cf.exclude: 277 if fnmatch.fnmatch(file, entry): 278 # info(6, 'File %s matches against glob %s' % (file, entry)) 279 return True 280 return False 281 282 def mail(subject, msg): 283 info(2, 'Sending mail to: %s' % cf.mailto) 284 try: 285 smtp = smtplib.SMTP(cf.smtpserver) 286 # server.set_debuglevel(1) 287 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) 288 for email in cf.mailto.split(): 289 smtp.sendmail('dconf@%s' % os.uname()[1], email, 'To: %s\n%s' % (email, msg)) 290 smtp.quit() 291 except: 292 info(1, 'Sending mail via %s failed.' % cf.smtpserver) 293 294 def main(): 295 global extension, hostname, timestamp, ts 296 297 # if cf.rpm or which('rpm'): 298 try: 299 import rpm 300 ts = rpm.TransactionSet() 301 # ts.setVSFlags(rpm.RPMVSF_NORSA | rpm.RPMVSF_NODSA) 302 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES | rpm.RPMVSF_NOHDRCHK | rpm._RPMVSF_NODIGESTS | rpm.RPMVSF_NEEDPAYLOAD) 303 except: 304 # else: 305 info(2, 'Disabling RPM capability since the rpm-python bindings could not be loaded.') 306 cf.rpm = False 307 ts = None 308 309 if cf.compression == 'gzip': 310 extension = '.log.gz' 311 elif cf.compression == 'bzip2': 312 extension = '.log.bz2' 313 else: 314 extension = '.log' 315 316 hostname = os.uname()[1].split('.')[0] 317 # hostname = os.uname()[1] 318 # hostname = socket.gethostbyaddr(socket.gethostname())[0] 319 timestamp = time.strftime('%Y%m%d-%H%M%S', time.localtime()) 320 321 os.umask(077) 322 323 # os.setenv('LC_ALL', 'C') 324 os.environ['LC_ALL'] = 'C' 325 326 syslog.openlog('dconf[%d]' % os.getpid()) 327 syslog.syslog('Dconf %s started.' % VERSION) 328 329 ### Add to cron 330 if cf.cron and not op.output: 331 if cf.cron in ('hourly', 'daily', 'weekly', 'monthly'): 332 cronfile = os.path.join('/etc', 'cron.%s' % cf.cron, 'dconf') 333 if os.path.isdir(os.path.dirname(cronfile)): 334 if (os.path.realpath(cronfile) != which('dconf')): 335 info(2, 'Adding dconf to cron (%s).' % cf.cron) 336 symlink(which('dconf'), cronfile) 337 else: 338 info(2, 'Path %s does not exist, ignoring.' % os.path.dirname(cronfile)) 339 else: 340 info(2, 'Option cron should be set to hourly, daily, weekly or monthly.') 341 342 ### Remove from cron 343 for item in ('hourly', 'daily', 'weekly', 'monthly'): 344 if item != cf.cron: 345 cronfile = os.path.join('/etc', 'cron.%s' % item, 'dconf') 346 if os.path.isfile(cronfile): 347 info(2, 'Removing dconf from cron (%s).' % item) 348 remove(cronfile) 349 350 if op.output == '-': 351 logfile = '- (stdout)' 352 log = sys.stdout 353 op.quiet = True 354 op.verbose = 0 355 elif op.output: 356 logfile = op.output 357 log = dzopen(logfile, 'w') 358 else: 359 logfile = os.path.join(cf.logdir, 'dconf-' + hostname + '-' + timestamp + extension) 360 latestlog = os.path.join(cf.logdir, 'dconf-' + hostname + '-latest' + extension) 361 previouslog = os.path.join(cf.logdir, 'dconf-' + hostname + '-previous' + extension) 362 oldestlog = os.path.join(cf.logdir, 'dconf-' + hostname + '-oldest' + extension) 363 mkdir(cf.logdir) 364 os.chmod(cf.logdir, 0700) 365 log = dzopen(logfile, 'w') 366 367 info(2, 'Building file: %s' % logfile) 368 sections = cf.sections.keys() 369 sections.sort() 370 for section in sections: 371 stitle = False 372 373 if cf.sections[section].has_key('cmds'): 374 for line in cf.sections[section]['cmds']: 375 cmdline = line.split() 376 if not cmdline: continue 377 378 cmd = which(cmdline[0].strip()) 379 extra = ' '.join(cmdline[1:]) 380 if not cmd: 381 info(5, 'Cmd %s not found in PATH, excluding.' % cmdline[0].strip()) 382 continue 383 384 if not os.access(cmd, os.X_OK): 385 info(1, 'Cmd %s cannot be executed, excluding.' % cmd) 386 continue 387 388 if not stitle: 389 stitle = True 390 log.write(sectiontitle(section)) 391 392 info(4, 'Processing cmd %s' % cmd) 393 (mode, t, t, t, uid, gid, size, t, t, t) = os.stat(cmd) 394 owner, group = getowner(uid, gid) 395 title = '--[ Cmd: %s %s ]--(%04o, %s, %s, %s)--' % (cmd, extra, stat.S_IMODE(mode), owner, group, size) 396 log.write(title + '-' * (80 - len(title)) + '\n') 397 (o, i) = popen2.popen4('%s %s' % (cmd, extra)) 398 log.write(o.read() + '\n') 399 400 if cf.sections[section].has_key('files'): 401 for line in cf.sections[section]['files']: 402 flist = line.split('|') 403 if not flist: continue 404 extra = '|'.join(flist[1:]) 405 if extra: extra = extra + ' ' 406 for file in glob.glob(flist[0].strip()): 407 if not file or not os.path.isfile(file): continue 408 409 if fnmatches(file): 410 info(5, 'File %s is in the exclude list, excluding.' % file) 411 continue 412 413 (mode, t, t, t, uid, gid, size, t, t, t) = os.stat(file) 414 if not size: continue 415 416 if ts: 417 h = fromrpmdb(file) 418 if h and md5check(h, file): 419 info(5, 'File %s has not been changed since installation, excluding.' % file) 420 continue 421 422 if not os.access(file, os.R_OK): 423 info(1, 'File %s cannot be read, excluding.' % file) 424 continue 425 426 if not stitle: 427 stitle = True 428 log.write(sectiontitle(section)) 429 430 info(4, 'Processing file %s' % file) 431 owner, group = getowner(uid, gid) 432 title = '--[ File: %s %s]--(%04o, %s, %s, %s)--' % (file, extra, stat.S_IMODE(mode), owner, group, size) 433 log.write(title + '-' * (80 - len(title)) + '\n') 434 (o, i) = popen2.popen4('cat %s %s' % (file, extra)) 435 log.write(o.read() + '\n') 436 log.close() 437 438 syslog.syslog('Dconf %s ended succesfully.' % VERSION) 439 syslog.closelog() 440 441 if op.output: 442 return 443 444 if os.path.isfile(latestlog): 445 if sha1sum(latestlog).digest() != sha1sum(logfile).digest(): 446 info(2, 'New logfile is different than last logfile, keeping.') 447 symlink(os.path.basename(os.path.realpath(latestlog)), previouslog) 448 symlink(os.path.basename(logfile), latestlog) 449 if cf.mailto: 450 mail('changes to %s' % os.uname()[1], diff(os.path.realpath(previouslog), os.path.realpath(latestlog))) 451 else: 452 info(2, 'New logfile is identical to last logfile, removing.') 453 remove(logfile) 454 else: 455 info(2, 'Marking this as a first time run, symlinking.') 456 symlink(os.path.basename(logfile), oldestlog) 457 symlink(os.path.basename(logfile), previouslog) 458 symlink(os.path.basename(logfile), latestlog) 459 if cf.mailto: 460 mail('initial config for %s' % os.uname()[1], dzopen(logfile, 'r').read()) 461 462 ### Workaround for python <= 2.2.1 463 try: 464 True, False 465 except NameError: 466 True = 1 467 False = 0 468 469 ### Main entrance 470 if __name__ == '__main__': 471 op=Options(sys.argv[1:]) 472 cf=Config() 473 for cfgfile in cf.include.split(): 474 if os.path.isfile(cfgfile): 475 info(2, 'Processing configfile %s.' % cfgfile) 476 cf.includefile(cfgfile) 477 else: 478 info(3, 'Configfile %s does not exist, ignoring.') 479 480 try: 481 main() 482 except KeyboardInterrupt, e: 483 cleanup() 484 die(6, 'Exiting on user request') 485 except: 486 cleanup() 487 raise 488 489 # vim:ts=4:sw=4