--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/poc Fri Oct 23 23:50:23 2009 +0200
@@ -0,0 +1,33 @@
+#! /bin/bash -e
+LC_ALL=C
+
+FTP=backup.ccos.de
+FULL=Sun
+NODE=`uname -n`
+KEY=x
+
+#---
+DATE=$(date -I)
+
+#DATE_FULL=$(date -I -d "last $FULL")
+
+
+ftp() {
+ set -x
+ lftp $FTP -e "$*; exit"
+ set +x
+}
+
+if test $(date +%a) = $FULL; then
+ ftp ls | grep -q "$NODE-$DATE" \
+ || ftp mkdir "$NODE-$DATE"
+
+ dump -w 2>/dev/null | while read fs rest; do
+ test "${fs:0:1}" = "/" || continue
+ name=${fs//\//_}
+ dump -u -f- -0 $fs \
+ | KEY="$KEY" openssl enc -salt -blowfish -pass env:KEY -e \
+ | ftpipe -p ftp://$FTP/$NODE-$DATE/$name.dump.0.ssl
+ done
+
+fi
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/py2.d/DEFAULT Fri Oct 23 23:50:23 2009 +0200
@@ -0,0 +1,5 @@
+# used by all config
+KEY = x
+FTP_HOST = backup.ccos.de
+FTP_DIR = backups/daily/py2
+FTP_PASSIVE = 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/py2b Fri Oct 23 23:50:23 2009 +0200
@@ -0,0 +1,217 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+
+use IO::File;
+use File::Basename;
+use Net::FTP;
+use Perl6::Slurp;
+use Getopt::Long;
+use Sys::Hostname;
+use Pod::Usage;
+use POSIX qw(strftime);;
+use if $ENV{DEBUG} => qw(Smart::Comments);
+
+$ENV{LC_ALL} = "C";
+
+my $opt_level = 0;
+my $opt_today = strftime("%F", localtime);
+my @opt_debug = ();
+my $opt_verbose = 0;
+#my $opt_node = hostname;
+#my $opt_dir = "backups/$opt_node/daily";
+
+# all configs are below
+my $CONFIG_DIR = "./py2.d";
+my $NODE = hostname;
+
+sub get_configs($);
+sub get_candidates();
+sub verbose(@);
+
+MAIN: {
+ GetOptions(
+ "l|level=i" => \$opt_level,
+ "d|debug:s" => sub { push @opt_debug, split /,/, $_[1] },
+ "h|help" => sub { pod2usage(-exit => 0, -verbose => 1) },
+ "m|man" => sub { pod2usage(-exit => 0, -verbose => 3) },
+ "v|verbose" => \$opt_verbose,
+ ) or pod2usage;
+
+ my %cf = get_configs($CONFIG_DIR);
+ my %default = %{$cf{DEFAULT}};
+ ### config: %cf
+
+ my %dev = get_candidates();
+ ### current devices: %dev
+
+ my $ftp = new FTP($default{FTP_HOST},
+ Passive => $default{FTP_PASSIVE},
+ Debug => @opt_debug ~~ /^ftp$/) or die $@;
+ $ftp->login or die $ftp->message;
+ $ftp->try(mkpath => $default{FTP_DIR});
+ $ftp->try(cwd => $default{FTP_DIR});
+
+ if ($opt_level == 0) {
+ $ftp->try(mkpath => $opt_today);
+ $ftp->try(cwd => $opt_today);
+ }
+ else {
+ # find the last full backup
+ my $last_full = (reverse sort grep /^\d{4}-\d{2}-\d{2}$/, $ftp->ls)[0];
+ die "no last full backup found in @{[$ftp->pwd]}\n"
+ if not $last_full;
+ $ftp->try(cwd => $last_full);
+ }
+
+ # now sitting inside the directory for the last full backup
+ verbose "Now in @{[$ftp->pwd]}.\n";
+
+ # and now we can start doing something with our filesystems
+ foreach my $dev (keys %dev) {
+ my $file = basename($dev) . ".$level.gz.ssl";
+ my $label = "$NODE:" . basename($dev{$dev});
+ verbose "Working on $dev as $dev{$dev}, stored as $file\n";
+ $ENV{key} = $default{KEY};
+ my $dumper = open(my $dump, "-|") or do {
+ my $head = <<__;
+#! /bin/bash
+echo "LEVEL $opt_level: $dev $dev{$dev}"
+read -p "sure to restore? (yes/no): "
+test "\$REPLY" = "yes" || exit
+exec dd if=\$0 bs=10k skip=1 | openssl enc -d -blowfish "\$@" | gzip -d | restore -rf-
+exit
+__
+ print $head, " " x (10240 - length($head) - 1), "\n";
+ exec "dump -$opt_level -L $label -f- -u $dev{$dev}"
+ . "| gzip"
+ . "| openssl enc -pass env:key -salt -blowfish";
+ die "Can't exec dumper\n";
+ };
+ $ftp->try(put => $dump, $file);
+ verbose "Done.\n";
+ }
+
+}
+
+sub verbose(@) {
+ return if not $opt_verbose;
+ print @_;
+}
+
+sub get_candidates() {
+# return the list of backup candidates
+
+ my %dev;
+
+ foreach (slurp("/etc/fstab")) {
+ my ($dev, $mp, $fstype, $options, $dump, $check)
+ = split;
+ next if not $dump;
+
+ # $dev does not have to contain the real device
+ my $rdev = $dev;
+ if ($dev ~~ /^(LABEL|UUID)=/) {
+ chomp($rdev = `blkid -c /dev/null -o device -t '$dev'`);
+ }
+ $dev{$dev} = $rdev;
+ }
+
+ return %dev;
+}
+
+sub get_configs($) {
+ local $_;
+ my %r;
+ foreach (glob("$_[0]/*")) {
+ my $f = new IO::File $_ or die "Can't open $_: $!\n";
+ my %h = map { split /\s*=\s*/, $_, 2 } grep {!/^\s*#/} <$f>;
+ map { chomp } values %h;
+ if (basename($_) eq "DEFAULT") {
+ $r{DEFAULT} = \%h;
+ next;
+ }
+ if (exists $h{DEV}) {
+ $r{$h{DEV}} = \%h;
+ next;
+ }
+
+ if (exists $h{MOUNT}) {
+ $r{$h{MOUNT}} = \%h;
+ next;
+ }
+ }
+ return %r;
+}
+
+{ package FTP;
+ use strict;
+ use warnings;
+ use base qw(Net::FTP);
+
+ sub new {
+ my $class = shift;
+ return bless Net::FTP->new(@_) => $class;
+ }
+
+ sub try {
+ my $self = shift;
+ my $func = shift;
+ $self->$func(@_)
+ or die "FTP $func failed: " . $self->message . "\n";
+ }
+
+ sub mkpath {
+ my $self = shift;
+ my $current = $self->pwd();
+ foreach (split /\/+/, $_[0]) {
+ next if $self->cwd($_);
+ return undef if not $self->message ~~ /no such .*dir/i;
+ return undef if not $self->SUPER::mkdir($_);
+ return undef if not $self->cwd($_);
+ }
+ $self->cwd($current);
+ }
+}
+
+__END__
+
+=head1 NAME
+
+py2b - backup tool
+
+=head1 SYNOPSIS
+
+ py2b [--level <level>] [options]
+
+=head1 OPTIONS
+
+=over
+
+=item B<-d>|B<--debug> [I<item>]
+
+Enables debugging for the specified items (comma separated).
+If no item is specified, just some debugging is done.
+
+Valid items are B<ftp> and currently nothing else.
+
+Even more debugging is shown using the DEBUG=1 environment setting.
+
+=item B<-l>|B<--level> I<level>
+
+The backup level. Level other than "0" needs a previous
+level 0 (full) backup. (default: 0)
+
+=item B<-v>|B<--verbose>
+
+Be verbose. (default: no)
+
+=back
+
+=head1 FILES
+
+The B<config> file should be mentioned.
+
+=cut
+
+# vim:sts=4 sw=4 aw ai sm: