scripts/surveillance/comptes_inactifs.py
2014-08-14 22:28:40 +02:00

332 lines
11 KiB
Python
Executable file

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""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/surveillance/comptes_inactifs/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()
import syslog
syslog.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
compiled_regex = [re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}).*(?:'
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.syslog("LDAP(lock): derniereConnexion=<%s>, login=<%s>" %
(strftime("%Y-%m-%dT%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.compiled_regex:
m = r.match(line)
if m: break
if not m: continue
date = list(strptime(m.group(1), "%Y-%m-%dT%H:%M:%S"))
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.syslog("%(parsed_lines)s ligne(s) traitée(s)" % locals())
syslog.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(os.path.join(a.home(), '.forward')) 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 .forward, 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(os.path.join(a.home(), '.forward')): 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)