Commit | Line | Data |
---|---|---|
99248ed2 | 1 | #! @PERL@ |
99248ed2 MW |
2 | ### |
3 | ### Synchronize snapshot with remotely mounted filesystem | |
4 | ### | |
5 | ### (c) 2011 Mark Wooding | |
6 | ### | |
7 | ||
8 | ###----- Licensing notice --------------------------------------------------- | |
9 | ### | |
13678d88 MW |
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 | |
99248ed2 MW |
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 | ### | |
13678d88 | 17 | ### distorted-backup is distributed in the hope that it will be useful, |
99248ed2 MW |
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 | ### | |
13678d88 MW |
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, | |
99248ed2 MW |
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; |