sbin/ftbackup
changeset 49 ce823daf2141
parent 48 708e3b8bd670
parent 46 23a3977d923d
child 50 40533435cfd3
equal deleted inserted replaced
48:708e3b8bd670 49:ce823daf2141
     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: