#!/usr/bin/perl -w

use strict; 
use POSIX qw(ceil);

my $target = "/target"; 
my @datadirs = qw(var home); 
my $defaultmirror = 'http://ftp.debian.nl/debian'; 
my $defaultlocaltime = "Europe/Amsterdam";

my @packages = qw(
    less wget w3m vim libncurses5-dev make gcc mbr bzip2 mdadm ssh finger
);
my @failpackages = qw(
    lilo
);

BEGIN { 
    print <<END;

raidthingy.pl version 9, released 2009-01-20.

Welcome to this installer thingy. Using the procedure described at
http://juerd.nl/debianraid, this tool installs a Debian GNU/Linux system on
RAID 1, 5, or 6.

Before using this tool, you must evaluate it (by reading its source code) and
determine if it is suitable for your intended application. The author accepts
no responsibility. This tool was downloaded from an insecure site and may just
as well be a trojan horse, so read the source! If you don't understand the
source code, then this program is not for you.

In other words: your computer may explode, and it is not my fault.

IF YOU HAVE READ THE SOURCE CODE AND REALLY WANT TO CONTINUE, type this line
followed by enter:

    Yeah, whatever.

Otherwise, just hit enter to abort.

END

    while (print("Destroy data? "), defined($_ = <>)) { 
        /^$/ and warn("Aborted.\n")  and exit;
        /^Yeah, whatever\.?$/i and last;
        warn "That's not the correct line.\n"; 
    } 
}

sub SYSTEM { 
    my @copy = @_; 
    s/\bmissing\d\b/missing/ for @copy;  # XXX - ugly!
    TRY: {
        printf "Executing: %s\n", map "[$_]", @copy;
        system @copy;
        if ($?) { 
            warn "Command failed, \$? == $?\n";
            print "What do you want to do? ('abort', 'retry', 'ignore')";
            for (;;) {
                print 'Now what? ';
                my $i = <>; 
                $i =~ /^abort$/ and die "Aborted.\n"; 
                $i =~ /^retry$/ and redo TRY;
                $i =~ /^ignore$/ and last;
            }
        }
    }
}

SANITY_CHECKS: {
    # XXX - need many more!
    $< == 0 or die "Root access required...\n";

    -d $target and die "$target exists, aborted.\n";

    `which $_` or die "'$_' is needed, but couldn't be found!\n"
	for qw(modprobe mdadm debootstrap);

    SYSTEM "modprobe md";
    SYSTEM "modprobe raid1";
    SYSTEM "modprobe raid5";
    SYSTEM "modprobe raid6";

    if (not -e "/dev/md0") {
        SYSTEM "mknod /dev/md$_ b 9 $_" for 0..3;
    }
}

my @autodetected = do {
    my $lookingfor;
    my @d;
    for (glob "/sys/block/sd*") {
        next if not -e "$_/size";
        my $s = 0 + `cat $_/size`;
        next if not $s;
        $lookingfor ||= $s;
        push @d, /(\w+)$/ if $s == $lookingfor;
    }
    @d;
};

my $autodetected = @autodetected 
    ? "\n\nIf you want to use \"@autodetected\", just press enter."
    : "";

print <<END;

Which drives do you wish to use? Enter 1 or 2 drives for RAID 1, 3+ for RAID 5,
4+ for RAID 6. Do note that this tool only functions correctly if the drives
are identical, at least 40 GB, and empty. (80 GB or larger is recommended.)

If you enter only one drive, a 2 drive array with the second drive "missing"
will be assumed. You can add it later.

The drives you enter must be valid devices in /dev. If you want to use sda and
sdb, enter "sda sdb".$autodetected
END

my @drives;

while (not @drives) {
    print 'Drives: ';
    @drives = map "/dev/$_", split ' ', <>;
    @drives = map "/dev/$_", @autodetected if not @drives;
    for (my @copy = @drives) {
        if (not -w) {
            warn "$0: $_: $!\n";
            @drives = ();
        }
    }
}

my $raid;

if (@drives > 2) {
    my $r6a = @drives < 4 ? "" : " or RAID 6";
    my $r6b = @drives < 4 ? "" : "RAID 6 - Striping with double parity: " .
                                 "any 2 drives may fail.\n";
    my $r1 = @drives - 1;

    print <<END;

Do you want RAID 1 or RAID 5$r6a?

RAID 1 - Mirroring: any $r1 drives may fail.
RAID 5 - Striping with parity: any 1 drive may fail.
$r6b
END

    while (not $raid) {
        print 'RAID level? ';
        my $i = <>;
        $i =~ s/^raid//i;
        $i =~ /^([15])$/ and $raid = $1;
        $i =~ /^(6)$/ and $r6a and $raid = $1;
    }
}
$raid ||= 1;

print <<END;

It's usually a good idea to have a small root partition and have a separate
data space. Do you want a root filesystem that doesn't occupy the entire drive,
and a separate data storage space that fills the rest?

yes   - Create a /data and bind both /home and /var to subdirs there.
no    - Use the entire free space for one big root filesystem.
empty - Create the md array, but don't use it just yet (Hint: xen+lvm!).

Enter "yes", "no", or "empty".
END

my $data;

while (not defined $data) {
    print 'Separate data store? ';
    my $i = <>;
    $i =~ /^yes$/   and $data = "yes";
    $i =~ /^no$/    and $data = "";
    $i =~ /^empty$/ and $data = "empty";
}

my $rootsize;

if ($data) {
    print <<END;

How large should the root filesystem be? Enter an integer, or leave this
one empty to use the default of 25 GB.
END

    while (not defined $rootsize) {
        print 'Root size in GB? ';
        my $i = <>;
        $i =~ /^(\d+)$/ and $rootsize = $1;
        $i =~ /^$/    and $rootsize = 25;
    }

    $rootsize = ceil($rootsize / (@drives - 1)) if $raid == 5;
    $rootsize = ceil($rootsize / (@drives - 2)) if $raid == 6;
}

print <<END;

Pick a debian mirror near you from http://www.debian.org/mirror/list, and enter
the full URI here. Make sure you include "http://" or "ftp://" and a trailing
"/debian". If you enter nothing, $defaultmirror will be used.
END

print 'Mirror: ';

my $mirror = <>;
chomp $mirror;
$mirror ||= $defaultmirror;

print <<END;

What is your localtime? Pick one from /usr/share/zoneinfo/

Examples: "CET", "Europe/Amsterdam".

DO NOT GUESS, but enter an actual known file!

The default is $defaultlocaltime, so if you want that, just press enter.
END

print 'Timezone: ';

my $localtime = <>;
chomp $localtime;
$localtime ||= $defaultlocaltime;

print <<END;

We'll be bootstrapping using lenny, but if you want, we can upgrade to sid
("unstable") after that. Do you want a bleading edge system?

Enter "yes" or "no".
END

my $dist;

while (not $dist) {
    print 'Sid? ';
    my $i = <>;
    $i =~ /^yes$/ and $dist = "sid";
    $i =~ /^no$/  and $dist = "lenny";
}

print <<END;

Think of a cute name for this machine (the hostname). Some people like to use a
fully qualified name here, others exclude the domain part. Debian suggests you
use only the hostname, so "foo" instead of "foo.bar". There is no default for
this option.
END

my $hostname;

while (not $hostname) {
    print 'Hostname: ';
    $hostname = <>;
    chomp $hostname;
}

print <<END;

LAST CHANCE TO EXIT BEFORE EXISTING DATA IS DESTROYED.
Hit enter to continue or ^C to abort.
END

<>;

SYSTEM "dd if=/dev/zero of=$_ count=1 bs=512" for @drives;


my $rootpart = "";
$rootpart = "new logical default +${rootsize}G" if $rootsize;
my $p6 = $rootsize ? "type 6 FD" : "";

my $fdisk = <<"END";
new primary 1 default +64M
new primary 2 default +1G
new extended 3 default default
$rootpart
new logical default default
activate 1
type 1 FD
type 2 FD
type 5 FD
$p6
write
END

$fdisk =~ s/\bdefault\b/\0/g;
$fdisk =~ s/([a-z])\w+/$1/g;

# Stupid fdisk won't work with fast pipes...

#$fdisk = join '', map "$_\\r", split ' ', $fdisk;
#$fdisk =~ tr/\0//d;
#SYSTEM "echo '$fdisk' | fdisk $_" for @drives;


for (@drives) {
    print "Executing slowly: [fdisk $_]\n";
    open my $fdiskpipe, "|fdisk $_" or die $!;
    my $o = select $fdiskpipe;
    $|++;
    select $o;
    for (split ' ', $fdisk) {
        select(undef, undef, undef, 0.3);
        my $c = $_;
        $c =~ s/\0//g;
        print "$_\n";
        print $fdiskpipe "$_\n";
    }
    close $fdiskpipe;
    $? and die "fdisk failed, \$? == $?.\n"   
}


{
    my @drives = @drives == 1 ? (@drives, 'missing') : @drives;

    my $ndrives = @drives;

    SYSTEM "mdadm --create /dev/md0 -e 0.90 -n $ndrives -l 1 ". "@{[ map $_ . 1, @drives ]}";

    SYSTEM "mdadm --create /dev/md1 -e 0.90 -n $ndrives -l $raid @{[ map $_ . 2, @drives ]}";
    SYSTEM "mdadm --create /dev/md2 -e 0.90 -n $ndrives -l $raid @{[ map $_ . 5, @drives ]}";
    SYSTEM "mdadm --create /dev/md3 -e 0.90 -n $ndrives -l $raid @{[ map $_ . 6, @drives ]}"
        if $data;
}

SYSTEM "mkfs.ext3 /dev/md0";
SYSTEM "mkswap    /dev/md1";
SYSTEM "mkfs.ext3 /dev/md2";

SYSTEM "mkdir $target";
SYSTEM "mount /dev/md2 $target";

SYSTEM "mkdir $target/boot";
SYSTEM "mount /dev/md0 $target/boot";

if ($data eq "yes") {
    SYSTEM "mkfs.ext3 /dev/md3 -O dir_index" if $data eq "yes";
    SYSTEM "mkdir $target/data";
    SYSTEM "mount /dev/md3 $target/data";

    for (@datadirs) {
        SYSTEM "mkdir $target/data/$_";
        SYSTEM "mkdir $target/$_";
        SYSTEM "mount --bind $target/data/$_ $target/$_";
    }
}

SYSTEM "debootstrap etch $target $mirror";

SYSTEM "cd $target/etc; rm -f hostname resolv.conf localtime";

SYSTEM "cp /etc/resolv.conf $target/etc/resolv.conf";
SYSTEM "ln -s /usr/share/zoneinfo/$localtime $target/etc/localtime";
SYSTEM "echo $hostname > $target/etc/hostname";
SYSTEM "perl -i.default -pe's/FSCKFIX=no/FSCKFIX=yes/' $target/etc/default/rcS";

my $datafs = "";
$datafs = "/dev/md3   /data ext3 defaults,errors=remount-ro 0 1" if $data eq "yes";

my $fstab = <<END;
/dev/md0   /boot ext3 defaults 0 1
/dev/md1   none  swap swap
/dev/md2   /     ext3 defaults,errors=remount-ro 0 1
$datafs
proc       /proc proc
END

if ($data eq "yes") {
    $fstab .= "/data/$_ /$_ bind bind\n" for @datadirs;
}

SYSTEM "echo '$fstab' > $target/etc/fstab";

my $sources = <<END;
deb $mirror $dist main contrib non-free
deb-src $mirror $dist main contrib non-free
deb http://security.debian.org/ etch/updates main
END

SYSTEM "echo '$sources' > $target/etc/apt/sources.list";

my $lilo = <<END;
boot=/dev/md0
root=/dev/md2
compact
lba32
read-only

image=/vmlinuz
initrd=/initrd.img
label=Linux
END

SYSTEM "echo '$lilo' > $target/etc/lilo.conf";

my $interfaces = <<END;
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
# or:
# iface eth0 inet static
#     address 1.2.3.4
#     netmask 255.255.255.0
#     gateway 1.2.3.1
END

SYSTEM "echo '$interfaces' > $target/etc/network/interfaces";

print "--> chrooting to $target <--\n";
chroot $target or die $!;
$ENV{HOSTNAME} = $hostname;

SYSTEM "mount -t proc none /proc";
SYSTEM "shadowconfig on";

chdir '/dev';
SYSTEM "MAKEDEV generic";

if ($dist eq 'sid') {
	SYSTEM "apt-get update";
	SYSTEM "apt-get -y --force-yes install dpkg apt";
	SYSTEM "apt-get -y --allow-unauthenticated install gnupg debian-keyring";
	SYSTEM "gpg --list-keys";  # init ~/.gnupg
	SYSTEM "echo keyring /usr/share/keyrings/debian-keyring.gpg >> ~/.gnupg/gpg.conf";
}
SYSTEM "apt-get update";  # Yes, a second time.

SYSTEM "apt-get -y dist-upgrade";
SYSTEM "apt-get -y install @packages";

print "Executing carelessly: [apt-get -y install @failpackages]\n";
system "apt-get -y install @failpackages";  # lilo may fail at this point



SYSTEM "install-mbr $_" for @drives;


print <<END;

Almost done... Set a root password:
END

SYSTEM "passwd";

my $kvtries = 0;
my $kv;
{
    $kv = join "", grep /latest stable/, `finger \@kernel.org | sed -ne 2p`;
    sleep 1, redo if !$kv and $kvtries++ < 5;
}
$kv &&= "\n\n$kv\n";

print <<END;

Here ends the automated part of the installation. In order to be able to boot
your new system, you will need to install a kernel. You're on your own for
that. This is step 15 at http://juerd.nl/debianraid.$kv

Don't forget to compile in RAID 1 and 5 support!! 

After you've installed your new kernel as /boot/vmlinuz, run lilo.

If you need to configure your network for something other than DHCP, edit
/etc/network/interfaces.

When you're all done, exit the chrooted shell and reboot. Good luck!

END

chdir '/usr/src';
SYSTEM "bash";

print "\nYou're no longer chrooted now.\n";
