2 # Copyright (c) 2008, 2009, 2010 Zmanda, Inc. All Rights Reserved.
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.
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
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
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
25 package Amanda::Application::Amsamba;
26 use base qw(Amanda::Application);
34 use Amanda::Constants;
35 use Amanda::Config qw( :init :getconf config_dir_relative );
36 use Amanda::Debug qw( :logging );
38 use Amanda::Util qw( :constants :quoting);
42 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, $allow_anonymous, $directory) = @_;
43 my $self = $class->SUPER::new($config);
45 if (defined $gnutar_path) {
46 $self->{gnutar} = $gnutar_path;
48 $self->{gnutar} = $Amanda::Constants::GNUTAR;
50 if (defined $smbclient_path) {
51 $self->{smbclient} = $smbclient_path;
53 $self->{smbclient} = $Amanda::Constants::SAMBA_CLIENT;
55 if (defined $amandapass) {
56 $self->{amandapass} = config_dir_relative($amandapass);
58 $self->{amandapass} = "$Amanda::Paths::CONFIG_DIR/amandapass";
61 $self->{config} = $config;
62 $self->{host} = $host;
64 $self->{disk} = $disk;
66 $self->{disk} = $device;
68 if (defined $device) {
69 $self->{device} = $device;
71 $self->{device} = $disk;
73 if ($self->{disk} =~ /^\\\\/) {
78 $self->{level} = [ @{$level} ];
79 $self->{index} = $index;
80 $self->{message} = $message;
81 $self->{collection} = $collection;
82 $self->{record} = $record;
83 $self->{calcsize} = $calcsize;
84 $self->{exclude_file} = [ @{$exclude_file} ];
85 $self->{exclude_list} = [ @{$exclude_list} ];
86 $self->{exclude_optional} = $exclude_optional;
87 $self->{include_file} = [ @{$include_file} ];
88 $self->{include_list} = [ @{$include_list} ];
89 $self->{include_optional} = $include_optional;
90 $self->{recover_mode} = $recover_mode;
91 $self->{allow_anonymous} = $allow_anonymous;
92 $self->{directory} = $directory;
98 # $self->{exclude_file}
99 # $self->{exclude_list}
100 # $self->{include_file}
101 # $self->{include_list}
105 # $self->{include_filename}
106 sub validate_inexclude {
109 if ($#{$self->{exclude_file}} + $#{$self->{exclude_list}} >= -1 &&
110 $#{$self->{include_file}} + $#{$self->{include_list}} >= -1) {
111 $self->print_to_server_and_die("Can't have both include and exclude",
112 $Amanda::Script_App::ERROR);
115 if ($#{$self->{exclude_file}} >= 0) {
116 $self->{exclude} = [ @{$self->{exclude_file}} ];
118 foreach my $file (@{$self->{exclude_list}}) {
119 if (!open(FF, $file)) {
120 if ($self->{action} eq 'check' && !$self->{exclude_optional}) {
121 $self->print_to_server("Open of '$file' failed: $!",
122 $Amanda::Script_App::ERROR);
128 push @{$self->{exclude}}, $_;
133 if ($self->{action} eq "restore" and defined $self->{'include_list'}) {
134 # put all include in a single file $self->{'include_filename'}
135 $self->{'include_filename'} = "$AMANDA_TMPDIR/amsamba.$$.include";
136 open INC_FILE, ">$self->{'include_filename'}";
137 if ($#{$self->{include_file}} >= 0) {
138 print INC_FILE "$self->{include_file}\n";
140 foreach my $file (@{$self->{include_list}}) {
141 if (!open(FF, $file)) {
142 if ($self->{action} eq 'check' && !$self->{include_optional}) {
143 $self->print_to_server("Open of '$file' failed: $!",
144 $Amanda::Script_App::ERROR);
154 # add command line include for amrestore
155 for(my $i=1;defined $ARGV[$i]; $i++) {
156 my $param = $ARGV[$i];
158 print INC_FILE "$1\n";
163 # put all include in $self->{'include'} they will be added on
165 if ($#{$self->{include_file}} >= 0) {
166 $self->{include} = [ @{$self->{include_file}} ];
169 foreach my $file (@{$self->{include_list}}) {
170 if (!open(FF, $file)) {
171 if ($self->{action} eq 'check' && !$self->{include_optional}) {
172 $self->print_to_server("Open of '$file' failed: $!",
173 $Amanda::Script_App::ERROR);
179 push @{$self->{include}}, $_;
184 # add command line include for amrestore
185 if ($self->{action} eq "restore") {
186 for(my $i=1;defined $ARGV[$i]; $i++) {
187 my $param = $ARGV[$i];
189 push @{$self->{include}}, $1;
196 # $self->{directory} == //host/share/subdir \\host\share\subdir
198 # $self->{device} == //host/share/subdir \\host\share\subdir
200 # $self->{cifshost} = //host \\host
201 # $self->{share} = //host/share \\host\share
202 # $self->{sambashare} = \\host\share \\host\share
203 # $self->{subdir} = subdir subdir
206 my $to_parse = $self->{directory};
207 $to_parse = $self->{device} if !defined $to_parse;;
209 return if !defined $to_parse;
212 if ($to_parse =~ m,^(\\\\[^\\]+\\[^\\]+)\\(.*)$,) {
216 $self->{share} = $to_parse
218 $self->{sambashare} = $self->{share};
219 $to_parse =~ m,^(\\\\[^\\]+)\\[^\\]+,;
220 $self->{cifshost} = $1;
222 if ($to_parse =~ m,^(//[^/]+/[^/]+)/(.*)$,) {
226 $self->{share} = $to_parse
228 $self->{sambashare} = $self->{share};
229 $self->{sambashare} =~ s,/,\\,g;
230 $to_parse =~ m,^(//[^/]+)/[^/]+,;
231 $self->{cifshost} = $1;
236 # Read $self->{amandapass} file.
238 # $self->{cifshost} == //host/share
239 # $self->{share} == //host/share
241 # $self->{domain} = domain to connect to.
242 # $self->{username} = username (-U)
243 # $self->{password} = password
250 $self->{domain} = undef;
251 $self->{username} = undef;
252 $self->{password} = undef;
254 debug("amandapass: $self->{amandapass}");
255 if (!open($amandapass, $self->{amandapass})) {
256 if ($self->{allow_anonymous}) {
257 $self->{username} = $self->{allow_anonymous};
258 debug("cannot open password file '$self->{amandapass}': $!\n");
259 debug("Using anonymous user: $self->{username}");
262 $self->print_to_server_and_die(
263 "cannot open password file '$self->{amandapass}': $!",
264 $Amanda::Script_App::ERROR);
268 while ($line = <$amandapass>) {
270 next if $line =~ /^#/;
271 my ($diskname, $userpasswd, $domain, $extra) = Amanda::Util::split_quoted_string_friendly($line);
273 debug("Trailling characters ignored in amandapass line");
275 if (defined $diskname &&
277 ($self->{unc}==0 && $diskname =~ m,^(//[^/]+)/\*$, && $1 eq $self->{cifshost}) ||
278 ($self->{unc}==1 && $diskname =~ m,^(\\\\[^\\]+)\\\*$, && $1 eq $self->{cifshost}) ||
279 $diskname eq $self->{share} ||
280 $diskname eq $self->{sambashare})) {
281 if (defined $userpasswd && $userpasswd ne "") {
282 $self->{domain} = $domain if defined $domain && $domain ne "";
283 my ($username, $password) = split('%', $userpasswd, 2);
284 $self->{username} = $username;
285 $self->{password} = $password;
286 $self->{password} = undef if (defined $password && $password eq "");
288 $self->{username} = "guest";
295 if ($self->{allow_anonymous}) {
296 $self->{username} = $self->{allow_anonymous};
297 debug("Cannot find password for share $self->{share} in $self->{amandapass}");
298 debug("Using anonymous user: $self->{username}");
301 $self->print_to_server_and_die(
302 "Cannot find password for share $self->{share} in $self->{amandapass}",
303 $Amanda::Script_App::ERROR);
306 sub command_support {
309 print "CONFIG YES\n";
312 print "MAX-LEVEL 1\n";
313 print "INDEX-LINE YES\n";
314 print "INDEX-XML NO\n";
315 print "MESSAGE-LINE YES\n";
316 print "MESSAGE-XML NO\n";
317 print "RECORD YES\n";
318 print "COLLECTION NO\n";
319 print "MULTI-ESTIMATE NO\n";
320 print "CALCSIZE NO\n";
321 print "CLIENT-ESTIMATE YES\n";
322 print "EXCLUDE-FILE YES\n";
323 print "EXCLUDE-LIST YES\n";
324 print "EXCLUDE-OPTIONAL YES\n";
325 print "INCLUDE-FILE YES\n";
326 print "INCLUDE-LIST YES\n";
327 print "INCLUDE-OPTIONAL YES\n";
328 print "RECOVER-MODE SMB\n";
331 sub command_selfcheck {
334 $self->print_to_server("disk " . quote_string($self->{disk}));
336 $self->print_to_server("amsamba version " . $Amanda::Constants::VERSION,
337 $Amanda::Script_App::GOOD);
339 if (!defined($self->{smbclient}) || $self->{smbclient} eq "") {
340 $self->print_to_server(
341 "smbclient not set; you must define the SMBCLIENT-PATH property",
342 $Amanda::Script_App::ERROR);
344 elsif (! -e $self->{smbclient}) {
345 $self->print_to_server("$self->{smbclient} doesn't exist",
346 $Amanda::Script_App::ERROR);
348 elsif (! -x $self->{smbclient}) {
349 $self->print_to_server("$self->{smbclient} is not executable",
350 $Amanda::Script_App::ERROR);
352 my @sv = `$self->{smbclient} --version`;
354 $sv[0] =~ /^[^0-9]*(.*)$/;
356 $self->print_to_server("amsamba smbclient-version $sv",
357 $Amanda::Script_App::GOOD);
359 $self->print_to_server(
360 "[Can't get " . $self->{smbclient} . " version]\n",
361 $Amanda::Script_App::ERROR);
365 $self->print_to_server("$self->{smbclient}",
366 $Amanda::Script_App::GOOD);
367 if (!defined $self->{disk} || !defined $self->{device}) {
370 $self->parsesharename();
372 $self->validate_inexclude();
374 print "OK " . $self->{share} . "\n";
375 print "OK " . $self->{device} . "\n";
376 print "OK " . $self->{directory} . "\n" if defined $self->{directory};
378 my ($child_rdr, $parent_wtr);
379 if (defined $self->{password}) {
380 # Don't set close-on-exec
382 pipe($child_rdr, $parent_wtr);
384 $parent_wtr->autoflush(1);
386 my($wtr, $rdr, $err);
387 $err = Symbol::gensym;
388 my $pid = open3($wtr, $rdr, $err, "-");
391 if (defined $self->{password}) {
392 my $ff = $child_rdr->fileno;
393 debug("child_rdr $ff");
394 $parent_wtr->close();
395 $ENV{PASSWD_FD} = $child_rdr->fileno;
400 push @ARGV, $self->{smbclient}, $self->{share};
401 push @ARGV, "" if (!defined $self->{password});
402 push @ARGV, "-U", $self->{username},
404 if (defined $self->{domain}) {
405 push @ARGV, "-W", $self->{domain},
407 if (defined $self->{subdir}) {
408 push @ARGV, "-D", $self->{subdir},
410 push @ARGV, "-c", "quit";
411 debug("execute: " . $self->{smbclient} . " " .
413 exec {$self->{smbclient}} @ARGV;
416 if (defined $self->{password}) {
417 my $ff = $parent_wtr->fileno;
418 debug("parent_wtr $ff");
419 $parent_wtr->print($self->{password});
420 $parent_wtr->close();
423 debug("No password");
429 debug("stderr: " . $_);
431 # message if samba server is configured with 'security = share'
432 next if /Server not using user level security and no password supplied./;
433 $self->print_to_server("smbclient: $_",
434 $Amanda::Script_App::ERROR);
442 sub command_estimate {
445 $self->parsesharename();
447 $self->validate_inexclude();
449 my $level = $self->{level}[0];
450 my ($child_rdr, $parent_wtr);
451 if (defined $self->{password}) {
452 # Don't set close-on-exec
454 pipe($child_rdr, $parent_wtr);
456 $parent_wtr->autoflush(1);
458 my($wtr, $rdr, $err);
459 $err = Symbol::gensym;
460 my $pid = open3($wtr, $rdr, $err, "-");
463 if (defined $self->{password}) {
464 my $ff = $child_rdr->fileno;
465 debug("child_rdr $ff");
466 $parent_wtr->close();
467 $ENV{PASSWD_FD} = $child_rdr->fileno;
472 push @ARGV, $self->{smbclient}, $self->{share};
473 push @ARGV, "" if (!defined($self->{password}));
474 push @ARGV, "-d", "0",
475 "-U", $self->{username},
477 if (defined $self->{domain}) {
478 push @ARGV, "-W", $self->{domain},
480 if (defined $self->{subdir}) {
481 push @ARGV, "-D", $self->{subdir},
484 push @ARGV, "-c", "archive 0;recurse;du";
486 push @ARGV, "-c", "archive 1;recurse;du";
488 debug("execute: " . $self->{smbclient} . " " .
490 exec {$self->{smbclient}} @ARGV;
493 if (defined $self->{password}) {
494 my $ff = $parent_wtr->fileno;
495 debug("parent_wtr $ff");
496 debug("password $self->{password}");
497 $parent_wtr->print($self->{password});
498 $parent_wtr->close();
503 my $size = $self->parse_estimate($err);
505 output_size($level, $size);
516 next if /blocks of size/;
517 next if /blocks available/;
520 next if /dumped \d+ files and directories/;
521 # message if samba server is configured with 'security = share'
522 next if /Server not using user level security and no password supplied./;
524 if ($_ =~ /^Total number of bytes: (\d*)/) {
528 $self->print_to_server("smbclient: $_",
529 $Amanda::Script_App::ERROR);
539 print "$level -1 -1\n";
543 my($ksize) = int $size / (1024);
544 $ksize=32 if ($ksize<32);
545 print "$level $ksize 1\n";
552 my $level = $self->{level}[0];
554 $self->parsesharename();
556 $self->validate_inexclude();
558 my $pid_tee = open3(\*INDEX_IN, '>&STDOUT', \*INDEX_TEE, "-");
564 while (($size = POSIX::read(0, $buf, 32768)) > 0) {
565 POSIX::write(1, $buf, $size);
566 POSIX::write(2, $buf, $size);
570 my ($child_rdr, $parent_wtr);
571 if (defined $self->{password}) {
572 # Don't set close-on-exec
574 pipe($child_rdr, $parent_wtr);
576 $parent_wtr->autoflush(1);
579 $err = Symbol::gensym;
580 my $pid = open3($wtr, ">&INDEX_IN", $err, "-");
583 if (defined $self->{password}) {
584 my $ff = $child_rdr->fileno;
585 debug("child_rdr $ff");
586 $parent_wtr->close();
587 $ENV{PASSWD_FD} = $child_rdr->fileno;
591 push @ARGV, $self->{smbclient}, $self->{share};
592 push @ARGV, "" if (!defined($self->{password}));
593 push @ARGV, "-d", "0",
594 "-U", $self->{username},
596 if (defined $self->{domain}) {
597 push @ARGV, "-W", $self->{domain},
599 if (defined $self->{subdir}) {
600 push @ARGV, "-D", $self->{subdir},
605 $comm = "tarmode full reset hidden system quiet;";
607 $comm = "tarmode inc noreset hidden system quiet;";
610 if ($#{$self->{exclude}} >= 0) {
613 if ($#{$self->{include}} >= 0) {
617 if ($#{$self->{exclude}} >= 0) {
618 $comm .= " " . join(" ", @{$self->{exclude}});
620 if ($#{$self->{include}} >= 0) {
621 $comm .= " " . join(" ", @{$self->{include}});
623 push @ARGV, "-c", $comm;
624 debug("execute: " . $self->{smbclient} . " " .
626 exec {$self->{smbclient}} @ARGV;
629 if (defined $self->{password}) {
630 my $ff = $parent_wtr->fileno;
631 debug("parent_wtr $ff");
632 debug("password $self->{password}");
633 $parent_wtr->print($self->{password});
634 $parent_wtr->close();
637 debug("No password");
643 debug("$self->{gnutar} -tf -");
644 my $pid_index1 = open2($index, '<&INDEX_TEE', $self->{gnutar}, "-tf", "-");
647 my $index_fd = $index->fileno;
648 debug("index $index_fd");
649 if (defined($self->{index})) {
651 open($indexout_fd, '>&=4') ||
652 $self->print_to_server_and_die("Can't open indexout_fd: $!",
653 $Amanda::Script_App::ERROR);
654 $self->parse_backup($index, $self->{mesgout}, $indexout_fd);
658 $self->parse_backup($index_fd, $self->{mesgout}, undef);
664 debug("stderr: " . $_);
666 next if /^tarmode is now /;
667 next if /dumped (\d+) files and directories/;
668 # message if samba server is configured with 'security = share'
669 next if /Server not using user level security and no password supplied./;
670 if (/^Total bytes written: (\d*)/) {
673 $self->print_to_server("smbclient: $_",
674 $Amanda::Script_App::ERROR);
678 my $ksize = $size / 1024;
682 print {$self->{mesgout}} "sendbackup: size $ksize\n";
683 print {$self->{mesgout}} "sendbackup: end\n";
688 $self->print_to_server_and_die("smbclient returned error",
689 $Amanda::Script_App::ERROR);
696 my($fhin, $fhout, $indexout) = @_;
700 if(defined($indexout)) {
701 if(defined($self->{index})) {
713 sub command_index_from_output {
714 index_from_output(0, 1);
718 sub index_from_output {
719 my($fhin, $fhout) = @_;
722 next if /^Total bytes written:/;
729 sub command_index_from_image {
732 open($index_fd, "$self->{gnutar} --list --file - |") ||
733 $self->print_to_server_and_die("Can't run $self->{gnutar}: $!",
734 $Amanda::Script_App::ERROR);
735 index_from_output($index_fd, 1);
738 sub command_restore {
742 $self->parsesharename();
743 chdir(Amanda::Util::get_original_cwd());
745 if ($self->{recover_mode} eq "smb") {
746 $self->validate_inexclude();
748 push @cmd, $self->{smbclient}, $self->{share};
749 push @cmd, "" if (!defined $self->{password});
750 push @cmd, "-d", "0",
751 "-U", $self->{username};
753 if (defined $self->{domain}) {
754 push @cmd, "-W", $self->{domain};
756 if (defined $self->{'include_filename'}) {
757 push @cmd, "-TFx", "-", "$self->{'include_filename'}";
759 push @cmd, "-Tx", "-";
760 if ($#{$self->{include}} >= 0) {
761 push @cmd, @{$self->{include}};
763 for(my $i=1;defined $ARGV[$i]; $i++) {
764 my $param = $ARGV[$i];
769 my ($parent_rdr, $child_wtr);
770 if (defined $self->{password}) {
771 # Don't set close-on-exec
773 pipe($parent_rdr, $child_wtr);
775 $child_wtr->autoflush(1);
777 my($wtr, $rdr, $err);
778 $err = Symbol::gensym;
779 my $pid = open3($wtr, $rdr, $err, "-");
781 $child_wtr->print($self->{password});
785 if (defined $self->{password}) {
787 $ENV{PASSWD_FD} = $parent_rdr->fileno;
789 debug("cmd:" . join(" ", @cmd));
790 exec { $cmd[0] } @cmd;
791 die("Can't exec '", $cmd[0], "'");
793 push @cmd, $self->{gnutar}, "-xpvf", "-";
794 if (defined $self->{directory}) {
795 if (!-d $self->{directory}) {
796 $self->print_to_server_and_die(
797 "Directory $self->{directory}: $!",
798 $Amanda::Script_App::ERROR);
800 if (!-w $self->{directory}) {
801 $self->print_to_server_and_die(
802 "Directory $self->{directory}: $!",
803 $Amanda::Script_App::ERROR);
805 push @cmd, "--directory", $self->{directory};
807 if ($#{$self->{include_list}} == 0) {
808 push @cmd, "--files-from", $self->{include_list}[0];
810 if ($#{$self->{exclude_list}} == 0) {
811 push @cmd, "--exclude-from", $self->{exclude_list}[0];
813 for(my $i=1;defined $ARGV[$i]; $i++) {
814 my $param = $ARGV[$i];
818 debug("cmd:" . join(" ", @cmd));
819 exec { $cmd[0] } @cmd;
820 die("Can't exec '", $cmd[0], "'");
824 sub command_validate {
827 if (!defined($self->{gnutar}) || !-x $self->{gnutar}) {
828 return $self->default_validate();
831 my(@cmd) = ($self->{gnutar}, "-tf", "-");
832 debug("cmd:" . join(" ", @cmd));
833 my $pid = open3('>&STDIN', '>&STDOUT', '>&STDERR', @cmd) ||
834 $self->print_to_server_and_die("Unable to run @cmd: $!",
835 $Amanda::Script_App::ERROR);
838 $self->print_to_server_and_die("$self->{gnutar} returned error",
839 $Amanda::Script_App::ERROR);
844 sub command_print_command {
851 Usage: amsamba <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --calcsize.
868 my $opt_smbclient_path;
870 my @opt_exclude_file;
871 my @opt_exclude_list;
872 my $opt_exclude_optional;
873 my @opt_include_file;
874 my @opt_include_list;
875 my $opt_include_optional;
876 my $opt_recover_mode;
877 my $opt_allow_anonymous;
880 Getopt::Long::Configure(qw{bundling});
882 'version' => \$opt_version,
883 'config=s' => \$opt_config,
884 'host=s' => \$opt_host,
885 'disk=s' => \$opt_disk,
886 'device=s' => \$opt_device,
887 'level=s' => \@opt_level,
888 'index=s' => \$opt_index,
889 'message=s' => \$opt_message,
890 'collection=s' => \$opt_collection,
891 'record' => \$opt_record,
892 'calcsize' => \$opt_calcsize,
893 'gnutar-path=s' => \$opt_gnutar_path,
894 'smbclient-path=s' => \$opt_smbclient_path,
895 'amandapass=s' => \$opt_amandapass,
896 'exclude-file=s' => \@opt_exclude_file,
897 'exclude-list=s' => \@opt_exclude_list,
898 'exclude-optional=s' => \$opt_exclude_optional,
899 'include-file=s' => \@opt_include_file,
900 'include-list=s' => \@opt_include_list,
901 'include-optional=s' => \$opt_include_optional,
902 'recover-mode=s' => \$opt_recover_mode,
903 'allow-anonymous=s' => \$opt_allow_anonymous,
904 'directory=s' => \$opt_directory,
907 if (defined $opt_version) {
908 print "amsamba-" . $Amanda::Constants::VERSION , "\n";
912 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, $opt_allow_anonymous, $opt_directory);
914 $application->do($ARGV[0]);