#!/usr/bin/python
#    This program 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 3 of the License, or
#    (at your option) any later version.
#
#    This program 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, see <http://www.gnu.org/licenses/>.

""" cas - core accessibility system.
"""
import sys
import optparse
import os
import ConfigParser
import smtplib

from subprocess import Popen, PIPE
from datetime import datetime

from cas.network import Download
from cas.core import CoreBase, CoreException
from cas.util import UtilBase, Logging
from cas.rpmutils import RPMBase

# shutil from python 2.6 includes several enhancements for our use
# mainly in the _move_ method.
import cas.cas_shutil as shutil

if sys.version_info[:2] < (2,4):
    raise SystemExit("Python >= 2.4 required")

# Configuration parsing of /etc/cas.conf
config = ConfigParser.ConfigParser()
config.read("/etc/cas.conf")
WORKDIRECTORY = config.get("settings","workDirectory")
RPMS = config.get("settings","rpms")
DEBUGLEVEL = config.get("settings","debugLevel")
SERVERS = config.get("settings", "servers")
SMTPHOST = config.get("settings", "mailServer")

# Check for some advanced configurations
# Test to see if we provide a 32bit crash binary
# mainly used for x86_64 system who wish to analyze
# 32bit cores.
CRASH_32=None
if config.has_option("advanced", "crash_32"):
    CRASH_32=config.get("advanced", "crash_32")
    
class CoreHandler(object):
    def __init__(self, filename, dst, logger):
        self.filename = filename
        self.basename = os.path.basename(self.filename)
        self.dst = dst
        self.currentDirectory = os.path.realpath(os.curdir)
        self.casLog = logger
        self.tool = CoreBase()

    def run(self):
        if(self.filename.startswith("http") or self.filename.startswith("ftp")):
            self.casLog.info("Downloading %s" % (self.filename,))
            # filename is a url, process it with our download module
            # this should return the the abspath to our processed directory
            # and downloaded file
            self.filename = Download(self.filename, self.currentDirectory).get()
        if not os.path.isfile(self.filename):
            # not a url in this case so tests for local existence
            # TODO: add support to check remote hosts
            self.casLog.debug("Unable to find file %s" % (self.filename,))
            sys.exit(1)
        if self.tool.isCorefile(self.filename):
            # No need to proceed to extracting corefile since we assume
            # this is already at the proper stage.
            shutil.move(self.filename,
                        os.path.join(self.dst, self.basename))
            self.filename = os.path.join(self.dst, self.basename)
            return self.filename
        try:
            self.casLog.info("Detected a compressed core, extracting.. please wait as " \
                             "this process can take a long time.")
            # ok so some decompression utilites like gzip, bzip do not extract
            # files into the `cwd` unless specified with a to-stdout option.
            # this is a pain so we just move everything to `cwd` and proceed
            # from there.
            dst = os.path.join(self.currentDirectory, self.basename)
            shutil.move(self.filename, dst)
            corepath = self.tool.extractCore(dst)
            # corefile extracted now move it to work directory, pull basename
            # from corepath since we auto-detect the core file from extraction
            coreBasename = os.path.basename(corepath)
            shutil.move(corepath,os.path.join(self.dst, coreBasename))
            self.filename = os.path.join(self.dst, coreBasename)
            return self.filename
        except CoreException, err:
            self.casLog.debug(err)
            sys.exit(1)

class TimestampHandler(object):
    def __init__(self, corefile, logger):
        self.corefile = corefile
        self.casLog = logger
        self.util = UtilBase()
        self.tool = CoreBase()

    def run(self):
        # dig through the buildstamp database and attempt to match it with the
        # one found in the core
        rpmDB = self.util.load(RPMS)
        try:
            coreTimestamp = self.tool.timestamp(self.corefile)
        except CoreException, err:
            self.casLog.debug(err)
            sys.exit(1)
        for k,v in rpmDB.iteritems():
            for coreObj in rpmDB[k]:
                debugKernel, timestamp = coreObj
                if timestamp and coreTimestamp in timestamp:
                    return (k, debugKernel)
        self.casLog.debug("Unable to match timestamp %s" % (coreTimestamp,))
        sys.exit(1)

class CasApplication(object):
    def __init__(self, args):
        self.parse_options(args)
        self.util = UtilBase()
        self.rpmTool = RPMBase()

    def parse_options(self, args):
        # build option - arguement list in the form of
        # cas -i <id> -f <filename> -m user@example.com
        parser = optparse.OptionParser(usage="cas [opts] args")
        parser.add_option("-i","--identifier", dest="identifier",
                          help="Unique ID for core")
        parser.add_option("-f","--file", dest="filename",
                          help="Filename")
        parser.add_option("-e","--email", dest="email",
                          help="Define email for results (must be valid!)",
                          action="store")
        parser.add_option("-m","--modules", dest="kernel_modules",
                          help="Extract associated kernel modules",
                          action="store_true")
        self.opts, args = parser.parse_args()

        if not self.opts.identifier:
            parser.error("A unique identifier number is missing.")
        elif not self.opts.filename:
            parser.error("A file object is missing.")

        self.filename = self.opts.filename
        self.identifier = self.opts.identifier
        self.email = self.opts.email
        self.extractKernelModules = self.opts.kernel_modules
        # we want to allow for multiple cores under same identifier
        # so we base the hierarchy /workDirectory/identifier/datetime
        datenow = datetime.now()
        dateFormatted = datenow.strftime("%m.%d.%y.%I.%M.%S")
        self.storagePath = os.path.join(WORKDIRECTORY, self.identifier)
        self.storagePath = os.path.join(self.storagePath, dateFormatted)
        
        # build logger object to deal with logging per job and keep things
        # clean and easy to debug
        self.casLog = Logging(self.storagePath, self.identifier)

    def run(self):
        # setup directory structure
        if not os.path.isdir(self.storagePath):
            os.makedirs(self.storagePath)
        self.casLog.info("Starting job at %s" % (self.storagePath,))
        # begin core extraction analysis
        corefile = CoreHandler(self.filename, self.storagePath, self.casLog).run()
        self.casLog.info("Corefile prepared, processing %s" % (corefile,))
        debuginfo, debugKernel = TimestampHandler(corefile, self.casLog).run()
        filterString = "*/%s" % (debugKernel,)
        self.casLog.info("Extracting debug kernel with filter %s" % (filterString,))
        self.rpmTool.extract(debuginfo, self.storagePath,
                             filter=filterString,
                             return_results=False)

        # define absolute path to debugkernel
        debugKernel = os.path.abspath(debugKernel)
        # setup crash file to finalize the processing of the core file
        self.util.buildCrashFile(self.storagePath, corefile, debugKernel)
        # Pull the architecture from the elf file to match up with a 
        # server providing this architecture
        debugKernelArch = self.util.debugKernelArch(debugKernel)
        # Read current machine arch to see if we can bypass func and proceed
        # with processing the core on the current machine
        currentMachineArch = Popen(["uname","-m"], stdout=PIPE, stderr=PIPE)
        currentMachineArch = currentMachineArch.stdout.read().strip()
        # Check if we have installed crash 32bit on system
        if debugKernelArch == "i686" and CRASH_32 is not None:
            # DONE: re-write crash output library to take into account
            # 32bit crash on same system.
            self.util.buildCrashFile(self.storagePath, corefile, debugKernel,
                                     CRASH_32)
            self.casLog.info("Current machine suitable for processing 32 bit core, "\
                             "running crash.")
            cmd = os.path.join(self.storagePath,"crash")
            cmdPipe = Popen([cmd], stdout=PIPE, stderr=PIPE)
            cmdData = cmdPipe.communicate()
            # pull status code to verify crash even ran to completeness
            sts, out, err = (cmdPipe.returncode, cmdData[0].strip(), 
                             cmdData[1].strip())
            if sts:
                self.casLog.debug("crash problem: err: %s, out: %s" % (err, out))
        elif debugKernelArch == currentMachineArch:
            import platform
            # Define current machine hostname, mainly used for email results.
            casProcessMachine = platform.uname()[1]
            # The machine is suitable for processing the core through crash.
            self.casLog.info("Current machine suitable for processing core, running crash.")
            cmd = os.path.join(self.storagePath,"crash")
            # DONE: capture any errors returned from crash when processing core.
            cmdPipe = Popen([cmd], stdout=PIPE, stderr=PIPE)
            cmdData = cmdPipe.communicate()
            # pull status code to verify crash even ran to completeness
            sts, out, err = (cmdPipe.returncode, cmdData[0].strip(), 
                             cmdData[1].strip())
            if sts:
                self.casLog.debug("crash problem: err: %s, out: %s" % (err, out))
        else:
            # The machine running cas isn't capable of processing this core, lets
            # attempt with Func. Assuming Func is installed and a server database
            # is configured we attempt to process the core at another machine.
            try:
                import func.overlord.client as fc
                self.casLog.info("Crash file built, locating suitable %s system for " \
                       "processing" % (debugKernelArch,))
                if os.path.isfile(SERVERS):
                    serverList = self.util.load(SERVERS)
                    if serverList.has_key(debugKernelArch):
                        # TODO: Randomize server selection
                        # TODO: Verify remote server is reachable
                        casProcessMachine = serverList[debugKernelArch][0]
                        self.casLog.info("Machine %s found, processing crash output" % (casProcessMachine,))
                        cmd = os.path.join(self.storagePath,"crash")
                        client = fc.Overlord(casProcessMachine)
                        clientDict = client.command.run(cmd)
                        # Only necessary for debugging why running of crash failed.
                        sts, out, err = clientDict[clientDict.keys()[0]]
                        if sts:
                            self.casLog.debug(out.strip())
                    else:
                        self.casLog.info("No servers available for arch and current system not "\
                               "suitable for processing, please run cas-admin -h " \
                               "for more information")
                else:
                    self.casLog.info("No servers database found, please run cas-admin -h for " \
                          "more information")
                    sys.exit(1)
            # DONE: Possibly handle this exception more gracefully?
            except ImportError:
                self.casLog.info("Current running machine is not suitable for processing this core " \
                    "and http://fedorahosted.org/func is not installed/configured properly.")
                self.casLog.info("Finishing job without processing the core, please find a suitable %s "\
                    "machine in order to view this core in crash." % (debugKernelArch,))
        crashOutFile = os.path.join(self.storagePath,"crash.out")
        if os.path.isfile(crashOutFile) and self.extractKernelModules:
            self.casLog.info("Extracting loaded kernels modules. This will " \
                             "take several minutes.")
            # Here we extract kernel modules processed from crash.out
            # this is usually desired during filesystem, storage
            # analysis of a core.
            crashOutFH = open(crashOutFile, 'r')
            crashOutData = crashOutFH.readlines()
            # search for MODULE NAME SIZE OBJECT FILE line
            for item in enumerate(crashOutData):
                idx, txt = item
                if 'MODULE' in txt:
                    index = idx
            moduleList = []
            # we have our index of above, now we obtain
            # the loaded modules from the list
            for item in crashOutData[index+1:]:
                moduleList.append(item.split()[1])
            # shift through moduleList extracting from
            # kernel as we go
            for module in moduleList:
                # This will extract all modules for each kernel within
                # the debuginfo.
                # TODO: Only extract the module for the debug kernel in use.
                moduleFilter = "*/"+module+".ko*"
                self.rpmTool.extract(debuginfo, self.storagePath,
                                     filter=moduleFilter,
                                     return_results=False)
        if os.path.isfile(crashOutFile) and self.email:
            self.casLog.info("Crash output processed, sending email to " \
                             "%s" % (self.email,))
            try:
                # Compose email msg of results
                msg = "Subject: CAS results for %s\r\n\n" % (self.identifier,)
                msg += "Location: %s\n" % (self.storagePath,)
                msg += "Server: %s\n" % (casProcessMachine,)
                msg += "Output data:\n"
                crashOutFH = open(crashOutFile,'r')
                msg += crashOutFH.read()
                crashOutFH.close()
                try:
                    # for some reason smtplib doesn't have proper exception
                    # handling for name or service not known :(
                    mailServer = smtplib.SMTP(SMTPHOST)
                except:
                    self.casLog.debug("Unable to connect to mail server: %s, " \
                                      "no email results sent." % (SMTPHOST,))
                mailServer.set_debuglevel(0)
                mailServer.sendmail(self.email,self.email,msg)
                mailServer.quit()
            except smtplib.SMTPException, e:
                self.casLog.debug(e)
        self.casLog.info("Job on %s complete and located " \
                         "in %s." % (self.filename, self.storagePath))
        return

if __name__=="__main__":
    # Begin CAS
    app = CasApplication(sys.argv[1:])
    sys.exit(app.run())
