deb version 0.1
authorHeiko Schlittermann <hs@schlittermann.de>
Sat, 21 Nov 2009 22:03:50 +0100
changeset 10 e2e657251d08
parent 9 ab747c27a38b
child 11 a0d340ff424b
deb version 0.1
Makefile
Makefile.in
build-stamp
configure
configure-stamp
debian/control
debian/files
debian/ftbackup.debhelper.log
debian/ftbackup/DEBIAN/conffiles
debian/ftbackup/DEBIAN/control
debian/ftbackup/DEBIAN/md5sums
debian/ftbackup/etc/ftbackup/default
debian/ftbackup/usr/sbin/ftbackup
debian/ftbackup/usr/share/doc/ftbackup/README.Debian
debian/ftbackup/usr/share/doc/ftbackup/changelog.Debian.gz
debian/ftbackup/usr/share/doc/ftbackup/copyright
debian/rules
ftbackup
ftbackup.conf.example
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,16 @@
+DESTDIR = 
+prefix = /usr
+sbindir = ${prefix}/sbin
+
+SCRIPT = ftbackup
+
+all:
+	# nothing
+
+install:	all
+	install -m 0755 -d ${DESTDIR}${sbindir}
+	install -m 0755 ${SCRIPT} ${DESTDIR}${sbindir}
+	
+
+clean:
+	# nothing
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile.in	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,16 @@
+DESTDIR = 
+prefix = @prefix@
+sbindir = ${prefix}/sbin
+
+SCRIPT = ftbackup
+
+all:
+	# nothing
+
+install:	all
+	install -m 0755 -d ${DESTDIR}${sbindir}
+	install -m 0755 ${SCRIPT} ${DESTDIR}${sbindir}
+	
+
+clean:
+	# nothing
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/configure	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,17 @@
+#! /bin/bash
+
+prefix="/usr/local"
+
+tmp=$(getopt -n $0 -o p -l prefix: -- "$@")
+eval set -- "$tmp"
+
+while true; do
+    case "$1" in 
+	--prefix)   prefix="$2"; shift 2
+		    ;;
+	--)	    break
+		    ;;
+    esac
+done
+
+sed -e "s|@prefix@|$prefix|g" <Makefile.in >Makefile
--- a/debian/control	Wed Oct 28 23:00:02 2009 +0100
+++ b/debian/control	Sat Nov 21 22:03:50 2009 +0100
@@ -4,10 +4,12 @@
 Maintainer: Heiko Schlittermann <hs@schlittermann.de>
 Build-Depends: debhelper (>= 7)
 Standards-Version: 3.7.3
-Homepage: <insert the upstream URL, if relevant>
+Homepage: https://keller.schlittermann.de/hg/incubator/ftbackup
 
 Package: ftbackup
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}
-Description: <insert up to 60 chars description>
- <insert long description, indented with spaces>
+Architecture: all
+Depends: ${shlibs:Depends}, ${misc:Depends}, openssl, libnet-ftp-perl
+Recommends: ftpipe
+Description: encrypted backup via ftp
+ This is more or less a wrapper around dump(8). It saves the
+ dumped data as encrypted files on a FTP server.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/files	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,1 @@
+ftbackup_0.1-1_all.deb unknown extra
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup.debhelper.log	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,14 @@
+dh_installdirs
+dh_installchangelogs
+dh_installdocs
+dh_installexamples
+dh_installman
+dh_link
+dh_strip
+dh_compress
+dh_fixperms
+dh_installdeb
+dh_shlibdeps
+dh_gencontrol
+dh_md5sums
+dh_builddeb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/DEBIAN/conffiles	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,1 @@
+/etc/ftbackup/default
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/DEBIAN/control	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,12 @@
+Package: ftbackup
+Version: 0.1-1
+Architecture: all
+Maintainer: Heiko Schlittermann <hs@schlittermann.de>
+Installed-Size: 72
+Recommends: ftpipe
+Section: unknown
+Priority: extra
+Homepage: https://keller.schlittermann.de/hg/incubator/ftbackup
+Description: encrypted backup via ftp
+ This is more or less a wrapper around dump(8). It saves the
+ dumped data as encrypted files on a FTP server.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/DEBIAN/md5sums	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,4 @@
+7863cc361a29c8c28337971237a9e0a1  usr/share/doc/ftbackup/changelog.Debian.gz
+f7fe181d07923779d32311dbe6c8c32a  usr/share/doc/ftbackup/copyright
+9387c4b8d486c6b4ec9c9366d04ed3f9  usr/share/doc/ftbackup/README.Debian
+89d8c556a686d6f37ecf97c5452b5704  usr/sbin/ftbackup
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/etc/ftbackup/default	Sat Nov 21 22:03:50 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/usr/sbin/ftbackup	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,389 @@
+#! /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 Time::Local;
+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 = undef;
+my $opt_today = strftime("%F", localtime $NOW);
+my @opt_debug = ();
+my $opt_verbose = 0;
+my $opt_dry = 0;
+my $opt_force = 0;
+my $opt_label = "daily";
+
+sub get_configs(@);
+sub get_candidates();
+sub verbose(@);
+sub iso2epoch($);
+
+our @AT_EXIT;
+END { $_->() foreach @AT_EXIT };
+$SIG{INT} = sub { warn "Got signal INT\n"; exit 1 };
+
+my %CONFIG = (
+    FTP_DIR => "backup/<LABEL>/<NODE>",
+    FTP_PASSIVE => 1,
+    FULL_CYCLE => 7,	    # not used yet
+);
+
+MAIN: {
+    Getopt::Long::Configure("bundling");
+    GetOptions(
+	"l|level=i" => \$opt_level,
+	"L|label=s" => \$opt_label,
+	"d|debug:s" => sub { push @opt_debug, split /,/, $_[1] },
+	"v|verbose" => \$opt_verbose,
+	"dry" => \$opt_dry,
+	"f|force" => \$opt_force,
+	"h|help" => sub { pod2usage(-exit => 0, -verbose => 1) },
+	"m|man" => sub { pod2usage(-exit => 0, -verbose => 3) },
+    ) or pod2usage;
+
+
+    my %cf = (%CONFIG, get_configs(@CONFIGS));
+    $cf{FTP_DIR} =~ s/<NODE>/$NODE/g;
+    $cf{FTP_DIR} =~ s/<LABEL>/$opt_label/g;
+    my @dev = get_candidates();
+    ### %cf
+    ### @dev
+
+    my @errors = ();
+    push @errors, "Need FTP_HOST (see config)." if not defined $cf{FTP_HOST};
+    push @errors, "Need KEY (see config)." if not defined $cf{KEY};
+    die join "\n", @errors, "" if @errors;
+
+    my $ftp = new FTP($cf{FTP_HOST}, 
+	Passive => $cf{FTP_PASSIVE}, 
+	Debug => @opt_debug ~~ /^ftp$/) or die $@;
+    $ftp->login or die $ftp->message;
+    $ftp->home($ftp->try(pwd => ()));
+    $ftp->try(binary => ());
+    $ftp->try(mkpath => $cf{FTP_DIR});    
+    $ftp->try(cwd => $cf{FTP_DIR});
+
+    ### @dev
+
+    # and now we can start doing something with our filesystems
+    DEVICE: foreach my $dev (@dev) {
+	my $dir = $dev->{mountpoint};
+	$dir =~ s/_/__/g;
+	$dir =~ s/\//_/g;
+	$dir = "$cf{FTP_DIR}/$dir";
+
+	$ftp->home();
+	$ftp->try(mkpath => $dir);
+	$ftp->try(cwd => $dir);
+
+	verbose "Now in @{[$ftp->pwd]}.\n";
+
+	# examine the situation and decide about the level
+	# FIXME: currently we simply run a full dump every FULL_CYCLE
+	# days, the intermediate dumps are level 1
+	my @last;
+	foreach (reverse sort $ftp->ls) {
+	    /^(?<date>.*)\.(?<level>\d+)$/;
+	    $last[$+{level}] = $+{date};
+	    last if $+{level} == 0;
+	}
+
+	if (not defined $opt_level) {
+	    $opt_level = 
+		($NOW - iso2epoch $last[0])/86400 > $cf{FULL_CYCLE} ?  0 : 1;
+	}
+
+	my $file = strftime("%F_%R", localtime $NOW)
+	    . ".$opt_level";
+	my $label = "$NODE:" . basename($dev->{rdev});
+	verbose "\tdumping $dev->{dev} as $dev->{rdev} on $dev->{mountpoint} to $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}
+	if ($dev->{lvm}) {
+	    # we can do a snapshot
+	    # FIXME: calculate the size
+	    my $snap = "$dev->{lvm}{path}-0";
+
+	    verbose "Creating snapshot $snap\n";
+	    system($_ = "lvcreate -s -L 1G -n $snap $dev->{lvm}{path} >/dev/null");
+	    die "failed system command: $_\n" if $?;
+
+	    $dev->{cleanup} = sub { system "lvdisplay $snap &>/dev/null"
+				      . " && lvremove -f $snap >/dev/null" };
+	    push @AT_EXIT, $dev->{cleanup};
+
+	    (my $device) = (grep /lv name/i, `lvdisplay $snap`)[0] =~ /(\S+)\s*$/;
+
+	    system($_ = "fsck -f @{[$opt_verbose ? '-C0' : '']} -y $device");
+	    warn "fsck on $device (using: $_) failed\n" if $?;
+
+	    ($dev->{dump}) = $device;
+
+	}
+	else {
+	    $dev->{dump} = $dev->{rdev}
+	}
+
+	### $dev
+
+	$ENV{key} = $cf{KEY};
+	my $dumper = open(my $dump, "-|") or do {
+	    my $head = <<__;
+#! /bin/bash
+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/XXXXX/sprintf "% 5s", "+" . (length($head) +1)/e;
+	    print $head;
+	    exec "dump -$opt_level -L $label -f- -u -z6 $dev->{dump}"
+	    . "| openssl enc -pass env:key -salt -blowfish";
+	    die "Can't exec dumper\n";
+	};
+
+	$ftp->try(put => $dump, $file);
+	$dev->{cleanup}->() if $dev->{cleanup};
+	verbose "Done.\n";
+    }
+
+}
+
+sub verbose(@) {
+    return if not $opt_verbose; 
+    print @_;
+}
+
+sub get_candidates() {
+# return the list of backup candidates
+
+    my @dev;
+
+    # later we need the major of the device mapper
+    my $dev_mapper = 0;
+    $_ = (grep /device.mapper/, slurp("/proc/devices"))[0]
+	and $dev_mapper = (split)[0];
+
+    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)=/) {
+	    # NOTE: dump is able to handle LABEL=... too, but I think
+	    # it's more easy for recovery to know the real device
+	    chomp($rdev = `blkid -c /dev/null -o device -t '$dev'`);
+	}
+	$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) =~ /(.+[^-])-([^-].+)/;
+	    $lvm->{path} = "$lvm->{vg}/$lvm->{lv}";
+	}
+
+	push @dev, {
+	    dev => $dev,
+	    rdev => $rdev,
+	    mountpoint => $mp,
+	    fstype => $fstype,
+	    lvm => $lvm,
+	};
+    }
+
+    return @dev;
+}
+
+sub get_configs(@) {
+    local $_;
+    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*#/ and /=/} <$f>;
+	map { chomp } values %h;
+	%r = (%r, %h);
+    }
+    return %r;
+}
+
+{ package FTP; 
+  use strict;
+  use warnings;
+  use base qw(Net::FTP);
+
+  my %data;
+
+  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);
+  }
+
+  sub home {
+    my $self = shift;
+    return $data{ref $self}{home} = shift if @_;
+    $self->try(cwd => exists $data{ref $self}{home} ? $data{ref $self}{home} : "/");
+    return $self->pwd();
+  }
+
+  sub get_home { return $data{ref shift}{home} };
+}
+
+sub iso2epoch($) {
+    $_[0] =~ /(?<year>\d+)\D(?<mon>\d+)\D(?<mday>\d+)
+	      (?:\D(?<hour>\d\d)\D(?<min>\d\d)(?:\D(?<sec>\d\d))?)?/x;
+    my %iso = ((sec => 0, min => 0, hour => 0), %+);
+    $iso{mon}--;
+    $iso{year} += 1900 if $iso{year} < 100;
+    return timelocal(@iso{qw/sec min hour mday mon year/});
+}
+
+__END__
+
+=head1 NAME
+
+ftbackup - ftp backup tool
+
+=head1 SYNOPSIS
+
+    ftbackup [--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<-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
+level 0 (full) backup. If not specified, it is choosen automagically.
+(default: undef)
+
+=item B<-L>|B<--label> I<label>
+
+The label for the backup. (default: daily)
+
+=item B<-v>|B<--verbose>
+
+Be verbose. (default: no)
+
+=back
+
+=head1 FILES
+
+=head2 Configuration
+
+The config files are searched in the following places:
+
+    /etc/ftbackup
+    ~/.ftbackup
+    ./ftbackup.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.
+
+The config file may contain the following items (listed with their built in defaults)
+
+    KEY		= <no default>
+    FTP_HOST	= <no default>
+    FTP_DIR	= "backup/<LABEL>/<NODE>"
+    FTP_PASSIVE = 1
+    FULL_CYCLE	= 7
+
+=head2 F<.netrc>
+
+You may miss the login information for the FTP server. Currently we rely on a valid
+F<~/.netrc> entry. An example line of the F<~/.netrc>:
+
+    machine ... login ... password ...
+
+=cut
+
+# vim:sts=4 sw=4 aw ai sm:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/usr/share/doc/ftbackup/README.Debian	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,6 @@
+ftbackup for Debian
+-------------------
+
+<possible notes regarding this package - if none, delete this file>
+
+ -- Heiko Schlittermann <hs@schlittermann.de>  Mon, 26 Oct 2009 23:42:43 +0100
Binary file debian/ftbackup/usr/share/doc/ftbackup/changelog.Debian.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/ftbackup/usr/share/doc/ftbackup/copyright	Sat Nov 21 22:03:50 2009 +0100
@@ -0,0 +1,24 @@
+This package was debianized by Heiko Schlittermann <hs@schlittermann.de> on
+Mon, 26 Oct 2009 23:42:43 +0100.
+
+It was downloaded from <url://example.com>
+
+Upstream Author(s):
+
+    <put author's name and email here>
+    <likewise for another author>
+
+Copyright:
+
+    <Copyright (C) YYYY Name OfAuthor>
+    <likewise for another author>
+
+License:
+
+    <Put the license of the package here indented by 4 spaces>
+
+The Debian packaging is (C) 2009, Heiko Schlittermann <hs@schlittermann.de> and
+is licensed under the GPL, see `/usr/share/common-licenses/GPL'.
+
+# Please also look if there are files or directories which have a
+# different copyright/license attached and list them here.
--- a/debian/rules	Wed Oct 28 23:00:02 2009 +0100
+++ b/debian/rules	Sat Nov 21 22:03:50 2009 +0100
@@ -11,14 +11,12 @@
 
 
 
-
-
 configure: configure-stamp
 configure-stamp:
 	dh_testdir
 	# Add here commands to configure the package.
-
-	touch configure-stamp
+	./configure --prefix=/usr
+	touch $@
 
 
 build: build-stamp
@@ -27,7 +25,7 @@
 	dh_testdir
 
 	# Add here commands to compile the package.
-	$(MAKE)
+	chmod +x ftbackup
 	#docbook-to-man debian/ftbackup.sgml > ftbackup.1
 
 	touch $@
@@ -51,6 +49,8 @@
 	# Add here commands to install the package into debian/ftbackup.
 	$(MAKE) DESTDIR=$(CURDIR)/debian/ftbackup install
 
+	install -d $(CURDIR)/debian/ftbackup/etc/ftbackup
+	install -m 0600 ftbackup.conf.example $(CURDIR)/debian/ftbackup/etc/ftbackup/default
 
 # Build architecture-independent files here.
 binary-indep: build install
@@ -78,7 +78,7 @@
 	dh_link
 	dh_strip
 	dh_compress
-	dh_fixperms
+	dh_fixperms -X default
 #	dh_perl
 #	dh_makeshlibs
 	dh_installdeb
--- a/ftbackup	Wed Oct 28 23:00:02 2009 +0100
+++ b/ftbackup	Sat Nov 21 22:03:50 2009 +0100
@@ -68,6 +68,11 @@
     ### %cf
     ### @dev
 
+    my @errors = ();
+    push @errors, "Need FTP_HOST (see config)." if not defined $cf{FTP_HOST};
+    push @errors, "Need KEY (see config)." if not defined $cf{KEY};
+    die join "\n", @errors, "" if @errors;
+
     my $ftp = new FTP($cf{FTP_HOST}, 
 	Passive => $cf{FTP_PASSIVE}, 
 	Debug => @opt_debug ~~ /^ftp$/) or die $@;
@@ -315,6 +320,13 @@
 
     ftbackup [--level <level>] [options]
 
+=head1 DESCRIPTION
+
+The B<ftbackup> tools saves the partitions (file systems) marked in
+F</etc/fstb> to an FTP host. It uses dump(8) for generating the backup
+and openssl(1) for encrypting the data stream (and thus the written
+files).
+
 =head1 OPTIONS
 
 =over
--- a/ftbackup.conf.example	Wed Oct 28 23:00:02 2009 +0100
+++ b/ftbackup.conf.example	Sat Nov 21 22:03:50 2009 +0100
@@ -2,10 +2,10 @@
 # the commented values are the built in defaults
 
 # The encryption key
-KEY = <your key here>
+# KEY = 
 
 # FTP-Server hostname
-FTP_HOST = <your backup server>
+# FTP_HOST = 
 
 # FTP-Server base directory
 # the following expansion work: