1 #! /usr/bin/perl |
|
2 |
|
3 use 5.010; |
|
4 use strict; |
|
5 use warnings; |
|
6 |
|
7 use File::Basename; |
|
8 use Net::FTP; |
|
9 use Perl6::Slurp; |
|
10 use Getopt::Long; |
|
11 use Sys::Hostname; |
|
12 use Pod::Usage; |
|
13 use POSIX qw(strftime); |
|
14 use Date::Parse qw(str2time); |
|
15 use Cwd qw(realpath); |
|
16 use English qw(-no_match_vars); |
|
17 use if $ENV{DEBUG} => qw(Smart::Comments); |
|
18 |
|
19 $ENV{LC_ALL} = "C"; |
|
20 |
|
21 my $ME = basename $0; |
|
22 my $VERSION = '<VERSION>'; |
|
23 |
|
24 my @CONFIGS = ("/etc/$ME.conf", "$ENV{HOME}/.$ME.conf", "$ME.conf"); |
|
25 |
|
26 my $HOSTNAME = hostname; |
|
27 my $NOW = time(); |
|
28 |
|
29 my $opt_level = undef; |
|
30 my $opt_today = strftime("%F", localtime $NOW); |
|
31 my @opt_debug = (); |
|
32 my $opt_verbose = 0; |
|
33 my $opt_dry = 0; |
|
34 my $opt_force = 0; |
|
35 my $opt_label = "daily"; |
|
36 my $opt_info = 0; |
|
37 my $opt_config = ""; |
|
38 my $opt_clean = 1; |
|
39 my $opt_dumpdates = "/var/lib/dumpdates"; |
|
40 |
|
41 sub get_configs(@); |
|
42 sub get_candidates(); |
|
43 sub verbose(@); |
|
44 sub update_devnames($$$); |
|
45 sub get_history(@); |
|
46 sub calculate_level($@); |
|
47 sub real_device($); |
|
48 sub get_estimate($$); |
|
49 sub devno($); |
|
50 sub unlink_old_dumps($$); |
|
51 |
|
52 our @AT_EXIT; |
|
53 END { $_->() foreach @AT_EXIT } |
|
54 $SIG{INT} = sub { warn "Got signal INT\n"; exit 1 }; |
|
55 |
|
56 my %CONFIG = ( |
|
57 FTP_DIR => "backup/<LABEL>/<HOSTNAME>", |
|
58 FTP_PASSIVE => 1, |
|
59 COMPRESSION_LEVEL => 6, |
|
60 FULL_CYCLE => 7, |
|
61 KEEP => 2, |
|
62 ); |
|
63 |
|
64 |
|
65 MAIN: { |
|
66 |
|
67 Getopt::Long::Configure("bundling"); |
|
68 GetOptions( |
|
69 "l|level=i" => \$opt_level, |
|
70 "L|label=s" => \$opt_label, |
|
71 "d|debug:s" => sub { push @opt_debug, split /,/, $_[1] }, |
|
72 "v|verbose" => \$opt_verbose, |
|
73 "i|info" => \$opt_info, |
|
74 "dry" => sub { $opt_dry = 1; $opt_verbose = 1 }, |
|
75 #"f|force" => \$opt_force, |
|
76 "h|help" => sub { pod2usage(-exit => 0, -verbose => 1) }, |
|
77 "m|man" => sub { pod2usage(-exit => 0, -verbose => 3) }, |
|
78 "C|config=s" => sub { @CONFIGS = ($_[1]) }, |
|
79 "V|version" => sub { print "$ME: $VERSION\n"; exit 0 }, |
|
80 "c|clean!" => \$opt_clean, |
|
81 "D|dumpdates=s" => \$opt_dumpdates, |
|
82 ) or pod2usage; |
|
83 |
|
84 my %cf = (%CONFIG, get_configs(@CONFIGS)); |
|
85 $cf{FTP_DIR} =~ s/<HOSTNAME>/$HOSTNAME/g; |
|
86 $cf{FTP_DIR} =~ s/<LABEL>/$opt_label/g; |
|
87 |
|
88 # get the backup candiates -> all file systems from /etc/fstab |
|
89 # with a dump frequence > 0 |
|
90 my @devs = get_candidates(); |
|
91 |
|
92 ### %cf |
|
93 ### @devs |
|
94 |
|
95 |
|
96 verbose +(map { "candidate: $_->{dev} as $_->{rdev}\n" } @devs), "\n"; |
|
97 |
|
98 my @errors = (); |
|
99 push @errors, "Need FTP_HOST (see config)." if not defined $cf{FTP_HOST}; |
|
100 push @errors, "Need KEY (see config)." if not defined $cf{KEY}; |
|
101 push @errors, "Command `dump' not found." if system("command -v dump >/dev/null"); |
|
102 die "$ME: pre-flight check failed:\n\t", |
|
103 join("\n\t" => @errors), "\n" if @errors; |
|
104 |
|
105 my $ftp; |
|
106 |
|
107 if (not "output" ~~ \@opt_debug) { |
|
108 $ftp = new FTP( |
|
109 $cf{FTP_HOST}, |
|
110 Passive => $cf{FTP_PASSIVE}, |
|
111 Debug => "ftp" ~~ \@opt_debug, |
|
112 ) or die $@; |
|
113 $ftp->login or die $ftp->message; |
|
114 $ftp->home($ftp->try(pwd => ())); |
|
115 $ftp->try(binary => ()); |
|
116 $ftp->try(mkpath => $cf{FTP_DIR}); |
|
117 $ftp->try(cwd => $cf{FTP_DIR}); |
|
118 } |
|
119 |
|
120 # get_history the situation - we rely on $opt_dumpdates |
|
121 @devs = get_history(@devs); |
|
122 @devs = calculate_level($cf{FULL_CYCLE}, @devs); |
|
123 |
|
124 ### @devs |
|
125 |
|
126 if ($opt_info) { |
|
127 my $lr = (reverse sort { $a <=> $b } map { length $_->{rdev} } @devs)[0]; |
|
128 my $ld = (reverse sort { $a <=> $b } map { length $_->{dev} } @devs)[0]; |
|
129 my $ln = (reverse sort { $a <=> $b } map { length $_->{devno} } @devs)[0]; |
|
130 |
|
131 my %l; |
|
132 foreach my $dev (@devs) { |
|
133 $l{$dev} = sprintf "%*s (%*s %*s)", -$ld => $dev->{dev}, |
|
134 -$lr => $dev->{rdev}, |
|
135 -$ln => $dev->{devno}; |
|
136 } |
|
137 |
|
138 say "\ncurrent situation\n", |
|
139 "------------------"; |
|
140 foreach my $dev (@devs) { |
|
141 if (!$dev->{last}) { say "$l{$dev}: never" } |
|
142 else { |
|
143 for (my $i = 0; $i < @{$dev->{last}}; $i++) { |
|
144 say "$l{$dev}: $i ", defined($dev->{last}[$i]) ? scalar localtime($dev->{last}[$i]) : "-"; |
|
145 } |
|
146 } |
|
147 } |
|
148 |
|
149 say "\nplan for next dump\n", |
|
150 "------------------"; |
|
151 foreach my $dev (@devs) { |
|
152 say "$l{$dev}: level $dev->{level}"; |
|
153 } |
|
154 |
|
155 |
|
156 exit; |
|
157 } |
|
158 |
|
159 # and now we can start doing something with our filesystems |
|
160 DEVICE: foreach my $dev (@devs) { |
|
161 my $dir = $dev->{mountpoint}; |
|
162 $dir =~ s/_/__/g; |
|
163 $dir =~ s/\//_/g; |
|
164 $dir = "$cf{FTP_DIR}/$dir"; |
|
165 |
|
166 my @last; |
|
167 if ($ftp) { |
|
168 $ftp->home(); |
|
169 $ftp->try(mkpath => $dir); |
|
170 $ftp->try(cwd => $dir); |
|
171 |
|
172 #verbose "Now in @{[$ftp->pwd]}.\n" if $ftp; |
|
173 unlink_old_dumps($ftp, $cf{KEEP} + 1) |
|
174 if $opt_clean; |
|
175 |
|
176 # examine the situation and decide about the level |
|
177 # FIXME: currently we simply run a full dump every FULL_CYCLE |
|
178 # days, the intermediate dumps are level 1 |
|
179 foreach (reverse sort $ftp->ls) { |
|
180 /^(?<date>.*)\.(?<level>\d+)$/ or next; |
|
181 $last[$+{level}] = str2time $+{date}; |
|
182 } |
|
183 } |
|
184 |
|
185 # now check, which of the old backups can be purged |
|
186 # The config KEEP tells us how many full dumps we need to |
|
187 # keep. The pre-dump cleaning should keep this number |
|
188 # and after successfull dump we need to cleanup again |
|
189 #$last[0] = [ sort { $a->{stamp} <=> $b->{stamp} } @{$last[0]} ]; |
|
190 |
|
191 # for safety we check if there is really a full dump not older than xxx days |
|
192 if ($dev->{level} > 0) { |
|
193 if (!@last) { |
|
194 $dev->{level} = 0; |
|
195 warn "adjusted backup level to 0, last full backup missing\n"; |
|
196 } elsif (($NOW - $last[0]) > ($cf{FULL_CYCLE} * 86_400)) { |
|
197 $dev->{level} = 0; |
|
198 warn sprintf "adjusted backup level to 0, last full backup is %.1f days old\n", |
|
199 ($NOW - $last[0])/86_400; |
|
200 } |
|
201 } |
|
202 |
|
203 my $file = strftime("%FT%R.$dev->{level}", localtime $NOW); |
|
204 my $label = basename($dev->{rdev}); |
|
205 verbose "> $dev->{dev} ($dev->{rdev}\@$dev->{mountpoint}) to @{[$ftp->pwd]}/$file\n"; |
|
206 next if $opt_dry; |
|
207 |
|
208 # For LVM do a snapshot, for regular partitions |
|
209 # do nothing. But anyway the device to dump is named in $dev->{dump} |
|
210 if ($dev->{lvm}) { |
|
211 |
|
212 # we can do a snapshot |
|
213 # FIXME: check the snapshot name is not used already |
|
214 my $snap = "$dev->{lvm}{path}-snap.0"; |
|
215 |
|
216 verbose "Creating snapshot $snap\n"; |
|
217 system($_ = |
|
218 "lvcreate -s -L 1G -n $snap $dev->{lvm}{path} >/dev/null"); |
|
219 die "failed system command: $_\n" if $?; |
|
220 |
|
221 $dev->{cleanup} = sub { |
|
222 system "lvdisplay $snap &>/dev/null" |
|
223 . " && lvremove -f $snap >/dev/null"; |
|
224 }; |
|
225 push @AT_EXIT, $dev->{cleanup}; |
|
226 |
|
227 (my $device) = |
|
228 (grep /lv name/i, `lvdisplay $snap`)[0] =~ /(\S+)\s*$/; |
|
229 |
|
230 for (my $retries = 3 ; $retries ; $retries--) { |
|
231 system($_ = |
|
232 "fsck -f @{[$opt_verbose ? '-C0' : '']} -y $device"); |
|
233 last if not $?; |
|
234 warn "fsck on $device (using: $_) failed" |
|
235 . ($retries > 1 ? ", retrying…\n" : "") . "\n"; |
|
236 } |
|
237 |
|
238 ($dev->{dump}) = $device; |
|
239 |
|
240 } |
|
241 else { |
|
242 $dev->{dump} = $dev->{rdev}; |
|
243 } |
|
244 |
|
245 ### $dev |
|
246 |
|
247 $ENV{key} = $cf{KEY}; |
|
248 my $dumper = open(my $dump, "-|") or do { |
|
249 print <<__HEAD; |
|
250 #! /bin/bash |
|
251 LC_ALL=C |
|
252 if test -t 1; then |
|
253 cat <<___ |
|
254 HOSTNAME : $HOSTNAME |
|
255 DATE : $NOW @{[scalar localtime $NOW]} |
|
256 LEVEL : $dev->{level} |
|
257 DEVICE : $dev->{dev} |
|
258 REAL_DEVICE: $dev->{rdev} |
|
259 MOUNTPOINT : $dev->{mountpoint} |
|
260 FSTYPE : $dev->{fstype} |
|
261 DEVICE_NO : $dev->{devno} |
|
262 |
|
263 # For recovery pass everything following the first |
|
264 # ^### START to "recover -rf -". Or do one of the following |
|
265 # lines: |
|
266 # sh <THIS SCRIPT> | restore -rf - |
|
267 # sh <(ftpipe <URL>) -pass file:/dev/tty | restore -rf - |
|
268 ___ |
|
269 exit 0 |
|
270 fi |
|
271 while read; do |
|
272 test "\$REPLY" = "### START" \\ |
|
273 && exec openssl enc -d -blowfish "\$@" |
|
274 done <"\$0" |
|
275 |
|
276 ### START |
|
277 __HEAD |
|
278 |
|
279 |
|
280 update_devnames($opt_dumpdates, $dev->{rdev} => $dev->{dump}) |
|
281 if $opt_dumpdates; |
|
282 |
|
283 exec "dump -$dev->{level} -L $label -f- -u -z$cf{COMPRESSION_LEVEL} $dev->{dump}" |
|
284 . "| openssl enc -pass env:key -salt -blowfish"; |
|
285 die "Can't exec dumper\n"; |
|
286 }; |
|
287 |
|
288 if ($ftp) { |
|
289 $ftp->try(put => $dump, $file); |
|
290 } |
|
291 else { |
|
292 print while <$dump>; |
|
293 warn "STOPPED after the first dump\n"; |
|
294 exit; |
|
295 } |
|
296 $dev->{cleanup}->() if $dev->{cleanup}; |
|
297 verbose "Done.\n"; |
|
298 |
|
299 update_devnames($opt_dumpdates, $dev->{dump} => $dev->{rdev}) |
|
300 if $opt_dumpdates; |
|
301 |
|
302 unlink_old_dumps($ftp, $cf{KEEP}) |
|
303 if $ftp and $opt_clean; |
|
304 } |
|
305 |
|
306 } |
|
307 |
|
308 sub verbose(@) { |
|
309 return if not $opt_verbose; |
|
310 print STDERR @_; |
|
311 } |
|
312 |
|
313 sub get_candidates() { |
|
314 |
|
315 # return the list of backup candidates |
|
316 |
|
317 my @devs; |
|
318 |
|
319 # later we need the major of the device mapper |
|
320 my $dev_mapper = (grep /device.mapper/, slurp("/proc/devices"))[0]; |
|
321 $dev_mapper = (split " " => $dev_mapper)[0] if defined $dev_mapper; |
|
322 |
|
323 # find all non comment lines |
|
324 foreach (grep !/^\s*#/, slurp("/etc/fstab")) { |
|
325 my ($dev, $mp, $fstype, $options, $dump, $check) = split; |
|
326 next if not $dump; |
|
327 |
|
328 # $dev does not have to contain the real device |
|
329 my $rdev = real_device($dev); |
|
330 my ($major, $minor) = devno($rdev); |
|
331 |
|
332 # if it's LVM we gather more information (to support snapshots) |
|
333 my $lvm; |
|
334 if ($_ = (grep { /:$major:$minor\s*$/ } `lvdisplay -c`)[0] |
|
335 and /\s*(?<path>\S+?):/) |
|
336 { |
|
337 ($lvm->{path} = $+{path}) =~ s/^\/dev\///; |
|
338 } |
|
339 |
|
340 push @devs, |
|
341 { |
|
342 dev => $dev, |
|
343 rdev => $rdev, |
|
344 mountpoint => $mp, |
|
345 fstype => $fstype, |
|
346 lvm => $lvm, |
|
347 devno => "$major:$minor", |
|
348 }; |
|
349 } |
|
350 |
|
351 return @devs; |
|
352 } |
|
353 |
|
354 sub get_configs(@) { |
|
355 local $_; |
|
356 my %r = (); |
|
357 foreach (grep { -f } map { (-d) ? glob("$_/*") : $_ } @_) { |
|
358 |
|
359 # check permission and ownership |
|
360 { |
|
361 my $p = (stat)[2] & 07777; |
|
362 my $u = (stat _)[4]; |
|
363 die |
|
364 "$ME: $_ has wrong permissions: found @{[sprintf '%04o', $p]}, need 0600\n" |
|
365 if $p != 0600; |
|
366 die |
|
367 "$ME: owner of $_ ($u) is not the EUID ($EUID) of this process\n" |
|
368 if (stat _)[4] != $EUID; |
|
369 |
|
370 # FIXME: should check the containing directories too! |
|
371 }; |
|
372 |
|
373 open(my $f, $_) or die "Can't open $_: $!\n"; |
|
374 my %h = map { split /\s*=\s*/, $_, 2 } grep { !/^\s*#/ and /=/ } <$f>; |
|
375 map { chomp } values %h; |
|
376 %r = (%r, %h); |
|
377 } |
|
378 return %r; |
|
379 } |
|
380 |
|
381 { |
|
382 |
|
383 package FTP; |
|
384 use strict; |
|
385 use warnings; |
|
386 use base qw(Net::FTP); |
|
387 |
|
388 my %data; |
|
389 |
|
390 sub new { |
|
391 my $class = shift; |
|
392 return bless Net::FTP->new(@_) => $class; |
|
393 } |
|
394 |
|
395 sub try { |
|
396 my $self = shift; |
|
397 my $func = shift; |
|
398 $self->$func(@_) |
|
399 or die "FTP $func failed: " . $self->message . "\n"; |
|
400 } |
|
401 |
|
402 sub mkpath { |
|
403 my $self = shift; |
|
404 my $current = $self->pwd(); |
|
405 foreach (split /\/+/, $_[0]) { |
|
406 next if $self->cwd($_); |
|
407 return undef if not $self->message ~~ /no such .*dir/i; |
|
408 return undef if not $self->SUPER::mkdir($_); |
|
409 return undef if not $self->cwd($_); |
|
410 } |
|
411 $self->cwd($current); |
|
412 } |
|
413 |
|
414 sub home { |
|
415 my $self = shift; |
|
416 return $data{ ref $self }{home} = shift if @_; |
|
417 $self->try(cwd => exists $data{ ref $self }{home} |
|
418 ? $data{ ref $self }{home} |
|
419 : "/"); |
|
420 return $self->pwd(); |
|
421 } |
|
422 |
|
423 sub get_home { return $data{ ref shift }{home} } |
|
424 } |
|
425 |
|
426 sub update_devnames($$$) { |
|
427 my ($file, $from, $to) = @_; |
|
428 open(my $f, "+>>", $file) or die "Can't open $file: $!\n"; |
|
429 seek($f, 0, 0); |
|
430 my $_ = join "", <$f>; |
|
431 s/^$from\s/$to /mg; |
|
432 truncate($f, 0); |
|
433 # fix the dumpdates |
|
434 print $f $_; |
|
435 close($f); |
|
436 } |
|
437 |
|
438 sub real_device($) { |
|
439 my $dev = shift; |
|
440 |
|
441 if ($dev ~~ /^(LABEL|UUID)=/) { |
|
442 # NOTE: dump is able to handle LABEL=... too, but I think |
|
443 # it's more easy for recovery to know the real device |
|
444 chomp($dev = `blkid -c /dev/null -o device -t '$dev'`); |
|
445 } |
|
446 $dev = realpath($dev); |
|
447 } |
|
448 |
|
449 sub devno($) { |
|
450 stat shift or return wantarray ? () : undef; |
|
451 my @mm = ((stat _)[6] >> 8, (stat _)[6] & 0xff); |
|
452 return wantarray ? @mm : "$mm[0]:$mm[1]"; |
|
453 } |
|
454 |
|
455 |
|
456 # put the last dump information (level and date) into |
|
457 # the device structure - information is obtained from $opt_dumpdates |
|
458 sub get_history(@) { |
|
459 my @devs = @_; |
|
460 my %dd; |
|
461 |
|
462 open(my $dd, "+>>", $opt_dumpdates); |
|
463 seek($dd, 0, 0); |
|
464 while (<$dd>) { |
|
465 my ($dev, $level, $date) = /^(\S+)\s+(\d+)\s+(.{30})/ |
|
466 or die "Can't parse $opt_dumpdates: `$_'\n"; |
|
467 my $rdev = real_device($dev); |
|
468 my $devno = devno($rdev); |
|
469 |
|
470 push @{$dd{$rdev}} => { |
|
471 dev => $dev, |
|
472 rdev => real_device($dev), |
|
473 level => $level, |
|
474 date => str2time($date), |
|
475 devno => scalar(devno(real_device($dev))), |
|
476 } |
|
477 } |
|
478 close($dd); |
|
479 |
|
480 foreach my $dev (@devs) { |
|
481 my $dd = $dd{$dev->{rdev}}; |
|
482 |
|
483 if (!$dd) { |
|
484 $dev->{last} = undef; |
|
485 next; |
|
486 } |
|
487 |
|
488 foreach my $dump (@$dd) { |
|
489 $dev->{last}[$dump->{level}] = $dump->{date}; |
|
490 } |
|
491 } |
|
492 |
|
493 ### @devs |
|
494 return @devs; |
|
495 } |
|
496 |
|
497 sub get_estimate($$) { |
|
498 my ($dev, $level) = @_; |
|
499 warn "% estimating $dev->{rdev} at level $level\n"; |
|
500 chomp(my $_ = `dump -S -$level $dev->{rdev}`); |
|
501 return $_; |
|
502 } |
|
503 |
|
504 sub calculate_level($@) { |
|
505 my ($cycle, @devs) = @_; |
|
506 |
|
507 foreach my $dev (@devs) { |
|
508 if (defined $opt_level) { |
|
509 $dev->{level} = $opt_level; |
|
510 } |
|
511 elsif (!$dev->{last} |
|
512 or not $dev->{last}[0] |
|
513 or $NOW - $dev->{last}[0] > ($cycle * 86_400)) { |
|
514 $dev->{level} = 0; |
|
515 } |
|
516 else { $dev->{level} = 1 } |
|
517 |
|
518 # now we'll see if the level really saves space compared |
|
519 # with the next lower level |
|
520 my @estimates; |
|
521 while (my $l = $dev->{level} > 0) { |
|
522 $estimates[$l] //= get_estimate($dev, $l); |
|
523 $estimates[$l - 1] //= get_estimate($dev, $l - 1); |
|
524 |
|
525 last if my $savings = ($estimates[$l-1] - $estimates[$l]) / $estimates[$l-1] >= 0.10; |
|
526 warn "% savings for level $dev->{level} on $dev->{dev} are @{[int($savings * 100)]}%: ", |
|
527 "will use level ", $dev->{level} - 1, "\n"; |
|
528 --$dev->{level}; |
|
529 } |
|
530 } |
|
531 |
|
532 return @devs; |
|
533 } |
|
534 |
|
535 sub unlink_old_dumps($$) { |
|
536 my ($ftp, $keep) = @_; |
|
537 my @dumps; |
|
538 foreach ($ftp->ls) { |
|
539 /^(?<date>.*)\.(?<level>\d+)$/ or next; |
|
540 push @{$dumps[$+{level}]} => { file => $_, date => $+{date}, stamp => str2time($+{date})}; |
|
541 } |
|
542 |
|
543 ### @dumps |
|
544 |
|
545 # sort the level 0 dumps by date and remove all but the last $keep |
|
546 # ones. |
|
547 # if we found level 0 dumps, we remove all level 1+ dumps older than |
|
548 # the oldest level 0 dump we'll remove |
|
549 @{$dumps[0]} = reverse sort { $a->{stamp} <=> $b->{stamp} } @{$dumps[0]}; |
|
550 my @unlink = @{$dumps[0]}[$keep..$#{$dumps[0]}]; |
|
551 push @unlink => grep { $_->{stamp} <= $unlink[0]->{stamp} } @{@dumps[1..$#dumps]} |
|
552 if @unlink; |
|
553 ### @unlink |
|
554 |
|
555 foreach (@unlink) { |
|
556 say "DELETE: $_->{file}"; |
|
557 next if $opt_dry; |
|
558 $ftp->delete($_->{file}); |
|
559 } |
|
560 } |
|
561 |
|
562 |
|
563 #/dev/vda1 0 Thu Apr 14 12:54:31 2011 +0200 |
|
564 #/dev/vda1 1 Thu Apr 14 12:54:16 2011 +0200 |
|
565 |
|
566 __END__ |
|
567 |
|
568 =head1 NAME |
|
569 |
|
570 ftbackup - ftp backup tool |
|
571 |
|
572 =head1 SYNOPSIS |
|
573 |
|
574 ftbackup [--level <level>] [options] |
|
575 |
|
576 =head1 DESCRIPTION |
|
577 |
|
578 The B<ftbackup> tools saves the partitions (file systems) marked in |
|
579 F</etc/fstab> to an FTP host. It uses dump(8) for generating the backup |
|
580 and openssl(1) for encrypting the data stream (and thus the written |
|
581 files). |
|
582 |
|
583 =head1 OPTIONS |
|
584 |
|
585 =over |
|
586 |
|
587 =item B<-D>|B<--dumpdates> I<file> |
|
588 |
|
589 Update the I<file> as dumpdates file. (default: /var/lib/dumpdates) |
|
590 |
|
591 =item B<-d>|B<--debug> [I<item>] |
|
592 |
|
593 Enables debugging for the specified items (comma separated). |
|
594 If no item is specified, just some debugging is done. |
|
595 |
|
596 Valid items are B<ftp>, B<output>, B<devices> and currently nothing else. |
|
597 |
|
598 =over |
|
599 |
|
600 =item B<ftp> |
|
601 |
|
602 This switches on debugging of the used L<Net::FTP> module. |
|
603 |
|
604 =item B<output> |
|
605 |
|
606 The output is not sent via FTP but to stdout. Beware! |
|
607 |
|
608 =back |
|
609 |
|
610 Even more debugging is shown using the DEBUG=1 environment setting. |
|
611 |
|
612 =item B<--clean> |
|
613 |
|
614 Cleanup older backups we do not need (that is: incremental backups with |
|
615 no previous full backup. The number of old backups we keep |
|
616 is read from the configuration file. (default: 1) |
|
617 |
|
618 =item B<--dry> |
|
619 |
|
620 Dry run, no real backup is done, this option implies B<--verbose>. (default: off) |
|
621 |
|
622 =item B<-f>|B<--force> |
|
623 |
|
624 Use more power (e.g. overwrite a previous level backup and remove all |
|
625 invalidated other backups). (default: 0 and not implemented) |
|
626 |
|
627 =item B<-i>|B<--info> |
|
628 |
|
629 Just output information about the last backups and exit. (default: off) |
|
630 |
|
631 =item B<-l>|B<--level> I<level> |
|
632 |
|
633 The backup level. Level other than "0" needs a previous |
|
634 level 0 (full) backup. If not specified, it is choosen automagically. |
|
635 (default: undef) |
|
636 |
|
637 =item B<-L>|B<--label> I<label> |
|
638 |
|
639 The label for the backup. (default: daily) |
|
640 |
|
641 =item B<-v>|B<--verbose> |
|
642 |
|
643 Be verbose. (default: no) |
|
644 |
|
645 =back |
|
646 |
|
647 =head1 FILES |
|
648 |
|
649 =head2 Configuration |
|
650 |
|
651 The config files are searched in the following places: |
|
652 |
|
653 /etc/ftbackup.conf |
|
654 ~/.ftbackup.conf |
|
655 ./ftbackup.conf |
|
656 |
|
657 If the location is a directory, all (not hidden) files in this directory are |
|
658 considered to be config, if the location a file itself, this is considered to |
|
659 be a config file. The config files have to be mode 0600 and they have to be |
|
660 owned by the EUID running the process. |
|
661 |
|
662 The config file may contain the following items (listed with their built in defaults) |
|
663 |
|
664 KEY = <no default> |
|
665 FTP_HOST = <no default> |
|
666 FTP_DIR = "backup/<LABEL>/<HOSTNAME>" |
|
667 FTP_PASSIVE = 1 |
|
668 COMPRESSION_LEVEL = 6 |
|
669 FULL_CYCLE = 7 |
|
670 KEEP = 2 |
|
671 |
|
672 =over |
|
673 |
|
674 =item KEY |
|
675 |
|
676 The encryption key to use. (We use symmetric blowfish encryption currently.) |
|
677 |
|
678 =item FTP_HOST |
|
679 |
|
680 The FTP host to send the backup to. |
|
681 |
|
682 =item FTP_DIR |
|
683 |
|
684 A template for storing the backup file(s). Each dumped file system needs |
|
685 its own directory! |
|
686 |
|
687 =item FTP_PASSIVE |
|
688 |
|
689 A switch to activate the usage of passive FTP. |
|
690 |
|
691 =item COMPRESSION_LEVEL |
|
692 |
|
693 The level of the used gzip compression. |
|
694 |
|
695 =item FULL_CYCLE |
|
696 |
|
697 A full backup is forced if the last full backup is older than thi number |
|
698 of days. |
|
699 |
|
700 =item KEEP |
|
701 |
|
702 The number of full backups (including the current one!) to keep. It means, that |
|
703 normally you'll get KEEP backups in your backup directory. Useless |
|
704 incremental backups are deleted automgically. |
|
705 |
|
706 =back |
|
707 |
|
708 |
|
709 |
|
710 =head2 F<.netrc> |
|
711 |
|
712 You may miss the login information for the FTP server. Currently we rely on a valid |
|
713 F<~/.netrc> entry. An example line of the F<~/.netrc>: |
|
714 |
|
715 machine ... login ... password ... |
|
716 |
|
717 =cut |
|
718 |
|
719 # vim:sts=4 sw=4 aw ai sm: |
|