#!/usr/bin/perl -w

=head1 NAME

xen-resize-guest - Resize a loopback or LVM based xen guest.

=head1 SYNOPSIS

  xen-resize-guest [options]

  Help Options:
   --help     Show help information.
   --manual   Read the manual for this script.
   --version  Show the version information and exit.
   --verbose  Show diagnostic output.

  General Options:
   --add      Specify the amount of space to add, e.g. --add=1gb
   --dir      Specify the path to the loopback image root.
   --force    Force the resize to happen without a last-chance delay.
   --hostname Specify the hostname of the guest to resize.

=cut


=head1 OPTIONS

=over 8

=item B<--add>
Specify the amount of storage to add to the primary disk.

=item B<--dir>
Specify the directory where the loopback files are based.

=item B<--force>
Don't pause for 10 seconds prior to commencing.

=item B<--help>
Show help information.

=item B<--hostname>
Specify the hostname to delete.

=item B<--lvm>
Specify the volume group to use.

=item B<--manual>
Read the manual for this script.

=item B<--version>
Show the version number and exit.

=back

=cut


=head1 DESCRIPTION

  This tool will ease the resizing of Xen guests, whether they are based
 upon loopback files or LVM partitions.

  Whilst the process of resizing a guest is pretty simple it can be fiddly
 to do the steps correctly in the right order:

1.  Shutdown the guest.
2.  Unmount the volume, if it is mounted.
3.  Add to the space.
4.  Check the filesystem.
5.  Resize the filesystem.
6.  Restart the guest.

  More than once I've heard of users making mistakes and breaking their
 filesystems; hence this tool.

=cut

=head1 AUTHOR

 Steve
 --
 http://www.steve.org.uk/

=cut


=head1 LICENSE

Copyright (c) 2005-2007 by Steve Kemp.  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 English;
use Env;
use Getopt::Long;
use Pod::Usage;
use Text::Template;



#
#  Configuration values read from the command line, or configuration file.
#
my %CONFIG;


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



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


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


#
#  Validate our arguments.
#
testArguments();


#
#  The path to the file, or device, to resize.
#
my $path = undef;

if ( $CONFIG{ 'dir' } )
{

    #
    #  Make sure we can find the disk
    #
    $path =
      $CONFIG{ 'dir' } . "/domains/" . $CONFIG{ 'hostname' } . "/disk.img";
    if ( !-e $path )
    {
        print <<EOF;

  The disk image for the guest domain $CONFIG{'hostname'} was not found
 where we expected it to be:

   $path

  Please check and try again, or report this as a bug.
EOF
        exit 1;
    }

    #
    #  Ensure the file is an ext3 filesystem.
    #
    my $type = `file $path`;
    if ( $type !~ /ext3/ )
    {
        print "Filesystem type is not understood.\n";
        print "We only handle ext3 right now!\n";
        print "Aborting\n";
        exit 1;
    }


    #
    #  OK we have the size, we have the image.
    #
    #  Resize with DD
    #
    print "Preparing to resize image: $path\n";

    #
    #  Allow panic
    #
    if ( !$CONFIG{ 'force' } )
    {
        foreach my $i ( 1 .. 10 )
        {
            my $delay = 10 - $i;
            print "Sleeping for $delay seconds to allow cancel\n";
            sleep 1;
        }
    }

    print "DO NOT INTERRUPT\n";

    my $cmd = "dd if=/dev/zero bs=1M count=$CONFIG{'add'}k >> $path";
    system($cmd );
}
elsif ( $CONFIG{ 'lvm' } )
{

    #
    #  Make sure we can find the disk
    #
    $path = "/dev/" . $CONFIG{ 'lvm' } . "/" . $CONFIG{ 'hostname' } . "-disk";
    if ( !-e $path )
    {
        print <<EOF;

  The disk image for the guest domain $CONFIG{'hostname'} was not found
 where we expected it to be:

   $path

  Please check and try again, or report this as a bug.
EOF
        exit 1;
    }

    #
    #  Ensure the file is an ext3 filesystem.
    #
    my $type = `head $path | file -`;
    if ( $type !~ /ext3/ )
    {
        print "Filesystem type is not understood.\n";
        print "We only handle ext3 right now!\n";
        print "Aborting\n";
        exit 1;
    }


    #
    #  OK we have the size, we have the image.
    #
    #  Resize with DD
    #
    print "Preparing to resize image: $path\n";

    #
    #  Allow panic
    #
    if ( !$CONFIG{ 'force' } )
    {
        foreach my $i ( 1 .. 10 )
        {
            my $delay = 10 - $i;
            print "Sleeping for $delay seconds to allow cancel\n";
            sleep 1;
        }
    }

    print "DO NOT INTERRUPT\n";
    my $cmd = "lvextend -L+$CONFIG{'add'}M $path";
    system($cmd );
}
else
{
    print "Unknown storage type.  (Neither LVM nor loopback.)\n";
    exit 1;
}


#
#  Check filesystem.
#
print "Checking filesystem\n";
system("fsck.ext3 -f $path");

#
#  Run ext3resize.
#
print "Resizing in place\n";
system("resize2fs $path");


#
#  Job done.
#
print "All done\n";
exit 0;



=begin doc

  Read the configuration file specified.

=end doc

=cut

sub readConfigurationFile
{
    my ($file) = (@_);

    open( FILE, "<", $file ) or die "Cannot read file '$file' - $!";

    my $line = "";

    while ( defined( $line = <FILE> ) )
    {
        chomp $line;
        if ( $line =~ s/\\$// )
        {
            $line .= <FILE>;
            redo unless eof(FILE);
        }

        # Skip lines beginning with comments
        next if ( $line =~ /^([ \t]*)\#/ );

        # Skip blank lines
        next if ( length($line) < 1 );

        # Strip trailing comments.
        if ( $line =~ /(.*)\#(.*)/ )
        {
            $line = $1;
        }

        # Find variable settings
        if ( $line =~ /([^=]+)=([^\n]+)/ )
        {
            my $key = $1;
            my $val = $2;

            # Strip leading and trailing whitespace.
            $key =~ s/^\s+//;
            $key =~ s/\s+$//;
            $val =~ s/^\s+//;
            $val =~ s/\s+$//;

            # command expansion?
            if ( $val =~ /(.*)`([^`]+)`(.*)/ )
            {

                # store
                my $pre  = $1;
                my $cmd  = $2;
                my $post = $3;

                # get output
                my $output = `$cmd`;
                chomp($output);

                # build up replacement.
                $val = $pre . $output . $post;
            }

            # Store value.
            $CONFIG{ $key } = $val;
        }
    }

    close(FILE);
}


=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;

    #
    #  Local variables.
    #
    my %install;


    #
    #  Parse options.
    #
    GetOptions(

        # Misc. options
        "add=s",      \$CONFIG{ 'add' },
        "dir=s",      \$install{ 'dir' },
        "lvm=s",      \$install{ 'lvm' },
        "hostname=s", \$CONFIG{ 'hostname' },
        "force",      \$CONFIG{ 'force' },

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

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


    if ($VERSION)
    {
        print("xen-resize-guest release $RELEASE\n");
        exit 1;
    }

    #
    #  Setup mutually exclusive options in such a way that
    # they will allow the configuration values to be overriden by
    # the command line.
    #
    if ( $install{ 'lvm' } )
    {
        $CONFIG{ 'lvm' } = $install{ 'lvm' };
        $CONFIG{ 'dir' } = undef;
        delete $CONFIG{ 'dir' };
    }
    if ( $install{ 'dir' } )
    {
        $CONFIG{ 'dir' } = $install{ 'dir' };
        $CONFIG{ 'lvm' } = undef;
        delete $CONFIG{ 'lvm' };
    }
}



=begin doc

  Test our arguments are complete.

=end doc

=cut

sub testArguments
{

    #
    #  Make sure we received a hostname.
    #
    if ( !$CONFIG{ 'hostname' } )
    {
        print <<EOF;

  A hostname is mandatory!

  Please specify one with --hostname=some.host.name

EOF
        exit 1;
    }

    #
    #  Make sure we received a size.
    #
    if ( !$CONFIG{ 'add' } )
    {
        print <<EOF;

  Please specify a size to increase the guest by:

   --add=1Gb    -> Increase the size of the disk image by 1Gb.
   --add=1      -> Increase the size of the disk image by 1Mb.

EOF
        exit 1;
    }

    #
    #  Make sure the guest isn't running
    #
    if ( xenRunning( $CONFIG{ 'hostname' } ) )
    {
        print "The guest $CONFIG{'hostname'} appears to be running!\n";
        exit 1;
    }

    #
    #  We should either have LVM *or* directory - not neither or both.
    #
    my $options = 0;
    $options += 1
      if ( defined( $CONFIG{ 'lvm' } ) && length( $CONFIG{ 'lvm' } ) );
    $options += 1
      if ( defined( $CONFIG{ 'dir' } ) && length( $CONFIG{ 'dir' } ) );

    #
    #  Report
    #
    if ( $options == 0 )
    {
        print "Please specify one of --lvm or --dir\n";
        exit 1;
    }
    if ( $options > 1 )
    {
        print "Please specify only one of --lvm or --dir - not both!\n";
        exit 1;
    }

    #
    #  Convert from Gb -> Mb;
    #
    if ( $CONFIG{ 'add' } =~ /^([0-9.]+)Gb*$/i )
    {
        $CONFIG{ 'add' } = $1 * 1024;
    }
    if ( $CONFIG{ 'add' } =~ /^([0-9.]+)Mb*$/i )
    {
        $CONFIG{ 'add' } = $1;
    }
}



=begin doc

  Test to see if the given instance is running.

=end doc

=cut

sub xenRunning
{
    my ($hostname) = (@_);

    my $running = 0;

    open( CMD, "xm list $hostname 2>/dev/null |" ) or
      die "Failed to run 'xm list $hostname'";
    while (<CMD>)
    {
        my $line = $_;
        $running = 1 if ( $line =~ /\Q$hostname\E/ );
    }
    close(CMD);

    return ($running);
}
