| 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;
|