Subversion Repositories zfs_utils

Rev

Rev 41 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 41 Rev 42
Line 50... Line 50...
50
#   makeReplicateCommands: create zfs send commands for replication based on snapshot lists
50
#   makeReplicateCommands: create zfs send commands for replication based on snapshot lists
51
 
51
 
52
 
52
 
53
# Exported functions and variables
53
# Exported functions and variables
54
 
54
 
55
our @EXPORT_OK = qw(loadConfig shredFile mountDriveByLabel mountGeli logMsg runCmd makeReplicateCommands sendReport $logFileName $displayLogsOnConsole $lastRunError);
55
our @EXPORT_OK = qw(loadConfig shredFile mountDriveByLabel unmountDriveByLabel mountGeli logMsg runCmd makeReplicateCommands sendReport fatalError getDirectoryList cleanDirectory $logFileName $displayLogsOnConsole $lastRunError);
56
 
56
 
57
 
57
 
58
our $VERSION = '0.2';
58
our $VERSION = '0.2';
59
our $logFileName = '/tmp/zfs_utils.log'; # this can be overridden by the caller, and turned off with empty string
59
our $logFileName = '/tmp/zfs_utils.log'; # this can be overridden by the caller, and turned off with empty string
60
our $displayLogsOnConsole = 1; # if non-zero, log messages are also printed to console
60
our $displayLogsOnConsole = 1; # if non-zero, log messages are also printed to console
Line 178... Line 178...
178
    } else {
178
    } else {
179
       return '';
179
       return '';
180
    }
180
    }
181
}
181
}
182
 
182
 
-
 
183
# finds and unmounts a drive defined by $driveInfo.
-
 
184
# on success, removes the mount point if empty.
-
 
185
sub unmountDriveByLabel {
-
 
186
   my ( $driveInfo ) = @_;
-
 
187
   unless ($driveInfo->{label}) {
-
 
188
      logMsg("unmountDriveByLabel: No drive label provided");
-
 
189
      return '';
-
 
190
   }
-
 
191
   unless ( $driveInfo->{label} =~ /^[a-zA-Z0-9_\-]+$/ ) {
-
 
192
      logMsg("unmountDriveByLabel: Invalid label '$driveInfo->{label}'");
-
 
193
      return '';
-
 
194
   }
-
 
195
 
-
 
196
   logMsg("unmountDriveByLabel: Looking for drive with label '$driveInfo->{label}'");
-
 
197
   # default to /mnt/label if not provided
-
 
198
   $driveInfo->{mountPath} //= "/mnt/$driveInfo->{label}"; # this is where we'll mount it if we find it
-
 
199
   
-
 
200
   runCmd( "mount | grep '$driveInfo->{mountPath}'" );
-
 
201
   if ( $lastRunError ) {
-
 
202
     logMsg("Drive with label '$driveInfo->{label}' is not mounted");
-
 
203
     return '';
-
 
204
   }
-
 
205
 
-
 
206
   # unmount device
-
 
207
   runCmd( "umount $driveInfo->{mountPath}" );
-
 
208
   if ( $lastRunError ) {
-
 
209
     logMsg("Failed to unmount $driveInfo->{mountPath}: $!");
-
 
210
     return '';
-
 
211
   }
-
 
212
 
-
 
213
   # and remove the directory if empty (find command will return empty string or one filename)
-
 
214
   rmdir $driveInfo->{mountPath} unless runCmd( "find $driveInfo->{mountPath} -mindepth 1 -print -quit");
-
 
215
   return $driveInfo->{mountPath};
-
 
216
}
-
 
217
 
183
## Load a YAML configuration file into a hashref.
218
## Load a YAML configuration file into a hashref.
184
## If the file does not exist, and a default hashref is provided,
219
## If the file does not exist, and a default hashref is provided,
185
## create the file by dumping the default to YAML, then return the default.
220
## create the file by dumping the default to YAML, then return the default.
186
sub loadConfig {
221
sub loadConfig {
187
    my ($filename, $default) = @_;
222
    my ($filename, $default) = @_;
Line 211... Line 246...
211
                  1;
246
                  1;
212
               } or do {
247
               } or do {
213
                  logMsg("No YAML writer available (YAML::XS or YAML::Tiny). Could not create $filename");
248
                  logMsg("No YAML writer available (YAML::XS or YAML::Tiny). Could not create $filename");
214
               };
249
               };
215
         };
250
         };
216
 
-
 
217
         die "Failed to write default config to $filename:$!\n" unless $wrote;
251
         die "Failed to write default config to $filename:$!\n" unless $wrote;
218
        }
252
      } # if default
219
 
-
 
220
        # No default provided; nothing to create
253
      # No default provided; nothing to create
221
        return {};
254
      return {};
222
    }
255
   } # unless -e $filename
223
 
256
 
224
    my $yaml;
257
   my $yaml;
225
 
258
 
226
    # Try YAML::XS first, fall back to YAML::Tiny
259
   # Try YAML::XS first, fall back to YAML::Tiny
227
    eval {
260
   eval {
228
        require YAML::XS;
261
      require YAML::XS;
229
        YAML::XS->import();
262
      YAML::XS->import();
230
        $yaml = YAML::XS::LoadFile($filename);
263
      $yaml = YAML::XS::LoadFile($filename);
231
        logMsg("using YAML::XS to load $filename");
264
      logMsg("using YAML::XS to load $filename");
232
        1;
265
      1;
233
    } or do {
266
   } or do {
234
        eval {
267
      eval {
235
            require YAML::Tiny;
268
         require YAML::Tiny;
236
            YAML::Tiny->import();
269
         YAML::Tiny->import();
237
            $yaml = YAML::Tiny->read($filename);
270
         $yaml = YAML::Tiny->read($filename);
238
            $yaml = $yaml->[0] if $yaml;  # YAML::Tiny returns an arrayref of documents
271
         $yaml = $yaml->[0] if $yaml;  # YAML::Tiny returns an arrayref of documents
239
            logMsg("using YAML::Tiny to load $filename");
272
         logMsg("using YAML::Tiny to load $filename");
240
            1;
273
         1;
241
        } or do {
274
      } or do {
242
            logMsg("No YAML parser installed (YAML::XS or YAML::Tiny). Skipping config load from $filename");
275
         logMsg("No YAML parser installed (YAML::XS or YAML::Tiny). Skipping config load from $filename");
243
            return ($default && ref $default eq 'HASH') ? $default : {};
276
         return ($default && ref $default eq 'HASH') ? $default : {};
244
        };
277
      };
245
    };
278
   };
246
    # Ensure we have a hashref
279
   # Ensure we have a hashref
247
    die "Config file $filename did not produce a HASH.\n" unless (defined $yaml && ref $yaml eq 'HASH');
280
   die "Config file $filename did not produce a HASH.\n" unless (defined $yaml && ref $yaml eq 'HASH');
248
 
281
 
249
    return $yaml;
282
   return $yaml;
250
}
283
}
251
 
284
 
252
 
285
 
253
# Mount a GELI-encrypted ZFS pool.
286
# Mount a GELI-encrypted ZFS pool.
254
# $geliConfig - hashref containing configuration for geli
287
# $geliConfig - hashref containing configuration for geli
Line 463... Line 496...
463
   chmod 0600, $target;
496
   chmod 0600, $target;
464
 
497
 
465
   return 1;
498
   return 1;
466
}
499
}
467
 
500
 
-
 
501
# make a bunch of replicate commands and return them to the caller as a list
-
 
502
# $rootDataSet - string, the root of the snapshots exclusive of the dataset itself
-
 
503
# $sourceSnapsRef
-
 
504
# $statusRef
-
 
505
# $newStatusRef
-
 
506
# returns
468
sub makeReplicateCommands {
507
sub makeReplicateCommands {
469
   my ($sourceSnapsRef, $statusRef, $newStatusRef) = @_;
508
   my ( $sourceSnapsRef, $statusRef, $newStatusRef) = @_;
470
   $sourceSnapsRef ||= [];
509
   $sourceSnapsRef ||= [];
471
   $statusRef     ||= [];
510
   $statusRef     ||= [];
472
   $newStatusRef  ||= [];
511
   $newStatusRef  ||= [];
-
 
512
   $rootDataSet .= '/';
473
 
513
 
474
   # parse snapshots: each line is expected to have snapshot fullname as first token: pool/fs@snap ...
514
   # parse snapshots: each line is expected to have snapshot fullname as first token: pool/fs@snap ...
475
   my %snaps_by_fs;
515
   my %snaps_by_fs;
476
   foreach my $line (@$sourceSnapsRef) {
516
   foreach my $line (@$sourceSnapsRef) {
477
      next unless defined $line && $line =~ /\S/;
517
      next unless defined $line && $line =~ /\S/;
Line 573... Line 613...
573
#   email - email address to send report to
613
#   email - email address to send report to
574
#   targetDrive - hashref with keys:
614
#   targetDrive - hashref with keys:
575
#       label - GPT or msdosfs label of the target drive
615
#       label - GPT or msdosfs label of the target drive
576
#       mount_point - optional mount point to use (if not provided, /mnt/label is used)
616
#       mount_point - optional mount point to use (if not provided, /mnt/label is used)
577
# $subject is the email subject
617
# $subject is the email subject
-
 
618
# $message is the message to include in the email body
578
# $logFile is the path to the log file to send/copy
619
# $logFile is the path to the log file to include in the report
579
sub sendReport {
620
sub sendReport {
580
   my ( $reportConfig, $subject, $logFile ) = @_;
621
   my ( $reportConfig, $message, $logFile ) = @_;
581
   return unless defined $reportConfig;
622
   return unless defined $reportConfig;
-
 
623
   $logFile //= $reportConfig->{logFile};
582
   logMsg( "Beginning sendReport" );
624
   logMsg( "Beginning sendReport" );
583
   # if they have set an e-mail address, try to e-mail the report
-
 
584
   if ( defined $reportConfig->{email} && $reportConfig->{email} ne '' ) {
-
 
585
      logMsg( "Sending report via e-mail to $reportConfig->{email}");
-
 
586
      sendEmailReport( $reportConfig->{email}, $subject, $logFile );
-
 
587
   }
-
 
588
   # if targetDrive defined and there is a valid label for it, try to mount it and write the report there
625
   # if targetDrive defined and there is a valid label for it, try to mount it and write the report there
589
   if ( defined $reportConfig->{targetDrive} && defined $reportConfig->{targetDrive}->{label} && $reportConfig->{targetDrive}->{label} ) {
626
   if ( defined $reportConfig->{targetDrive} && defined $reportConfig->{targetDrive}->{label} && $reportConfig->{targetDrive}->{label} ) {
590
      logMsg( "Saving report to disk with label $reportConfig->{targetDrive}->{label}" );
627
      logMsg( "Saving report to disk with label $reportConfig->{targetDrive}->{label}" );
591
      my $mountPoint = mountDriveByLabel( $reportConfig->{targetDrive}->{label}, $reportConfig->{targetDrive}->{mount_point}, 300 );
628
      my $mountPoint = mountDriveByLabel( $reportConfig->{targetDrive}->{label}, $reportConfig->{targetDrive}->{mount_point}, 300 );
592
      if ( defined $mountPoint && $mountPoint ) {
629
      if ( defined $mountPoint && $mountPoint ) {
Line 595... Line 632...
595
         rmdir $mountPoint;
632
         rmdir $mountPoint;
596
      } else {
633
      } else {
597
         logMsg( "Warning: could not mount report target drive with label '$reportConfig->{targetDrive}->{label}'" );
634
         logMsg( "Warning: could not mount report target drive with label '$reportConfig->{targetDrive}->{label}'" );
598
      }
635
      }
599
   }
636
   }
-
 
637
   # if they have set an e-mail address, try to e-mail the report
-
 
638
   if ( defined $reportConfig->{email} && $reportConfig->{email} ne '' ) {
-
 
639
      logMsg( "Sending report via e-mail to $reportConfig->{email}" );
-
 
640
      $reportConfig->{subject} //= 'Replication Report from ' . `hostname`;
-
 
641
      sendEmailReport( $reportConfig->{email}, $reportConfig->{subject}, $message, $logFile );
-
 
642
   }
600
}
643
}
601
 
644
 
602
# Copy the report log file to the specified mount point.
645
# Copy the report log file to the specified mount point.
603
# $logFile is the path to the log file to copy.
646
# $logFile is the path to the log file to copy.
604
# $mountPoint is the mount point of the target drive.
647
# $mountPoint is the mount point of the target drive.
Line 619... Line 662...
619
# $to is the recipient email address.
662
# $to is the recipient email address.
620
# $subject is the email subject.
663
# $subject is the email subject.
621
# $logFile is the path to the log file to send.
664
# $logFile is the path to the log file to send.
622
# Does nothing if any parameter is invalid.
665
# Does nothing if any parameter is invalid.
623
sub sendEmailReport {
666
sub sendEmailReport {
624
   my ( $to, $subject, $logFile ) = @_;
667
   my ( $to, $subject, $message, $logFile ) = @_;
625
   return unless defined $to && $to ne '';
668
   return unless defined $to && $to ne '';
626
   $subject //= 'Sneakernet Replication Report from ' . `hostname`;
669
   $subject //= 'Sneakernet Replication Report from ' . `hostname`;
-
 
670
   $message //= '';
627
   $logFile //= '';
671
   $logFile //= '';
628
 
672
 
629
   logMsg( "Sending email report to $to with subject '$subject'" );
673
   logMsg( "Sending email report to $to with subject '$subject'" );
630
   open my $mailfh, '|-', '/usr/sbin/sendmail -t' or do {
674
   open my $mailfh, '|-', '/usr/sbin/sendmail -t' or do {
631
      logMsg( "Could not open sendmail: $!" );
675
      logMsg( "Could not open sendmail: $!" );
Line 635... Line 679...
635
   print $mailfh "Subject: $subject\n";
679
   print $mailfh "Subject: $subject\n";
636
   print $mailfh "MIME-Version: 1.0\n";
680
   print $mailfh "MIME-Version: 1.0\n";
637
   print $mailfh "Content-Type: text/plain; charset=\"utf-8\"\n";
681
   print $mailfh "Content-Type: text/plain; charset=\"utf-8\"\n";
638
   print $mailfh "\n"; # end of headers
682
   print $mailfh "\n"; # end of headers
639
   
683
   
-
 
684
   print $mailfh "$message\n";
640
   print $mailfh "Following is the report for replication\n\n";
685
   print $mailfh "\nLog contents:\n\n";
641
 
-
 
642
   if ( -e $logFile && open my $logfh, '<', $logFile ) {
686
   if ( -e $logFile && open my $logfh, '<', $logFile ) {
643
      while ( my $line = <$logfh> ) {
687
      while ( my $line = <$logfh> ) {
644
         print $mailfh $line;
688
         print $mailfh $line;
645
      }
689
      }
646
      close $logfh;
690
      close $logfh;
Line 649... Line 693...
649
   };
693
   };
650
 
694
 
651
   close $mailfh;
695
   close $mailfh;
652
}  
696
}  
653
 
697
 
-
 
698
# Get all file names (not directories) from a directory
-
 
699
# $dirname is directory to scan
-
 
700
# returns arrayref
-
 
701
sub getDirectoryList {
-
 
702
   my $dirname = shift;
-
 
703
   opendir( my $dh, $dirname ) || return 0;
-
 
704
   # get all file names, but leave directories alone
-
 
705
   my @files = map{ $dirname . "/$_" } grep { -f "$dirname/$_" } readdir($dh);
-
 
706
   closedir $dh;
-
 
707
   return \@files;
-
 
708
}
-
 
709
 
-
 
710
# clean all files from a directory, but not any subdirectories
-
 
711
sub cleanDirectory {
-
 
712
   my $dirname = shift;
-
 
713
   logMsg( "Cleaning up $dirname of all files" );
-
 
714
   my $files = getDirectoryList( $dirname );
-
 
715
   # clean up a directory
-
 
716
   foreach my $file (@$files) {
-
 
717
      unlink $file or warn "Could not unlink $file: #!\n";
-
 
718
   }
-
 
719
   return 1;
-
 
720
}
-
 
721
 
-
 
722
# handle fatal error by logging message and dying
-
 
723
# message - message to log, and also sent via email if applicable
-
 
724
# config - configuration hashref (optional)
-
 
725
# cleanupRoutine - code reference to cleanup routine (optional)
-
 
726
# if cleanupRoutine is provided, it will be called before dying passing it the config hashref
-
 
727
sub fatalError {
-
 
728
   my ( $message, $config, $cleanupRoutine ) = @_;
-
 
729
   logMsg( "FATAL ERROR: $message" );
-
 
730
   if ( defined $cleanupRoutine && ref $cleanupRoutine eq 'CODE' ) {
-
 
731
      logMsg( "Running cleanup routine before fatal error" );
-
 
732
      eval {
-
 
733
         $cleanupRoutine->( $config, $message );
-
 
734
         1;
-
 
735
      } or do {
-
 
736
         logMsg( "Cleanup routine failed: $@" );
-
 
737
      };
-
 
738
   }
-
 
739
   die;
-
 
740
}
-
 
741
 
-
 
742
 
654
1;
743
1;