#! /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()