- 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 #! /usr/bin/env python
# -*- coding: iso-8859-15 -*- # -*- coding: iso-8859-15 -*-
""" """DESCRIPTION
Repère les comptes inactifs en parsant les logs de dernière connexion Ce script repère les comptes inactifs en parsant les logs de dernière
de sshd et dovecot. 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 # Copyright (C) 2006 Stéphane Glondu
# Licence : GPLv2 # Licence : GPLv2
@ -16,17 +22,25 @@ from socket import gethostname
from smtplib import SMTP from smtplib import SMTP
host = gethostname() 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>" 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') 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 email_tools import send_email, parse_mail_template
from ldap_crans import crans_ldap from ldap_crans import crans_ldap
from config import ann_scol from config import ann_scol
db = crans_ldap() db = crans_ldap()
from syslog import *
openlog('comptes_inactifs')
def nb_mails_non_lus(login): def nb_mails_non_lus(login):
""" """
@ -40,110 +54,117 @@ def nb_mails_non_lus(login):
else: else:
return 0 return 0
except: 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 return None
class ComptesInactifs: class ComptesInactifs:
re = sre.compile(r'^(\w+\s+\d+\s+\d+:\d+:\d+).*(?:' # liste d'expressions régulières qui seront testées sur les lignes de log
r'dovecot.*Login: user=<|' # le premier groupe doit correspondre à la date, le second au login
r'sshd.*Accepted.*for ' re = [sre.compile(r'^(\w+\s+\d+\s+\d+:\d+:\d+).*(?:'
r')([^ >]+).*$') r'dovecot.*Login: user=<|'
r'sshd.*Accepted.*for '
r')([^ >]+).*$'),
sre.compile(r'^.*comptes_inactifs.*derniereConnexion=<([^>]+)>, '
r'login=<([^>]+)>')]
def __init__(self, filename): def __init__(self):
''' Lecture du dico et acquisition du lock ''' """ Initialisation """
self.filename = filename self.dic = {}
# Systeme de lock sommaire def search(self, query, mode=''):
self.lockfile = filename + '.lock' """ CransLdap.search allégé """
if os.path.isfile(self.lockfile): query = '(&(objectClass=cransAccount)(objectClass=adherent)(%s))' % query
f = file(self.lockfile) result = db.conn.search_s(db.base_dn, db.scope['adherent'], query)
(lhost, pid) = f.read().strip().split() result = [db.make(x, mode) for x in result]
f.close() return result
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 def commit_to_ldap(self):
if os.path.isfile(filename): """
self.dic = cPickle.load(file(filename)) Sauvegarde du dico dans la base LDAP.
else: Renvoie le nombre d'entrées mises à jour.
self.dic = {} """
total = 0
def close(self): for (login, timestamp) in self.dic.items():
''' Sauvegarde du dico et suppression du lock ''' a = self.search('uid=%s' % login, 'w')
cPickle.dump(self.dic, file(self.filename, 'w')) if not a:
os.remove(self.lockfile) # 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): def update(self, login, timestamp):
""" """
Met à jour l'entrée correspondant au login donné, ainsi que Met à jour l'entrée correspondant au login donné.
l'entrée '!', qui correspond à la plus vieille entrée.
""" """
dic = self.dic dic = self.dic
timestamp = int(timestamp) 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]: if not dic.has_key(login) or timestamp > dic[login]:
dic[login] = timestamp dic[login] = timestamp
def update_from_syslog(self, loglines): 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] annee = localtime(time())[0]
now = time() + 600 now = time() + 600
nombre = 0 nombre = 0
for line in loglines: 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 if not m: continue
date = list(strptime(m.group(1), "%b %d %H:%M:%S")) date = list(strptime(m.group(1), "%b %d %H:%M:%S"))
date[0] = annee date[0] = annee
t = mktime(date) 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: if t > now:
date[0] = annee - 1 date[0] = annee - 1
t = mktime(date) t = mktime(date)
self.update(m.group(2), t) self.update(m.group(2).lower(), t)
nombre += 1 nombre += 1
print '%d ligne(s) pertinente(s)' % nombre return nombre
def do_log(self): 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) Lit des lignes de log sur l'entrée std et met à jour la base LDAP.
print 'Lecture des logs terminée' """
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): def do_dump(self):
""" Affiche le contenu du dico """ """
liste = self.dic.items() Affiche la liste des dernières connexions, triées par date.
liste.sort(lambda x, y: cmp(x[1], y[1])) """
data = [(x[0], strftime('%d/%m/%Y %H:%M', localtime(x[1]))) liste = self.search('derniereConnexion=*')
for x in liste] liste = [(x.derniereConnexion(), x.compte()) for x in liste]
print tableau(data, liste.sort()
largeur = (20, 18)) 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): def get_idle_accounts(self, since=32*24*3600):
""" """
Renvoie la liste des couples (login, objet Adherent) de ceux qui Renvoie la liste des objets Adherent de ceux qui ne se sont pas
ne se sont pas connectés depuis since secondes, par défaut un mois connectés depuis since secondes, par défaut un mois (32 jours,
(32 jours, pour etre sûr). pour être sûr).
""" """
oldest = self.dic.get('!', int(time()))
limit = int(time()) - since limit = int(time()) - since
liste = [] return self.search('|(!(derniereConnexion=*))(derniereConnexion<=%d)' % limit)
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): def do_summary(self):
""" """
@ -172,16 +193,21 @@ comptes_inactifs.py
inscrits = [] inscrits = []
anciens = [] anciens = []
for (x, a) in self.get_idle_accounts(): liste = self.get_idle_accounts()
date = self.dic.get(x) # 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: if date:
date = strftime(u'%d/%m/%Y %H:%M', localtime(date)) date = strftime(u'%d/%m/%Y %H:%M', localtime(date))
else: else:
date = u'Jamais' date = u'Jamais'
forward = os.path.isfile('/home/%s/.forward' % x) and u'X' or u'' forward = os.path.isfile('/home/%s/.forward' % login) and u'X' or u''
mail = nb_mails_non_lus(x) mail = nb_mails_non_lus(login)
mail = mail == None and u'?' or mail > 0 and u'X' or u' ' 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(): if ann_scol in a.paiement():
inscrits.append(ligne) inscrits.append(ligne)
else: else:
@ -195,16 +221,19 @@ comptes_inactifs.py
inscrits = tableau(inscrits, titres, largeurs, alignements) inscrits = tableau(inscrits, titres, largeurs, alignements)
anciens_total = len(anciens) anciens_total = len(anciens)
anciens = tableau(anciens, titres, largeurs, alignements) anciens = tableau(anciens, titres, largeurs, alignements)
oldest = strftime(u'%d/%m/%Y %H:%M', oldest = strftime(u'%d/%m/%Y %H:%M', localtime(oldest_log))
localtime(self.dic.get('!', time())))
send_email(mail_sender, send_email(mail_sender,
mail_address, mail_report,
u'Comptes inactifs', u'Comptes inactifs',
modele % locals()) modele % locals(),
debug = debug)
def do_spam(self): 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 : # Nombre de personnes concernées, en expansant de droite à gauche :
# inscrit/ancien, avec/sans procmail, avec/sans mail non lu # inscrit/ancien, avec/sans procmail, avec/sans mail non lu
# Voir aussi template_path # Voir aussi template_path
@ -214,24 +243,90 @@ comptes_inactifs.py
smtp = SMTP() smtp = SMTP()
smtp.connect() smtp.connect()
for (x, a) in self.get_idle_accounts(): for a in self.get_idle_accounts():
pass # 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() 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__': if __name__ == '__main__':
args = sys.argv[1:] args = sys.argv[1:]
if len(args) != 1: if len(args) != 1:
sys.stderr.write("Arguments incorrects\n") sys.stderr.write("Arguments incorrects\n")
usage()
sys.exit(1) sys.exit(1)
commande = args[0] commande = args[0]
if commande not in ('log', 'dump', 'summary'): if commande not in actions:
sys.stderr.write("Commande incorrecte : %s\n" % commande) sys.stderr.write("Commande incorrecte : %s\n" % commande)
sys.exit(1) usage()
sys.exit(2)
ci_db = ComptesInactifs('/usr/scripts/var/comptes_inactifs.dict') ci = ComptesInactifs()
eval('ci_db.do_%s()' % commande) eval('ci.do_%s()' % commande)
ci_db.close()