configure.ac: Don't let the LIBS setting leak out.
[distorted-backup] / snap.rfreezefs.in
1 #! @PERL@
2 ###
3 ### Synchronize snapshot with remotely mounted filesystem
4 ###
5 ### (c) 2011 Mark Wooding
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the distorted.org.uk backup suite.
11 ###
12 ### distorted-backup is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
16 ###
17 ### distorted-backup is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ### GNU General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License along
23 ### with distorted-backup; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 use Socket;
27
28 ###--------------------------------------------------------------------------
29 ### Utilities.
30
31 (our $QUIS = $0) =~ s:^.*/::;
32 sub whine ($) { my ($msg) = @_; print STDERR "$QUIS: $msg\n"; }
33 sub fail ($) { my ($msg) = @_; whine $msg; exit $! || ($? >> 8) || 255; }
34
35 our @CLEANUP = ();
36 sub cleanup (&) { my ($func) = @_; unshift @CLEAUP, $func; }
37 END { local $?; for my $func (@CLEANUP) { &$func } }
38
39 sub gripelist ($@) {
40 my ($gripe, @things) = @_;
41 fail "$gripe: " . join(", ", @things) if @things;
42 }
43
44 ###--------------------------------------------------------------------------
45 ### Parse command line.
46
47 our $USAGE = "usage: $QUIS DEVICE [KEY=VALUE ...]";
48 sub version { print "$QUIS, version 1.0.0\n"; }
49 sub help {
50 print <<EOF;
51 $USAGE
52
53 Option keys:
54 dir=MOUNTPT Mount point of filesystem on remote host [required].
55 host=[USER@]NAME Name or address of remote host [required].
56 op=OPERATION `snap' to create snapshot, or `unsnap' to remove.
57 rfreezefs=PATH Location of `rfreezefs' program on remote host.
58 ssh=PATH Location of remote-shell program on local host.
59 subtype=TYPE Type of snapshot to create [required].
60
61 Other option keys are passed to the underlying snapshot TYPE.
62 EOF
63 }
64 @ARGV >= 1 or do { print STDERR $USAGE, "\n"; exit 1; };
65 $ARGV[0] eq "-v" || $ARGV[0] eq "--version" and do { version; exit; };
66 $ARGV[0] eq "-h" || $ARGV[0] eq "--help" and do { version; help; exit; };
67
68 our $DEV = shift;
69 our %OPT = ( dir => undef,
70 host => undef,
71 op => "snap",
72 rfreezefs => "rfreezefs",
73 ssh => "ssh",
74 subtype => undef );
75 our @PASS = ();
76
77 for my $i (@ARGV) {
78 $i =~ /^([^\s=]+)=(.*)$/ or fail "malformed option `$i'";
79 my ($k, $v) = ($1, $2);
80 if ($k =~ /^([^.]+)\.(.+)$/) {
81 if ($2 eq "rfreezefs") { $k = $1; }
82 }
83 if (exists $OPT{$k}) { $OPT{$k} = $v; }
84 else { push @PASS, $i; }
85 }
86 gripelist "missing arguments", grep { !defined $OPT{$_} } keys %OPT;
87
88 (my $host = $OPT{host}) =~ s/^.*@//;
89 my $addr = inet_aton $host or fail "failed to resolve `$OPT{host}'";
90
91 ###--------------------------------------------------------------------------
92 ### Remove a snapshot if requested.
93
94 if ($OPT{op} eq "unsnap") {
95
96 ## This doesn't require negotiation with the remote end.
97 if ($OPT{unsnap}) {
98 exec "snap.$OPT{subtype}", $DEV, "op=unsnap", @PASS;
99 fail "exec snap.$OPT{subtype}: $!";
100 }
101
102 } elsif ($OPT{op} ne "snap") {
103 fail "unknown operation `$OPT{op}'";
104 }
105
106 ###--------------------------------------------------------------------------
107 ### Run `rfreezefs' on the remote host and collect information.
108
109 (my $dir = $OPT{dir}) =~ s/\'/'\\''/g;
110 open SSH, "-|", $OPT{ssh}, $OPT{host}, "$OPT{rfreezefs} -n '$dir'"
111 or fail "open(ssh): $!";
112 cleanup { close SSH };
113
114 our %INF = ( PORT => undef );
115 our %TOK = ();
116 our %RTOK = ();
117 our $PORT = undef;
118
119 while (<SSH>) {
120 my @f = split;
121 if ($f[0] eq "PORT") { $INF{$f[0]} = $f[1]; }
122 elsif ($f[1] eq "TOKEN") { $TOK{$f[1]} = $f[2]; $RTOK{$f[2]} = $f[1]; }
123 elsif ($f[0] eq "READY") { last; }
124 }
125
126 gripelist "missing information", grep { !defined $INF{$_} } keys %INF;
127 gripelist "missing tokens",
128 grep { !exists $TOK{$_} } "FREEZE", "FROZEN", "THAW", "THAWED";
129
130 ###--------------------------------------------------------------------------
131 ### Create the snapshot.
132
133 ## Connect to the socket.
134 socket SK, PF_INET, SOCK_STREAM, 0 or fail "socket: $!";
135 cleanup { close SK };
136 select SK; $| = 1;
137 connect SK, sockaddr_in($INF{PORT}, $addr) or fail "connect: $!";
138
139 ## Communication with the server.
140 sub rffscmd ($;$) {
141 my ($cmd, $rpl) = @_;
142 print SK $TOK{$cmd}, "\n" or fail "write <$cmd>: $!";
143 if ($rpl) {
144 chomp (my $line = <SK>);
145 if ($line ne $TOK{$rpl}) {
146 my $what = exists $RTOK{$line} ? "<$RTOK{$line}>" : "`$line'";
147 fail "unexpected response $what to <$cmd>";
148 }
149 }
150 }
151
152 ## Freeze the remote filesystem.
153 rffscmd(FREEZE, FROZEN);
154
155 ## Create the snapshot locally using the appropriate mechanism. This will
156 ## print the snapshot device name.
157 my $rc = system "snap.$OPT{subtype}", $DEV, @PASS;
158 $rc and fail "snap.$OPT{subtype} failed (rc = $rc)";
159
160 ## Discard the snapshot again if anything goes wrong.
161 cleanup {
162 if ($?) {
163 my $rc = system "snap.$OPT{subtype}", $DEV, "unsnap", @PASS;
164 $rc and
165 whine "snap.$OPT{subtype} failed to unsnap (rc = $rc) " .
166 "while recovering";
167 }
168 };
169
170 ## Thaw the remote filesystem.
171 rffscmd(THAW, THAWED);
172
173 ###----- That's all, folks --------------------------------------------------
174
175 exit 0;