FYI, here is a perl script I recently wrote to summarize my backups at
one glance so I can identify missed backups, errors, missed files etc.
It organizes a lot of data in a simple text file.
I call the script daily from cron.
It doesn't *need* to be run as 'backuppc' (or root) though it works
better if it does.

It's not fully polished but it works well
--------------------------------------------------------------------------------
#!/usr/bin/perl
#========================================================================
#
# BackupPC_summarizeBackups.pl
#                       
#
# DESCRIPTION

#   Summarize status of latest backups (full and incremental) with one
#   (long) tabular line per host.
#
#   Provides the following columns per host
#     General Status:
#       HOST             Name of host
#       STATUS           Current status
#                          Idle - if idle
#                          NNNNm - Active time in minutes if backing up
#                          Fail - if last backup failed
#                          Man - if set to manual backups (BackupsDisable = 1)
#                          Disab - if backups disabled (BackupsDisable = 2)
#      LAST              Fractional days since last backup (full or incremental)
#
#    Full Backup Status:
#      FULL              Fractional days since last full backup
#      FILES             Number of files in last full
#      SIZE              Size of last full
#      TIME              Time to complete last full (in minutes)
#      ERRS/BAD          Number of errors/bad files in last full
#
#    Incremental  Backup Status:
#      INCR              Fractional days since last incremental backup
#      FILES             Number of files in last incremental
#      SIZE              Size of last incremental
#      TIME              Time to complete last incremental (in minutes)
#      ERRS/BAD          Number of errors/bad files in last incremental
#
#    Sanity Checks (worry if current numbers differ substantially from this)
#      MAX_FILES         Maximum files in past $lookback_days (default 365)
#      MAX_SIZE          Max backup size in past $lookback_days (default 365)
#
# AUTHOR
#   Jeff Kosowsky
#
# COPYRIGHT
#   Copyright (C) 2025  Jeff Kosowsky
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#========================================================================
#
# Version 0.2 June 2025
#
# CHANGELOG:
# 0.1 (June 2025)
# 0.2 (July 2025)
#
#========================================================================
use strict;
use warnings;
use lib "/usr/share/backuppc/lib";
use BackupPC::Lib;
use POSIX qw(strftime);

# Configuration
my $lookback_days = 365; # Configurable lookback period for recent backups
my $host_width = 20; #Max width of host name

# Initialize BackupPC
my $bpc = BackupPC::Lib->new('/var/lib/backuppc', undef, undef, 1)
    or die "Failed to initialize BackupPC: $@\n";
my $Conf = $bpc->ConfigDataRead()
    or die "Failed to load config: $@\n";
my $now = time;

# Get Status and Info
my $Status = {};
my $Info = {};
my $err = $bpc->ServerConnect();
if (!$err) { # Read dynamically via Server_Message (requires backuppc or root 
privilege)
    my $reply = $bpc->ServerMesg("status hosts");
    if (!defined $reply) {
        warn "ServerMesg for hosts returned undef\n";
    } else {
        my %Status;
        eval($reply);
        if ($@) {
            warn "Eval error for hosts: $@\n";
        } else {
            $Status = \%Status;
        }
    }
    $reply = $bpc->ServerMesg("status info"); # Get system info including pool 
stats
    if (!defined $reply) {
        warn "ServerMesg for info returned undef\n";
    } else {
        my %Info;
        eval($reply);
        if ($@) {
            warn "Eval error for info: $@\n";
        } else {
            $Info = \%Info;
        }
    }
} else { # Read from 'logDir/status.pl' using StatusDataRead
    warn "ServerConnect failed, falling back to status.pl\n";
    my $log_dir = $bpc->LogDir
        or die "LogDir not defined\n";
    my $status_file = "$log_dir/status.pl";
    if (!(-f $status_file && -r $status_file)) {
        warn "$status_file not found or not readable\n";
    } else { # Read from logDir/status.pl  using StatusDataRead
        ($Status, $Info) = $bpc->{storage}->StatusDataRead();
        if (!defined $Status ||  ref($Status) ne 'HASH') {
            warn "Failed to read status from $status_file\n"
                unless defined($Status);
            $Status = {};
        }
        if (!defined $Info || ref($Info) ne 'HASH') {
            warn "Failed to read valid Info from $status_file\n";
            $Info = {};
        }
    }
}

# Check if BackupPC is running
my $backuppc_running = defined($Info->{pid});

# Print warning if BackupPC is not running
print "***WARNING*** BackupPC not running!\n" unless $backuppc_running;

# Print header
printf "%-*s %-8s %-7s | %-7s %-10s %-10s %-7s %-10s | %-7s %-10s %-10s %-7s 
%-10s | %-10s %-10s\n",
    $host_width + 1,
    "HOST", "STATUS", "LAST",
    "FULL", "FILES", "SIZE", "TIME", "ERRS/BAD",
    "INCR", "FILES", "SIZE", "TIME", "ERRS/BAD", 
    "MAX_FILES", "MAX_SIZE";

# Get host list
my $hosts = $bpc->HostInfoRead()
    or die "Failed to read hosts: $@\n";
my @host_data;
my @host_nobackups;

foreach my $host (keys %$hosts) {
    my $status = "Idle";
    my $days_since_last = 999999;
    my $days_since_full = undef;
    my $days_since_incr = undef;
    my $files_full = '-';
    my $size_full = '-';
    my $time_full = '-';
    my $errs_bad_full = '-';
    my $files_incr = '-';
    my $size_incr = '-';
    my $time_incr = '-';
    my $errs_bad_incr = '-';
    my $max_files = 0;
    my $max_size = 0;
    my $has_recent_backup = 0;
    my $has_valid_backup = 0;
    my $last_backup_time = 0;

    # Get current status
    my $host_status = $Status->{$host} // {};
    if (defined $host_status->{job} && $host_status->{job} eq "Backup" &&
        defined $host_status->{reason} && $host_status->{reason} eq 
"Reason_backup_in_progress") {
        my $start_time = $host_status->{startTime} // $now;
        $status = sprintf "%dm", ($now - $start_time) / 60;
    } elsif (defined $host_status->{backoffTime} && $host_status->{backoffTime} 
> $now) {
        $status = sprintf "[%.1fh]", ($host_status->{backoffTime} - $now) / 
3600;
    } elsif (defined $host_status->{reason} && $host_status->{reason} eq 
"Reason_backup_queued") {
        $status = "QUEUE";
    } elsif (defined $host_status->{reason} && $host_status->{reason} eq 
"Reason_host_disabled") {
        $status = "Disab";
    } elsif (defined $host_status->{error}) {
        $status = "Fail";
    } else {
        my $backups_disable = $Conf->{BackupsDisable} // 0; #General config.pl 
configuration
        my $host_conf = $bpc->ConfigDataRead($host); #Host-specific 
configuration in confDir/pc
        $backups_disable = $host_conf->{BackupsDisable} if $host_conf && 
defined $host_conf->{BackupsDisable};
        if ($backups_disable == 1 && $status eq "Idle") {
            $status = "Man";
        } elsif ($backups_disable == 2 && $status eq "Idle") {
            $status = "Disab";
        }
    }

    # Get backups
    my @backups = $bpc->BackupInfoRead($host);

    foreach my $backup (@backups) {
        my $type = $backup->{type} // '';
        my $backup_time = $backup->{startTime} || 0;
        next if !$type || $type eq "partial";

        my $n_files = $backup->{nFiles} || 0;
        my $size = $backup->{size} || 0; # Bytes
        my $backup_duration = ($backup->{endTime} && $backup->{startTime}) ? 
                              commify(int(($backup->{endTime} - 
$backup->{startTime}) / 60 + 0.5)) : '-';

        my $xfer_errs = $backup->{xferErrs} || 0;
        my $xfer_bad =  $backup->{xferBadFile} || 0;

        if ($type eq "active" && $backup_time) {
            $status = sprintf "%dm", ($now - $backup_time) / 60; # Time since 
backup start in minutes
            $has_valid_backup = 1;
        }

        if ($type eq "full" || $type eq "incr") {
            $has_valid_backup = 1;
            if ($backup_time > $last_backup_time) {
                $last_backup_time = $backup_time;
                $days_since_last = sprintf "%.1f", ($now - $backup_time) / 
86400;
            }
        }

        if ($type eq "full" && (!defined $days_since_full || $backup_time >= 
($now - ($days_since_full * 86400)))) {
            $days_since_full = sprintf "%.1f", ($now - $backup_time) / 86400;
            $files_full = commify($n_files);
            $size_full = commify(int($size / 1048576)); # MiB
            $time_full = $backup_duration;
            $errs_bad_full = sprintf "%d/%d", $xfer_errs, $xfer_bad;
        }

        if ($type eq "incr" && (!defined $days_since_incr || $backup_time >= 
($now - ($days_since_incr * 86400)))) {
            $days_since_incr = sprintf "%.1f", ($now - $backup_time) / 86400;
            $files_incr = commify($n_files);
            $size_incr = commify(int($size / 1048576)); # MiB
            $time_incr = $backup_duration;
            $errs_bad_incr = sprintf "%d/%d", $xfer_errs, $xfer_bad;
        }

        if ($backup_time > $now - $lookback_days * 86400) {
            $has_recent_backup = 1;
            $max_files = $n_files if $n_files > $max_files;
            $max_size= $size if $size > $max_size;
        }
    }

    if (!$has_valid_backup) {
        push @host_nobackups, {
            host => $host, 
            status => $status,
        };
        next;
    }

    my $max_files_output = $has_recent_backup ? commify($max_files) : '-';
    my $max_size_output = $has_recent_backup ? commify(int($max_size / 
1048576)) : '-';

    push @host_data, {
        host            => $host,
        days_since_last => $days_since_last,
        status          => $status,
        days_since_full => $days_since_full // '-',
        files_full      => $files_full,
        size_full       => $size_full,
        time_full       => $time_full,
        errs_bad_full   => $errs_bad_full,
        days_since_incr => $days_since_incr // '-',
        files_incr      => $files_incr,
        size_incr       => $size_incr,
        time_incr       => $time_incr,
        errs_bad_incr   => $errs_bad_incr,
        max_files       => $max_files_output,
        max_size        => $max_size_output,
        recent_backup   => $has_recent_backup,
    };
}

# Sort by LAST column (ascending), then hostname
@host_data = sort {
    $a->{days_since_last} <=> $b->{days_since_last} ||
    $a->{host} cmp $b->{host}
} @host_data;

# Print hosts with backups
my $last_had_star = 0;
foreach my $data (@host_data) {
    if (!$data->{recent_backup} && !$last_had_star) {
        print "\n";
    }
    printf "%s%-*s %-8s %-7s | %-7s %-10s %-10s %-7s %-10s | %-7s %-10s %-10s 
%-7s %-10s | %-10s %-10s\n",
        $data->{recent_backup} ? ' ' : '*',
        $host_width,
        substr($data->{host}, 0, $host_width),
        $data->{status},
        $data->{days_since_last} == 999999 ? '-' : $data->{days_since_last},
        $data->{days_since_full},
        $data->{files_full},
        $data->{size_full},
        $data->{time_full},
        $data->{errs_bad_full},
        $data->{days_since_incr},
        $data->{files_incr},
        $data->{size_incr},
        $data->{time_incr},
        $data->{errs_bad_incr},
        $data->{max_files},
        $data->{max_size};
    $last_had_star = !$data->{recent_backup};
}

# Print hosts with no backups
print "\n";
foreach my $data (sort { $a->{host} cmp $b->{host} } @host_nobackups) {
    printf "%-*s %-8s\n", $host_width+1, "*" . substr($data->{host}, 0, 
$host_width), $data->{status};
}

# Print pool statistics
print "\nPool Statistics:\n";
printf "%-15s %-15s %-15s\n", "", "Pool", "CPool";
printf "%-15s %-15s %-15s\n", " " x 15, "-" x 15, "-" x 15;
printf "%-15s %-15s %-15s\n", "Files:", 
    commify($Info->{poolFileCnt} || 0), 
    commify($Info->{cpool4FileCnt} || 0);
printf "%-15s %-15s %-15s\n", "Size (GiB):", 
    commify(sprintf("%.2f", ($Info->{poolKb} || 0) / (1024**2))), 
    commify(sprintf("%.2f", ($Info->{cpool4Kb} || 0) / (1024**2)));
printf "%-15s %-15s %-15s\n", "Max Links:", commify($Info->{poolFileLinkMax} || 
0), commify($Info->{cpool4FileLinkMax} || 0);
printf "%-15s %-15s %-15s\n", "Removed Files:", commify($Info->{poolFileCntRm} 
|| 0), commify($Info->{cpool4FileCntRm} || 0);

# Add commas to numbers
sub commify {
    my $num = shift;
    return $num if $num eq '-';  # special case you added

    my ($int, $dec) = $num =~ /^(-?\d+)(\.\d+)?$/;
    return $num unless defined $int;

    $int = reverse $int;
    $int =~ s/(\d{3})(?=\d)/$1,/g;
    $int = reverse $int;

    return defined $dec ? "$int$dec" : $int;
}

--------------------------------------------------------------------------------

G.W. Haywood wrote at about 14:08:11 +0100 on Friday, July 25, 2025:
 > Hi there,
 > 
 > On Fri, 25 Jul 2025, Matthew Pounsett wrote:
 > 
 > > .. what I'm more interested in doing is setting up something to
 > > interpret BackupPC_serverMesg output so that I can get backup status
 > > into my actual monitoring system.
 > 
 > Two things.
 > 
 > First, there's a script here
 > 
 > https://github.com/moisseev/BackupPC_report
 > 
 > which you might find useful.  I've run it a couple of times and it
 > seems to do what it's supposed to do but I can't say more than that.
 > If it doesn't actually do what you want at least it will be a start.
 > 
 > Second, please take a look at github issue #365 and please feel free
 > to comment on the sort of things that you'd find useful in an API.
 > 
 > -- 
 > 
 > 73,
 > Ged.
 > 
 > 
 > _______________________________________________
 > BackupPC-users mailing list
 > BackupPC-users@lists.sourceforge.net
 > List:    https://lists.sourceforge.net/lists/listinfo/backuppc-users
 > Wiki:    https://github.com/backuppc/backuppc/wiki
 > Project: https://backuppc.github.io/backuppc/


_______________________________________________
BackupPC-users mailing list
BackupPC-users@lists.sourceforge.net
List:    https://lists.sourceforge.net/lists/listinfo/backuppc-users
Wiki:    https://github.com/backuppc/backuppc/wiki
Project: https://backuppc.github.io/backuppc/

Reply via email to