scripts/surveillance/comptes_inactifs.py
glondu 018f2d59c4 Factorisation. Preparation de l'envoi des mails.
darcs-hash:20060507164909-68412-59836281afda0c54b73d318d9318e071a0b52246.gz
2006-05-07 18:49:09 +02:00

237 lines
7.4 KiB
Python
Executable file

#! /usr/bin/env python
# -*- coding: iso-8859-15 -*-
"""
Repère les comptes inactifs en parsant les logs de dernière connexion
de sshd et dovecot.
"""
# Copyright (C) 2006 Stéphane Glondu
# Licence : GPLv2
import sys, os, sre, time, cPickle
from time import mktime, time, localtime, strptime, strftime
from socket import gethostname
from smtplib import SMTP
host = gethostname()
mail_address = u'disconnect@crans.org'
mail_sender = u"Comptes inactifs <disconnect@crans.org>"
template_path = '/usr/scripts/templates/comptes_inactifs.%d'
sys.path.append('/usr/scripts/gestion')
from affich_tools import tableau
from email_tools import send_email, parse_mail_template
from ldap_crans import crans_ldap
from config import ann_scol
db = crans_ldap()
def nb_mails_non_lus(login):
"""
Renvoie le nombre de mails non lus de login, ou None si impossible à
déterminer.
"""
try:
maildir = '/var/mail/%s/new' % login
if os.path.isdir(maildir):
return len(os.listdir(maildir))
else:
return 0
except:
# Arrive quand le script n'a pas les bons droits pour lire /var/mail
return None
class ComptesInactifs:
re = sre.compile(r'^(\w+\s+\d+\s+\d+:\d+:\d+).*(?:'
r'dovecot.*Login: user=<|'
r'sshd.*Accepted.*for '
r')([^ >]+).*$')
def __init__(self, filename):
''' Lecture du dico et acquisition du lock '''
self.filename = filename
# Systeme de lock sommaire
self.lockfile = filename + '.lock'
if os.path.isfile(self.lockfile):
f = file(self.lockfile)
(lhost, pid) = f.read().strip().split()
f.close()
if lhost != host or os.system('ps %s >/dev/null 2>&1') == 0:
# Le lock est actif ou sur une autre machine
raise RuntimeError('Lock actif')
f = file(self.lockfile, 'w')
f.write('%s %d\n' % (host, os.getpid()))
f.close()
# À partir de là, on a le lock
if os.path.isfile(filename):
self.dic = cPickle.load(file(filename))
else:
self.dic = {}
def close(self):
''' Sauvegarde du dico et suppression du lock '''
cPickle.dump(self.dic, file(self.filename, 'w'))
os.remove(self.lockfile)
def update(self, login, timestamp):
"""
Met à jour l'entrée correspondant au login donné, ainsi que
l'entrée '!', qui correspond à la plus vieille entrée.
"""
dic = self.dic
timestamp = int(timestamp)
# Mise à jour de l'entrée la plus vieille
if not dic.has_key('!') or timestamp < dic['!']:
dic['!'] = timestamp
# Mise à jour de l'entrée correspondant au login
if not dic.has_key(login) or timestamp > dic[login]:
dic[login] = timestamp
def update_from_syslog(self, loglines):
""" Met à jour le dico avec les lignes de syslog données """
annee = localtime(time())[0]
now = time() + 600
nombre = 0
for line in loglines:
m = self.re.match(line)
if not m: continue
date = list(strptime(m.group(1), "%b %d %H:%M:%S"))
date[0] = annee
t = mktime(date)
if t > now:
date[0] = annee - 1
t = mktime(date)
self.update(m.group(2), t)
nombre += 1
print '%d ligne(s) pertinente(s)' % nombre
def do_log(self):
""" Lit des lignes de log sur l'entrée std et met à jour le dico """
self.update_from_syslog(sys.stdin)
print 'Lecture des logs terminée'
def do_dump(self):
""" Affiche le contenu du dico """
liste = self.dic.items()
liste.sort(lambda x, y: cmp(x[1], y[1]))
data = [(x[0], strftime('%d/%m/%Y %H:%M', localtime(x[1])))
for x in liste]
print tableau(data,
largeur = (20, 18))
def get_idle_accounts(self, since=32*24*3600):
"""
Renvoie la liste des couples (login, objet Adherent) de ceux qui
ne se sont pas connectés depuis since secondes, par défaut un mois
(32 jours, pour etre sûr).
"""
oldest = self.dic.get('!', int(time()))
limit = int(time()) - since
liste = []
for x in os.listdir('/home'):
if os.path.isdir('/var/mail/' + x) and self.dic.get(x, oldest) < limit:
a = db.search('uid=%s' % x)['adherent']
if a:
liste.append((x, a[0]))
else:
print 'uid=%s introuvable' % x
liste.sort()
return liste
def do_summary(self):
"""
Envoie à disconnect un résume des comptes inactifs depuis plus d'un
mois.
"""
modele = u"""*Membres inscrits ne s'étant pas connectés depuis plus d'un mois*
%(inscrits)s
Total : %(inscrits_total)d
*Anciens membres ne s'étant pas connectés depuis plus d'un mois*
%(anciens)s
Total : %(anciens_total)d
Légende :
- F : existence d'un .forward
- M : existence de mails non lus
L'analyse des logs remonte au %(oldest)s.
--
comptes_inactifs.py
"""
inscrits = []
anciens = []
for (x, a) in self.get_idle_accounts():
date = self.dic.get(x)
if date:
date = strftime(u'%d/%m/%Y %H:%M', localtime(date))
else:
date = u'Jamais'
forward = os.path.isfile('/home/%s/.forward' % x) and u'X' or u''
mail = nb_mails_non_lus(x)
mail = mail == None and u'?' or mail > 0 and u'X' or u' '
ligne = (a.id(), x, a.Nom(), date, forward, mail)
if ann_scol in a.paiement():
inscrits.append(ligne)
else:
anciens.append(ligne)
titres = (u'aid', u'Login', u'Nom', u'Dernière connexion', u'F', u'M')
largeurs = (6, 15, 20, 20, 1, 1)
alignements = ('d', 'g', 'c', 'c', 'c', 'c')
inscrits_total = len(inscrits)
inscrits = tableau(inscrits, titres, largeurs, alignements)
anciens_total = len(anciens)
anciens = tableau(anciens, titres, largeurs, alignements)
oldest = strftime(u'%d/%m/%Y %H:%M',
localtime(self.dic.get('!', time())))
send_email(mail_sender,
mail_address,
u'Comptes inactifs',
modele % locals())
def do_spam(self):
""" Envoie un mail explicatif aux possesseurs de compte inactif """
# Nombre de personnes concernées, en expansant de droite à gauche :
# inscrit/ancien, avec/sans procmail, avec/sans mail non lu
# Voir aussi template_path
stats = [0, 0, 0, 0, 0, 0, 0, 0]
# On factorise la connexion
smtp = SMTP()
smtp.connect()
for (x, a) in self.get_idle_accounts():
pass
smtp.quit()
if __name__ == '__main__':
args = sys.argv[1:]
if len(args) != 1:
sys.stderr.write("Arguments incorrects\n")
sys.exit(1)
commande = args[0]
if commande not in ('log', 'dump', 'summary'):
sys.stderr.write("Commande incorrecte : %s\n" % commande)
sys.exit(1)
ci_db = ComptesInactifs('/usr/scripts/var/comptes_inactifs.dict')
eval('ci_db.do_%s()' % commande)
ci_db.close()