#!/usr/bin/python

# Copyright 2019,2020,2022 VMware, Inc.
# All rights reserved. -- VMware Confidential

"""ESX utility to generate a .DD image off of a live ESX host.

This script collects all ESX payloads from the current running system, and
generates a .DD image off of these payloads. On PXE-booted systems, the bootbank
payloads are retrieved from the PXE directory, whose URL is set in the 'bootUrl'
VMKernel boot option.
"""
import os
import shutil
from tempfile import TemporaryDirectory

from borautils.busybox import wget
from esxutils import getVmkBootOptions, runCli
from systemStorage import BOOTBANK_LINK, LOCKER_LINK
from systemStorage.ddImage import DDImage
from systemStorage.esxboot import BOOTLOADER_DIR
from systemStorage.systemStorageDebug import BootDeviceInfo
from vmware import vsi
from vmware.esximage.Utils.BootCfg import BootCfg

MBOOT_CFG = 'boot.cfg'


def updateBootcfg(localBootCfg, bootCfg=None, bootOpts={}):
   if bootCfg is None:
      bootCfg = BootCfg(localBootCfg)
   bootCfg.kernelopt.pop('bootUrl', None)

   # default kernel opt
   bootCfg.kernelopt['debugLogToSerial'] = '1'
   bootCfg.kernelopt['logPort'] = 'com1'
   bootCfg.kernelopt['esxDDImage'] = 'TRUE'

   # input kernel opt wins
   bootCfg.kernelopt.update(bootOpts)

   bootCfg.prefix = ""
   bootCfg.timeout = 3
   bootCfg.updated = BootCfg.BOOTSTATE_UPDATED

   with open(localBootCfg, 'wb+') as cfg:
      bootCfg.write(cfg)


def fetchBootbankFilesFromPxeDir(bootUrl, cacheDir, bootOpts):
   """Fetch the bootbank files from the current PXE directory.

   Download ESX boot files from the given boot URL. This function assumes
   that the system was booted over HTTP.
   """

   localBootCfg = os.path.join(cacheDir, MBOOT_CFG)
   wget(os.path.join(bootUrl, MBOOT_CFG), localBootCfg)

   bootCfg = BootCfg(localBootCfg)
   updateBootcfg(localBootCfg, bootCfg=bootCfg, bootOpts=bootOpts)

   bootFiles = ([bootCfg.kernel] + bootCfg.modules)

   for relPath in bootFiles:
      url = os.path.join(bootUrl, relPath)
      localPath = os.path.join(cacheDir, relPath)
      wget(url, localPath)


def fetchBootbankFilesFromBootVol(bootVol, cacheDir, bootOpts):
   """Fetch the bootbank files from the current boot disk.

   Copy ESX boot files from the given boot vol UUID.
   """
   bootbank = os.path.join('/vmfs/volumes', bootVol)
   for mod in os.listdir(bootbank):
      shutil.copy(os.path.join(bootbank, mod), cacheDir)

   localBootCfg = os.path.join(cacheDir, MBOOT_CFG)
   bootCfg = BootCfg(localBootCfg)
   if 'state.tgz' in bootCfg.modules:
      bootCfg.modules.remove('state.tgz')

   updateBootcfg(localBootCfg, bootCfg=bootCfg, bootOpts=bootOpts)


def live2dd(output, bootOpts={}, stageDir=None):
   """Generate an ESX .DD image from a live ESX host.
   """
   ddImage = DDImage(output)
   boot = runCli('system boot device get'.split(), True)
   bootVol = boot['Boot Filesystem UUID']
   vmkOpts = getVmkBootOptions()
   if bootOpts:
      vmkOpts.update(bootOpts)

   with TemporaryDirectory(prefix='_live2dd', dir=stageDir) as tmpDir:
      bootbank1Dir = os.path.join(tmpDir, 'bootbank1')
      os.mkdir(bootbank1Dir)
      if bootVol:
         fetchBootbankFilesFromBootVol(bootVol, bootbank1Dir, vmkOpts)
      elif 'bootUrl' in vmkOpts:
         bootUrl = vmkOpts['bootUrl']
         fetchBootbankFilesFromPxeDir(bootUrl, bootbank1Dir, vmkOpts)
      elif BootDeviceInfo().isPXE():
         # The bootCmdLine start with the full path to the vmkBoot image.
         # On a PXE booted system, this incl. the URL
         bootUrl = vsi.get('/system/bootCmdLine')['bootCmdLineStr'].split()[0]
         bootUrl = bootUrl.rsplit('/', 1)[0]
         fetchBootbankFilesFromPxeDir(bootUrl, bootbank1Dir, vmkOpts)
      else:
         raise Exception('ESXi should boot from disk or pxeUrl')

      ddImage.write(BOOTLOADER_DIR, bootbank1Dir, LOCKER_LINK)


if __name__ == "__main__":
   from argparse import ArgumentParser

   desc = "Generate ESX DD images."

   parser = ArgumentParser(description=desc)
   parser.add_argument('output', help="Path to output file")
   parser.add_argument('--stageDir', metavar='STAGEDIR',
                       help="Staging directory to use")
   parser.add_argument('--bootOpts', metavar='BOOTOPTS', type=str,
                       default='', help="Boot options")

   args = parser.parse_args()

   live2dd(args.output, bootOpts=BootCfg.kerneloptToDict(args.bootOpts),
           stageDir=args.stageDir)
