| 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);
|
55 |
our @EXPORT_OK = qw(loadConfig shredFile mountDriveByLabel mountGeli logMsg runCmd makeReplicateCommands sendReport $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
|
| 61 |
our $merge_stderr = 0; # if set to 1, stderr is captured in runCmd
|
61 |
our $merge_stderr = 0; # if set to 1, stderr is captured in runCmd
|
| - |
|
62 |
our $lastRunError = 0; # tracks the last error code from runCmd
|
| 62 |
|
63 |
|
| 63 |
# Execute a command and return its output.
|
64 |
# Execute a command and return its output.
|
| 64 |
# If called in scalar context, returns the full output as a single string.
|
65 |
# If called in scalar context, returns the full output as a single string.
|
| 65 |
# If called in list context, returns the output split into lines.
|
66 |
# If called in list context, returns the output split into lines.
|
| 66 |
# If $merge_stderr is true (default), stderr is merged into stdout (only for scalar commands).
|
67 |
# If $merge_stderr is true (default), stderr is merged into stdout (only for scalar commands).
|
| Line 71... |
Line 72... |
| 71 |
my $output = '';
|
72 |
my $output = '';
|
| 72 |
|
73 |
|
| 73 |
logMsg( "Running command [$cmd]" );
|
74 |
logMsg( "Running command [$cmd]" );
|
| 74 |
$cmd .= ' 2>&1' if $merge_stderr;
|
75 |
$cmd .= ' 2>&1' if $merge_stderr;
|
| 75 |
$output = `$cmd`;
|
76 |
$output = `$cmd`;
|
| - |
|
77 |
$lastRunError = $?;
|
| - |
|
78 |
if ( $lastRunError ) {
|
| 76 |
if ($? == -1) {
|
79 |
if ($? == -1) {
|
| 77 |
logMsg( "failed to execute: $!");
|
80 |
logMsg( "failed to execute: $!");
|
| 78 |
return ''
|
81 |
return '';
|
| 79 |
}
|
- |
|
| 80 |
elsif ($? & 127) { # fatal error, exit program
|
82 |
} elsif ($? & 127) { # fatal error, exit program
|
| 81 |
logMsg( sprintf( "child died with signal %d, %s coredump\n", ($? & 127), ($? & 128) ? 'with' : 'without' ) );
|
83 |
logMsg( sprintf( "child died with signal %d, %s coredump\n", ($? & 127), ($? & 128) ? 'with' : 'without' ) );
|
| 82 |
die;
|
84 |
die;
|
| 83 |
}
|
- |
|
| 84 |
else {
|
85 |
} elsif ($? >> 8) { # it had some return code other than 0
|
| 85 |
logMsg( sprintf( "child exited with value %d\n", $? >> 8 ) );
|
86 |
logMsg( sprintf( "child exited with value %d\n", $? >> 8 ) );
|
| - |
|
87 |
}
|
| 86 |
}
|
88 |
}
|
| 87 |
$output //= '';
|
89 |
$output //= '';
|
| 88 |
|
90 |
|
| 89 |
if (wantarray) {
|
91 |
if (wantarray) {
|
| 90 |
return $output eq '' ? () : split(/\n/, $output);
|
92 |
return $output eq '' ? () : split(/\n/, $output);
|
| Line 139... |
Line 141... |
| 139 |
# default to /mnt/label if not provided
|
141 |
# default to /mnt/label if not provided
|
| 140 |
$driveInfo->{mountPath} //= "/mnt/$driveInfo->{label}"; # this is where we'll mount it if we find it
|
142 |
$driveInfo->{mountPath} //= "/mnt/$driveInfo->{label}"; # this is where we'll mount it if we find it
|
| 141 |
$driveInfo->{filesystem} //= 'ufs'; # default to mounting ufs
|
143 |
$driveInfo->{filesystem} //= 'ufs'; # default to mounting ufs
|
| 142 |
# The location for the label depends on filesystem. Only providing access to ufs and msdos here for safety.
|
144 |
# The location for the label depends on filesystem. Only providing access to ufs and msdos here for safety.
|
| 143 |
# gpt labeled drives for ufs are in /dev/gpt/, for msdosfs in /dev/msdosfs/
|
145 |
# gpt labeled drives for ufs are in /dev/gpt/, for msdosfs in /dev/msdosfs/
|
| 144 |
$driveInfo->{mountPath} = $driveInfo->{filesystem} eq 'msdos' ? "/dev/msdosfs/$driveInfo->{label}" : "/dev/gpt/$driveInfo->{label}";
|
146 |
$driveInfo->{label} = $driveInfo->{filesystem} eq 'msdos' ? "/dev/msdosfs/$driveInfo->{label}" : "/dev/gpt/$driveInfo->{label}";
|
| 145 |
# drive already mounted, just return the path
|
147 |
# drive already mounted, just return the path
|
| 146 |
return $driveInfo->{mountPath} if ( runCmd( "mount | grep '$driveInfo->{mountPath}'" ) );
|
148 |
my $output = runCmd( "mount | grep '$driveInfo->{mountPath}'" );
|
| - |
|
149 |
return $driveInfo->{mountPath} if ( $lastRunError == 0 ); # grep found it for us
|
| 147 |
# default to 10 minutes (600 seconds) if not provided
|
150 |
# default to 10 minutes (600 seconds) if not provided
|
| 148 |
$driveInfo->{timeout} //= 600;
|
151 |
$driveInfo->{timeout} //= 600;
|
| 149 |
# default to checking every minute if not provided
|
152 |
# default to checking every minute if not provided
|
| 150 |
$driveInfo->{check_interval} //= 15;
|
153 |
$driveInfo->{check_interval} //= 15;
|
| 151 |
# wait up to $timeout seconds for device to appear, checking every 10 seconds
|
154 |
# wait up to $timeout seconds for device to appear, checking every 10 seconds
|
| 152 |
while ( $driveInfo->{timeout} > 0 ) {
|
155 |
while ( $driveInfo->{timeout} > 0 ) {
|
| 153 |
if ( -e "$driveInfo->{label}" ) {
|
156 |
if ( -e "$driveInfo->{label}" ) {
|
| 154 |
last;
|
157 |
last;
|
| 155 |
} else {
|
158 |
} else {
|
| - |
|
159 |
print "Waiting for drive labeled $driveInfo->{label}\n";
|
| 156 |
sleep $driveInfo->{check_interval};
|
160 |
sleep $driveInfo->{check_interval};
|
| 157 |
$driveInfo->{timeout} -= $driveInfo->{check_interval};
|
161 |
$driveInfo->{timeout} -= $driveInfo->{check_interval};
|
| 158 |
print "Waiting for drive labeled $driveInfo->{label}\n";
|
- |
|
| 159 |
}
|
162 |
}
|
| 160 |
}
|
163 |
}
|
| 161 |
# if we found it, mount and return mount path
|
164 |
# if we found it, mount and return mount path
|
| 162 |
if ( -e "$driveInfo->{label}" ) {
|
165 |
if ( -e "$driveInfo->{label}" ) {
|
| 163 |
# ensure mount point
|
166 |
# ensure mount point
|
| 164 |
unless ( -d $driveInfo->{mountPath} || make_path($driveInfo->{mountPath}) ) {
|
167 |
unless ( -d $driveInfo->{mountPath} || make_path($driveInfo->{mountPath}) ) {
|
| 165 |
logMsg("Failed to create $driveInfo->{mountPath}: $!");
|
168 |
logMsg("Failed to create $driveInfo->{mountPath}: $!");
|
| 166 |
return '';
|
169 |
return '';
|
| 167 |
}
|
170 |
}
|
| 168 |
# mount device (let mount detect filesystem)
|
171 |
# mount device
|
| 169 |
unless ( runCmd( "mount -t $driveInfo->{filesystem} $driveInfo->{label} $driveInfo->{mountPath}" ) ) {
|
172 |
runCmd( "mount -t $driveInfo->{filesystem} $driveInfo->{label} $driveInfo->{mountPath}" );
|
| - |
|
173 |
if ( $lastRunError ) {
|
| 170 |
logMsg("Failed to mount $driveInfo->{label} on $driveInfo->{mountPath}: $!");
|
174 |
logMsg("Failed to mount $driveInfo->{label} on $driveInfo->{mountPath}: $!");
|
| 171 |
return '';
|
175 |
return '';
|
| 172 |
}
|
176 |
}
|
| 173 |
return $driveInfo->{mountPath};
|
177 |
return $driveInfo->{mountPath};
|
| 174 |
} else {
|
178 |
} else {
|
| Line 495... |
Line 499... |
| 495 |
my %from_names = map { $_ => 1 } grep { defined $_ } @from_values;
|
499 |
my %from_names = map { $_ => 1 } grep { defined $_ } @from_values;
|
| 496 |
my $single_from_name = (keys %from_names == 1) ? (keys %from_names)[0] : undef;
|
500 |
my $single_from_name = (keys %from_names == 1) ? (keys %from_names)[0] : undef;
|
| 497 |
|
501 |
|
| 498 |
if ($any_from_missing) {
|
502 |
if ($any_from_missing) {
|
| 499 |
# full recursive send from root
|
503 |
# full recursive send from root
|
| 500 |
$commands{'root_fs'} = sprintf('zfs send -R %s@%s', $root_fs, $single_to_name);
|
504 |
$commands{$root_fs} = sprintf('zfs send -R %s@%s', $root_fs, $single_to_name);
|
| 501 |
}
|
505 |
}
|
| 502 |
elsif ($single_from_name) {
|
506 |
elsif ($single_from_name) {
|
| 503 |
# incremental recursive send, but don't do it if they are the same
|
507 |
# incremental recursive send, but don't do it if they are the same
|
| 504 |
$commands{$root_fs} = sprintf('zfs send -R -I %s@%s %s@%s',
|
508 |
$commands{$root_fs} = sprintf('zfs send -R -I %s@%s %s@%s',
|
| 505 |
$root_fs, $single_from_name, $root_fs, $single_to_name)
|
509 |
$root_fs, $single_from_name, $root_fs, $single_to_name)
|
| Line 551... |
Line 555... |
| 551 |
# $subject is the email subject
|
555 |
# $subject is the email subject
|
| 552 |
# $logFile is the path to the log file to send/copy
|
556 |
# $logFile is the path to the log file to send/copy
|
| 553 |
sub sendReport {
|
557 |
sub sendReport {
|
| 554 |
my ( $reportConfig, $subject, $logFile ) = @_;
|
558 |
my ( $reportConfig, $subject, $logFile ) = @_;
|
| 555 |
return unless defined $reportConfig;
|
559 |
return unless defined $reportConfig;
|
| - |
|
560 |
logMsg( "Beginning sendReport" );
|
| - |
|
561 |
# if they have set an e-mail address, try to e-mail the report
|
| 556 |
if ( defined $reportConfig->{email} && $reportConfig->{email} ne '' ) {
|
562 |
if ( defined $reportConfig->{email} && $reportConfig->{email} ne '' ) {
|
| - |
|
563 |
logMsg( "Sending report via e-mail to $reportConfig->{email}");
|
| 557 |
sendEmailReport( $reportConfig->{email}, $subject, $logFile );
|
564 |
sendEmailReport( $reportConfig->{email}, $subject, $logFile );
|
| 558 |
}
|
565 |
}
|
| - |
|
566 |
# if targetDrive defined and there is a valid label for it, try to mount it and write the report there
|
| - |
|
567 |
if ( defined $reportConfig->{targetDrive} && defined $reportConfig->{targetDrive}->{label} && $reportConfig->{targetDrive}->{label} ) {
|
| 559 |
if ( defined $reportConfig->{targetDrive} ) {
|
568 |
logMsg( "Saving report to disk with label $reportConfig->{targetDrive}->{label}" );
|
| 560 |
my $mountPoint = mountDriveByLabel( $reportConfig->{targetDrive}->{label}, $reportConfig->{targetDrive}->{mount_point}, 300 );
|
569 |
my $mountPoint = mountDriveByLabel( $reportConfig->{targetDrive}->{label}, $reportConfig->{targetDrive}->{mount_point}, 300 );
|
| 561 |
if ( defined $mountPoint ) {
|
570 |
if ( defined $mountPoint && $mountPoint ) {
|
| 562 |
copyReportToDrive( $logFile, $mountPoint );
|
571 |
copyReportToDrive( $logFile, $mountPoint );
|
| 563 |
`umount $mountPoint`;
|
572 |
`umount $mountPoint`;
|
| 564 |
rmdir $mountPoint;
|
573 |
rmdir $mountPoint;
|
| 565 |
} else {
|
574 |
} else {
|
| 566 |
logMsg( "Warning: could not mount report target drive with label '$reportConfig->{targetDrive}->{label}'" );
|
575 |
logMsg( "Warning: could not mount report target drive with label '$reportConfig->{targetDrive}->{label}'" );
|
| Line 590... |
Line 599... |
| 590 |
# $logFile is the path to the log file to send.
|
599 |
# $logFile is the path to the log file to send.
|
| 591 |
# Does nothing if any parameter is invalid.
|
600 |
# Does nothing if any parameter is invalid.
|
| 592 |
sub sendEmailReport {
|
601 |
sub sendEmailReport {
|
| 593 |
my ( $to, $subject, $logFile ) = @_;
|
602 |
my ( $to, $subject, $logFile ) = @_;
|
| 594 |
return unless defined $to && $to ne '';
|
603 |
return unless defined $to && $to ne '';
|
| 595 |
return unless defined $subject && $subject ne '';
|
604 |
$subject //= 'Sneakernet Replication Report from ' . `hostname`;
|
| 596 |
return unless defined $logFile && -e $logFile;
|
605 |
$logFile //= '';
|
| 597 |
|
606 |
|
| 598 |
logMsg( "Sending email report to $to with subject '$subject'" );
|
607 |
logMsg( "Sending email report to $to with subject '$subject'" );
|
| 599 |
open my $mailfh, '|-', '/usr/sbin/sendmail -t' or do {
|
608 |
open my $mailfh, '|-', '/usr/sbin/sendmail -t' or do {
|
| 600 |
logMsg( "Could not open sendmail: $!" );
|
609 |
logMsg( "Could not open sendmail: $!" );
|
| 601 |
return;
|
610 |
return;
|
| Line 603... |
Line 612... |
| 603 |
print $mailfh "To: $to\n";
|
612 |
print $mailfh "To: $to\n";
|
| 604 |
print $mailfh "Subject: $subject\n";
|
613 |
print $mailfh "Subject: $subject\n";
|
| 605 |
print $mailfh "MIME-Version: 1.0\n";
|
614 |
print $mailfh "MIME-Version: 1.0\n";
|
| 606 |
print $mailfh "Content-Type: text/plain; charset=\"utf-8\"\n";
|
615 |
print $mailfh "Content-Type: text/plain; charset=\"utf-8\"\n";
|
| 607 |
print $mailfh "\n"; # end of headers
|
616 |
print $mailfh "\n"; # end of headers
|
| - |
|
617 |
|
| - |
|
618 |
print $mailfh "Following is the report for replication\n\n";
|
| 608 |
|
619 |
|
| 609 |
open my $logfh, '<', $logFile or do {
|
620 |
if ( -e $logFile && open my $logfh, '<', $logFile ) {
|
| 610 |
logMsg( "Could not open log file $logFile for reading: $!" );
|
621 |
while ( my $line = <$logfh> ) {
|
| - |
|
622 |
print $mailfh $line;
|
| - |
|
623 |
}
|
| 611 |
close $mailfh;
|
624 |
close $logfh;
|
| 612 |
return;
|
625 |
} else {
|
| - |
|
626 |
logMsg( "Could not open log file [$logFile] for reading: $!" );
|
| 613 |
};
|
627 |
};
|
| 614 |
while ( my $line = <$logfh> ) {
|
- |
|
| 615 |
print $mailfh $line;
|
- |
|
| 616 |
}
|
628 |
|
| 617 |
close $logfh;
|
- |
|
| 618 |
close $mailfh;
|
629 |
close $mailfh;
|
| 619 |
}
|
630 |
}
|
| 620 |
|
631 |
|
| 621 |
1;
|
632 |
1;
|