Subversion Repositories zfs_utils

Rev

Rev 31 | Rev 34 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
24 rodolico 1
#! /usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
6
use FindBin;
7
use lib "$FindBin::Bin/..";
27 rodolico 8
use Data::Dumper;
9
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel mountGeli runCmd $logFileName $displayLogsOnConsole);
24 rodolico 10
 
33 rodolico 11
# if set, will not actually write files to disk
12
my $DEBUG = 0;
13
 
24 rodolico 14
# set the log file to be next to this script
15
$logFileName = "$FindBin::Bin/sneakernet.log";
31 rodolico 16
# log only for one run
17
unlink ( $logFileName ) if -f $logFileName;
18
 
24 rodolico 19
# display all log messages on console in addition to the log file
20
$displayLogsOnConsole = 1;
21
 
22
my $configFileName = "$0.conf.yaml";
23
 
24
my $config = {
25
   # file created on source server to track last copyed dataset
26
   'status_file' => "$0.status",
27
   #information about source server
28
   'source_server' => {
29
      'hostname' => '', # used to see if we are on source
30
      'poolname' => '', # name of the ZFS pool to export
31
   },
32
   #information about target server
33
   'target_server' => {
34
      'hostname' => '', # used to see if we are on target
35
      'poolname' => '', # name of the ZFS pool to import
36
      # if this is set, the dataset uses GELI, so we must decrypt and
37
      # mount it first
38
      'geli' => {
39
         'keydiskname' => 'replica', # the GPT label of the key disk
40
         'keyfile' => 'geli.key', # the name of the key file on keydiskname
41
         'localKey' => 'e98c660cccdae1226550484d62caa2b72f60632ae0c607528aba1ac9e7bfbc9c', # hex representation of the local key part
42
         'target' => '/media/geli.key', # location to create the combined keyfile
43
         'poolname' => '', # name of the ZFS pool to import
44
         'diskList' => [ 
45
            '/dev/gpt/sneakernet_disk' 
46
            ], # list of disks to try to mount the dataset from
47
      }
48
   },
27 rodolico 49
 
24 rodolico 50
   'transport' => {
51
      # this is the GPT label of the sneakernet disk
52
      'disk_label' => 'sneakernet',
53
      # where we want to mount it
54
      'mount_point' => '/mnt/sneakernet',
55
      # amount of time to wait for the disk to appear
56
      'timeout' => 600,
57
      # if set, all files will be encrypted with this key/IV during transport
58
      'encryption' => {
59
         'key'    => '', # openssl rand 32 | xxd -p | tr -d '\n' > test.key
60
         'IV'     => '00000000000000000000000000000000',
61
      },
62
   },
63
   'datasets' => {
64
      'iscsi' => {
65
         'source' => 'storage/backup/iscsi',
66
         'target' => 'storage/backup/iscsi',
67
         'filename' => 'iscsi'
68
      },
69
      'files_share'  => {
70
         'source' => 'storage/backup/files_share',
71
         'target' => 'storage/backup/files_share',
72
         'filename' => 'files_share'
73
      },
74
   }
75
};
76
 
30 rodolico 77
# read the status file and return as list
78
sub getStatusFile {
79
   my $filename = shift;
80
   # read in history/status file
81
   my @lines;
82
   if ( -e $filename && open my $fh, '<', $filename ) {
83
      chomp( @lines = <$fh> );
84
      close $fh;
85
      logMsg("Read status file '$filename' with contents:\n" . join( "\n", @lines ) . "\n");
86
   } else {
87
      logMsg("Error: could not read status file '$filename': $!");
88
      die;
89
   }
90
   return \@lines;
91
}
24 rodolico 92
 
30 rodolico 93
# write the status list to file
94
sub writeStatusFile {
95
   my ( $filename, $statusList ) = @_;
96
   # backup existing status file
97
   if ( -e $filename ) {
98
      rename( $filename, "$filename.bak" ) or do {
99
         logMsg("Error: could not backup existing status file '$filename': $!");
100
         die;
101
      };
102
   }
103
   # write new status file
104
   if ( open my $fh, '>', $filename ) {
105
      foreach my $line ( @$statusList ) {
106
         print $fh "$line\n";
107
      }
108
      close $fh;
109
      logMsg("Wrote status file '$filename' with contents:\n" . join( "\n", @$statusList ) . "\n");
110
   } else {
111
      logMsg("Error: could not write status file '$filename': $!");
112
      die;
113
   }
114
}
115
 
31 rodolico 116
# simple sub to take root/dataset/datset/dataset and turn it into
117
# dataset.dataset.dataset
118
sub replaceSlashWithDot {
119
   my $string = shift;
120
   my @parts = split( "/", $string );
121
   shift @parts;
122
   return join( '.', @parts );
123
}
124
 
30 rodolico 125
# perform replication on source server
126
# $config - configuration hashref
127
# $statusList - list of last snapshots replicated for each dataset in previous replications
128
# return new status list after replication containing updated last snapshots
129
# this script will actually replicate the datasets to the sneakernet disk
130
sub doSourceReplication {
131
   my ($config, $statusList) = @_;
132
   my $newStatus = [];
133
   foreach my $dataset ( sort keys %{$config->{datasets}} ) {
31 rodolico 134
      logMsg("Processing dataset '$dataset'");
135
      # get list of all snapshots on dataset
33 rodolico 136
      my $sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source} " ) ];
30 rodolico 137
      # process dataset here
138
      my $commands = makeReplicateCommands($sourceList, $statusList, $newStatus );
31 rodolico 139
      if ( %$commands ) {
140
         foreach my $cmd ( keys %$commands ) {
141
            my $command = $commands->{$cmd};
142
            $command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
143
            $command .= " > $config->{transport}->{mount_point}/" . replaceSlashWithDot($cmd);
144
            logMsg("Running command: $command");
33 rodolico 145
            runCmd(  $command  ) unless $DEBUG;
31 rodolico 146
         }
147
      } else {
148
         logMsg( "Nothing to do for $dataset" ); 
30 rodolico 149
      }
150
   }
151
   return $newStatus;
152
}
153
 
33 rodolico 154
# clean all files from a directory, but not any subdirectories
155
sub cleanDirectory {
156
   my $dirname = shift;
157
   logMsg( "Cleaning up $dirname of all files" );
158
   # clean up a directory
159
   opendir( my $dh, $dirname ) || fatalError( "Can not open $dirname: #!" );
160
   # get all file names, but leave directories alone
161
   my @files = map{ $dirname . "/$_" } grep { -f "$dirname/$_" } readdir($dh);
162
   closedir $dh;
163
   foreach my $file (@files) {
164
      unlink $file or warn "Could not unlink $file: #!\n";
165
   }
166
   return;
167
 }
168
 
169
 
170
 
31 rodolico 171
# how to handle a fatal error
172
sub fatalError {
173
   my $message = shift;
174
   logMsg( $message );
175
   die;
176
}
30 rodolico 177
 
31 rodolico 178
 
30 rodolico 179
##################### main program starts here #####################
180
# Example to create a random key for encryption/decryption:
24 rodolico 181
# generate a random key with
182
# openssl rand 32 | xxd -p | tr -d '\n' > test.key
183
 
184
# If a YAML config file exists next to the script, load and merge it
185
$config = loadConfig($configFileName, $config );
27 rodolico 186
 
25 rodolico 187
# set some defaults
188
$config->{'status_file'} = "$0.status" unless ( defined $config->{'status_file'} );
24 rodolico 189
 
25 rodolico 190
 
31 rodolico 191
fatalError( "Invalid config file: missing source and/or target server" )
24 rodolico 192
    unless (defined $config->{source_server} && defined $config->{target_server});
193
 
31 rodolico 194
# mount the transport drive, fatal error if we can not find it
195
fatalError( "Unable to mount tranport drive with label $config->{transport}->{disk_label}" )
196
   unless $config->{transport}->{mount_point} =  mountDriveByLabel( $config->{transport}->{disk_label}, $config->{transport}->{mount_point}, $config->{transport}->{timeout} );
197
 
24 rodolico 198
my $servername = `hostname -s`;
199
chomp $servername;
200
if ( $servername eq $config->{source_server}->{hostname} ) {
33 rodolico 201
    logMsg "Running as source server";
202
    # remove all files from transport disk, but leave all subdirectories alone
203
    cleanDirectory( $config->{transport}->{mount_point} );
30 rodolico 204
    my $statusList = getStatusFile($config->{status_file});
205
    $statusList = doSourceReplication($config, $statusList);
206
    writeStatusFile($config->{status_file}, $statusList);
24 rodolico 207
    # source server logic here
208
} elsif ( $servername eq $config->{target_server}->{hostname} ) {
33 rodolico 209
    logMsg "Running as target server";
210
    die "Target Server code not complete\n";
30 rodolico 211
    die "GELI target server logic not yet implemented\n" if ( defined $config->{target_server}->{geli} );
24 rodolico 212
    mountGeli( $config->{target_server}->{geli} ) if ( defined $config->{target_server}->{geli} );
213
} else {
25 rodolico 214
    logMsg "This server ($servername) is neither source nor target server as per config\n";
215
    die;
24 rodolico 216
}
217
 
31 rodolico 218
# unmount the sneakernet drive
219
`umount $config->{transport}->{mount_point}`;
220
# and remove the directory
33 rodolico 221
rmdir $config->{transport}->{mount_point};
31 rodolico 222
 
25 rodolico 223
1;
224
 
225
 
24 rodolico 226
#`cat $config->{input} | openssl enc -aes-256-cbc -K $config->{key} -iv $config->{IV} > $config->{output}`;
227
 
228
# this will decrypt $config->{output} to stdout
229
#`cat $config->{output} | openssl enc -aes-256-cbc -d -K $config->{key} -iv $config->{IV} > test.out`;