Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 429 lines (370 sloc) 16.211 kB
#!/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_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 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.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 = 3
msg = XapiMessage(sys.argv[1])
# We only mail messages with priority lower than or equal to max_priority
if msg.get_priority() > min_priority:
return 0
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)
Jump to Line
Something went wrong with that request. Please try again.