/[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

Contents of /psinib.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.12 - (show annotations)
Sun Oct 12 16:13:38 2003 UTC (20 years, 6 months ago) by dpavlin
Branch: MAIN
Changes since 1.11: +22 -18 lines
File MIME type: text/plain
better logging, report correct number of dirs, don't create local file if
remote permission doesn't allow reading

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 # $ psinib.pl mountscript
16
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 use Fcntl qw(LOCK_EX LOCK_NB);
25 use Digest::MD5;
26 use File::Basename;
27
28 # configuration
29 my $LOG_TIME_FMT = '%Y-%m-%d %H:%M:%S'; # strftime format for logfile
30 my $DIR_TIME_FMT = '%Y%m%d'; # strftime format for backup dir
31
32 my $LOG = '/var/log/backup.log'; # add path here...
33 #$LOG = '/tmp/backup.log';
34
35 # store backups in which directory
36 #my $BACKUP_DEST = '/backup/isis_backup';
37 my $BACKUP_DEST = '/tmp/backup/';
38
39 # files to ignore in backup
40 my @ignore = ('.md5sum', '.backupignore', 'backupignore.txt');
41
42 # open log
43 open(L, ">> $LOG") || die "can't open log $LOG: $!";
44 select((select(L), $|=1)[0]); # flush output
45
46 # make a lock on logfile
47
48 my $c = 0;
49 {
50 flock L, LOCK_EX | LOCK_NB and last;
51 sleep 1;
52 redo if ++$c < 10;
53 # no response for 10 sec, bail out
54 xlog("ABORT","can't take lock on $LOG -- another $0 running?");
55 exit 1;
56 }
57
58 # taint path: nmblookup should be there!
59 $ENV{'PATH'} = "/usr/bin:/bin";
60
61 my $mounts = shift @ARGV ||
62 'mountscript';
63 # die "usage: $0 mountscript";
64
65
66 my @in_backup; # shares which are backeduped this run
67
68 my $p = new Net::Ping->new("tcp", 2);
69 # ping will try tcp connect to netbios-ssn (139)
70 $p->{port_num} = getservbyname("netbios-ssn", "tcp");
71
72 my $backup_ok = 0;
73
74 my $smb;
75 my %smb_atime;
76 my %smb_mtime;
77 my %file_md5;
78
79 open(M, $mounts) || die "can't open $mounts: $!";
80 while(<M>) {
81 chomp;
82 next if !/^\s*smbmount\s/;
83 my (undef,$share,undef,$opt) = split(/\s+/,$_,4);
84
85 my ($user,$passwd,$workgroup,$ip);
86
87 foreach (split(/,/,$opt)) {
88 my ($n,$v) = split(/=/,$_,2);
89 if ($n =~ m/username/i) {
90 if ($v =~ m#^(.+)/(.+)%(.+)$#) {
91 ($user,$passwd,$workgroup) = ($1,$2,$3);
92 } elsif ($v =~ m#^(.+)/(.+)$#) {
93 ($user,$workgroup) = ($1,$2);
94 } elsif ($v =~ m#^(.+)%(.+)$#) {
95 ($user,$passwd) = ($1,$2);
96 } else {
97 $user = $v;
98 }
99 } elsif ($n =~ m#workgroup#i) {
100 $workgroup = $v;
101 } elsif ($n =~ m#ip#i) {
102 $ip = $v;
103 }
104 }
105
106 push @in_backup,$share;
107
108
109 my ($host,$dir,$date_dir) = share2host_dir($share);
110 my $bl = "$BACKUP_DEST/$host/$dir/latest"; # latest backup
111 my $bc = "$BACKUP_DEST/$host/$dir/$date_dir"; # current one
112 my $real_bl;
113 if (-l $bl) {
114 $real_bl=readlink($bl) || die "can't read link $bl: $!";
115 $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
116 if (-l $bc && $real_bl eq $bc) {
117 print "$share allready backuped...\n";
118 $backup_ok++;
119 next;
120 }
121
122 }
123
124
125 print "working on $share\n";
126
127 # try to nmblookup IP
128 $ip = get_ip($share) if (! $ip);
129
130 if ($ip) {
131 xlog($share,"IP is $ip");
132 if ($p->ping($ip)) {
133 if (snap_share($share,$user,$passwd,$workgroup)) {
134 $backup_ok++;
135 }
136 }
137 }
138 }
139 close(M);
140
141 xlog("","$backup_ok backups completed of total ".($#in_backup+1)." this time (".int($backup_ok*100/($#in_backup+1))." %)");
142
143 1;
144
145 #-------------------------------------------------------------------------
146
147
148 # get IP number from share
149 sub get_ip {
150 my $share = shift;
151
152 my $host = $1 if ($share =~ m#//([^/]+)/#);
153
154 my $ip = `nmblookup $host`;
155 if ($ip =~ m/(\d+\.\d+\.\d+\.\d+)\s$host/i) {
156 return $1;
157 }
158 }
159
160
161 # write entry to screen and log
162 sub xlog {
163 my $share = shift;
164 my $t = strftime $LOG_TIME_FMT, localtime;
165 my $m = shift || '[no log entry]';
166 print STDERR $m,"\n";
167 print L "$t $share\t$m\n";
168 }
169
170 # dump warn and dies into log
171 BEGIN { $SIG{'__WARN__'} = sub { xlog('WARN',$_[0]) ; warn $_[0] } }
172 BEGIN { $SIG{'__DIE__'} = sub { xlog('DIE',$_[0]) ; die $_[0] } }
173
174
175 # split share name to host, dir and currnet date dir
176 sub share2host_dir {
177 my $share = shift;
178 my ($host,$dir);
179 if ($share =~ m#//([^/]+)/(.+)$#) {
180 ($host,$dir) = ($1,$2);
181 $dir =~ s/\W/_/g;
182 $dir =~ s/^_+//;
183 $dir =~ s/_+$//;
184 } else {
185 print "Can't parse share $share into host and directory!\n";
186 return;
187 }
188 return ($host,$dir,strftime $DIR_TIME_FMT, localtime);
189 }
190
191
192 # make a snapshot of a share
193 sub snap_share {
194
195 my $share = shift;
196
197 my %param = ( debug => 0 );
198
199 $param{username} = shift || warn "can't find username for share $share";
200 $param{password} = shift || warn "can't find passwod for share $share";
201 $param{workgroup} = shift || warn "can't find workgroup for share $share";
202
203 my ($host,$dir,$date_dir) = share2host_dir($share);
204
205 # latest backup directory
206 my $bl = "$BACKUP_DEST/$host/$dir/latest";
207 # current backup directory
208 my $bc = "$BACKUP_DEST/$host/$dir/$date_dir";
209
210 my $real_bl;
211 if (-l $bl) {
212 $real_bl=readlink($bl) || die "can't read link $bl: $!";
213 $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
214 } else {
215 print "no old backup, trying to find last backup, ";
216 if (opendir(BL_DIR, "$BACKUP_DEST/$host/$dir")) {
217 my @bl_dirs = sort grep { !/^\./ && -d "$BACKUP_DEST/$host/$dir/$_" } readdir(BL_DIR);
218 closedir(BL_DIR);
219 $real_bl=pop @bl_dirs;
220 print "using $real_bl as latest...\n";
221 $real_bl="$BACKUP_DEST/$host/$dir/$real_bl" if (substr($real_bl,0,1) ne "/");
222 if ($real_bl eq $bc) {
223 xlog($share,"latest from today (possible partial backup)");
224 rename $real_bl,$real_bl.".partial" || warn "can't reaname partial backup: $!";
225 $real_bl .= ".partial";
226 }
227 } else {
228 print "this is first run...\n";
229 }
230 }
231
232 if (-l $bc && $real_bl && $real_bl eq $bc) {
233 print "$share allready backuped...\n";
234 return 1;
235 }
236
237 die "You should really create BACKUP_DEST [$BACKUP_DEST] by hand! " if (!-e $BACKUP_DEST);
238
239 if (! -e "$BACKUP_DEST/$host") {
240 mkdir "$BACKUP_DEST/$host" || die "can't make dir for host $host, $BACKUP_DEST/$host: $!";
241 print "created host directory $BACKUP_DEST/$host...\n";
242 }
243
244 if (! -e "$BACKUP_DEST/$host/$dir") {
245 mkdir "$BACKUP_DEST/$host/$dir" || die "can't make dir for share $share, $BACKUP_DEST/$host/$dir $!";
246 print "created dir for share $share, $BACKUP_DEST/$host/$dir...\n";
247 }
248
249 mkdir $bc || die "can't make dir for current backup $bc: $!";
250
251 my @dirs = ( "/" );
252 my @smb_dirs = ( "/" );
253
254 my $transfer = 0; # bytes transfered over network
255
256 # this will store all available files and sizes
257 my @files;
258 my %file_size;
259 my %file_atime;
260 my %file_mtime;
261 #my %file_md5;
262
263 my @smb_files;
264 my %smb_size;
265 #my %smb_atime;
266 #my %smb_mtime;
267
268 sub norm_dir {
269 my $foo = shift;
270 my $prefix = shift;
271 $foo =~ s#//+#/#g;
272 $foo =~ s#/+$##g;
273 $foo =~ s#^/+##g;
274 return $prefix.$foo if ($prefix);
275 return $foo;
276 }
277
278 # read local filesystem
279 my $di = 0;
280 while ($di <= $#dirs && $real_bl) {
281 my $d=$dirs[$di++];
282 opendir(DIR,"$real_bl/$d") || warn "opendir($real_bl/$d): $!\n";
283
284 # read .backupignore if exists
285 if (-f "$real_bl/$d/.backupignore") {
286 open(I,"$real_bl/$d/.backupignore");
287 while(<I>) {
288 chomp;
289 push @ignore,norm_dir("$d/$_");
290 }
291 close(I);
292 #print STDERR "ignore: ",join("|",@ignore),"\n";
293 link "$real_bl/$d/.backupignore","$bc/$d/.backupignore" ||
294 warn "can't copy $real_bl/$d/.backupignore to current backup dir: $!\n";
295 }
296
297 # read .md5sum if exists
298 if (-f "$real_bl/$d/.md5sum") {
299 open(I,"$real_bl/$d/.md5sum");
300 while(<I>) {
301 chomp;
302 my ($md5,$f) = split(/\s+/,$_,2);
303 $file_md5{$f}=$md5;
304 }
305 close(I);
306 }
307
308 my @clutter = readdir(DIR);
309 foreach my $f (@clutter) {
310 next if ($f eq '.');
311 next if ($f eq '..');
312 my $pr = norm_dir("$d/$f"); # path relative
313 my $pf = norm_dir("$d/$f","$real_bl/"); # path full
314 if (grep(/^\Q$pr\E$/,@ignore) == 0) {
315 if (-f $pf) {
316 push @files,$pr;
317 $file_size{$pr}=(stat($pf))[7];
318 $file_atime{$pr}=(stat($pf))[8];
319 $file_mtime{$pr}=(stat($pf))[9];
320 } elsif (-d $pf) {
321 push @dirs,$pr;
322 } else {
323 print STDERR "not file or directory: $pf\n";
324 }
325 } else {
326 print STDERR "ignored: $pr\n";
327 }
328 }
329 }
330
331 # local dir always include /
332 xlog($share,($#files+1)." files and ".($#dirs)." dirs on local disk before backup");
333
334 # read smb filesystem
335
336 xlog($share,"smb to $share as $param{username}/$param{workgroup}");
337
338 # FIX: how to aviod creation of ~/.smb/smb.conf ?
339 $smb = new Filesys::SmbClient(%param) || die "SmbClient :$!\n";
340
341 $di = 0;
342 while ($di <= $#smb_dirs) {
343 my $d=$smb_dirs[$di];
344 my $pf = norm_dir($d,"smb:$share/"); # path full
345 my $D = $smb->opendir($pf);
346 if (! $D) {
347 xlog($share,"FATAL: $share [$pf]: $!");
348 # remove failing dir
349 delete $smb_dirs[$di];
350 return 0; # failed
351 }
352 $di++;
353
354 my @clutter = $smb->readdir_struct($D);
355 foreach my $item (@clutter) {
356 my $f = $item->[1];
357 next if ($f eq '.');
358 next if ($f eq '..');
359 my $pr = norm_dir("$d/$f"); # path relative
360 my $pf = norm_dir("$d/$f","smb:$share/"); # path full
361 if (grep(/^\Q$pr\E$/,@ignore) == 0) {
362 if ($item->[0] == main::SMBC_FILE) {
363 push @smb_files,$pr;
364 $smb_size{$pr}=($smb->stat($pf))[7];
365 $smb_atime{$pr}=($smb->stat($pf))[10];
366 $smb_mtime{$pr}=($smb->stat($pf))[11];
367 } elsif ($item->[0] == main::SMBC_DIR) {
368 push @smb_dirs,$pr;
369 } else {
370 print STDERR "not file or directory [",$item->[0],"]: $pf\n";
371 }
372 } else {
373 print STDERR "smb ignored: $pr\n";
374 }
375 }
376 }
377
378 xlog($share,($#smb_files+1)." files and ".($#smb_dirs)." dirs on remote share");
379
380 # sync dirs
381 my $lc = List::Compare->new(\@dirs, \@smb_dirs);
382
383 my @dirs2erase = $lc->get_Lonly;
384 my @dirs2create = $lc->get_Ronly;
385 xlog($share,($#dirs2erase+1)." dirs to erase and ".($#dirs2create+1)." dirs to create");
386
387 # create new dirs
388 foreach (sort @smb_dirs) {
389 mkdir "$bc/$_" || warn "mkdir $_: $!\n";
390 }
391
392 # sync files
393 $lc = List::Compare->new(\@files, \@smb_files);
394
395 my @files2erase = $lc->get_Lonly;
396 my @files2create = $lc->get_Ronly;
397 xlog($share,($#files2erase+1)." files to erase and ".($#files2create+1)." files to create");
398
399 sub smb_copy {
400 my $smb = shift;
401
402 my $from = shift;
403 my $to = shift;
404
405
406 my $l = 0;
407
408 foreach my $f (@_) {
409 #print "smb_copy $from/$f -> $to/$f\n";
410 my $md5 = Digest::MD5->new;
411
412 my $fd = $smb->open("$from/$f");
413 if (! $fd) {
414 xlog("WARNING","can't open smb file $from/$f: $!");
415 next;
416 }
417
418 if (! open(F,"> $to/$f")) {
419 xlog("WARNING","can't open new file $to/$f: $!");
420 next;
421 }
422
423 while (defined(my $b=$smb->read($fd,4096))) {
424 print F $b;
425 $l += length($b);
426 $md5->add($b);
427 }
428
429 $smb->close($fd);
430 close(F);
431
432 $file_md5{$f} = $md5->hexdigest;
433
434 # FIX: this fails with -T
435 my ($a,$m) = ($smb->stat("$from/$f"))[10,11];
436 utime $a, $m, "$to/$f" ||
437 warn "can't update utime on $to/$f: $!\n";
438
439 }
440 return $l;
441 }
442
443 # copy new files
444 foreach (@files2create) {
445 $transfer += smb_copy($smb,"smb:$share",$bc,$_);
446 }
447
448 my $size_sync = 0;
449 my $atime_sync = 0;
450 my $mtime_sync = 0;
451 my @sync_files;
452 my @ln_files;
453
454 foreach ($lc->get_intersection) {
455
456 my $f;
457
458 if ($file_size{$_} != $smb_size{$_}) {
459 $f=$_;
460 $size_sync++;
461 }
462 if ($file_atime{$_} != $smb_atime{$_}) {
463 $f=$_;
464 $atime_sync++;
465 }
466 if ($file_mtime{$_} != $smb_mtime{$_}) {
467 $f=$_;
468 $mtime_sync++;
469 }
470
471 if ($f) {
472 push @sync_files, $f;
473 } else {
474 push @ln_files, $_;
475 }
476 }
477
478 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.");
479
480 foreach (@sync_files) {
481 $transfer += smb_copy($smb,"smb:$share",$bc,$_);
482 }
483
484 xlog($share,"$transfer bytes transfered...");
485
486 foreach (@ln_files) {
487 link "$real_bl/$_","$bc/$_" || warn "link $real_bl/$_ -> $bc/$_: $!\n";
488 }
489
490 # remove files
491 foreach (sort @files2erase) {
492 unlink "$bc/$_" || warn "unlink $_: $!\n";
493 }
494
495 # remove not needed dirs (after files)
496 foreach (sort @dirs2erase) {
497 rmdir "$bc/$_" || warn "rmdir $_: $!\n";
498 }
499
500 # remove old .md5sum
501 foreach (sort @dirs) {
502 unlink "$bc/$_/.md5sum" if (-e "$bc/$_/.md5sum");
503 }
504
505 # create .md5sum
506 my $last_dir = '';
507 my $md5;
508 foreach my $f (sort { $file_md5{$a} cmp $file_md5{$b} } keys %file_md5) {
509 my $dir = dirname($f);
510 my $file = basename($f);
511 #print "$f -- $dir / $file<--\n";
512 if ($dir ne $last_dir) {
513 close($md5) if ($md5);
514 open($md5, ">> $bc/$dir/.md5sum") || warn "can't create $bc/$dir/.md5sum: $!";
515 $last_dir = $dir;
516 #print STDERR "writing $last_dir/.md5sum\n";
517 }
518 print $md5 $file_md5{$f}," $file\n";
519 }
520 close($md5) if ($md5);
521
522 # create leatest link
523 #print "ln -s $bc $real_bl\n";
524 if (-l $bl) {
525 unlink $bl || warn "can't remove old latest symlink $bl: $!\n";
526 }
527 symlink $bc,$bl || warn "can't create latest symlink $bl -> $bc: $!\n";
528
529 # FIX: sanity check -- remove for speedup
530 xlog($share,"failed to create latest symlink $bl -> $bc...") if (readlink($bl) ne $bc || ! -l $bl);
531
532 xlog($share,"backup completed...");
533
534 return 1;
535 }
536 __END__
537 #-------------------------------------------------------------------------
538
539
540 =head1 NAME
541
542 psinib - Perl Snapshot Is Not Incremental Backup
543
544 =head1 SYNOPSIS
545
546 ./psinib.pl
547
548 =head1 DESCRIPTION
549
550 This script in current version support just backup of Samba (or Micro$oft
551 Winblowz) shares to central disk space. Central disk space is organized in
552 multiple directories named after:
553
554 =over 4
555
556 =item *
557 server which is sharing files to be backed up
558
559 =item *
560 name of share on server
561
562 =item *
563 dated directory named like standard ISO date format (YYYYMMDD).
564
565 =back
566
567 In each dated directory you will find I<snapshot> of all files on
568 exported share on that particular date.
569
570 You can also use symlink I<latest> which will lead you to
571 last completed backup. After that you can use some other backup
572 software to transfer I<snapshot> to tape, CD-ROM or some other media.
573
574 =head2 Design considerations
575
576 Since taking of share snapshot every day requires a lot of disk space and
577 network bandwidth, B<psinib> uses several techniques to keep disk usage and
578 network traffic at acceptable level:
579
580 =over 3
581
582 =item - usage of hard-links to provide same files in each snapshot (as opposed
583 to have multiple copies of same file)
584
585 =item - usage of file size, atime and mtime to find changes of files without
586 transferring whole file over network (just share browsing is transfered
587 over network)
588
589 =item - usage of C<.md5sum> files (compatible with command-line utility
590 C<md5sum>) to keep file between snapshots hard-linked
591
592 =back
593
594 =head1 CONFIGURATION
595
596 This section is not yet written.
597
598 =head1 HACKS, TRICKS, BUGS and LIMITATIONS
599
600 This chapter will have all content that doesn't fit anywhere else.
601
602 =head2 Can snapshots be more frequent than daily?
603
604 There is not real reason why you can't take snapshot more often than
605 once a day. Actually, if you are using B<psinib> to backup Windows
606 workstations you already know that they tend to come-and-go during the day
607 (reboots probably ;-), so running B<psinib> several times a day increases
608 your chance of having up-to-date backup (B<psinib> will not make multiple
609 snapshots for same day, nor will it update snapshot for current day if
610 it already exists).
611
612 However, changing B<psinib> to produce snapshots which are, for example, hourly
613 is a simple change of C<$DIR_TIME_FMT> which is currently set to
614 C<'%Y%m%d'> (see I<strftime> documentation for explanation of that
615 format). If you change that to C<'%Y%m%d-%H> you can have hourly snapshots
616 (if your network is fast enough, that is...). Also, some of messages in
617 program will sound strange, but other than that it should work.
618 I<You have been warned>.
619
620 =head2 Do I really need to share every directory which I want to snapshot?
621
622 Actually, no. Due to usage of C<Filesys::SmbClient> module, you can also
623 specify sub-directory inside your share that you want to backup. This feature
624 is most useful if you want to use administrative shares (but, have in mind
625 that you have to enter your Win administrator password in unencrypted file on
626 disk to do that) like this:
627
628 smbmount //server/c$/WinNT/fonts /mnt -o username=administrator%win
629
630 After that you will get directories with snapshots like:
631
632 server/c_WinNT_fonts/yyyymmdd/....
633
634 =head2 Won't I run out of disk space?
635
636 Of course you will... Snapshots and logfiles will eventually fill-up your disk.
637 However, you can do two things to stop that:
638
639 =head3 Clean snapshort older than x days
640
641 You can add following command to your C<root> crontab:
642
643 find /backup/isis_backup -type d -mindepth 3 -maxdepth 3 -mtime +11 -exec rm -Rf {} \;
644
645 I assume that C</backup/isis_backup> is directory in which are your snapshots
646 and that you don't want to keep snapshots older than 11 days (that's
647 C<-mtime +11> part of command).
648
649 =head3 Rotate your logs
650
651 I will leave that to you. I relay on GNU/Debian's C<logrotate> to do it for me.
652
653 =head2 What are I<YYYYMMDD.partial> directories?
654
655 If there isn't I<latest> symlink in snapshot directory, it's preatty safe to
656 assume that previous backup from that day failed. So, that directory will
657 be renamed to I<YYYYMMDD.partial> and snapshot will be performed again,
658 linking same files (other alternative would be to erase that dir and find
659 second-oldest directory, but this seemed like more correct approach).
660
661 =head1 AUTHOR
662
663 Dobrica Pavlinusic <dpavlin@rot13.org>
664
665 L<http://www.rot13.org/~dpavlin/>
666
667 =head1 LICENSE
668
669 This product is licensed under GNU Public License (GPL) v2 or later.
670
671 =cut

  ViewVC Help
Powered by ViewVC 1.1.26