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