bin/imager.save
changeset 26 496ee9b0f488
parent 24 c64604030f4c
child 32 02ef2d1b190a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/imager.save	Fri Jul 29 10:53:14 2011 +0200
@@ -0,0 +1,228 @@
+#! /usr/bin/perl
+
+use 5.010;
+use strict;
+use warnings;
+use POSIX qw(strftime);
+use autodie qw(:all);
+use Digest::MD5 qw(md5_hex);
+use File::Path qw(mkpath);
+use File::Basename;
+use File::Temp;
+use Sys::Hostname;
+use IO::Compress::Gzip qw(gzip $GzipError :level :strategy);
+use Hash::Util qw(lock_keys);
+use Getopt::Long;
+use Pod::Usage;
+
+use constant KiB      => 1024;
+use constant MiB      => 1024 * KiB;
+use constant GiB      => 1024 * MiB;
+use constant NOW      => time();
+use constant DATETIME => strftime("%Y-%m-%dT%H:%M:%SZ" => gmtime(NOW));
+
+sub get_devsize;
+sub get_devname;
+
+$SIG{INT} = sub { die "Got INT\n" };
+
+my %o = (
+    compress  => undef,
+    verbose   => undef,
+    blocksize => 2 * MiB,
+);
+lock_keys(%o);
+
+my $NOW = time();
+
+MAIN: {
+    my ($src, $dst);
+
+    my $idx  = "{DIR}/idx/{HOSTNAME}/{DEVICE}/";
+    my $data = "{DIR}/data";
+    my $size;
+
+    GetOptions(
+        "h|help" => sub { pod2usage(-verbose => 1, exit => 0) },
+        "m|man"  => sub {
+            pod2usage(
+                -verbose   => 2,
+                exit       => 0,
+                -noperldoc => system("perldoc -V >/dev/null 2>&1")
+            );
+        },
+        "z|compress:i" => sub { $o{compress} = $_[1] ? $_[1] : Z_BEST_SPEED },
+        "b|blocksize=s" => sub {
+            given ($_[1]) {
+                when (/(\d+)G/i) { $o{blocksize} = $1 * GiB };
+                when (/(\d+)M/i) { $o{blocksize} = $1 * MiB };
+                when (/(\d+)K/i) { $o{blocksize} = $1 * KiB };
+                when (/^(\d+)$/) { $o{blocksize} = $1 };
+                default {
+                    die "Blocksize $_[1] is incorrect!\n"
+                };
+            }
+        },
+      )
+      and @ARGV == 2
+      or pod2usage;
+    ($src, $dst) = @ARGV;
+
+    foreach ($idx, $data) {
+        s/{DIR}/$dst/g;
+        s/{HOSTNAME}/hostname/eg;
+        s/{DEVICE}/get_devname($src)/eg;
+    }
+    $size = get_devsize($src);
+
+    -d $dst or die "$0: $dst: $!\n";
+    mkpath([$data, $idx]);
+
+    my $index = File::Temp->new(DIR => $idx);
+    print {$index} <<__EOT;
+# imager
+format: 1
+host: @{[hostname]}
+filesystem: $src
+blocksize: $o{blocksize}
+devsize: $size
+timestamp: @{[NOW]}
+datetime: @{[DATETIME]}
+
+__EOT
+
+    open(my $in => $src);
+    binmode($in);
+    local $/ = \(my $bs = $o{blocksize});
+    local $| = 1;
+
+    my %stats = (
+        written => 0,
+        skipped => 0,
+        todo    => 1 + int($size / $o{blocksize}),
+    );
+
+    local $SIG{ALRM} = sub {
+        my $speed = ($stats{written} + $stats{skipped}) / (time - $^T + 1);
+        say sprintf
+"# done %5.1f%% | %24s (%*d of $stats{todo}, written %*d, skipped %*d)",
+          100 * (($stats{written} + $stats{skipped}) / $stats{todo}),
+          ($speed ? (scalar localtime($^T + $stats{todo} / $speed)) : ""),
+          length($stats{todo}) => $stats{written} + $stats{skipped},
+          length($stats{todo}) => $stats{written},
+          length($stats{todo}) => $stats{skipped};
+        alarm(5);
+    };
+    $SIG{ALRM}->();
+
+    while (my $buffer = <$in>) {
+	my ($file, $ext, $cs);
+	$file = $cs = md5_hex($buffer);
+	$file =~ s/(?<fn>(?<prefix>...).*)/$+{prefix}\/$+{fn}/g;
+	$ext = $o{compress} ? ".gz" : "";
+
+        # the extension we do not put into the index
+        my $log = sprintf "%12d %s %s" => ($. - 1), $cs, $file;
+
+        if (not(-e "$data/$file" or -e "$data/$file.gz")) {
+            mkpath dirname("$data/$file.gz");
+            my $out = File::Temp->new(
+                TEMPLATE => ".XXXXXXX",
+                DIR      => dirname("$data/$file")
+            );
+            binmode($out);
+            if ($o{compress}) {
+                gzip(
+                    \$buffer  => $out,
+                    -Minimal  => 1,
+                    -Level    => Z_BEST_SPEED,
+                    -Strategy => Z_FILTERED
+                ) or die $GzipError;
+            }
+            else { print {$out} $buffer }
+            close($out);
+            rename($out => "$data/$file$ext");
+            $log .= " *";
+            $stats{written}++;
+        }
+        else {
+            $log .= "  ";
+            $stats{skipped}++;
+        }
+
+        say {$index} $log;
+    }
+    $SIG{ALRM}->();
+    alarm 0;
+
+    say {$index} "# DONE (runtime " . (time() - $^T) . "s)";
+
+    say "# DONE (runtime " . (time() - $^T) . "s)";
+    say "# WRITTEN $stats{written}, SKIPPED $stats{skipped} blocks";
+    say "# SAVINGS "
+      . sprintf "%3d%%" => 100 *
+      ($stats{skipped} / ($stats{written} + $stats{skipped}));
+
+    rename $index->filename => "$idx/" . DATETIME;
+    close $index;
+
+}
+
+sub get_devsize {
+    my ($devname) = @_;
+    open(my $fh => $devname);
+    seek($fh, 0, 2);
+    return tell($fh);
+}
+
+sub get_devname {
+    my $_ = shift;
+    s/^\/dev\///;
+    s/_/__/g;
+    s/\//_/g;
+    return $_;
+}
+
+__END__
+
+=head1 NAME
+
+    imager.save - create a block device snapshot
+
+=head1 SYNOPSIS
+
+    imager.save [options] {device} {destination}
+
+=head1 DESCRIPTION
+
+This tool creates a snapshot of a blockdevice.
+Just call it like
+
+    imager.save /dev/sda1 /media/backup
+
+This will create F</media/backup/{data,idx}>, if not already existing.
+The index (blocklist) goes to
+I<destination>F</idx/>I<hostname>F</>I<devicename>.  The data goes to
+I<destination>/F<data/>.
+
+=head1 OPTIONS
+
+=over
+
+=item B<-z> [I<level>]|B<--compress>[=I<level>]
+
+Use compression when writing the blocks to disk. (default: off)
+
+=item B<-b> I<blocksize>|B<--blocksize>=I<blocksize>
+
+The blocksize used. (may be suffixed with K, M, G). (default: 2MiB)
+
+=item B<-h>|B<--help>
+
+=item B<-m>|B<--man>
+
+The short and longer help. 
+
+=back
+
+=cut