Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The reason I created this is that I wanted more granular control of how my setup
- Allows you to abort execution if configurable thresholds are broken
- Allows you to `scrub` after `sync`
- Logs the raw snapraid output as well as formatted text
- Creates a nicely formatted report and sends it via email or discord
- Creates a nicely formatted report and sends it via email, Discord, or [Apprise](https://github.com/caronc/apprise)
- Provides live insight into the sync/scrub process in Discord
- Spin down selected hard drives after script completion

Expand Down
5 changes: 5 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"enabled": false,
"webhook_id": "",
"webhook_token": ""
},
"apprise": {
"enabled": false,
"binary": "/usr/bin/apprise",
"config": "/var/lib/apprise/config.yml"
}
},
"logs": {
Expand Down
34 changes: 28 additions & 6 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,37 @@
"required": [
"enabled",
"webhook_id",
"webhook_token"
]
"webhook_token"]
},
"apprise": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"examples": [
true
],
"description": "Whether or not to send notifications and reports to Apprise."
},
"binary": {
"type": "string",
"examples": [
"/usr/bin/apprise"
],
"description": "The location of `apprise`."
},
"config": {
"type": "string",
"examples": ["/etc/apprise.yml"],
"description": "Location of the apprise configuration yml."
}
},
"additionalProperties": false,
"required": ["enabled", "binary", "config"]
}
},
"additionalProperties": false,
"required": [
"email",
"discord"
]
"required": ["email", "discord", "apprise"]
},
"logs": {
"type": "object",
Expand Down
83 changes: 83 additions & 0 deletions reports/apprise_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from operator import itemgetter


def create_apprise_report(report_data):
sync_job_ran, scrub_job_ran, sync_job_time, scrub_job_time, diff_data, zero_subsecond_count, \
scrub_stats, drive_stats, smart_drive_data, global_fp, total_time = itemgetter(
'sync_job_ran',
'scrub_job_ran',
'sync_job_time',
'scrub_job_time',
'diff_data',
'zero_subsecond_count',
'scrub_stats',
'drive_stats',
'smart_drive_data',
'global_fp',
'total_time')(report_data)

#
# Create email report

sync_report = f'**Sync Job**'

if sync_job_ran:
sync_report = sync_report + f'''
Job finished successfully in {sync_job_time}.
File diff summary as follows:
- {diff_data["added"]} added
- {diff_data["removed"]} removed
- {diff_data["updated"]} updated
- {diff_data["moved"]} moved
- {diff_data["copied"]} copied
- {diff_data["restored"]} restored
'''
else:
sync_report = sync_report + 'Sync job did **not** run.'

touch_report = '**Touch job**'

if zero_subsecond_count > 0:
touch_report = touch_report + f'''
'A total of {zero_subsecond_count} file(s) had their sub-second value fixed.
'''
else:
touch_report = touch_report + 'No zero sub-second files were found.'

scrub_report = '**Scrub Job**'

if scrub_job_ran:
scrub_report = scrub_report + f'''
Job finished successfully in {scrub_job_time}.
{scrub_stats["unscrubbed"]}% of the array has not been scrubbed, with the oldest block at {scrub_stats["scrub_age"]} day(s).
'''
# , the median at {scrub_stats["median"]} day(s), and the newest at {scrub_stats["newest"]} day(s).
else:
scrub_report = scrub_report + 'Scrub Job did **not** run.'

# Check if any drive had an error count
drives_with_errors = []
for drive in smart_drive_data:
error_count = drive.get("error_count")
if isinstance(error_count, str) and error_count.isdigit():
error_count = int(error_count)
if isinstance(error_count, int) and error_count > 0:
drive["error_count"] = error_count
drives_with_errors.append(drive)

# Summarize SMART drive report
smart_summary = f'**SMART Summary**\nFailure Probability: {global_fp}%'

if drives_with_errors:
smart_summary += "\nDrives with errors:"
for drive in drives_with_errors:
smart_summary += f"\n- {drive['disk']} ({drive['device']}) - Error Count: {drive['error_count']}"

email_report = f'''SnapRAID job completed successfully in {total_time}
{touch_report}
{sync_report}
{scrub_report}
{smart_summary}
'''

return email_report
42 changes: 38 additions & 4 deletions snapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from reports.discord_report import create_discord_report
from reports.email_report import create_email_report
from reports.apprise_report import create_apprise_report
from utils import format_delta, get_relative_path, human_readable_size, run_script

#
Expand Down Expand Up @@ -101,7 +102,8 @@ def notify_and_handle_error(message, error):


def notify_warning(message, embeds=None):
return send_discord(f':warning: [**WARNING!**] {message}', embeds=embeds)
send_apprise(':warning: [**WARNING!**]', message)
send_discord(f':warning: [**WARNING!**] {message}', embeds=embeds)


def notify_info(message, embeds=None, message_id=None):
Expand Down Expand Up @@ -179,6 +181,32 @@ def send_email(subject, message):
log.debug(f'Successfully sent email to {to_email}')


def send_apprise(subject, message):
log.debug('Attempting to send apprise notification...')

is_enabled, apprise_bin, config_loc = itemgetter(
'enabled', 'binary', 'config')(config['notifications']['apprise'])

if not is_enabled:
return

if not os.path.isfile(apprise_bin):
raise FileNotFoundError('Unable to find apprise executable', apprise_bin)

result = subprocess.run([
apprise_bin,
'-vv',
'-t', subject,
'-b', message,
'--config=' + config_loc
], capture_output=True, text=True)

if result.stderr:
raise ConnectionError('Unable to send notification', result.stderr)

log.debug(f'Successfully sent apprise notification')


#
# Snapraid Helpers

Expand Down Expand Up @@ -535,6 +563,7 @@ def get_snapraid_config():
with open(config_file, 'r') as file:
snapraid_config = file.read()

#Split parity handling
file_regex = re.compile(r'^(content|(?:\d+-)?parity) +(.+/\w+.(?:content|(?:\d+-)?parity)) *$',
flags=re.MULTILINE)
parity_files = []
Expand All @@ -544,7 +573,8 @@ def get_snapraid_config():
if m[1] == 'content':
content_files.append(m[2])
else:
parity_files.append(m[2])
for p in m[2].split(','):
parity_files.append(p)

return content_files, parity_files

Expand Down Expand Up @@ -678,9 +708,13 @@ def main():
'total_time': total_time
}

email_report = create_email_report(report_data)
if config['notifications']['email']['enabled']:
email_report = create_email_report(report_data)
send_email('SnapRAID Job Completed Successfully', email_report)

send_email('SnapRAID Job Completed Successfully', email_report)
if config['notifications']['apprise']['enabled']:
apprise_report = create_apprise_report(report_data)
send_apprise('SnapRAID Job Completed Successfully', apprise_report)

if config['notifications']['discord']['enabled']:
(discord_message, embeds) = create_discord_report(report_data)
Expand Down