
Ignore-this: 3462b33a0e5bda9f6078366e693dc003 darcs-hash:20090425003120-ffbb2-ad3f2406658ca095aa576df89af0ec38dda1940e.gz
338 lines
11 KiB
Python
338 lines
11 KiB
Python
#! /usr/bin/env python
|
|
# -*- coding: iso-8859-15 -*-
|
|
|
|
"""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
|
|
|
|
|
|
import sys, os, re, time, cPickle
|
|
from time import mktime, time, localtime, strptime, strftime
|
|
from socket import gethostname
|
|
from smtplib import SMTP
|
|
|
|
host = gethostname()
|
|
debug = None
|
|
mail_report = u'disconnect@crans.org'
|
|
mail_sender = u"Comptes inactifs <disconnect@crans.org>"
|
|
template_path = '/usr/scripts/templates/comptes_inactifs.%d.txt'
|
|
actions = ('log', 'dump', 'summary', 'spam')
|
|
|
|
sys.path.append('/usr/scripts/gestion')
|
|
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):
|
|
"""
|
|
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(object):
|
|
# 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 = [re.compile(r'^(\w+\s+\d+\s+\d+:\d+:\d+).*(?:'
|
|
r'dovecot.*Login: user=<|'
|
|
r'sshd.*Accepted.*for '
|
|
r')([^ >]+).*$'),
|
|
re.compile(r'^.*comptes_inactifs.*derniereConnexion=<([^>]+)>, '
|
|
r'login=<([^>]+)>')]
|
|
|
|
def __init__(self):
|
|
""" Initialisation """
|
|
self.dic = {}
|
|
|
|
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
|
|
|
|
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é.
|
|
"""
|
|
dic = self.dic
|
|
timestamp = int(timestamp)
|
|
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.
|
|
Renvoie le nombre de lignes traitées.
|
|
"""
|
|
annee = localtime(time())[0]
|
|
now = time() + 600
|
|
nombre = 0
|
|
for line in loglines:
|
|
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).lower(), t)
|
|
nombre += 1
|
|
return nombre
|
|
|
|
def do_log(self):
|
|
"""
|
|
Lit des lignes de log sur l'entrée std et met à jour la base LDAP.
|
|
"""
|
|
parsed_lines = self.update_from_syslog(sys.stdin)
|
|
updated_entries = self.commit_to_ldap()
|
|
syslog("%(parsed_lines)s ligne(s) traitée(s)" % locals())
|
|
syslog("%(updated_entries)s entrée(s) mise(s) à jour dans la base LDAP" % locals())
|
|
if parsed_lines == 0 or updated_entries == 0:
|
|
sys.stderr.write("""Erreur lors de la mise à jour de la base LDAP :
|
|
%(parsed_lines)s ligne(s) traitée(s)
|
|
%(updated_entries)s entrée(s) mise(s) à jour dans la base LDAP
|
|
""" % locals())
|
|
|
|
def do_dump(self):
|
|
"""
|
|
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 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).
|
|
"""
|
|
limit = int(time()) - since
|
|
liste = self.search("derniereConnexion<=%d" % limit)
|
|
for x in self.search("!(derniereConnexion=*)"):
|
|
if x.dateInscription() <= limit:
|
|
liste.append(x)
|
|
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
|
|
|
|
--
|
|
comptes_inactifs.py
|
|
"""
|
|
inscrits = []
|
|
anciens = []
|
|
|
|
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' % 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(), login, 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)
|
|
|
|
send_email(mail_sender,
|
|
mail_report,
|
|
u'Comptes inactifs',
|
|
modele % locals(),
|
|
debug = debug)
|
|
|
|
def do_spam(self):
|
|
"""
|
|
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
|
|
stats = [0, 0, 0, 0, 0, 0, 0, 0]
|
|
|
|
# On factorise la connexion
|
|
smtp = SMTP()
|
|
smtp.connect()
|
|
|
|
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 a.dateInscription()
|
|
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,
|
|
cc = debug,
|
|
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"Forward", u"Mails", u"Nombre"),
|
|
largeur = (10, 9, 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) == 0:
|
|
usage()
|
|
sys.exit(0)
|
|
|
|
for commande in args:
|
|
if commande not in actions:
|
|
sys.stderr.write("Commande incorrecte : %s\n" % commande)
|
|
usage()
|
|
sys.exit(2)
|
|
|
|
ci = ComptesInactifs()
|
|
for commande in args:
|
|
eval('ci.do_%s()' % commande)
|