- On utilise la base LDAP

- Documentation
 - Generation des mails a envoyer aux adherents (phase de test)

darcs-hash:20060520161003-68412-858bb7d320a22e72a88d9fe1cd3de9ca0f367586.gz
This commit is contained in:
glondu 2006-05-20 18:10:03 +02:00
parent 5499472a9c
commit 17771d5e4b

View file

@ -1,10 +1,16 @@
#! /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.
"""
"""DESCRIPTION
Ce script repère les comptes inactifs en parsant les logs de dernière
connexion de sshd et dovecot, et en lisant et mettant à jour les champs
derniereConnexion dans la base LDAP.
UTILISATION
%(prog)s action
ACTIONS POSSIBLES
%(acts)s"""
# Copyright (C) 2006 Stéphane Glondu
# Licence : GPLv2
@ -16,17 +22,25 @@ from socket import gethostname
from smtplib import SMTP
host = gethostname()
mail_address = u'disconnect@crans.org'
debug = u'disconnect@crans.org'
mail_report = u'disconnect@crans.org'
mail_sender = u"Comptes inactifs <disconnect@crans.org>"
template_path = '/usr/scripts/templates/comptes_inactifs.%d'
template_path = '/usr/scripts/templates/comptes_inactifs.%d.txt'
actions = ('log', 'dump', 'summary', 'spam')
# Date de début d'analyse des logs : 19/03/2006 05:27 GMT
oldest_log = 1142746043
sys.path.append('/usr/scripts/gestion')
from affich_tools import tableau
from affich_tools import tableau, cprint
from email_tools import send_email, parse_mail_template
from ldap_crans import crans_ldap
from config import ann_scol
db = crans_ldap()
from syslog import *
openlog('comptes_inactifs')
def nb_mails_non_lus(login):
"""
@ -40,110 +54,117 @@ def nb_mails_non_lus(login):
else:
return 0
except:
# Arrive quand le script n'a pas les bons droits pour lire /var/mail
# 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')([^ >]+).*$')
# liste d'expressions régulières qui seront testées sur les lignes de log
# le premier groupe doit correspondre à la date, le second au login
re = [sre.compile(r'^(\w+\s+\d+\s+\d+:\d+:\d+).*(?:'
r'dovecot.*Login: user=<|'
r'sshd.*Accepted.*for '
r')([^ >]+).*$'),
sre.compile(r'^.*comptes_inactifs.*derniereConnexion=<([^>]+)>, '
r'login=<([^>]+)>')]
def __init__(self, filename):
''' Lecture du dico et acquisition du lock '''
self.filename = filename
def __init__(self):
""" Initialisation """
self.dic = {}
# 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()
def search(self, query, mode=''):
""" CransLdap.search allégé """
query = '(&(objectClass=cransAccount)(objectClass=adherent)(%s))' % query
result = db.conn.search_s(db.base_dn, db.scope['adherent'], query)
result = [db.make(x, mode) for x in result]
return result
# À 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 commit_to_ldap(self):
"""
Sauvegarde du dico dans la base LDAP.
Renvoie le nombre d'entrées mises à jour.
"""
total = 0
for (login, timestamp) in self.dic.items():
a = self.search('uid=%s' % login, 'w')
if not a:
# Probablement un adhérent récemment parti
continue
a = a[0]
if a._modifiable == 'w':
a.derniereConnexion(timestamp)
if a.modifs: total += 1
a.save()
else:
# on loggue on espérant que les logs seront réinjectés
# plus tard
syslog("LDAP(lock): derniereConnexion=<%s>, login=<%s>" %
(strftime("%b %d %H:%M:%S", localtime(timestamp)), login))
return total
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.
Met à jour l'entrée correspondant au login donné.
"""
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 """
"""
Met à jour le dico avec les lignes de syslog données.
Renvoie le nombre de lignes traitées.
"""
annee = localtime(time())[0]
now = time() + 600
nombre = 0
for line in loglines:
m = self.re.match(line)
for r in self.re:
m = r.match(line)
if m: break
if not m: continue
date = list(strptime(m.group(1), "%b %d %H:%M:%S"))
date[0] = annee
t = mktime(date)
# les lignes de syslog n'indiquent pas l'année
# on suppose qu'une date dans le futur est en fait l'année dernière
if t > now:
date[0] = annee - 1
t = mktime(date)
self.update(m.group(2), t)
self.update(m.group(2).lower(), t)
nombre += 1
print '%d ligne(s) pertinente(s)' % nombre
return 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'
"""
Lit des lignes de log sur l'entrée std et met à jour la base LDAP.
"""
print '%s ligne(s) traitée(s)' % self.update_from_syslog(sys.stdin)
print '%s entrée(s) mise(s) à jour dans la base LDAP' % self.commit_to_ldap()
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))
"""
Affiche la liste des dernières connexions, triées par date.
"""
liste = self.search('derniereConnexion=*')
liste = [(x.derniereConnexion(), x.compte()) for x in liste]
liste.sort()
liste = [(x[1], strftime('%d/%m/%Y %H:%M', localtime(x[0])))
for x in liste]
cprint(tableau(liste,
titre = (u'Login', u'Dernière connexion'),
largeur = (20, 20)))
cprint(u"Total : %d" % len(liste))
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).
Renvoie la liste des objets Adherent de ceux qui ne se sont pas
connectés depuis since secondes, par défaut un mois (32 jours,
pour être 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
return self.search('|(!(derniereConnexion=*))(derniereConnexion<=%d)' % limit)
def do_summary(self):
"""
@ -172,16 +193,21 @@ comptes_inactifs.py
inscrits = []
anciens = []
for (x, a) in self.get_idle_accounts():
date = self.dic.get(x)
liste = self.get_idle_accounts()
# on trie par login
liste.sort(lambda x, y: cmp(x.compte(), y.compte()))
for a in liste:
login = a.compte()
date = a.derniereConnexion()
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)
forward = os.path.isfile('/home/%s/.forward' % login) and u'X' or u''
mail = nb_mails_non_lus(login)
mail = mail == None and u'?' or mail > 0 and u'X' or u' '
ligne = (a.id(), x, a.Nom(), date, forward, mail)
ligne = (a.id(), login, a.Nom(), date, forward, mail)
if ann_scol in a.paiement():
inscrits.append(ligne)
else:
@ -195,16 +221,19 @@ comptes_inactifs.py
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())))
oldest = strftime(u'%d/%m/%Y %H:%M', localtime(oldest_log))
send_email(mail_sender,
mail_address,
mail_report,
u'Comptes inactifs',
modele % locals())
modele % locals(),
debug = debug)
def do_spam(self):
""" Envoie un mail explicatif aux possesseurs de compte inactif """
"""
Envoie un mail explicatif aux possesseurs de compte inactif
(doit être exécuté en tant que root).
"""
# Nombre de personnes concernées, en expansant de droite à gauche :
# inscrit/ancien, avec/sans procmail, avec/sans mail non lu
# Voir aussi template_path
@ -214,24 +243,90 @@ comptes_inactifs.py
smtp = SMTP()
smtp.connect()
for (x, a) in self.get_idle_accounts():
pass
for a in self.get_idle_accounts():
# initialisation des champs
login = a.compte()
mail = nb_mails_non_lus(login)
nom = a.Nom()
date = a.derniereConnexion() or oldest_log
date = strftime(u'%d/%m/%Y %H:%M', localtime(date))
i = 0
# est-ce un membre inscrit ?
if ann_scol not in a.paiement(): i += 4
# a-t-il un .forward ?
if not os.path.isfile('/home/%s/.forward' % login): i += 2
# a-il-des mails non lus ?
if not mail: i += 1
# on incrémente
stats[i] += 1
if i == 1:
# on laisse tranquilles les membres inscrits sans mails non
# lus qui ont un .forward
continue
(sujet, corps) = parse_mail_template(template_path % i)
corps = corps % locals()
if debug:
sujet = u"[Message de test %d] %s" % (i, sujet)
if stats[i] > 1: continue
send_email(mail_sender,
u"%s <%s@crans.org>" % (nom, login),
sujet,
corps,
server = smtp,
debug = debug)
recapitulatif = []
total = 0
for i in range(0, 8):
total += stats[i]
recapitulatif.append((((i & 4) and 'n' or 'o'),
((i & 2) and 'n' or 'o'),
((i & 1) and 'n' or 'o'),
stats[i]))
recapitulatif = tableau(recapitulatif,
titre = (u"Inscrits", u"Procmail", u"Mails", u"Nombre"),
largeur = (10, 10, 7, 8),
alignement = ('c', 'c', 'c', 'd'))
recapitulatif += u"""
Total : %d
--
comptes_inactifs.py
""" % total
send_email(mail_sender,
mail_report,
u"Récapitulatif des comptes inactifs",
recapitulatif,
server = smtp,
debug = debug)
smtp.quit()
def usage():
""" Afficher l'aide. """
prog = sys.argv[0]
acts = [x + '\n' + '\n'.join(eval("ComptesInactifs.do_%s.__doc__" % x).split('\n')[1:-1])
for x in actions]
acts = '\n'.join(acts)
print __doc__ % locals()
if __name__ == '__main__':
args = sys.argv[1:]
if len(args) != 1:
sys.stderr.write("Arguments incorrects\n")
usage()
sys.exit(1)
commande = args[0]
if commande not in ('log', 'dump', 'summary'):
if commande not in actions:
sys.stderr.write("Commande incorrecte : %s\n" % commande)
sys.exit(1)
usage()
sys.exit(2)
ci_db = ComptesInactifs('/usr/scripts/var/comptes_inactifs.dict')
eval('ci_db.do_%s()' % commande)
ci_db.close()
ci = ComptesInactifs()
eval('ci.do_%s()' % commande)