#!/usr/bin/env python
# -*- coding: utf-8 -*-

# statistics-summary.py
# ---------------------

# Copyright (C) 2008-2009 Michel Blockelet <blockelet@crans.org>

# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.



"""Outil pour créer un résumé des statistiques de bcfg2."""

# Importations
import os, sys, time
import xml.dom.minidom

## Configuration
# Fichier de statistiques
statsfile = '/var/lib/bcfg2/etc/statistics.xml'
# Nombre de jours à partir desquels l'hôte est considéré comme vieux
daysold = 2



def processHost(node, ignore):
    """Traite les statistiques d'un hôte à partir de son noeud XML."""

    # On prend les statistiques les plus récentes (le fichier de statistiques
    #  peut contenir plusieurs runs)
    stamps = node.getElementsByTagName('OpStamps')
    curtime = 0
    for stamp in stamps:
        if float(stamp.getAttribute('start')) > curtime:
            curtime = float(stamp.getAttribute('start'))
            stats = stamp.parentNode

    # On regarde l'état
    if stats.getAttribute('state') == 'clean':
        state = 'clean'
        lines = [u' * %s - %s - clean' % (node.getAttribute('name'),
            stats.getAttribute('time'))]
        nbbad = 0
        ignored = 0
    else:
        # On récupère les différents points pas à jour
        bad = stats.getElementsByTagName('Bad')[0]
        nbbad = (int(stats.getAttribute('total'))
            - int(stats.getAttribute('good')))
        cfgfiles = bad.getElementsByTagName('ConfigFile')
        packages = bad.getElementsByTagName('Package')
        services = bad.getElementsByTagName('Service')
        actions = bad.getElementsByTagName('Action')

        # On ignore ce qu'il faut
        ignored = 0
        ignorestr = u''
        if 'config' in ignore and len(cfgfiles) > 0:
            ignored += len(cfgfiles)
            ignorestr += u' %d configfiles' % len(cfgfiles)
        if 'packages' in ignore and len(packages) > 0:
            ignored += len(packages)
            ignorestr += u' %d packages' % len(packages)
        if 'actions' in ignore and len(actions) > 0:
            ignored += len(actions)
            ignorestr += u' %d actions' % len(actions)
        if 'services' in ignore and len(services) > 0:
            ignored += len(services)
            ignorestr += u' %d services' % len(services)
        if ignored > 0:
            ignorestr = u' - %d ignored :%s' % (ignored, ignorestr)

        # On affiche tous les trucs pas bons
        if ignored == nbbad:
            # On a tout ignoré
            state = 'ignored'
            lines = [u' * %s - %s%s' % (node.getAttribute('name'),
                stats.getAttribute('time'), ignorestr)]
        else:
            # On a des trucs à afficher
            state = 'bad'
            lines = []
            lines.append(u' * %s - %s' % (node.getAttribute('name'),
                stats.getAttribute('time')))
            lines.append(u'  State : %s - %d bad%s'
                % (stats.getAttribute('state'), nbbad, ignorestr))

            # Fichiers de configuration
            if len(cfgfiles) > 0 and 'config' not in ignore:
                lines.append(u'  * Config files :')
                for cfgfile in cfgfiles:
                    if cfgfile.getAttribute('current_exists') == 'false':
                        lines.append(u'   + %s' % cfgfile.getAttribute('name'))
                    else:
                        lines.append(u'   * %s' % cfgfile.getAttribute('name'))

            # Paquets
            if len(packages) > 0 and 'packages' not in ignore:
                lines.append(u'  * Packages :')
                for package in packages:
                    if package.getAttribute('current_version') == '':
                        lines.append(u'   + %s :: %s'
                            % (package.getAttribute('name'),
                            package.getAttribute('version')))
                    else:
                        lines.append(u'   * %s :: %s -> %s'
                            % (package.getAttribute('name'),
                            package.getAttribute('current_version'),
                            package.getAttribute('version')))

            # Actions
            if len(actions) > 0 and 'actions' not in ignore:
                lines.append(u'  * Actions :')
                for action in actions:
                    lines.append(u'   * %s' % action.getAttribute('name'))

            # Services
            if len(services) > 0 and 'services' not in ignore:
                lines.append(u'  * Services :')
                for service in services:
                    lines.append(u'   * %s' % service.getAttribute('name'))

            lines.append('')

    return (time.time() - curtime > daysold * 24 * 60 * 60, state, lines, nbbad - ignored)


def deleteHosts(hostslist, writeback):
    """Supprime les hôtes de la liste hostslist du fichier de statistiques.
    Si hostslist évalue à False, demande une confirmation puis supprime
    tous les hôtes vieux."""
    doc = xml.dom.minidom.parse(statsfile)
    hoststodel = []
    for node in doc.getElementsByTagName('Node'):
        if not hostslist:
            # On sélectionne les hôtes vieux
            stamps = node.getElementsByTagName('OpStamps')
            curtime = 0
            for stamp in stamps:
                if float(stamp.getAttribute('start')) > curtime:
                    curtime = float(stamp.getAttribute('start'))
            if time.time() - curtime > daysold * 24 * 60 * 60:
                hoststodel.append(node)
        else:
            # On sélectionne les hôtes donnés en argument
            if node.getAttribute('name') in hostslist:
                hoststodel.append(node)

    # Suppression des hôtes dans le schéma XML
    for node in hoststodel:
        print u'Suppression de l\'hôte : %s' % node.getAttribute('name')
        node.parentNode.removeChild(node)

    if hoststodel:
        if writeback:
            # Enregistrement des données dans le fichier
            print u'Arrêt de bcfg2-server ...'
            if os.system('/etc/init.d/bcfg2-server stop'):
                print u'Erreur !'
            print u'Sauvegarde ...'
            doc.writexml(open(statsfile, 'w'))
            print u'Redémarrage de bcfg2-server ...'
            if os.system('/etc/init.d/bcfg2-server start'):
                print u'Erreur !'
        else:
            print u'Utilisez l\'option -w en tant que root pour enregistrer.'
    else:
        print u'Aucun hôte à supprimer !'


def genStats(ignore):
    """Génère les statistiques, en ignorant les éléments de la
    liste ignore."""
    # On traite le fichier de statistiques
    doc = xml.dom.minidom.parse(statsfile)

    oldhosts = []
    dirtyhosts = []
    cleanhosts = []

    oldnb = 0
    badnb = 0
    cleannb = 0

    # On récupère les informations des différents hôtes
    for curnode in doc.getElementsByTagName('Node'):
        (isold, state, hostlines, urgency) = processHost(curnode, ignore)
        if isold:
            oldnb += 1
            if state in ['clean', 'ignored']:
                oldhosts.append((0, hostlines))
            else:
                oldhosts.append((urgency, hostlines))
        else:
            if state in ['clean', 'ignored']:
                cleanhosts.append((0, hostlines))
                cleannb += 1
            else:
                dirtyhosts.append((urgency, hostlines))
                badnb += 1
    
    # Get the dirtier hosts first.
    dirtyhosts.sort()
    dirtyhosts.reverse()
    oldhosts.sort()
    oldhosts.reverse()


    # Header
    print (u'*** Bcfg2 Statistics on %s, for %d hosts ***'
        % (time.strftime("%c"), oldnb + badnb + cleannb))
    print u' *   Old hosts : %d' % oldnb
    print u' *   Bad hosts : %d' % badnb
    print u' * Clean hosts : %d' % cleannb
    print

    # Hôtes vieux
    if oldnb:
        print u'*** Old hosts (more than %s days) :' % daysold
        for host in oldhosts:
            for line in host[1]:
                print line
        print

    # Hôtes sales
    if badnb:
        print u'*** Bad hosts :'
        for host in dirtyhosts:
            for line in host[1]:
                print line
        print

    # Hôtes propres
    if cleannb:
        print u'*** Clean hosts (or with ignored dirtyness) :'
        for host in cleanhosts:
            for line in host[1]:
                print line

if __name__ == '__main__':
    if '-h' in sys.argv:
        # Message d'utilisation
        print u'Outil pour créer un résumé des statistiques de bcfg2.'
        print u''
        print u' * Usage :'
        print u'  * Génération de statistiques :'
        print u'   -c ou --no-config pour ignorer les ConfigFiles'
        print u'   -p ou --no-packages pour ignorer les Packages'
        print u'   -a ou --no-actions pour ignorer les Actions'
        print u'   -s ou --no-services pour ignorer les Services'
        print u''
        print u'  * Suppression d\'hôtes des statistiques :'
        print u'   -d ou --delete seuls pour supprimer tous les hôtes vieux'
        print u'   -d ou --delete [hôte]... pour supprimer certains hôtes'
        print u'   -w ou --write pour enregistrer les modifications'
        print u'    (sinon, ne fait qu\'afficher les hôtes concernés)'
    else:
        # Lecture des options
        ignore = []
        if '-c' in sys.argv or '--no-config' in sys.argv:
            ignore.append('config')
        if '-p' in sys.argv or '--no-packages' in sys.argv:
            ignore.append('packages')
        if '-a' in sys.argv or '--no-actions' in sys.argv:
            ignore.append('actions')
        if '-s' in sys.argv or '--no-services' in sys.argv:
            ignore.append('services')

        if os.access(statsfile, os.F_OK):
            if '-d' in sys.argv or '--delete' in sys.argv:
                # Mode suppression
                deleteHosts([x for x in sys.argv[1:]
                    if x not in ['-d', '--delete', '-w']], os.getuid() == 0 and
                    ('-w' in sys.argv or '--write' in sys.argv))
            else:
                # Mode génération des statistiques
                genStats(ignore)
        else:
            print u'Impossible d\'accéder au fichier de statistiques :'
            print u' %s !' % statsfile
