#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Script to guide user to submit a bug report against Xen.
# Author: Wei Liu <wei.liu2@citrix.com>

from __future__ import with_statement
import sys
import os
import subprocess
import re
import tempfile
import shutil
import email
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEAudio import MIMEAudio
from email.MIMEImage import MIMEImage
from email.MIMEMessage import MIMEMessage
from email.MIMEBase import MIMEBase
from email.Header import Header
import mimetypes
import smtplib
import getpass
import rfc822

LOG_DIR = '/var/log/'
XEN_LOG_DIR = LOG_DIR + 'xen/'

SYSTEM_LOGS = [LOG_DIR + x for x in ['syslog', 'messages', 'debug']]

XEN_LOGS = [XEN_LOG_DIR + x for x in
            ['xend-debug.log', 'xenstored-trace.log',
             'xen-hotplug.log', 'xend.log', 'domain-builder-ng.log'] +
            ['xend.log.%d' % z for z in range(1, 3)]]

FILES_TO_SEND = SYSTEM_LOGS + XEN_LOGS

default_editor = '/usr/bin/vim'


report_template = \
'''We would like you to answer the following questions (feel free to
write up your own report if you're a power user):

* What's the sotware setup?
  * Dom0: e.g. 64 bit CentOS 6.2
  * DomU: e.g. Debian Wheezy
  * Xen: e.g. 4.1 installed with apt-get / 4.2 compiled from tarball
  * toolstack: xl / xm / libvirt ...

* What's the hardware setup?

* What were you trying to achieve?

* What commands did you run?

* What's the expected outcome?

* What's the actual outcome?

'''


def xen_toolstack():
    try:
        child = subprocess.Popen(["xen-detect"], stdout=subprocess.PIPE)
        output, err = child.communicate()
        output = output.strip()
        if 'Not running' in output:
            print "Not running on Xen, quit..."
            sys.exit(0)
        matches = re.match(r"Running in (HVM|PV) context on Xen v(\d+\.\d+)",
                           output)
        if matches:
            xen_version = matches.group(2)
        else:
            print "No Xen version found in xen-detect output:", output
            sys.exit(1)
    except Exception, exn:
        print "Failed to run xen-detect:", exn
        sys.exit(1)

    if float(xen_version) >= 4.2:
        toolstack = 'xl'
    else:
        toolstack = 'xm'

    return toolstack


def which_editor():
    for editor in [
        os.environ.get("VISUAL"),
        os.environ.get("EDITOR"),
        default_editor]:
        if editor:
            break

    return editor


def edit_bug_report(filename):
    editor = which_editor()
    subprocess.call([editor, filename])


def print_bug_report(filename):
    print '===== BUG REPORT START ====='
    with open(filename, mode='r') as f:
        print f.read()
    print '===== BUG REPORT END ====='


def create_bug_report(tmpdir):
    with tempfile.NamedTemporaryFile(mode='w',
                                     prefix='xen-bug-report-',
                                     dir=tmpdir,
                                     delete=False) as f:
        f.write(report_template)
        bug_report_name = f.name

    edit_bug_report(bug_report_name)

    print "Bug report body saved in", bug_report_name
    print_bug_report(bug_report_name)

    while yes("Revise bug report?"):
        edit_bug_report(bug_report_name)
        print_bug_report(bug_report_name)

    return bug_report_name


# Following code taken from Debian's reportbug utility
#
ascii_range = ''.join([chr(ai) for ai in range(32,127)])
notascii = re.compile(r'[^'+re.escape(ascii_range)+']')
notascii2 = re.compile(r'[^'+re.escape(ascii_range)+r'\s]')


class BetterMIMEText(MIMEText):
    def __init__(self, _text, _subtype='plain', _charset=None):
        MIMEText.__init__(self, _text, _subtype, 'us-ascii')
        # Only set the charset paraemeter to non-ASCII if the body
        # includes unprintable characters
        if notascii2.search(_text):
            self.set_param('charset', _charset)


def encode_if_needed(text, charset, encoding='q'):
    needed = False

    if notascii.search(text):
        # Fall back on something vaguely sensible if there are high chars
        # and the encoding is us-ascii
        if charset == 'us-ascii':
            charset = 'iso-8859-15'
        return Header(text, charset)
    else:
        return Header(text, 'us-ascii')


def rfc2047_encode_address(addr, charset, mua=None):
    newlist = []
    addresses = rfc822.AddressList(addr).addresslist
    for (realname, address) in addresses:
        if realname:
            newlist.append( email.Utils.formataddr(
                (str(rfc2047_encode_header(realname, charset, mua)), address)))
        else:
            newlist.append( address )
    return ', '.join(newlist)


def rfc2047_encode_header(header, charset, mua=None):
    if mua: return header
    #print repr(header), repr(charset)

    return encode_if_needed(header, charset)


def mail_info():
    to_addrs, from_addr, cc_addrs = 'xen-devel@lists.xen.org', None, None
    msg = '''This mail will be sent to Xen-devel (xen-devel@lists.xen.org) by default.
Do you want to send it to different address?'''
    if yes(msg, default_to_y=False):
        to_addrs = raw_input("Please enter address(es): ")
        while len(to_addrs) == 0:
            print "Receiver address(es) cannot be empty, try again please."
            to_addrs = raw_input("Please enter address: ")
    to_addrs = rfc2047_encode_address(to_addrs, 'us-ascii')

    from_addr = raw_input("Your email address: ")
    while len(from_addr) == 0:
        print "Sender address cannot be empty, try again please."
        from_addr = raw_input("Your email address: ")
    from_addr = rfc2047_encode_address(from_addr, 'us-ascii')

    cc_addrs = raw_input("Cc this mail to: ")
    if len(cc_addrs) != 0:
        cc_addrs = rfc2047_encode_address(cc_addrs, 'us-ascii')

    subject = raw_input("Subject of your bug report (please be concise): ")
    while len(subject) == 0:
        print "Subject cannot be empty, try again please."
        subject = raw_input('Subject of your bug report (please be concise, better prefix it with "[BUG] "): ')

    return subject, from_addr, to_addrs, cc_addrs


def assemble_mail(subject, from_addr, to_addrs, cc_addrs, body, attachments,
                  tmpdir):
    mimetypes.init()

    message = MIMEMultipart('mixed')
    message['Subject'] = subject
    message['To'] = to_addrs
    message['From'] = from_addr
    if len(cc_addrs) != 0: message['Cc'] = cc_addrs
    message.preamble = 'Xen-bug preamble\n'
    message.epilogue = 'Xen-bug epilogue\n'

    body = BetterMIMEText(body)
    body.add_header('Content-Disposition', 'inline')
    message.attach(body)

    for fn in attachments:
        ctype = None
        cset = None
        info = subprocess.Popen(['file', '--mime', '--brief', fn],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT).communicate()[0]
        if info:
            match = re.match(r'([^;, ]*)(,[^;]+)?(?:; )?(.*)', info)
            if match:
                ctype, junk, extras = match.groups()
                match = re.search(r'charset=([^,]+|"[^,"]+")', extras)
                if match:
                    cset = match.group(1)
                # No MIME type, fall back
                if '/' not in ctype:
                    ctype = None

        # If `file` does not work, try to guess based on file ext
        if not ctype:
            ctype, encoding = mimetypes.guess_type(fn, strict=False)

        # OK, all failed
        if not ctype:
            ctype = 'application/octet-stream'

        maintype, subtype = ctype.split('/', 1)

        with open(fn, 'rb') as f:
            content = f.read()

        if maintype == 'text':
            part = BetterMIMEText(content, _subtype=subtype,
                                  _charset=cset)
        elif maintype == 'message':
            part = MIMEMessage(email.message_from_string(content),
                               _subtype=subtype)
        elif maintype == 'image':
            part = MIMEImage(content, _subtype=subtype)
        elif maintype == 'audio':
            part = MIMEAudio(content, _subtype=subtype)
        else:
            part = MIMEBase(maintype, subtype)
            part.set_payload(content)
            email.Encoders.encode_base64(part)

        part.add_header('Content-Disposition', 'attachment',
                        filename=fn.split('/')[-1])
        message.attach(part)

    message['Message-ID'] = email.Utils.make_msgid('xen-bug')
    message['Date'] = email.Utils.formatdate(localtime=True)

    with tempfile.NamedTemporaryFile(mode='w',
                                     prefix='xen-bug-email-',
                                     dir=tmpdir,
                                     delete=False) as f:
        f.write(message.as_string())
        print "Email saved to", f.name

    return message


def send_email(message):

    use_ssl = yes("Use SMTP SSL?", default_to_y=False)

    host = raw_input("Host: ")
    while len(host) == 0:
        host = raw_input("Host: ")
    try:
        port = int(raw_input("Port (default 25 / 465): "))
    except:
        port = use_ssl and 465 or 25
    print "Using port", port

    addrs = [str(x) for x in (message.get_all('To', []) +
                              message.get_all('Cc', []) +
                              message.get_all('Bcc', []))]
    from_addr = message.get_all('From', [])
    if len(from_addr) == 0:
        print "Huh? empty from addr?"
        sys.exit(1)

    alist = email.Utils.getaddresses(addrs)

    message = message.as_string()

    to_addrs = [x[1] for x in alist]
    smtp_message = re.sub(r'(?m)^[.]', '..', message)

    try:
        if use_ssl:
            server = smtplib.SMTP_SSL(host, port)
        else:
            server = smtplib.SMTP(host, port)

        if yes("Server requires authentication?", must_choose=True):
            user = raw_input("Username: ")
            password = getpass.getpass("Password: ")
            server.login(user, password)

        server.sendmail(from_addr, to_addrs, smtp_message)

        server.quit()

    except Exception, exn:
        print exn
        print "\nFailed to interact with server, please find all files in system temporary directory"
        sys.exit(1)

    print "Done sending email"


def yes(prompt, default_to_y=True, must_choose=False):
    msg = prompt + (default_to_y and " [Y/n] " or " [y/N] ")
    if must_choose:
        msg = prompt + " [y/n] "

    answer = raw_input(msg)
    while len(answer) == 0 and must_choose:
        answer = raw_input(msg)

    if len(answer) == 0:
        return default_to_y

    return answer.lower()[0] == 'y'


def no(prompt, default_to_y=True, must_choose=False):
    return not yes(prompt, default_to_y, must_choose)


def collect_info(toolstack, tmpdir):
    commands_to_run = [['xenstore-ls', '-fp'],
                       [toolstack, 'info']]

    file_list = []

    if no("""Do you want to collect information from Xen toolstack?"""):
        return file_list

    try:
        for cmd in commands_to_run:
            fn = '-'.join(cmd)
            child = subprocess.Popen(cmd, stdout=subprocess.PIPE)
            output, err = child.communicate()
            with tempfile.NamedTemporaryFile(mode='w',
                                             prefix='xen-bug-' + fn + '-',
                                             dir=tmpdir,
                                             delete=False) as f:
                f.write(output)
                file_list.append(f.name)
                print "Output of", ' '.join(cmd), "saved in", f.name
    except Exception, exn:
        print "Failed to run command", ' '.join(cmd)
        print exn
        sys.exit(1)

    return file_list


def collect_files(tmpdir):
    file_list = []

    if yes("Do you want to send system log files?"):
        for f in FILES_TO_SEND:
            if not os.path.exists(f):
                continue
            if yes("Include %s?" % f):
                fn = f.split('/')[-1]
                dst_f = '/'.join([tmpdir, fn])
                shutil.copyfile(f, dst_f)
                file_list.append(dst_f)

    msg = '''Do you want to include other files?
Interesting files might be:
   * toolstack output like "xl -vvv COMMAND"
   * QEMU log
   * DomU console output
   * DomU config file
   * ...
'''
    if yes(msg):
        msg = "File to include (full path), empty line to finish: "
        f = raw_input(msg)
        while f != '':
            if not os.path.exists(f):
                print "File %s doesn't exist" % f
            else:
                fn = f.split('/')[-1]
                dst_f = '/'.join([tmpdir, fn])
                if dst_f in file_list:
                    print "File %s already in list" % f
                else:
                    shutil.copyfile(f, dst_f)
                    file_list.append(dst_f)
            f = raw_input(msg)
    return file_list


def list_files(file_list):
    if len(file_list) == 0:
        print "No files included"
    else:
        print "Files to be submitted:"
        for f in file_list:
            print "  *", f


def create_tmp_dir():
    return tempfile.mkdtemp(prefix="xen-bug-")


def hello():
    print "Xen-bug: simple tool to collect files for sending bug report against Xen"


def goodbye():
    print "Thank you for reporting bug against Xen, bye"


def main():
    files_to_send = []

    hello()

    toolstack = xen_toolstack()
    tmpdir = create_tmp_dir()

    files_to_send += collect_info(toolstack, tmpdir)
    files_to_send += collect_files(tmpdir)
    list_files(files_to_send)

    bug_report = create_bug_report(tmpdir)

    if yes("Do you want xen-bug to generate email for you?"):
        with open(bug_report, 'r') as f:
            body = f.read()

            subject, from_addr, to_addrs, cc_addrs = mail_info()
            message = assemble_mail(subject, from_addr, to_addrs,
                                    cc_addrs, body,
                                    files_to_send,
                                    tmpdir)

        if yes("Do you want to send bug report via email?"):
            send_email(message)

    print "You can find all collected files in", tmpdir

    goodbye()


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print "\nInterrupted"
        sys.exit(1)
