* Changes for 2.1.2 release.
[BackupPC.git] / bin / BackupPC_nightly
1 #!/bin/perl
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_nightly: Nightly cleanup & statistics script.
5 #
6 # DESCRIPTION
7 #
8 #   BackupPC_nightly performs several administrative tasks:
9 #
10 #      - monthly aging of per-PC log files (only with -m option)
11 #
12 #      - pruning files from pool no longer used (ie: those with only one
13 #        hard link).
14 #
15 #      - sending email to users and administrators (only with -m option)
16 #
17 #   Usage: BackupPC_nightly [-m] poolRangeStart poolRangeEnd
18 #
19 #   Flags:
20 #
21 #     -m   Do monthly aging of per-PC log files and sending of email.
22 #          Otherise, BackupPC_nightly just does pool pruning.
23 #
24 #   The poolRangeStart and poolRangeEnd arguments are integers from 0 to 255.
25 #   These specify which parts of the pool to process.  There are 256 2nd-level
26 #   directories in the pool (0/0, 0/1, ..., f/e, f/f).  BackupPC_nightly
27 #   processes the given subset of this list (0 means 0/0, 255 means f/f).
28 #   Therefore, arguments of 0 255 process the entire pool, 0 127 does
29 #   the first half (ie: 0/0 through 7/f), 127 255 does the other half
30 #   (eg: 8/0 through f/f) and 0 15 does just the first 1/16 of the pool
31 #   (ie: 0/0 through 0/f).
32 #
33 # AUTHOR
34 #   Craig Barratt  <cbarratt@users.sourceforge.net>
35 #
36 # COPYRIGHT
37 #   Copyright (C) 2001-2004  Craig Barratt
38 #
39 #   This program is free software; you can redistribute it and/or modify
40 #   it under the terms of the GNU General Public License as published by
41 #   the Free Software Foundation; either version 2 of the License, or
42 #   (at your option) any later version.
43 #
44 #   This program is distributed in the hope that it will be useful,
45 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
46 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
47 #   GNU General Public License for more details.
48 #
49 #   You should have received a copy of the GNU General Public License
50 #   along with this program; if not, write to the Free Software
51 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
52 #
53 #========================================================================
54 #
55 # Version 2.1.2, released 5 Sep 2005.
56 #
57 # See http://backuppc.sourceforge.net.
58 #
59 #========================================================================
60
61 use strict;
62 no  utf8;
63 use lib "/usr/local/BackupPC2.1.0/lib";
64 use BackupPC::Lib;
65 use BackupPC::FileZIO;
66 use Getopt::Std;
67
68 use File::Find;
69 use File::Path;
70 use Data::Dumper;
71
72 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
73 my $TopDir = $bpc->TopDir();
74 my $BinDir = $bpc->BinDir();
75 my %Conf   = $bpc->Conf();
76 my(%Status, %Info, %Jobs, @BgQueue, @UserQueue, @CmdQueue);
77
78 $bpc->ChildInit();
79
80 my %opts;
81 if ( !getopts("m", \%opts) || @ARGV != 2 ) {
82     print("usage: $0 [-m] poolRangeStart poolRangeEnd\n");
83     exit(1);
84 }
85 if ( $ARGV[0] !~ /^(\d+)$/ || $1 > 255 ) {
86     print("$0: bad poolRangeStart '$ARGV[0]'\n");
87     exit(1);
88 }
89 my $poolRangeStart = $1;
90 if ( $ARGV[1] !~ /^(\d+)$/ || $1 > 255 ) {
91     print("$0: bad poolRangeEnd '$ARGV[1]'\n");
92     exit(1);
93 }
94 my $poolRangeEnd = $1;
95
96 if ( $opts{m} ) {
97     my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
98     if ( $err ) {
99         print("Can't connect to server ($err)\n");
100         exit(1);
101     }
102     my $reply = $bpc->ServerMesg("status hosts");
103     $reply = $1 if ( $reply =~ /(.*)/s );
104     eval($reply);
105 }
106
107 ###########################################################################
108 # When BackupPC_nightly starts, BackupPC will not run any simultaneous
109 # BackupPC_dump commands.  We first do things that contend with
110 # BackupPC_dump, eg: aging per-PC log files etc.
111 ###########################################################################
112 doPerPCLogFileAging() if ( $opts{m} );
113
114 ###########################################################################
115 # Get statistics on the pool, and remove files that have only one link.
116 ###########################################################################
117
118 my $fileCnt;       # total number of files
119 my $dirCnt;        # total number of directories
120 my $blkCnt;        # total block size of files
121 my $fileCntRm;     # total number of removed files
122 my $blkCntRm;      # total block size of removed files
123 my $blkCnt2;       # total block size of files with just 2 links
124                    # (ie: files that only occur once among all backups)
125 my $fileCntRep;    # total number of file names containing "_", ie: files
126                    # that have repeated md5 checksums
127 my $fileRepMax;    # worse case number of files that have repeated checksums
128                    # (ie: max(nnn+1) for all names xxxxxxxxxxxxxxxx_nnn)
129 my $fileLinkMax;   # maximum number of hardlinks on a pool file
130 my $fileCntRename; # number of renamed files (to keep file numbering
131                    # contiguous)
132 my %FixList;       # list of paths that need to be renamed to avoid
133                    # new holes
134 my @hexChars = qw(0 1 2 3 4 5 6 7 8 9 a b c d e f);
135
136 for my $pool ( qw(pool cpool) ) {
137     for ( my $i = $poolRangeStart ; $i <= $poolRangeEnd ; $i++ ) {
138         my $dir        = "$hexChars[int($i / 16)]/$hexChars[$i % 16]";
139         # print("Doing $pool/$dir\n") if ( ($i % 16) == 0 );
140         $fileCnt       = 0;
141         $dirCnt        = 0;
142         $blkCnt        = 0;
143         $fileCntRm     = 0;
144         $blkCntRm      = 0;
145         $blkCnt2       = 0;
146         $fileCntRep    = 0;
147         $fileRepMax    = 0;
148         $fileLinkMax   = 0;
149         $fileCntRename = 0;
150         %FixList       = ();
151         find({wanted => \&GetPoolStats}, "$TopDir/$pool/$dir")
152                                             if ( -d "$TopDir/$pool/$dir" );
153         my $kb   = $blkCnt / 2;
154         my $kbRm = $blkCntRm / 2;
155         my $kb2  = $blkCnt2 / 2;
156
157         #
158         # Main BackupPC_nightly counts the top-level directory
159         #
160         $dirCnt++ if ( $opts{m} && -d "$TopDir/$pool" && $i == 0 );
161
162         #
163         # Also count the next level directories
164         #
165         $dirCnt++ if ( ($i % 16) == 0
166                        && -d "$TopDir/$pool/$hexChars[int($i / 16)]" );
167
168         #
169         # Now make sure that files with repeated checksums are still
170         # sequentially numbered
171         #
172         foreach my $name ( sort(keys(%FixList)) ) {
173             my $rmCnt = $FixList{$name} + 1;
174             my $new = -1;
175             for ( my $old = -1 ; ; $old++ ) {
176                 my $oldName = $name;
177                 $oldName .= "_$old" if ( $old >= 0 );
178                 if ( !-f $oldName ) {
179                     #
180                     # We know we are done when we have missed at least
181                     # the number of files that were removed from this
182                     # base name, plus a couple just to be sure
183                     #
184                     last if ( $rmCnt-- <= 0 );
185                     next;
186                 }
187                 my $newName = $name;
188                 $newName .= "_$new" if ( $new >= 0 );
189                 $new++;
190                 next if ( $oldName eq $newName );
191                 rename($oldName, $newName);
192                 $fileCntRename++;
193             }
194         }
195         print("BackupPC_stats $i = $pool,$fileCnt,$dirCnt,$kb,$kb2,$kbRm,"
196                               . "$fileCntRm,$fileCntRep,$fileRepMax,"
197                               . "$fileCntRename,$fileLinkMax\n");
198     }
199 }
200
201 ###########################################################################
202 # Tell BackupPC that it is now ok to start running BackupPC_dump
203 # commands.  We are guaranteed that no BackupPC_link commands will
204 # run since only a single CmdQueue command runs at a time, and
205 # that means we are safe.
206 ###########################################################################
207 printf("BackupPC_nightly lock_off\n");
208
209 ###########################################################################
210 # Send email 
211 ###########################################################################
212 if ( $opts{m} ) {
213     print("log BackupPC_nightly now running BackupPC_sendEmail\n");
214     system("$BinDir/BackupPC_sendEmail")
215 }
216
217 #
218 # Do per-PC log file aging
219 #
220 sub doPerPCLogFileAging
221 {
222     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
223     if ( $mday == 1 ) {
224         foreach my $host ( keys(%Status) ) {
225             my $lastLog = $Conf{MaxOldPerPCLogFiles} - 1;
226             unlink("$TopDir/pc/$host/LOG.$lastLog")
227                     if ( -f "$TopDir/pc/$host/LOG.$lastLog" );
228             unlink("$TopDir/pc/$host/LOG.$lastLog.z")
229                     if ( -f "$TopDir/pc/$host/LOG.$lastLog.z" );
230             for ( my $i = $lastLog - 1 ; $i >= 0 ; $i-- ) {
231                 my $j = $i + 1;
232                 if ( -f "$TopDir/pc/$host/LOG.$i" ) {
233                     rename("$TopDir/pc/$host/LOG.$i",
234                            "$TopDir/pc/$host/LOG.$j");
235                 } elsif ( -f "$TopDir/pc/$host/LOG.$i.z" ) {
236                     rename("$TopDir/pc/$host/LOG.$i.z",
237                            "$TopDir/pc/$host/LOG.$j.z");
238                 }
239             }
240             #
241             # Compress the log file LOG -> LOG.0.z (if enabled).
242             # Otherwise, just rename LOG -> LOG.0.
243             #
244             BackupPC::FileZIO->compressCopy("$TopDir/pc/$host/LOG",
245                                             "$TopDir/pc/$host/LOG.0.z",
246                                             "$TopDir/pc/$host/LOG.0",
247                                             $Conf{CompressLevel}, 1);
248             open(LOG, ">", "$TopDir/pc/$host/LOG") && close(LOG);
249         }
250     }
251 }
252
253 sub GetPoolStats
254 {
255     my($nlinks, $nblocks) = (lstat($_))[3, 12];
256  
257     if ( -d _ ) {
258         $dirCnt++;
259         return;
260     } elsif ( ! -f _ ) {
261         return;
262     }
263     if ( $nlinks == 1 ) {
264         $blkCntRm += $nblocks;
265         $fileCntRm++;
266         unlink($_);
267         #
268         # We must keep repeated files numbered sequential (ie: files
269         # that have the same checksum are appended with _0, _1 etc).
270         # There are two cases: we remove the base file xxxx, but xxxx_0
271         # exists, or we remove any file of the form xxxx_nnn.  We remember
272         # the base name and fix it up later (not in the middle of find).
273         #
274         my($baseName);
275         ($baseName = $File::Find::name) =~ s/_\d+$//;
276         $FixList{$baseName}++;
277     } else {
278         if ( /_(\d+)$/ ) {
279             $fileRepMax = $1 + 1 if ( $fileRepMax <= $1 );
280             $fileCntRep++;
281         }
282         $fileCnt += 1;
283         $blkCnt  += $nblocks;
284         $blkCnt2 += $nblocks if ( $nlinks == 2 );
285         $fileLinkMax = $nlinks if ( $fileLinkMax < $nlinks );
286     }
287 }