#!/usr/local/bin/python2.3
'''queue_repair.py - qmail tools in Python.
Copyright (C) 2001 Charles Cazabon <pqt @ discworld.dyndns.org>
This program is free software; you can redistribute it and/or
modify it under the terms of version 2 of the GNU General Public License
as published by the Free Software Foundation. A copy of this license should
be included in the file COPYING.
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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
'''
__version__ = '0.9.0'
__author__ = 'Charles Cazabon <pqt @ discworld.dyndns.org>'
import sys
import os
from stat import *
import string
import pwd
import grp
import getopt
#######################################
# globals
#######################################
confqmail = '/var/qmail'
wd = None
testmode = 1
checked_dir = {}
checked_owner = {}
checked_mode = {}
#######################################
# data
#######################################
users = {
'alias' : None,
'qmaild' : None,
'qmaill' : None,
'qmailp' : None,
'qmailq' : None,
'qmailr' : None,
'qmails' : None,
}
groups = {
'qmail' : None,
'qqnofiles' : None,
}
dirs = {
# Directories to check; format is:
# key: pathname - all paths are relative to conf-qmail
# data: (user, group, mode, split)
# split is: 0 : no, 1 : yes, -1 : only with big-todo
'queue' : ('qmailq', 'qmail', 0750, 0),
'queue/bounce' : ('qmails', 'qmail', 0700, 0),
'queue/info' : ('qmails', 'qmail', 0700, 1),
'queue/intd' : ('qmailq', 'qmail', 0700, -1),
'queue/local' : ('qmails', 'qmail', 0700, 1),
'queue/lock' : ('qmailq', 'qmail', 0750, 0),
'queue/mess' : ('qmailq', 'qmail', 0750, 1),
'queue/pid' : ('qmailq', 'qmail', 0700, 0),
'queue/remote' : ('qmails', 'qmail', 0700, 1),
'queue/todo' : ('qmailq', 'qmail', 0750, -1),
}
nondirs = {
# Files to check; format is:
# key: pathname - all paths are relative to conf-qmail
# data: (user, group, mode)
'queue/lock/sendmutex' : ('qmails', 'qmail', 0600),
'queue/lock/tcpto' : ('qmailr', 'qmail', 0644),
}
#######################################
# functions
#######################################
#######################################
def primes(min, max):
'''primes(min, max)
Return a list of primes between min and max inclusive.
'''
result = []
primelist = [2]
if min <= 2:
result.append(2)
i = 3
while i <= max:
for p in primelist:
if (i % p == 0) or (p * p > i): break
if (i % p <> 0):
primelist.append(i)
if i >= min:
result.append(i)
i = i + 2
return result
#######################################
def err(s, showhelp=0):
'''err(s, showhelp=0)
Write s + '\n' to stderr, optionally call show_help(), and exit.
'''
sys.stderr.write('%s\n' % s)
if showhelp:
show_help()
if wd:
os.chdir(wd)
sys.exit(1)
#######################################
def msg(s):
'''msg(s)
Write s + '\n' to stdout.
'''
sys.stdout.write('%s\n' % s)
#######################################
def is_splitdir(is_split, bigtodo):
'''is_splitdir(is_split, bigtodo)
Return 0 if directory should not contain split subdirectories, 1 if it
should.
'''
return (is_split == 1) or (is_split == -1 and bigtodo)
#######################################
def determine_users():
'''determine_users()
Look up UIDs and GIDs for all keys in globals users and groups which are
not already set.
'''
global users, groups
msg('finding qmail UIDs/GIDs...')
us = users.keys()
gs = groups.keys()
for u in us:
if users[u]:
# Handle case of someone else determining UIDs for us
msg(' %-7s preset as UID %i' % (u, users[u]))
continue
try:
users[u] = pwd.getpwnam(u)[2]
except KeyError:
err('no uid for %s' % u)
msg(' %-7s : UID %i' % (u, users[u]))
for g in gs:
if groups[g]:
# Handle case of someone else determining GIDs for us
msg(' %-7s preset as GID %i' % (g, groups[g]))
continue
try:
groups[g] = grp.getgrnam(g)[2]
except KeyError:
err('no gid for %s' % g)
msg(' %-7s : GID %i' % (g, groups[g]))
#######################################
def check_dir(path, user, group, mode):
'''check_dir(path, user, group, mode)
Verify path is an existing directory, that it owned by user:group, and
that it has octal mode mode. If testmode is set, create path if it
doesn't exist.
'''
if checked_dir.has_key(path):
return
msg(' checking directory %s...' % path)
if not os.path.exists(path):
msg(' directory %s does not exist' % path)
if not testmode:
os.makedirs(path, mode)
else:
if os.path.islink(path):
msg(' %s is a symlink instead of directory' % path)
if not testmode:
os.unlink(path)
if not os.path.isdir(path):
msg(' %s is not a directory' % path)
if not testmode:
os.unlink(path)
chown(path, user, group)
chmod(path, mode)
checked_dir[path] = None
#######################################
def chown(path, user, group):
'''chown(path, user, group)
Verify path is owned by user:group, and make it so if testmode is not set.
'''
if checked_owner.has_key(path):
return
uid = users[user]
gid = groups[group]
try:
s = os.stat(path)
if s[ST_UID] != uid or s[ST_GID] != gid:
msg(' %s ownership %i:%i, should be %s:%s' % (path,
s[ST_UID], s[ST_GID], user, group))
if not testmode:
os.chown(path, uid, gid)
s = os.stat(path)
msg(' fixed, %s ownership %i:%i' % (path, s[ST_UID], s[ST_GID]))
else:
msg(' testmode, not fixing')
except OSError, o:
err(o or '[no error message]')
checked_owner[path] = None
#######################################
def chmod(path, mode):
'''chmod(path, mode)
Verify path has mode mode, and make it so if testmode is not set.
'''
if checked_mode.has_key(path):
return
try:
s = os.stat(path)
curmode = S_IMODE(s[ST_MODE])
if curmode != mode:
msg(' %s is mode %o, should be %o' % (path, curmode, mode))
if not testmode:
os.chmod(path, mode)
s = os.stat(path)
newmode = S_IMODE(s[ST_MODE])
msg(' changed %s mode to %o' % (path, newmode))
else:
msg(' testmode, not fixing')
except OSError, o:
err(o or '[no error message]')
checked_mode[path] = None
#######################################
def determine_split():
'''determine_split()
Return probable conf-split value of queue based on contents.
'''
splits = []
msg('determining conf-split...')
for (path, (user, group, mode, is_split)) in dirs.items():
if is_split != 1:
continue
highest = 0
contents = os.listdir(path)
for item in contents:
p = os.path.join(path, item)
if os.path.islink(p):
msg(' found unexpected symlink %s' % p)
continue
if not os.path.isdir(p):
msg(' found unexpected non-directory %s' % p)
continue
try:
i = int(item)
except ValueError:
msg(' found unexpected directory %s' % p)
continue
if i > highest:
highest = i
splits.append(highest)
split = splits[0]
for i in splits[1:]:
if i != split:
err(' not all subdirectories split the same; use --split N to force')
# First split directory is '0'
split = split + 1
msg(' conf-split appears to be %i' % split)
return split
#######################################
def determine_bigtodo(split):
'''determine_bigtodo(split)
Return 1 if big-todo appears to be in use based on contents of queue,
0 otherwise.
'''
splits = []
bigtodo = 0
msg('determining big-todo...')
for i in range(split):
p = os.path.join('queue/todo', str(i))
if os.path.islink(p):
msg(' found unexpected symlink %s' % p)
elif os.path.isdir(p):
splits.append(i)
elif not os.path.exists(p):
# big-todo probably not in use
pass
else:
msg(' found unexpected direntry %s' % p)
if splits == range(split):
# big-todo apparently in use
bigtodo = 1
msg(' big-todo found')
elif splits:
# big-todo in use, but doesn't match split
err(' todo split != split; if using --split N, use --bigtodo to force')
else:
msg(' big-todo not found')
return bigtodo
#######################################
def check_dirs(paths, split, bigtodo):
'''check_dirs(paths, split, bigtodo)
Verify ownership, mode, and contents of each queue directory in paths.
'''
msg('checking main queue directories...')
_dirs = paths.keys()
_dirs.sort()
for path in _dirs:
(user, group, mode, is_split) = paths[path]
check_dir(path, user, group, mode)
msg('checking split sub-directories...')
for (path, (user, group, mode, is_split)) in paths.items():
if path in ('queue', 'queue/lock'):
# Nothing in these directories to check at this point
continue
this_split = is_splitdir(is_split, bigtodo)
if not this_split:
splits = []
else:
splits = range(split)
for i in splits:
splitpath = os.path.join(path, str(i))
check_dir(splitpath, user, group, mode)
try:
contents = os.listdir(path)
except OSError:
# Directory missing
if testmode:
continue
err('bug -- directory %s missing, should exist by now' % path)
for item in contents:
p = os.path.join(path, item)
if this_split:
if (is_split == -1) and os.path.isfile(p):
# Found possible file in path while converting queue to
# big-todo
try:
i = int(item)
if not testmode:
# Move to '0' split subdirectory; will be
# fixed later by check_hash_and_ownership
new_p = os.path.join(path, '0', item)
msg(' moving %s to %s' % (p, new_p))
os.rename(p, new_p)
except ValueError:
# Not a message file
msg(' found unexpected file %s' % p)
continue
# This directory should contain only split subdirectories
if not os.path.isdir(p):
msg(' found unexpected direntry %s' % p)
continue
try:
i = int(item)
if i not in splits:
msg(' found unexpected split subdirectory %s' % p)
if not testmode:
files = os.listdir(p)
for f in files:
# Move any files in this to-be-remove split subdir
# into the 0 splitdir. Will be moved into the
# proper split subdir later by
# check_hash_and_ownership().
filep = os.path.join(p, f)
msg(' preserving file %s' % filep)
os.rename(filep, os.path.join(path, '0', f))
os.removedirs(p)
except ValueError:
msg(' found unexpected direntry %s' % p)
else:
# This directory should contain only files
if os.path.isdir(p):
msg(' found unexpected directory %s' % p)
try:
i = int(item)
except ValueError:
msg(' %s not a split subdirectory; ignoring' % p)
continue
msg(' %s is a split subdirectory; %s should not be split' % (p, path))
if not testmode:
savefiles = os.listdir(p)
if savefiles:
msg(' moving files from %s to %s' % (p, path))
for f in savefiles:
os.rename(os.path.join(p, f), os.path.join(path, f))
os.rmdir(p)
elif not os.path.isfile(p):
msg(' found unexpected direntry %s; ignoring' % p)
continue
else:
# Found file
pass
#######################################
def check_files(paths):
'''check_files(paths)
Verify ownership and mode of each queue file in paths.
'''
msg('checking files...')
for (path, (user, group, mode)) in paths.items():
if os.path.exists(path):
if not os.path.isfile(path):
msg(' %s is not a file' % path)
if not testmode:
os.unlink(path)
else:
msg(' file %s does not exist' % path)
if not os.path.exists(path) and not testmode:
open(path, 'w')
chown(path, user, group)
chmod(path, mode)
#######################################
def check_trigger():
'''check_trigger()
Verify ownership, mode, and inode type of trigger fifo.
'''
path = 'queue/lock/trigger'
user = 'qmails'
group = 'qmail'
if not os.path.exists(path):
msg(' %s missing' % path)
else:
if os.path.islink(path):
msg(' %s is a symlink instead of fifo' % path)
if not testmode:
os.unlink(path)
else:
mode = os.stat(path)[ST_MODE]
if not S_ISFIFO(mode):
msg(' %s not a fifo' % path)
if not testmode:
os.unlink(path)
if not os.path.exists(path) and not testmode:
os.mkfifo(path)
chown(path, user, group)
chmod(path, 0622)
#######################################
def check_messages(path, split):
'''check_messages(path, split)
Return list of files found under path which are not named after their
inode number.
'''
misnamed = []
msg('checking queue/mess files...')
for i in range(split):
messdir = os.path.join(path, str(i))
try:
contents = os.listdir(messdir)
except OSError:
continue
for f in contents:
p = os.path.join(messdir, f)
if os.path.islink(p):
msg(' found unexpected symlink %s' % p)
continue
elif not os.path.isfile(p):
msg(' found unexpected non-file %s' % p)
continue
try:
filenum = int(f)
except ValueError:
msg(' found unexpected file %s' % p)
continue
s = os.stat(p)
inode = s[ST_INO]
if filenum == inode:
continue
# Found mess file not named after inode
msg(' %s is inode %i' % (p, inode))
# Will be fixed by fix_inode_names()
misnamed.append((i, filenum, inode))
return misnamed
#######################################
def fix_inode_names(paths, split, bigtodo, misnamed):
'''fix_inode_names(paths, split, bigtodo, misnamed)
For each path in paths, correct file names based on results of
check_messages(). Correct split sub-directory location as well.
'''
msg('fixing misnamed messages...')
for (path, (user, junk, junk, is_split)) in paths.items():
for (oldhash, oldno, newno) in misnamed:
if not is_splitdir(is_split, bigtodo):
old_p = os.path.join(path, str(oldno))
new_p = os.path.join(path, str(newno))
else:
old_p = os.path.join(path, str(oldhash), str(oldno))
new_p = os.path.join(path, str(newno % split), str(newno))
if os.path.exists(old_p):
if os.path.islink(old_p):
msg(' found unexpected symlink %s' % old_p)
continue
if not os.path.isfile(old_p):
msg(' found unexpected direntry %s' % old_p)
continue
msg(' %s should be %s' % (old_p, new_p))
if not testmode:
os.rename(old_p, new_p)
msg(' fixed')
#######################################
def check_hash_and_ownership(paths, split, bigtodo):
'''check_hash_and_ownership(paths, split, bigtodo)
For each path in paths, correct file ownership, mode, and split subdirectory
of all files found.
'''
msg('checking split locations...')
for (path, (user, group, junk, is_split)) in paths.items():
if path in ('queue', 'queue/lock'):
# Nothing in these directories to check at this point
continue
elif path in ('queue/mess', 'queue/todo'):
mode = 0644
else:
mode = 0600
this_split = is_splitdir(is_split, bigtodo)
if this_split:
splits = range(split)
else:
splits = ['']
for splitval in splits:
_dir = os.path.join(path, str(splitval))
try:
contents = os.listdir(_dir)
except OSError:
if not testmode:
err('bug -- directory %s missing, should exist by now' % _dir)
continue
for f in contents:
old_p = os.path.join(_dir, f)
try:
if not os.path.isfile(old_p):
raise ValueError
j = int(f)
except ValueError:
msg(' found unexpected direntry %s; ignoring' % old_p)
continue
# Check ownership and mode
chown(old_p, user, group)
chmod(old_p, mode)
if not this_split:
continue
# Check whether file is in correct split sub-directory
hashv = j % split
if hashv != splitval:
# message in wrong split dir
new_p = os.path.join(path, str(hashv), f)
msg(' %s should be %s' % (old_p, new_p))
if not testmode:
os.rename(old_p, new_p)
# Ensure ownership and mode
chown(new_p, user, group)
chmod(new_p, mode)
msg(' fixed')
#######################################
def get_current_messages(split):
'''get_current_messages(split)
Return list of all message files under queue/mess.
'''
messages = []
msg('finding current messages...')
for i in range(split):
path = os.path.join('queue/mess', str(i))
try:
contents = os.listdir(path)
except OSError:
continue
for item in contents:
try:
messages.append(int(item))
except ValueError:
if testmode:
pass
else:
msg(' found unexpected direntry %s' % os.path.join(path, item))
messages.sort()
msg(' found %i messages' % len(messages))
return messages
#######################################
def check_queue(qmaildir=confqmail, test=1, force_split=None, force_bigtodo=None, force_create=0, mathishard=0):
'''check_queue(qmaildir=confqmail, test=1, force_split=None, force_bigtodo=None, force_create=0, mathishard=0)
Verify (and correct if test is not set) queue structure rooted at
qmaildir/queue.
Determine conf-split automatically if force_split is not set.
Determine if big-todo is in use automatically if force_bigtodo is not set.
'''
global wd
global testmode
testmode = test
split = None
wd = os.getcwd()
try:
os.chdir(qmaildir)
except StandardError:
err('failed to chdir to %s' % qmaildir)
if testmode:
msg('running in test-only mode')
else:
msg('running in repair mode')
determine_users()
if not force_split:
try:
split = determine_split()
except OSError:
msg('basic queue directories not found at %s' % qmaildir)
if not split:
if not force_create:
err(' use --create to force creation of queue at %s' % qmaildir)
# --create implies --repair
testmode = 0
if not force_split:
err('if creating a new queue, you must supply a conf-split value with --split')
split = int(force_split)
if split < 1:
err('split must be >= 1')
if not force_bigtodo:
err('if creating a new queue, you must supply either --bigtodo or --no-bigtodo')
msg('using forced conf-split of %i' % split)
msg('creating new queue at %s' % qmaildir)
l = int(split * 0.8)
h = int(split * 1.2)
suggested_splits = primes(l, h)
if not split in suggested_splits:
msg('split should be prime, not %i: suggestions %s' % (split, suggested_splits))
if not mathishard and not testmode:
err(' use --i-want-a-broken-conf-split to force non-prime split')
if force_bigtodo == 1:
bigtodo = 1
msg('using forced big-todo')
elif force_bigtodo == -1:
bigtodo = 0
msg('using forced non-big-todo')
else:
bigtodo = determine_bigtodo(split)
check_dirs(dirs, split, bigtodo)
check_files(nondirs)
check_trigger()
misnamed = check_messages('queue/mess', split)
# Handle misnamed files in directories
if misnamed:
fix_inode_names(dirs, split, bigtodo, misnamed)
# Handle mis-hashed files and bad owner/group/mode
check_hash_and_ownership(dirs, split, bigtodo)
#######################################
def show_help():
'''show_help()
Display usage information.
'''
msg('\n'
'Usage: queue_repair.py [options] [conf-qmail]\n')
msg('Options:\n'
' conf-qmail (default: %s)' % confqmail)
msg(
' -t or --test Test only; do not modify the filesystem\n'
' -r or --repair Repair errors found (default: test)\n'
' -b or --bigtodo Force use of big-todo (default: auto)\n'
' -n or --no-bigtodo Force non-use of big-todo (default: auto)\n'
' -s N or --split N Force conf-split of N (default: auto)\n'
' -c or --create Force creation of queue (default: no)\n'
' --i-want-a-broken-conf-split Force non-prime conf-split (default: no)\n'
' -h or --help This text\n'
)
#######################################
def main():
'''main()
Parse options and call check_queue().
'''
msg('queue_repair.py v. %s\n'
'Copyright (C) 2001 %s' % (__version__, __author__))
msg('Licensed under the GNU General Public License version 2\n')
optionlist = 's:bnrthc'
longoptionlist = ('split=', 'bigtodo', 'no-bigtodo', 'repair', 'test',
'i-want-a-broken-conf-split', 'help', 'create')
force_split = None
force_bigtodo = None
test = 1
qmaildir = confqmail
mathishard = 0
create = 0
try:
options, args = getopt.getopt(sys.argv[1:], optionlist, longoptionlist)
for (option, value) in options:
if option in ('-s', '--split'):
try:
force_split = int(value)
if force_split < 1:
raise ValueError
except ValueError:
raise getopt.error, 'split value must be a positive integer (%s)' % value
elif option in ('-n', '--no-bigtodo'):
force_bigtodo = -1
elif option in ('-b', '--bigtodo'):
force_bigtodo = 1
elif option in ('-r', '--repair'):
test = 0
elif option in ('-t', '--test'):
test = 1
elif option == '--i-want-a-broken-conf-split':
mathishard = 1
elif option in ('-h', '--help'):
show_help()
sys.exit(0)
elif option in ('-c', '--create'):
create = 1
if args:
if len(args) > 1:
raise getopt.error, 'conf-qmail must be a single argument (%s)' % string.join(args)
qmaildir = args[0]
except getopt.error, o:
err('Error: %s' % o, showhelp=1)
check_queue(qmaildir, test, force_split, force_bigtodo, create, mathishard)
#######################################
if __name__ == '__main__':
main()
syntax highlighted by Code2HTML, v. 0.9.1