diff --git a/config.yaml b/config.yaml index 336ba14..f3295e1 100644 --- a/config.yaml +++ b/config.yaml @@ -1,3 +1,6 @@ -recipientList: -- natedwarshuis@gmail.com +gmail: + username: natedwarshuis@gmail.com + passwd: bwsasfxqjbookmed + recipientList: + - natedwarshuis@gmail.com state: disarmed diff --git a/main.py b/main.py index 8d262a4..1b80f8c 100755 --- a/main.py +++ b/main.py @@ -35,8 +35,6 @@ if __name__ == '__main__': GPIO.setmode(GPIO.BCM) resetUSBDevice('1-1') - from notifier import criticalError - stateMachine = StateMachine() # TODO: segfaults are annoying :( @@ -47,14 +45,7 @@ if __name__ == '__main__': time.sleep(31536000) except Exception: - t = traceback.format_exc() - - try: - criticalError(t) - except NameError: - pass - - logger.critical(t) + logger.critical(traceback.format_exc()) finally: clean() diff --git a/notifier.py b/notifier.py index 1600900..a85ea7d 100644 --- a/notifier.py +++ b/notifier.py @@ -5,60 +5,70 @@ from datetime import datetime from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) -COMMASPACE=', ' +gmail = ConfigFile('config.yaml')['gmail'] -RECIPIENT_LIST=ConfigFile('config.yaml')['recipientList'] - -GMAIL_USER='natedwarshuis@gmail.com' -GMAIL_PWD='bwsasfxqjbookmed' - -def getNextDate(): +def _getNextDate(): m = datetime.now().month + 1 y = datetime.now().year y = y + 1 if m > 12 else y return datetime(year=y, month=m%12, day=1, hour=12, minute=0) @async(daemon=True) -def scheduleAction(action): +def _scheduleAction(action): while 1: - nextDate = getNextDate() + nextDate = _getNextDate() sleepTime = nextDate - datetime.today() - logger.info('Next monthly test scheduled at %s (%s)', nextDate, sleepTime) + _logger.info('Next monthly test scheduled at %s (%s)', nextDate, sleepTime) time.sleep(sleepTime.days * 86400 + sleepTime.seconds) action() -# probably an easier way to do this in logging module @async(daemon=False) -def sendEmail(subject, body): +def _sendToGmail(username, passwd, recipiantList, subject, body, server='smtp.gmail.com', port=587): msg = MIMEMultipart() msg['Subject'] = subject - msg['From'] = GMAIL_USER - msg['To'] = COMMASPACE.join(RECIPIENT_LIST) + msg['From'] = username + msg['To'] = ', '.join(recipiantList) msg.attach(MIMEText(body, 'plain')) - s = SMTP('smtp.gmail.com', 587) + s = SMTP(server, port) s.starttls() - s.login(GMAIL_USER, GMAIL_PWD) + s.login(username, passwd) s.send_message(msg) s.quit() def monthlyTest(): subject = 'harrison4hegemon - automated monthly test' body = 'this is an automated message - please do not reply\n\nin the future this may have useful information' - sendEmail(subject, body) - logger.info('Sending monthly test to email list') + sendEmail(gmail['username'], gmail['passwd'], gmail['recipientList'], subject, body) + _logger.debug('Sending monthly test to email list') def intruderAlert(): subject = 'harrison4hegemon - intruder detected' body = 'intruder detected - alarm was tripped on ' + time.strftime("%H:%M:%S - %d/%m/%Y") - sendEmail(subject, body) - logger.info('Sending intruder alert to email list') - -def criticalError(err): - subject = 'harrison4hegemon - critical error' - sendEmail(subject, err) - logger.info('Sending critical error to email list') + sendEmail(gmail['username'], gmail['passwd'], gmail['recipientList'], subject, body) + _logger.info('intruder detected') + _logger.debug('Sending intruder alert to email list') -scheduleAction(monthlyTest) +class GmailHandler(logging.Handler): + ''' + Logging handler that sends records to gmail. This is almost like the + SMTPHandler except that the username and fromaddr are the same and + credentials are mandatory + ''' + def __init__(self, username, passwd, recipientList, subject): + super().__init__() + self.username = username + self.passwd = passwd + self.recipientList = recipientList + self.subject = subject + + def emit(self, record): + try: + _sendToGmail(self.username, self.passwd, self.recipientList, + self.subject, self.format(record)) + except: + self.handleError(record) + +_scheduleAction(monthlyTest) diff --git a/sharedLogging.py b/sharedLogging.py index d97edbd..e00a72d 100644 --- a/sharedLogging.py +++ b/sharedLogging.py @@ -1,33 +1,40 @@ import logging, os from subprocess import run, PIPE, CalledProcessError -from logging.handlers import TimedRotatingFileHandler +from logging.handlers import TimedRotatingFileHandler, SMTPHandler -""" +''' Logger conventions -- CRITICAL: for things that cause crashes. sends email +- CRITICAL: for things that cause crashes. only level with gmail - ERROR: for things that cause startup/shutdown issues - WARNING: for recoverable issues that may cause future problems - INFO: state changes and sensor readings - DEBUG: all extraneous crap -""" -# formats console output depending on whether we have gluster +Init order (very essential) +1) init console output (this will go to journald) and format as console only +2) init the module level _logger so we can log anything that happens as we build + the other loggers +3) mount glusterfs, any errors here will go to console output +4) once gluster is mounted, add to root _logger and remove "console only" warning + from console +5) import gmail, this must come here as it uses loggers for some of its setup +6) init gmail handler +''' + def _formatConsole(gluster = False): + ''' + formats console output depending on whether we have gluster + ''' c = '' if gluster else '[CONSOLE ONLY] ' fmt = logging.Formatter('[%(name)s] [%(levelname)s] ' + c + '%(message)s') console.setFormatter(fmt) -# init console, but don't expect gluster to be here yet -console = logging.StreamHandler() -_formatConsole(gluster = False) - -rootLogger = logging.getLogger() -rootLogger.setLevel(logging.DEBUG) -rootLogger.addHandler(console) - -logger = logging.getLogger(__name__) - class GlusterFSHandler(TimedRotatingFileHandler): + ''' + Logic to mount timed rotating file within a gluster volume. Note that this + class will mount itself automatically. Note that the actual filepaths for + logging are hardcoded here + ''' def __init__(self, server, volume, mountpoint, options=None): if not os.path.exists(mountpoint): raise FileNotFoundError @@ -42,7 +49,7 @@ class GlusterFSHandler(TimedRotatingFileHandler): if not os.path.exists(logdest): os.mkdir(logdest) elif os.path.isfile(logdest): - logger.error('%s is present but is a file (vs a directory). ' \ + _logger.error('%s is present but is a file (vs a directory). ' \ 'Please (re)move this file to prevent data loss', logdest) raise SystemExit @@ -55,8 +62,8 @@ class GlusterFSHandler(TimedRotatingFileHandler): def _mount(self): if os.path.ismount(self._mountpoint): - # NOTE: this assumes that the already-mounted device is the one intended - logger.warning('Device already mounted at {}'.format(self._mountpoint)) + # this assumes that the already-mounted device is the one intended + _logger.warning('Device already mounted at {}'.format(self._mountpoint)) else: dst = self._server + ':/' + self._volume cmd = ['mount', '-t', 'glusterfs', dst, self._mountpoint] @@ -72,14 +79,32 @@ class GlusterFSHandler(TimedRotatingFileHandler): run(cmd, check=True, stdout=PIPE, stderr=PIPE) except CalledProcessError as e: stderr = e.stderr.decode('ascii').rstrip() - logger.error(stderr) + _logger.error(stderr) raise SystemExit def close(self): - TimedRotatingFileHandler.close(self) # must close file stream before unmounting + ''' + Close file and dismount (must be in this order). Called when + 'removeHandler' is invoked + ''' + TimedRotatingFileHandler.close(self) self._unmount() -# ...now activate gluster +''' +Init sequence (see above) +''' +# 1 +console = logging.StreamHandler() +_formatConsole(gluster = False) + +rootLogger = logging.getLogger() +rootLogger.setLevel(logging.DEBUG) +rootLogger.addHandler(console) + +# 2 +_logger = logging.getLogger(__name__) + +# 3 gluster = GlusterFSHandler( server = '192.168.11.39', volume = 'pyledriver', @@ -87,10 +112,22 @@ gluster = GlusterFSHandler( options = 'backupvolfile-server=192.168.11.48' ) +# 4 _formatConsole(gluster = True) rootLogger.addHandler(gluster) -# this should only be called at the end to clean up +# 5 +from notifier import gmail, GmailHandler + +# 6 +gmail = GmailHandler(gmail['username'], gmail['passwd'], gmail['recipientList'], + 'harrison4hegemon - critical error') +gmail.setLevel(logging.CRITICAL) +rootLogger.addHandler(gmail) + +''' +Clean up +''' def unmountGluster(): rootLogger.removeHandler(gluster) _formatConsole(gluster = False)