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)

Lang: 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
656fi

The Script (Windows/PowerShell)

Lang: 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)

Lang: bash
 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 Sites

Windows Configuration (backup-config.csv)

Lang: 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 Controller

Usage Examples

Linux/Bash

Lang: 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.com

Windows/PowerShell

Lang: 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 36

Automated Scheduling

Linux Cron Job

Lang: bash
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.com

Windows Task Scheduler

Lang: powershell
 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

  1. Reads configuration file containing system and backup path information
  2. Scans backup locations to find most recent backup files/directories
  3. Calculates backup age and determines status (success/warning/critical)
  4. Measures backup sizes for storage capacity tracking
  5. Generates comprehensive report in HTML, text, or CSV format
  6. Highlights problems with color-coding and status indicators
  7. Sends email notifications with customizable alerting levels
  8. Provides statistics on overall backup health
  9. 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

Lang: bash
1#!/bin/bash
2# Nagios check plugin
3/opt/scripts/backup-report-generator.sh -f text --hide-success > /dev/null
4exit $?

PRTG Network Monitor

Lang: powershell
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 $exitCode

Tips

  1. Run daily to catch backup failures quickly
  2. Adjust thresholds based on your backup schedule (daily backups: 28-48 hours is reasonable)
  3. Use HTML format for better readability in email clients
  4. Store reports for compliance and audit purposes
  5. Combine with monitoring systems for automated alerting
  6. Test email delivery before relying on automated reports
  7. Document backup paths clearly in configuration file
  8. Review warnings promptly to prevent failures

See Also

Download

Lang: bash
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"