#!/usr/bin/perl -w

=encoding utf8

=head1 NAME

xt-install-image - Install a fresh copy of GNU/Linux into a directory

=head1 SYNOPSIS

  xt-install-image [options]

  Help Options:
   --help           Show this scripts help information.
   --manual         Read this scripts manual.
   --version        Show the version number and exit.

  Debugging Options:
   --verbose        Be verbose in our execution.

  Mandatory Options:
   --location       The location to use for the new installation
   --dist           The name of the distribution which has been installed.

  Misc Options:
   --arch           Pass the given arch setting to debootstrap or rpmstrap.
   --config         Read the specified config file in addition to the global
                    configuration file.
   --mirror         The mirror to use when installing with 'debootstrap'.
   --apt_proxy      The proxy to use when installing with 'debootstrap'.
   --keyring        The keyring to use when installing with 'debootstrap'.

  Installation Options:
   --install-method  Specify the installation method to use.
   --install-source  Specify the installation source to use.
   --debootstrap-cmd Specify which debootstrap command to
                     use. Defaults to debootstrap if both, debootstrap
                     and cdebootstrap are installed.

  All other options from xen-create-image will be passed as environmental
 variables.


=head1 NOTES

  This script is invoked by xen-create-image after to create a new
 distribution of Linux.  Once the script has been created the companion
 script xt-customize-image will be invoked to perform the network
 configuration, etc.


=head1 INSTALLATION METHODS

  There are several available methods of installation, depending upon the
 users choice.  Only one option may be chosen at any given time.

  The methods available are:

=over 8

=item B<debootstrap>
Install the distribution specified by the B<--dist> argument using the debootstrap.  If you use this option you must specify a mirror with B<--mirror>.

=item B<copy>
Copy the given directory recursively.  This local directory is assumed to contain a complete installation.  Specify the directory to copy with the B<--install-source> argument.

=item B<rinse>
Install the distribution specified by B<--dist> using the rinse command.

=item B<rpmstrap>
Install the distribution specified by B<--dist> using the rpmstrap command.

=item B<tar>
Untar a .tar file into the new installation location.  This tarfile is assumed to contain a complete archived system.   Specify the directory to copy with the B<--install-source> argument.

=back


=head1 AUTHORS

 Steve Kemp, https://steve.fi/
 Axel Beckert, https://axel.beckert.ch/
 Dmitry Nedospasov, http://www.nedos.net/
 Stéphane Jourdois


=head1 LICENSE

Copyright (c) 2005-2009 by Steve Kemp, (c) 2010 by The Xen-Tools
Development Team. All rights reserved.

This module is free software;
you can redistribute it and/or modify it under
the same terms as Perl itself.
The LICENSE file contains the full text of the license.

=cut


use strict;
use Env;
use File::Copy;
use Getopt::Long;
use Pod::Usage;
use Xen::Tools::Common;


#
#  Configuration values read from the command line.
#
my %CONFIG;

#
# Release number.
#
my $RELEASE = '4.9';


#
# Dispatch table mapping installation types to their names.
#
# The names are the methods, and the hash keys are:
#
#  sub           The routine to execute.
#  needBinary    If set the name of an executable we need to exist.
#  needFile      Defined if we need an install-source file specified.
#  needDirectory Defined if we need an install-source directory specified.
#
#
my $debootstrap_cmd;
my %dispatch = (
                 "copy" => { sub           => \&do_copy,
                             needBinary    => "/bin/cp",
                             needDirectory => 1,
                           },
                 "debootstrap" => { sub        => \&do_debootstrap,
                                    needBinary => ["/usr/sbin/debootstrap", "/usr/bin/cdebootstrap", "/usr/bin/cdebootstrap-static"],
                                    var        => \$debootstrap_cmd,
                                  },
                 "cdebootstrap" => { sub        => \&do_debootstrap,
                                     needBinary => ["/usr/sbin/cdebootstrap", "/usr/bin/cdebootstrap-static"],
                                     var        => \$debootstrap_cmd,
                                  },
                 "rinse" => { sub        => \&do_rinse,
                              needBinary => "/usr/sbin/rinse",
                            },
                 "rpmstrap" => { sub        => \&do_rpmstrap,
                                 needBinary => "/usr/bin/rpmstrap",
                               },
                 "tar" => { sub        => \&do_tar,
                            needBinary => "/bin/tar",
                            needFile   => 1,
                          } );



#
#  Read the global configuration file.
#
readConfigurationFile("/etc/xen-tools/xen-tools.conf", \%CONFIG);


#
#  Parse the command line arguments.
#
parseCommandLineArguments();


#
#  If we received a configuration file then read it.
#
if ( $CONFIG{ 'config' } )
{
    my $path = $CONFIG{ 'config' };

    # If not fully-qualified then read from /etc/xen-tools.
    if ( $path !~ /^[\/]/ )
    {
        $path = "/etc/xen-tools/" . $path;
    }

    # Read the file, if it exists.
    readConfigurationFile($path, \%CONFIG) if ( -e $path );
}


#
#  Check our arguments
#
checkArguments();


#
#  Now lookup our installation type and dispatch control to it.
#
if ( defined( $CONFIG{ 'install-method' } ) &&
     length( $CONFIG{ 'install-method' } ) )
{

    #
    #  Get the entry from the dispatch table.
    #
    my $installer = $dispatch{ lc( $CONFIG{ 'install-method' } ) };

    if ( defined($installer) )
    {

        #
        #  If we found it.
        #

        # Do we need to test for a binary.
        if ( $installer->{ 'needBinary' } ) {
            if ( 'ARRAY' eq ref $installer->{ 'needBinary' } ) {
                unless (ref $installer->{ 'var' }) {
                    die "Assertion: If dispatch->->needBinary is an array ref, dispatch->->var must exist";
                }

                foreach my $binary (@{$installer->{ 'needBinary' }}) {
                    if (-x $binary) {
                        ${$installer->{ 'var' }} = $binary;
                        last;
                    }
                }

                unless ( ${$installer->{ 'var' }}  ) {
                    print
                        "One of the following binaries are required for the installation, but none was found\n";
                    print "\t" . join(', ', @{$installer->{ 'needBinary' }}) . "\n";
                    exit 1;
                }
            }
            else
            {
                if ( !-x $installer->{ 'needBinary' } ) {
                    print
                        "The following required binary for the installation was not found\n";
                    print "\t" . $installer->{ 'needBinary' } . "\n";
                    exit 1;
                }
            }
        }

        # Do we need a directory specified as the installation source?
        if ( ( $installer->{ 'needDirectory' } ) &&
             ( !$CONFIG{ 'install-source' } || !-d $CONFIG{ 'install-source' } )
           )
        {
            print "Please specify the source directory with --install-source\n";
            if ( $CONFIG{ 'install-source' } )
            {
                print
                  "The specified directory $CONFIG{'install-source'} does not exist.\n";
            }
            exit 1;
        }

        # Do we need a file specified as the installation source?
        if ( ( $installer->{ 'needFile' } ) &&
             ( !$CONFIG{ 'install-source' } || !-e $CONFIG{ 'install-source' } )
           )
        {
            print "Please specify the source file with --install-source\n";

            if ( $CONFIG{ 'install-source' } )
            {
                print
                  "The specified file $CONFIG{'install-source'} does not exist.\n";
            }
            exit 1;
        }

        #
        #  Now we can call the appropriate handler.
        #
        $installer->{ 'sub' }->();

        #
        #  Did the operation succeed?
        #
        #  Test that we have some "standard" files present.
        #

        checkForCommonFilesInChroot($CONFIG{ 'location' },
                                    "installed system");

        #
        #  All done.
        #
        exit 0;
    }
    else
    {
        print "The installation method specified is invalid.\n";
        exit 1;
    }
}
else
{
    print "An installation method is mandatory\n";
    exit 1;
}



=begin doc

  Parse the command line arguments this script was given.

=end doc

=cut

sub parseCommandLineArguments
{
    my $HELP    = 0;
    my $MANUAL  = 0;
    my $VERSION = 0;

    #
    #  Parse options.
    #
    GetOptions(

        # Mandatory
        "location=s", \$CONFIG{ 'location' },
        "dist=s",     \$CONFIG{ 'dist' },
        "hostname=s", \$CONFIG{ 'hostname' },

        # Installation method
        "install-method=s", \$CONFIG{ 'install-method' },
        "install-source=s", \$CONFIG{ 'install-source' },
        "debootstrap-cmd=s", \$CONFIG{ 'debootstrap-cmd' },

        # Misc
        "arch=s",   \$CONFIG{ 'arch' },
        "cache=s",  \$CONFIG{ 'cache' },
        "cachedir=s", \$CONFIG{ 'cachedir' },
        "config=s", \$CONFIG{ 'config' },
        "mirror=s", \$CONFIG{ 'mirror' },
        "keyring=s", \$CONFIG{ 'keyring' },
        "apt_proxy=s", \$CONFIG{ 'apt_proxy' },

        # Help.
        "verbose", \$CONFIG{ 'verbose' },
        "help",    \$HELP,
        "manual",  \$MANUAL,
        "version", \$VERSION
    );

    pod2usage(1) if $HELP;
    pod2usage( -verbose => 2 ) if $MANUAL;


    if ($VERSION)
    {
        print "xt-install-image release $RELEASE\n";
        exit;
    }
}



=begin doc

  Test that the command line arguments we were given make sense.

=end doc

=cut

sub checkArguments
{

    #
    #  We require a location.
    #
    if ( !defined( $CONFIG{ 'location' } ) )
    {
        print "The '--location' argument is mandatory\n";
        exit 1;
    }


    #
    #  Test that the location we've been given exists
    #
    if ( !-d $CONFIG{ 'location' } )
    {
        print "The installation directory we've been given doesn't exist\n";
        print "We tried to use : $CONFIG{'location'}\n";
        exit 1;
    }


    #
    #  We require a distribution name.
    #
    if ( !defined( $CONFIG{ 'dist' } ) )
    {
        print "The '--dist' argument is mandatory\n";
        exit 1;
    }


    #
    #  Test that the distribution name we've been given
    # to configure has a collection of hook scripts.
    #
    #  If there are no scripts then we clearly cannot
    # customise it!
    #
    my $dir = "/usr/share/xen-tools/" . $CONFIG{ 'dist' } . ".d";

    if ( !-d $dir )
    {
        print <<EOR;

  We are trying to configure an installation of $CONFIG{'dist'} in
 $CONFIG{'location'} - but there is no hook directory for us to use.

  This means we would not know how to configure this installation.

  We would expect the hook directory to be $dir.

  Aborting.
EOR
        exit 1;
    }


    #
    #  Test that we received a valid installation type.
    #
    my $valid = 0;
    if ( defined( $CONFIG{ 'install-method' } ) )
    {
        foreach my $recognised ( keys %dispatch )
        {
            $valid = 1
              if ( lc( $CONFIG{ 'install-method' } ) eq lc($recognised) );
        }
    }
    else
    {
        $valid = 1;
    }

    if ( !$valid )
    {
        print <<EOF;
  Please specify the installation method to use.

  For example:

     --install-method=copy        --install-source=/some/path
     --install-method=debootstrap
     --install-method=rpmstrap
     --install-method=tar         --install-source=/some/file.tar

EOF
        exit;
    }

}



=begin doc

  Check if there are some common files in some chroot

=end doc

=cut

sub checkForCommonFilesInChroot {
    my ($chroot, $what) = @_;
    foreach my $file (qw( /bin/ls /bin/cp ))
    {
        if ( !-x $chroot.$file )
        {
            print STDERR <<EOT;
WARNING ($0): The $what at $chroot doesn\'t seem to be a full system.
WARNING ($0): The $what is missing the common file: $file.
EOT
        }
    }
}



=begin doc

  This function will copy all the .deb files from one directory
 to another as a caching operation which will speed up installations via
 debootstrap.

=end doc

=cut

sub copyDebFiles
{
    my ( $source, $dest ) = (@_);

    print "Copying files from $source -> $dest\n";

    #
    # Loop over only .deb files.
    #
    foreach my $file ( sort ( glob( $source . "/*.deb" ) ) )
    {
        my $name = $file;
        if ( $name =~ /(.*)\/(.*)/ )
        {
            $name = $2;
        }

        #
        #  Only copy if the file doesn't already exist.
        #
        if ( !( -e $dest . "/" . $name ) )
        {
            File::Copy::cp( $file, $dest );
        }
    }

    print "Done\n";
}



###
#
#  Installation functions follow.
#
###



=begin doc

  Install a new image of a distribution using `cp`.

=end doc

=cut

sub do_copy
{

    #
    #  Check if the copy source has at least some "standard" files present.
    #
    checkForCommonFilesInChroot($CONFIG{ 'install-source' },
                                "installation source");

    #
    #  Find the copy command to run from the configuration file.
    #
    my $cmd = $CONFIG{ 'copy-cmd' };
    if ( !defined($cmd) )
    {
        print "Falling back to default copy command\n";
        $cmd = '/bin/cp -a $src/* $dest';    # Note: single quotes.
    }

    #
    #  Expand the source and the destination.
    #
    $cmd =~ s/\$src/$CONFIG{'install-source'}/g;
    $cmd =~ s/\$dest/$CONFIG{'location'}/g;

    #
    #  Run the copy command.
    #
    runCommand($cmd, \%CONFIG);
}



=begin doc

  Install a new image of Debian using 'debootstrap'.

=end doc

=cut

sub do_debootstrap
{

    #
    #  The command is a little configurable - mostly to allow you
    # to use cdebootstrap.
    #
    my $cmd = $CONFIG{ 'debootstrap-cmd' } || $debootstrap_cmd;
    print "Using $cmd as debootstrap command\n";
    my $cachedir = $CONFIG{ 'cachedir' };

    #
    #  Cache from host -> new installation if we've got caching
    # enabled.
    #
    if ( $CONFIG{ 'cache' } eq "yes" )
    {
        print "\nCopying files from host to image.\n";
        unless( -d $cachedir ) {
            my $xtcache = '/var/cache/xen-tools/archives/';
            print("$cachedir not found, defaulting to $xtcache\n");
            unless ( -d $xtcache ) {
                system "mkdir -p $xtcache";
            }
            $cachedir = $xtcache;
        }
        runCommand("mkdir -p $CONFIG{'location'}/var/cache/apt/archives", \%CONFIG);
        copyDebFiles( "$cachedir",
                      "$CONFIG{'location'}/var/cache/apt/archives" );
        print("Done\n");
    }

    #
    #  Propagate --verbose appropriately.
    #
    my $EXTRA = '';
    if ( $CONFIG{ 'verbose' } )
    {
        $EXTRA = ' --verbose';
    }

    #
    #  Propagate the --arch argument
    #
    if ( $CONFIG{ 'arch' } )
    {
        $EXTRA .= " --arch $CONFIG{'arch'}";
    }

    #
    #  Propagate the --keyring argument
    #
    if ( $CONFIG{ 'keyring' } )
    {
        $EXTRA .= " --keyring=$CONFIG{'keyring'}";
    }

    #
    #  Setup http_proxy so that debootstrap pulls files through the apt-proxy
    #
    if ( $CONFIG{ 'apt_proxy' } )
    {
        print("Using apt_proxy: $CONFIG{'apt_proxy'}\n");
        $ENV{'http_proxy'} = $CONFIG{'apt_proxy'};
    }

    #
    #  This is the command we'll run
    #
    my $command =
      "$cmd $EXTRA $CONFIG{'dist'} $CONFIG{'location'} $CONFIG{'mirror'}";

    #
    #  Run the command.
    #
    runCommand($command, \%CONFIG);


    #
    #  Cache from the new installation -> the host if we've got caching
    # enabled.
    #
    if ( $CONFIG{ 'cache' } eq "yes" )
    {
        print "\nCopying files from new installation to host.\n";
        copyDebFiles( "$CONFIG{'location'}/var/cache/apt/archives",
                      "$cachedir" );
        print("Done\n");
    }


}



=begin doc

  Install a new distribution of GNU/Linux using the rinse tool.

=end doc

=cut

sub do_rinse
{

    #
    #  The command we're going to run.
    #
    my $command =
      "rinse --distribution=$CONFIG{'dist'} --directory=$CONFIG{'location'}";

    #
    #  Propagate the --arch argument
    #
    if ( $CONFIG{ 'arch' } )
    {
        $command .= " --arch $CONFIG{'arch'}";
    }
    else
    {
        my $uname_machine = `uname -m`;
        chomp($uname_machine);

        if ($uname_machine eq 'x86_64')
        {
            $command .= " --arch amd64";
        }
        elsif ($uname_machine =~ /^i[3-6]86$/) {
            $command .= " --arch i386";
        }
        else
        {
            die "Local architecture ($uname_machine) not supported by rinse.\n".
                "Please choose a supported installation architecture (i386 or amd64) explicitly."
        }
    }

    #
    #  Propagate the verbosity setting.
    #
    if ( $CONFIG{ 'verbose' } )
    {
        $command .= " --verbose";
    }

    runCommand($command, \%CONFIG);
}



=begin doc

  Install a new distribution of GNU/Linux using the rpmstrap tool.

=end doc

=cut

sub do_rpmstrap
{

    #
    #  Propagate the verbosity setting.
    #
    my $EXTRA = '';
    if ( $CONFIG{ 'verbose' } )
    {
        $EXTRA .= " --verbose";
    }

    #
    #  Propagate any arch setting we might have.
    #
    if ( $CONFIG{ 'arch' } )
    {
        $EXTRA .= " --arch $CONFIG{'arch'}";
    }

    #
    #  Setup mirror if present.
    #
    my $mirror = "";
    $mirror = $CONFIG{ 'mirror' } if ( $CONFIG{ 'mirror' } );

    #
    #  The command we're going to run.
    #
    my $command = "rpmstrap $EXTRA $CONFIG{'dist'} $CONFIG{'location'} $mirror";
    runCommand($command, \%CONFIG);
}



=begin doc

  Install a new image of a distribution using `tar`.

=end doc

=cut

sub do_tar
{

    #
    #  Find the tar command to run from the configuration file.
    #
    my $cmd = $CONFIG{ 'tar-cmd' };
    if ( !defined($cmd) )
    {
        print "Falling back to default tar command\n";
        $cmd = '/bin/tar --numeric-owner -xvf $src';    # Note: single quotes.
    }

    #
    #  Expand the tarfile.
    #
    $cmd =~ s/\$src/$CONFIG{'install-source'}/g;

    #
    #  Run a command to copy an installed system into the new root.
    #
    runCommand("cd $CONFIG{'location'} && $cmd", \%CONFIG);
}
