1#!@PERL@
2# Copyright (c) 2008-2013 Zmanda, Inc.  All Rights Reserved.
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License
6# as published by the Free Software Foundation; either version 2
7# of the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17#
18# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20
21use lib '@amperldir@';
22use strict;
23use warnings;
24use Getopt::Long;
25
26package Amanda::Application::Amzfs_sendrecv;
27use base qw(Amanda::Application Amanda::Application::Zfs);
28use File::Copy;
29use File::Path;
30use IPC::Open3;
31use Sys::Hostname;
32use Symbol;
33use Amanda::Constants;
34use Amanda::Config qw( :init :getconf  config_dir_relative );
35use Amanda::Debug qw( :logging );
36use Amanda::Paths;
37use Amanda::Util qw( :constants quote_string );
38
39sub new {
40    my $class = shift;
41    my ($config, $host, $disk, $device, $level, $index, $message, $collection, $record, $df_path, $zfs_path, $pfexec_path, $pfexec, $keep_snapshot, $exclude_list, $include_list, $directory) = @_;
42    my $self = $class->SUPER::new($config);
43
44    $self->{config}     = $config;
45    $self->{host}       = $host;
46    if (defined $disk) {
47	$self->{disk}      = $disk;
48    } else {
49	$self->{disk}      = $device;
50    }
51    if (defined $device) {
52	$self->{device}    = $device;
53    } else {
54	$self->{device}    = $disk;
55    }
56    $self->{level}      = [ @{$level} ];
57    $self->{index}      = $index;
58    $self->{message}    = $message;
59    $self->{collection} = $collection;
60    $self->{record}     = $record;
61    $self->{df_path}       = $df_path;
62    $self->{zfs_path}      = $zfs_path;
63    $self->{pfexec_path}   = $pfexec_path;
64    $self->{pfexec}        = $pfexec;
65    $self->{keep_snapshot} = $keep_snapshot;
66    $self->{pfexec_cmd}    = undef;
67    $self->{exclude_list}  = [ @{$exclude_list} ];
68    $self->{include_list}  = [ @{$include_list} ];
69    $self->{directory}     = $directory;
70
71    if ($self->{keep_snapshot} =~ /^YES$/i) {
72        $self->{keep_snapshot} = "YES";
73	if (!defined $self->{record}) {
74	    $self->{keep_snapshot} = "NO";
75 	}
76    }
77
78    return $self;
79}
80
81sub check_for_backup_failure {
82   my $self = shift;
83
84   $self->zfs_destroy_snapshot();
85}
86
87sub command_support {
88   my $self = shift;
89
90   print "CONFIG YES\n";
91   print "HOST YES\n";
92   print "DISK YES\n";
93   print "MAX-LEVEL 9\n";
94   print "INDEX-LINE YES\n";
95   print "INDEX-XML NO\n";
96   print "MESSAGE-LINE YES\n";
97   print "MESSAGE-XML NO\n";
98   print "RECORD YES\n";
99   print "COLLECTION NO\n";
100   print "CLIENT-ESTIMATE YES\n";
101}
102
103sub command_selfcheck {
104    my $self = shift;
105
106    $self->print_to_server("disk " . quote_string($self->{disk}),
107			   $Amanda::Script_App::GOOD);
108
109    $self->print_to_server("amzfs-sendrecv version " . $Amanda::Constants::VERSION,
110			   $Amanda::Script_App::GOOD);
111    $self->zfs_set_value();
112
113    if (!defined $self->{device}) {
114	return;
115    }
116
117    if ($self->{error_status} == $Amanda::Script_App::GOOD) {
118	$self->zfs_create_snapshot();
119	$self->zfs_destroy_snapshot();
120	print "OK " . $self->{device} . "\n";
121    }
122
123    if ($#{$self->{include_list}} >= 0) {
124	$self->print_to_server("include-list not supported for backup",
125			       $Amanda::Script_App::ERROR);
126    }
127    if ($#{$self->{exclude_list}} >= 0) {
128	$self->print_to_server("exclude-list not supported for backup",
129			       $Amanda::Script_App::ERROR);
130    }
131}
132
133sub command_estimate() {
134    my $self = shift;
135    my $level = 0;
136
137    if ($#{$self->{include_list}} >= 0) {
138	$self->print_to_server("include-list not supported for backup",
139			       $Amanda::Script_App::ERROR);
140    }
141    if ($#{$self->{exclude_list}} >= 0) {
142	$self->print_to_server("exclude-list not supported for backup",
143			       $Amanda::Script_App::ERROR);
144    }
145
146    $self->zfs_set_value();
147    $self->zfs_create_snapshot();
148
149    while (defined ($level = shift @{$self->{level}})) {
150      debug "Estimate of level $level";
151      my $size = $self->estimate_snapshot($level);
152      output_size($level, $size);
153    }
154
155    $self->zfs_destroy_snapshot();
156
157    exit 0;
158}
159
160sub output_size {
161   my($level) = shift;
162   my($size) = shift;
163   if($size == -1) {
164      print "$level -1 -1\n";
165      #exit 2;
166   }
167   else {
168      my($ksize) = int $size / (1024);
169      $ksize=32 if ($ksize<32);
170      print "$level $ksize 1\n";
171   }
172}
173
174sub command_backup {
175    my $self = shift;
176
177    if ($#{$self->{include_list}} >= 0) {
178	$self->print_to_server("include-list not supported for backup",
179			       $Amanda::Script_App::ERROR);
180    }
181    if ($#{$self->{exclude_list}} >= 0) {
182	$self->print_to_server("exclude-list not supported for backup",
183			       $Amanda::Script_App::ERROR);
184    }
185
186    $self->zfs_set_value();
187    $self->zfs_create_snapshot();
188
189    my $size = -1;
190    my $level = $self->{level}[0];
191    my $cmd;
192    debug "Backup of level $level";
193    if ($level == 0) {
194       $cmd = "$self->{pfexec_cmd} $self->{zfs_path} send $self->{filesystem}\@$self->{snapshot} | $Amanda::Paths::amlibexecdir/teecount";
195    } else {
196      my $refsnapshotname = $self->zfs_find_snapshot_level($level-1);
197      debug "Referenced snapshot name: $refsnapshotname|";
198      if ($refsnapshotname ne "") {
199        $cmd = "$self->{pfexec_cmd} $self->{zfs_path} send -i $self->{filesystem}\@$refsnapshotname $self->{filesystem}\@$self->{snapshot} | $Amanda::Paths::amlibexecdir/teecount";
200      } else {
201        $self->print_to_server_and_die("cannot backup snapshot '$self->{filesystem}\@$self->{snapshot}': reference snapshot doesn't exists for level $level", $Amanda::Script_App::ERROR);
202      }
203    }
204
205    debug "running (backup): $cmd";
206    my($wtr, $err, $pid);
207    my($errmsg);
208    $err = Symbol::gensym;
209    $pid = open3($wtr, '>&STDOUT', $err, $cmd);
210    close $wtr;
211
212    if (defined($self->{index})) {
213	my $indexout;
214	open($indexout, '>&=4') ||
215	$self->print_to_server_and_die("Can't open indexout: $!",
216				       $Amanda::Script_App::ERROR);
217	print $indexout "/\n";
218	close($indexout);
219    }
220
221    $errmsg = <$err>;
222    waitpid $pid, 0;
223    close $err;
224    if ($? !=  0) {
225        if (defined $errmsg) {
226            $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
227        } else {
228            $self->print_to_server_and_die("cannot backup snapshot '$self->{filesystem}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
229        }
230    }
231    $size = $errmsg;
232    debug "Dump done";
233
234    my($ksize) = int ($size/1024);
235    $ksize=32 if ($ksize<32);
236
237    print {$self->{mesgout}} "sendbackup: size $ksize\n";
238    print {$self->{mesgout}} "sendbackup: end\n";
239
240    # destroy all snapshot of this level and higher
241    $self->zfs_purge_snapshot($level, 9);
242
243    if ($self->{keep_snapshot} eq 'YES') {
244	$self->zfs_rename_snapshot($level);
245    } else {
246	$self->zfs_destroy_snapshot();
247    }
248
249    exit 0;
250}
251
252sub estimate_snapshot
253{
254    my $self = shift;
255    my $level = shift;
256
257    debug "\$filesystem = $self->{filesystem}";
258    debug "\$snapshot = $self->{snapshot}";
259    debug "\$level = $level";
260
261    my $cmd;
262    if ($level == 0) {
263      $cmd = "$self->{pfexec_cmd} $self->{zfs_path} get -Hp -o value referenced $self->{filesystem}\@$self->{snapshot}";
264    } else {
265      my $refsnapshotname = $self->zfs_find_snapshot_level($level-1);
266      debug "Referenced snapshot name: $refsnapshotname|";
267      if ($refsnapshotname ne "") {
268        $cmd = "$self->{pfexec_cmd} $self->{zfs_path} send -i $refsnapshotname $self->{filesystem}\@$self->{snapshot} | /usr/bin/wc -c";
269      } else {
270        return "-1";
271      }
272    }
273    debug "running (estimate): $cmd";
274    my($wtr, $rdr, $err, $pid);
275    $err = Symbol::gensym;
276    $pid = open3($wtr, $rdr, $err, $cmd);
277    close $wtr;
278    my ($msg) = <$rdr>;
279    my ($errmsg) = <$err>;
280    waitpid $pid, 0;
281    close $rdr;
282    close $err;
283    if ($? !=  0) {
284        if (defined $msg && defined $errmsg) {
285            $self->print_to_server_and_die("$msg, $errmsg", $Amanda::Script_App::ERROR);
286        } elsif (defined $msg) {
287            $self->print_to_server_and_die($msg, $Amanda::Script_App::ERROR);
288        } elsif (defined $errmsg) {
289            $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
290        } else {
291	    $self->print_to_server_and_die("cannot estimate snapshot '$self->{snapshot}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
292	}
293    }
294    if ($level == 0) {
295	my $compratio = $self->get_compratio();
296	$compratio =~ s/x$//;
297	$msg *= $compratio;
298    }
299
300    return $msg;
301}
302
303sub get_compratio
304{
305    my $self = shift;
306
307    my $cmd;
308    $cmd =  "$self->{pfexec_cmd} $self->{zfs_path} get -Hp -o value compressratio $self->{filesystem}\@$self->{snapshot}";
309    debug "running (get-compression): $cmd";
310    my($wtr, $rdr, $err, $pid);
311    $err = Symbol::gensym;
312    $pid = open3($wtr, $rdr, $err, $cmd);
313    close $wtr;
314    my ($msg) = <$rdr>;
315    chomp($msg) if defined $msg;
316    my ($errmsg) = <$err>;
317    chomp($errmsg) if defined $errmsg;
318    waitpid $pid, 0;
319    close $rdr;
320    close $err;
321    if ($? !=  0) {
322        if (defined $msg && defined $errmsg) {
323            $self->print_to_server_and_die("$msg, $errmsg", $Amanda::Script_App::ERROR);
324        } elsif (defined $msg) {
325            $self->print_to_server_and_die($msg, $Amanda::Script_App::ERROR);
326        } elsif (defined $errmsg) {
327            $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
328        } else {
329	    $self->print_to_server_and_die("cannot read compression ratio '$self->{snapshot}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
330	}
331    }
332    return $msg
333}
334
335sub command_index_from_output {
336}
337
338sub command_index_from_image {
339}
340
341sub command_restore {
342    my $self = shift;
343
344    my $current_snapshot;
345    my $level = $self->{level}[0];
346    my $device = $self->{device};
347    if (defined $device) {
348	$device =~ s,^/,,;
349	$current_snapshot = $self->zfs_build_snapshotname($device);
350	$self->{'snapshot'} = $self->zfs_build_snapshotname($device, $level);
351    }
352
353    my $directory = $device;
354    $directory = $self->{directory} if defined $self->{directory};
355    $directory =~ s,^/,,;
356
357    my @cmd = ();
358
359    if ($self->{pfexec_cmd}) {
360	push @cmd, $self->{pfexec_cmd};
361    }
362    push @cmd, $self->{zfs_path};
363    push @cmd, "recv";
364    push @cmd, $directory;
365
366    debug("cmd:" . join(" ", @cmd));
367    system @cmd;
368
369    my $snapshotname;
370    my $newsnapname;
371    if (defined $device) {
372	$snapshotname = "$directory\@$current_snapshot";
373	$newsnapname = "$directory\@$self->{'snapshot'}";
374    } else {
375	# find snapshot name
376	@cmd = ();
377	if ($self->{pfexec_cmd}) {
378	    push @cmd, $self->{pfexec_cmd};
379	}
380	push @cmd, $self->{zfs_path};
381	push @cmd, "list";
382	push @cmd, "-r";
383	push @cmd, "-t";
384	push @cmd, "snapshot";
385	push @cmd, $directory;
386	debug("cmd:" . join(" ", @cmd));
387
388	my($wtr, $rdr, $err, $pid);
389	my($msg, $errmsg);
390	$err = Symbol::gensym;
391	$pid = open3($wtr, $rdr, $err, @cmd);
392	close $wtr;
393	while ($msg = <$rdr>) {
394	    next if $msg =~ /^NAME/;
395	    my ($name, $used, $avail) = split(/ +/, $msg);
396	    if ($name =~ /-current$/) {
397		$snapshotname = $name;
398		last;
399	    }
400	}
401	$errmsg = <$err>;
402	waitpid $pid, 0;
403    	close $rdr;
404	close $err;
405
406	if (defined $snapshotname and defined($level)) {
407	    $newsnapname = $snapshotname;
408	    $newsnapname =~ s/current$/$level/;
409	} else {
410	    # destroy the snapshot
411	    # restoring next level will fail.
412	    @cmd = ();
413	    if ($self->{pfexec_cmd}) {
414		push @cmd, $self->{pfexec_cmd};
415	    }
416	    push @cmd, $self->{zfs_path};
417	    push @cmd, "destroy";
418	    push @cmd, $snapshotname;
419
420	    debug("cmd:" . join(" ", @cmd));
421	    system @cmd;
422	}
423    }
424
425    if (defined $newsnapname) {
426	# rename -current snapshot to -level
427	@cmd = ();
428	if ($self->{pfexec_cmd}) {
429	    push @cmd, $self->{pfexec_cmd};
430	}
431	push @cmd, $self->{zfs_path};
432	push @cmd, "rename";
433	push @cmd, $snapshotname;
434	push @cmd, $newsnapname;
435
436	debug("cmd:" . join(" ", @cmd));
437	system @cmd;
438    }
439}
440
441sub command_print_command {
442}
443
444package main;
445
446sub usage {
447    print <<EOF;
448Usage: amzfs-sendrecv <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --df-path=<path/to/df> --zfs-path=<path/to/zfs> --pfexec-path=<path/to/pfexec> --pfexec=<yes|no> --keep-snapshot=<yes|no>.
449EOF
450    exit(1);
451}
452
453my $opt_config;
454my $opt_host;
455my $opt_disk;
456my $opt_device;
457my @opt_level;
458my $opt_index;
459my $opt_message;
460my $opt_collection;
461my $opt_record;
462my $df_path  = 'df';
463my $zfs_path = 'zfs';
464my $pfexec_path = 'pfexec';
465my $pfexec = "NO";
466my $opt_keep_snapshot = "YES";
467my @opt_exclude_list;
468my @opt_include_list;
469my $opt_directory;
470
471my @orig_argv = @ARGV;
472
473Getopt::Long::Configure(qw{bundling});
474GetOptions(
475    'config=s'        => \$opt_config,
476    'host=s'          => \$opt_host,
477    'disk=s'          => \$opt_disk,
478    'device=s'        => \$opt_device,
479    'level=s'         => \@opt_level,
480    'index=s'         => \$opt_index,
481    'message=s'       => \$opt_message,
482    'collection=s'    => \$opt_collection,
483    'record'          => \$opt_record,
484    'df-path=s'       => \$df_path,
485    'zfs-path=s'      => \$zfs_path,
486    'pfexec-path=s'   => \$pfexec_path,
487    'pfexec=s'        => \$pfexec,
488    'keep-snapshot=s' => \$opt_keep_snapshot,
489    'exclude-list=s'  => \@opt_exclude_list,
490    'include-list=s'  => \@opt_include_list,
491    'directory=s'     => \$opt_directory,
492) or usage();
493
494my $application = Amanda::Application::Amzfs_sendrecv->new($opt_config, $opt_host, $opt_disk, $opt_device, \@opt_level, $opt_index, $opt_message, $opt_collection, $opt_record, $df_path, $zfs_path, $pfexec_path, $pfexec, $opt_keep_snapshot, \@opt_exclude_list, \@opt_include_list, $opt_directory);
495
496Amanda::Debug::debug("Arguments: " . join(' ', @orig_argv));
497
498$application->do($ARGV[0]);
499
500