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