Commit | Line | Data |
---|---|---|
99248ed2 | 1 | #! @PERL@ |
99248ed2 MW |
2 | ### |
3 | ### Run backups as instructed by a configuration file | |
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 Data::Dumper; | |
25 | use Errno qw(:POSIX); | |
26 | use Fcntl qw(:mode); | |
27 | use Getopt::Long qw(:config gnu_compat bundling no_ignore_case); | |
28 | use IO::Handle; | |
29 | use MIME::Base64; | |
30 | use POSIX; | |
31 | use Text::ParseWords; | |
32 | ||
33 | our $VERSION = "@VERSION@"; | |
34 | ||
35 | our %C = ( etc => "@sysconfdir@", | |
36 | sbin => "@sbindir@", | |
37 | libexec => "@pkglibexecdir@", | |
38 | bkp => "@bkplibexecdir@" ); | |
39 | ||
40 | ###-------------------------------------------------------------------------- | |
41 | ### Utilities. | |
42 | ||
43 | our $EVAL = 0; | |
44 | (our $QUIS = $0) =~ s:^.*/::; | |
45 | sub whine ($) { my ($msg) = @_; print STDERR "$QUIS: $msg\n"; } | |
46 | sub fail ($) { | |
47 | my ($msg) = @_; | |
48 | if ($EVAL) { die $msg . "\n"; } | |
49 | else { whine $msg; exit $! || ($? >> 8) || 255; } | |
50 | } | |
51 | ||
52 | sub try (&) { my ($body) = @_; local $EVAL = 1; &$body (); } | |
53 | ||
54 | sub decodewait ($) { | |
55 | my ($rc) = @_; | |
56 | ## Return a string describing the process exit status RC. | |
57 | ||
58 | if (!$rc) { return "ok"; } | |
59 | elsif ($rc & 255) { return sprintf "signal %d", $rc & 127; } | |
60 | else { return sprintf "rc = %d", $rc >> 8; } | |
61 | } | |
62 | ||
63 | sub shellquote ($) { | |
64 | my ($word) = @_; | |
65 | ## Quotify WORD so that a shell won't mangle it. | |
66 | ||
67 | $word =~ s/'/'\\''/g; | |
68 | return "'" . $word . "'"; | |
69 | } | |
70 | ||
71 | sub run ($@) { | |
72 | my ($what, @args) = @_; | |
73 | ## Run a program with ARGS, collecting and returning its output. | |
74 | ||
75 | open my $f, "-|", @args or fail "open pipe ($what): $!"; | |
76 | chomp (my @out = <$f>); | |
77 | if (!close $f) { | |
78 | $? and fail "$what failed: " . decodewait $?; | |
79 | fail "close pipe ($what)"; | |
80 | } | |
81 | return wantarray ? @out : $out[0]; | |
82 | } | |
83 | ||
84 | sub now () { | |
85 | ## Report the current time. | |
86 | ||
87 | return strftime "%Y-%m-%d %H:%M:%S %z", localtime; | |
88 | } | |
89 | ||
90 | ###-------------------------------------------------------------------------- | |
91 | ### Parse command line. | |
92 | ||
93 | our $USAGE = "usage: $QUIS [-n] [-a ASSET] [-c FILE] [KEY=VALUE ...]"; | |
94 | sub version { print "$QUIS, version $VERSION\n"; } | |
95 | sub help { | |
96 | print <<EOF; | |
97 | $USAGE | |
98 | ||
99 | Options: | |
100 | -h, --help Show this help text. | |
101 | -v, --version Show the program version number. | |
102 | -a, --asset=ASSET Back up ASSET, rather than all assets. | |
103 | -c, --config=FILE Use configuration FILE, not $CONF. | |
104 | -n, --noact Don't actually run the dump programs. | |
105 | EOF | |
106 | } | |
107 | ||
108 | our $CONF = "$C{etc}/bkptab"; | |
109 | our @ASSET = (); | |
110 | our $NOACT = 0; | |
111 | GetOptions('help|h|?' => sub { version; help; exit; }, | |
112 | 'version|v' => sub { version; exit; }, | |
113 | 'asset|a=s' => \@ASSET, | |
114 | 'config-file|c=s' => \$CONF, | |
115 | 'noact|n' => \$NOACT) | |
116 | or do { print STDERR $USAGE, "\n"; exit 1; }; | |
117 | ||
118 | ###-------------------------------------------------------------------------- | |
119 | ### Parse the configuration file. | |
120 | ||
121 | our %OVERRIDE = (); | |
122 | our %SECMAP = ( CONFIG => sub { | |
123 | my ($k, $v) = @_; | |
124 | $C{$k} = $v unless $OVERRIDE{$k}; | |
125 | } ); | |
126 | ||
127 | our %DUMP = (); | |
128 | our @ORDER = (); | |
129 | our %ASSET = map { $_ => 1 } @ASSET; | |
130 | ||
131 | ## Override configuration from the environment. | |
132 | while (my ($e, $v) = each %ENV) { | |
133 | next unless $e =~ /^BKP_([_A-Za-z0-9]+)$/; | |
134 | (my $k = $1) =~ tr/_A-Z/-a-z/; | |
135 | $C{$k} = $v; | |
136 | $OVERRIDE{$k} = 1; | |
137 | } | |
138 | ||
139 | ## Handy sub for extracting a configuration variable. | |
140 | sub config ($) { | |
141 | my ($k) = @_; | |
142 | exists $C{$k} or fail "$CONF:$.: unknown config variable `$k'"; | |
143 | return $C{$k}; | |
144 | } | |
145 | ||
146 | ## Parse the configuration file proper. | |
147 | open CF, "<", $CONF or fail "open config ($CONF): $!"; | |
148 | my $kvfunc = $SECMAP{CONFIG}; | |
149 | while (my $line = <CF>) { | |
150 | ||
151 | ## Handle continuation lines and comments. | |
152 | chomp $line; | |
153 | while ($line =~ /\\\s*$/) { | |
154 | chomp (my $more = <CF>); | |
155 | $line =~ s/\\\s*$/$more/; | |
156 | } | |
157 | next if $line =~ /^\s*([#;]|$)/; | |
158 | ||
159 | if ($line =~ /^\s*\[\s*(\S.*\S|\S|)\s*\]\s*/) { | |
160 | ## Section header: set the kvfunc appropriately. | |
161 | ||
162 | if (exists $SECMAP{$1}) { | |
163 | $kvfunc = $SECMAP{$1}; | |
164 | } elsif (!@ASSET || $ASSET{$1}) { | |
165 | my $asset = $1; | |
166 | if (!exists $DUMP{$asset}) { | |
167 | $DUMP{$asset} = []; | |
168 | push @ORDER, $asset; | |
169 | } | |
170 | $kvfunc = sub { | |
171 | my ($k, $v) = @_; | |
172 | push @{$DUMP{$asset}}, [$k, shellwords $v]; | |
173 | } | |
174 | } else { | |
175 | $kvfunc = sub { }; | |
176 | } | |
177 | } elsif ($line =~ /\s*(\S.*\S|\S)\s*[=:]\s*(\S.*\S|\S|)\s*$/) { | |
178 | ## Assignment line. Process this according to the current kvfunc. | |
179 | ||
180 | my ($k, $v) = ($1, $2); | |
181 | $v =~ s/\$\{([^}]+)\}/config $1/ge; | |
182 | $kvfunc->($1, $2); | |
183 | } else { | |
184 | ## Something else: it's an error. | |
185 | ||
186 | fail "$CONF:$.: unrecognized line"; | |
187 | } | |
188 | } | |
189 | ||
190 | ## Done. | |
191 | close CF or fail "close config ($CONF): $!"; | |
192 | @ORDER or fail "no matching assets to dump"; | |
193 | ||
194 | ## Export the configuration. | |
195 | while (my ($k, $v) = each %C) { | |
196 | next unless $k =~ /^[-A-Za-z0-9]+$/; | |
197 | (my $e = $k) = tr/-a-z/_A-Z/; | |
198 | $ENV{$k} = $v; | |
199 | } | |
200 | ||
201 | ###-------------------------------------------------------------------------- | |
202 | ### Establish a safe temporary directory. | |
203 | ||
204 | sysopen RAND, "/dev/urandom", O_RDONLY or fail "open (random): $!"; | |
205 | my $win = 0; | |
206 | our $TMPDIR; | |
207 | for (my $i = 0; $i < 1000; $i++) { | |
208 | my $n = sysread RAND, my $buf, 12; | |
209 | if (!defined $n) { fail "read (random): $!"; } | |
210 | elsif ($n < 12) { fail "short read (random)"; } | |
211 | my $rand = encode_base64 $buf, ""; | |
212 | $TMPDIR = ($ENV{TMPDIR} // "/tmp") . "/bkp.$$.$rand"; | |
213 | $win = 1, last if mkdir $TMPDIR, 0700; | |
214 | fail "mkdir (tmp): $!" unless $! == ENOENT; | |
215 | } | |
216 | $win or fail "failed to make temp directory"; | |
217 | $ENV{BKP_TMPDIR} = $TMPDIR; | |
218 | END { chdir "/"; system "rm", "-rf", $TMPDIR if defined $TMPDIR; } | |
219 | close RAND; | |
220 | ||
221 | chdir $TMPDIR or fail "chdir ($TMPDIR): $!"; | |
222 | ||
223 | ###-------------------------------------------------------------------------- | |
224 | ### Wade through the list of things to do, dumping assets. | |
225 | ||
226 | sub bkpadmin ($@) { | |
227 | my ($op, @args) = @_; | |
228 | ## Invoke an administration operation. | |
229 | ||
230 | return run "bkpadmin $op", | |
231 | "ssh", $C{host}, | |
232 | join " ", map { shellquote $_ } qw(userv root bkpadmin), $op, @args; | |
233 | } | |
234 | ||
235 | ## Make sure there's a volume mounted. | |
236 | bkpadmin "mount"; | |
237 | ||
238 | ## Go through each asset dumping all of the tags. | |
239 | for my $asset (@ORDER) { | |
240 | ||
241 | ## Start a log for this asset. | |
242 | if ($NOACT) { | |
243 | open LOG, ">&", STDERR or fail "dup stderr (log)"; | |
244 | } else { | |
245 | open LOG, ">", "$asset.log" or fail "open ($asset.log): $!"; | |
246 | } | |
247 | ||
248 | ## Find out when the last dump was done. | |
249 | my ($level, $date, $time, $tz) = split " ", bkpadmin "level", $asset; | |
250 | $ENV{BKP_LEVEL} = $level; | |
251 | $ENV{BKP_LASTDATE} = my $lastdate = "$date $time $tz"; | |
252 | $ENV{BKP_ASSET} = $asset; | |
253 | ||
254 | ## Prepare the dump. | |
255 | unless ($NOACT) { | |
256 | my $target = bkpadmin "prep", $asset, $level; | |
257 | $ENV{BKP_TARGET} = $target; | |
258 | } | |
259 | ||
260 | ## Make sure we can dispose of the results if there's a Perl failure | |
261 | ## somewhere here. | |
262 | try { | |
263 | ||
264 | ## Start writing the log. | |
265 | printf LOG "%s: Commence dump of asset `%s' at level %d (since %s)\n", | |
266 | now, $asset, $level, $lastdate; | |
267 | ||
268 | ## Dump the individual tags. | |
269 | my $lose = 0; | |
270 | for my $dump (@{$DUMP{$asset}}) { | |
271 | my ($tag, $type, @args) = @$dump; | |
272 | ||
273 | ## Make a log note. | |
274 | printf LOG "%s: Dump tag `%s' (%s) begins\n", now, $tag, $type; | |
275 | flush LOG or fail "write ($asset.log): $!"; | |
276 | ||
277 | ## Run the dump helper. | |
278 | if ($NOACT) { $? = 0; } | |
279 | else { | |
280 | defined (my $kid = fork) or fail "fork: $!"; | |
281 | unless ($kid) { | |
282 | open STDOUT, ">&", LOG and | |
283 | open STDERR, ">&", LOG or | |
284 | fail "dup: $!"; | |
285 | exec "$C{bkp}/bkp.$type", "$tag", @args; | |
286 | fail "exec (bkp.$type): $!"; | |
287 | } | |
288 | waitpid $kid, 0 or fail "waitpid: $!"; | |
289 | } | |
290 | ||
291 | ## Deal with the aftermath. | |
292 | if ($?) { | |
293 | printf LOG "%s: Dump tag `%s' failed (%s)\n", now, $tag, | |
294 | decodewait $?; | |
295 | printf STDERR "%s: %s: Dump asset `%s' tag `%s' FAILED\n", | |
296 | $QUIS, now, $asset, $tag; | |
297 | $lose++; | |
298 | } elsif ($NOACT) { | |
299 | printf LOG "%s: Dump tag `%s' not performed (--noact)\n", now, $tag; | |
300 | } else { | |
301 | printf LOG "%s: Dump tag `%s' ok\n", now, $tag; | |
302 | } | |
303 | } | |
304 | ||
305 | ## Report completion of the asset. | |
306 | printf LOG "%s: Dump of asset `%s' completed %s\n", now, $asset, | |
307 | $lose == 0 ? "successfully" : "with $lose failures"; | |
308 | error LOG and fail "write ($asset.log): $!"; | |
309 | close LOG or fail "close ($asset.log): $!"; | |
310 | ||
311 | ## Copy the log to the server and commit it. | |
312 | unless ($NOACT) { | |
313 | run "scp $asset.log", | |
314 | "scp", "$asset.log", "$C{host}:$target/$asset.log"; | |
315 | bkpadmin $lose ? "fail" : "commit", $asset; | |
316 | } | |
317 | }; | |
318 | ||
319 | ## If anything failed above, then try to mark the asset as a failure and | |
320 | ## abort. | |
321 | if ($@) { | |
322 | try { bkpadmin "fail", $asset; }; | |
323 | fail $@; | |
324 | } | |
325 | } | |
326 | ||
327 | ###----- That's all, folks -------------------------------------------------- |