text: # Copyright (c) 2008-2012 Zmanda, Inc.  All Rights Reserved.
  # Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
  #
  # This program is free software; you can redistribute it and/or
  # modify it under the terms of the GNU General Public License
  # as published by the Free Software Foundation; either version 2
  # of the License, or (at your option) any later version.
  #
  # This program is distributed in the hope that it will be useful, but
  # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  # for more details.
  #
  # You should have received a copy of the GNU General Public License along
  # with this program; if not, write to the Free Software Foundation, Inc.,
  # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
  #
  # Contact information: Carbonite Inc., 756 N Pastoria Ave
  # Sunnyvale, CA 94085, or: http://www.zmanda.com
  
  package Amanda::Application::Zfs;
  
  use strict;
  use warnings;
  use Symbol;
  use IPC::Open3;
  use Amanda::Debug qw( :logging );
  
  =head1 NAME
  
  Amanda::Application::Zfs -- collection of function to use with zfs
  
  =head1 SYNOPSIS
  
  =head1 INTERFACE
  
  =cut
  
  sub zfs_set_value {
      my $self = shift;
  
      if (defined $self->{execute_where} && $self->{execute_where} ne "client") {
  	$self->print_to_server_and_die(" Script must be run on the client 'execute-where client'", $Amanda::Script_App::ERROR);
      }
      if ($self->{df_path} ne "df" && !-e $self->{df_path}) {
  	$self->print_to_server_and_die("Can't execute DF-PATH '$self->{df_path}' command",
                          $Amanda::Script_App::ERROR);
      }
      if ($self->{zfs_path} ne "zfs" && !-e $self->{zfs_path}) {
          $self->print_to_server_and_die("Can't execute ZFS-PATH '$self->{zfs_path}' command",
                          $Amanda::Script_App::ERROR);
      }
  
      if ($self->{pfexec} =~ /^YES$/i) {
          $self->{pfexec_cmd} = $self->{pfexec_path};
      }
      if (defined $self->{pfexec_cmd} && $self->{pfexec_cmd} ne "pfexec" && !-e $self->{pfexec_cmd}) {
          $self->print_to_server_and_die("Can't execute PFEXEC-PATH '$self->{pfexec_cmd}' command",
                          $Amanda::Script_App::ERROR);
      }
      if (!defined $self->{pfexec_cmd}) {
          $self->{pfexec_cmd} = "";
      }
  
      if (!defined $self->{device}) {
  	if ($self->{action} eq "check") {
  	    return;
  	} else {
  	    $self->print_to_server_and_die("'--device' is not provided",
  					   $Amanda::Script_App::ERROR);
  	}
      }
  
      my $device = $self->{device};
      $device = $self->{target} if defined $self->{target};
      # determine if $device is a mountpoint or ZFS dataset
      my $cmd = "$self->{pfexec_cmd} $self->{zfs_path} get -H -o value mountpoint $device";
      debug "running: $cmd";
      my($wtr, $rdr, $err, $pid);
      $err = Symbol::gensym;
      $pid = open3($wtr, $rdr, $err, $cmd);
      close $wtr;
      my $zmountpoint = <$rdr>;
      waitpid $pid, 0;
      close $rdr;
      close $err;
  
      if ($? == 0) {
          chomp $zmountpoint;
  
          # zfs dataset supplied
          $self->{filesystem} = $device;
  
          # check if zfs volume
          if ($zmountpoint ne '-') {
              $self->{mountpoint} = $zmountpoint;
          } else {
              $self->{mountpoint} = undef;
          }
      } else {
          # filesystem, directory or invalid ZFS dataset name
          $cmd = "$self->{df_path} $device";
          debug "running: $self->{df_path} $device";
          $err = Symbol::gensym;
          $pid = open3($wtr, $rdr, $err, $cmd);
          close $wtr;
          my @ret;
          while (<$rdr>) {
              chomp;
              push @ret,$_;
          }
          my $errmsg = <$err>;
          waitpid $pid, 0;
          close $rdr;
          close $err;
  
          if ($? != 0) {
  	    my $ret = $ret[0];
              # invalid filesystem of ZFS dataset name
              if (defined $errmsg) {
                  chomp $errmsg;
              }
              if (defined $ret && defined $errmsg) {
                  $self->print_to_server_and_die("$ret, $errmsg", $Amanda::Script_App::ERROR);
              } elsif (defined $ret) {
                  $self->print_to_server_and_die($ret, $Amanda::Script_App::ERROR);
              } elsif (defined $errmsg) {
                  $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
              } else {
                  $self->print_to_server_and_die(
                              "Failed to find mount points: $device",
                              $Amanda::Script_App::ERROR);
              }
          }
  
          my $size = @ret;
          if ($size eq 1) {
              # Solaris type df
              @ret = split /:/, $ret[0];
              if ($ret[0] =~ /(\S*)(\s*)(\()(\S*)(\s*)(\))$/) {
                  $self->{mountpoint} = $1;
                  $self->{filesystem} = $4;
              } else {
                  $self->print_to_server_and_die(
                              "Failed to find mount points: $device",
                              $Amanda::Script_App::ERROR);
              }
          } else {
              # FreeBSD type df with header
              if ($ret[1] =~ /^(\S+)(\s+)((\S+)(\s+))+(\S+)(\s*)$/) {
                  $self->{mountpoint} = $6;
                  $self->{filesystem} = $1;
              } else {
                  $self->print_to_server_and_die(
                              "Failed to find mount points: $device",
                              $Amanda::Script_App::ERROR);
              }
          }
  
          $cmd = "$self->{pfexec_cmd} $self->{zfs_path} get -H -o value mountpoint $self->{filesystem}";
          debug "running: $cmd|";
          $err = Symbol::gensym;
          $pid = open3($wtr, $rdr, $err, $cmd);
          close $wtr;
          $zmountpoint = <$rdr>;
  	chomp $zmountpoint;
          $errmsg = <$err>;
          waitpid $pid, 0;
          close $rdr;
          close $err;
  
          if ($? != 0) {
              if (defined $errmsg) {
                  chomp $errmsg;
              }
              if (defined $zmountpoint && defined $errmsg) {
                  $self->print_to_server_and_die($zmountpoint, $errmsg, $Amanda::Script_App::ERROR);
              } elsif (defined $zmountpoint) {
                  $self->print_to_server_and_die($zmountpoint, $Amanda::Script_App::ERROR);
              } elsif (defined $errmsg) {
                  $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
              } else {
                  $self->print_to_server_and_die(
                          "Failed to find mount points: $self->{filesystem}",
                          $Amanda::Script_App::ERROR);
              }
          }
          if ($zmountpoint ne 'legacy' && $zmountpoint ne $self->{mountpoint}) {
              $self->print_to_server_and_die(
                  "mountpoint from 'df' ($self->{mountpoint}) and 'zfs list' ($zmountpoint) differ",
                  $Amanda::Script_App::ERROR);
          }
  
          if (!($device =~ /^$self->{mountpoint}/)) {
              $self->print_to_server_and_die(
                  "mountpoint '$self->{mountpoint}' is not a prefix of the device '$device'",
                  $Amanda::Script_App::ERROR);
          }
  
      }
  
      $self->{snapshot} = $self->zfs_build_snapshotname($device);
      if (defined $self->{mountpoint}) {
  	if ($device =~ /^$self->{mountpoint}/) {
              $self->{dir} = $device;
              $self->{dir} =~ s,^$self->{mountpoint},,;
              $self->{target} = $self->{mountpoint} . "/.zfs/snapshot/" .
                                $self->{snapshot} . $self->{dir};
  	} else { # device is not the mountpoint
  	    $self->{target} = $self->{mountpoint} . "/.zfs/snapshot/" .
  			      $self->{snapshot};
  	}
      }
  }
  
  sub zfs_create_snapshot {
      my $self = shift;
  
      my $cmd = "$self->{pfexec_cmd} $self->{zfs_path} snapshot $self->{filesystem}\@$self->{snapshot}";
      debug "running: $cmd";
      my($wtr, $rdr, $err, $pid);
      $err = Symbol::gensym;
      $pid = open3($wtr, $rdr, $err, $cmd);
      close $wtr;
      my ($msg) = <$rdr>;
      my ($errmsg) = <$err>;
      waitpid $pid, 0;
      close $rdr;
      close $err;
      if( $? != 0 ) {
          if(defined $msg && defined $errmsg) {
              $self->print_to_server_and_die("$msg, $errmsg", $Amanda::Script_App::ERROR);
          } elsif (defined $msg) {
              $self->print_to_server_and_die($msg, $Amanda::Script_App::ERROR);
          } elsif (defined $errmsg) {
              $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
          } else {
              $self->print_to_server_and_die("cannot create snapshot '$self->{filesystem}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
          }
      }
  }
  
  sub zfs_destroy_snapshot {
      my $self = shift;
  
      my $cmd = "$self->{pfexec_cmd} $self->{zfs_path} destroy $self->{filesystem}\@$self->{snapshot}";
      debug "running: $cmd|";
      my($wtr, $rdr, $err, $pid);
      my($msg, $errmsg);
      $err = Symbol::gensym;
      $pid = open3($wtr, $rdr, $err, $cmd);
      close $wtr;
      $msg = <$rdr>;
      $errmsg = <$err>;
      waitpid $pid, 0;
      close $rdr;
      close $err;
      if( $? != 0 ) {
          if(defined $msg && defined $errmsg) {
              $self->print_to_server_and_die("$msg, $errmsg", $Amanda::Script_App::ERROR);
          } elsif (defined $msg) {
              $self->print_to_server_and_die($msg, $Amanda::Script_App::ERROR);
          } elsif (defined $errmsg) {
              $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
          } else {
              $self->print_to_server_and_die("cannot destroy snapshot '$self->{filesystem}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
          }
      }
  }
  
  sub zfs_destroy_snapshot_level {
      my $self = shift;
      my $level = shift;
  
      my $snapshotname = $self->zfs_find_snapshot_level($level);
      debug "zfs_destroy_snapshot_level: Current $snapshotname";
      if ($snapshotname ne "") {
        my $cmd = "$self->{pfexec_cmd} $self->{zfs_path} destroy  $self->{filesystem}\@$snapshotname";
        debug "running: $cmd|";
        my($wtr, $rdr, $err, $pid);
        my($msg, $errmsg);
        $err = Symbol::gensym;
        $pid = open3($wtr, $rdr, $err, $cmd);
        close $wtr;
        $msg = <$rdr>;
        $errmsg = <$err>;
        waitpid $pid, 0;
        close $rdr;
        close $err;
        if( $? != 0 ) {
            if(defined $msg && defined $errmsg) {
                $self->print_to_server_and_die("$msg, $errmsg", $Amanda::Script_App::ERROR);
            } elsif (defined $msg) {
                $self->print_to_server_and_die($msg, $Amanda::Script_App::ERROR);
            } elsif (defined $errmsg) {
                $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
            } else {
                $self->print_to_server_and_die("cannot destroy snapshot '$self->{filesystem}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
            }
        }
      }
  }
  
  sub zfs_rename_snapshot {
      my $self = shift;
      my $level = shift;
  
      my $device = $self->{device};
      $device = $self->{target} if defined $self->{target};
      my $newsnapshotname = $self->zfs_build_snapshotname($device, $level);
      my $cmd = "$self->{pfexec_cmd} $self->{zfs_path} rename $self->{filesystem}\@$self->{snapshot} $self->{filesystem}\@$newsnapshotname";
      debug "running: $cmd|";
      my($wtr, $rdr, $err, $pid);
      my($msg, $errmsg);
      $err = Symbol::gensym;
      $pid = open3($wtr, $rdr, $err, $cmd);
      close $wtr;
      $msg = <$rdr>;
      $errmsg = <$err>;
      waitpid $pid, 0;
      close $rdr;
      close $err;
      if( $? != 0 ) {
          if(defined $msg && defined $errmsg) {
              $self->print_to_server_and_die("$msg, $errmsg", $Amanda::Script_App::ERROR);
          } elsif (defined $msg) {
              $self->print_to_server_and_die($msg, $Amanda::Script_App::ERROR);
          } elsif (defined $errmsg) {
              $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
          } else {
              $self->print_to_server_and_die("cannot rename snapshot '$self->{filesystem}\@$self->{snapshot}': unknown reason", $Amanda::Script_App::ERROR);
          }
      }
  }
  
  sub zfs_purge_snapshot {
      my $self = shift;
      my $minlevel = shift;
      my $maxlevel = shift;
  
      my $level;
      for ($level = $maxlevel; $level >= $minlevel; $level--) {
  	debug "zfs_purge_snapshot: Check for existing snapshot at level $level";
          $self->zfs_destroy_snapshot_level($level);
      }
  }
  
  sub zfs_find_snapshot_level {
      my $self = shift;
      my $level = shift;
  
      my $device = $self->{device};
      $device = $self->{target} if defined $self->{target};
      my $snapshotname = $self->zfs_build_snapshotname($device, $level);
  
      my $cmd =  "$self->{pfexec_cmd} $self->{zfs_path} list -t snapshot $self->{filesystem}\@$snapshotname";
      debug "running: $cmd|";
      my($wtr, $rdr, $err, $pid);
      my($msg, $errmsg);
      $err = Symbol::gensym;
      $pid = open3($wtr, $rdr, $err, $cmd);
      close $wtr;
      $msg = <$rdr>;
      $errmsg = <$err>;
      waitpid $pid, 0;
      close $rdr;
      close $err;
      if( $? != 0 ) {
  	return "";
      }
      return $snapshotname;
  }
  
  sub zfs_build_snapshotname {
      my $self = shift;
      my $device = shift;
      my $level = shift;
  
      my $snapshotname = "";
  
      if ($self->{action} eq 'check') {
  	$snapshotname = "amanda-" . Amanda::Util::sanitise_filename($device) . "-check";
      } elsif (!defined $level) {
  	$snapshotname = "amanda-" . Amanda::Util::sanitise_filename($device) . "-current";
      } else {
  	$snapshotname = "amanda-" . Amanda::Util::sanitise_filename($device) . "-" . $level;
      }
  
      return $snapshotname;
  }
  
  1;
  