Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 479 lines (416 sloc) 18.391 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_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)
Jump to Line
Something went wrong with that request. Please try again.