ba45e274e190abd639a1039c194d52c9711cc50e
[debian/amanda] / application-src / amsamba.pl
1 #!@PERL@ 
2 # Copyright (c) 2005-2008 Zmanda Inc.  All Rights Reserved.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22 use Getopt::Long;
23
24 package Amanda::Application::Amsamba;
25 use base qw(Amanda::Application);
26 use File::Copy;
27 use File::Path;
28 use IPC::Open2;
29 use IPC::Open3;
30 use Sys::Hostname;
31 use Symbol;
32 use IO::Handle;
33 use Amanda::Constants;
34 use Amanda::Config qw( :init :getconf  config_dir_relative );
35 use Amanda::Debug qw( :logging );
36 use Amanda::Paths;
37 use Amanda::Util qw( :constants :quoting);
38
39 sub new {
40     my $class = shift;
41     my ($config, $host, $disk, $device, $level, $index, $message, $collection, $record, $calcsize, $gnutar_path, $smbclient_path, $amandapass, $exclude_file, $exclude_list, $exclude_optional, $include_file, $include_list, $include_optional, $recover_mode) = @_;
42     my $self = $class->SUPER::new();
43
44     if (defined $gnutar_path) {
45         $self->{gnutar}     = $gnutar_path;
46     } else {
47         $self->{gnutar}     = $Amanda::Constants::GNUTAR;
48     }
49     if (defined $smbclient_path) {
50         $self->{smbclient}  = $smbclient_path;
51     } else {
52         $self->{smbclient}  = $Amanda::Constants::SAMBA_CLIENT;
53     }
54     if (defined $amandapass) {
55         $self->{amandapass}  = $amandapass;
56     } else {
57         $self->{amandapass}  = "$Amanda::Paths::sysconfdir/amandapass";
58     }
59
60     $self->{config}           = $config;
61     $self->{host}             = $host;
62     $self->{disk}             = $disk;
63     $self->{device}           = $device;
64     $self->{level}            = [ @{$level} ];
65     $self->{index}            = $index;
66     $self->{message}          = $message;
67     $self->{collection}       = $collection;
68     $self->{record}           = $record;
69     $self->{calcsize}         = $calcsize;
70     $self->{exclude_file}     = [ @{$exclude_file} ];
71     $self->{exclude_list}     = [ @{$exclude_list} ];
72     $self->{exclude_optional} = $exclude_optional;
73     $self->{include_file}     = [ @{$include_file} ];
74     $self->{include_list}     = [ @{$include_list} ];
75     $self->{include_optional} = $include_optional;
76     $self->{recover_mode}     = $recover_mode;
77
78     return $self;
79 }
80
81 # on entry:
82 #   $self->{exclude_file}
83 #   $self->{exclude_list}
84 #   $self->{include_file}
85 #   $self->{include_list}
86 #on exit:
87 #  $self->{exclude}
88 #  $self->{include}
89 sub validate_inexclude {
90     my $self = shift;
91
92     if ($#{$self->{exclude_file}} + $#{$self->{exclude_list}} >= -1 &&
93         $#{$self->{include_file}} + $#{$self->{include_list}} >= -1) {
94         $self->print_to_server_and_die($self->{action},
95                                        "Can't have both include and exclude",
96                                        $Amanda::Script_App::ERROR);
97     }
98
99     if ($#{$self->{exclude_file}} >= 0) {
100         $self->{exclude} = [ @{$self->{exclude_file}} ];
101     }
102     foreach my $file (@{$self->{exclude_list}}) {
103         if (!open(FF, $file)) {
104             if ($self->{action} eq 'check' && !$self->{exclude_optional}) {
105                 $self->print_to_server($self->{action},
106                                        "Open of '$file' failed: $!",
107                                        $Amanda::Script_App::ERROR);
108             }
109             next;
110         }
111         while (<FF>) {
112             chomp;
113             push @{$self->{exclude}}, $_;
114         }
115         close(FF);
116     }
117     if ($#{$self->{include_file}} >= 0) {
118         $self->{include} = [ @{$self->{include_file}} ];
119     }
120     foreach my $file (@{$self->{include_list}}) {
121         if (open(FF, $file)) {
122             if ($self->{action} eq 'check') {
123                 $self->print_to_server($self->{action},
124                                        "Open of '$file' failed: $!",
125                                        $Amanda::Script_App::ERROR);
126             }
127             next;
128         }
129         while (<FF>) {
130             chomp;
131             push @{$self->{include}}, $_;
132         }
133         close(FF);
134     }
135 }
136
137 # on entry:
138 #   $self->disk == //host/share/subdir
139 # on exit:
140 #   self->{cifshost} = //host
141 #   $self->{share} = //host/share
142 #   $self->{sambashare} = \\host\share
143 #   $self->{subdir} = subdir
144 sub parsesharename {
145     my $self = shift;
146
147     return if !defined $self->{disk};
148
149     if ($self->{disk} =~ m,^(//[^/]+/[^/]+)/(.*)$,) {
150         $self->{share} = $1;
151         $self->{subdir} = $2
152     } else {
153         $self->{share} = $self->{disk};
154     }
155     $self->{sambashare} = $self->{share};
156     $self->{sambashare} =~ s,/,\\,g;
157     $self->{disk} =~ m,^(//[^/]+)/[^/]+,;
158     $self->{cifshost} = $1;
159 }
160
161
162 # Read $self->{amandapass} file.
163 # on entry:
164 #   $self->{share} == //host/share
165 # on exit:
166 #   $self->{domain}   = domain to connect to.
167 #   $self->{username} = username (-U)
168 #   $self->{password} = password
169 sub findpass {
170     my $self = shift;
171
172     my $amandapass;
173     my $line;
174
175     open($amandapass, $self->{amandapass});
176     while ($line = <$amandapass>) {
177         chomp $line;
178         next if $line =~ /^#/;
179         my ($diskname, $userdomain) = Amanda::Util::skip_quoted_string($line);
180         if (defined $diskname &&
181             ($diskname eq '*' ||
182              ($diskname =~ m,^(//[^/]+)/\*$, && $1 eq $self->{cifshost}) ||
183              $diskname eq $self->{share} ||
184              $diskname eq $self->{sambashare})) {
185             my ($userpasswd, $domain) = split ' ', $userdomain;
186             $self->{domain} = $domain;
187             my ($username, $password) = split('%', $userpasswd);
188             $self->{username} = $username;
189             $self->{password} = $password;
190             close($amandapass);
191             return;
192         }
193     }
194     close($amandapass);
195     $self->print_to_server_and_die($self->{action},"Cannot find password for share $self->{share} in $self->{amandapass}", $Amanda::Script_App::ERROR);
196 }
197
198 sub command_support {
199     my $self = shift;
200
201     print "CONFIG YES\n";
202     print "HOST YES\n";
203     print "DISK YES\n";
204     print "MAX-LEVEL 1\n";
205     print "INDEX-LINE YES\n";
206     print "INDEX-XML NO\n";
207     print "MESSAGE-LINE YES\n";
208     print "MESSAGE-XML NO\n";
209     print "RECORD YES\n";
210     print "COLLECTION NO\n";
211     print "MULTI-ESTIMATE NO\n";
212     print "CALCSIZE NO\n";
213     print "EXCLUDE-FILE YES\n";
214     print "EXCLUDE-LIST YES\n";
215     print "EXCLUDE-OPTIONAL YES\n";
216     print "INCLUDE-FILE YES\n";
217     print "INCLUDE-LIST YES\n";
218     print "INCLUDE-OPTIONAL YES\n";
219     print "RECOVER-MODE SMB\n";
220 }
221
222 sub command_selfcheck {
223     my $self = shift;
224
225     $self->{action} = 'check';
226     $self->parsesharename();
227     $self->findpass();
228     $self->validate_inexclude();
229
230     print "OK " . $self->{share} . "\n";
231     print "OK " . $self->{disk} . "\n";
232     print "OK " . $self->{device} . "\n";
233
234     my ($child_rdr, $parent_wtr);
235     $^F=10;
236     pipe($child_rdr,  $parent_wtr);
237     $parent_wtr->autoflush(1);
238     my($wtr, $rdr, $err);
239     $err = Symbol::gensym;
240     my $pid = open3($wtr, $rdr, $err, "-");
241     if ($pid == 0) {
242         #child
243         my $ff = $child_rdr->fileno;
244         debug("child_rdr $ff");
245         $parent_wtr->close();
246         $ENV{PASSWD_FD} = $child_rdr->fileno;
247         close(1);
248         close(2);
249         my @ARGV = ();
250         push @ARGV, $self->{smbclient}, $self->{share},
251                     "-U", $self->{username},
252                     "-E";
253         if (defined $self->{domain}) {
254             push @ARGV, "-W", $self->{domain},
255         }
256         if (defined $self->{subdir}) {
257             push @ARGV, "-D", $self->{subdir},
258         }
259         push @ARGV, "-c", "quit";
260         debug("execute: " . $self->{smbclient} . " " .
261               join(" ", @ARGV));
262         exec {$self->{smbclient}} @ARGV;
263     }
264     #parent
265     my $ff = $parent_wtr->fileno;
266     debug("parent_wtr $ff");
267     debug("password $self->{password}");
268     $parent_wtr->print($self->{password});
269     $parent_wtr->close();
270     $child_rdr->close();
271     close($wtr);
272     close($rdr);
273     while (<$err>) {
274         chomp;
275         debug("stderr: " . $_);
276         next if /^Domain=/;
277         $self->print_to_server($self->{action}, "smbclient: $_",
278                                $Amanda::Script_App::ERROR);
279     }
280     close($err);
281     waitpid($pid, 0);
282     #check binary
283     #check statefile
284     #check amdevice
285 }
286
287 sub command_estimate {
288     my $self = shift;
289
290     $self->{action} = 'estimate';
291     $self->parsesharename();
292     $self->findpass();
293     $self->validate_inexclude();
294
295     my $level = $self->{level}[0];
296     my ($child_rdr, $parent_wtr);
297     $^F=10;
298     pipe($child_rdr,  $parent_wtr);
299     $parent_wtr->autoflush(1);
300     my($wtr, $rdr, $err);
301     $err = Symbol::gensym;
302     my $pid = open3($wtr, $rdr, $err, "-");
303     if ($pid == 0) {
304         #child
305         my $ff = $child_rdr->fileno;
306         debug("child_rdr $ff");
307         $parent_wtr->close();
308         $ENV{PASSWD_FD} = $child_rdr->fileno;
309         close(0);
310         close(1);
311         my @ARGV = ();
312         push @ARGV, $self->{smbclient}, $self->{share},
313                     "-d", "0",
314                     "-U", $self->{username},
315                     "-E";
316         if (defined $self->{domain}) {
317             push @ARGV, "-W", $self->{domain},
318         }
319         if (defined $self->{subdir}) {
320             push @ARGV, "-D", $self->{subdir},
321         }
322         if ($level == 0) {
323             push @ARGV, "-c", "archive 0;recurse;du";
324         } else {
325             push @ARGV, "-c", "archive 1;recurse;du";
326         }
327         debug("execute: " . $self->{smbclient} . " " .
328               join(" ", @ARGV));
329         exec {$self->{smbclient}} @ARGV;
330     }
331     #parent
332     my $ff = $parent_wtr->fileno;
333     debug("parent_wtr $ff");
334     debug("password $self->{password}");
335     $parent_wtr->print($self->{password});
336     $parent_wtr->close();
337     $child_rdr->close();
338     close($wtr);
339     close($rdr);
340     my $size = $self->parse_estimate($err);
341     close($err);
342     output_size($level, $size);
343     waitpid($pid, 0);
344 }
345
346 sub parse_estimate {
347     my $self = shift;
348     my($fh)  = shift;
349     my($size) = -1;
350     while(<$fh>) {
351         chomp;
352         next if /^\s*$/;
353         next if /blocks of size/;
354         next if /blocks available/;
355         next if /^\s*$/;
356         next if /^Domain=/;
357         next if /dumped \d+ files and directories/;
358         debug("stderr: $_");
359         if ($_ =~ /^Total number of bytes: (\d*)/) {
360             $size = $1;
361             last;
362         } else {
363             $self->print_to_server($self->{action}, "smbclient: $_",
364                                    $Amanda::Script_App::ERROR);
365         }
366     }
367     return $size;
368 }
369
370 sub output_size {
371    my($level) = shift;
372    my($size) = shift;
373    if($size == -1) {
374       print "$level -1 -1\n";
375       #exit 2;
376    }
377    else {
378       my($ksize) = int $size / (1024);
379       $ksize=32 if ($ksize<32);
380       print "$level $ksize 1\n";
381    }
382 }
383
384 sub command_backup {
385     my $self = shift;
386
387     $self->{action} = 'backup';
388     $self->parsesharename();
389     $self->findpass();
390     $self->validate_inexclude();
391
392     my $level = $self->{level}[0];
393     my $mesgout_fd;
394     open($mesgout_fd, '>&=3') || die();
395     $self->{mesgout} = $mesgout_fd;
396
397     my $pid_tee = open3(\*INDEX_IN, '>&STDOUT', \*INDEX_TEE, "-");
398     if ($pid_tee == 0) {
399         close(INDEX_IN);
400         close(INDEX_TEE);
401         my $buf;
402         my $size = -1;
403         while (($size = POSIX::read(0, $buf, 32768)) > 0) {
404             POSIX::write(1, $buf, $size);
405             POSIX::write(2, $buf, $size);
406         }
407         exit 0;
408     }
409     my ($child_rdr, $parent_wtr);
410     $^F=10;
411     pipe($child_rdr,  $parent_wtr);
412     $^F=2;
413     $parent_wtr->autoflush(1);
414     my($wtr, $err);
415     $err = Symbol::gensym;
416     my $pid = open3($wtr, ">&INDEX_IN", $err, "-");
417     if ($pid == 0) {
418         #child
419         my $ff = $child_rdr->fileno;
420         debug("child_rdr $ff");
421         $parent_wtr->close();
422         $ENV{PASSWD_FD} = $child_rdr->fileno;
423         close(0);
424         my @ARGV = ();
425         push @ARGV, $self->{smbclient}, $self->{share},
426                     "-d", "0",
427                     "-U", $self->{username},
428                     "-E";
429         if (defined $self->{domain}) {
430             push @ARGV, "-W", $self->{domain},
431         }
432         if (defined $self->{subdir}) {
433             push @ARGV, "-D", $self->{subdir},
434         }
435         my $comm ;
436         if ($level == 0) {
437             $comm = "-Tqca";
438         } else {
439             $comm = "-Tqcg";
440         }
441         if ($#{$self->{exclude}} >= 0) {
442             $comm .= "X";
443         }
444         if ($#{$self->{include}} >= 0) {
445             $comm .= "I";
446         }
447         push @ARGV, $comm, "-";
448         if ($#{$self->{exclude}} >= 0) {
449             push @ARGV, @{$self->{exclude}};
450         }
451         if ($#{$self->{include}} >= 0) {
452             push @ARGV, @{$self->{include}};
453         }
454         debug("execute: " . $self->{smbclient} . " " .
455               join(" ", @ARGV));
456         exec {$self->{smbclient}} @ARGV;
457     }
458
459     my $ff = $parent_wtr->fileno;
460     debug("parent_wtr $ff");
461     debug("password $self->{password}");
462     $parent_wtr->print($self->{password});
463     $parent_wtr->close();
464     $child_rdr->close();
465     close($wtr);
466
467     #index process 
468     my $index;
469     debug("$self->{gnutar} -tf -");
470     my $pid_index1 = open2($index, '<&INDEX_TEE', $self->{gnutar}, "-tf", "-");
471     close(INDEX_IN);
472     my $size = -1;
473     my $index_fd = $index->fileno;
474     debug("index $index_fd");
475     if (defined($self->{index})) {
476         my $indexout_fd;
477         open($indexout_fd, '>&=4') || die();
478         $self->parse_backup($index, $mesgout_fd, $indexout_fd);
479         close($indexout_fd);
480     }
481     else {
482         $self->parse_backup($index_fd, $mesgout_fd, undef);
483     }
484     close($index);
485
486     while (<$err>) {
487         chomp;
488         debug("stderr: " . $_);
489         next if /^Domain=/;
490         next if /dumped (\d+) files and directories/;
491         if (/^Total bytes written: (\d*)/) {
492             $size = $1;
493         } else {
494             $self->print_to_server($self->{action}, "smbclient: $_",
495                                    $Amanda::Script_App::ERROR);
496         }
497     }
498     if ($size >= 0) {
499         my $ksize = $size / 1024;
500         if ($ksize < 32) {
501             $ksize = 32;
502         }
503         print $mesgout_fd "sendbackup: size $ksize\n";
504         print $mesgout_fd "sendbackup: end\n";
505     }
506
507     waitpid $pid, 0;
508     if ($? != 0) {
509         $self->print_to_server_and_die($self->{action},
510                                        "smbclient returned error",
511                                        $Amanda::Script_App::ERROR);
512     }
513     exit 0;
514 }
515
516 sub parse_backup {
517     my $self = shift;
518     my($fhin, $fhout, $indexout) = @_;
519     my $size  = -1;
520     while(<$fhin>) {
521         if ( /^\.\//) {
522             if(defined($indexout)) {
523                 if(defined($self->{index})) {
524                     s/^\.//;
525                     print $indexout $_;
526                 }
527             }
528         }
529         else {
530             print $fhout "? $_";
531         }
532     }
533 }
534
535 sub command_index_from_output {
536    index_from_output(0, 1);
537    exit 0;
538 }
539
540 sub index_from_output {
541    my($fhin, $fhout) = @_;
542    my($size) = -1;
543    while(<$fhin>) {
544       next if /^Total bytes written:/;
545       next if !/^\.\//;
546       s/^\.//;
547       print $fhout $_;
548    }
549 }
550
551 sub command_index_from_image {
552    my $self = shift;
553    my $index_fd;
554    open($index_fd, "$self->{gnutar} --list --file - |") || die();
555    index_from_output($index_fd, 1);
556 }
557
558 sub command_restore {
559     my $self = shift;
560     my @cmd = ();
561
562     $self->{restore} = 'backup';
563     $self->parsesharename();
564     chdir(Amanda::Util::get_original_cwd());
565
566     if ($self->{recover_mode} eq "smb") {
567         $self->findpass();
568         push @cmd, $self->{smbclient}, $self->{share},
569                    "-d", "0",
570                    "-U", $self->{username};
571         
572         if (defined $self->{domain}) {
573             push @cmd, "-W", $self->{domain};
574         }
575         push @cmd, "-Tx", "-";
576         for(my $i=1;defined $ARGV[$i]; $i++) {
577             my $param = $ARGV[$i];
578             $param =~ /^(.*)$/;
579             push @cmd, $1;
580         }
581         my ($parent_rdr, $child_wtr);
582         $^F=10;
583         pipe($parent_rdr,  $child_wtr);
584         $child_wtr->autoflush(1);
585         my($wtr, $rdr, $err);
586         $err = Symbol::gensym;
587         my $pid = open3($wtr, $rdr, $err, "-");
588         if ($pid == 0) {
589             $child_wtr->print($self->{password});
590             $child_wtr->close();
591             exit 0;
592         }
593         $child_wtr->close();
594         $ENV{PASSWD_FD} = $parent_rdr->fileno;
595         debug("cmd:" . join(" ", @cmd));
596         exec { $cmd[0] } @cmd;
597         die("Can't exec '", $cmd[0], "'");
598     } else {
599         push @cmd, $self->{gnutar}, "-xpvf", "-";
600         for(my $i=1;defined $ARGV[$i]; $i++) {
601             my $param = $ARGV[$i];
602             $param =~ /^(.*)$/;
603             push @cmd, $1;
604         }
605         debug("cmd:" . join(" ", @cmd));
606         exec { $cmd[0] } @cmd;
607         die("Can't exec '", $cmd[0], "'");
608     }
609 }
610
611 sub command_validate {
612    my $self = shift;
613
614    $self->{validate} = 'backup';
615    my(@cmd) = ($self->{gnutar}, "-tf", "-");
616    debug("cmd:" . join(" ", @cmd));
617    my $pid = open3('>&STDIN', '>&STDOUT', '>&STDERR', @cmd) || die("validate", "Unable to run @cmd");
618    waitpid $pid, 0;
619    if( $? != 0 ){
620        die("validate", "$self->{gnutar} returned error");
621    }
622    exit(0);
623 }
624
625 sub command_print_command {
626 }
627
628 package main;
629
630 sub usage {
631     print <<EOF;
632 Usage: amsamba <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --calcsize.
633 EOF
634     exit(1);
635 }
636
637 my $opt_version;
638 my $opt_config;
639 my $opt_host;
640 my $opt_disk;
641 my $opt_device;
642 my @opt_level;
643 my $opt_index;
644 my $opt_message;
645 my $opt_collection;
646 my $opt_record;
647 my $opt_calcsize;
648 my $opt_gnutar_path;
649 my $opt_smbclient_path;
650 my $opt_amandapass;
651 my @opt_exclude_file;
652 my @opt_exclude_list;
653 my $opt_exclude_optional;
654 my @opt_include_file;
655 my @opt_include_list;
656 my $opt_include_optional;
657 my $opt_recover_mode;
658
659 Getopt::Long::Configure(qw{bundling});
660 GetOptions(
661     'version'            => \$opt_version,
662     'config=s'           => \$opt_config,
663     'host=s'             => \$opt_host,
664     'disk=s'             => \$opt_disk,
665     'device=s'           => \$opt_device,
666     'level=s'            => \@opt_level,
667     'index=s'            => \$opt_index,
668     'message=s'          => \$opt_message,
669     'collection=s'       => \$opt_collection,
670     'record'             => \$opt_record,
671     'calcsize'           => \$opt_calcsize,
672     'gnutar_path'        => \$opt_gnutar_path,
673     'smbclient_path'     => \$opt_smbclient_path,
674     'amandapass'         => \$opt_amandapass,
675     'exclude-file=s'     => \@opt_exclude_file,
676     'exclude-list=s'     => \@opt_exclude_list,
677     'exclude-optional=s' => \$opt_exclude_optional,
678     'include-file=s'     => \@opt_include_file,
679     'include-list=s'     => \@opt_include_list,
680     'include-optional=s' => \$opt_include_optional,
681     'recover-mode=s'     => \$opt_recover_mode,
682 ) or usage();
683
684 if (defined $opt_version) {
685     print "amsamba-" . $Amanda::Constants::VERSION , "\n";
686     exit(0);
687 }
688
689 my $application = Amanda::Application::Amsamba->new($opt_config, $opt_host, $opt_disk, $opt_device, \@opt_level, $opt_index, $opt_message, $opt_collection, $opt_record, $opt_calcsize, $opt_gnutar_path, $opt_smbclient_path, $opt_amandapass, \@opt_exclude_file, \@opt_exclude_list, $opt_exclude_optional, \@opt_include_file, \@opt_include_list, $opt_include_optional, $opt_recover_mode);
690
691 $application->do($ARGV[0]);