# HG changeset patch # User root@py2.ccos.de # Date 1256334623 -7200 # Node ID 8bd8bd02c04a50011565512fc9cfbb9a5d485eaa pre-alpha diff -r 000000000000 -r 8bd8bd02c04a poc --- /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 diff -r 000000000000 -r 8bd8bd02c04a py2.d/DEFAULT --- /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 diff -r 000000000000 -r 8bd8bd02c04a py2b --- /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 ] [options] + +=head1 OPTIONS + +=over + +=item B<-d>|B<--debug> [I] + +Enables debugging for the specified items (comma separated). +If no item is specified, just some debugging is done. + +Valid items are B and currently nothing else. + +Even more debugging is shown using the DEBUG=1 environment setting. + +=item B<-l>|B<--level> I + +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 file should be mentioned. + +=cut + +# vim:sts=4 sw=4 aw ai sm: