#!/usr/bin/perl -w

# $Id: myufsbackup 7 2009-12-27 21:51:03Z fjo@ogris.net $

# modules
use strict;
use warnings;
use Getopt::Std;
use DBI;

# default options
my %opts = (
	c => "/usr/local/etc/myufsbackup.conf"
);

# get command line options
$Getopt::Std::STANDARD_HELP_VERSION = 1;
if (!getopts("c:fuv", \%opts)) {
	print STDERR "use 'myufsbackup --help' to see all options\n";
	exit(255);
}

# default config
my %cfg = (
	dbhost => "",
	dbport => 3306,
	dbsock => "/tmp/mysql.sock",
	dbuser => "root",
	dbpass => "",
	backupdir => "/var",
	snapdir => ".snap",
	snapname => "myufsbackup",
	mountdir => "/backup",
	mdunit => 2,
	extraflush => 1,
	mount => "/sbin/mount",
	mdconfig => "/sbin/mdconfig",
	umount => "/sbin/umount",
);

if (-r $opts{c}) {
	# load config file
	my $lineno = 0;
	open(FH, $opts{c}) || die "$opts{c}: $!";
	while (<FH>) {
		$lineno++;

		next if /^#/;
		next if /^\s*$/;
		s/\r|\n$//g;

		die "$opts{c}: line $lineno is invalid"
			if !/^([^\s]+)\s+=\s+(.*)$/;
		my ($key, $value) = ($1, $2);

		die "$opts{c}: line $lineno: unknown option '$key'"
			if !exists $cfg{$key};

		$cfg{$key} = $value;
	}
	close(FH);
	print "loaded $opts{c}\n" if $opts{v};
}

# check config
die "need either dbhost or dbsock\n" if (!$cfg{dbhost} && !$cfg{dbsock});
die "need dbport\n" if ($cfg{dbhost} && !defined $cfg{dbport});
die "need mdunit\n" if !defined $cfg{mdunit};
foreach (qw(dbuser backupdir snapdir snapname mountdir mount mdconfig
		umount)) {
	die "need $_\n" if !$cfg{$_};
}
foreach (qw(backupdir mountdir)) {
	die "$_ '$cfg{$_}' invalid: need absolute path\n"
		if (substr($cfg{$_}, 0, 1) ne "/");
}
foreach (qw(snapdir snapname)) {
	die "$_ '$cfg{$_}' invalid: need relative path\n"
		if ($cfg{$_} =~ /\//);
}

if ($opts{u}) {
	# cleanup mode: unmount snapshot, remove memory disk, remove snapshot
	&execute($cfg{umount}, $cfg{mountdir});
	print "unmounted $cfg{mountdir}\n" if $opts{v};

	&execute($cfg{mdconfig}, "-d", "-u", $cfg{mdunit});
	print "removed memory disk #$cfg{mdunit}\n" if $opts{v};

	my $dir = "$cfg{backupdir}/$cfg{snapdir}/$cfg{snapname}";
	$dir =~ s/\/+/\//g;
	die "cannot unlink $dir: $!" if ((unlink($dir) != 1) && !$opts{f});
	print "removed $dir\n" if $opts{v};
} else {
	# backup mode: make snap dir, flush+lock mysql, create snapshot,
        #              release mysql lock, mount snapshot
	my $dsn = "DBI:mysql:";
	$dsn .= "mysql_sock=$cfg{dbsock}" if $cfg{dbsock};
	$dsn .= "host=$cfg{dbhost};port=$cfg{dbport}" if $cfg{dbhost};

	my $dbh = DBI->connect($dsn, $cfg{dbuser}, $cfg{dbpass},
		{ RaiseError => 1 });
	print "connected to MySQL server\n" if $opts{v};

	my $dir = "$cfg{backupdir}/$cfg{snapdir}";
	$dir =~ s/\/+/\//g;
	&mkdirhier($dir);
	print "created directory $dir\n" if $opts{v};

	$dbh->do("FLUSH TABLES") if $opts{extraflush};
	print "flushed MySQL tables\n" if $opts{v};
	
	$dbh->do("FLUSH TABLES WITH READ LOCK");
	print "flushed and locked MySQL tables\n" if $opts{v};
	
	$dir .= "/$cfg{snapname}";
	&execute($cfg{mount}, "-u", "-o", "snapshot", $dir, $cfg{backupdir});
	print "created snapshot of $cfg{backupdir} on $dir\n" if $opts{v};

	$dbh->disconnect();
	print "disconnected from MySQL server\n" if $opts{v};

	&execute($cfg{mdconfig}, "-a", "-t", "vnode", "-u", $cfg{mdunit},
		"-f", $dir);
	print "attached memory disk #$cfg{mdunit} for $dir\n" if $opts{v};

	&mkdirhier($cfg{mountdir});
	&execute($cfg{mount}, "-o", "ro", "/dev/md$cfg{mdunit}",
		$cfg{mountdir});
	print "mounted memory disk #$cfg{mdunit} on $cfg{mountdir}\n"
		if $opts{v};
}

# make directory hierarchy, die on error
sub mkdirhier() {
	my ($dir) = @_;
	my $path = "";
	foreach (split("/", $dir)) {
		next if !$_;
		$path .= "/$_";
		next if -d $path;
		die "cannot create directory $path: $!"
			if (!mkdir($path, 0755) && !$opts{f});
	}
}

# execute shell command, die on error
sub execute() {
	my $cmd = join(" ", map { "'$_'" } @_);
	`$cmd`;
	die "command failed: $cmd\n" if ($? && !$opts{f});
}

# print version
sub VERSION_MESSAGE() {
	print "myufsbackup V0.1\n";
}

# show usage
sub HELP_MESSAGE() {
	print <<EOF;

usage: myufsbackup [-c <configfile>] [-f] [-u] [-v]
       myufsbackup [--help]
       myufsbackup [--version]

    -c <configfile>    use given configfile instead of
                       /usr/local/etc/myufsbackup.conf
    -f                 force: do not die on error
    -u                 unmount: detach snapshot instead of creating it
    -v                 verbose mode: print out what is done
    --help             this help ;-)
    --version          print version

EOF
}
