From 5033042b8cdb14b3384affee3c29e43ca8e3803b Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 2 Jan 2016 17:00:51 -0500 Subject: [PATCH] backups: email the administrator when there's a problem Refactor by moving the email-the-admin code out of the status checks and into a new separate tool. This is why I suppressed non-error output of the backups last commit - so it doesn't send a daily email. --- CHANGELOG.md | 4 +++ management/daily_tasks.sh | 4 +-- management/email_administrator.py | 42 ++++++++++++++++++++++++ management/status_checks.py | 54 ++++++++++++------------------- 4 files changed, 68 insertions(+), 36 deletions(-) create mode 100755 management/email_administrator.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6efd0..34f6442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Control Panel: * Report free memory usage. +System: + +* The daily backup will now email the administrator if there is a problem. + v0.15 (January 1, 2016) ----------------------- diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index 6237e6c..69d14c7 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -2,7 +2,7 @@ # This script is run daily (at 3am each night). # Take a backup. -management/backup.py +management/backup.py | management/email_administrator.py "Backup Status" # Run status checks and email the administrator if anything changed. -management/status_checks.py --show-changes --smtp +management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice" diff --git a/management/email_administrator.py b/management/email_administrator.py new file mode 100755 index 0000000..84d2746 --- /dev/null +++ b/management/email_administrator.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +# Reads in STDIN. If the stream is not empty, mail it to the system administrator. + +import sys + +import smtplib +from email.message import Message + +from utils import load_environment + +# Load system environment info. +env = load_environment() + +# Process command line args. +subject = sys.argv[1] + +# Administrator's email address. +admin_addr = "administrator@" + env['PRIMARY_HOSTNAME'] + +# Read in STDIN. +content = sys.stdin.read().strip() + +# If there's nothing coming in, just exit. +if content == "": + sys.exit(0) + +# create MIME message +msg = Message() +msg['From'] = "\"%s\" <%s>" % (env['PRIMARY_HOSTNAME'], admin_addr) +msg['To'] = admin_addr +msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject) +msg.set_payload(content, "UTF-8") + +# send +smtpclient = smtplib.SMTP('localhost', 25) +smtpclient.ehlo() +smtpclient.sendmail( + admin_addr, # MAIL FROM + admin_addr, # RCPT TO + msg.as_string()) +smtpclient.quit() diff --git a/management/status_checks.py b/management/status_checks.py index a7e4a62..f7578dd 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -767,15 +767,11 @@ def check_miab_version(env, output): output.print_error("A new version of Mail-in-a-Box is available. You are running version %s. The latest version is %s. For upgrade instructions, see https://mailinabox.email. " % (this_ver, latest_ver)) -def run_and_output_changes(env, pool, send_via_email): +def run_and_output_changes(env, pool): import json from difflib import SequenceMatcher - if not send_via_email: - out = ConsoleOutput() - else: - import io - out = FileOutput(io.StringIO(""), 70) + out = ConsoleOutput() # Run status checks. cur = BufferedOutput() @@ -834,28 +830,6 @@ def run_and_output_changes(env, pool, send_via_email): out.add_heading(category) out.print_warning("This section was removed.") - if send_via_email: - # If there were changes, send off an email. - buf = out.buf.getvalue() - if len(buf) > 0: - # create MIME message - from email.message import Message - msg = Message() - msg['From'] = "\"%s\" " % (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME']) - msg['To'] = "administrator@%s" % env['PRIMARY_HOSTNAME'] - msg['Subject'] = "[%s] Status Checks Change Notice" % env['PRIMARY_HOSTNAME'] - msg.set_payload(buf, "UTF-8") - - # send to administrator@ - import smtplib - mailserver = smtplib.SMTP('localhost', 25) - mailserver.ehlo() - mailserver.sendmail( - "administrator@%s" % env['PRIMARY_HOSTNAME'], # MAIL FROM - "administrator@%s" % env['PRIMARY_HOSTNAME'], # RCPT TO - msg.as_string()) - mailserver.quit() - # Store the current status checks output for next time. os.makedirs(os.path.dirname(cache_fn), exist_ok=True) with open(cache_fn, "w") as f: @@ -886,7 +860,7 @@ class FileOutput: words = re.split("(\s+)", message) linelen = 0 for w in words: - if linelen + len(w) > self.width-1-len(first_line): + if self.width and (linelen + len(w) > self.width-1-len(first_line)): print(file=self.buf) print(" ", end="", file=self.buf) linelen = 0 @@ -902,10 +876,22 @@ class FileOutput: class ConsoleOutput(FileOutput): def __init__(self): self.buf = sys.stdout - try: - self.width = int(shell('check_output', ['stty', 'size']).split()[1]) - except: - self.width = 76 + + # Do nice line-wrapping according to the size of the terminal. + # The 'stty' program queries standard input for terminal information. + if sys.stdin.isatty(): + try: + self.width = int(shell('check_output', ['stty', 'size']).split()[1]) + except: + self.width = 76 + + else: + # However if standard input is not a terminal, we would get + # "stty: standard input: Inappropriate ioctl for device". So + # we test with sys.stdin.isatty first, and if it is not a + # terminal don't do any line wrapping. When this script is + # run from cron, or if stdin has been redirected, this happens. + self.width = None class BufferedOutput: # Record all of the instance method calls so we can play them back later. @@ -933,7 +919,7 @@ if __name__ == "__main__": run_checks(False, env, ConsoleOutput(), pool) elif sys.argv[1] == "--show-changes": - run_and_output_changes(env, pool, sys.argv[-1] == "--smtp") + run_and_output_changes(env, pool) elif sys.argv[1] == "--check-primary-hostname": # See if the primary hostname appears resolvable and has a signed certificate.