#!/usr/bin/perl -w
#
# (c) 2007, Bernhard Walle <bernhard.walle@gmx.de>
#
# This script is free software; you can redistribute it and/or modify it under
# the same terms as Perl itself.
#
use strict;

use English;
use POSIX;
use Term::ReadLine;
use Linux::Bootloader;
use Linux::Bootloader::Detect;
use File::Basename;
use Getopt::Long;

##
# globals
##
my $VERSION = "0.1.0";
my %configfiles = (
    'grub'          => '/boot/grub/menu.lst',
    'lilo'          => '/etc/lilo.conf',
    'elilo'         => '/etc/elilo.conf'
);
my %options = (
    'list_only'     => 0,
    'batch'         => 0,
    'execute'       => -1,
    'number'        => -1,
    'help'          => 0
);
my $term = new Term::ReadLine 'kexec-bootloader';
my $bootloadertype;

##
# Checks if the script runs on a supported architecture and exits if not
##
sub check_arch
{
    my $arch;

    $arch = Linux::Bootloader::Detect::detect_architecture('linux');
    if ($arch ne 'i386' && $arch ne 'x86_64' && $arch ne 'ia64') {
        print "This script is only designed for i386, x86_64 and ia64.\n";
        exit 1;
    }
}

##
# Checks if the script runs as root and exits if not.
##
sub check_user
{
    if ($UID != 0) {
        print "This script must be started as root\n";
        exit 1;
    }
}

##
# Prints help
##
sub print_help
{
    print "kexec-bootloader $VERSION\n";
    print "Reads bootloader configuration and uses kexec to boot the kernel\n";
    print "\n";
    print "Options:\n";
    print "    -h | --help        Prints this help message\n";
    print "    -n # | --number #  Chooses entry # without asking\n";
    print "    -e | --execute     Not only load the kernel, also run it\n";
    print "    -l | --list        Lists the possible numbers for -n\n";
}

##
# Executes the given program, checks the return value of a process 
# and if it's non-zero, exits this program.
##
sub system_check_retval_exit
{
    my $program = shift;

    my $retval = system($program);
    if (POSIX::WEXITSTATUS($retval) != 0) {
        exit POSIX::WEXITSTATUS($retval);
    }
}

##
# Shows the list of possible entries
##
sub entries_show
{
    my @entries = (@_);
    my $entry;
    my $number = 0;

    foreach $entry (@entries) {
        printf " (%2d) %s\n", $number++, $entry->{'title'};
    }
}

##
# Shows the entries in a list. The list consists of hash references.
# Let the user choose one entry and return a reference to that entry.
# Returns undef if the user made an invalid choice.
##
sub entries_choose
{
    my @entries = (@_);
    my $input;
    my $num;

    print "\n";
    $input = $term->readline('Choice ');
    unless ($input =~ m/^\d+$/) {
        return undef;
    }
    $num = int($input);

    if ($num < 0 || $num > $#entries) {
        return undef;
    }

    return $entries[$num];
}

##
# Asks if the kexec should be executed.
##
sub ask_exec
{
    my $answer;
    
    print "Do you want to execute kexec now?\n";
    $answer = $term->readline('y/N ');
    chomp $answer;

    return ($answer eq 'j' || $answer eq 'y');
}

##
# Reads all entries. Returns a list of hash references. Expects a
# bootloader object as parameter.
##
sub read_entries
{
    my $bootloader = shift;
    my @entries = ();
    my $current_number = 0;

    while (1) {
        # create a new object each time because of a bug that
        # doesn't re-initialise the array
        my $bootloader = create_bootloader_and_read_config();
        my $current_entry = $bootloader->read_entry($current_number);
        my %entry = ();
        $current_number++;
        if (not defined $current_entry) {
            last;
        }

        %entry = %{$current_entry};
        if ($entry{'kernel'}) {
            push @entries, \%entry;
        }
    }

    return @entries;
}

##
# Finds the bootloader type of the system
##
sub find_loader_type
{
    my @bootloaders;
    my $bootloader;
    my $lt;
    
    # detect bootloader
    @bootloaders = Linux::Bootloader::Detect::detect_bootloader_from_conf();

    if ($#bootloaders < 0) {
        print "No bootloader found on this system\n";
        exit 1;
    } 

    # use always the first bootloader, but print a warning
    # if there's more than one
    $lt = $bootloaders[0];
    if ($#bootloaders > 0) {
        print "WARNING: More than one bootloader found on this system\n";
        print "         Using $lt\n";
    }

    return $lt;
}

##
# Creates a bootloader object and reads the configuration.
sub create_bootloader_and_read_config
{
    my $bootloader;

    $bootloader = new Linux::Bootloader();
    $bootloader->read($configfiles{$bootloadertype});

    return $bootloader;
}

##
# Returns the kernel of the entry
##
sub get_file
{
    my $entry = shift;
    my $filename = shift;
    my $error = shift;
    my $file = $entry->{$filename};

    # check for the filename file directly
    if (-f $file) {
        return $file;
    }

    # if it doesn't exist, look in /boot
    $file = "/boot/" . basename($file);
    if (-f $file) {
        return $file;
    }

    if ($error) {
        print "The specified $filename $file doesn't exist\n";
    }

    return undef;
}

##
# Returns the command line for kexec
##
sub get_kexec_cmdline
{
    my $entry = shift;
    my $kernel;
    my $initrd;
    my $cmdline;
    my $append;
    
    $kernel = get_file($entry, 'kernel', 1);
    if (not defined $kernel) {
        return undef;
    }

    # having no initrd is ok, just treat is at static kernel image
    $initrd = get_file($entry, 'initrd', 0);

    # get the append line
    $append = $entry->{'args'};
    if (not defined $append) {
        print "No append line found\n";
        return undef;
    }

    $cmdline = "kexec";

    # add the kernel image
    $cmdline .= " -l \"$kernel\"";

    # add the initrd image if necessary
    if (defined $initrd) {
        $cmdline .= " --initrd=\"$initrd\"";
    }

    # add the command line
    $cmdline .= " --append='$append'";

    return $cmdline;
}

##
# The MAIN program
##

my $result = GetOptions(
    "h|help"        => \$options{'help'},
    "l|list"        => \$options{'list_only'},
    "n|number=i"    => \$options{'number'},
    "e|execute"     => \$options{'execute'}
);
if ($result == 0) {
    exit 1;
}

# help?
if ($options{'help'}) {
    print_help();
    exit 0;
}

# set batch
if ($options{'number'} >= 0 || $options{'execute'} >= 0) {
    $options{'batch'} = 1;
}

# check if we're running as root
check_arch();
check_user();

$bootloadertype = find_loader_type();
my @all_entries = read_entries();

if ($options{'batch'} <= 0) {
    if (not $options{'list_only'}) {
        print "Choose an entry:\n\n";
    }
    entries_show(@all_entries);
}
if ($options{'list_only'}) {
    exit 0;
}

my $used_entry;
if ($options{'number'} >= 0) {
    $used_entry = $all_entries[$options{'number'}];
} else {
    if ($options{'batch'} <= 0) {
        $used_entry = entries_choose(@all_entries);
    }
}

if (!(defined $used_entry) && !($options{'execute'} > 0)) {
    print "Invalid number\n";
    exit 1;
}

if (defined $used_entry) {
    my $kexec = get_kexec_cmdline($used_entry);
    if (not defined $kexec) {
        exit 1;
    }

    system_check_retval_exit($kexec);
}


if (not $options{'batch'}) {
    if (ask_exec()) {
        system_check_retval_exit("kexec -e");
    }
} elsif ($options{'execute'} == 1) {
    system_check_retval_exit("kexec -e");
}

exit 0;

# vim: set sw=4 ts=4 et:
