#!/usr/bin/env python
#
# mail-alarm: uses ssmtp to send a mail message, to pool:other_config:mail-destination
#
# If /etc/mail-alarm.conf exists then it is used as the ssmtp config.
# However, this script first replaces any macros with keys from pool:other-config.
# For example, if /etc/mail-alarm.conf contains the text @MYMACRO@ then it will
# be replaced by pool:other-config:ssmtp-mymacro
#
# If /etc/mail-alarm.conf does not exist the default_config string below is used and
# the only thing that needs be set is pool:other-config:ssmtp-mailhub

import XenAPI
import sys
import os
import tempfile
import traceback
import syslog
from xml.dom import minidom
from xml.sax.saxutils import unescape
from xml.parsers.expat import ExpatError
from socket import getfqdn

# Go read man ssmtp.conf
default_config="""
mailhub=@MAILHUB@
FromLineOverride=YES
"""

ma_username="__dom0__mail_alarm"

def log_err(err):
    print >>sys.stderr, err
    syslog.syslog(syslog.LOG_USER | syslog.LOG_ERR, "%s: %s" % (sys.argv[0], err))

def get_pool_name():
    session = XenAPI.xapi_local()
    session.xenapi.login_with_password(ma_username, "")
    try:
        opaque_ref = session.xenapi.pool.get_all()[0]
        pool_name = session.xenapi.pool.get_name_label(opaque_ref)
        if pool_name == "":
            master_ref = session.xenapi.pool.get_master(opaque_ref)
            master_name = session.xenapi.host.get_name_label(master_ref)
            return master_name
        else:
            return pool_name
    finally:
        session.xenapi.session.logout()

def get_pool_other_config():
    session = XenAPI.xapi_local()
    session.xenapi.login_with_password(ma_username, "")
    try:
        opaque_ref = session.xenapi.pool.get_all()[0]
        return session.xenapi.pool.get_other_config(opaque_ref)
    finally:
        session.xenapi.session.logout()

def get_vmpp_alarm_config(uuid):
    session = XenAPI.xapi_local()
    session.xenapi.login_with_password(ma_username, "")
    try:
        opaque_ref = session.xenapi.VMPP.get_by_uuid(uuid)
        vmpp_alarm_config = session.xenapi.VMPP.get_alarm_config(opaque_ref)
        vmpp_is_alarm_enabled = session.xenapi.VMPP.get_is_alarm_enabled(opaque_ref)
        try:
            vmpp_smtp_server=vmpp_alarm_config['smtp_server']
            vmpp_smtp_port=vmpp_alarm_config['smtp_port']
            vmpp_email_address=vmpp_alarm_config['email_address']
        except:
            log_err("VMPP uuid=%s: not sending email alert due to incomplete configuration" % uuid)
            sys.exit(1)
        other_config = {'ssmtp-mailhub':"%s %s" % (vmpp_smtp_server,vmpp_smtp_port),'mail-destination':vmpp_email_address}
        return vmpp_is_alarm_enabled,other_config
    finally:
        session.xenapi.session.logout()

def get_VM_params(uuid):
    session = XenAPI.xapi_local()
    session.xenapi.login_with_password(ma_username, "")
    try:
        try:
            opaque_ref = session.xenapi.VM.get_by_uuid(uuid)
            return session.xenapi.VM.get_record(opaque_ref)
        finally:
            session.xenapi.session.logout()
    except:
        return {}

def get_host_params(uuid):
    session = XenAPI.xapi_local()
    session.xenapi.login_with_password(ma_username, "")
    try:
        try:
            opaque_ref = session.xenapi.host.get_by_uuid(uuid)
            return session.xenapi.host.get_record(opaque_ref)
        finally:
            session.xenapi.session.logout()
    except:
        return {}

def get_search_replace(other_config):
    sr = []
    for key in other_config: 
        if key.startswith('ssmtp-'):
            replacement_text = other_config[key]
            search_text = "@" + key[6:].upper() + "@"
            sr.append((search_text, replacement_text))
    return sr

def get_destination(other_config):
    if other_config.has_key('mail-destination'):
        return other_config['mail-destination']

def get_config_file():
    try:
        return open('/etc/mail-alarm.conf').read()
    except:
        return default_config

class EmailTextGenerator:
    pass

class CpuUsageAlarmETG(EmailTextGenerator):
    def __init__(self, cls, obj_uuid, value, alarm_trigger_period, alarm_trigger_level):
        if not alarm_trigger_period: alarm_trigger_period = 60
        if cls == 'Host':
            self.params = get_host_params(obj_uuid)
        elif cls == 'VM':
            self.params = get_VM_params(obj_uuid)
        else:
            raise Exception, "programmer error"
        self.cls = cls
        self.value = value
        self.alarm_trigger_period = alarm_trigger_period
        self.alarm_trigger_level = alarm_trigger_level

    def generate_subject(self):
        pool_name = get_pool_name()
        return '[%s] XenServer Alarm: CPU usage on %s "%s"' % (pool_name, self.cls, self.params['name_label'])
    
    def generate_body(self):
        return \
            'CPU usage on %s "%s" has been on average %.1f%% for the last %d seconds.\n' \
            'This alarm is set to be triggered when CPU usage is more than %.1f%%.\n' \
            '\n' \
            'For Alarm Settings, please log into your XenCenter Console and click on "%s"->\n' \
            '"Properties"->"Alerts"\n' % \
            (self.cls, 
             self.params['name_label'],
             self.value * 100.0,
             self.alarm_trigger_period,
             self.alarm_trigger_level * 100.0, 
             (self.cls == 'Host') and 'Server' or 'VM')

class NetworkUsageAlarmETG(EmailTextGenerator):
    def __init__(self, cls, obj_uuid, value, alarm_trigger_period, alarm_trigger_level):
        if not alarm_trigger_period: alarm_trigger_period = 60
        if cls == 'Host':
            self.params = get_host_params(obj_uuid)
        elif cls == 'VM':
            self.params = get_VM_params(obj_uuid)
        else:
            raise Exception, "programmer error"
        self.cls = cls
        self.value = value
        self.alarm_trigger_period = alarm_trigger_period
        self.alarm_trigger_level = alarm_trigger_level

    def generate_subject(self):
        pool_name = pool_name = get_pool_name()
        return '[%s] XenServer Alarm: Network usage on %s "%s"' % (pool_name, self.cls, self.params['name_label'])
    
    def generate_body(self):
        return \
            'Network usage on %s "%s" has been on average %d B/s for the last %d seconds.\n' \
            'This alarm is set to be triggered when Network usage is more than %d B/s.\n' \
            '\n' \
            'For Alarm Settings, please log into your XenCenter Console and click on "%s"->\n' \
            '"Properties"->"Alerts"\n' % \
            (self.cls, 
             self.params['name_label'],
             self.value,
             self.alarm_trigger_period,
             self.alarm_trigger_level, 
             (self.cls == 'Host') and 'Server' or 'VM')

class DiskUsageAlarmETG(EmailTextGenerator):
    def __init__(self, cls, obj_uuid, value, alarm_trigger_period, alarm_trigger_level):
        if not alarm_trigger_period: alarm_trigger_period = 60
        if cls != 'VM':
            raise Exception, "programmer error - this alarm should only be available for VMs"
        self.params = get_VM_params(obj_uuid)
        self.cls = cls
        self.value = value
        self.alarm_trigger_period = alarm_trigger_period
        self.alarm_trigger_level = alarm_trigger_level

    def generate_subject(self):
        pool_name = get_pool_name()
        return '[%s] XenServer Alarm: Disk usage on VM "%s"' % (pool_name, self.params['name_label'])
    
    def generate_body(self):
        return \
            'Disk usage on VM "%s" has been on average %d B/s for the last %d seconds.\n' \
            'This alarm is set to be triggered when Disk usage is more than %d B/s.\n' \
            '\n' \
            'For Alarm Settings, please log into your XenCenter Console and click on "VM"->\n' \
            '"Properties"->"Alerts"\n' % \
            (self.params['name_label'],
             self.value,
             self.alarm_trigger_period,
             self.alarm_trigger_level)

class Dom0FSUsageAlarmETG(EmailTextGenerator):
    def __init__(self, cls, obj_uuid, value, alarm_trigger_level):
        if not alarm_trigger_level: alarm_trigger_level = 0.9
        if cls != 'VM':
            raise Exception, "programmer error - this alarm should only be available for control domain VM"
        self.params = get_VM_params(obj_uuid)
        self.cls = cls
        self.value = value
        self.alarm_trigger_level = alarm_trigger_level

    def generate_subject(self):
        pool_name = get_pool_name()
        return '[%s] XenServer Alarm: Filesystem nearly full on "%s"' % (pool_name, self.params['name_label'])
    
    def generate_body(self):
        return \
            'The filesystem usage on "%s" is at %.1f%%.\n' \
            'This alarm is set to be triggered when filesystem usage is more than %.1f%%.\n' \
            '\n' % \
            (self.params['name_label'],
             self.value * 100.0,
             self.alarm_trigger_level * 100.0)

class WlbConsultationFailure(EmailTextGenerator):
    def __init__(self, cls, obj_uuid):
        self.cls = cls
        self.params = get_VM_params(obj_uuid)

    def generate_subject(self):
        pool_name = get_pool_name()
        return '[%s] XenServer Alarm: Attempt to consult wlb for VM "%s" failed' % (self.params['name_label'], pool_name)
    
    def generate_body(self):
        return \
            'A workload balancing consultation for VM %s failed.\n' \
            'The operation was completed using the default algorithm instead of a workload balancing recommendation.\n' \
            '\n' % \
            (self.params['name_label'])
            
class WlbOptimizationAlert(EmailTextGenerator):
    def __init__(self, optimization_mode, severity):
        self.optimization_mode = optimization_mode
        self.severity = severity
        self.pool_name = pool_name = get_pool_name()

    def generate_subject(self):
        return 'Workload Balancing Alert: Optimization alert from pool %s' % (self.pool_name)
    
    def generate_body(self):
        return \
            'The Workload Balancing server has reported that pool %s is in need of optimization.\n' \
            '%s is in optimization mode %s and is in a %s state.\n' \
            '\n' % \
            (self.pool_name,
             self.pool_name,
             self.optimization_mode,
             self.severity)
            

class HAHostFailedETG(EmailTextGenerator):
    def __init__(self, text):
        self.text = text

    def generate_subject(self):
        pool_name = get_pool_name()
        return '[%s] XenServer HA Alarm: %s' % (pool_name, self.text)
    
    def generate_body(self):
        return \
            '%s\n' \
            '\n' \
            'This alarm is set to be triggered when a host belonging to a high availability pool fails.' \
            '\n' % self.text

class VmppETG(EmailTextGenerator):
    def __init__(self, msg):
        self.msg = msg

    def generate_subject(self):
        msg = self.msg
        return "[%s] XenServer Message: %s %s %s" % (msg.pool_name, msg.cls, msg.obj_uuid, msg.name)

    def generate_body(self):
        msg = self.msg
        msg_body = unescape(msg.body)
        try:
            xmldoc = minidom.parseString(msg_body)
            body_message = xmldoc.getElementsByTagName('message')[0]
            email_message = body_message.getElementsByTagName('email')[0].firstChild.data
            return \
            "Field\t\tValue\n-----\t\t-----\nName:\t\t%s\nPriority:\t%s\nClass:\t\t%s\n" \
            "Object UUID:\t%s\nTimestamp:\t%s\nMessage UUID:\t%s\nPool name:\t%s\nBody:\t\t%s\n" % \
            (msg.name,msg.priority,msg.cls,msg.obj_uuid,msg.timestamp,msg.uuid,msg.pool_name,email_message)
        except:
            log_err("Badly formatted XML, or missing field")
            sys.exit(1)

class XapiMessage:
    def __init__(self, xml):
        "Parse message XML"
        try:
            xmldoc = minidom.parseString(xml)
            def get_text(tag):
                return xmldoc.getElementsByTagName(tag)[0].firstChild.toxml()
            self.name      = get_text('name')
            self.priority  = get_text('priority')
            self.cls       = get_text('cls')
            self.obj_uuid  = get_text('obj_uuid')
            self.timestamp = get_text('timestamp')
            self.uuid      = get_text('uuid')
        except:
            log_err("Badly formatted XML, or missing field")
            sys.exit(1)
        try:
            self.body      = get_text('body')
        except:
            self.body      = ""
        self.pool_name = get_pool_name()

    def get_priority(self):
        return int(self.priority)

    def get_cls(self):
        return self.cls

    def get_obj_uuid(self):
        return self.obj_uuid

    def __get_email_text_generator(self):
        """Returns an EmailTextGenerator object appropriate to this XapiMessage or None if none found"""
        if hasattr(self,'cached_etg'):
            return self.cached_etg

        if self.cls == 'VMPP':
            etg = VmppETG(self)
        elif self.name == 'ALARM':
            # Extract the current level of the variable
            # (this will raise an exception if the 1st line of <body> is not in the correct format, namely "value: %f\n")
            value_line = self.body.split("\n",2)[0]
            key, val = value_line.split(':', 2)
            assert(key == 'value')
            value = float(val)

            # Extract a few key config elements
            config_xml_escaped = self.body.split("config:")[1]
            config_xml = config_xml_escaped.replace('&gt;','>').replace('&lt;','<').replace('&quot;','"')
            config_xmldoc = minidom.parseString(config_xml)
            def get_alarm_config(tag, cast):
                try:   return cast(config_xmldoc.getElementsByTagName(tag)[0].getAttribute('value'))
                except:return None
            name                  = get_alarm_config('name',str)
            alarm_trigger_level   = get_alarm_config('alarm_trigger_level',float)
            alarm_trigger_period  = get_alarm_config('alarm_trigger_period',int)
            
            # Set the alarm text generator
            if name == 'cpu_usage':
                etg = CpuUsageAlarmETG(self.cls, self.obj_uuid, value, alarm_trigger_period, alarm_trigger_level)
            elif name == 'network_usage':
                etg = NetworkUsageAlarmETG(self.cls, self.obj_uuid, value, alarm_trigger_period, alarm_trigger_level)
            elif name == 'disk_usage':
                etg = DiskUsageAlarmETG(self.cls, self.obj_uuid, value, alarm_trigger_period, alarm_trigger_level)
            elif name == 'fs_usage':
                etg = Dom0FSUsageAlarmETG(self.cls, self.obj_uuid, value, alarm_trigger_level)
            else:
                etg = None
        elif self.name == 'HA_HOST_FAILED':
            etg = HAHostFailedETG(self.body)
        elif self.name == 'WLB_CONSULTATION_FAILED':
            etg = WlbConsultationFailure(self.cls, self.obj_uuid)
        elif self.name == 'WLB_OPTIMIZATION_ALERT':
            severity_line = self.body.split()[0]
            severity = str(severity_line.split('severity:')[1])
            mode_line = self.body.split()[1]
            optimization_mode = str(mode_line.split('mode:')[1])          
            etg = WlbOptimizationAlert(optimization_mode, severity)
        else:
            etg = None

        self.cached_etg = etg
        return etg

    def generate_email_subject(self):
        generator = self.__get_email_text_generator()
        if generator:
            return generator.generate_subject()
        else:
            return "[%s] XenServer Message: %s %s %s" % (self.pool_name, self.cls, self.obj_uuid, self.name)
    
    def generate_email_body(self):
        generator = self.__get_email_text_generator()
        if generator:
            return generator.generate_body()
        else:
            return \
                "Field\t\tValue\n-----\t\t-----\nName:\t\t%s\nPriority:\t%s\nClass:\t\t%s\n" \
                "Object UUID:\t%s\nTimestamp:\t%s\nMessage UUID:\t%s\nPool name:\t%s\nBody:\t\t%s\n" % \
                (self.name,self.priority,self.cls,self.obj_uuid,self.timestamp,self.uuid,self.pool_name,self.body)


def main():
    other_config = get_pool_other_config()
    if other_config.has_key('mail-min-priority'):
        min_priority = int(other_config['mail-min-priority'])
    else:
        min_priority = 5

    msg = XapiMessage(sys.argv[1])

    # We only mail messages with level min_priority or higher
    if msg.get_priority() < min_priority:
        return 0

    if msg.get_cls() == "VMPP":
        config = default_config
        vmpp_is_alarm_enabled, other_config = get_vmpp_alarm_config(msg.get_obj_uuid())
        if not vmpp_is_alarm_enabled:
          return 0
    else:        
        config = get_config_file()

    search_replace = get_search_replace(other_config)
    destination = get_destination(other_config)

    if not destination:
        log_err("pool:other-config:mail-destination not specified")
        return 1

    # Replace macros in config file using search_replace list
    for s,r in search_replace:
        config = config.replace(s, r)

    # Write out a temporary file containing the new config
    fd, fname = tempfile.mkstemp(prefix="mail-", dir="/tmp")
    try:
        os.write(fd, config)
        os.close(fd)

        # Run ssmtp to send mail
        chld_stdin, chld_stdout = os.popen2(["/usr/sbin/ssmtp", "-C%s" % fname, destination])
        chld_stdin.write("From: noreply@%s\n" % getfqdn().encode('utf-8'))
        chld_stdin.write('Content-Type: text/plain; charset="utf-8"\n')
        chld_stdin.write("To: %s\n" % destination.encode('utf-8'))
        chld_stdin.write("Subject: %s\n" % msg.generate_email_subject().encode('utf-8'))
        chld_stdin.write("\n")
        chld_stdin.write(msg.generate_email_body().encode('utf-8'))
        chld_stdin.close()
        chld_stdout.close()
        os.wait()

    finally:
        os.unlink(fname)

if __name__ == '__main__':
    rc = 1
    try:
        rc = main()
    except:
        ex = sys.exc_info()
        err = traceback.format_exception(*ex)
        for exline in err:
            log_err(exline)

    sys.exit(rc)
