#!/usr/bin/env perl # Copyright (c) 2026, rodolico # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # updateConfigKeys - Sample script for updating configuration keys during sneakernet cleanup # # This script demonstrates how to programmatically update configuration keys in a YAML # configuration file during the cleanup phase of the sneakernet process. It's particularly # useful for updating sensitive values like encryption keys, hostnames, email addresses, # and other configuration parameters that may need to be changed after a sneakernet # transfer is complete. # # The script uses dot notation to reference nested configuration keys (e.g., # 'target.geli.poolname' or 'datasets.dataset1.target') and creates a timestamped # backup before making any changes. # # FEATURES: # - Update existing keys with new values # - Delete keys by using 'DELETE' as the value # - Create new nested keys if they don't exist # - Automatic timestamped backup before modifications # # This is provided as a sample implementation that can be customized for specific # cleanup requirements in sneakernet workflows. # # USAGE: # Edit the @updates array in the script to specify the configuration keys you want # to update or delete: # - To update: 'key.path=newvalue' # - To delete: 'key.path=DELETE' # # Then run the script to apply the changes to the configuration file. # # REQUIREMENTS: # - Perl with YAML::Tiny module installed. This must be installed on the system where # the script is executed. (target system for sneakernet) # use strict; use warnings; use YAML::Tiny; use Data::Dumper; use File::Copy qw(copy); use POSIX qw(strftime); # YAML::Tiny wrapper functions my $LoadFile = sub { my $yaml = YAML::Tiny->read($_[0]); return $yaml->[0]; }; my $DumpFile = sub { my ($file, $data) = @_; my $yaml = YAML::Tiny->new($data); $yaml->write($file); }; # Configuration file path (set this variable) my $configFile = 'sneakernet.conf.yaml'; # Result array to collect output messages my @result; my @errors; my $caller = caller(); # Check if called from another script and, if not, prints updates to STDOUT # if we are being called from sneakernet, get verbosity level, otherwise just set to 5 my $verbosityLevel = ($caller && defined $ZFS_Utils::verboseLoggingLevel) ? $ZFS_Utils::verboseLoggingLevel : 5; # Array of key-value pairs in dot notation # To update a key: 'key.path=newvalue' # To delete a key: 'key.path=DELETE' my @updates = ( 'debug=1', 'dryrun=0', 'source.hostname=production', 'source.report.email=admin@example.com', 'target.geli.poolname=newbackup', 'datasets.dataset1.target=newbackup', # 'datasets.oldDataset=DELETE', # Example: delete a key ); # Create backup of the original file with timestamp my $timestamp = strftime("%Y%m%d_%H%M%S", localtime); my $backupFile = "$configFile.bak.$timestamp"; if (!copy($configFile, $backupFile)) { push @errors, "Failed to create backup: $!\n"; goto RETURN; } push @result, "Created backup: $backupFile\n"; # Load the YAML configuration file my $config = $LoadFile->($configFile); if ( $verbosityLevel ) { push @result, "Original configuration:\n"; push @result, Dumper($config); push @result, "\n" . "=" x 60 . "\n\n"; } # Process each update foreach my $update (@updates) { # Split the update into key path and value my ($keyPath, $value) = split(/=/, $update, 2); unless (defined $value) { push @errors, "Warning: Invalid format for update '$update'. Skipping.\n"; next; } # Split the key path by dots my @keys = split(/\./, $keyPath); # Check if this is a deletion operation if ($value eq 'DELETE') { # Navigate to the parent hash my $current = $config; my $found = 1; for (my $i = 0; $i < @keys - 1; $i++) { my $key = $keys[$i]; if (exists $current->{$key} && ref($current->{$key}) eq 'HASH') { $current = $current->{$key}; } else { $found = 0; last; } } my $finalKey = $keys[-1]; if ($found && exists $current->{$finalKey}) { my $oldValue = $current->{$finalKey}; delete $current->{$finalKey}; push @result, "Deleted: $keyPath\n"; push @result, " Old value: $oldValue\n\n"; } else { push @errors, "Delete skipped (key not found): $keyPath\n\n"; } } else { # Navigate/create the nested hash structure my $current = $config; for (my $i = 0; $i < @keys - 1; $i++) { my $key = $keys[$i]; # Create the nested hash if it doesn't exist $current->{$key} = {} unless exists $current->{$key}; $current = $current->{$key}; } # Set the final value my $finalKey = $keys[-1]; my $oldValue = exists $current->{$finalKey} ? $current->{$finalKey} : 'undef'; $current->{$finalKey} = $value; push @result, "Updated: $keyPath\n"; push @result, " Old value: $oldValue\n"; push @result, " New value: $value\n\n"; } } if ( $verbosityLevel > 2 ) { push @result, "=" x 60 . "\n\n"; push @result, "Updated configuration:\n"; push @result, Dumper($config); } # Save the updated configuration back to the file $DumpFile->($configFile, $config); push @result, "\nChanges saved to $configFile\n"; push @result, "Backup saved as $backupFile\n"; RETURN: chomp @result; chomp @errors; # When run standalone, print to STDOUT if ($caller) { return ( join( "\n", @result ) . "\n", @errors ? join("\n", @errors) : "" ); } else { print "Results Summary:\n"; print join( "\n", @result ) . "\n"; if (@errors) { print "\n=== CLEANUP SCRIPT ERRORS ===\n"; print join("\n", @errors) . "\n"; } exit(@errors > 0 ? 1 : 0); } # End of script