#!/usr/bin/perl

# Copyright (C) 2010  Andrew V. Stepanov <stanv@altlinux.org>

#     This script build distro dependent part for Discovery initrd.
#     Later, this part will be merged with generic part of Discovery initrd.

##
# Necessary Perl modules
#
use File::Basename;
use Getopt::Long;
use Template;
use File::Temp qw/ tempdir /;
use File::Path;
use File::Path qw(remove_tree);
use File::Copy;
use File::Copy::Recursive qw(dircopy);

##
# Predefined variables
#

# Script name
my $PROG = basename(__FILE__);

# Hasher user created during RPM package installation
my $HSHUSR  = "_mknetboot";

# mkimage template profile location, according to hasher user home dir
my $PROFILE = "mkimage-profile-netboot";

# Unpacked nbroot prefix
my $NBPREF = "/usr/share/xcat/netboot";

# Unpacked initrd location, for further merge with xCAT stuff "$NBPREF/<ARCH>/nbroot_base"
my $NBROOT;

# Build image with predefined apt.conf
my $APTCONF  = "apt.conf";

# Create mkimage profile according with user configuration files:
my $PACKAGES_SRC = "/etc/xcat/alt/netboot-packages";
my $MODULES_SRC  = "/etc/xcat/alt/netboot-modules";
my $FILES_SRC    = "/etc/xcat/alt/netboot-files";

# Lock directory, for insure one copy program run at time
my $LOCKDIR;
my $LOCKDIR_CREATED;

# Temporary directory with generated mkimage profile
my $TEMPDIR;

# Temporary directory with generated mkimage profile at predefined path
my $predefined_tmpdir = "undefined";

# Cleanup or not mkimage build directory. Default: cleanup
my $no_cleanup = 0;
my $cleanup = 1;
my $nocleanup;

##
# Functions
#

# Print usefull information
sub show_usage()
{
    printf "\n%s - create initrd image and kernel for xCAT netboot.\n", uc "$PROG";
    printf "\nUSAGE: %s -a <arch>\n", "$PROG";
    printf "\nOPTIONS:\n";
    printf "\t-v, --verbose\t print a message for each action;\n";
    printf "\t-h, --help\t show this text and exit;\n";
    print " \t-d, --no-cleanup\t do not cleanup temporary build directory;\n";
    print " \t-f, --predefined-tmp[=<PATH>]\t use predefined path for build dir. Do not auto cleanup.;\n";
    printf "\nRESULTS:\n";
    printf "\troot FS for netboot will be at: %s\n", "$NBPREF/<arch>/nbroot_base";
    printf "\tkernel image will be at tftp directory\n";
    printf "\nSUPPORTED ARCHITECTURES:\n";
    printf "\tx86, x86_64\n";
    printf "\nCONFIGURATION FILES:\n";
    printf "\t$PACKAGES_SRC\n";
    printf "\t$MODULES_SRC\n";
    printf "\t$FILES_SRC\n";
}

# Cleanup handler for build mkimage
sub exit_handler()
{
    # Invoke mkimage for clenup workdir
    if ( -d "$TEMPDIR" ) {
        if ( $cleanup ) {
           print "$PROG: Cleanup dirty mkimage work directory $TEMPDIR\n";
           $ret = system("make", "-C", "$TEMPDIR", "clean");
           if ( $ret ) {
               print (STDERR "$PROG: Cleanup failed");
           }
           print "$PROG: recursively remove $TEMPDIR\n";
           rmtree("$TEMPDIR", { verbose => 0, error => \$err});
           if (@$err) {
               printf (STDERR "$PROG: Problem to recursively remove $TEMPDIR\n");
           }
        } else {
           print "$PROG: Do not cleanup temporary directory ($TEMPDIR). Please cleanup manually.\n";
        }
    }

    # Remove lockdir
    if ( -d "$LOCKDIR_CREATED" ) {
        print "$PROG: remove lockdir $LOCKDIR\n";
        rmdir "$LOCKDIR" or die "$PROG: can't create lock dir ($LOCKDIR).";
    }
}

sub signal_handler() {
    my($signal) = @_;
    printf (STDERR "$PROG: catch signal $signal\n");
    exit_handler;
    die "Somebody sent me a SIG$signal";
}


##
# Program start here
#

# Get user ID
my $real_user_id       = $<;

# Information about hasher user
my $hshuid;
my $hshgid;
my $hshhome;

# Build mkimage profile for target and architecture
my $arch;
my $target;

# Verbose mode
my $verbose;

# Print help only
my $help;

# Generic variable for holding return result
my $ret;

# Parse command line options
$ret = GetOptions (
    'a|arch=s' => \$arch,
    'h|help' => \$help,
    'v|verbose' => \$verbose,
    'd|no-cleanup' => \$no_cleanup,
    'f|predefined-tmp:s' => \$predefined_tmpdir,
);

unless ($ret) {
    printf (STDERR "Use: %s <-h|--help> for more information\n", "$PROG");
    exit 1;
}

# User asked for verbose mode?
if ($verbose) {
    $verbose = "-v";
}

# Cleanup temporary directory?
if ( $no_cleanup or $predefined_tmpdir ne "undefined" ) {
    # Invoke same executable, with same options, but other UserID
    $nocleanup = "--no-cleanup";
    $cleanup = 0;
    $no_cleanup = 1;
}

# User asked for help?
if ($help) {
    &show_usage;
    exit 0;
}

# Check terget architecture
unless ($arch) {
    printf (STDERR "Not enough arguments. \n");
    printf (STDERR "Use: %s <-h|--help> for more information\n", "$PROG");
    exit 1;
}

if ( "$arch" ne "x86" && "$arch" ne "x86_64" ) {
    printf (STDERR "%s: Architecture %s not supported.\n", "$PROG", "$arch");
    printf (STDERR "Use: %s <-h|--help> for more information\n", "$PROG");
    exit 1;
}

# x86 architecture == i586 mkimage target
$target = $arch;
$target =~ s/^x86$/i586/;

$NBROOT="$NBPREF/$arch/nbroot_base";

# Get info about hasher user
$hshuid  = (getpwnam("$HSHUSR"))[2];
unless ($hshuid) {
    die "Can't get UID for $HSHUSR\n";
}
$hshgid  = (getpwnam("$HSHUSR"))[3];
$hshhome = (getpwnam("$HSHUSR"))[-2];

# This program doesn't attended to run with anybody else, except root
if ( $real_user_id != 0 && $real_user_id != $hshuid ) {
    printf (STDERR "%s: Executed with unknown user ID: %s. Please run as root.\n", "$PROG", "$real_user_id");
    exit 1
}

##
# Varibles for macrosses substitution in mkimage profile
#
# Kernel image name
my $m_kernel = "nbk.$arch";
# Directory with initrd and kernel image
my $m_resultsdir = "$hshhome";
# mkimage target
my $m_arch = "$target";
# Verbose flag
my $m_verbose;
# Put generated initrd image under name
my $m_initrd = "nbroot-$arch.tar";

if ( $verbose ) {
    $m_verbose = "Yes";
}

##
# root part:
#   * recall script with hasher user
#   * delete any previsious images
#   * copy results
if ( $real_user_id == 0 ) {

    # Inform user about current settings and further actions
    printf "$PROG: Build image for $arch architecture and $target target\n";
    printf "$PROG: Invoke $PROG as $HSHUSR, UID: $hshuid, GID: $hshgid\n";

    # Clean any previsious initrd / kernel image for specified architecture
    if ( -f "$m_resultsdir/$m_kernel" ) {
        print "$PROG: Delete previsious kernel image $m_resultsdir/$m_kernel\n";
        unlink("$m_resultsdir/$m_kernel");
    }

    if ( -f "$m_resultsdir/$m_initrd" ) {
        print "$PROG: Delete previsious initrd image $m_resultsdir/$m_initrd\n";
        unlink("$m_resultsdir/$m_initrd");
    }

    # Call this script again, with hasher user rights.
    $ret = system("su", "-s", "/bin/sh", "-l", "$HSHUSR", "-c", "/usr/bin/$PROG $nocleanup $verbose -a $arch -p $predefined_tmpdir");
    if ( $ret ) {
        die "$PROG: build failed";
    }

    # Check for mkimage results
    unless ( -f "$m_resultsdir/$m_kernel" && -f "$m_resultsdir/$m_initrd" ) {
        die "$PROG: can't find results from mkimage"
    }

    # Delete previsious unpacked initrd image
    if ( -d "$NBROOT" ) {
        print "$PROG: remove previsious $NBROOT\n";
        remove_tree( "$NBROOT", {keep_root => 1, error => \my $err } );
        if (@$err) {
            die "$PROG: Can't recursive delete $NBROOT";
        }
    } else {
        mkpath("$NBROOT", {error => \$err});
        if (@$err) {
            die "$PROG: Failed to crate $NBROOT directory";
        }
    }

    # Extract new initrd tarball, for further merge with xCAT initrd part
    print "$PROG: unpack new initrd image to $NBROOT\n";
    $ret = system("tar", "-C", "$NBROOT", "--preserve-permissions", "--same-owner", "--extract", "--file", "$m_resultsdir/$m_initrd");
    if ( $ret ) {
        die "$PROG: tar can't unpack tarball $m_resultsdir/$m_initrd to $NBROOT\n";
    }

    # Lookup xCAT configuration for tftpdir location
    my $tftpdir = `gettab key=tftpdir site.value`;
    chomp $tftpdir;
    if ( "$tftpdir" eq "" ) {
        die "$PROG: Failed to lookup xcat `site' table for tftpdir location.";
    }
    unless ( -d "$tftpdir" ) {
        die "$PROG: Can't find  $tftpdir directory.";
    }
    print "$PROG: Key site.tftpdir point to $tftpdir\n";

    # Check for TFTP directory existence
    unless ( -d "$tftpdir" ) {
        die "xCAT database refer to \`$tftpdir' for TFTP root. But it is not accessible.";
    }

    unless ( -d "$tftpdir/xcat" ) {
        mkpath("$tftpdir/xcat", {error => \$err});
        if (@$err) {
            die "$PROG: Failed to crate $tftpdir/xcat directory";
        }
    }

    # Copy kernel to tftpdir
    print "$PROG: copy new kernel $m_kernel to $tftpdir/xcat/\n";
    copy("$m_resultsdir/$m_kernel", "$tftpdir/xcat/") or die "Copy failed: $!";
}

##
# User part:
#   * create temporary directory for mkimage profile
#   * install cleanup handler
#   * expand macrosses in mkimage profile
#   * call mkimage
if ( $real_user_id == $hshuid ) {
    my $osver="alt";
    my $db_src_list;
    my $installdir;

    $LOCKDIR="$hshhome/$PROG.lock";

    if ( -d "$LOCKDIR" ) {
        die "$PROG: already run. Exist $LOCKDIR.";
    }

    # Sure this program involved only once at time
    mkdir "$LOCKDIR" or die "$PROG: can't create lock dir ($LOCKDIR).";
    $LOCKDIR_CREATED="$LOCKDIR";
    printf "$PROG: Lockdir $LOCKDIR created\n";

    # Install handlers
    foreach $name ('HUP', 'INT', 'QUIT', 'TERM', 'EXIT') {
        printf "$PROG: Install signal handler for $name signal\n";
        $SIG{$name} = 'exit_handler';
    }

    # User want to use predefined build dir.
    unless ( "$predefined_tmpdir" eq "undefined" ) {
        # User didn't supplies location. Use default one.
        unless ( $predefined_tmpdir ) {
            $predefined_tmpdir="$hshhome/$PROG-build/$arch";
        }

        if ( -d "$predefined_tmpdir" ) {
            die "Sorry, $predefined_tmpdir already exist. Please remove it first.";
        }

        mkpath ( "$predefined_tmpdir", {error => \my $err} );
        if (@$err) {
            die "Can't create tempory directory for build purposes";
        }
        $TEMPDIR="$predefined_tmpdir";
    } else {
        # Create temporary build directory
        $TEMPDIR = tempdir ( "$PROG.XXXXXX", DIR => "$hshhome", CLEANUP => $cleanup )
            or die "Cant create tempory directory for build purposes";
    }
    print "$PROG: Create mkimage profile at temporary directory $TEMPDIR\n";

    # Read xCAT configuration
    $db_src_list = `gettab key=aptsrclist_$arch site.value`;
    chomp $db_src_list;
    if ( "$db_src_list" eq "" ) {
        die "$PROG: Failed to lookup xcat configuration for site.aptsrclist_$arch key.";
    }
    unless ( -f "$db_src_list" ) {
        die "$PROG: Can't find $db_src_list file.";
    }

    $installdir = `gettab key="installdir" site.value`;
    chomp $installdir;
    if ( "$installdir" eq "" ) {
        die "$PROG: Failed to lookup xcat configuration for site.installdir key.";
    }
    unless ( -d "$installdir" ) {
        die "$PROG: Can't find  $installdir directory.";
    }

    unless ( -d "$hshhome/$PROFILE" ) {
        die "$PROG: Can't locate directory with mkimage profile at $hshhome/$PROFILE";
    }

    # Used by hooks
    my $BINARIES_DST = "$TEMPDIR/initrd-binaries";
    my $MODULES_DST = "$TEMPDIR/initrd-modules";
    my $FILES_DST = "$TEMPDIR/initrd-files";

    # Packages list for mkimage profile
    my $PACKAGES_DST = "$TEMPDIR/initrd-packages";

    # Copy mkimage profile to temporary directory
    dircopy("$PROFILE","$TEMPDIR") or die $!;

    # Copy user configuration for mkimage
    unless ( -f "$PACKAGES_SRC" ) { die "$PROG: Can't locate $PACKAGES_SRC file"; }
    unless ( -f "$MODULES_SRC" )  { die "$PROG: Can't locate $MODULES_SRC file"; }
    unless ( -f "$FILES_SRC" )    { die "$PROG: Can't locate $FILES_SRC file"; }
    copy("$PACKAGES_SRC", "$PACKAGES_DST") or die "Copy failed: $!";
    copy("$MODULES_SRC", "$MODULES_DST") or die "Copy failed: $!";
    copy("$FILES_SRC", "$FILES_DST") or die "Copy failed: $!";

    # Template constructor method
    my $tt = Template->new({
            INCLUDE_PATH => "$TEMPDIR",
            OUTPUT_PATH  => "$TEMPDIR"
        }) || die "$Template::ERROR\n";

    # List all possible macrosses in Makefile template
    my $vars_Makefile = {
        arch       => "$m_arch",
        verbose    => "$m_verbose",
        initrdname => "$m_initrd",
        kernelname => "$m_kernel",
        resultsdir => "$m_resultsdir"
    };

    # Fill Makefile template with correct values
    $tt->process("Makefile.in", $vars_Makefile, "Makefile") || die $tt->error();

    # 1. sources list for repository created by `copycds'
    if (-d "$installdir/$osver/$arch/ALTLinux" ) {
        open ( FIND,
            qq#find "$installdir/$osver/$arch/ALTLinux" -mindepth 1 -maxdepth 1 -type d -regex '.*/RPMS\\..*' -printf '%P\\n' | sed -e 's/^RPMS\\.//' |#) || die "Failed: $!\n";

        open ( SRCLIST, ">", "$TEMPDIR/sources.list" ) or die "can't create $TEMPDIR/sources.list: $!\n";

        while ( <FIND> ) {
            my $comp = $_;
            print "$PROG: Add to sources.list $comp component\n";
            print SRCLIST "rpm file:$installdir/$osver/$arch ALTLinux $comp\n";
        }
    }

    # 2. copy predefined sources lists in `site' table (XXX@stanv: maybe various branches for same architecture!)
    # Open existing source.list for appending
    open ( SRCLIST, ">>", "$TEMPDIR/sources.list" ) or die "Could not open file $TEMPDIR/sources.list: $!";

    # Open source.list defined in `site' table
    open ( SITESRCLIST, "<", "$db_src_list") or die "Could not open file B.txt: $!";

    # Join source.list from `site' table and autogenerated source.list
    while ( my $line = <SITESRCLIST> ) {
        print SRCLIST $line;
    }

    # Close opened file streams
    close SRCLIST;
    close SITESRCLIST;

    # 3. update apt.conf
    unless ( -f "$TEMPDIR/$APTCONF" ) {
        die "$PROG: Can't locate $APTCONF template in $TEMPDIR";
    }
    open ( APTCONF, ">>", "$TEMPDIR/$APTCONF" ) or die "Could not open file $TEMPDIR/$APTCONF: $!";
    print APTCONF qq#Dir::Etc::SourceList "$TEMPDIR/sources.list";\n#;
    close APTCONF;

    # Build kernel and initrd from generated mkimage profile
    print "$PROG: Build kernel and initrd for, generated mkimage profile at $TEMPDIR\n";
    $ret = system("make", "-C", "$TEMPDIR", "all");
    if ( $ret ) {
        die "$PROG: Failed build mkimage profile\n";
    }

    # Remove lockdir
    if ( -d "$LOCKDIR_CREATED" ) {
        print "$PROG: Remove lockdir $LOCKDIR\n";
        rmdir "$LOCKDIR" or die "$PROG: can't create lock dir ($LOCKDIR).";
    }
}

exit 0;

END {
    if ( $real_user_id == $hshuid ) {
        exit_handler;
    }
}

__END__

Hello World!
