#!/usr/bin/perl
use strict;
use warnings;
use File::Slurp;
use File::Temp qw/tempdir/;

use Getopt::Long;

my ($image, $store, $vessel, $base, $flags, $skiptest, $partial, $rcfile, $user, $pass);
GetOptions(
    "image=s" => \$image,
    "store=s" => \$store,
    "vessel=s" => \$vessel,
    "base=s" => \$base,
    "flags=s" => \$flags,
    "skip-test" => \$skiptest,
    "partial" => \$partial,
    "rcfile=s" => \$rcfile,
    "username=s" => \$user,
    "password=s" => \$pass,
) or die "Option parsing failure";

die "No image specified" unless $image;
my @datastores = VMware::Vix::Host->datastore;
$store = $datastores[0] if @datastores == 1 and not $store;
die "No datastore specified, and can't intuit it" unless $store;
die "No vessel specified" unless $vessel;
$base = $image . "-base" unless $base;
$flags = "--flags $flags" if $flags;
$skiptest = $skiptest ? "--skip-test" : "";

my $passwd = read_file('.passwd');
chomp $passwd;

# Connect to the host
my $host = VMware::Vix::Host->new( password => $passwd )
    or die VMware::Vix::Host->error;

# Connect to VM
my $vm = $host->open( store => $store, name => $image )
    or die VMware::Vix::Host->error;
die "VM is @{[$vm->power_state]}!\n" if $vm->power_state ne "powered off";

# Find the path to the store
my $path = $host->datastore($store);
die "Can't find store $store" unless $path;

# Explode if either path is mounted
my $mounted = qx(/usr/bin/vmware-mount -L);
die "Disk is already mounted; please unmount before continuing.\n"
    if $mounted =~ m{$path/($image|$base)};

# XXX: This used to be a tmpfile, but it exploded -- I don't recall
# how right now.  For now, you need a "disk-image" directory to mount
# it under.
my $mountpoint = "disk-image";

# Clone from image if base doesn't exist
unless ( -e "$path/$base" ) {
    unless ( -e "$path/$image" ) {
        die "Can't find image in $path/$image to clone from";
    }
    warn "Creating base '$base' from '$image'...\n";
    !system( '/usr/bin/rsync', "-az", "$path/$image/", "$path/$base/" )
        or die "rsync failed";
}

unless ($partial) {
    # Rsync to clean it up
    warn "Cloning image...\n";
    !system(
        '/usr/bin/rsync',   "-az", "--delete",
        "$path/$base/", "$path/$image/"
    ) or die "rsync failed";
}

# Mount the disk image
$vm->mount($mountpoint);

# Copy files over
warn "Installing source...\n";
system( '/bin/rm', '-rf', "$mountpoint/opt/build", "$mountpoint/opt/install" )
    if $partial;
!system( '/usr/bin/svn', 'co', '-q', $vessel, "$mountpoint/opt/build" )
    or die "svn co failed";

# Write init file
open( RC, ">", "$mountpoint/etc/rc.local" ) or die "Can't write rc.init: $!";
print RC <<EOT;
#!/bin/sh
aptitude install build-essential autoconf libtool -y
cd /opt/build/
./bin/shipwright-builder --install-base /opt/install $flags $skiptest 2>&1 | tee /opt/build/complete.log 
halt
EOT
close RC;

# Unmount
`sync`;
sleep 5;
$vm->unmount;

# Start 'er up!
warn "Starting build...\n";
$vm->power_on;

# Wait for it to finish
my $laststate = "";
my $lastinstall = 0;
{
    sleep 10;
    my $state = $vm->power_state;
    if ($laststate ne $state) {
        warn ucfirst($state) . "...\n";
        if ($state =~ /tools running/ and $laststate !~ /tools running/ and $user and $pass) {
            warn "Logging in..\n";
            $vm->login($user, $pass);
        }
    }
    if ($state =~ /tools running/ and $user and $pass) {
        require YAML;
        eval {$vm->copy("/opt/install/installed.yml","installed.yml")};
        unless ($@) {
            my $ref = YAML::LoadFile("installed.yml");
            if ($ref) {
                warn "Installed " . scalar(@{$ref}) ." packages\n"
                    if scalar(@{$ref}) != $lastinstall;
                $lastinstall = scalar @{$ref};
            }
        }
    }
    $laststate = $state;
    redo unless $state =~ /powered off/;
}
sleep 20;

# Check if it succeeded
$vm->mount($mountpoint);
!system( "cp", "$mountpoint/opt/build/complete.log", "complete.log" )
    or warn "(Copy of log failed?)\n";
unlink("$mountpoint/etc/rc.local");
die "Build failure!  See complete.log\n"
    unless -e "$mountpoint/opt/install/bin-wrapped";
!system( 'cp', $rcfile, "$mountpoint/etc/rc.local" )
    or warn "(Copy of rc.local failed?)\n" if $rcfile;

# If we want a partial build, don't clone into a clean image, just
# stop now
if ($partial) {
    warn "Partial image build successful!\n";
    exit;
}

# Copy out of the image
warn "Successfully built!  Copying out of image...\n";
!system(
    "/usr/bin/rsync", "-az",
    "--delete",       "$mountpoint/opt/install/",
    "installed-image/"
) or die "rsync extract failed";
$vm->unmount;

# Rsync a clean copy over
warn "Cloning a clean image...\n";
!system(
    "/usr/bin/rsync",   "-az", "--delete",
    "$path/$base/", "$path/$image/"
) or die "rsync failed";

# Mount it again, and copy the built version
warn "Installing binaries...\n";
$vm->mount($mountpoint);
!system(
    "/usr/bin/rsync", "-az",
    "installed-image/", "$mountpoint/opt/install/"
) or die "rsync placement failed";
!system( 'cp', $rcfile, "$mountpoint/etc/rc.local" )
    or die "run rc.init copy failed" if $rcfile;

# Prepend the installed path to PATH
my $PATH = do {local @ARGV = "$mountpoint/etc/environment"; $_ = <>; close ARGV; $_};
$PATH =~ s/PATH="(.*)"\n?/$1/;
open(ENV, ">", "$mountpoint/etc/environment") or die "Can't open environment for writing: $!";
print ENV qq{PATH="/opt/install/bin:$PATH"\n};
close ENV;

# Unmount
`sync`;
$vm->unmount;

# Snapshot in a clean state, then power it on to take it for a test ride
$vm->power_on;
warn "Image started!\n";


package VMware::Vix::Host;
use VMware::Vix::Simple;
use VMware::Vix::API::Constants;
use Carp;

use XML::Simple;
our %DATASTORES;

BEGIN {
    my $stores
        = XMLin( "/etc/vmware/hostd/datastores.xml", ForceArray => ["e"] );
    if ($stores) {
        for my $k ( keys %{ $stores->{LocalDatastores}{e} } ) {
            $DATASTORES{$k} = $stores->{LocalDatastores}{e}{$k}{path};
        }
    }
}

sub new {
    my $class = shift;
    my %args  = @_;
    my ( $err, $hostHandle ) = HostConnect( VIX_API_VERSION,
        VIX_SERVICEPROVIDER_VMWARE_VI_SERVER,
        $args{host} || "https://localhost:8333/sdk",
        0,
        $args{user} || $ENV{USER},
        $args{password},
        0,
        VIX_INVALID_HANDLE
    );
    croak "VMware::Vix::Host->new: " . GetErrorText($err) if $err != VIX_OK;
    return bless \$hostHandle, $class;
}

sub vms {
    my $self = shift;
    my ( $err, @vms ) = FindItems( $$self, VIX_FIND_REGISTERED_VMS, 0 );
    croak "VMware::Vix::Host->vms: " . GetErrorText($err) if $err != VIX_OK;
    return @vms;
}

sub open {
    my $self = shift;
    return VMware::Vix::VM->new( @_, host => $self );
}

sub disconnect {
    my $self = shift;
    HostDisconnect($$self);
}

sub datastore {
    my $class = shift;
    return keys %DATASTORES unless @_;
    my $name  = shift;
    return $DATASTORES{$name};
}

sub DESTROY {
    shift->disconnect;
}

package VMware::Vix::VM;
use VMware::Vix::Simple;
use VMware::Vix::API::Constants;
use Scalar::Util qw/dualvar/;
use Carp;

our %PROPERTY;
our %POWERSTATE;
our %MOUNTS;

BEGIN {
    %PROPERTY = (
        power_state   => VIX_PROPERTY_VM_POWER_STATE,
        pathname      => VIX_PROPERTY_VM_VMX_PATHNAME,
        team_pathname => VIX_PROPERTY_VM_VMTEAM_PATHNAME,
    );
    %POWERSTATE = (
        VIX_POWERSTATE_POWERING_OFF()   => "powering off",
        VIX_POWERSTATE_POWERED_OFF()    => "powered off",
        VIX_POWERSTATE_POWERING_ON()    => "powering on",
        VIX_POWERSTATE_POWERED_ON()     => "powered on",
        VIX_POWERSTATE_SUSPENDING()     => "suspending",
        VIX_POWERSTATE_SUSPENDED()      => "suspended",
        VIX_POWERSTATE_TOOLS_RUNNING()  => "tools running",
        VIX_POWERSTATE_RESETTING()      => "resetting",
        VIX_POWERSTATE_BLOCKED_ON_MSG() => "blocked on message",
        VIX_POWERSTATE_PAUSED()         => "paused",
#       0x0400                          ,  "??",
        VIX_POWERSTATE_RESUMING()       => "resuming",
    );
}

sub new {
    my $class = shift;
    my %args  = @_;
    croak "No host given"
        unless $args{host} and $args{host}->isa("VMware::Vix::Host");
    if ( $args{image} ) {
    } elsif ( $args{store} and ( $args{path} || $args{name} ) ) {
        croak "Datastore $args{store} not known" unless $args{host}->datastore( $args{store} );
        $args{image}
            = $args{path}
            ? "[$args{store}] $args{path}"
            : "[$args{store}] $args{name}/$args{name}.vmx";
    } else {
        croak "Must specify either an 'image' or a 'store' and 'path'";
    }
    my ( $err, $vmHandle ) = VMOpen( ${ $args{host} }, $args{image} );
    croak "VMware::Vix::VM->new: " . GetErrorText($err) if $err != VIX_OK;
    return bless \$vmHandle, $class;
}

sub get_property {
    my $self = shift;
    my %args = @_;
    croak "No name provided" unless $args{name};
    croak "No lookup value for $args{name}"
        unless exists $PROPERTY{ $args{name} };
    my ( $err, $value ) = GetProperties( $$self, $PROPERTY{ $args{name} } );
    croak "VMware::Vix::VM->get_property: " . GetErrorText($err)
        if $err != VIX_OK;
    return $value;
}

sub power_state {
    my $self = shift;
    my $num = $self->get_property( name => "power_state" );
    my @flags
        = map { $POWERSTATE{$_} } grep { $num & $_ } sort {$a <=> $b} keys %POWERSTATE;
    return dualvar( $num, join( ", ", @flags ) || "??" );
}

sub power_on {
    my $self = shift;
    my %args = @_;
    my $err  = VMPowerOn( $$self, VIX_VMPOWEROP_NORMAL, VIX_INVALID_HANDLE );
    croak "VMware::Vix::VM->power_on: " . GetErrorText($err)
        if $err != VIX_OK;
    return 1;
}

sub absolute {
    my $self = shift;
    my $path = shift;
    $path =~ s{^\[(.*?)\] }{VMware::Vix::Host->datastore($1)."/"}e and defined VMware::Vix::Host->datastore($1)
        or return undef;
    return $path;
}

sub path {
    my $self = shift;
    return $self->get_property( name => "pathname" );
}

sub disk {
    my $self = shift;
    my $path = $self->get_property( name => "pathname" );
    $path =~ s/\.vmx$/.vmdk/;
    return $path;
}

sub mount {
    my $self = shift;
    my $path = shift;
    !system( '/usr/bin/vmware-mount', $self->absolute( $self->disk ), $path )
        or croak "mount failed: $@";
    $MOUNTS{"$self"} = $path;
    return 1;
}

sub unmount {
    my $self = shift;
    return unless $MOUNTS{"$self"};
    !system( '/usr/bin/vmware-mount', '-d', delete $MOUNTS{"$self"} )
        or croak "unmount failed: $@";
    return 1;
}

sub snapshot {
    my $self = shift;
    my ( $err, $snapHandle ) = VMCreateSnapshot(
        $$self,
        undef,    # name
        undef,    #description
        VIX_SNAPSHOT_INCLUDE_MEMORY,
        VIX_INVALID_HANDLE
    );
    croak "VMware::Vix::VM->snapshot: " . GetErrorText($err)
        if $err != VIX_OK;
    return $snapHandle;
}

sub login {
    my $self = shift;
    my ($user, $password) = @_;
    my $err = VMLoginInGuest( $$self, $user, $password, 0);
    croak "VMware::Vix::VM->login: " . GetErrorText($err)
        if $err != VIX_OK;
    return 1;
}

sub copy {
    my $self = shift;
    my ($src, $dst) = @_;
    my $err = VMCopyFileFromGuestToHost($$self, $src, $dst, 0, VIX_INVALID_HANDLE);
    croak "VMware::Vix::VM->copy: " . GetErrorText($err)
        if $err != VIX_OK;
    return 1;
}

DESTROY {
    my $self = shift;
    $self->unmount;
    ReleaseHandle($$self);
}
