diff --git a/.gitignore b/.gitignore index c0fde79..2446be6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ snapraid-runner.conf +oauth2_creds.json diff --git a/README.md b/README.md index ca28549..3bf98de 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Snapraid Runner Script +# Snapraid Runner Script - Yagmail Fork This script runs snapraid and sends its output to the console, a log file and via email. All this is configurable. @@ -8,10 +8,17 @@ scheduler. It supports Windows, Linux and macOS and requires at least python3.7. +**This is a fork** of the original script that supports sending from new **gmail accounts that can't enable smtp access**. Yagmail is used to authenticate via oauth2, this requires some manual setup of the sending account and of the oauth2 credentials on the first run of the script. + +[This](https://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html) shows how to setup your sending google account via the Google API Console. And [This](https://github.com/kootenpv/yagmail#oauth2) walks through the prompts that the console will ask about on the first run. + +This fork also adds an optional check on the number of modified files to abort if it is over a configurable threshold, this is a rudimentary method of stopping some ransomware. **This is a very poor method of ransomeware detection and mitigation**, offline and or immutable backups are highly suggested. + ## How to use * If you don’t already have it, download and install [the latest python version](https://www.python.org/downloads/). -* Download [the latest release](https://github.com/Chronial/snapraid-runner/releases) +* Install yagmail via pip if you are planning on using it +* Download [the latest release](https://github.com/AndyHegemann/snapraid-runner/releases) of this script and extract it anywhere or clone this repository via git. * Copy/rename the `snapraid-runner.conf.example` to `snapraid-runner.conf` and edit its contents. You need to at least configure `snapraid.executable` and @@ -20,23 +27,34 @@ It supports Windows, Linux and macOS and requires at least python3.7. `py -3 snapraid-runner.py` on Windows. ## Features -* Runs `diff` before `sync` to see how many files were deleted and aborts if +* Runs `diff` before `sync` to see how many files were deleted and or modified and aborts if that number exceeds a set threshold. * Can create a size-limited rotated logfile. * Can send notification emails after each run or only for failures. + * Can attach log after each run or only for failures * Can run `scrub` after `sync` ## Scope of this project and contributions -Snapraid-runner is supposed to be a small tool with clear focus. It should not +~Snapraid-runner is supposed to be a small tool with clear focus. It should not have any dependencies to keep installation trivial. I always welcome bugfixes and contributions, but be aware that I will not merge new features that I feel -do not fit the core purpose of this tool. +do not fit the core purpose of this tool.~ -I keep the PRs for features I do not plan on merging open, so if there's a +~I keep the PRs for features I do not plan on merging open, so if there's a feature you are missing, you can have a look -[at the open PRs](https://github.com/Chronial/snapraid-runner/pulls). +[at the open PRs](https://github.com/Chronial/snapraid-runner/pulls).~ + +I added features to the original that I wanted, please feel free to do the same to my fork. There is a very good chance I broke something, but if I'm not going to use it then I probably won't get around to fix it. PRs for fixes and features will probably get merged if you feel like opening one. ## Changelog + +### vA0.6 (24 Jul 2022) +* Add Yagmail (oauth2) support +* Add attaching log file to email report + * Add attach log file only on error +* Add abort on too many modified files +* Add --ignore-modifythreshold + ### Unreleased * Add --ignore-deletethreshold (by exterrestris, #25) * Add support for scrub --plan, replacing --percentage (thanks to fmoledina) diff --git a/snapraid-runner.conf.example b/snapraid-runner.conf.example index bc8fca4..f9252e5 100644 --- a/snapraid-runner.conf.example +++ b/snapraid-runner.conf.example @@ -1,12 +1,15 @@ [snapraid] ; path to the snapraid executable (e.g. /bin/snapraid) -executable = snapraid +executable = snapraid.exe ; path to the snapraid config to be used config = snapraid.conf ; abort operation if there are more deletes than this, set to -1 to disable deletethreshold = 40 +; abort operation if there are more modify's than this, set to -1 to disable, for rudimenary +; ransomware protection +modifythreshold = 1 ; if you want touch to be ran each time -touch = false +touch = true [logging] ; logfile to write to, leave empty to disable @@ -15,16 +18,27 @@ file = snapraid.log maxsize = 5000 [email] +;set true to use yagmail's oauth2 for gmail insead of smtp for sending the email +use_yagmail = true ; when to send an email, comma-separated list of [success, error] sendon = success,error ; set to false to get full programm output via email short = true subject = [SnapRAID] Status Report: -from = -to = +from = +to = ; maximum email size in KiB maxsize = 500 +[yagmail] +; for use only if use_oauth = true, sets the oauth2 credentials file +; location, if no file exists then one will be created when the program is ran +oauth2_file = oauth2_creds.json +;set true to attach the log file to email +send_log = false +;set true to attach the log file to email only if there is an error +iff_error = false + [smtp] host = ; leave empty for default port diff --git a/snapraid-runner.py b/snapraid-runner.py index 56b8e9e..23f8368 100755 --- a/snapraid-runner.py +++ b/snapraid-runner.py @@ -66,11 +66,10 @@ def snapraid_command(command, args={}, *, allow_statuscodes=[]): def send_email(success): - import smtplib from email.mime.text import MIMEText from email import charset - - if len(config["smtp"]["host"]) == 0: + + if len(config["smtp"]["host"]) == 0 and not config["email"]["use_yagmail"]: logging.error("Failed to send email because smtp host is not set") return @@ -98,22 +97,35 @@ def send_email(success): (" SUCCESS" if success else " ERROR") msg["From"] = config["email"]["from"] msg["To"] = config["email"]["to"] - smtp = {"host": config["smtp"]["host"]} - if config["smtp"]["port"]: - smtp["port"] = config["smtp"]["port"] - if config["smtp"]["ssl"]: - server = smtplib.SMTP_SSL(**smtp) + + if config["email"]["use_yagmail"]: + import yagmail + + attachment_path = [] + if config["yagmail"]["send_log"] and not (config["yagmail"]["iff_error"] and success): + attachment_path = config["logging"]["file"] + + yag = yagmail.SMTP(msg["From"], oauth2_file=config["yagmail"]["oauth2_file"]) + yag.send(to=msg["To"], subject=msg["Subject"], contents=body, attachments=attachment_path) else: - server = smtplib.SMTP(**smtp) - if config["smtp"]["tls"]: - server.starttls() - if config["smtp"]["user"]: - server.login(config["smtp"]["user"], config["smtp"]["password"]) - server.sendmail( - config["email"]["from"], - [config["email"]["to"]], - msg.as_string()) - server.quit() + import smtplib + + smtp = {"host": config["smtp"]["host"]} + if config["smtp"]["port"]: + smtp["port"] = config["smtp"]["port"] + if config["smtp"]["ssl"]: + server = smtplib.SMTP_SSL(**smtp) + else: + server = smtplib.SMTP(**smtp) + if config["smtp"]["tls"]: + server.starttls() + if config["smtp"]["user"]: + server.login(config["smtp"]["user"], config["smtp"]["password"]) + server.sendmail( + config["email"]["from"], + [config["email"]["to"]], + msg.as_string()) + server.quit() def finish(is_success): @@ -133,27 +145,29 @@ def load_config(args): global config parser = configparser.RawConfigParser() parser.read(args.conf) - sections = ["snapraid", "logging", "email", "smtp", "scrub"] + sections = ["snapraid", "logging", "email", "yagmail", "smtp", "scrub"] config = dict((x, defaultdict(lambda: "")) for x in sections) for section in parser.sections(): for (k, v) in parser.items(section): config[section][k] = v.strip() int_options = [ - ("snapraid", "deletethreshold"), ("logging", "maxsize"), - ("scrub", "older-than"), ("email", "maxsize"), + ("snapraid", "deletethreshold"), ("snapraid", "modifythreshold"), + ("logging", "maxsize"), ("scrub", "older-than"), ("email", "maxsize"), ] for section, option in int_options: try: config[section][option] = int(config[section][option]) except ValueError: - config[section][option] = 0 - + config[section][option] = 0 + + config["snapraid"]["touch"] = (config["snapraid"]["touch"].lower() == "true") + config["email"]["short"] = (config["email"]["short"].lower() == "true") + config["yagmail"]["send_log"] = (config["yagmail"]["send_log"].lower() == "true") + config["yagmail"]["iff_error"] = (config["yagmail"]["iff_error"].lower() == "true") config["smtp"]["ssl"] = (config["smtp"]["ssl"].lower() == "true") config["smtp"]["tls"] = (config["smtp"]["tls"].lower() == "true") config["scrub"]["enabled"] = (config["scrub"]["enabled"].lower() == "true") - config["email"]["short"] = (config["email"]["short"].lower() == "true") - config["snapraid"]["touch"] = (config["snapraid"]["touch"].lower() == "true") # Migration if config["scrub"]["percentage"]: @@ -165,6 +179,9 @@ def load_config(args): if args.ignore_deletethreshold: config["snapraid"]["deletethreshold"] = -1 + if args.ignore_modifythreshold: + config["snapraid"]["modifythreshold"] = -1 + def setup_logger(): log_format = logging.Formatter( @@ -210,6 +227,8 @@ def main(): help="Do not scrub (overrides config)") parser.add_argument("--ignore-deletethreshold", action='store_true', help="Sync even if configured delete threshold is exceeded") + parser.add_argument("--ignore-modifythreshold", action='store_true', + help="Sync even if configured modify threshold is exceeded") args = parser.parse_args() if not os.path.exists(args.conf): @@ -276,6 +295,14 @@ def run(): logging.error("Run again with --ignore-deletethreshold to sync anyways") finish(False) + if (config["snapraid"]["modifythreshold"] >= 0 and + diff_results["update"] > config["snapraid"]["modifythreshold"]): + logging.error( + "Modified files exceed modify threshold of {}, aborting".format( + config["snapraid"]["modifythreshold"])) + logging.error("Run again with --ignore-modifythreshold to sync anyways") + finish(False) + if (diff_results["remove"] + diff_results["add"] + diff_results["move"] + diff_results["update"] == 0): logging.info("No changes detected, no sync required")