diff --git a/surveillance/comptes_inactifs.py b/surveillance/comptes_inactifs.py new file mode 100755 index 00000000..140dbc9f --- /dev/null +++ b/surveillance/comptes_inactifs.py @@ -0,0 +1,207 @@ +#! /usr/bin/env python +# -*- coding: iso-8859-15 -*- + +""" +Repère les comptes inactifs en parsant les logs de derniere connexion +de sshd et dovecot. +""" + +# Copyright (C) 2006 Stéphane Glondu +# Licence : GPLv2 + + +import sys, os, sre, time, cPickle +from time import mktime, time, localtime, strptime, strftime +from socket import gethostname + +host = gethostname() +mail_address = u'disconnect@crans.org' +mail_sender = u"Comptes inactifs " + +sys.path.append('/usr/scripts/gestion') +from affich_tools import tableau +from email_tools import send_email +from ldap_crans import crans_ldap +from config import ann_scol +db = crans_ldap() + + +class ComptesInactifs: + re = sre.compile(r'^(\w+\s+\d+\s+\d+:\d+:\d+).*(?:' + r'dovecot.*Login: user=<|' + r'sshd.*Accepted.*for ' + r')([^ >]+).*$') + + def __init__(self, filename): + ''' Lecture du dico et acquisition du lock ''' + 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() + + # A partir de la, 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 update(self, login, timestamp): + """ + Met a jour l'entree correspondant au login donnee, ainsi que + l'entree '!', qui correspond a la plus vieille entree. + """ + dic = self.dic + timestamp = int(timestamp) + + # Mise a jour de l'entree la plus vieille + if not dic.has_key('!') or timestamp < dic['!']: + dic['!'] = timestamp + + # Mise a jour de l'entree correspondant au login + if not dic.has_key(login) or timestamp > dic[login]: + dic[login] = timestamp + + def update_from_syslog(self, loglines): + """ Met a jour le dico avec les lignes de syslog donnees """ + annee = localtime(time())[0] + now = time() + 600 + nombre = 0 + for line in loglines: + m = self.re.match(line) + if not m: continue + date = list(strptime(m.group(1), "%b %d %H:%M:%S")) + date[0] = annee + t = mktime(date) + if t > now: + date[0] = annee - 1 + t = mktime(date) + self.update(m.group(2), t) + nombre += 1 + print '%d ligne(s) pertinente(s)' % nombre + + def do_log(self): + """ Lit des lignes de log sur l'entree std et met a jour le dico """ + self.update_from_syslog(sys.stdin) + print 'Lecture des logs terminee' + + 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)) + + def get_idle_accounts(self, since=32*24*3600): + """ + Renvoie la liste de ceux qui ne se sont pas connectes depuis + since secondes, par defaut un mois (32 jours, pour etre sur). + """ + oldest = self.dic.get('!', int(time())) + limit = int(time()) - since + liste = [a + for a in os.listdir('/home') + if os.path.isdir('/var/mail/' + a) + and self.dic.get(a, oldest) < limit] + return liste + + def do_summary(self): + 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 + +L'analyse des logs remonte au %(oldest)s. + +-- +comptes_inactifs.py +""" + liste = [] + for x in self.get_idle_accounts(): + a = db.search('uid=%s' % x)['adherent'] + if a: + liste.append((x, a[0])) + else: + print 'uid=%s introuvable' % x + liste.sort() + + inscrits = [] + anciens = [] + + for (x, a) in liste: + date = self.dic.get(x) + 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'' + try: + maildir = '/var/mail/%s/new' % x + mail = os.path.isdir(maildir) and os.listdir(maildir) and u'X' or u'' + except: + # Arrive quand le script n'a pas les bons droits pour lire + # /var/mail + mail = u'?' + ligne = (a.id(), x, 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) + oldest = strftime(u'%d/%m/%Y %H:%M', + localtime(self.dic.get('!', time()))) + + send_email(mail_sender, + mail_address, + u'Comptes inactifs', + modele % locals()) + + +if __name__ == '__main__': + args = sys.argv[1:] + + if len(args) != 1: + sys.stderr.write("Arguments incorrects\n") + sys.exit(1) + + commande = args[0] + if commande not in ('log', 'dump', 'summary'): + sys.stderr.write("Commande incorrecte : %s\n" % args[1]) + sys.exit(1) + + ci_db = ComptesInactifs('/usr/scripts/var/comptes_inactifs.dict') + eval('ci_db.do_%s()' % commande) + ci_db.close()