pre-alpha
authorroot@py2.ccos.de
Fri, 23 Oct 2009 23:50:23 +0200
changeset 0 8bd8bd02c04a
child 1 505c5bd23279
pre-alpha
poc
py2.d/DEFAULT
py2b
--- /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: