py2b
changeset 4 79ab63474be7
parent 2 5f03a7843dc2
child 5 96697a91fbd2
--- 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<level>
 
 The backup level. Level other than "0" needs a previous
@@ -271,7 +305,16 @@
 
 =head1 FILES
 
-The B<config> 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