- 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:
parent
5499472a9c
commit
17771d5e4b
1 changed files with 187 additions and 92 deletions
|
@ -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
|
||||||
|
# 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'dovecot.*Login: user=<|'
|
||||||
r'sshd.*Accepted.*for '
|
r'sshd.*Accepted.*for '
|
||||||
r')([^ >]+).*$')
|
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
|
|
||||||
|
|
||||||
# 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 = {}
|
self.dic = {}
|
||||||
|
|
||||||
def close(self):
|
def search(self, query, mode=''):
|
||||||
''' Sauvegarde du dico et suppression du lock '''
|
""" CransLdap.search allégé """
|
||||||
cPickle.dump(self.dic, file(self.filename, 'w'))
|
query = '(&(objectClass=cransAccount)(objectClass=adherent)(%s))' % query
|
||||||
os.remove(self.lockfile)
|
result = db.conn.search_s(db.base_dn, db.scope['adherent'], query)
|
||||||
|
result = [db.make(x, mode) for x in result]
|
||||||
|
return result
|
||||||
|
|
||||||
|
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):
|
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=*')
|
||||||
|
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]
|
for x in liste]
|
||||||
print tableau(data,
|
cprint(tableau(liste,
|
||||||
largeur = (20, 18))
|
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()
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue