# HG changeset patch # User Heiko Schlittermann # Date 1256596081 -3600 # Node ID 79ab63474be71cd104738b2f0985f637978ac3c9 # Parent 5f03a7843dc2c93832a6c7835a2a3cc9dcac2106 Level 0 and other backups work. Timestamp based filenames. Compression is done inside of dump. No automagic levelling. Config file handling. diff -r 5f03a7843dc2 -r 79ab63474be7 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Mon Oct 26 23:28:01 2009 +0100 @@ -0,0 +1,1 @@ +py2b.conf diff -r 5f03a7843dc2 -r 79ab63474be7 py2.d/DEFAULT --- a/py2.d/DEFAULT Sun Oct 25 23:53:47 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -# used by all config -KEY = x -FTP_HOST = backup.ccos.de -FTP_DIR = backups/daily/py2 -FTP_PASSIVE = 1 diff -r 5f03a7843dc2 -r 79ab63474be7 py2b --- a/py2b Sun Oct 25 23:53:47 2009 +0100 +++ b/py2b Mon Oct 26 23:28:01 2009 +0100 @@ -10,23 +10,27 @@ use Sys::Hostname; use Pod::Usage; use POSIX qw(strftime);; +use English qw(-no_match_vars); +use 5.10.0; use if $ENV{DEBUG} => qw(Smart::Comments); $ENV{LC_ALL} = "C"; +my $ME = basename $0; + +my @CONFIGS = ("/etc/$ME", "$ENV{HOME}/.$ME", "$ME.conf"); + +my $NODE = hostname; +my $NOW = time(); + my $opt_level = 0; -my $opt_today = strftime("%F", localtime); +my $opt_today = strftime("%F", localtime $NOW); my @opt_debug = (); my $opt_verbose = 0; my $opt_dry = 0; -#my $opt_node = hostname; -#my $opt_dir = "backups/$opt_node/daily"; +my $opt_force = 0; -# all configs are below -my $CONFIG_DIR = "./py2.d"; -my $NODE = hostname; - -sub get_configs($); +sub get_configs(@); sub get_candidates(); sub verbose(@); @@ -34,6 +38,12 @@ END { $_->() foreach @AT_EXIT }; $SIG{INT} = sub { warn "Got signal INT\n"; exit 1 }; +my %CONFIG = ( + FTP_DIR => "backup/daily/$NODE", + FTP_PASSIVE => 1, + FULL_CYCLE => 7, # not used +); + MAIN: { GetOptions( "l|level=i" => \$opt_level, @@ -42,33 +52,33 @@ "m|man" => sub { pod2usage(-exit => 0, -verbose => 3) }, "v|verbose" => \$opt_verbose, "dry" => \$opt_dry, + "f|force" => \$opt_force, ) or pod2usage; - my %cf = get_configs($CONFIG_DIR); - my %default = %{$cf{DEFAULT}}; - ### config: %cf - + my %cf = (%CONFIG, get_configs(@CONFIGS)); my @dev = get_candidates(); ### current candiates: @dev - my $ftp = new FTP($default{FTP_HOST}, - Passive => $default{FTP_PASSIVE}, + my $ftp = new FTP($cf{FTP_HOST}, + Passive => $cf{FTP_PASSIVE}, Debug => @opt_debug ~~ /^ftp$/) or die $@; $ftp->login or die $ftp->message; $ftp->try(binary => ()); - $ftp->try(mkpath => $default{FTP_DIR}); - $ftp->try(cwd => $default{FTP_DIR}); + $ftp->try(mkpath => $cf{FTP_DIR}); + $ftp->try(cwd => $cf{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); + given ($opt_level) { + when(0) { + $ftp->try(mkpath => $opt_today); + $ftp->try(cwd => $opt_today); + } + default { + # find the last full backup directory + 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 @@ -77,9 +87,16 @@ # and now we can start doing something with our filesystems foreach my $dev (@dev) { - my $file = basename($dev->{dev}) . ".$opt_level.gz.ssl"; + my $file = basename($dev->{dev}) . "." + . strftime("%F_%R", localtime $NOW) + . ".$opt_level.ssl"; my $label = "$NODE:" . basename($dev->{rdev}); verbose "Working on $dev->{dev} as $dev->{rdev}, stored as $file\n"; + next if $opt_dry; + + ## complain if there is already a full backup in this + ## sequence + ##die "level 0 dir should be empty\n" if @{$ftp->try(ls => "*.0.*")}; # For LVM do a snapshot, for regular partitions # do nothing. But anyway the device to dump is named in $dev->{dump} @@ -110,20 +127,30 @@ ### $dev - $ENV{key} = $default{KEY}; + $ENV{key} = $cf{KEY}; my $dumper = open(my $dump, "-|") or do { my $head = <<__; #! /bin/bash -echo "LEVEL $opt_level: $dev->{dev} $dev->{rdev} ($dev->{dump})" >&2 -tail -c XXXX \$0 | openssl enc -d -blowfish "\$@" | gzip -d +if test "\$1" = "--info"; then + cat <<___ +NODE : $NODE +DATE : $NOW @{[localtime $NOW]} +LEVEL : $opt_level +DEVICE : $dev->{dev} +REAL_DEVICE: $dev->{rdev} +MOUNTPOINT : $dev->{mountpoint} +FSTYPE : $dev->{fstype} +___ + exit 0 +fi +tail -c XXXXX \$0 | openssl enc -d -blowfish "\$@" exit __ # adjust the placeholder - $head =~ s/XXXX/sprintf "% 4s", "+" . (length($head) +1)/e; + $head =~ s/XXXXX/sprintf "% 5s", "+" . (length($head) +1)/e; print $head; - exec "dump -$opt_level -L $label -f- -u $dev->{dump}" - . "| gzip" + exec "dump -$opt_level -L $label -f- -u -z6 $dev->{dump}" . "| openssl enc -pass env:key -salt -blowfish"; die "Can't exec dumper\n"; }; @@ -163,6 +190,7 @@ $rdev = readlink $rdev while -l $rdev; # if it's LVM we gather more information (to support snapshots) + # FIXME: could have used `lvdisplay -c' my $lvm; if ((stat $rdev)[6] >> 8 == $dev_mapper) { @{$lvm}{qw/vg lv/} = map { s/--/-/g; $_ } basename($rdev) =~ /(.+[^-])-([^-].+)/; @@ -172,7 +200,7 @@ push @dev, { dev => $dev, rdev => $rdev, - mount_point => $mp, + mountpoint => $mp, fstype => $fstype, lvm => $lvm, }; @@ -181,26 +209,27 @@ return @dev; } -sub get_configs($) { +sub get_configs(@) { local $_; - my %r; - foreach (glob("$_[0]/*")) { + my %r = (); + foreach (grep {-f} map { (-d) ? glob("$_/*") : $_ } @_) { + + # check permission and ownership + { + my $p = (stat)[2] & 07777; + my $u = (stat _)[4]; + die "$ME: $_ has wrong permissions: found @{[sprintf '%04o', $p]}, need 0600\n" + if $p != 0600; + die "$ME: owner of $_ ($u) is not the EUID ($EUID) of this process\n" + if (stat _)[4] != $EUID; + + # FIXME: should check the containing directories too! + }; + my $f = new IO::File $_ or die "Can't open $_: $!\n"; - my %h = map { split /\s*=\s*/, $_, 2 } grep {!/^\s*#/} <$f>; + my %h = map { split /\s*=\s*/, $_, 2 } grep {!/^\s*#/ and /=/} <$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; - } + %r = (%r, %h); } return %r; } @@ -258,6 +287,11 @@ Even more debugging is shown using the DEBUG=1 environment setting. +=item B<-f>|B<--force> + +Use more power (e.g. overwrite a previous level backup and remove all +invalidated other backups). (default: 0) + =item B<-l>|B<--level> I The backup level. Level other than "0" needs a previous @@ -271,7 +305,16 @@ =head1 FILES -The B file should be mentioned. +The config files are searched in the following places: + + /etc/py2b + ~/.py2b + ./py2b.conf + +If the location is a directory, all (not hidden) files in this directory are +considered to be config, if the location a file itself, this is considered to +be a config file. The config files have to be mode 0600 and they have to be +owned by the EUID running the process. =cut diff -r 5f03a7843dc2 -r 79ab63474be7 py2b.conf.example --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/py2b.conf.example Mon Oct 26 23:28:01 2009 +0100 @@ -0,0 +1,16 @@ +# example config +# the commented values are the built in defaults + +# The encryption key +KEY = + +# FTP-Server hostname +FTP_HOST = + +# FTP-Server base directory +# the following expansion work: +# $NODE +# FTP_DIR = backups/daily/$NODE + +# if we need passive mode for file transfer +# FTP_PASSIVE = 1