blob: add06d4fb937914295b1e64d857e90ac6fda8e9f [file] [log] [blame]
#!/usr/bin/perl -w
# Copyright (C) 2012 STRATO. All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public
# License v2 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this program; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 021110-1307, USA.
#
# commands:
#
# mkdir <ino> <name> [mode [owner [group]]]
# mkfile <ino> <name> [size [mode [owner [group]]]]
# mksparse <ino> <name> [size [mode [owner [group]]]]
# mknod <ino> <name> <b|c> <major> <minor> [mode [owner [group]]]
# mksymlink <ino> <name> <relative-src> [owner [group]]
# mkfifo <ino> <name> [mode [owner [group]]]
# link <src> <target>
# rename <src> <dst>
# unlink <name>
# rmdir <name>
# write <name> <offset> <length>
# truncate <name> <length>
# mtime <name>
# atime <name>
# chown <name> <uid>[:<gid>]
# chmod <name> <mod>
# truncate <name> [<length>]
# remount
# snapshot
use strict;
use Getopt::Std;
use POSIX qw(mkfifo);
use Fcntl qw(:seek);
my $btrfs = "/usr/local/bin/btrfs";
$| = 1;
select STDERR;
$| = 1;
select STDOUT;
my %opts = ();
my $allok = getopts("b:p:vz:", \%opts);
if (!$allok || !$opts{b} == !$opts{z} || !$opts{p}) {
die "usage: $0 -p path {-z zfs|-b btrfs}\n";
}
my $path = $opts{p};
my $zfs_name = $opts{z};
my $btrfs_dev = $opts{b};
my @actions = ();
my %inodes = ();
my $actions_i = 0;
while (local $_ = <STDIN>) {
chomp;
s/\s+/ /g;
next if !$_ || $_ eq " " || /^ ?#/;
push @{$actions[$actions_i]}, [$., $_];
if (/^(?:snapshot|remount)$/) {
$actions_i++;
}
}
my @state;
my $ln;
sub get_path($);
sub tempname($) {
my $ino = shift;
die "ino not defined in line $ln\n" unless defined $ino;
return "$path/tempname-$ino";
}
sub mkpath($) {
my $p = shift;
die "path not defined in line $ln\n" unless defined $p;
die "path $p does not begin with / in line $ln" unless $p =~ /^\//;
return "$path$p";
}
sub expand_size($) {
my $size = shift;
my $e = 1;
my ($n, $m) = $size =~ /(\d+)([kKmMgGtTpP])?/;
return $n if (!defined $m);
$e *= 1024;
return $n * $e if ($m =~ /[kK]/);
$e *= 1024;
return $n * $e if ($m =~ /[mM]/);
$e *= 1024;
return $n * $e if ($m =~ /[gG]/);
$e *= 1024;
return $n * $e if ($m =~ /[tT]/);
$e *= 1024;
return $n * $e if ($m =~ /[pP]/);
die "invalid qualifier $m in size in line $ln";
}
if ($zfs_name) {
system("zfs destroy -r $zfs_name");
system("zfs create -o mountpoint=$path $zfs_name");
die "zfs create $zfs_name failed\n" if ($?);
} else {
system("mkfs.btrfs -L refgen-fs $btrfs_dev");
die "mkfs.btrfs $btrfs_dev failed\n" if ($?);
system("mount -o noatime,inode_cache $btrfs_dev $path");
die "mount $btrfs_dev failed\n" if ($?);
}
my $snap = 1;
my %names;
foreach my $a (@actions) {
my @rules;
my @makes;
foreach (@$a) {
my ($ln, $action) = @$_;
my ($a, @param) = split / /, $action;
push @rules, [$ln, $a, @param];
if ($a =~ /^mk/) {
@makes[$param[0]] = [$ln, $a, @param];
}
}
#
# create all files/dirs we need
#
my @deletes;
for my $i (1 ... @makes-1) {
my $r = $makes[$i];
if (defined $r) {
$ln = $r->[0];
my $a = $r->[1];
my $ino = $r->[2];
my $name = tempname($ino);
my $mode = undef;
my $owner;
my $group;
if ($state[$i]->{created}) {
die "ino $ino already exist. Consider ".
"using remount in line $ln\n";
}
print "create $i\n" if $opts{v};
if ($a eq "mkdir") {
$mode = $r->[4] || 0750;
$owner = $r->[5] || "0";
$group = $r->[6] || "0";
mkdir($name)
or die "mkdir($name) failed ".
"in line $ln: $!\n";
} elsif ($a eq "mksymlink") {
my $src = $r->[4];
$owner = $r->[5] || "0";
$group = $r->[6] || "0";
symlink($src, $name)
or die "symlink($src, $name) failed ".
"in line $ln: $!\n";
} elsif ($a eq "mkfifo") {
$mode = $r->[4] || 0750;
$owner = $r->[5] || "0";
$group = $r->[6] || "0";
mkfifo($name, $mode)
or die "mkfifo($name, $mode) failed ".
"in line $ln: $!\n";
} elsif ($a eq "mkfile" || $a eq "mksparse") {
my $buf;
my $size = $r->[4] || 654321;
$mode = $r->[5] || 0640;
$owner = $r->[6] || "0";
$group = $r->[7] || "0";
$size = expand_size($size);
open(FH, ">$name")
or die "creating file $name ".
"failed in line $ln: $!\n";
if ($a eq "mkfile") {
if ($size > 104857600) {
die "size > 100M in line $ln\n";
}
open(R, "</dev/urandom")
or die "open /dev/urandom ".
"failed: $!\n";
sysread(R, $buf, $size)
or die "read from /dev/urandom".
" failed: $!\n";
close(R)
or die "close /dev/urandom ".
"failed: $!\n";
syswrite(FH, $buf)
or die "writing to file $name ".
"failed in line ".
"$ln: $!\n";
} else {
seek(FH, $size, SEEK_SET)
or die "failed to seek to ".
"$size in $name at ".
"line $ln: $!\n";
truncate(FH, $size)
or die "failed to truncate to ".
"$size in $name at ".
"line $ln: $!\n";
}
close(FH)
or die "closing file $name ".
"failed in line $ln: $!\n";
} elsif ($a eq "mknod") {
my $buf;
my $type = $r->[4];
my $major = $r->[5];
my $minor = $r->[6];
$mode = $r->[7] || 0640;
$owner = $r->[8] || "0";
$group = $r->[9] || "0";
system("mknod $name $type $major $minor");
die "mknod $name $type $major $minor failed ".
"in line $ln\n" if ($?);
} else {
die "invalid action $a in line $ln\n";
}
if (defined $mode) {
chmod($mode, $name)
or die sprintf("chmod(0%o, %s) failed ".
"in line $ln: $!\n",
$mode, $name);
}
system("chown", "-h", "$owner:$group", $name);
die "chown($owner, $group, $name) failed ".
"in line $ln\n" if $?;
$state[$i]->{created} = 1;
$state[$i]->{links} = 1;
$state[$i]->{tempname} = $name;
my $fs_ino = (lstat $name)[1];
if (!exists $inodes{$ino}) {
$inodes{$ino} = $fs_ino;
} elsif ($fs_ino != $inodes{$ino}) {
die "fatal error: inode $ino became inode ".
"$inodes{$ino} earlier, while now ".
"(line $ln) it is $fs_ino\n";
}
} elsif (!defined $state[$i]->{created}) {
my $name = tempname($i);
print "temp create $i\n" if $opts{v};
mkdir($name)
or die "creating temp dir $name failed: $!\n";
push @deletes, $name;
}
}
foreach my $name (@deletes) {
print "temp delete $name\n" if $opts{v};
rmdir $name;
}
#
# replay actions
#
foreach my $r (@rules) {
$ln = $r->[0];
my $a = $r->[1];
if ($a =~ /^mk/) {
my $ino = $r->[2];
my $p = mkpath($r->[3]);
print "move $ino to $p\n" if $opts{v};
rename($state[$ino]->{tempname}, $p)
or die "move $ino to $p failed ".
"in line $ln: $!\n";
delete $state[$ino]->{tempname};
$state[$ino]->{names}->{$p} = 1;
$names{$p} = $ino;
} elsif ($a eq "link") {
my $source = mkpath($r->[2]);
my $target = mkpath($r->[3]);
my $ino = $names{$source};
die "source $source not found in line $ln\n"
unless $ino;
print "link $source to $target\n" if $opts{v};
link($source, $target)
or die "link($source, $target) failed ".
"in line $ln\n";
$state[$ino]->{names}->{$target} = 1;
++$state[$ino]->{links};
$names{$target} = $ino;
} elsif ($a eq "rename") {
my $src = mkpath($r->[2]);
my $dst = mkpath($r->[3]);
my $ino = $names{$src};
die "source $src not found in line $ln\n" unless $ino;
print "move $src to $dst\n" if $opts{v};
foreach my $n (sort keys %names) {
if ($n =~ /^$src(\/.*)/) {
my $new = $dst.$1;
my $ino = $names{$n};
print "rewrite $n to $new, ino $ino\n"
if $opts{v};
delete $state[$ino]->{names}->{$n};
$state[$ino]->{names}->{$new} = 1;
delete $names{$n};
$names{$new} = $ino;
}
delete $state[$ino]->{names}->{$src};
$state[$ino]->{names}->{$dst} = 1;
delete $names{$src};
$names{$dst} = $ino;
}
rename($src, $dst)
or die "rename $src to $dst failed ".
"in line $ln: $!\n";
} elsif ($a eq "unlink") {
my $p = mkpath($r->[2]);
my $ino = $names{$p};
die "source $p not found in line $ln\n" unless $ino;
print "unlink $p\n" if $opts{v};
unlink($p)
or die "unlink $p failed in line $ln: $!\n";
delete $names{$p};
delete $state[$ino]->{names}->{$p};
if (--$state[$ino]->{links} == 0) {
print "last ref to ino $ino\n" if $opts{v};
delete $state[$ino];
}
} elsif ($a eq "rmdir") {
my $p = mkpath($r->[2]);
my $ino = $names{$p};
die "source $p not found in line $ln\n" unless $ino;
print "rmdir $p\n" if $opts{v};
rmdir($p)
or die "rmdir $p failed in line $ln: $!\n";
delete $names{$p};
delete $state[$ino]->{names}->{$p};
if (--$state[$ino]->{links} == 0) {
print "last ref to ino $ino\n";
delete $state[$ino];
}
} elsif ($a eq "write") {
my $buf;
my $p = mkpath($r->[2]);
my $off = $r->[3];
my $len = $r->[4];
$len = expand_size($len);
$off = expand_size($off);
open(R, "</dev/urandom")
or die "open /dev/urandom failed: $!\n";
sysread(R, $buf, $len)
or die "read from /dev/urandom failed: $!\n";
close(R)
or die "close /dev/urandom failed: $!\n";
open(FH, "+<$p")
or die "open $p for writing failed ".
"in line $ln: $!\n";
seek(FH, $off, 0)
or die "seek($p, $off) failed ".
"in line $ln: $!\n";
syswrite(FH, $buf)
or die "writing to file $p failed ".
"in line $ln: $!\n";
close(FH)
or die "close of $p failed in line $ln: $!\n";
} elsif ($a eq "truncate") {
my $p = mkpath($r->[2]);
my $size = $r->[3];
$size = expand_size($size);
truncate($p, $size)
or die "truncate $p to $size failed ".
"in line $ln: $!\n";
} elsif ($a eq "chmod") {
my $p = mkpath($r->[2]);
my $mode = $r->[3];
chmod(oct($mode), $p)
or die "chmod $p to $mode failed ".
"in line $ln: $!\n";
} elsif ($a eq "chown") {
my $p = mkpath($r->[2]);
my ($uid, $gid) = (split(/:/, $r->[3]), 0);
system("chown", "-h", "$uid:$gid", $p);
die "chown $p to $uid:$gid failed ".
"in line $ln\n" if $?;
} elsif ($a eq "atime") {
my $p = mkpath($r->[2]);
qx{touch -a $p};
if ($?) {
die "atime update $p failed in line $ln\n";
}
} elsif ($a eq "mtime") {
my $p = mkpath($r->[2]);
qx{touch -m $p};
if ($?) {
die "mtime update $p failed in line $ln\n";
}
} elsif ($a eq "truncate") {
my $p = mkpath($r->[2]);
my $size = $r->[3] ? $r->[3] : 0;
truncate($p, $size)
or die "truncate $p to $size failed ".
"in line $ln: $!\n";
} elsif ($a eq "remount") {
# ignore
} elsif ($a eq "snapshot") {
print "create snapshot $snap\n" if $opts{v};
if ($zfs_name) {
system("zfs snapshot $zfs_name\@$snap");
die "creating snapshot $zfs_name\@$snap ".
"failed\n" if ($?);
} else {
system("$btrfs subvol snap -r $path ".
"$path/\@$snap");
die "creating snapshot $path\@$snap failed\n"
if ($?);
}
++$snap;
} else {
die "invalid action $a in line $ln\n";
}
}
#
# umount/mount
#
if ($zfs_name) {
system("zfs umount $zfs_name");
die "zfs umount $zfs_name failed\n" if ($?);
system("zfs mount $zfs_name");
die "zfs mount $zfs_name failed\n" if ($?);
}
}