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