Subversion Repositories zfs_utils

Rev

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

Rev 46 Rev 48
Line 33... Line 33...
33
# Optionally uses symmetric encryption to encrypt datasets during transport.
33
# Optionally uses symmetric encryption to encrypt datasets during transport.
34
# On the target server, can optionally use GELI to encrypt the datasets on disk.
34
# On the target server, can optionally use GELI to encrypt the datasets on disk.
35
# Requires a configuration file in YAML format next to the script.
35
# Requires a configuration file in YAML format next to the script.
36
# Author: R. W. Rodlico <rodo@dailydata.net>
36
# Author: R. W. Rodlico <rodo@dailydata.net>
37
# Created: December 2025
37
# Created: December 2025
-
 
38
#
38
# Revision History:
39
# Revision History:
39
# Version: 0.1 2025-12-10 Initial version
40
# Version: 0.1 RWR 2025-12-10
-
 
41
# Development version
-
 
42
#
-
 
43
# Version: 1.0 RWR 2025-12-15
-
 
44
# Tested and ready for initial release
-
 
45
 
40
 
46
 
41
use strict;
47
use strict;
42
use warnings;
48
use warnings;
43
 
49
 
44
our $VERSION = '0.1';
50
our $VERSION = '1.0';
45
 
51
 
46
use File::Basename;
52
use File::Basename;
47
use FindBin;
53
use FindBin;
48
use lib "$FindBin::Bin/..";
54
use lib "$FindBin::Bin/..";
49
use Data::Dumper;
55
use Data::Dumper;
Line 144... Line 150...
144
         'dataset' => 'files_share',
150
         'dataset' => 'files_share',
145
      },
151
      },
146
   }
152
   }
147
};
153
};
148
 
154
 
149
# read the status file and return as list. If the file doesn't exits, returns an empty list
155
## Read the status file and return its lines as an ARRAYREF.
-
 
156
##
-
 
157
## Arguments:
-
 
158
##   $filename - path to the status file (string)
-
 
159
##
-
 
160
## Behavior:
-
 
161
##   - If the file exists and is readable, reads all lines, chomps newlines and returns an ARRAYREF
-
 
162
##     containing the lines (each line generally holds a fully qualified snapshot name).
-
 
163
##   - If the file does not exist or cannot be opened, logs a message and returns an empty ARRAYREF.
-
 
164
##
-
 
165
## Returns: ARRAYREF of lines (possibly empty).
150
sub getStatusFile {
166
sub getStatusFile {
151
   my $filename = shift;
167
   my $filename = shift;
152
   # read in history/status file
168
   # read in history/status file
153
   my @lines = ();
169
   my @lines = ();
154
   if ( -e $filename && open my $fh, '<', $filename ) {
170
   if ( -e $filename && open my $fh, '<', $filename ) {
Line 159... Line 175...
159
      logMsg("Error: could not read status file '$filename', assuming a fresh start: $!");
175
      logMsg("Error: could not read status file '$filename', assuming a fresh start: $!");
160
   }
176
   }
161
   return \@lines;
177
   return \@lines;
162
}
178
}
163
 
179
 
164
# write the status list to file
180
## Write the status list to disk safely.
-
 
181
##
-
 
182
## Arguments:
-
 
183
##   $filename   - path to the status file to write
-
 
184
##   $statusList - ARRAYREF of lines to write into the file
-
 
185
##
-
 
186
## Behavior:
-
 
187
##   - If an existing file is present, renames it to `$filename.bak` as a simple backup.
-
 
188
##   - Writes the provided lines to `$filename` (one per line).
-
 
189
##   - Logs the written contents. Dies on failure to backup or write the file.
165
sub writeStatusFile {
190
sub writeStatusFile {
166
   my ( $filename, $statusList ) = @_;
191
   my ( $filename, $statusList ) = @_;
167
   # backup existing status file
192
   # backup existing status file
168
   if ( -e $filename ) {
193
   if ( -e $filename ) {
169
      rename( $filename, "$filename.bak" ) or do {
194
      rename( $filename, "$filename.bak" ) or do {
Line 182... Line 207...
182
      logMsg("Error: could not write status file '$filename': $!");
207
      logMsg("Error: could not write status file '$filename': $!");
183
      die;
208
      die;
184
   }
209
   }
185
}
210
}
186
 
211
 
187
# simple sub to take root/dataset/datset/dataset and turn it into
212
## Convert a path-like dataset name into a filename-safe string.
-
 
213
##
-
 
214
## Examples:
-
 
215
##   'pool/fs/sub' => 'pool.fs.sub' (default)
-
 
216
##
-
 
217
## Arguments:
188
# dataset.dataset.dataset
218
##   $string       - input string to convert
-
 
219
##   $delimiter    - input delimiter to split on (default: '/')
-
 
220
##   $substitution - output separator to join with (default: '.')
-
 
221
##
-
 
222
## Returns: a joined string suitable for use as a filename.
189
sub dirnameToFileName {
223
sub dirnameToFileName {
190
   my ( $string, $delimiter, $substitution ) = @_;
224
   my ( $string, $delimiter, $substitution ) = @_;
191
   $delimiter //= '/';
225
   $delimiter //= '/';
192
   $substitution //= '.';
226
   $substitution //= '.';
193
   my @parts = split( /\Q$delimiter\E/, $string );
227
   my @parts = split( /\Q$delimiter\E/, $string );
194
   return join( $substitution, @parts );
228
   return join( $substitution, @parts );
195
}
229
}
196
 
230
 
197
# perform replication on source server
231
## Perform replication for all configured datasets on the source server.
-
 
232
##
198
# $config - configuration hashref
233
## Arguments:
-
 
234
##   $config     - configuration HASHREF (loaded from YAML). Must contain `datasets` and `transport` entries.
199
# $statusList - list of last snapshots replicated for each dataset in previous replications
235
##   $statusList - ARRAYREF of previously replicated snapshot full names (used to compute incremental sends)
-
 
236
##
-
 
237
## Behavior:
200
# return new status list after replication containing updated last snapshots
238
##   - Iterates over datasets defined in `$config->{datasets}`.
201
# this script will actually replicate the datasets to the sneakernet disk
239
##   - For each dataset, enumerates available snapshots on the source and calls
-
 
240
##     `makeReplicateCommands` to produce zfs send commands.
-
 
241
##   - Commands are optionally piped through `openssl enc` when `transport.encryption.key` is set.
-
 
242
##   - The output is written to files on the transport mount point (one file per dataset snapshot set).
-
 
243
##   - Respects `$config->{dryrun}`: no commands are executed when dryrun is enabled.
-
 
244
##
-
 
245
## Returns: ARRAYREF `$newStatus` containing updated status lines (latest snapshots per dataset).
202
sub doSourceReplication {
246
sub doSourceReplication {
203
   my ($config, $statusList) = @_;
247
   my ($config, $statusList) = @_;
204
   my $newStatus = [];
248
   my $newStatus = [];
205
   foreach my $dataset ( sort keys %{$config->{datasets}} ) {
249
   foreach my $dataset ( sort keys %{$config->{datasets}} ) {
206
      logMsg("Processing dataset '$dataset'");
250
      logMsg("Processing dataset '$dataset'");
207
      # get list of all snapshots on dataset
251
      # get list of all snapshots on dataset
208
      my $sourceList;
252
      my $sourceList;
209
#      print Dumper( $config ) . "\n";
-
 
210
#      print "$dataset\n";
-
 
211
#      print Dumper( $config->{datasets}->{$dataset} ) . "\n";
-
 
212
#      die;
-
 
213
      if ( -e "$scriptDirectory/test.status") {
253
      if ( -e "$scriptDirectory/test.status") {
214
         $sourceList = getStatusFile( "$scriptDirectory/test.status" );
254
         $sourceList = getStatusFile( "$scriptDirectory/test.status" );
215
      } else {
255
      } else {
216
         $sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source}" ) ];
256
         $sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source}" ) ];
217
      }
257
      }
Line 227... Line 267...
227
                     );
267
                     );
228
      if ( %$commands ) {
268
      if ( %$commands ) {
229
         foreach my $cmd ( keys %$commands ) {
269
         foreach my $cmd ( keys %$commands ) {
230
            my $command = $commands->{$cmd};
270
            my $command = $commands->{$cmd};
231
            my $outputFile = $cmd;
271
            my $outputFile = $cmd;
232
            $outputFile = replaceSlashWithDot($outputFile);
272
            my $outfile = dirnameToFileName( $cmd );
233
            $command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
273
            $command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
234
            $command .= " > $config->{transport}->{mount_point}/" . dirnameToFileName( $cmd );
274
            $command .= " > $config->{transport}->{mount_point}/$outfile";
235
            logMsg("Running command: $command");
275
            logMsg("Running command: $command");
236
            runCmd(  $command  ) unless $config->{dryrun};
276
            runCmd(  $command  ) unless $config->{dryrun};
237
         }
277
         }
238
      } else {
278
      } else {
239
         logMsg( "Nothing to do for $dataset" ); 
279
         logMsg( "Nothing to do for $dataset" ); 
240
      }
280
      }
241
   }
281
   }
242
   return $newStatus;
282
   return $newStatus;
243
}
283
}
244
 
284
 
245
# perform cleanup actions
285
## Perform cleanup and final reporting after replication.
-
 
286
##
-
 
287
## Arguments:
246
# $config - configuration hashref
288
##   $config  - configuration HASHREF (required)
247
# $message - optional message to include in the report
289
##   $message - OPTIONAL message to include in the report
248
#
290
##
-
 
291
## Behavior:
-
 
292
##   - Logs disk usage for the transport mount and zpool list for diagnostics.
-
 
293
##   - Ensures the report subject and message are populated, then attempts to unmount
-
 
294
##     the transport drive and send the report (via `sendReport`).
-
 
295
##   - If `shutdown_after_replication` is set in the running role's config, attempts
-
 
296
##     to shut down the machine (honors `$config->{dryrun}`).
249
sub cleanup{
297
sub cleanup{
250
   my ( $config, $message ) = @_;
298
   my ( $config, $message ) = @_;
251
   # add disk space utilization information on transport to the log
299
   # add disk space utilization information on transport to the log
252
   logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" );
300
   logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" );
253
   # add information about the server (zpools) to the log
301
   # add information about the server (zpools) to the log
Line 264... Line 312...
264
      logMsg( "Shutting down target server as per configuration" );
312
      logMsg( "Shutting down target server as per configuration" );
265
      runCmd( "shutdown -p now" ) unless $config->{dryrun};
313
      runCmd( "shutdown -p now" ) unless $config->{dryrun};
266
   }
314
   }
267
}
315
}
268
 
316
 
269
# update the target datasets from the files on the transport drive
317
## Update the target zfs datasets from files on the transport drive.
-
 
318
##
-
 
319
## Arguments:
-
 
320
##   $config - configuration HASHREF containing `transport` and `datasets` entries.
-
 
321
##
-
 
322
## Behavior:
-
 
323
##   - Reads all regular files from the transport mount point (via `getDirectoryList`).
-
 
324
##   - For each file, determines the intended target dataset based on the filename and
-
 
325
##     the `datasets` mapping in the config, optionally decrypts via `openssl enc -d`,
-
 
326
##     and pipes the stream into `zfs receive -F` to update the target dataset.
-
 
327
##   - Uses `runCmd` to execute the receive commands and logs the executed command string.
270
sub updateTarget {
328
sub updateTarget {
271
   my $config = shift;
329
   my $config = shift;
272
   my $files = getDirectoryList( $config->{transport}->{mount_point});
330
   my $files = getDirectoryList( $config->{transport}->{mount_point});
273
   foreach my $filename ( @$files ) {
331
   foreach my $filename ( @$files ) {
274
      my $targetDataset = basename( $filename );
332
      my $targetDataset = basename( $filename );