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