I wrote this Perl routine earlier this year to generate a text-based
1-page summary of my backups (180-column landscape mode).
I find it more useful and easier/faster to read and parse than the
built-in WebUI version.
I put it in my crontab and run it once a day, delivering me a text
table in my email daily
Here is the code:
--------------------------------------------------------------------------------
#!/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;
}
_______________________________________________
BackupPC-users mailing list
[email protected]
List: https://lists.sourceforge.net/lists/listinfo/backuppc-users
Wiki: https://github.com/backuppc/backuppc/wiki
Project: https://backuppc.github.io/backuppc/