Backup Report Generator
Generate HTML/text report of backup status with email notifications
Table of Contents
Generate comprehensive backup status reports showing last backup time, size, duration, and failed/missing backups with email notifications to administrators.
Overview
This script generates detailed backup status reports in HTML or text format, monitors backup completion status, tracks backup sizes and durations, highlights failures, and sends automated email reports to administrators.
Use Case: Daily/weekly backup verification, compliance reporting, proactive backup monitoring, and alerting for failed or missing backups.
Platform: Cross-platform (Bash for Linux, PowerShell for Windows) Requirements: Mail server access (sendmail/mailx for Linux, SMTP for Windows) Execution Time: 10-60 seconds (depending on number of systems)
The Script (Linux/Bash)
1#!/bin/bash
2
3#
4# backup-report-generator.sh - Generate backup status reports
5#
6# Author: glyph.sh
7# Reference: https://glyph.sh/kb/backup-monitoring/
8#
9
10set -euo pipefail
11
12# Color codes
13RED='\033[0;31m'
14YELLOW='\033[1;33m'
15GREEN='\033[0;32m'
16BLUE='\033[0;34m'
17CYAN='\033[0;36m'
18BOLD='\033[1m'
19NC='\033[0m' # No Color
20
21# Configuration
22SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/backup-config.conf}"
24REPORT_FORMAT="${REPORT_FORMAT:-html}"
25OUTPUT_FILE="${OUTPUT_FILE:-}"
26SEND_EMAIL="${SEND_EMAIL:-false}"
27EMAIL_TO="${EMAIL_TO:-}"
28EMAIL_FROM="${EMAIL_FROM:-backup-reports@$(hostname -d 2>/dev/null || echo 'localhost')}"
29BACKUP_AGE_WARNING="${BACKUP_AGE_WARNING:-28}" # Hours
30BACKUP_AGE_CRITICAL="${BACKUP_AGE_CRITICAL:-48}" # Hours
31SHOW_SUCCESS="${SHOW_SUCCESS:-true}"
32
33# Statistics
34TOTAL_SYSTEMS=0
35SUCCESSFUL_BACKUPS=0
36FAILED_BACKUPS=0
37MISSING_BACKUPS=0
38WARNING_BACKUPS=0
39TOTAL_BACKUP_SIZE=0
40
41# Functions
42print_header() {
43 echo -e "${CYAN}========================================${NC}"
44 echo -e "${CYAN} Backup Report Generator${NC}"
45 echo -e "${CYAN}========================================${NC}"
46 echo ""
47}
48
49print_usage() {
50 cat << EOF
51Usage: $0 [OPTIONS]
52
53Generate comprehensive backup status reports.
54
55OPTIONS:
56 -c, --config FILE Configuration file (default: backup-config.conf)
57 -f, --format FORMAT Report format: html, text, csv (default: html)
58 -o, --output FILE Save report to file (default: stdout)
59 -e, --email TO Send report via email to address(es)
60 --from EMAIL Email from address (default: backup-reports@domain)
61 -w, --warning HOURS Age warning threshold in hours (default: 28)
62 -C, --critical HOURS Age critical threshold in hours (default: 48)
63 --hide-success Don't show successful backups in report
64 -h, --help Show this help message
65
66CONFIGURATION FILE FORMAT:
67 # Lines starting with # are comments
68 # Format: system_name|backup_path|type|description
69 web-server-01|/backups/web01|files|Web Server Files
70 db-server-01|/backups/db01|database|MySQL Database
71 mail-server|/backups/mail|files|Mail Server Backup
72
73EXAMPLES:
74 # Generate HTML report to stdout
75 $0
76
77 # Generate report and save to file
78 $0 -f html -o /tmp/backup-report.html
79
80 # Generate and email report
81 $0 -e admin@company.com,ops@company.com
82
83 # Text format with custom thresholds
84 $0 -f text -w 24 -C 36
85
86EOF
87}
88
89load_config() {
90 if [[ ! -f "$CONFIG_FILE" ]]; then
91 echo -e "${RED}Error: Configuration file not found: $CONFIG_FILE${NC}" >&2
92 echo -e "${YELLOW}Create a configuration file with format:${NC}" >&2
93 echo -e "${YELLOW}system_name|backup_path|type|description${NC}" >&2
94 exit 1
95 fi
96}
97
98get_backup_info() {
99 local backup_path="$1"
100 local info_array=()
101
102 if [[ ! -d "$backup_path" ]] && [[ ! -f "$backup_path" ]]; then
103 echo "missing|0|0|Never"
104 return
105 fi
106
107 # Find most recent backup file/directory
108 local latest_backup=""
109 if [[ -d "$backup_path" ]]; then
110 latest_backup=$(find "$backup_path" -type f -name "*.tar.gz" -o -name "*.tar.bz2" -o -name "*.zip" -o -name "*.sql" 2>/dev/null | head -1)
111 if [[ -z "$latest_backup" ]]; then
112 # Check for dated subdirectories
113 latest_backup=$(find "$backup_path" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1)
114 fi
115 else
116 latest_backup="$backup_path"
117 fi
118
119 if [[ -z "$latest_backup" ]] || [[ ! -e "$latest_backup" ]]; then
120 echo "missing|0|0|Never"
121 return
122 fi
123
124 # Get modification time
125 local mod_time=$(stat -c %Y "$latest_backup" 2>/dev/null || stat -f %m "$latest_backup" 2>/dev/null || echo "0")
126 local current_time=$(date +%s)
127 local age_hours=$(( (current_time - mod_time) / 3600 ))
128
129 # Get size
130 local size_bytes=0
131 if [[ -d "$latest_backup" ]]; then
132 size_bytes=$(du -sb "$latest_backup" 2>/dev/null | awk '{print $1}')
133 else
134 size_bytes=$(stat -c %s "$latest_backup" 2>/dev/null || stat -f %z "$latest_backup" 2>/dev/null || echo "0")
135 fi
136
137 # Format last backup time
138 local last_backup_time=$(date -d "@$mod_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$mod_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "Unknown")
139
140 # Determine status
141 local status="success"
142 if [[ $age_hours -ge $BACKUP_AGE_CRITICAL ]]; then
143 status="critical"
144 elif [[ $age_hours -ge $BACKUP_AGE_WARNING ]]; then
145 status="warning"
146 fi
147
148 echo "$status|$size_bytes|$age_hours|$last_backup_time"
149}
150
151format_bytes() {
152 local bytes=$1
153 if [[ $bytes -eq 0 ]]; then
154 echo "0 B"
155 elif [[ $bytes -lt 1024 ]]; then
156 echo "${bytes} B"
157 elif [[ $bytes -lt 1048576 ]]; then
158 echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1024}") KB"
159 elif [[ $bytes -lt 1073741824 ]]; then
160 echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1048576}") MB"
161 else
162 echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1073741824}") GB"
163 fi
164}
165
166generate_html_header() {
167 cat << 'EOF'
168<!DOCTYPE html>
169<html lang="en">
170<head>
171 <meta charset="UTF-8">
172 <meta name="viewport" content="width=device-width, initial-scale=1.0">
173 <title>Backup Status Report</title>
174 <style>
175 body {
176 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
177 line-height: 1.6;
178 color: #333;
179 max-width: 1200px;
180 margin: 0 auto;
181 padding: 20px;
182 background-color: #f5f5f5;
183 }
184 .header {
185 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
186 color: white;
187 padding: 30px;
188 border-radius: 8px;
189 margin-bottom: 30px;
190 box-shadow: 0 4px 6px rgba(0,0,0,0.1);
191 }
192 .header h1 {
193 margin: 0 0 10px 0;
194 font-size: 28px;
195 }
196 .header .meta {
197 opacity: 0.9;
198 font-size: 14px;
199 }
200 .summary {
201 display: grid;
202 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
203 gap: 15px;
204 margin-bottom: 30px;
205 }
206 .summary-card {
207 background: white;
208 padding: 20px;
209 border-radius: 8px;
210 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
211 text-align: center;
212 }
213 .summary-card .number {
214 font-size: 36px;
215 font-weight: bold;
216 margin: 10px 0;
217 }
218 .summary-card .label {
219 color: #666;
220 font-size: 14px;
221 text-transform: uppercase;
222 letter-spacing: 0.5px;
223 }
224 .summary-card.success .number { color: #10b981; }
225 .summary-card.warning .number { color: #f59e0b; }
226 .summary-card.critical .number { color: #ef4444; }
227 .summary-card.info .number { color: #3b82f6; }
228
229 table {
230 width: 100%;
231 background: white;
232 border-collapse: collapse;
233 border-radius: 8px;
234 overflow: hidden;
235 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
236 }
237 th {
238 background-color: #4f46e5;
239 color: white;
240 padding: 15px;
241 text-align: left;
242 font-weight: 600;
243 text-transform: uppercase;
244 font-size: 12px;
245 letter-spacing: 0.5px;
246 }
247 td {
248 padding: 12px 15px;
249 border-bottom: 1px solid #f0f0f0;
250 }
251 tr:hover {
252 background-color: #f9fafb;
253 }
254 tr:last-child td {
255 border-bottom: none;
256 }
257 .status {
258 display: inline-block;
259 padding: 4px 12px;
260 border-radius: 12px;
261 font-size: 12px;
262 font-weight: 600;
263 text-transform: uppercase;
264 }
265 .status.success {
266 background-color: #d1fae5;
267 color: #065f46;
268 }
269 .status.warning {
270 background-color: #fed7aa;
271 color: #92400e;
272 }
273 .status.critical {
274 background-color: #fee2e2;
275 color: #991b1b;
276 }
277 .status.missing {
278 background-color: #e5e7eb;
279 color: #1f2937;
280 }
281 .footer {
282 margin-top: 30px;
283 text-align: center;
284 color: #666;
285 font-size: 12px;
286 }
287 .age-warning { color: #f59e0b; font-weight: 600; }
288 .age-critical { color: #ef4444; font-weight: 600; }
289 </style>
290</head>
291<body>
292 <div class="header">
293 <h1>Backup Status Report</h1>
294 <div class="meta">
295 Generated on: $(date "+%Y-%m-%d %H:%M:%S %Z")<br>
296 Hostname: $(hostname)<br>
297 Report Period: Last $(($BACKUP_AGE_CRITICAL / 24)) days
298 </div>
299 </div>
300EOF
301}
302
303generate_html_summary() {
304 cat << EOF
305 <div class="summary">
306 <div class="summary-card info">
307 <div class="label">Total Systems</div>
308 <div class="number">$TOTAL_SYSTEMS</div>
309 </div>
310 <div class="summary-card success">
311 <div class="label">Successful</div>
312 <div class="number">$SUCCESSFUL_BACKUPS</div>
313 </div>
314 <div class="summary-card warning">
315 <div class="label">Warnings</div>
316 <div class="number">$WARNING_BACKUPS</div>
317 </div>
318 <div class="summary-card critical">
319 <div class="label">Failed/Missing</div>
320 <div class="number">$(($FAILED_BACKUPS + $MISSING_BACKUPS))</div>
321 </div>
322 </div>
323
324 <table>
325 <thead>
326 <tr>
327 <th>System</th>
328 <th>Type</th>
329 <th>Status</th>
330 <th>Last Backup</th>
331 <th>Age</th>
332 <th>Size</th>
333 <th>Description</th>
334 </tr>
335 </thead>
336 <tbody>
337EOF
338}
339
340generate_html_footer() {
341 cat << 'EOF'
342 </tbody>
343 </table>
344
345 <div class="footer">
346 <p>Generated by Backup Report Generator | glyph.sh</p>
347 <p>Contact your system administrator if you notice any issues with backups.</p>
348 </div>
349</body>
350</html>
351EOF
352}
353
354generate_text_header() {
355 cat << EOF
356========================================
357 BACKUP STATUS REPORT
358========================================
359Generated: $(date "+%Y-%m-%d %H:%M:%S %Z")
360Hostname: $(hostname)
361Report Period: Last $(($BACKUP_AGE_CRITICAL / 24)) days
362
363SUMMARY
364----------------------------------------
365Total Systems: $TOTAL_SYSTEMS
366Successful Backups: $SUCCESSFUL_BACKUPS
367Warnings: $WARNING_BACKUPS
368Failed/Missing: $(($FAILED_BACKUPS + $MISSING_BACKUPS))
369Total Backup Size: $(format_bytes $TOTAL_BACKUP_SIZE)
370
371DETAILED REPORT
372----------------------------------------
373EOF
374}
375
376add_html_row() {
377 local system="$1"
378 local type="$2"
379 local status="$3"
380 local size_bytes="$4"
381 local age_hours="$5"
382 local last_backup="$6"
383 local description="$7"
384
385 local age_class=""
386 if [[ $age_hours -ge $BACKUP_AGE_CRITICAL ]]; then
387 age_class='class="age-critical"'
388 elif [[ $age_hours -ge $BACKUP_AGE_WARNING ]]; then
389 age_class='class="age-warning"'
390 fi
391
392 local age_display="${age_hours}h ago"
393 if [[ $age_hours -ge 24 ]]; then
394 age_display="$((age_hours / 24))d ago"
395 fi
396
397 cat << EOF
398 <tr>
399 <td><strong>$system</strong></td>
400 <td>$type</td>
401 <td><span class="status $status">$status</span></td>
402 <td>$last_backup</td>
403 <td $age_class>$age_display</td>
404 <td>$(format_bytes $size_bytes)</td>
405 <td>$description</td>
406 </tr>
407EOF
408}
409
410add_text_row() {
411 local system="$1"
412 local type="$2"
413 local status="$3"
414 local size_bytes="$4"
415 local age_hours="$5"
416 local last_backup="$6"
417 local description="$7"
418
419 local status_symbol="✓"
420 case "$status" in
421 success) status_symbol="✓" ;;
422 warning) status_symbol="⚠" ;;
423 critical) status_symbol="✗" ;;
424 missing) status_symbol="?" ;;
425 esac
426
427 local age_display="${age_hours}h ago"
428 if [[ $age_hours -ge 24 ]]; then
429 age_display="$((age_hours / 24))d ago"
430 fi
431
432 printf "%-20s %-10s [%s] %-20s %10s %15s\n" \
433 "$system" \
434 "$type" \
435 "$status_symbol $status" \
436 "$last_backup" \
437 "$age_display" \
438 "$(format_bytes $size_bytes)"
439
440 if [[ -n "$description" ]]; then
441 printf " Description: %s\n" "$description"
442 fi
443 echo ""
444}
445
446process_backups() {
447 while IFS='|' read -r system backup_path type description; do
448 # Skip comments and empty lines
449 [[ "$system" =~ ^[[:space:]]*# ]] && continue
450 [[ -z "$system" ]] && continue
451
452 ((TOTAL_SYSTEMS++))
453
454 # Get backup info
455 IFS='|' read -r status size_bytes age_hours last_backup <<< "$(get_backup_info "$backup_path")"
456
457 # Update statistics
458 case "$status" in
459 success)
460 ((SUCCESSFUL_BACKUPS++))
461 TOTAL_BACKUP_SIZE=$((TOTAL_BACKUP_SIZE + size_bytes))
462 ;;
463 warning)
464 ((WARNING_BACKUPS++))
465 TOTAL_BACKUP_SIZE=$((TOTAL_BACKUP_SIZE + size_bytes))
466 ;;
467 critical)
468 ((FAILED_BACKUPS++))
469 ;;
470 missing)
471 ((MISSING_BACKUPS++))
472 ;;
473 esac
474
475 # Skip successful backups if requested
476 if [[ "$SHOW_SUCCESS" == "false" ]] && [[ "$status" == "success" ]]; then
477 continue
478 fi
479
480 # Add to report
481 if [[ "$REPORT_FORMAT" == "html" ]]; then
482 add_html_row "$system" "$type" "$status" "$size_bytes" "$age_hours" "$last_backup" "$description"
483 elif [[ "$REPORT_FORMAT" == "text" ]]; then
484 add_text_row "$system" "$type" "$status" "$size_bytes" "$age_hours" "$last_backup" "$description"
485 fi
486 done < "$CONFIG_FILE"
487}
488
489send_email_report() {
490 local report_file="$1"
491 local subject="Backup Status Report - $(date +%Y-%m-%d)"
492
493 if [[ $(($FAILED_BACKUPS + $MISSING_BACKUPS)) -gt 0 ]]; then
494 subject="[ALERT] $subject - $(($FAILED_BACKUPS + $MISSING_BACKUPS)) Failed/Missing"
495 elif [[ $WARNING_BACKUPS -gt 0 ]]; then
496 subject="[WARNING] $subject - $WARNING_BACKUPS Warnings"
497 fi
498
499 if command -v mail &> /dev/null; then
500 # Use mail command (mailx)
501 if [[ "$REPORT_FORMAT" == "html" ]]; then
502 mail -s "$subject" -a "Content-Type: text/html" -r "$EMAIL_FROM" "$EMAIL_TO" < "$report_file"
503 else
504 mail -s "$subject" -r "$EMAIL_FROM" "$EMAIL_TO" < "$report_file"
505 fi
506 elif command -v sendmail &> /dev/null; then
507 # Use sendmail
508 {
509 echo "To: $EMAIL_TO"
510 echo "From: $EMAIL_FROM"
511 echo "Subject: $subject"
512 if [[ "$REPORT_FORMAT" == "html" ]]; then
513 echo "Content-Type: text/html; charset=UTF-8"
514 fi
515 echo ""
516 cat "$report_file"
517 } | sendmail -t
518 else
519 echo -e "${RED}Error: No mail command found (mail or sendmail)${NC}" >&2
520 return 1
521 fi
522}
523
524# Parse command line arguments
525while [[ $# -gt 0 ]]; do
526 case $1 in
527 -c|--config)
528 CONFIG_FILE="$2"
529 shift 2
530 ;;
531 -f|--format)
532 REPORT_FORMAT="$2"
533 shift 2
534 ;;
535 -o|--output)
536 OUTPUT_FILE="$2"
537 shift 2
538 ;;
539 -e|--email)
540 SEND_EMAIL=true
541 EMAIL_TO="$2"
542 shift 2
543 ;;
544 --from)
545 EMAIL_FROM="$2"
546 shift 2
547 ;;
548 -w|--warning)
549 BACKUP_AGE_WARNING="$2"
550 shift 2
551 ;;
552 -C|--critical)
553 BACKUP_AGE_CRITICAL="$2"
554 shift 2
555 ;;
556 --hide-success)
557 SHOW_SUCCESS=false
558 shift
559 ;;
560 -h|--help)
561 print_usage
562 exit 0
563 ;;
564 *)
565 echo -e "${RED}Error: Unknown option: $1${NC}" >&2
566 print_usage
567 exit 1
568 ;;
569 esac
570done
571
572# Main execution
573print_header
574
575# Load configuration
576load_config
577
578# Prepare output
579TEMP_REPORT=$(mktemp)
580trap "rm -f $TEMP_REPORT" EXIT
581
582# Generate report
583{
584 if [[ "$REPORT_FORMAT" == "html" ]]; then
585 generate_html_header
586 generate_html_summary
587 process_backups
588 generate_html_footer
589 elif [[ "$REPORT_FORMAT" == "text" ]]; then
590 # Process backups first to get statistics
591 BACKUP_DATA=$(mktemp)
592 while IFS='|' read -r system backup_path type description; do
593 [[ "$system" =~ ^[[:space:]]*# ]] && continue
594 [[ -z "$system" ]] && continue
595 ((TOTAL_SYSTEMS++))
596 IFS='|' read -r status size_bytes age_hours last_backup <<< "$(get_backup_info "$backup_path")"
597 case "$status" in
598 success) ((SUCCESSFUL_BACKUPS++)); TOTAL_BACKUP_SIZE=$((TOTAL_BACKUP_SIZE + size_bytes)) ;;
599 warning) ((WARNING_BACKUPS++)); TOTAL_BACKUP_SIZE=$((TOTAL_BACKUP_SIZE + size_bytes)) ;;
600 critical) ((FAILED_BACKUPS++)) ;;
601 missing) ((MISSING_BACKUPS++)) ;;
602 esac
603 echo "$system|$backup_path|$type|$description|$status|$size_bytes|$age_hours|$last_backup" >> "$BACKUP_DATA"
604 done < "$CONFIG_FILE"
605
606 generate_text_header
607 while IFS='|' read -r system backup_path type description status size_bytes age_hours last_backup; do
608 [[ "$SHOW_SUCCESS" == "false" ]] && [[ "$status" == "success" ]] && continue
609 add_text_row "$system" "$type" "$status" "$size_bytes" "$age_hours" "$last_backup" "$description"
610 done < "$BACKUP_DATA"
611 rm -f "$BACKUP_DATA"
612
613 echo "========================================"
614 echo "Report generated by glyph.sh"
615 fi
616} > "$TEMP_REPORT"
617
618# Output or save report
619if [[ -n "$OUTPUT_FILE" ]]; then
620 cp "$TEMP_REPORT" "$OUTPUT_FILE"
621 echo -e "${GREEN}Report saved to: $OUTPUT_FILE${NC}"
622else
623 cat "$TEMP_REPORT"
624fi
625
626# Send email if requested
627if [[ "$SEND_EMAIL" == "true" ]]; then
628 echo ""
629 echo -e "${BLUE}Sending email report to: $EMAIL_TO${NC}"
630 if send_email_report "$TEMP_REPORT"; then
631 echo -e "${GREEN}Email sent successfully!${NC}"
632 else
633 echo -e "${RED}Failed to send email${NC}" >&2
634 fi
635fi
636
637# Summary
638echo ""
639echo -e "${CYAN}========================================${NC}"
640echo -e "${CYAN} Report Summary${NC}"
641echo -e "${CYAN}========================================${NC}"
642echo -e "Total Systems: ${BLUE}$TOTAL_SYSTEMS${NC}"
643echo -e "Successful Backups: ${GREEN}$SUCCESSFUL_BACKUPS${NC}"
644echo -e "Warnings: ${YELLOW}$WARNING_BACKUPS${NC}"
645echo -e "Failed/Missing: ${RED}$(($FAILED_BACKUPS + $MISSING_BACKUPS))${NC}"
646echo -e "Total Backup Size: ${BLUE}$(format_bytes $TOTAL_BACKUP_SIZE)${NC}"
647echo ""
648
649# Exit with appropriate code
650if [[ $(($FAILED_BACKUPS + $MISSING_BACKUPS)) -gt 0 ]]; then
651 exit 2
652elif [[ $WARNING_BACKUPS -gt 0 ]]; then
653 exit 1
654else
655 exit 0
656fiThe Script (Windows/PowerShell)
1<#
2.SYNOPSIS
3 Generate backup status reports with email notifications
4
5.DESCRIPTION
6 Generates comprehensive backup status reports showing last backup time,
7 size, duration, and highlights failed or missing backups.
8
9.PARAMETER ConfigFile
10 Path to configuration file containing backup locations
11
12.PARAMETER Format
13 Report format: HTML, Text, or CSV (default: HTML)
14
15.PARAMETER OutputFile
16 Save report to file instead of displaying
17
18.PARAMETER EmailTo
19 Email address(es) to send report to (comma-separated)
20
21.PARAMETER EmailFrom
22 Email from address
23
24.PARAMETER SmtpServer
25 SMTP server for sending email
26
27.PARAMETER WarningHours
28 Age threshold in hours for warning status (default: 28)
29
30.PARAMETER CriticalHours
31 Age threshold in hours for critical status (default: 48)
32
33.PARAMETER HideSuccess
34 Don't show successful backups in report
35
36.EXAMPLE
37 .\backup-report-generator.ps1
38
39.EXAMPLE
40 .\backup-report-generator.ps1 -Format HTML -OutputFile C:\Reports\backup-report.html
41
42.EXAMPLE
43 .\backup-report-generator.ps1 -EmailTo "admin@company.com" -SmtpServer smtp.office365.com
44
45.NOTES
46 Author: glyph.sh
47 Reference: https://glyph.sh/kb/backup-monitoring/
48#>
49
50[CmdletBinding()]
51param(
52 [string]$ConfigFile = ".\backup-config.csv",
53 [ValidateSet("HTML", "Text", "CSV")]
54 [string]$Format = "HTML",
55 [string]$OutputFile = "",
56 [string]$EmailTo = "",
57 [string]$EmailFrom = "backup-reports@$env:USERDNSDOMAIN",
58 [string]$SmtpServer = "",
59 [int]$WarningHours = 28,
60 [int]$CriticalHours = 48,
61 [switch]$HideSuccess
62)
63
64# Statistics
65$script:TotalSystems = 0
66$script:SuccessfulBackups = 0
67$script:FailedBackups = 0
68$script:MissingBackups = 0
69$script:WarningBackups = 0
70$script:TotalBackupSize = 0
71
72function Write-Header {
73 Write-Host "========================================" -ForegroundColor Cyan
74 Write-Host " Backup Report Generator" -ForegroundColor Cyan
75 Write-Host "========================================" -ForegroundColor Cyan
76 Write-Host ""
77}
78
79function Get-BackupInfo {
80 param([string]$BackupPath)
81
82 if (-not (Test-Path $BackupPath)) {
83 return @{
84 Status = "Missing"
85 Size = 0
86 AgeHours = 0
87 LastBackup = "Never"
88 }
89 }
90
91 # Find most recent backup file
92 $latestBackup = Get-ChildItem -Path $BackupPath -Recurse -File -Include *.bak,*.zip,*.7z,*.vhd,*.vhdx |
93 Sort-Object LastWriteTime -Descending |
94 Select-Object -First 1
95
96 if (-not $latestBackup) {
97 # Check for dated subdirectories
98 $latestBackup = Get-ChildItem -Path $BackupPath -Directory |
99 Sort-Object LastWriteTime -Descending |
100 Select-Object -First 1
101 }
102
103 if (-not $latestBackup) {
104 return @{
105 Status = "Missing"
106 Size = 0
107 AgeHours = 0
108 LastBackup = "Never"
109 }
110 }
111
112 $ageHours = [Math]::Round(((Get-Date) - $latestBackup.LastWriteTime).TotalHours, 1)
113 $sizeBytes = if ($latestBackup.PSIsContainer) {
114 (Get-ChildItem -Path $latestBackup.FullName -Recurse -File | Measure-Object -Property Length -Sum).Sum
115 } else {
116 $latestBackup.Length
117 }
118
119 $status = "Success"
120 if ($ageHours -ge $CriticalHours) {
121 $status = "Critical"
122 } elseif ($ageHours -ge $WarningHours) {
123 $status = "Warning"
124 }
125
126 return @{
127 Status = $status
128 Size = $sizeBytes
129 AgeHours = $ageHours
130 LastBackup = $latestBackup.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
131 }
132}
133
134function Format-Bytes {
135 param([long]$Bytes)
136
137 if ($Bytes -eq 0) { return "0 B" }
138 if ($Bytes -lt 1KB) { return "$Bytes B" }
139 if ($Bytes -lt 1MB) { return "{0:N2} KB" -f ($Bytes / 1KB) }
140 if ($Bytes -lt 1GB) { return "{0:N2} MB" -f ($Bytes / 1MB) }
141 return "{0:N2} GB" -f ($Bytes / 1GB)
142}
143
144function Generate-HTMLReport {
145 param($BackupData)
146
147 $html = @"
148<!DOCTYPE html>
149<html lang="en">
150<head>
151 <meta charset="UTF-8">
152 <meta name="viewport" content="width=device-width, initial-scale=1.0">
153 <title>Backup Status Report</title>
154 <style>
155 body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; }
156 .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 8px; margin-bottom: 30px; }
157 .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }
158 .summary-card { background: white; padding: 20px; border-radius: 8px; text-align: center; }
159 .summary-card .number { font-size: 36px; font-weight: bold; margin: 10px 0; }
160 .summary-card.success .number { color: #10b981; }
161 .summary-card.warning .number { color: #f59e0b; }
162 .summary-card.critical .number { color: #ef4444; }
163 table { width: 100%; background: white; border-collapse: collapse; border-radius: 8px; overflow: hidden; }
164 th { background-color: #4f46e5; color: white; padding: 15px; text-align: left; }
165 td { padding: 12px 15px; border-bottom: 1px solid #f0f0f0; }
166 tr:hover { background-color: #f9fafb; }
167 .status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
168 .status.success { background-color: #d1fae5; color: #065f46; }
169 .status.warning { background-color: #fed7aa; color: #92400e; }
170 .status.critical { background-color: #fee2e2; color: #991b1b; }
171 .status.missing { background-color: #e5e7eb; color: #1f2937; }
172 </style>
173</head>
174<body>
175 <div class="header">
176 <h1>Backup Status Report</h1>
177 <div>Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") | Host: $env:COMPUTERNAME</div>
178 </div>
179 <div class="summary">
180 <div class="summary-card"><div class="label">Total Systems</div><div class="number">$($script:TotalSystems)</div></div>
181 <div class="summary-card success"><div class="label">Successful</div><div class="number">$($script:SuccessfulBackups)</div></div>
182 <div class="summary-card warning"><div class="label">Warnings</div><div class="number">$($script:WarningBackups)</div></div>
183 <div class="summary-card critical"><div class="label">Failed/Missing</div><div class="number">$($script:FailedBackups + $script:MissingBackups)</div></div>
184 </div>
185 <table>
186 <thead>
187 <tr><th>System</th><th>Type</th><th>Status</th><th>Last Backup</th><th>Age</th><th>Size</th><th>Description</th></tr>
188 </thead>
189 <tbody>
190"@
191
192 foreach ($backup in $BackupData) {
193 $ageDisplay = if ($backup.AgeHours -ge 24) { "$([Math]::Round($backup.AgeHours / 24, 1))d ago" } else { "$($backup.AgeHours)h ago" }
194 $html += "<tr><td><strong>$($backup.System)</strong></td><td>$($backup.Type)</td><td><span class='status $($backup.Status.ToLower())'>$($backup.Status)</span></td><td>$($backup.LastBackup)</td><td>$ageDisplay</td><td>$(Format-Bytes $backup.Size)</td><td>$($backup.Description)</td></tr>"
195 }
196
197 $html += @"
198 </tbody>
199 </table>
200</body>
201</html>
202"@
203
204 return $html
205}
206
207function Send-EmailReport {
208 param(
209 [string]$ReportContent,
210 [string]$To,
211 [string]$From,
212 [string]$SmtpServer
213 )
214
215 $subject = "Backup Status Report - $(Get-Date -Format 'yyyy-MM-dd')"
216
217 if (($script:FailedBackups + $script:MissingBackups) -gt 0) {
218 $subject = "[ALERT] $subject - $($script:FailedBackups + $script:MissingBackups) Failed/Missing"
219 } elseif ($script:WarningBackups -gt 0) {
220 $subject = "[WARNING] $subject - $($script:WarningBackups) Warnings"
221 }
222
223 $mailParams = @{
224 To = $To -split ','
225 From = $From
226 Subject = $subject
227 Body = $ReportContent
228 BodyAsHtml = ($Format -eq "HTML")
229 SmtpServer = $SmtpServer
230 }
231
232 Send-MailMessage @mailParams
233}
234
235# Main execution
236Write-Header
237
238# Load configuration
239if (-not (Test-Path $ConfigFile)) {
240 Write-Host "Error: Configuration file not found: $ConfigFile" -ForegroundColor Red
241 Write-Host "Create a CSV file with columns: System,BackupPath,Type,Description" -ForegroundColor Yellow
242 exit 1
243}
244
245$backupConfigs = Import-Csv $ConfigFile
246$backupData = @()
247
248foreach ($config in $backupConfigs) {
249 $script:TotalSystems++
250
251 $info = Get-BackupInfo -BackupPath $config.BackupPath
252
253 switch ($info.Status) {
254 "Success" { $script:SuccessfulBackups++; $script:TotalBackupSize += $info.Size }
255 "Warning" { $script:WarningBackups++; $script:TotalBackupSize += $info.Size }
256 "Critical" { $script:FailedBackups++ }
257 "Missing" { $script:MissingBackups++ }
258 }
259
260 if (-not $HideSuccess -or $info.Status -ne "Success") {
261 $backupData += [PSCustomObject]@{
262 System = $config.System
263 Type = $config.Type
264 Status = $info.Status
265 Size = $info.Size
266 AgeHours = $info.AgeHours
267 LastBackup = $info.LastBackup
268 Description = $config.Description
269 }
270 }
271}
272
273# Generate report
274$report = switch ($Format) {
275 "HTML" { Generate-HTMLReport -BackupData $backupData }
276 "Text" { $backupData | Format-Table -AutoSize | Out-String }
277 "CSV" { $backupData | ConvertTo-Csv -NoTypeInformation }
278}
279
280# Output report
281if ($OutputFile) {
282 $report | Out-File -FilePath $OutputFile -Encoding UTF8
283 Write-Host "Report saved to: $OutputFile" -ForegroundColor Green
284} else {
285 Write-Output $report
286}
287
288# Send email if requested
289if ($EmailTo -and $SmtpServer) {
290 Write-Host "`nSending email report to: $EmailTo" -ForegroundColor Blue
291 try {
292 Send-EmailReport -ReportContent $report -To $EmailTo -From $EmailFrom -SmtpServer $SmtpServer
293 Write-Host "Email sent successfully!" -ForegroundColor Green
294 } catch {
295 Write-Host "Failed to send email: $_" -ForegroundColor Red
296 }
297}
298
299# Summary
300Write-Host "`n========================================" -ForegroundColor Cyan
301Write-Host " Report Summary" -ForegroundColor Cyan
302Write-Host "========================================" -ForegroundColor Cyan
303Write-Host "Total Systems: $($script:TotalSystems)" -ForegroundColor Blue
304Write-Host "Successful Backups: $($script:SuccessfulBackups)" -ForegroundColor Green
305Write-Host "Warnings: $($script:WarningBackups)" -ForegroundColor Yellow
306Write-Host "Failed/Missing: $($script:FailedBackups + $script:MissingBackups)" -ForegroundColor Red
307Write-Host "Total Backup Size: $(Format-Bytes $script:TotalBackupSize)" -ForegroundColor Blue
308Write-Host ""
309
310# Exit with appropriate code
311if (($script:FailedBackups + $script:MissingBackups) -gt 0) {
312 exit 2
313} elseif ($script:WarningBackups -gt 0) {
314 exit 1
315} else {
316 exit 0
317}Configuration File Setup
Linux Configuration (backup-config.conf)
1# Backup configuration file
2# Format: system_name|backup_path|type|description
3
4# File servers
5file-server-01|/backups/fileserver01|files|Main File Server
6file-server-02|/backups/fileserver02|files|Branch Office Files
7
8# Databases
9sql-server-01|/backups/sql/production|database|Production SQL Database
10mysql-server|/backups/mysql|database|MySQL Application DB
11
12# Virtual machines
13vmware-host|/backups/vms|vm|VMware Virtual Machines
14hyper-v-host|/backups/hyperv|vm|Hyper-V VMs
15
16# Applications
17exchange-server|/backups/exchange|email|Exchange Mailboxes
18sharepoint|/backups/sharepoint|application|SharePoint SitesWindows Configuration (backup-config.csv)
1System,BackupPath,Type,Description
2FILE-SERVER-01,\\nas\backups\fileserver01,Files,Main File Server
3SQL-SERVER-01,D:\Backups\SQL,Database,Production SQL Database
4EXCHANGE-01,\\nas\backups\exchange,Email,Exchange Mailboxes
5VM-HOST-01,\\nas\backups\vms,VM,Virtual Machines
6DC-01,\\nas\backups\systemstate,SystemState,Domain ControllerUsage Examples
Linux/Bash
1# Generate HTML report
2./backup-report-generator.sh
3
4# Generate and save report
5./backup-report-generator.sh -f html -o /var/reports/backup-report.html
6
7# Send email report
8./backup-report-generator.sh -e admin@company.com,ops@company.com
9
10# Text format with custom thresholds
11./backup-report-generator.sh -f text -w 24 -C 36
12
13# Hide successful backups (show only problems)
14./backup-report-generator.sh --hide-success -e admin@company.comWindows/PowerShell
1# Generate HTML report
2.\backup-report-generator.ps1
3
4# Save to file
5.\backup-report-generator.ps1 -Format HTML -OutputFile C:\Reports\backup-report.html
6
7# Send email report
8.\backup-report-generator.ps1 -EmailTo "admin@company.com" -SmtpServer "smtp.office365.com"
9
10# Show only problems
11.\backup-report-generator.ps1 -HideSuccess -EmailTo "admin@company.com" -SmtpServer "smtp.company.com"
12
13# Custom thresholds
14.\backup-report-generator.ps1 -WarningHours 24 -CriticalHours 36Automated Scheduling
Linux Cron Job
1# Edit crontab
2crontab -e
3
4# Daily report at 8 AM
50 8 * * * /opt/scripts/backup-report-generator.sh -e admin@company.com > /dev/null 2>&1
6
7# Weekly summary on Monday at 9 AM
80 9 * * 1 /opt/scripts/backup-report-generator.sh -f html -o /var/reports/weekly-backup.html -e management@company.comWindows Task Scheduler
1# Create scheduled task for daily report
2$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
3 -Argument "-ExecutionPolicy Bypass -File C:\Scripts\backup-report-generator.ps1 -EmailTo admin@company.com -SmtpServer smtp.company.com"
4
5$trigger = New-ScheduledTaskTrigger -Daily -At 8:00AM
6
7Register-ScheduledTask -TaskName "Daily Backup Report" `
8 -Action $action `
9 -Trigger $trigger `
10 -Description "Generate and email daily backup status report" `
11 -User "SYSTEM"What It Does
- Reads configuration file containing system and backup path information
- Scans backup locations to find most recent backup files/directories
- Calculates backup age and determines status (success/warning/critical)
- Measures backup sizes for storage capacity tracking
- Generates comprehensive report in HTML, text, or CSV format
- Highlights problems with color-coding and status indicators
- Sends email notifications with customizable alerting levels
- Provides statistics on overall backup health
- Exits with appropriate codes for integration with monitoring systems
Report Status Levels
- Success - Backup completed within warning threshold (green)
- Warning - Backup older than warning threshold (yellow/orange)
- Critical - Backup older than critical threshold (red)
- Missing - No backup found at specified location (gray)
Email Alert Levels
- [ALERT] prefix - One or more backups failed or missing
- [WARNING] prefix - One or more backups in warning state
- No prefix - All backups successful
Exit Codes
- 0 - All backups successful
- 1 - One or more warnings
- 2 - One or more failures/missing backups
Integration with Monitoring
Nagios/Icinga
1#!/bin/bash
2# Nagios check plugin
3/opt/scripts/backup-report-generator.sh -f text --hide-success > /dev/null
4exit $?PRTG Network Monitor
1# PRTG sensor script
2.\backup-report-generator.ps1 -Format Text -HideSuccess | Out-Null
3$exitCode = $LASTEXITCODE
4Write-Host "<prtg><result><channel>Failed Backups</channel><value>$($script:FailedBackups + $script:MissingBackups)</value></result></prtg>"
5exit $exitCodeTips
- Run daily to catch backup failures quickly
- Adjust thresholds based on your backup schedule (daily backups: 28-48 hours is reasonable)
- Use HTML format for better readability in email clients
- Store reports for compliance and audit purposes
- Combine with monitoring systems for automated alerting
- Test email delivery before relying on automated reports
- Document backup paths clearly in configuration file
- Review warnings promptly to prevent failures
See Also
Download
1# Linux
2curl -o backup-report-generator.sh https://glyph.sh/scripts/backup-report-generator.sh
3chmod +x backup-report-generator.sh
4
5# Windows
6Invoke-WebRequest -Uri "https://glyph.sh/scripts/backup-report-generator.ps1" -OutFile "backup-report-generator.ps1"