#!/usr/bin/env python # # Author: Scott Haskell # Date: 08/30/2008 # Modified: 10/30/2009 # License: This is free software. You can re-distribuite it and/or modify # it under the terms of the GNU General Public License version 2 (GPLv2), as # published by the Free Software Foundation. # # Description: Script that reads body of Zenoss event email, passed by Procmail # to STDIN. Script attempts to find what state the event is in # (0 - new, 1 - acknowledged, 2 - suppressed) and acts accordingly. # # For detailed installation and configuration, please visit: # http://www.zenoss.com/Members/shaskell/email-ack/ \ # email-acknowledgement-postfix-procmail-and-python # import Globals import sys import os import string import re import commands import string import signal import email from Products.ZenUtils.ZenScriptBase import ZenScriptBase # Update for outgoing email MAIL = "/usr/sbin/sendmail -t " server_from_address = "zenoss@server.mydomain.com" cc_address = "" # Debug flag DEBUG = 0 # Alarm Handler timeout (in seconds) TIMEOUT = 15 # Alarm handler def handler(signum, frame): print 'Signal handler called with', signum raise IOError, "Operation timed out" sys.exit(255) # Zenackevent Class - borrowed from # http://dev.zenoss.com/trac/browser/trunk/Products/ZenEvents/zenackevents.py class zenackevents(ZenScriptBase): def ack(self, state=1, evids=(), userid=""): self.dmd.ZenEventManager.manage_setEventStates(state, evids, userid) # Parse email headers and save payload # Argument(s): None # Return: # 1) Email Payload # 2) From Address def parse_email(): payload = None m = email.message_from_file(sys.stdin) # Debug Email if(DEBUG): keys = m.keys() for key in keys: print("%s: %s") % (key, m[key]) # Get From Address try: from_address = m['From'] except: print "Can't parse From Address in Email" # Check for multipart message if(m.is_multipart()): # Save message instances and loop sub_parts = m.get_payload() # Get 'text/plain' instance for sub_part in sub_parts: if(sub_part.get_content_type() == "text/plain"): # save payload payload = sub_part.get_payload(decode=True) else: # save payload payload = m.get_payload(decode=True) if(DEBUG): print "DEBUG --" print payload print "-- DEBUG" if(not payload): print "Could not successfully get email payload - Exiting" sys.exit(1) return(payload, from_address) # Subroutine to extract email addres, device and event id from body # of email. # Argument(s): # 1) string - email (header and body as single string) # Return: # Dictionary that contains: # 1) email address of acknowledger # 2) eventid:device def parse(payload): events = {} evid = None dev = None # Save Device d = re.search(r'Device: (.*)', payload) try: dev = d.group(1) except: print "Could not parse Device Name -- Setting to Unknown" # Save Event ID # From what I can tell it's hex and digits repeating (at least 20 times) # I did an re.findall with this regex and it matched all instances of the # event ID in the email. e = re.findall(r'([a-f0-9-]{20,})', payload) if(e): evid = parse_evids(e) if(not evid): print "Could not parse Event ID - Exiting" sys.exit(1) if(dev): events[evid] = dev else: events[evid] = "Unknown" return(events) # It should match all evids, but if some random string gets detected in the # regex, take the highest occurance of what it believes to be the evid. # Arguments: # 1) dictionary - Event ID's # Returns: # 1) string - parsed event ID for ack def parse_evids(evids): all_evids = {} # initialize and increment evid occurances for v in evids: if(not all_evids.has_key(v)): all_evids[v] = 1 else: all_evids[v] += 1 k = all_evids.keys() # sort all evid keys by number of occurances if(len(all_evids) > 1): k.sort(key = all_evids.__getitem__) evid = k.pop() return(evid) # Subroutine to email that event was ack'd # Arguments: # 1) string - email address # 2) string - status message def email_ack(to_address, status, dev, evid, user, event_summary): p = os.popen(MAIL, 'w') p.write("From: %s\n" % (server_from_address)) p.write("To: %s\n" % (to_address)) if(cc_address): p.write("Cc: %s\n" % (cc_address)) p.write("Content-Type: text/plain\n") p.write("Subject: ACK: %s on %s\n\n" % (event_summary, dev)) p.write("%s\n" % status) exitcode = p.close() if exitcode: print "Exit code: %s" % exitcode # Subroutine to acknowledge event # Argument(s): # 1) zae object # 2) dictionary - events dictionary def ack_event(zae, events, from_address): user = None status = None # save date for logging cmd = "/bin/date" date = commands.getoutput(cmd) # extract user from email address try: u = re.search(r'(\w+)@.*', from_address) user = u.group(1) except: user = "admin" # loop through events dictionary and ack event(s) for k, v in events.iteritems(): state = None evids = [] evid = k dev = v try: # save event state; 0 == new, 1 == ack, 2 == suppressed event_summary = zae.dmd.ZenEventManager.getEventDetail(evid).summary state = int(zae.dmd.ZenEventManager.getEventDetail(evid).eventState) if(state == 0): try: # append to list, because manage_setEventStates expects list evids.append(evid) zae.ack(evids=evids, userid=user) status = "%s - Acknowledged evid: %s on %s by %s" \ % (date, evid, dev, user) # send confirmation email to acknowledger email_ack(from_address, status, dev, evid, user, event_summary) except: status = "%s - ERROR: Could not ack evid: %s on %s" \ % (date, evid, dev) if(state == 1): status = "%s - evid: %s on %s has already been acknowledged" \ % (date, evid, dev) if(state == 2): status = "%s - evid: %s on %s has been suppressed" \ % (date, evid, dev) except: if(not state): status = "could not get event state on event id %s - Exiting" \ % (evid) sys.exit(1) else: status = "%s - Eventid %s on %s has already cleared or \ has been moved to history" % (date, evid, dev) # log status message print status if __name__ == '__main__': events = {} zae = zenackevents(connect=True) # Set alarm handler and alarm signal.signal(signal.SIGALRM, handler) signal.alarm(TIMEOUT) (payload, from_address) = parse_email() events = parse(payload) ack_event(zae, events, from_address) # disable alarm signal.alarm(0) sys.exit(0)