/[psinib]/psinib.pl
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Annotation of /psinib.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.21 - (hide annotations)
Sat Nov 8 01:46:53 2003 UTC (20 years, 4 months ago) by dpavlin
Branch: MAIN
Changes since 1.20: +1 -1 lines
File MIME type: text/plain
don't warn on undef if there isn't subjects.txt file

1 dpavlin 1.1 #!/usr/bin/perl -w
2     #
3     # psinib - Perl Snapshot Is Not Incremental Backup
4     #
5     # written by Dobrica Pavlinusic <dpavlin@rot13.org> 2003-01-03
6     # released under GPL v2 or later.
7     #
8     # Backup SMB directories using file produced by LinNeighbourhood (or some
9     # other program [vi :-)] which produces file in format:
10     #
11     # smbmount service mountpoint options
12     #
13     #
14     # usage:
15 dpavlin 1.4 # $ psinib.pl mountscript
16 dpavlin 1.1
17     use strict 'vars';
18     use Data::Dumper;
19     use Net::Ping;
20     use POSIX qw(strftime);
21     use List::Compare;
22     use Filesys::SmbClient;
23     #use Taint;
24 dpavlin 1.2 use Fcntl qw(LOCK_EX LOCK_NB);
25 dpavlin 1.4 use Digest::MD5;
26     use File::Basename;
27 dpavlin 1.14 use Getopt::Long;
28 dpavlin 1.1
29     # configuration
30     my $LOG_TIME_FMT = '%Y-%m-%d %H:%M:%S'; # strftime format for logfile
31     my $DIR_TIME_FMT = '%Y%m%d'; # strftime format for backup dir
32    
33 dpavlin 1.16 # define timeout for ping
34     my $PING_TIMEOUT = 5;
35    
36 dpavlin 1.1 my $LOG = '/var/log/backup.log'; # add path here...
37 dpavlin 1.5 #$LOG = '/tmp/backup.log';
38 dpavlin 1.1
39     # store backups in which directory
40 dpavlin 1.13 my $BACKUP_DEST = '/backup/isis_backup';
41     #my $BACKUP_DEST = '/tmp/backup/';
42 dpavlin 1.1
43     # files to ignore in backup
44     my @ignore = ('.md5sum', '.backupignore', 'backupignore.txt');
45    
46     # open log
47 dpavlin 1.5 open(L, ">> $LOG") || die "can't open log $LOG: $!";
48 dpavlin 1.1 select((select(L), $|=1)[0]); # flush output
49 dpavlin 1.2
50     # make a lock on logfile
51    
52     my $c = 0;
53     {
54     flock L, LOCK_EX | LOCK_NB and last;
55     sleep 1;
56     redo if ++$c < 10;
57     # no response for 10 sec, bail out
58 dpavlin 1.12 xlog("ABORT","can't take lock on $LOG -- another $0 running?");
59 dpavlin 1.2 exit 1;
60     }
61 dpavlin 1.1
62     # taint path: nmblookup should be there!
63     $ENV{'PATH'} = "/usr/bin:/bin";
64    
65 dpavlin 1.17 my $use_ping = 1; # default: use syn tcp ping to verify that host is up
66     my $verbose = 1; # default verbosity level
67     my $quiet = 0;
68 dpavlin 1.20 my $email;
69 dpavlin 1.14
70     my $result = GetOptions(
71     "ping!" => \$use_ping, "backupdest!" => \$BACKUP_DEST,
72 dpavlin 1.17 "verbose+" => \$verbose, "quiet+" => \$quiet,
73 dpavlin 1.20 "email=s" => \$email,
74 dpavlin 1.14 );
75    
76 dpavlin 1.17 $verbose -= $quiet;
77    
78 dpavlin 1.1 my $mounts = shift @ARGV ||
79     'mountscript';
80     # die "usage: $0 mountscript";
81    
82 dpavlin 1.20 my $basedir = $0;
83     $basedir =~ s,/?[^/]+$,,g;
84    
85     # default subject for e-mail messages
86     my @subjects = ('Backup needs your attention!');
87     my $sub_nr = 0;
88     my $email_body;
89    
90     if ($email) {
91     # It will use (and require) Tie::File only if --email=foo@bar.com
92     # arguement is used!
93     use Tie::File;
94     tie @subjects, 'Tie::File', "$basedir/subjects.txt" || xlog("CONFIG","Can't find $basedir/subjects.txt... using default (only one)");
95     chdir; # this will change directory to HOME
96     if (open(SN,".psinib.subject")) {
97     $sub_nr = <SN>;
98     chomp($sub_nr);
99     close(SN);
100     }
101     $sub_nr++;
102     # skip comments in subjects.txt
103 dpavlin 1.21 while($subjects[$sub_nr] && $subjects[$sub_nr] =~ m/^#/) {
104 dpavlin 1.20 $sub_nr++;
105     }
106     $sub_nr = 0 if (! $subjects[$sub_nr]);
107    
108     if (open(SN,"> .psinib.subject")) {
109     print SN "$sub_nr\n";
110     close (SN);
111     } else {
112     xlog("CONFIG","Can't open .psinib.subject -- I can't cycle subjects...");
113     };
114     }
115 dpavlin 1.1
116     my @in_backup; # shares which are backeduped this run
117    
118 dpavlin 1.16 # init Net::Ping object
119 dpavlin 1.14 my $ping;
120     if ($use_ping) {
121 dpavlin 1.16 $ping = new Net::Ping->new("syn", 2);
122 dpavlin 1.14 # ping will try tcp connect to netbios-ssn (139)
123     $ping->{port_num} = getservbyname("netbios-ssn", "tcp");
124     }
125 dpavlin 1.1
126 dpavlin 1.16 # do syn ping to cifs port
127     sub host_up {
128     my $ping = shift || return;
129     my $host_ip = shift || xlog("host_up didn't get IP");
130     my $timeout = shift;
131     return 1 if (! $use_ping);
132    
133     $ping->ping($host_ip,$timeout);
134     my $return = 0;
135    
136     while (my ($host,$rtt,$ip) = $ping->ack) {
137     xlog("","HOST: $host [$ip] ACKed in $rtt seconds");
138     $return = 1 if ($ip eq $host_ip);
139     }
140     return $return;
141     }
142    
143 dpavlin 1.1 my $backup_ok = 0;
144    
145     my $smb;
146     my %smb_atime;
147     my %smb_mtime;
148 dpavlin 1.4 my %file_md5;
149 dpavlin 1.1
150     open(M, $mounts) || die "can't open $mounts: $!";
151     while(<M>) {
152     chomp;
153     next if !/^\s*smbmount\s/;
154     my (undef,$share,undef,$opt) = split(/\s+/,$_,4);
155    
156 dpavlin 1.11 my ($user,$passwd,$workgroup,$ip);
157 dpavlin 1.1
158     foreach (split(/,/,$opt)) {
159     my ($n,$v) = split(/=/,$_,2);
160     if ($n =~ m/username/i) {
161     if ($v =~ m#^(.+)/(.+)%(.+)$#) {
162     ($user,$passwd,$workgroup) = ($1,$2,$3);
163     } elsif ($v =~ m#^(.+)/(.+)$#) {
164     ($user,$workgroup) = ($1,$2);
165     } elsif ($v =~ m#^(.+)%(.+)$#) {
166     ($user,$passwd) = ($1,$2);
167     } else {
168     $user = $v;
169     }
170     } elsif ($n =~ m#workgroup#i) {
171     $workgroup = $v;
172 dpavlin 1.11 } elsif ($n =~ m#ip#i) {
173     $ip = $v;
174 dpavlin 1.1 }
175     }
176    
177     push @in_backup,$share;
178    
179 dpavlin 1.4
180     my ($host,$dir,$date_dir) = share2host_dir($share);
181     my $bl = "$BACKUP_DEST/$host/$dir/latest"; # latest backup
182     my $bc = "$BACKUP_DEST/$host/$dir/$date_dir"; # current one
183     my $real_bl;
184 dpavlin 1.9 if (-l $bl) {
185 dpavlin 1.4 $real_bl=readlink($bl) || die "can't read link $bl: $!";
186     $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
187 dpavlin 1.9 if (-l $bc && $real_bl eq $bc) {
188 dpavlin 1.17 xlog($share,"allready backuped...");
189 dpavlin 1.4 $backup_ok++;
190     next;
191     }
192    
193     }
194    
195    
196 dpavlin 1.17 xlog($share,"working on $share...");
197 dpavlin 1.1
198 dpavlin 1.11 # try to nmblookup IP
199     $ip = get_ip($share) if (! $ip);
200 dpavlin 1.1
201     if ($ip) {
202     xlog($share,"IP is $ip");
203 dpavlin 1.16 if (host_up($ping, $ip,$PING_TIMEOUT)) {
204 dpavlin 1.12 if (snap_share($share,$user,$passwd,$workgroup)) {
205     $backup_ok++;
206     }
207 dpavlin 1.1 }
208     }
209     }
210     close(M);
211    
212 dpavlin 1.20 my $total = ($#in_backup + 1) || 0;
213     my $pcnt = "";
214     $pcnt = "(".int($backup_ok*100/$total)." %)" if ($total > 0);
215     xlog("","$backup_ok backups completed of total $total this time".$pcnt);
216    
217     send_email();
218 dpavlin 1.1
219     1;
220    
221     #-------------------------------------------------------------------------
222    
223 dpavlin 1.4
224 dpavlin 1.1 # get IP number from share
225     sub get_ip {
226     my $share = shift;
227    
228     my $host = $1 if ($share =~ m#//([^/]+)/#);
229    
230     my $ip = `nmblookup $host`;
231     if ($ip =~ m/(\d+\.\d+\.\d+\.\d+)\s$host/i) {
232     return $1;
233     }
234     }
235    
236 dpavlin 1.20 # send e-mail with all messages
237     sub send_email {
238     return if (! $email || $email eq "" || !$email_body);
239     require Mail::Send;
240     my $msg = new Mail::Send;
241     $msg->to($email);
242     $msg->subject($subjects[$sub_nr]);
243     my $fn=$msg->open;
244     print $fn $email_body;
245     $fn->close;
246     }
247    
248 dpavlin 1.4
249     # write entry to screen and log
250 dpavlin 1.1 sub xlog {
251     my $share = shift;
252     my $t = strftime $LOG_TIME_FMT, localtime;
253     my $m = shift || '[no log entry]';
254 dpavlin 1.18 my $l = shift;
255     $l = 1 if (! defined $l); # default verbosity is 1
256 dpavlin 1.20 if ($verbose >= $l) {
257     if (! $email) {
258     print STDERR $m,"\n";
259     # don't e-mail mesages with verbosity < 1
260     } elsif ($l < 1) {
261     $email_body .= $m."\n";
262     }
263     }
264 dpavlin 1.1 print L "$t $share\t$m\n";
265     }
266    
267 dpavlin 1.7 # dump warn and dies into log
268 dpavlin 1.20 BEGIN { $SIG{'__WARN__'} = sub { xlog('WARN',$_[0],1) ; exit 1 } }
269     BEGIN { $SIG{'__DIE__'} = sub { xlog('DIE',$_[0],0) ; exit 1 } }
270 dpavlin 1.7
271 dpavlin 1.1
272 dpavlin 1.4 # split share name to host, dir and currnet date dir
273     sub share2host_dir {
274 dpavlin 1.1 my $share = shift;
275     my ($host,$dir);
276     if ($share =~ m#//([^/]+)/(.+)$#) {
277     ($host,$dir) = ($1,$2);
278     $dir =~ s/\W/_/g;
279     $dir =~ s/^_+//;
280     $dir =~ s/_+$//;
281     } else {
282 dpavlin 1.17 xlog($share,"Can't parse share $share into host and directory!",1);
283 dpavlin 1.1 return;
284     }
285 dpavlin 1.4 return ($host,$dir,strftime $DIR_TIME_FMT, localtime);
286     }
287    
288 dpavlin 1.1
289 dpavlin 1.4 # make a snapshot of a share
290     sub snap_share {
291    
292     my $share = shift;
293    
294     my %param = ( debug => 0 );
295    
296 dpavlin 1.8 $param{username} = shift || warn "can't find username for share $share";
297     $param{password} = shift || warn "can't find passwod for share $share";
298     $param{workgroup} = shift || warn "can't find workgroup for share $share";
299 dpavlin 1.4
300     my ($host,$dir,$date_dir) = share2host_dir($share);
301 dpavlin 1.1
302     # latest backup directory
303     my $bl = "$BACKUP_DEST/$host/$dir/latest";
304     # current backup directory
305     my $bc = "$BACKUP_DEST/$host/$dir/$date_dir";
306    
307     my $real_bl;
308 dpavlin 1.9 if (-l $bl) {
309 dpavlin 1.1 $real_bl=readlink($bl) || die "can't read link $bl: $!";
310     $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
311 dpavlin 1.19 if (! -e $real_bl) {
312     xlog($share,"latest link $bl -> $real_bl not valid, removing it");
313     unlink $bl || die "can't remove link $bl: $!";
314     undef $real_bl;
315     }
316 dpavlin 1.18 }
317     if (! $real_bl) {
318 dpavlin 1.19 xlog($share,"no old backup, trying to find last backup");
319 dpavlin 1.7 if (opendir(BL_DIR, "$BACKUP_DEST/$host/$dir")) {
320     my @bl_dirs = sort grep { !/^\./ && -d "$BACKUP_DEST/$host/$dir/$_" } readdir(BL_DIR);
321     closedir(BL_DIR);
322     $real_bl=pop @bl_dirs;
323 dpavlin 1.17 xlog($share,"using $real_bl as latest...");
324 dpavlin 1.7 $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
325     if ($real_bl eq $bc) {
326     xlog($share,"latest from today (possible partial backup)");
327     rename $real_bl,$real_bl.".partial" || warn "can't reaname partial backup: $!";
328     $real_bl .= ".partial";
329     }
330     } else {
331 dpavlin 1.17 xlog($share,"this is first run...");
332 dpavlin 1.7 }
333 dpavlin 1.1 }
334    
335 dpavlin 1.9 if (-l $bc && $real_bl && $real_bl eq $bc) {
336 dpavlin 1.17 xlog($share,"allready backuped...");
337 dpavlin 1.12 return 1;
338 dpavlin 1.1 }
339    
340     die "You should really create BACKUP_DEST [$BACKUP_DEST] by hand! " if (!-e $BACKUP_DEST);
341    
342     if (! -e "$BACKUP_DEST/$host") {
343     mkdir "$BACKUP_DEST/$host" || die "can't make dir for host $host, $BACKUP_DEST/$host: $!";
344 dpavlin 1.17 xlog($share,"created host directory $BACKUP_DEST/$host...");
345 dpavlin 1.1 }
346    
347     if (! -e "$BACKUP_DEST/$host/$dir") {
348     mkdir "$BACKUP_DEST/$host/$dir" || die "can't make dir for share $share, $BACKUP_DEST/$host/$dir $!";
349 dpavlin 1.17 xlog($share,"created dir for this share $BACKUP_DEST/$host/$dir...");
350 dpavlin 1.1 }
351    
352     mkdir $bc || die "can't make dir for current backup $bc: $!";
353    
354     my @dirs = ( "/" );
355     my @smb_dirs = ( "/" );
356    
357     my $transfer = 0; # bytes transfered over network
358    
359     # this will store all available files and sizes
360     my @files;
361     my %file_size;
362     my %file_atime;
363     my %file_mtime;
364 dpavlin 1.4 #my %file_md5;
365 dpavlin 1.13 %file_md5 = ();
366 dpavlin 1.1
367     my @smb_files;
368     my %smb_size;
369     #my %smb_atime;
370     #my %smb_mtime;
371    
372     sub norm_dir {
373     my $foo = shift;
374     my $prefix = shift;
375     $foo =~ s#//+#/#g;
376     $foo =~ s#/+$##g;
377     $foo =~ s#^/+##g;
378     return $prefix.$foo if ($prefix);
379     return $foo;
380     }
381    
382     # read local filesystem
383     my $di = 0;
384     while ($di <= $#dirs && $real_bl) {
385     my $d=$dirs[$di++];
386 dpavlin 1.7 opendir(DIR,"$real_bl/$d") || warn "opendir($real_bl/$d): $!\n";
387 dpavlin 1.1
388     # read .backupignore if exists
389 dpavlin 1.7 if (-f "$real_bl/$d/.backupignore") {
390     open(I,"$real_bl/$d/.backupignore");
391 dpavlin 1.1 while(<I>) {
392     chomp;
393     push @ignore,norm_dir("$d/$_");
394     }
395     close(I);
396 dpavlin 1.9 #print STDERR "ignore: ",join("|",@ignore),"\n";
397 dpavlin 1.7 link "$real_bl/$d/.backupignore","$bc/$d/.backupignore" ||
398     warn "can't copy $real_bl/$d/.backupignore to current backup dir: $!\n";
399 dpavlin 1.1 }
400    
401     # read .md5sum if exists
402 dpavlin 1.7 if (-f "$real_bl/$d/.md5sum") {
403     open(I,"$real_bl/$d/.md5sum");
404 dpavlin 1.1 while(<I>) {
405     chomp;
406     my ($md5,$f) = split(/\s+/,$_,2);
407     $file_md5{$f}=$md5;
408     }
409     close(I);
410     }
411    
412     my @clutter = readdir(DIR);
413     foreach my $f (@clutter) {
414     next if ($f eq '.');
415     next if ($f eq '..');
416     my $pr = norm_dir("$d/$f"); # path relative
417 dpavlin 1.7 my $pf = norm_dir("$d/$f","$real_bl/"); # path full
418 dpavlin 1.1 if (grep(/^\Q$pr\E$/,@ignore) == 0) {
419     if (-f $pf) {
420     push @files,$pr;
421     $file_size{$pr}=(stat($pf))[7];
422     $file_atime{$pr}=(stat($pf))[8];
423     $file_mtime{$pr}=(stat($pf))[9];
424     } elsif (-d $pf) {
425     push @dirs,$pr;
426     } else {
427 dpavlin 1.17 xlog($share,"not file or directory: $pf",0);
428 dpavlin 1.1 }
429     } else {
430 dpavlin 1.17 xlog($share,"ignored: $pr");
431 dpavlin 1.1 }
432     }
433     }
434    
435 dpavlin 1.12 # local dir always include /
436     xlog($share,($#files+1)." files and ".($#dirs)." dirs on local disk before backup");
437 dpavlin 1.1
438     # read smb filesystem
439    
440     xlog($share,"smb to $share as $param{username}/$param{workgroup}");
441    
442     # FIX: how to aviod creation of ~/.smb/smb.conf ?
443     $smb = new Filesys::SmbClient(%param) || die "SmbClient :$!\n";
444    
445     $di = 0;
446     while ($di <= $#smb_dirs) {
447 dpavlin 1.9 my $d=$smb_dirs[$di];
448 dpavlin 1.1 my $pf = norm_dir($d,"smb:$share/"); # path full
449 dpavlin 1.9 my $D = $smb->opendir($pf);
450     if (! $D) {
451 dpavlin 1.18 xlog($share,"FATAL: $share [$pf] as $param{username}/$param{workgroup}: $!",0);
452 dpavlin 1.9 # remove failing dir
453     delete $smb_dirs[$di];
454 dpavlin 1.12 return 0; # failed
455 dpavlin 1.9 }
456     $di++;
457 dpavlin 1.1
458     my @clutter = $smb->readdir_struct($D);
459     foreach my $item (@clutter) {
460     my $f = $item->[1];
461     next if ($f eq '.');
462     next if ($f eq '..');
463     my $pr = norm_dir("$d/$f"); # path relative
464     my $pf = norm_dir("$d/$f","smb:$share/"); # path full
465     if (grep(/^\Q$pr\E$/,@ignore) == 0) {
466     if ($item->[0] == main::SMBC_FILE) {
467     push @smb_files,$pr;
468     $smb_size{$pr}=($smb->stat($pf))[7];
469     $smb_atime{$pr}=($smb->stat($pf))[10];
470     $smb_mtime{$pr}=($smb->stat($pf))[11];
471     } elsif ($item->[0] == main::SMBC_DIR) {
472     push @smb_dirs,$pr;
473     } else {
474 dpavlin 1.17 xlog($share,"not file or directory [".$item->[0]."]: $pf",0);
475 dpavlin 1.1 }
476     } else {
477 dpavlin 1.17 xlog($share,"smb ignored: $pr");
478 dpavlin 1.1 }
479     }
480     }
481    
482 dpavlin 1.12 xlog($share,($#smb_files+1)." files and ".($#smb_dirs)." dirs on remote share");
483 dpavlin 1.1
484     # sync dirs
485     my $lc = List::Compare->new(\@dirs, \@smb_dirs);
486    
487     my @dirs2erase = $lc->get_Lonly;
488     my @dirs2create = $lc->get_Ronly;
489     xlog($share,($#dirs2erase+1)." dirs to erase and ".($#dirs2create+1)." dirs to create");
490    
491     # create new dirs
492     foreach (sort @smb_dirs) {
493     mkdir "$bc/$_" || warn "mkdir $_: $!\n";
494     }
495    
496     # sync files
497     $lc = List::Compare->new(\@files, \@smb_files);
498    
499     my @files2erase = $lc->get_Lonly;
500     my @files2create = $lc->get_Ronly;
501     xlog($share,($#files2erase+1)." files to erase and ".($#files2create+1)." files to create");
502    
503     sub smb_copy {
504     my $smb = shift;
505    
506     my $from = shift;
507     my $to = shift;
508    
509    
510     my $l = 0;
511    
512     foreach my $f (@_) {
513     #print "smb_copy $from/$f -> $to/$f\n";
514 dpavlin 1.4 my $md5 = Digest::MD5->new;
515    
516 dpavlin 1.1 my $fd = $smb->open("$from/$f");
517     if (! $fd) {
518 dpavlin 1.12 xlog("WARNING","can't open smb file $from/$f: $!");
519     next;
520     }
521    
522     if (! open(F,"> $to/$f")) {
523     xlog("WARNING","can't open new file $to/$f: $!");
524 dpavlin 1.1 next;
525     }
526    
527     while (defined(my $b=$smb->read($fd,4096))) {
528     print F $b;
529     $l += length($b);
530 dpavlin 1.4 $md5->add($b);
531 dpavlin 1.1 }
532    
533     $smb->close($fd);
534     close(F);
535    
536 dpavlin 1.4 $file_md5{$f} = $md5->hexdigest;
537    
538 dpavlin 1.1 # FIX: this fails with -T
539     my ($a,$m) = ($smb->stat("$from/$f"))[10,11];
540     utime $a, $m, "$to/$f" ||
541     warn "can't update utime on $to/$f: $!\n";
542    
543     }
544     return $l;
545     }
546    
547     # copy new files
548     foreach (@files2create) {
549     $transfer += smb_copy($smb,"smb:$share",$bc,$_);
550     }
551    
552     my $size_sync = 0;
553     my $atime_sync = 0;
554     my $mtime_sync = 0;
555     my @sync_files;
556     my @ln_files;
557    
558     foreach ($lc->get_intersection) {
559    
560     my $f;
561    
562     if ($file_size{$_} != $smb_size{$_}) {
563     $f=$_;
564     $size_sync++;
565     }
566     if ($file_atime{$_} != $smb_atime{$_}) {
567     $f=$_;
568     $atime_sync++;
569     }
570     if ($file_mtime{$_} != $smb_mtime{$_}) {
571     $f=$_;
572     $mtime_sync++;
573     }
574    
575     if ($f) {
576     push @sync_files, $f;
577     } else {
578     push @ln_files, $_;
579     }
580     }
581    
582     xlog($share,($#sync_files+1)." files will be updated (diff: $size_sync size, $atime_sync atime, $mtime_sync mtime), ".($#ln_files+1)." will be linked.");
583    
584     foreach (@sync_files) {
585     $transfer += smb_copy($smb,"smb:$share",$bc,$_);
586     }
587    
588     xlog($share,"$transfer bytes transfered...");
589    
590     foreach (@ln_files) {
591 dpavlin 1.7 link "$real_bl/$_","$bc/$_" || warn "link $real_bl/$_ -> $bc/$_: $!\n";
592 dpavlin 1.1 }
593    
594     # remove files
595     foreach (sort @files2erase) {
596     unlink "$bc/$_" || warn "unlink $_: $!\n";
597 dpavlin 1.13 delete $file_md5{$_};
598 dpavlin 1.1 }
599    
600     # remove not needed dirs (after files)
601     foreach (sort @dirs2erase) {
602     rmdir "$bc/$_" || warn "rmdir $_: $!\n";
603     }
604    
605 dpavlin 1.4 # remove old .md5sum
606     foreach (sort @dirs) {
607     unlink "$bc/$_/.md5sum" if (-e "$bc/$_/.md5sum");
608     }
609    
610 dpavlin 1.13 # erase stale entries in .md5sum
611     my @md5_files = keys %file_md5;
612     $lc = List::Compare->new(\@md5_files, \@smb_files);
613     foreach my $file ($lc->get_Lonly) {
614     xlog("NOTICE","removing stale '$file' from .md5sum");
615     delete $file_md5{$file};
616     }
617    
618 dpavlin 1.4 # create .md5sum
619     my $last_dir = '';
620     my $md5;
621 dpavlin 1.7 foreach my $f (sort { $file_md5{$a} cmp $file_md5{$b} } keys %file_md5) {
622 dpavlin 1.4 my $dir = dirname($f);
623     my $file = basename($f);
624 dpavlin 1.10 #print "$f -- $dir / $file<--\n";
625 dpavlin 1.4 if ($dir ne $last_dir) {
626     close($md5) if ($md5);
627     open($md5, ">> $bc/$dir/.md5sum") || warn "can't create $bc/$dir/.md5sum: $!";
628     $last_dir = $dir;
629 dpavlin 1.7 #print STDERR "writing $last_dir/.md5sum\n";
630 dpavlin 1.4 }
631     print $md5 $file_md5{$f}," $file\n";
632     }
633 dpavlin 1.11 close($md5) if ($md5);
634 dpavlin 1.1
635     # create leatest link
636 dpavlin 1.7 #print "ln -s $bc $real_bl\n";
637 dpavlin 1.9 if (-l $bl) {
638 dpavlin 1.7 unlink $bl || warn "can't remove old latest symlink $bl: $!\n";
639     }
640     symlink $bc,$bl || warn "can't create latest symlink $bl -> $bc: $!\n";
641    
642     # FIX: sanity check -- remove for speedup
643 dpavlin 1.9 xlog($share,"failed to create latest symlink $bl -> $bc...") if (readlink($bl) ne $bc || ! -l $bl);
644 dpavlin 1.1
645     xlog($share,"backup completed...");
646 dpavlin 1.12
647     return 1;
648 dpavlin 1.1 }
649 dpavlin 1.3 __END__
650 dpavlin 1.1 #-------------------------------------------------------------------------
651    
652 dpavlin 1.3
653     =head1 NAME
654    
655     psinib - Perl Snapshot Is Not Incremental Backup
656    
657     =head1 SYNOPSIS
658    
659 dpavlin 1.17 ./psinib.pl [OPTION]... [mount script]
660 dpavlin 1.3
661     =head1 DESCRIPTION
662    
663 dpavlin 1.17 Option can be one of more of following:
664    
665     =over 8
666    
667     =item C<--backupdest=dir>
668    
669     Specify backup destination directory (defaults is /data/
670    
671     =item C<--noping>
672    
673     Don't use ping to check if host is up (default is ti use tcp syn to cifs
674     port)
675    
676     =item C<--verbose -v>
677    
678     Increase verbosity level. Defailt is 1 which prints moderate amount of data
679     on STDOUT and STDERR.
680    
681     =item C<--quiet -q>
682    
683     Decrease verbosity level
684    
685 dpavlin 1.20 =item C<--email=email@domain>
686    
687     Send e-mails instead of dumping errors to STDERR. Useful for cron jobs.
688    
689 dpavlin 1.17 =back
690    
691 dpavlin 1.3 This script in current version support just backup of Samba (or Micro$oft
692     Winblowz) shares to central disk space. Central disk space is organized in
693     multiple directories named after:
694    
695     =over 4
696    
697     =item *
698     server which is sharing files to be backed up
699    
700     =item *
701     name of share on server
702    
703     =item *
704     dated directory named like standard ISO date format (YYYYMMDD).
705    
706     =back
707    
708     In each dated directory you will find I<snapshot> of all files on
709     exported share on that particular date.
710    
711     You can also use symlink I<latest> which will lead you to
712     last completed backup. After that you can use some other backup
713     software to transfer I<snapshot> to tape, CD-ROM or some other media.
714    
715     =head2 Design considerations
716    
717     Since taking of share snapshot every day requires a lot of disk space and
718     network bandwidth, B<psinib> uses several techniques to keep disk usage and
719     network traffic at acceptable level:
720    
721     =over 3
722    
723     =item - usage of hard-links to provide same files in each snapshot (as opposed
724     to have multiple copies of same file)
725    
726     =item - usage of file size, atime and mtime to find changes of files without
727     transferring whole file over network (just share browsing is transfered
728     over network)
729    
730     =item - usage of C<.md5sum> files (compatible with command-line utility
731 dpavlin 1.6 C<md5sum>) to keep file between snapshots hard-linked
732 dpavlin 1.3
733     =back
734    
735     =head1 CONFIGURATION
736    
737     This section is not yet written.
738    
739 dpavlin 1.4 =head1 HACKS, TRICKS, BUGS and LIMITATIONS
740    
741     This chapter will have all content that doesn't fit anywhere else.
742    
743     =head2 Can snapshots be more frequent than daily?
744 dpavlin 1.3
745     There is not real reason why you can't take snapshot more often than
746 dpavlin 1.4 once a day. Actually, if you are using B<psinib> to backup Windows
747     workstations you already know that they tend to come-and-go during the day
748     (reboots probably ;-), so running B<psinib> several times a day increases
749     your chance of having up-to-date backup (B<psinib> will not make multiple
750     snapshots for same day, nor will it update snapshot for current day if
751     it already exists).
752 dpavlin 1.3
753 dpavlin 1.4 However, changing B<psinib> to produce snapshots which are, for example, hourly
754 dpavlin 1.3 is a simple change of C<$DIR_TIME_FMT> which is currently set to
755     C<'%Y%m%d'> (see I<strftime> documentation for explanation of that
756     format). If you change that to C<'%Y%m%d-%H> you can have hourly snapshots
757     (if your network is fast enough, that is...). Also, some of messages in
758     program will sound strange, but other than that it should work.
759     I<You have been warned>.
760 dpavlin 1.4
761     =head2 Do I really need to share every directory which I want to snapshot?
762    
763     Actually, no. Due to usage of C<Filesys::SmbClient> module, you can also
764     specify sub-directory inside your share that you want to backup. This feature
765     is most useful if you want to use administrative shares (but, have in mind
766     that you have to enter your Win administrator password in unencrypted file on
767     disk to do that) like this:
768    
769     smbmount //server/c$/WinNT/fonts /mnt -o username=administrator%win
770    
771     After that you will get directories with snapshots like:
772    
773     server/c_WinNT_fonts/yyyymmdd/....
774    
775 dpavlin 1.6 =head2 Won't I run out of disk space?
776    
777     Of course you will... Snapshots and logfiles will eventually fill-up your disk.
778     However, you can do two things to stop that:
779    
780     =head3 Clean snapshort older than x days
781    
782     You can add following command to your C<root> crontab:
783    
784     find /backup/isis_backup -type d -mindepth 3 -maxdepth 3 -mtime +11 -exec rm -Rf {} \;
785    
786     I assume that C</backup/isis_backup> is directory in which are your snapshots
787     and that you don't want to keep snapshots older than 11 days (that's
788     C<-mtime +11> part of command).
789    
790     =head3 Rotate your logs
791    
792     I will leave that to you. I relay on GNU/Debian's C<logrotate> to do it for me.
793 dpavlin 1.7
794     =head2 What are I<YYYYMMDD.partial> directories?
795    
796     If there isn't I<latest> symlink in snapshot directory, it's preatty safe to
797     assume that previous backup from that day failed. So, that directory will
798     be renamed to I<YYYYMMDD.partial> and snapshot will be performed again,
799     linking same files (other alternative would be to erase that dir and find
800     second-oldest directory, but this seemed like more correct approach).
801 dpavlin 1.3
802 dpavlin 1.15 =head2 I can't connect to any share
803    
804     Please verify that nmblookup (which is part of samba package) is in /bin or
805     /usr/bin. Also verify that nmblookup returns IP address for your server
806     using:
807    
808     $ nmblookup tvhouse
809     querying tvhouse on 192.168.34.255
810     192.168.34.30 tvhouse<00>
811    
812     If you don't get any output, your samba might not listen to correct interface
813     (see interfaces in smb.conf).
814    
815 dpavlin 1.20 =head2 Aren't backups boring?
816    
817     No! If you have subjects.txt in same directory as C<psinib.pl> you can get
818     various funny subjects in your mail. They change over time as long as you
819     ignore your backup.
820    
821 dpavlin 1.3 =head1 AUTHOR
822    
823     Dobrica Pavlinusic <dpavlin@rot13.org>
824    
825 dpavlin 1.17 L<http:E<sol>E<sol>www.rot13.orgE<sol>~dpavlinE<sol>>
826 dpavlin 1.3
827     =head1 LICENSE
828    
829     This product is licensed under GNU Public License (GPL) v2 or later.
830    
831     =cut

  ViewVC Help
Powered by ViewVC 1.1.26