#!/usr/bin/perl
#######################################################################
# Copyright 2008-2011 VMware, Inc.  All rights reserved.
#######################################################################
# vibddi - Query and update vibs on a VMvisor dd image
#######################################################################

use Cwd 'abs_path';
use File::Basename;
use File::Copy;
use File::Path;
use Getopt::Std;
use Digest::MD5;

$Getopt::Std::STANDARD_HELP_VERSION = 1;

#######################################################################
#
# VERSION_MESSAGE --
#
#      Callback when "--version" arg is used
#
# Results
#      None
#
# Side effects:
#      None
#
#######################################################################

sub VERSION_MESSAGE
{
   print "Version: " . GetVibddiVersion() . "\n";
}


#######################################################################
#
# HELP_MESSAGE --
#
#      Callback when "--help" arg is used
#
# Results
#      None
#
# Side effects:
#      None
#
#######################################################################

sub HELP_MESSAGE
{
   PrintHelp($scriptName);
}


#######################################################################
#
# PrintHelp --
#
#      Prints how to use this utility
#
# Results
#      None
#
# Side effects:
#      None
#
#######################################################################

sub PrintHelp
{
   my $scriptName = shift;
   print<<__END_HELP__;

$scriptName: Query and update vibs on a VMvisor dd image

Usage:
$scriptName -h --- Print this

$scriptName -i <VMvisor-dd> -q --- Query vibs installed on the image

$scriptName -i <VMvisor-dd> -v <vib-path> [ -g ] --- Update the image with a single vib

$scriptName -i <VMvisor-dd> -m <metadata-URL> -b <bulletin-ID> [ -p <proxy> ] [ -g ] --- Update the image with an online bulletin

$scriptName -i <VMvisor-dd> -o <bundle-path> [ -g ] --- Update the image with an offline bundle

Where:
VMvisor-dd - The VMvisor dd image that is going to be customized

vib-path - The local file path to the vib

metadata-URL - The URL to the metadata.zip file (Ex. http://www.isv.com/depot/metadata.zip)

bulletin-ID - The bulletin ID to install

bundle-path - The local file path to the offline bundle

proxy (OPTIONAL) - Proxy used to download vib, for update operation only

-g (OPTIONAL) - Generate customized ThinESX/Recovery CD ISOs

__END_HELP__

   return;
}


#######################################################################
#
# ParseArgs --
#
#      Validates arguments passed by user and sets up variables
#
# Results
#      Returns the following string values:
#      - The image path
#      - Operation to perform (query, vib, bulletin, bundle)
#      - Target path (file or URL)
#      - Bulletin ID
#      - Proxy Server
#      - No signature check flag
#      - Generate ThinESX/Recovery CD ISOs flag
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub ParseArgs
{
   my %options = %{$_[0]};

   my ($visorTarget,
       $operation,
       $targetPath,
       $bulletinId,
       $proxy,
       $sigCheck,
       $generateCds);

   if (exists $options{"h"}) {
      PrintHelp($scriptName);
      exit($ERR_CODES{"Success"}{"RetCode"});
   }

   if (exists $options{"n"}) {
      $sigCheck = 0;
      LogEvent($scriptName,
               "WARNING: Bypassing signature verification is not supported");
   } else {
      $sigCheck = 1;
   }

   if (exists $options{"g"}) {
      $generateCds = 1;
   } else {
      $generateCds = 0;
   }

   if (exists $options{"p"}) {
      $proxy = $options{"p"};
   }

   if (exists $options{"i"}) {
      $visorTarget = abs_path($options{"i"});
      if (! -e $visorTarget) {
         LogEventAndDie($scriptName, "NotFound", $visorTarget);
      }
      if (IsVisor($visorTarget) != 0) {
         LogEventAndDie($scriptName, "BadImage", $visorTarget);
      }
   }

   if (exists $options{"q"}) {
      $operation = "query";
   }

   if (exists $options{"v"}) {
      if (length($operation) > 0) {
         LogEventAndDie($scriptName, "BadArgs", "multiple operations specified");
      }
      $operation = "vib";
      $targetPath = abs_path($options{"v"});
      if (! -e $targetPath) {
         LogEventAndDie($scriptName, "NotFound", $targetPath);
      }
   }

   if (exists $options{"m"}) {
      if (length($operation) > 0) {
         LogEventAndDie($scriptName, "BadArgs", "multiple operations specified");
      }
      $operation = "bulletin";
      if (! exists $options{"b"}) {
         LogEventAndDie($scriptName, "BadArgs", "bulletin ID not specified");
      }
      $targetPath = $options{"m"};
      $bulletinId = $options{"b"};
   }

   if (exists $options{"o"}) {
      if (length($operation) > 0) {
         LogEventAndDie($scriptName, "BadArgs", "multiple operations specified");
      }
      $operation = "bundle";
      $targetPath = abs_path($options{"o"});
      if (! -e $targetPath) {
         LogEventAndDie($scriptName, "NotFound", $targetPath);
      }
   }

   if (length($visorTarget) == 0) {
      LogEventAndDie($scriptName, "BadArgs", "no targets (-i, -d) specified");
   }

   if (length($operation) == 0) {
      LogEventAndDie($scriptName, "BadArgs", "no operation specified");
   }

   return ($visorTarget,
           $operation,
           $targetPath,
           $bulletinId,
           $proxy,
           $sigCheck,
           $generateCds);
}


#######################################################################
#
# RunCommand --
#
#      Executes an external command and logs stdout/stderr
#
# Results
#      Returns the return code of the command
#
# Side effects:
#      The application will terminate here if the command
#      is not found
#
#######################################################################

sub RunCommand
{
   my $command = shift;

   my @tempArray = split(/ /, $command);
   my $appName = $tempArray[0];

   LogEvent($scriptName, "Command: $command", 1);
   $command .= " 2>&1";
   my ($output, $rc) = PipeReadMulti($command, 1);
   foreach $line (@$output) {
      LogEvent($scriptName, "$appName: $line", 1);
   }
   if (length($rc) == 0) {
      $rc = 127;
   }

   if ($rc == 127) {
      LogEventAndDie($scriptName, "DepMissing", $appName);
   } else {
      LogEvent($scriptName, "$appName return code: $rc", 1);
   }

   return $rc;
}


#######################################################################
#
# GetPartitionInfo --
#
#      Uses fdisk to obtain partition information from the
#      dd image
#
# Results
#      Returns the sector size, bootbank offset, and altbootbank
#      offset as three separate values
#
# Side effects:
#      None
#
#######################################################################

sub GetPartitionInfo
{
   my $loopDev = shift;
   my $visorTarget = shift;

   my ($sectorSize,
       $part5Offset,
       $part6Offset,
       $part8Offset);
   my $bankDev5 = $loopDev . "p5";
   my $bankDev6 = $loopDev . "p6";
   my $bankDev8 = $loopDev . "p8";

   if (RunCommand("/sbin/losetup -r $loopDev $visorTarget") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   my @fdiskLines = PipeReadMulti("/sbin/fdisk -lu $loopDev");
   if (RunCommand("/sbin/losetup -d $loopDev") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }

   foreach $line (@fdiskLines) {
      if ($line =~ m/Units = sectors of 1/) {
         my @tempArray = split(/ /, $line);
         $sectorSize = $tempArray[$#tempArray - 1];
      } elsif ($line =~ m/$bankDev5/) {
         my @tempArray = split(/ +/, $line);
         $part5Offset = $tempArray[1];
      } elsif ($line =~ m/$bankDev6/) {
         my @tempArray = split(/ +/, $line);
         $part6Offset = $tempArray[1];
      } elsif ($line =~ m/$bankDev8/) {
         my @tempArray = split(/ +/, $line);
         $part8Offset = $tempArray[1];
      }
   }

   return ($sectorSize, $part5Offset, $part6Offset, $part8Offset);
}


#####################################################################
#
# RunCheck --
#
#      Validates the VFAT partition at an offset into a dd image
#      or on the USB device
#
# Results
#      None
#
# Side effects:
#      The application will terminate here if an error occurs
#
#####################################################################

sub RunCheck
{
   my $visorTarget = shift;
   my $partitionOffset = shift;

   my $checkCmd = "/sbin/ddfsck -i $visorTarget -o $partitionOffset";
   my $rc = RunCommand($checkCmd);
   if ($rc != 0) {
      LogEventAndDie($scriptName,
                     "BadCheckOp",
                     $DDFSCK_ERR_MSGS{$rc} . " (return code $rc)");
   }
}


#####################################################################
#
# ParseBootCfg --
#
#      Parses the bootloader configuration and returns a
#      hash with the properties as keys
#
# Results
#      Returns the populated hash, otherwise an
#      empty hash
#
# Side effects:
#      None
#
#####################################################################

sub ParseBootCfg
{
   my $bootcfgPath = shift;
   my $bootCfg = {};

   open(FILE, "<$bootcfgPath") || return $bootCfg;
   foreach $line (<FILE>) {
      chomp($line);
      if (length($line) > 0) {
         my @tempArray = split(/=/, $line);
         my $key = lc(shift(@tempArray));
         $bootCfg->{$key} = StringTrim(join("=", @tempArray));
      }
   }
   close(FILE);

   return $bootCfg;
}


#######################################################################
#
# GetFirmwareVersion --
#
#      Returns the firmware version from boot.cfg
#
# Results
#      Returns the version as a string
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub GetFirmwareVersion
{
   my $bootcfgPath = shift;

   my $bootCfg = ParseBootCfg($bootcfgPath);
   if (scalar(keys(%$bootCfg)) == 0) {
      LogEventAndDie($scriptName, "BadFileRead", $bootcfgPath);
   }

   return $bootCfg->{"build"};
}


#######################################################################
#
# ParseDebianStatus --
#
#      Parses a Debian status database and appends installed
#      package name and version to an array
#
# Results
#      Returns the packages array, otherwise an empty array
#
# Side effects:
#      None
#
#######################################################################

sub ParseDebianStatus
{
   my $statusPath = shift;

   my @installedPkgs = ();
   my $packageName;
   my $versionString;

   open(FILE, "<$statusPath") || return @installedPkgs;
   foreach $line (<FILE>) {
      chomp($line);
      if ($line =~ m/^Package/i) {
         my @tempArray = split(/:/, $line);
         $packageName = StringTrim($tempArray[1]);
      } elsif ($line =~ m/^Version/i) {
         my @tempArray = split(/:/, $line);
         $versionString = StringTrim($tempArray[1]);
         push(@installedPkgs, "< $packageName ($versionString) >\n");
      } elsif ($line =~ m/^Provides/i) {
         my @tempArray = split(/:/, $line);
         @tempArray = split(/,/, $tempArray[1]);
         foreach $package (@tempArray) {
            push(@installedPkgs, "< " . StringTrim($package) . " >\n");
         }
      }
   }
   close(FILE);

   return @installedPkgs;
}


#######################################################################
#
# RunQuery --
#
#      Finds the status database in oem.tgz and m.z (previously mod.tgz), 
#      parses it, and outputs packages installed on the VMvisor image.
#      Also outputs firmware, client, and tools versions.
#
# Results
#      Prints installed packages
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub RunQuery
{
   my $bootbankPath = shift;

   my $stageDir = "/tmp/ddistage";
   my $statusPath = "$stageDir/status";
   my $lockerStatusPath = "/locker/packages/usr/lib/ipkg/status";
   my $pkgdbDir = "$stageDir/var/db/esxupdate";

   my @installedPkgs = ();
   my @pkgDbs = ([ "$bootbankPath/oem.tgz", "$pkgdbDir/oem-pkgdb.tgz" ],
                 [ "$bootbankPath/m.z", "$pkgdbDir/custom-pkgdb.tgz" ],
                 [ "$bootbankPath/oem.tgz", "$stageDir/pkgdb.tgz" ],
                 [ "$bootbankPath/mod.tgz", "$stageDir/pkgdb.tgz" ],
                 [ "$bootbankPath/extmod.tgz", "$stageDir/pkgdb.tgz" ],
                 [ "$bootbankPath/pkgdb.tgz", "" ]);

   foreach my $bootModule (@pkgDbs) {
      my ($tgzTarget, $pkgdbPath) = @$bootModule;
      if (! -e $tgzTarget) {
         LogEvent($scriptName, "$tgzTarget does not exist, skipping", 1);
         next;
      }
      LogEvent($scriptName, "Checking if $tgzTarget is empty", 1);
      my $tarOutput = PipeReadSingle("tar -tf $tgzTarget");
      if (length($tarOutput) == 0) {
         LogEventAndDie($scriptName, "BadPipeRead", "tar");
      }
      if ($tarOutput ne ".emptytgz") {
         LogEvent($scriptName, "$tgzTarget is not empty, extracting", 1);
         if (CreateFreshDir($stageDir) != 0) {
            LogEventAndDie($scriptName, "BadDir", $stageDir);
         }
         if (RunCommand("tar -zxf $tgzTarget -C $stageDir") != 0) {
            LogEventAndDie($scriptName, "BadCmd");
         }

         if (length($pkgdbPath) != 0) {
            if (! -e $pkgdbPath) {
               LogEvent($scriptName, "$pkgdbPath does not exist, skipping", 1);
               next;
            }

            if (RunCommand("tar -zxf $pkgdbPath -C $stageDir") != 0) {
               LogEventAndDie($scriptName, "BadCmd");
            }
         }

         if (! -e $statusPath) {
            LogEvent($scriptName, "$statusPath does not exist, skipping", 1);
            next;
         }

         LogEvent($scriptName, "Reading database: $statusPath", 1);
         @installedPkgs = ParseDebianStatus($statusPath);

         foreach $pkgLine (@installedPkgs) {
            print STDOUT $pkgLine;
         }

         eval { rmtree($stageDir) };
      }
   }

   if (! -e $lockerStatusPath) {
      print STDOUT "< VI Client (factory default) >\n";
      print STDOUT "< VMware Tools (factory default) >\n";
   } else {
      LogEvent($scriptName, "Reading database: $lockerStatusPath", 1);
      @installedPkgs = ParseDebianStatus($lockerStatusPath);
      if (scalar(@installedPkgs) == 0) {
         LogEventAndDie($scriptName, "BadFileRead", $lockerStatusPath);
      }

      foreach $pkgLine (@installedPkgs) {
         print STDOUT $pkgLine;
      }
   }

   if (CleanUpMounts() != 0) {
      LogEventAndDie($scriptName, "BadUmount", $MOUNT_POINTS[$#MOUNT_POINTS]);
   }
}


#######################################################################
#
# ExtractVisorConfig --
#
#      Extract and set up Visor-specific data for esx4update
#
# Results
#      None
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub ExtractVisorConfig
{
   my $bootbankPath = shift;
   my $generateCds = shift;
   my $cdCache = shift;

   my $vgzName;
   my $vgzVMTarName;
   my $vgzTarName;
   my $stageDir = "/tmp/ddistage";

   my @vgzTargets = ("base", "sys", "s");
   foreach $vgzTarget (@vgzTargets) {
      if (-e "$bootbankPath/$vgzTarget.vgz") {
         $vgzName = "$vgzTarget.vgz";
         $vgzVMTarName = "$vgzTarget.vmtar";
         $vgzTarName = "$vgzTarget.tar";
         last;
      }
      if (-e "$bootbankPath/$vgzTarget.z") {
         $vgzName = "$vgzTarget.z";
         $vgzVMTarName = "$vgzTarget.vmtar";
         $vgzTarName = "$vgzTarget.tar";
         last;
      }
   }

   if (length($vgzName) == 0) {
      LogEventAndDie($scriptName, "BadImage");
   }

   LogEvent($scriptName, "Extracting $vgzName from $bootbankPath", 1);

   if (CreateFreshDir($stageDir) != 0) {
      LogEventAndDie($scriptName, "BadDir", $stageDir);
   }

   if (! -e "$bootbankPath/$vgzName") {
      LogEventAndDie($scriptName, "BadImage");
   }

   copy("$bootbankPath/$vgzName", "$stageDir/$vgzName") ||
      LogEventAndDie($scriptName, "BadCopy", "$stageDir/$vgzName");

   if (RunCommand("zcat $stageDir/$vgzName " .
                  "> $stageDir/$vgzVMTarName") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   if (RunCommand("vmtar -x $stageDir/$vgzVMTarName " .
                  "-o $stageDir/$vgzTarName") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   if (RunCommand("tar -xf $stageDir/$vgzTarName -C $stageDir") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }

   my $esxupdateDir = "etc/vmware/esxupdate";
   my $ipkgDir = "usr/lib/ipkg";

   my @environDirs = ("/$esxupdateDir", "/$ipkgDir");

   foreach $dir (@environDirs) {
      if (-d $dir) {
         LogEvent($scriptName, "Directory $dir already exists", 1);
      } else {
         if (CreateDir($dir) != 0) {
            LogEventAndDie($scriptName, "BadDir", $dir);
         }
      }
   }

   my @environPaths = ("$esxupdateDir/esxupdate.conf",
                       "$ipkgDir/status",
                       "sbin/cim-preinst.sh",
                       "sbin/cim-install.sh",
                       "sbin/vmkmod-preinst.sh",
                       "sbin/vmkmod-install.sh");

   foreach $file (@environPaths) {
      my $newFilePath = "/$file";
      if ($file =~ m/^sbin/i) {
         $newFilePath = "/usr$newFilePath";
      }
      LogEvent($scriptName, "Setting up $stageDir/$file as $newFilePath", 1);
      copy("$stageDir/$file", $newFilePath) ||
         LogEventAndDie($scriptName, "BadCopy", $newFilePath);
      if (exists($ENV{"ENV_NO_CHMOD"})) {
         LogEvent($scriptName, "Skipping chmod: $newFilePath", 1);
      } else {
         chmod(0744, $newFilePath) ||
            LogEventAndDie($scriptName, "BadChmod", "/$file");
      }
   }

   eval { rmtree($stageDir) };

   my $esxupdateLog = "/var/log/esxupdate.log";
   LogEvent($scriptName, "Setting esxupdate log location: $esxupdateLog", 1);
   if (WriteEnvProp("log.file", $esxupdateLog) != 0) {
      LogEventAndDie($scriptName, "BadConf", "log.file");
   }

   LogEvent($scriptName, "Setting offline mode for esxupdate", 1);
   if (WriteEnvProp("platform.offline", "true") != 0) {
      LogEventAndDie($scriptName, "BadConf", "platform.offline");
   }

   LogEvent($scriptName, "Setting restricted mode for esxupdate", 1);
   if (WriteEnvProp("platform.restricted", "true") != 0) {
      LogEventAndDie($scriptName, "BadConf", "platform.restricted");
   }

   if ($generateCds == 1) {
      LogEvent($scriptName,
               "Creating cache directory for generating ISOs: $cdCache",
               1);

      if (CreateFreshDir($cdCache) != 0) {
         LogEventAndDie($scriptName, "BadDir", $cdCache);
      }

      if (WriteEnvProp("vibddi.cdcache", $cdCache) != 0) {
         LogEventAndDie($scriptName, "BadConf", "vibddi.cdcache");
      }
   } else {
      if (WriteEnvProp("vibddi.cdcache", "none") != 0) {
         LogEventAndDie($scriptName, "BadConf", "vibddi.cdcache");
      }
   }

   my $esxupdateStageDir = ReadEnvProp("visor.packagingdir");
   if (-d $esxupdateStageDir) {
      eval { rmtree($esxupdateStageDir) };
   }
}


#######################################################################
#
# GetVibddiVersion --
#
#      Returns the version of the vibddi RPM installed
#
# Results
#      Returns the version as a string
#
# Side effects:
#      None
#
#######################################################################

sub GetVibddiVersion
{
   my @rpmNames = ("vibddi",
                   "vibddi64");

   foreach $rpm (@rpmNames) {
      my $rpmCmd = "rpm -q --queryformat '%{VERSION}-%{RELEASE}\n' vmware-esx-$rpm";
      my $rpmOutput = PipeReadSingle($rpmCmd);
      if ($rpmOutput !~ m/not installed/i) {
         return $rpmOutput;
      }
   }

   return "Unavailable";
}


#######################################################################
#
# BrandBootbank --
#
#      Prints command and version information about an
#      update operation to the ESXi bootbank
#
# Results
#      None
#
# Side effects:
#      None
#
#######################################################################

sub BrandBootbank
{
   my $bootbankPath = shift;
   my $updateCmd = shift;

   my $rpmVersion = GetVibddiVersion();
   open(FILE, ">>$bootbankPath/vibddi");
   print FILE "Version: $rpmVersion\n";
   print FILE "Command: $updateCmd\n\n";
   close(FILE);
}


#####################################################################
#
# RunUpdate --
#
#      Calls esx4update update operation
#
# Results
#      None
#
# Side effects:
#      The application will terminate here if an error occurs
#
#####################################################################

sub RunUpdate
{
   my $visorTarget = shift;
   my $bootbankOffset = shift;
   my $bootbankPath = shift;
   my $operation = shift;
   my $targetPath = shift;
   my $bulletinId = shift;
   my $proxy = shift;
   my $sigCheck = shift;

   LogEvent($scriptName, "Checking bootbank filesystems (pre-install)");
   RunCheck($visorTarget, $bootbankOffset);

   my $updateCmd = "/usr/sbin/esxupdate --nocache";
   if (length($proxy) > 0) {
      LogEvent($scriptName, "Using --http_proxy option: $proxy", 1);
      $updateCmd .= " --http_proxy=$proxy";
   }
   if ($sigCheck == 0) {
      LogEvent($scriptName, "Using --nosigcheck option", 1);
      $updateCmd .= " --nosigcheck";
   }

   if ($operation eq "vib") {
      LogEvent($scriptName, "Installing vib: $targetPath");
      $updateCmd .= " -b \"$targetPath\" update";
   } elsif ($operation eq "bulletin") {
      LogEvent($scriptName, "Installing: $bulletinId from $targetPath");
      $updateCmd .= " -m \"$targetPath\" -b \"$bulletinId\" update";
   } else {
      LogEvent($scriptName, "Installing offline bundle: $targetPath");
      $updateCmd .= " --bundle \"$targetPath\" update";
   }

   LogEvent($scriptName, "esxupdate log: " . ReadEnvProp("log.file"), 1);

   my $rc = RunCommand($updateCmd);
   if (($rc != 0) && ($rc != 80) && ($rc != 81)) {
      LogEventAndDie($scriptName,
                     "BadOp",
                     $ESX4UPDATE_ERR_MSGS{$rc} . " (return code $rc)");
   }

   LogEvent($scriptName, $ESX4UPDATE_ERR_MSGS{$rc} . " (return code $rc)", 1);
   LogEvent($scriptName, "Update operation completed successfully");

   my $firmwareVersion = GetFirmwareVersion("$bootbankPath/boot.cfg");

   BrandBootbank($bootbankPath, $updateCmd);

   if (CleanUpMounts() != 0) {
      LogEventAndDie($scriptName, "BadUmount", $MOUNT_POINTS[$#MOUNT_POINTS]);
   }

   LogEvent($scriptName, "Checking bootbank filesystems (post-install)");
   RunCheck($visorTarget, $bootbankOffset);

   return $firmwareVersion;
}


#######################################################################
#
# ParseInstallerConsts --
#
#      Reads the Consts.py file and parses out hard coded
#      image name, size, and product title
#
# Results
#      Returns image name, size, and product title, otherwise
#      three empty strings
#
# Side effects:
#      None
#
########################################################################

sub ParseInstallerConsts
{
   my $constsPath = shift;

   my ($imageName, $imageSize, $title);

   open(FILE, "$constsPath") || return ($imageName, $imageSize, $title);
   foreach $line (<FILE>) {
      if ($line =~ m/^VMVISOR_IMAGE/i) {
         my @temp = split(/=/, $line);
         $imageName = basename(StringTrim($temp[1]));
      } elsif ($line =~ m/^VISOR_IMAGE_SIZE/i) {
         my @temp = split(/=/, $line);
         $imageSize = StringTrim($temp[1]);
      } elsif ($line =~ m/^PRODUCT_TITLE/i) {
         my @temp = split(/=/, $line);
         $title = StringTrim($temp[1]);
      }
   }
   close(FILE);

   $imageName =~ s/\"//g;
   $title =~ s/\"//g;

   return ($imageName, $imageSize, $title);
}


#######################################################################
#
# UpdateInstallerConsts --
#
#      Updates image path and product title string variables to
#      reflect a new firmware version if necessary
#
# Results
#      Returns 0 if the file is updated, otherwise 1
#
# Side effects:
#      None
#
########################################################################

sub UpdateInstallerConsts
{
   my $constsPath = shift;
   my $newImageName = shift;
   my $newFirmwareVer = shift;
   my $oldTitle = shift;

   open(FILE, "$constsPath") || return 1;
   open(NEWFILE, ">$constsPath.new") || return 1;
   foreach $line (<FILE>) {
      if ($line =~ m/^VMVISOR_IMAGE/) {
         print NEWFILE "VMVISOR_IMAGE = ";
         print NEWFILE "\"$newImageName\"\n";
      } elsif ($line =~ m/^PRODUCT_TITLE/) {
         print NEWFILE "PRODUCT_TITLE = ";
         print NEWFILE "\"$oldTitle ($newFirmwareVer)\"\n";
      } else {
         print NEWFILE $line;
      }
   }
   close(FILE);
   close(NEWFILE);

   unlink($constsPath) || return 1;
   rename("$constsPath.new", $constsPath) || return 1;
   chmod(0444, $constsPath) || return 1;

   return 0;
}


#######################################################################
#
# RepackInstaller --
#
#      Injects new files from update operation into the ISO
#      filesystem for the installer to use (primarily for drivers)
#      and repacks the vgz
#
# Results
#      None
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub RepackInstaller
{
   my $installerDir = shift;
   my $vgzName = shift;
   my $cdCache = shift;
   my $newIsoDir = shift;
   my $stageDir = shift;

   my $repackDir = "$stageDir/repack";
   if (CreateFreshDir($repackDir) != 0) {
      LogEventAndDie($scriptName, "BadDir", $repackDir);
   }

   my @tgzList = ("$cdCache/mod.tgz",
                  "$cdCache/oem.tgz");

   foreach $tgz (@tgzList) {
      if (-e $tgz) {
         LogEvent($scriptName, "Combining $tgz and $vgzName.vgz", 1);
         if (RunCommand("tar -zxf $tgz -C $installerDir") != 0) {
            LogEventAndDie($scriptName, "BadCmd");
         }
      }
   }

   my @fileList = ListFilesInDir($installerDir);
   if (scalar(@fileList) == 0) {
      LogEventAndDie($scriptName,
                     "BadCd",
                     "Listing files failed: $installerDir");
   }

   my $tarCmd = "tar -cf $repackDir/$vgzName.tar -C $installerDir";
   foreach $file (@fileList) {
      $tarCmd .= " $file";
   }

   if (RunCommand("$tarCmd") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   if (RunCommand("vmtar -c $repackDir/$vgzName.tar " .
                  "-o $repackDir/$vgzName.vmtar") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   if (RunCommand("gzip -c $repackDir/$vgzName.vmtar " .
                  "> $newIsoDir/$vgzName.vgz") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   eval { rmtree($repackDir) };
}


#######################################################################
#
# GenerateCd --
#
#      Generates the customized ThinESX/Recovery CD ISO
#      The customization happens at two levels:
#
#      1.) The installer on the ISO filesystem
#      2.) The dd image that the installer will write to the device
#
# Results
#      None
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub GenerateCd
{
   my $isoPath = shift;
   my $visorTarget = shift;
   my $customDir = shift;
   my $cdCache = shift;
   my $newFirmwareVer = shift;

   my $stageDir = ReadEnvProp("visor.stagedir");
   if (length($stageDir) == 0) {
      LogEventAndDie($scriptName, "BadConf", "visor.stagedir");
   }

   my $mountDir = "$stageDir/isomnt";
   my $newIsoDir = "$stageDir/newiso";
   my $unpackDir = "$stageDir/unpack";
   my $installerDir = "$stageDir/installer";
   my $constsPath = "$installerDir/usr/lib/vmware/installer/Core/Consts.py";

   my @newDirs = ($stageDir,
                  $mountDir,
                  $newIsoDir,
                  $unpackDir,
                  $installerDir);

   foreach $dir (@newDirs) {
      if (CreateFreshDir($dir) != 0) {
         LogEventAndDie($scriptName, "BadDir", $dir);
      }
   }

   LogEvent($scriptName, "Mounting $isoPath to $mountDir", 1);
   if (MountTarget($isoPath, $mountDir, "iso", 1) != 0) {
      LogEventAndDie($scriptName, "BadMount", $isoPath);
   }

   LogEvent($scriptName, "Copying files from $mountDir to $newIsoDir", 1);
   if (CopyDirFirstLevel($mountDir, $newIsoDir) != 0) {
      LogEventAndDie($scriptName, "BadCopy", "$newIsoDir");
   } else {
      if (CleanUpMounts() != 0) {
         LogEventAndDie($scriptName,
                        "BadUmount",
                        $MOUNT_POINTS[$#MOUNT_POINTS]);
      }
   }

   my $ddImage = "imagedd";
   my $ddBzip = "$newIsoDir/$ddImage.bz2";
   if (! -e $ddBzip) {
      LogEventAndDie($scriptName, "BadCd", "Invalid ISO image");
   }

   my $cdType;
   my $vgzName;
   if (-e "$newIsoDir/recovery.vgz") {
      $cdType = "RecoveryCD";
      $vgzName = "recovery";
   } elsif (-e "$newIsoDir/install.vgz") {
      $cdType = "VMvisor-Installer";
      $vgzName = "install";
   } else {
      LogEventAndDie($scriptName, "BadCd", "Invalid ISO image");
   }

   LogEvent($scriptName, "Detected $cdType", 1);
   my $installerVgz = "$newIsoDir/$vgzName.vgz";

   if (RunCommand("zcat $installerVgz > " .
                  "$unpackDir/$vgzName.vmtar") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   if (RunCommand("vmtar -x $unpackDir/$vgzName.vmtar " .
                  "-o $unpackDir/$vgzName.tar") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }
   if (RunCommand("tar -xf $unpackDir/$vgzName.tar " .
                  "-C $installerDir") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }

   my ($imageName, $imageSize, $oldTitle) = ParseInstallerConsts($constsPath);
   if ((length($imageName) == 0) || (length($imageSize) == 0)) {
      LogEventAndDie($scriptName, "BadCd", "Parsing $constsPath failed");
   } else {
      LogEvent($scriptName,
               "Parsed $imageName and $imageSize from $constsPath",
               1);
   }

   if ($imageSize ne (-s $visorTarget)) {
      LogEventAndDie($scriptName, "BadCd", "Image file size mismatch");
   }

   unlink($ddBzip) ||
      LogEventAndDie($scriptName, "BadCd", "Delete failed: $ddBzip");

   copy($visorTarget, "$newIsoDir/$ddImage") ||
      LogEventAndDie($scriptName, "BadCopy", "$newIsoDir/$ddImage");

   $imageName = "VMware-VMvisor-big-$newFirmwareVer-x86_64.dd.bz2";
   if (UpdateInstallerConsts($constsPath,
                             $imageName,
                             $newFirmwareVer,
                             $oldTitle) != 0) {
      LogEventAndDie($scriptName, "BadCd", "Update failed: $constsPath");
   }

   LogEvent($scriptName, "Repacking image and ISO contents to $newIsoDir", 1);
   if (RunCommand("bzip2 -z $newIsoDir/$ddImage") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }

   my $md5File = "$newIsoDir/$ddImage.md5";
   if (GenerateMd5File($ddBzip, $imageName, $md5File) != 0) {
      LogEventAndDie($scriptName, "BadFileWrite", $md5File);
   }

   RepackInstaller($installerDir,
                   $vgzName,
                   $cdCache,
                   $newIsoDir,
                   $stageDir);

   my $newIsoPath = "$customDir/VMware-$cdType-$newFirmwareVer.iso";
   if (-e $newIsoPath) {
      LogEvent($scriptName, "$newIsoPath exists, modifying filename", 1);
      $newIsoPath .= "." . time();
   }

   my $mkisofsCmd = "mkisofs -r -J -l -o $newIsoPath -b isolinux.bin " .
                    "-c boot.cat -no-emul-boot -boot-load-size 4 " .
                    "-boot-info-table $newIsoDir";

   if (RunCommand("$mkisofsCmd") != 0) {
      LogEventAndDie($scriptName, "BadCmd");
   }

   foreach $dir (@newDirs) {
      eval { rmtree($dir) };
   }

   if (-e $newIsoPath) {
      LogEvent($scriptName, "Generated new ISO: $newIsoPath");
   } else {
      LogEventAndDie($scriptName, "BadCd", "mkisofs operation failed");
   }
}


#######################################################################
#
# CustomizeCds --
#
#      Traverse the ISO directory try to customize
#
# Results
#      None
#
# Side effects:
#      The application will terminate here if an error occurs
#
#######################################################################

sub CustomizeCds
{
   my $visorTarget = shift;
   my $cdCache = shift;
   my $newFirmwareVer = shift;

   my $isoDir = "/opt/vmware/vibddi/iso";
   my $customDir = "$isoDir/custom";

   if (CreateFreshDir($customDir) != 0) {
      LogEventAndDie($scriptName, "BadDir", $customDir);
   }

   my @isoPaths = ();
   opendir(DIR, $isoDir);
   my @dirList = readdir(DIR);
   closedir(DIR);
   foreach $file (@dirList) {
      if ($file =~ m/\.iso$/i) {
         push(@isoPaths, "$isoDir/$file");
      }
   }

   if (scalar(@isoPaths) == 0) {
      LogEvent($scriptName, "No ISO files to customized: $isoDir");
   }

   while (@isoPaths) {
      my $isoPath = pop(@isoPaths);
      LogEvent($scriptName, "Generating customized ISO: $isoPath");
      LogEvent($scriptName, "This may take a few minutes...");
      GenerateCd($isoPath,
                 $visorTarget,
                 $customDir,
                 $cdCache,
                 $newFirmwareVer);
   }

   eval { rmtree($cdCache) };
}


#
# Main
#

#
# Set up path variables and include helper routines
#

$scriptName = basename($0);
$scriptPath = dirname(abs_path($0));
require "/usr/lib/vmware/vibddi/perl/utils.pl";

#
# Set up signal handlers that will clean up and release the lock
#

use sigtrap("handler" => \&SignalHandler, "normal-signals");

#
# Validate that we are root and grab the lock
#

if (getpwuid($>) ne "root") {
   print STDERR $ERR_CODES{"NotRoot"}{"ErrMsg"} . "\n";
   exit $ERR_CODES{"NotRoot"}{"RetCode"};
}

#
# Set esxupdate module path and LD_LIBRARY_PATH
#

$ENV{"ESXUPDATEPATH"} = "/usr/lib/vmware/vibddi/python/site-packages";
$ENV{"LD_LIBRARY_PATH"} = "/usr/lib/vmware/vibddi";

#
# Setup and validate arguments
#

my %options = ();
if (! getopts("hngp:i:qv:m:b:o:", \%options)) {
   PrintHelp($scriptName);
   exit($ERR_CODES{"BadArgs"}{"RetCode"});
}

if (Lock() != 0) {
   LogEventAndDie($scriptName, "NoLock");
}

my ($visorTarget,
    $operation,
    $targetPath,
    $bulletinId,
    $proxy,
    $sigCheck,
    $generateCds) = ParseArgs(\%options);

my $isReadOnly = 0;
if ($operation eq "query") {
   $isReadOnly = 1;
}

my ($bootbankOffset, $lockerOffset);
my $bootbankPath = "/bootbank";

my $loopDev = FindLoopDevice();
if ($loopDev == 1) {
   LogEventAndDie($scriptName, "NoLoopDev");
}
LogEvent($scriptName, "Found loop device: $loopDev", 1);

#
# Query sector size and offset information from VMvisor dd
# image with fdisk
#

my ($sectorSize,
    $part5Offset,
    $part6Offset,
    $part8Offset) = GetPartitionInfo($loopDev, $visorTarget);

if (length($sectorSize) == 0) {
   LogEventAndDie($scriptName, "BadFdisk", "sector size");
}

if (length($part5Offset) == 0) {
   LogEventAndDie($scriptName, "BadFdisk", "bootbank partition offset");
}

if (length($part6Offset) == 0) {
   LogEventAndDie($scriptName, "BadFdisk", "altbootbank partition offset");
}

if (length($part8Offset) == 0) {
   LogEventAndDie($scriptName, "BadFdisk", "locker partition offset");
}

$bootbankOffset = $sectorSize * $part5Offset;
$lockerOffset = $sectorSize * $part8Offset;

#
# We only need to extract bits from VMVisor if doing an update operation
# This must be done on every run because different builds of VMvisor are
# not guaranteed to be the same
#

my $cdCache = "/tmp/vibddi/cdcache";
if ($operation ne "query") {
   LogEvent($scriptName, "Setting up configuration for esxupdate");
   LogEvent($scriptName, "This may take a few seconds...");

   #
   # Make sure vital esxupdate dependencies are in place
   #

   my $ipkgPath = "/bin/ipkg";
   if (! -l $ipkgPath) {
      LogEventAndDie($scriptName, "DepMissing", $ipkgPath);
   }

   my $vibenvPath = "/sbin/vib-env";
   if (! -l $vibenvPath) {
      LogEventAndDie($scriptName, "DepMissing", $vibenvPath);
   }

   my $libcryptoPath = "/usr/lib/vmware/vibddi/libcrypto.so.0.9.8";
   if (! -l $libcryptoPath) {
      LogEventAndDie($scriptName, "DepMissing", $libcryptoPath);
   }

   if (CreateDir($bootbankPath) != 0) {
      LogEventAndDie($scriptName, "BadDir", $bootbankPath);
   }

   LogEvent($scriptName,
            "Mounting $visorTarget ($bootbankOffset) to $bootbankPath",
            1);
   if (MountTarget($visorTarget,
                   $bootbankPath,
                   $bootbankOffset,
                   $isReadOnly) != 0) {
      LogEventAndDie($scriptName, "BadMount", $visorTarget);
   }

   ExtractVisorConfig($bootbankPath, $generateCds, $cdCache);
   if (CleanUpMounts() != 0) {
      LogEventAndDie($scriptName, "BadUmount", $MOUNT_POINTS[$#MOUNT_POINTS]);
   }

   $bootbankPath = ReadEnvProp("visor.bootbankdir");
}

#
# Set up mount points and mount the VMvisor bootbanks
#

if (CreateDir($bootbankPath) != 0) {
   LogEventAndDie($scriptName, "BadDir", $bootbankPath);
}

my $lockerPath = "/locker";
if (CreateFreshDir($lockerPath) != 0) {
   LogEventAndDie($scriptName, "BadDir", $lockerPath);
}

LogEvent($scriptName,
         "Mounting $visorTarget ($bootbankOffset) to $bootbankPath",
         1);
if (MountTarget($visorTarget,
                $bootbankPath,
                $bootbankOffset,
                $isReadOnly) != 0) {
   LogEventAndDie($scriptName, "BadMount", $visorTarget);
}

LogEvent($scriptName,
         "Mounting $visorTarget ($lockerOffset) to $lockerPath",
         1);
if (MountTarget($visorTarget, $lockerPath, $lockerOffset, $isReadOnly) != 0) {
   LogEventAndDie($scriptName, "BadMount", $visorTarget);
}

#
# Perform query or update operation
#

my $newFirmwareVer;
if ($operation eq "query") {
   RunQuery($bootbankPath);
} else {
   $newFirmwareVer = RunUpdate($visorTarget,
                               $bootbankOffset,
                               $bootbankPath,
                               $operation,
                               $targetPath,
                               $bulletinId,
                               $proxy,
                               $sigCheck);
}

#
# Generate the ThinESX/Recovery CD ISOs if specified
#

if ($generateCds == 1) {
   CustomizeCds($visorTarget, $cdCache, $newFirmwareVer);
}

#
# Release the lock
#

Unlock();
