#!/usr/bin/env python

"""
bcfg2-build-reports generates & distributes reports of statistic
information for Bcfg2."""

__revision__ = '$Revision$'

import copy
import getopt
import re
import os
import socket
import sys
from time import asctime, strptime
from lxml.etree import XML, XSLT, parse, Element, ElementTree, SubElement, tostring, XMLSyntaxError
# Compatibility imports
from Bcfg2.Bcfg2Py3k import ConfigParser

def generatereport(rspec, nrpt):
    """
    generatereport creates and returns an ElementTree representation
    of a report adhering to the XML spec for intermediate reports.
    """
    reportspec = copy.deepcopy(rspec)
    nodereprt = copy.deepcopy(nrpt)

    reportgood = reportspec.get("good", default = 'Y')
    reportmodified = reportspec.get("modified", default = 'Y')
    current_date = asctime()[:10]

    """Build regex of all the nodes we are reporting about."""
    pattern = re.compile( '|'.join([item.get("name") for item in reportspec.findall('Machine')]))

    for node in nodereprt.findall('Node'):
        if not (node.findall("Statistics") and pattern.match(node.get('name'))):
            # Don't know enough about node.
            nodereprt.remove(node)
            continue

        # Reduce to most recent Statistics entry.
        statisticslist = node.findall('Statistics')
        # This line actually sorts from most recent to oldest.
        statisticslist.sort(lambda y, x: cmp(strptime(x.get("time")), strptime(y.get("time"))))
        stats = statisticslist[0]

        [node.remove(item) for item in node.findall('Statistics')]

        # Add a good tag if node is good and we wnat to report such.
        if reportgood == 'Y' and stats.get('state') == 'clean':
            SubElement(stats,"Good")

        [stats.remove(item) for item in stats.findall("Bad") + stats.findall("Modified") if \
         item.getchildren() == []]
        [stats.remove(item) for item in stats.findall("Modified") if reportmodified == 'N']

        # Test for staleness -if stale add Stale tag.
        if stats.get("time").find(current_date) == -1:
            SubElement(stats,"Stale")
        node.append(stats)
    return nodereprt

def mail(mailbody, confi):
    """mail mails a previously generated report."""

    try:
        mailer = confi.get('statistics', 'sendmailpath')
    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        mailer = "/usr/sbin/sendmail"
    # Open a pipe to the mail program and
    # write the data to the pipe.
    pipe = os.popen("%s -t" % mailer, 'w')
    pipe.write(mailbody)
    exitcode = pipe.close()
    if exitcode:
        print("Exit code: %s" % exitcode)

def rss(reportxml, delivery, report):
    """rss appends a new report to the specified rss file
    keeping the last 9 articles.
    """
    # Check and see if rss file exists.
    for destination in delivery.findall('Destination'):
        try:
            fil = open(destination.attrib['address'], 'r')
            olddoc = XML(fil.read())

            # Defines the number of recent articles to keep.
            items = olddoc.find("channel").findall("item")[0:9]
            fil.close()
            fil = open(destination.attrib['address'], 'w')
        except (IOError, XMLSyntaxError):
            fil = open(destination.attrib['address'], 'w')
            items = []

        rssdata = Element("rss")
        channel = SubElement(rssdata, "channel")
        rssdata.set("version", "2.0")
        chantitle = SubElement(channel, "title")
        chantitle.text = report.attrib['name']
        chanlink = SubElement(channel, "link")

        # This can later link to WWW report if one gets published
        # simultaneously?
        chanlink.text = "http://www.mcs.anl.gov/cobalt/bcfg2"
        chandesc = SubElement(channel, "description")
        chandesc.text = "Information regarding the 10 most recent bcfg2 runs."

        channel.append(XML(reportxml))

        if items != []:
            for item in items:
                channel.append(item)

        tree = tostring(rssdata, encoding='UTF-8', xml_declaration=True)
        fil.write(tree)
        fil.close()

def www(reportxml, delivery):
    """www outputs report to."""

    # This can later link to WWW report if one gets published
    # simultaneously?
    for destination in delivery.findall('Destination'):
        fil = open(destination.attrib['address'], 'w')

        fil.write(reportxml)
        fil.close()

def fileout(reportxml, delivery):
    """Outputs to plain text file."""
    for destination in delivery.findall('Destination'):
        fil = open(destination.attrib['address'], 'w')

        fil.write(reportxml)
        fil.close()

def pretty_print(element, level=0):
    """Produce a pretty-printed text representation of element."""
    if element.text:
        fmt = "%s<%%s %%s>%%s</%%s>" % (level*" ")
        data = (element.tag, (" ".join(["%s='%s'" % keyval for keyval in list(element.attrib.items())])),
                element.text, element.tag)
    if element._children:
        fmt = "%s<%%s %%s>\n" % (level*" ",) + (len(element._children) * "%s") + "%s</%%s>\n" % (level*" ")
        data = (element.tag, ) + (" ".join(["%s='%s'" % keyval for keyval in list(element.attrib.items())]),)
        data += tuple([pretty_print(entry, level+2) for entry in element._children]) + (element.tag, )
    else:
        fmt = "%s<%%s %%s/>\n" % (level * " ")
        data = (element.tag, " ".join(["%s='%s'" % keyval for keyval in list(element.attrib.items())]))
    return fmt % data


if __name__ == '__main__':
    ping=True
    all=False
    if '-C' in sys.argv:
        cfpath = sys.argv[sys.argv.index('-C') + 1]
    else:
        cfpath = '/etc/bcfg2.conf'
    c = ConfigParser.ConfigParser()
    c.read([cfpath])
    configpath = "%s/etc/report-configuration.xml" % c.get('server', 'repository')
    statpath = "%s/etc/statistics.xml" % c.get('server', 'repository')
    clientsdatapath = "%s/Metadata/clients.xml" % c.get('server', 'repository')
    try:
        prefix = c.get('server', 'prefix')
    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        prefix = '/usr'

    transformpath = "/%s/share/bcfg2/xsl-transforms/" % (prefix)
    #websrcspath = "/usr/share/bcfg2/web-rprt-srcs/"

    try:
        opts, args = getopt.getopt(sys.argv[1:], "C:hAc:Ns:", ["help", "all", "config=","no-ping", "stats="])
    except getopt.GetoptError:
        mesg = sys.exc_info()[1]
        # Print help information and exit:
        print("%s\nUsage:\nbcfg2-build-reports [-h][-A (include ALL clients)] [-c <configuration-file>] [-s <statistics-file>][-N (do not ping clients)]" % (mesg))
        raise SystemExit(2)
    for o, a in opts:
        if o in ("-h", "--help"):
            print("Usage:\nbcfg2-build-reports [-h] [-c <configuration-file>] [-s <statistics-file>]")
            raise SystemExit
        if o in ("-A", "--all"):
            all=True
        if o in ("-c", "--config"):
            configpath = a
        if o in ("-N", "--no-ping"):
            ping = False
        if o in ("-s", "--stats"):
            statpath = a


    # See if hostinfo.xml exists, and is less than 23.5 hours old
    #try:
        #hostinstat = os.stat(hostinfopath)
        #if (time() - hostinstat[9])/(60*60) > 23.5:
    if ping:
        os.system('bcfg2-ping-sweep -C %s' % cfpath) # bcfg2-ping-sweep needs to be in path
    #except OSError:
    #    os.system('GenerateHostInfo')#Generate HostInfo needs to be in path


    """Reads data & config files."""
    try:
        statsdata = XML(open(statpath).read())
    except (IOError, XMLSyntaxError):
        print("bcfg2-build-reports: Failed to parse %s"%(statpath))
        raise SystemExit(1)
    try:
        configdata = XML(open(configpath).read())
    except (IOError, XMLSyntaxError):
        print("bcfg2-build-reports: Failed to parse %s"%(configpath))
        raise SystemExit(1)
    try:
        clientsdata = XML(open(clientsdatapath).read())
    except (IOError, XMLSyntaxError):
        print("bcfg2-build-reports: Failed to parse %s"%(clientsdatapath))
        raise SystemExit(1)

    # Merge data from three sources.
    nodereport = Element("Report", attrib={"time" : asctime()})
    # Should all of the other info in Metadata be appended?
    # What about all of the package stuff for other types of reports?
    for client in clientsdata.findall("Client"):
        nodel = Element("Node", attrib={"name" : client.get("name")})
        nodel.append(client)
        for nod in statsdata.findall("Node"):
            if client.get('name').find(nod.get('name')) == 0:
                for statel in nod.findall("Statistics"):
                    nodel.append(statel)
                    nodereport.append(nodel)

    if all:
        for nod in statsdata.findall("Node"):
            for client in clientsdata.findall("Client"):
                if client.get('name').find(nod.get('name')) == 0:
                    break
            else:
                nodel = Element("Node", attrib={"name" : nod.get("name")})
                client = Element("Client", attrib={"name" : nod.get("name"), "profile" : "default"})
                nodel.append(client)
                for statel in nod.findall("Statistics"):
                    nodel.append(statel)
                    nodereport.append(nodel)


    for reprt in configdata.findall('Report'):
        nodereport.set("name", reprt.get("name", default="BCFG Report"))

        if reprt.get('refresh-time') != None:
            nodereport.set("refresh-time", reprt.get("refresh-time", default="600"))

        procnodereport = generatereport(reprt, nodereport)

        for deliv in reprt.findall('Delivery'):
            # Is a deepcopy of procnodereport necessary?

            delivtype = deliv.get('type', default='nodes-digest')
            deliverymechanism = deliv.get('mechanism', default='www')

            # Apply XSLT, different ones based on report type, and options
            if deliverymechanism == 'null-operator': # Special Cases
                fileout(tostring(ElementTree(procnodereport).getroot(), encoding='UTF-8', xml_declaration=True), deliv)
                break
            transform = delivtype + '-' + deliverymechanism + '.xsl'

            try: # Make sure valid stylesheet is selected.
                os.stat(transformpath + transform)
            except:
                print("bcfg2-build-reports: Invalid report type or delivery mechanism.\n Can't find: "\
                      + transformpath + transform)
                raise SystemExit(1)

            try: # Try to parse stylesheet.
                stylesheet = XSLT(parse(transformpath + transform))
            except:
                print("bcfg2-build-reports: invalid XSLT transform file.")
                raise SystemExit(1)

            if deliverymechanism == 'mail':
                if delivtype == 'nodes-individual':
                    reportdata = copy.deepcopy(procnodereport)
                    for noden in reportdata.findall("Node"):
                        [reportdata.remove(y) for y in reportdata.findall("Node")]
                        reportdata.append(noden)
                        result = stylesheet.apply(ElementTree(reportdata))
                        outputstring = stylesheet.tostring(result)

                        if not outputstring == None:
                            toastring = ''
                            for desti in deliv.findall("Destination"):
                                toastring = "%s%s " % \
                                            (toastring, desti.get('address'))
                            # Prepend To: and From:
                            outputstring = "To: %s\nFrom: root@%s\n%s"% \
                                           (toastring, socket.getfqdn(), outputstring)
                            mail(outputstring, c) #call function to send

                else:
                    reportdata = copy.deepcopy(procnodereport)

                    result = stylesheet.apply(ElementTree(reportdata))
                    outputstring = stylesheet.tostring(result)

                    if not outputstring == None:
                        toastring = ''
                        for desti in deliv.findall("Destination"):
                            toastring = "%s%s " % \
                                        (toastring, desti.get('address'))
                            # Prepend To: and From:
                            outputstring = "To: %s\nFrom: root@%s\n%s"% \
                                           (toastring, socket.getfqdn(), outputstring)
                            mail(outputstring, c) #call function to send
            else:
                outputstring = tostring(stylesheet.apply(ElementTree(procnodereport)).getroot(), encoding='UTF-8', xml_declaration=True)
                if deliverymechanism == 'rss':
                    rss(outputstring, deliv, reprt)
                else: # Must be deliverymechanism == 'www':
                    www(outputstring, deliv)
