#!/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 <&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); }