#!/usr/bin/python
########################################################################
# Copyright (c) 2010-2021 VMware, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################

import fnmatch
import logging
import os
import json
import shutil
import stat
import tarfile
import time
import socket

from vmware import runcommand

from .. import CONFIG_STORE_ENABLED
from .. import Database
from .. import Errors
from .. import Vib
from .. import VibCollection
from ..Utils import HostInfo
from ..Utils import LockFile
from ..Utils import PathUtils
from ..Utils import Ramdisk
from ..Utils.Misc import byteToStr, configStoreLogInit, isString
from . import Installer

from .. import MIB

log = logging.getLogger('LiveImageInstaller')
LIVEINST_MARKER = 'var/run/update_altbootbank'

FAILURE_WARNING = '''It is not safe to continue. Please reboot the host \
immediately to discard the unfinished update.'''

SECURE_MOUNT_SCRIPT = '/usr/lib/vmware/secureboot/bin/secureMount.py'
CONFIG_UPGRADE_LIB_DIR = 'usr/lib/vmware/configmanager/upgrade/lib/*'
ESXUPDATE_SCRATCH = "/var/vmware/esxupdate"
LSOF_DUMP_FILE = os.path.join(ESXUPDATE_SCRATCH, 'lsof_on_umount.log')

class ExecuteCommandError(Exception):
   pass

def ConfigStoreCleanup():
   ''' Removal of config schemas during VIB removal might have left some
       config objects without schema entries in configstore. This step is
       needed to cleanup such objects. Should be performed after successully
       removing and enabling VIBs to avoid needing to restore configstore
       objects in case of a failure.
   '''
   if not CONFIG_STORE_ENABLED:
      return
   from libconfigstorepy import CleanupStore
   configStoreLogInit()
   CleanupStore()

def ConfigStoreRefresh(newVibs=None, cfgUpgradeModules=None,
                       stagedProfile=None, raiseException=True):
   ''' Configstore processing to handle live VIB install/remove/failure.
       Since Remediate does RemoveVibs and AddVibs, this is called only at
       AddVibs so jumpstarts have the new schemas. But not really needed at
       RemoveVibs.
       * Process schemas and default configs into configstore
       * Execute upgrade modules in case it is present
       Parameters:
       * newVibs - list of VIB IDs that are getting installed
       * cfgUpgradeModules - any applicable upgrade modules
       * stagedProfile - image profile staged in the staging area
       * raiseException - raise exception if processing failed and
                          raiseException is set
   '''
   if not CONFIG_STORE_ENABLED:
      return
   from libconfigstorepy import RefreshConfigStore
   errMsg = ('Failed to refresh ConfigStore. '
             'Please check syslog for ConfigStore error.')
   try:
      configStoreLogInit()
      newList = newVibs or []
      if RefreshConfigStore(newList, cfgUpgradeModules or []) == False:
         raise Errors.InstallationError(None, newList, errMsg)

      if newList:
         from lifecycle.filemanager.files import Files
         failures = Files.LiveInstall(newList, stagedProfile)
         if failures:
            log.error("Following VIB(s) contain writable files that are not "
                      "defined in schema: %s" % json.dumps(failures))
            errMsg = ('Failed to refresh ConfigStore. '
                  'VIB(s) contain writable files without schema definition.')
            raise Errors.InstallationError(None, newList, errMsg)
   except Exception:
      log.warn(errMsg)
      if raiseException:
         raise

class PostInstTrigger(object):
   '''Base class for system wide post installation trigger action. It does not
      implement any action.

      Attributes:
         * NAME - The name of the trigger
         * matchingvibs - List of VIB objects which trigger the class action
   '''
   NAME = ''

   def __init__(self):
      self.matchingvibs = []

   def Match(self, vib, operation):
      '''Check the properties of a VIB instance against the operation (add/remove)
         to be performed. Add the VIB instance to matchingvibs list if install/remove
         this vib will need to run trigger action.

         Parameters:
            * vib - A VIB instanace, which will be installed or removed.
      '''
      raise NotImplementedError('Must instantiate a subclass of '
                                'PostInstTrigger')

   def Run(self):
      '''Fire trigger action. This should be invoked after VIBs have been live
         installed/removed.

         NOTE: sub-class needs to implement _run() mehtod or override Run
               method.
      '''
      if self.matchingvibs:
         log.info("Executing post inst trigger : '%s'" % (self.NAME))
         self._run()

   def _run(self):
      raise NotImplementedError('Must instantiate a subclass of '
                                'PostInstTrigger')

class Sfcb():
   '''Handles communications with sfcbd when vib is added or removed by
      using a unix domain socket to send control characters. Now sfcbd
      may be administratively up or down so must first get app config
      to know app state.'''
   def __init__(self, operation):
      '''operation is add/remove for vib, client is either sfcb or wsman
         that needs updating its runtime state when vib is added or removed'''
      cmd = 'localcli --formatter json system wbem get'
      rpt = json.loads(RunCmdWithMsg(cmd))
      self.isRunning = rpt['Enabled']
      self.wsmanRunning = rpt['WS-Management Service']
      self.operation = operation

   def StartWbemServices(self):
      if self.isRunning:
         return
      cmd = 'localcli system wbem set --enable=true'
      RunCmdWithMsg(cmd)
      self.isRunning = True

   def SendUpdateRequest(self):
      '''This routine creates a UDS pipe to sfcbd and sends control code.'''
      sck = None
      try:
         sck = self._Connect()
         # see cayman_sfcb.git control.c for control codes
         UPDATE_CMD = 'A'
         if self.wsmanRunning:
            UPDATE_CMD += 'W'
         self._SendCmd(sck, UPDATE_CMD)
      finally:
         if sck:
            self._CloseSock(sck)

   def _Connect(self):
      '''return socket to sfcb mgmt interface  '''
      ctrl_path = '/var/run/sfcb.ctl'
      sck = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
      retries = 3
      while retries > 0:
         retries -= 1
         try:
            sck.connect(ctrl_path)
            return sck
         except Exception as err:
            time.sleep(3)
            log.error("Connect to '%s' err: %s" % (ctrl_path, err))
            if retries == 0:
               self._CloseSock(sck)
               raise

   def _CloseSock(self, sck):
      try:
         sck.shutdown(1) # close transmit, done sending
         sck.close()
      except Exception:
         pass

   def _SendCmd(self, sck, cmd):
      '''write all bytes in cmd to sck else throw exception'''
      totalsent = 0
      while totalsent < len(cmd):
         sent = sck.send(cmd[totalsent:].encode('utf-8'))
         if sent == 0:
            log.error("Unable to transmit command %s" % cmd)
            raise Exception("Unable to transmit command %s" % cmd)
         totalsent = totalsent + sent

class FirewallRefreshTrigger(PostInstTrigger):
   '''FirewallRefreshTrigger is the trigger to refresh firewall settings
   '''
   NAME = 'Firewall Refresh Trigger'

   def Match(self, vib, operation):
      if LiveImage._GetFilesFromVib(vib,
            lambda x: fnmatch.fnmatch(x, "etc/vmware/firewall/*")):
         self.matchingvibs.append(vib)
         log.info('%s is required to install/remove %s' % (self.NAME, vib.id))

   def _run(self):
      # User vpxuser is allowed in strict lockdown mode.
      cmd = 'VI_USERNAME=vpxuser /sbin/esxcli network firewall refresh'
      RunCmdWithMsg(cmd, 'Running firewall refresh...',
                    raiseexception=False)

class HostServiceRefreshTrigger(PostInstTrigger):
   '''HostServiceRefreshTrigger is the trigger to refresh host service system
   '''
   NAME = 'Service System Refresh Trigger'

   def Match(self, vib, operation):
      if LiveImage._GetFilesFromVib(vib,
            lambda x: fnmatch.fnmatch(x, "etc/vmware/service/*")):
         self.matchingvibs.append(vib)
         log.info('%s is required to install/remove %s' % (self.NAME, vib.id))

   def _run(self):
      # User vpxuser is allowed in strict lockdown mode.
      cmd = '/bin/vim-cmd -U vpxuser hostsvc/refresh_service'
      RunCmdWithMsg(cmd, 'Running service refresh...',
                    raiseexception=False)

class WBEMServiceEnableTrigger(PostInstTrigger):
   '''WBEMServiceEnableTrigger is the trigger to enable wbem services when
      custom cim provider is installed or if running reload CIM providers.
      Trigger does different things depending on configured state of sfcbd.
      see: esxcli system wbem get
      If sfcbd is running, send control character(s) to signal to sfcbd
      to reconfigure after add/remove of a cim provider and to restart openwsmand
      if configured to run. If sfcbd is not running and a cim provider is installed
      this trigger will enable wbem services/start up sfcbd/openwsmand.
   '''
   NAME = 'WBEM Service Enable Trigger'

   def Match(self, vib, operation):
      self.operation = operation
      self.vibRemove = True if operation == 'remove' else False
      provider_path = 'var/lib/sfcb/registration/*-providerRegister'
      if LiveImage._GetFilesFromVib(vib,
            lambda x: fnmatch.fnmatch(x, provider_path)):
          self.matchingvibs.append(vib)

   def _run(self):
      log.debug("Processing '%s' to request sfcb to reload providers" % \
                   (self.NAME))
      try:
         sfcb = Sfcb(self.operation)
         if self.operation == 'add' and not sfcb.isRunning:
             sfcb.StartWbemServices()
             return
         sfcb.SendUpdateRequest()
      except Exception as err:
         log.warning("Unable to reconfigure SFCB: %s" % err)

class ToolsRamdiskRefreshTrigger(PostInstTrigger):
   '''ToolsRamdiskRefreshTrigger is the trigger to refresh tools ramdisk
      containing payloads of tools-light VIB.

      This trigger is only required when below conditions are met:
      1. VIB being changed is tools-light.
      2. '/UserVars/ToolsRamdisk' is set
      3. '/tools' folder exists
      4. '/productLocker' points to '/tools'
   '''
   NAME = 'Tools Ramdisk Refresh Trigger'
   TOOLS_RAMDISK_PATH = '/tools'
   userVarsToolsRamdisk = None

   def Match(self, vib, operation):
      if (vib.name != 'tools-light' or
          not os.path.isdir(self.TOOLS_RAMDISK_PATH) or
          not os.path.samefile(self.TOOLS_RAMDISK_PATH, '/productLocker')):
         return
      try:
         if self.userVarsToolsRamdisk is None:
            from esxutils import runCli
            self.userVarsToolsRamdisk = runCli(['system', 'settings','advanced',
               'list', '-o', '/UserVars/ToolsRamdisk'], True)[0]['Int Value']
         if self.userVarsToolsRamdisk:
            self.matchingvibs.append(vib)
      except Exception as e:
         log.warning("Unable to check /UserVars/ToolsRamdisk value: %s" % e)

   def _run(self):
      log.debug("Processing '%s' to refresh tools ramdisk" % (self.NAME))

      # Cleanup TOOLS_RAMDISK_PATH
      for fileName in os.listdir(self.TOOLS_RAMDISK_PATH):
         filePath = os.path.join(self.TOOLS_RAMDISK_PATH, fileName)
         try:
            if os.path.isfile(filePath) or os.path.islink(filePath):
               os.unlink(filePath)
            elif os.path.isdir(filePath):
               shutil.rmtree(filePath)
         except Exception as e:
            log.error('Failed to delete %s. Reason: %s', filePath, e)

      # Copy the new tools payloads
      # shutil.copytree cannot be used here as the destination exists and it
      # raises FileExistsError. 'dirs_exist_ok' argument is useful but it got
      # added in python3.8. This might cause issues for legacy releases during
      # major upgrade. Hence, using distutils.dir_util.copy_tree instead.
      # https://docs.python.org/3/library/shutil.html#shutil.copytree
      from distutils.dir_util import copy_tree
      try:
         copy_tree('/locker/packages/vmtoolsRepo/', self.TOOLS_RAMDISK_PATH)
      except Exception as e:
         log.error('Failed to update /tools: %s', e)

POSTINST_TRIGGER_CLASSES = (FirewallRefreshTrigger,
                            HostServiceRefreshTrigger,
                            ToolsRamdiskRefreshTrigger,
                            WBEMServiceEnableTrigger)

#
# If an image is live staged, we don't need to stage it to bootbank again
#
class LiveImage(object):
   '''Encapsulates attributes of LiveImage
      Attributes:
         * root             - root of the LiveImage, default '/'
         * stageroot        - root of the staging directory
         * stagedatadir     - Temporary directory for payloads files (unziped)
         * database         - An instance of Database for live image
         * stagedatabase    - An instance of Database.Database representing the
                              package database for staged image. None if not
                              yet staged.
         * isstaged         - Boolean, indicate whether there is a staged image
   '''
   DB_DIR = os.path.join('var', 'db', 'esximg')
   DB_TGZ_FILE = 'imgdb.tgz'
   DB_LOCKFILE = '/var/run/liveimgdb.pid'

   CHKCONFIG_DB = 'etc/chkconfig.db'
   SERVICES_DIR = 'etc/init.d'
   STATE_BACKUP_DIR = 'var/run/statebackup'

   # Remediation-related constants.
   ADDS = 'adds'
   REMOVES = 'removes'
   START = 'start'
   STOP = 'stop'

   # stage and tardiskbackup ramdisk
   # GetContainerId() retrieves simulator name to generate unique ramdisk name
   # and path for each simulator environment. It return empty string if not in
   # simulator environment.
   STAGE_RAMDISK_NAME = HostInfo.GetContainerId() + 'stageliveimage'
   STAGE_DIR = os.path.join(os.path.sep, 'tmp', STAGE_RAMDISK_NAME)

   TARDISK_BACKUP_RAMDISK_NAME = HostInfo.GetContainerId() + 'tardiskbackup'
   TARDISK_BACKUP_DIR = os.path.join(os.path.sep, 'tmp',
                                     TARDISK_BACKUP_RAMDISK_NAME)

   def __init__(self, root = '/', stagedir = None):
      self._root = root
      self.database = Database.Database(os.path.join(self._root, self.DB_DIR),
            dbcreate = True)
      if stagedir is None:
         stagedir = self.STAGE_DIR
      self._stageroot = os.path.join(self._root, stagedir)
      self._stagedata = os.path.join(self._stageroot, 'data')
      self._stagedbpath = os.path.join(self._stageroot, 'imgdb')
      self._stageindicator = os.path.join(self._stageroot, 'staged')
      self._stagedb = None
      self._tardisksdir = os.path.join(root, 'tardisks')
      self._chkconfigdb = os.path.join(root, self.CHKCONFIG_DB)
      self._servicesdir = os.path.join(root, self.SERVICES_DIR)
      self._statebackupdir = os.path.join(root, self.STATE_BACKUP_DIR)
      self._servicestates = {"enabled": set(), "disabled": set(),
                             "added": set(), "removed": set(),
                             "upgradevibs": set()}
      # Map from init.d script to VIB IDs that are removed/added.
      self._serviceToVibMap = {
         self.ADDS: dict(),
         self.REMOVES: dict(),
      }
      # Map from VIB ID to component name(version) info.
      self._vibToCompMap = dict()
      self._triggers = []

      # Dictionary holding information for live vib failure recovery
      self._recovery = {"stoppedservices": [],
                        "startedservices": [],
                        "unmountedtardisks": [],
                        "mountedtardisks": [],
                        "jumpstartplugins": set(),
                        "rcscripts": set(),
                        "removedconfigs": []}

      # List holding config information for preserving config during
      # live vib update
      self._backupconfigs = []
      self._cfgupgrademodules = set()
      self.Load()

   @property
   def root(self):
      return self._root

   @property
   def stageroot(self):
      return self._stageroot

   @property
   def stagedatadir(self):
      return self._stagedata

   @property
   def stagedatabase(self):
      if self.isstaged:
         return self._stagedb
      return None
   @property
   def tardisksdir(self):
      return self._tardisksdir

   @property
   def chkconfigdb(self):
      return self._chkconfigdb

   @property
   def isstaged(self):
      return os.path.isfile(self._stageindicator)

   def Load(self):
      '''Load LiveImage and staging info.

         Exceptions:
            * DatabaseFormatError
            * DatabaseIOError
      '''
      dbLock = None
      try:
         dbLock = LockFile.acquireLock(self.DB_LOCKFILE)
         self.database.Load()
      except LockFile.LockFileError as e:
         msg = 'Unable to obtain a lock for database I/O: %s' % str(e)
         raise Errors.LockingError(msg)
      finally:
         if dbLock:
            dbLock.Unlock()

      if self.isstaged:
         try:
            self._stagedb = Database.Database(self._stagedbpath, dbcreate=False)
            self._stagedb.Load()
         except Exception as e:
            msg = ('Found a copy of staged image, but unable to load the '
                   'database: %s. Resetting live image as non-staged' % (e))
            log.warn(msg)
            self._UnSetStaged()
      # Populate profile.vibs and profile.bulletins to keep everything in the
      # image profile for the transaction.
      if self.database.profile is not None:
         self.database.profile.PopulateWithDatabase(self.database)
      if self.stagedatabase and self.stagedatabase.profile is not None:
         self.stagedatabase.profile.PopulateWithDatabase(self.stagedatabase)

   def StartTransaction(self, imgprofile, imgsize):
      '''Reset staging directory to empty.
      '''
      problems = self._CheckHost(imgprofile)
      if problems:
         msg = 'Not enough resources to run the transaction: %s' % (problems)
         raise Errors.InstallationError(None, None, msg)

      self._Setup(imgsize)
      # Create staging database with the imgprofile
      self._stagedb = Database.Database(self._stagedbpath, addprofile=False)
      self._stagedb.PopulateWith(imgProfile=imgprofile)

   def CompleteStage(self):
      '''Save staging database and staged indicator
      '''
      self._stagedb.Save()
      try:
         self._SetStaged()
      except Exception as e:
         msg = 'Failed to change the status of the staged image: %s' % (e)
         raise Errors.InstallationError(e, None, msg)

   def GetMaintenanceModeVibs(self, tgtvibs, adds, removes):
      '''Get lists of vibs which require maintenance mode to finish the
         transaction.
         Parameters:
            * tgtvibs - VibCollection instance for the desired image profile
                        vibs
            * adds    - List of Vib IDs which will be added during the
                        transaction
            * removes - List of Vib IDs which will be removed the during the
                        transaction
         Returns: A tuple (mmoderemove, mmodeinstall); mmoderemove
            is a set of VIB IDs which requires maintenance mode to be removed;
            mmodeinstall is a set of VIB IDs which requires maintenance mode to
            be installed.
      '''
      mmoderemove = set()
      mmodeinstall = set()
      for vibid in adds:
         if tgtvibs[vibid].maintenancemode.install:
            mmodeinstall.update([vibid,])
      for vibid in removes:
         if self.database.vibs[vibid].maintenancemode.remove:
            mmoderemove.update([vibid,])
      return (mmoderemove, mmodeinstall)

   def Remediate(self, checkmaintmode, hasConfigDowngrade):
      '''Live remove VIBs and enable new VIBs
      '''
      if self.stagedatabase.profile is None:
         msg = ('There is an error in the DB of staging directory: %s, Please '
                'try the update command again.' % (self._stagedbpath))
         raise Errors.InstallationError(None, None, msg)

      adds, removes = self.stagedatabase.profile.Diff(self.database.profile)
      vibstoadd = [self.stagedatabase.vibs[vibid] for vibid in adds]
      vibstoremove = [self.database.vibs[vibid] for vibid in removes]

      # Build VIB ID to component name(version) mapping, this is for providing
      # context in error handling.
      activeComps = (self.database.profile.components +
                     self.stagedatabase.profile.components)
      self._vibToCompMap = {vId: '%s(%s)' % (c.compNameStr, c.compVersionStr)
                            for c in activeComps.IterComponents()
                            for vId in c.vibids}

      self._UpdateServiceStates(vibstoadd, vibstoremove)

      # Initiate post-installation triggers
      for cls in POSTINST_TRIGGER_CLASSES:
         self._triggers.append(cls())
      self._UpdateTriggers(vibstoadd, vibstoremove)

      # check maintenance mode required for the transaction
      mmoderemove, mmodeinstall = self.GetMaintenanceModeVibs(
            self.stagedatabase.vibs, adds, removes)
      if mmoderemove or mmodeinstall:
         msg = ('MaintenanceMode is required to '
                'remove: [%s]; install: [%s].'
                   % (', '.join(mmoderemove),
                      ', '.join(mmodeinstall)))
         log.debug(msg)
         if checkmaintmode:
            mmode = HostInfo.GetMaintenanceMode()
            if not mmode:
               raise Errors.MaintenanceModeError(msg)
         else:
            log.warning('MaintenanceMode check was skipped...')

      # save config files
      cmd = '/sbin/backup.sh 0'
      RunCmdWithMsg(cmd, 'Running state backup...', raiseexception=False)

      # XXX Verify staged image

      # Backup state.tgz from /bootbank or /altbootbank (by checking
      # LIVEINST_MARKER) to /var/run/statebackup/ for live VIB config
      # persistent and failure handling.
      try:
         if not os.path.exists(self._statebackupdir):
            os.makedirs(self._statebackupdir)
         if os.path.isfile(os.path.join(self.root, LIVEINST_MARKER)):
            src = '/altbootbank/state.tgz'
         else:
            src = '/bootbank/state.tgz'
         dest = os.path.join(self._statebackupdir, 'state.tgz')
         shutil.copy2(src, dest)

         with tarfile.open(dest, 'r:gz') as stateTgz:
            stateTgz.extractall(path=self._statebackupdir)

         localTgz = os.path.join(self._statebackupdir, 'local.tgz')
         localTgzVe = localTgz + '.ve'

         if os.path.isfile(localTgzVe):
            # Decrypt local.tgz.ve
            RunCmdWithMsg('/bin/crypto-util ++coreDumpEnabled=false,mem=20 '
                          'envelope extract --aad ESXConfiguration %s %s'
                          % (localTgzVe, localTgz),
                          title='Decrypting %s...' % localTgzVe)

         if not os.path.isfile(localTgz):
            log.warning('Error backing up state: local.tgz has not been '
                        'extracted')

      except Exception as e:
         log.warning("Error backing up state using %s as source: %s",
                     src, str(e))

      # For vib removal case, we need to check the modified config file by
      # the existence of .# file. No need to check the original file permission
      # since it could be changed.
      # Also create the set of config upgrade modules of the new VIBs to
      # be used in _AddVibs() to refresh the config stote.
      vibsToRemoveMap = dict([(v.name, v) for v in vibstoremove])
      vibsToAddMap = dict([(v.name, v) for v in vibstoadd])
      for vibName, vibToAdd in vibsToAddMap.items():
         if vibName in vibsToRemoveMap:
            modules = self._GetFilesFromVib(vibToAdd,
               lambda x: fnmatch.fnmatch(x, CONFIG_UPGRADE_LIB_DIR))
            for fn in modules:
               self._cfgupgrademodules.add(os.path.abspath(fn))
            vibToRemove = vibsToRemoveMap[vibName]
            for fn in vibToRemove.GetModifiedConf():
               self._backupconfigs.append(fn)

      try:
         self._RemoveVibs(removes, hasConfigDowngrade)
         self._AddVibs(adds, hasConfigDowngrade)

         # Run secpolicytools, PR 581102
         # 1. setup policy rules for vmkaccess (/etc/vmware/secpolicy/*)
         # 2. Apply the security label for any new tardisks that have been
         #    installed and mounted.
         cmd = '/sbin/secpolicytools -p'
         RunCmdWithMsg(cmd, 'Applying VIB security policies...',
                       raiseexception=False)

         # Start daemons in order
         self._StartServices()

         # Invoke post inst triggers
         self._RunTriggers()

         # Cleanup configstore to get rid of objects without schema
         ConfigStoreCleanup()

         # Save live image database
         self._UpdateDB()
      except (ExecuteCommandError, Errors.InstallationError) as e:
         self._HandleLiveVibFailure(str(e))
         # Append reboot warning for live remediation errors.
         msg = "%s\n%s" % (str(e), FAILURE_WARNING)

         vibs = None
         cause = None
         if isinstance(e, Errors.InstallationError):
            vibs = e.vibs
            cause = e.cause
         raise Errors.LiveInstallationError(cause, vibs, msg)
      finally:
         # Remove the tardiskbackup ramdisk
         Ramdisk.RemoveRamdisk(self.TARDISK_BACKUP_RAMDISK_NAME,
                               self.TARDISK_BACKUP_DIR)

         # Delete state.tgz and extracted files.
         filesToRemove = ('state.tgz', 'local.tgz.ve', 'local.tgz',
                          'encryption.info')
         for fName in filesToRemove:
            fPath = os.path.join(self._statebackupdir, fName)
            if os.path.isfile(fPath):
               try:
                  os.remove(fPath)
               except Exception as e:
                  log.warning("Error deleting file %s: %s", fPath, e)

      # mark system live installed
      markerfile = os.path.join(self.root, LIVEINST_MARKER)
      msg = ("There was an error creating flag file '%s',"
             "configuration changes after installation might be lost "
             "after reboot." % (markerfile))
      try:
         open(markerfile, 'w').close()
      except IOError as e:
         log.warning("Creating file failed: %s" % e)
         log.warning(msg)


   def Cleanup(self):
      '''Remove data in staging area.
      '''
      # this is a bit tricky, have to track init, tardisk mapping,
      try:
         if os.path.exists(self._stageroot):
            # Unset the stage indicator first to avoid the race condition on
            # it with vib.get, vib.list or profile.get command running
            # at the same time.
            self._UnSetStaged()
            # remove staging ramdisk
            Ramdisk.RemoveRamdisk(self.STAGE_RAMDISK_NAME, self._stageroot)
      except EnvironmentError as e:
         msg = 'Cannot remove live image staging directory: %s' % (e)
         raise Errors.InstallationError(e, None, msg)
      self._stagedb = None

   def _RemoveVibs(self, removes, hasConfigDowngrade):
      log.debug('Starting to live remove VIBs: %s' % (', '.join(removes)))

      shutdownscripts = set()
      for vibid in removes:
         vib = self.database.vibs[vibid]
         log.info('Live removing %s-%s' % (vib.name, vib.version))
         # run shutdown script
         scripts = self._GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "etc/vmware/shutdown/shutdown.d/*"))
         shutdownscripts.update(scripts)

      self._ShutdownServices()

      # Module unloading
      log.debug('Starting to run etc/vmware/shutdown/shutdown.d/*')
      for script in shutdownscripts:
         RunCmdWithMsg(script)

      payloadsToUnmount = set()
      vibstates = self.database.profile.vibstates

      # Calculate the size of tardisks to be unmounted and create a new
      # ramdisk to backup them.
      totalsize = 0
      for vibid in removes:
         vib = self.database.vibs[vibid]
         payloadsToUnmount |= LiveImageInstaller.GetSupportedVibPayloads(vib)

         for name, localname in vibstates[vibid].payloads.items():
            if name in payloadsToUnmount:
               totalsize = (totalsize +
                  os.path.getsize(os.path.join(self._tardisksdir, localname)))

      ramdiskSize = int(round(totalsize / MIB)) + 1
      Ramdisk.CreateRamdisk(ramdiskSize, self.TARDISK_BACKUP_RAMDISK_NAME,
                            self.TARDISK_BACKUP_DIR, reserveSize=ramdiskSize)

      # umount tardisks
      for vibid in removes:
         vib = self.database.vibs[vibid]

         # Collect modified config for recovery
         for fn in vib.GetModifiedConf():
            self._recovery['removedconfigs'].append(fn)

         for name, localname in vibstates[vibid].payloads.items():
            if name in payloadsToUnmount:
               log.debug('Trying to unmount payload [%s] of VIB %s'
                         % (name, vibid))
               # Make a copy of the tardisk to be unmounted for recovery
               src = os.path.join(self._tardisksdir, localname)
               dest = os.path.join(self.TARDISK_BACKUP_DIR, localname)
               log.debug('Copying tardisk from %s to %s' % (src, dest))
               try:
                  shutil.copy2(src, dest)
               except IOError:
                  log.warning('Unable to copy file from %s to %s' % (src, dest))

               self._UnmountTardisk(localname, vibid)
               self._recovery["unmountedtardisks"].append([vibid, name,
                                                           localname])

         # Collect jumpstart plugins and rc scripts for recovery
         plugins = self._GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "usr/libexec/jumnpstart/plugins/*"))
         for fn in plugins:
            self._recovery["jumpstartplugins"].add(os.path.basename(fn))

         scripts = self._GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "etc/rc.local.d/*"))
         self._recovery["rcscripts"].update(scripts)

      if hasConfigDowngrade:
         # Handle config downgrade scenario.
         # Config downgrade is currently limited to HA/FDM, it is handled
         # by purging all current config, HA will push config again after
         # the downgraded version of VIB is installed.
         vibWithCS = [vId for vId in removes
                      if self.database.vibs[vId].hasConfigSchema]
         log.info('Handling config downgrade, purging config of VIB(s): %s',
                  ', '.join(vibWithCS))
         ConfigStoreRefresh()
         ConfigStoreCleanup()

      log.debug('done')

   def _AddVibs(self, adds, hasConfigDowngrade):
      log.debug('Starting to enable VIBs: %s' % (', '.join(adds)))

      jumpstartplugins = set()
      rcscripts = set()

      # ordering non-overlay and overlay VIB, so non-overlay VIBs being mounted
      # first
      regvibs = []
      overlayvibs = []
      for vibid in adds:
         vib = self.stagedatabase.vibs[vibid]
         if vib.overlay:
            overlayvibs.append(vibid)
         else:
            regvibs.append(vibid)

      for vibid in regvibs + overlayvibs:
         vib = self.stagedatabase.vibs[vibid]

         plugins = self._GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "usr/libexec/jumpstart/plugins/*"))
         for fn in plugins:
            jumpstartplugins.add(os.path.basename(fn))

         scripts = self._GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "etc/rc.local.d/*"))
         rcscripts.update(scripts)

         log.debug('Live installing %s-%s' % (vib.name, vib.version))

         payloadsToMount = LiveImageInstaller.GetSupportedVibPayloads(vib)
         vibstates = self.stagedatabase.profile.vibstates
         for name, localname in vibstates[vibid].payloads.items():
            if name in payloadsToMount:
               log.debug('Trying to mount payload [%s]' % (name))
               self._MountTardiskSecure(vibid, name, localname)
               self._recovery["mountedtardisks"].append(localname)

         # Restore config from /var/run/statebackup/local.tgz before
         # running jumpstart and rc script. Need to do following checks
         # before restoring config files:
         # 1. config files are in self._backupconfigs.
         # 2. same files are added when installing new vib.
         # 3. sticky bit are on for the added files.
         try:
            with tarfile.open(
                    os.path.join(self._statebackupdir, 'local.tgz'),
                    'r:gz') as localtgz:
               for fn in vib.filelist:
                  normalfile = PathUtils.CustomNormPath('/' + fn)
                  if os.path.isfile(normalfile) and \
                       os.stat(normalfile).st_mode & stat.S_ISVTX and \
                       fn in self._backupconfigs:
                     if hasConfigDowngrade:
                        log.info('Skip extracting sticky bit file %s for '
                                 'VIB %s, config downgrade is in progress.',
                                 fn, vib.id)
                     else:
                        log.info("Extracting %s from local.tgz", fn)
                        localtgz.extract(fn, '/')
         except Exception as e:
            log.warning("Error restoring config files for vib %s: %s",
                        vib.name, str(e))

      ConfigStoreRefresh(adds, list(self._cfgupgrademodules),
                         self.stagedatabase.profile)

      if jumpstartplugins:
         self._StartJumpStartPlugins(jumpstartplugins)

      if rcscripts:
         self._RunRcScripts(rcscripts)

      log.debug('Enabling VIBs is done.')

   def _UpdateDB(self, useStageDB=True):
      """Update live image database.
         Instead saving directly to the folder, meaning modifying the tardisk,
         create a temp imgdb (uncompressed tar) and mount it.
         The previous imgdb will be restored in case we have a failure.
         Parameter:
            * useStagingDB: When set to True, assume an image has been staged
                            and copy over the stage database before saving.
                            This is the scenario for a live VIB transaction.
      """
      if useStageDB:
         self.database.PopulateWith(database=self.stagedatabase)
         # Use staging root to place the temp files
         tmpDbDir = self._stageroot
      else:
         # Stage folder is not present, use a temp directory.
         import tempfile
         tmpDbDir = tempfile.mkdtemp(prefix='imgdb-')

      curDbPath = os.path.join(self._tardisksdir, self.DB_TGZ_FILE)
      newDbPath = os.path.join(tmpDbDir,
                               self.DB_TGZ_FILE.replace('.tgz', '.tar'))
      backupDbPath = os.path.join(tmpDbDir, '%s.bk' % self.DB_TGZ_FILE)

      tarDb = Database.TarDatabase(newDbPath, dbcreate=False)
      tarDb.PopulateWith(database=self.database)

      try:
         dbLock = LockFile.acquireLock(self.DB_LOCKFILE)
      except LockFile.LockFileError as e:
         msg = 'Unable to obtain a lock for database I/O: %s' % str(e)
         raise Errors.LockingError(msg)
      try:
         # A tardisk has to be uncompressed to be mounted
         tarDb.Save(savesig=True, gzip=False)

         # Unmount the old database and mount the new database, backup
         # the old one for restore on failure.
         if os.path.exists(curDbPath):
            shutil.copy2(curDbPath, backupDbPath)
            self._UnmountTardisk(self.DB_TGZ_FILE)
         self._MountImgdbSecure(newDbPath)
      except Exception:
         if os.path.exists(newDbPath):
            os.remove(newDbPath)
         if os.path.exists(backupDbPath):
            self._MountImgdbSecure(backupDbPath)
         raise
      else:
         if os.path.exists(backupDbPath):
            os.remove(backupDbPath)
      finally:
         dbLock.Unlock()
         if not useStageDB:
            shutil.rmtree(tmpDbDir)

   def _UpdateVib(self, newvib):
      """Update missing properties of vib metadata

         Parameters:
            * newvib   - The new vib to use as source
         Returns:
            None if the update succeeds, Exception otherwise
         Exceptions:
            VibFormatError
      """
      try:
         self._stagedb.vibs[newvib.id].SetSignature(newvib.GetSignature())
         self._stagedb.vibs[newvib.id].SetOrigDescriptor(newvib.GetOrigDescriptor())
      except Exception as e:
         msg = ("Failed to set signature or original descriptor of VIB "
                "%s: %s" % (newvib.id, e))
         raise Errors.VibFormatError(newvib.id, msg)

   def _Setup(self, imgsize):
      self.Cleanup()
      try:
         # Live image use uncompressed payload, while installation size is
         # compress size.
         # Exact ramdisk size is hard to figure, we assume compression ratio
         # will be at most 4, RAM beyond compression ratio 1 will be consumed
         # as needed with this max.
         tardiskMb = int(round(imgsize / MIB))
         Ramdisk.CreateRamdisk(tardiskMb * 4 + 1, self.STAGE_RAMDISK_NAME,
                               self._stageroot, reserveSize=tardiskMb)
         os.makedirs(self._stagedata)
      except Exception as e:
         msg = 'There was a problem setting up staging area: %s' % (e)
         raise Errors.InstallationError(e, None, msg)

   def _SetStaged(self):
      if not os.path.isfile(self._stageindicator):
         f = open(self._stageindicator, 'w')
         f.close()

   def _UnSetStaged(self):
      if os.path.exists(self._stageindicator):
         os.unlink(self._stageindicator)

   def _CheckHost(self, adds):
      '''TODO: Check the host environment to make sure:
            * enough memory to expand VIB
            * enough userworlds memory
      '''
      return []

   def _MountImgdbSecure(self, path):
      '''Mount imgdb tar with SecureMount to update the live database.
         SecureMount verifies that the tardisk contains only esximg database.
      '''
      args = [SECURE_MOUNT_SCRIPT, path]
      RunCmdWithMsg(args, 'Updating imgdb using %s...' % (path))

   def _MountTardiskSecure(self, vibid, payload, tardisk):
      '''Mount a tardisk with SecureMount using VIB and payload information.
         SecureMount verifies that the tardisk belongs to the VIB and is not
         tampered with.
      '''
      tardiskpath = os.path.join(self._stagedata, tardisk)
      args = [SECURE_MOUNT_SCRIPT, vibid, payload, tardiskpath]
      RunCmdWithMsg(args, 'Mounting tardisk %s...' % tardisk)

   def _UnmountTardisk(self, tardisk, vibId=None):
      cmd = 'rm /tardisks/%s' % (tardisk)
      # Sometimes the filesystem will report that the file is busy
      # or in use.  This is usually a transient error and simply retrying
      # might fix the issue.
      try:
         RunCmdWithRetries(cmd, 'Unmounting %s...' % tardisk)
      except Exception as e:
         if vibId:
            # We will log opened file descriptors and raise live unmount error
            # when we are unmounting an VIB tardisk.
            try:
               os.makedirs(ESXUPDATE_SCRATCH, exist_ok=True)
               RunCmdWithMsg('lsof > %s' % LSOF_DUMP_FILE,
                             raiseexception=False)
               log.debug('Open file descriptors have been dumped to %s',
                         LSOF_DUMP_FILE)
            except Exception as e:
               log.warn('Dumping file descriptors to %s failed: %s',
                        LSOF_DUMP_FILE, str(e))

            comp = self._vibToCompMap.get(vibId, 'N/A')
            msg = ('Failed to unmount tardisk %s of VIB %s: %s'
                   % (tardisk, vibId, str(e)))
            # Nest ComponentUnmountError in InstallationError to preserve VIB
            # info for esxcli while allowing advanced handling in vLCM.
            cause = Errors.ComponentUnmountError(comp, msg)
            raise Errors.InstallationError(cause, [vibId], msg)
         else:
            # Unmounting imgdb, or restoring environment after a live VIB
            # failure.
            raise

   @staticmethod
   def _GetFilesFromVib(vib, matchfn):
      files = []
      for fn in vib.filelist:
         normfile = PathUtils.CustomNormPath(fn)
         if matchfn(normfile):
            # return script with absolute path so the script can be run from any
            # directory.
            files.append(os.path.join('/', normfile.strip()))
      return files

   @staticmethod
   def _GetUpgradeVibs(adds, removes):
      addvibs = VibCollection.VibCollection()
      for vib in adds:
         addvibs.AddVib(vib)
      removevibs = VibCollection.VibCollection()
      for vib in removes:
         removevibs.AddVib(vib)

      allvibs = VibCollection.VibCollection()
      allvibs += addvibs
      allvibs += removevibs

      upgradevibs = set()
      scanner = allvibs.Scan()
      for vibs in (addvibs, removevibs):
         upgradevibs.update(scanner.GetUpdatesSet(vibs))
         upgradevibs.update(scanner.GetDowngradesSet(vibs))

      return upgradevibs

   def _RaiseServiceException(self, service, op, ex):
      """Raises ServiceEnable/DisableError nested in InstallationError
         according to the operation. This allows preservation of VIB info
         in esxcli and advanced error handling in vLCM.
      """
      if op == self.START:
         category = self.ADDS
         exType = Errors.ServiceEnableError
      else:
         category = self.REMOVES
         exType = Errors.ServiceDisableError
      vibId = self._serviceToVibMap[category].get(service, '')
      comp = self._vibToCompMap.get(vibId, 'N/A')
      msg = str(ex)

      cause = exType(comp, os.path.basename(service), msg)
      raise Errors.InstallationError(cause, [vibId], msg)

   def _UpdateServiceStates(self, adds, removes):
      filesToVibMap = lambda files, vId: {f: vId for f in files}

      enabled = set()
      disabled = set()
      serviceadds = set()
      serviceremoves = set()

      # set of vibids involving upgrade/downgrade
      upgradevibs = self._GetUpgradeVibs(adds, removes)

      filterfn = lambda x: fnmatch.fnmatch(x, "etc/init.d/*")
      for vib in adds:
         files = self._GetFilesFromVib(vib, filterfn)
         serviceadds.update(files)
         if vib.id in upgradevibs:
            self._servicestates["upgradevibs"].update(files)
         self._serviceToVibMap[self.ADDS].update(filesToVibMap(files, vib.id))
      for vib in removes:
         files = self._GetFilesFromVib(vib, filterfn)
         serviceremoves.update(files)
         if vib.id in upgradevibs:
            self._servicestates["upgradevibs"].update(files)
         self._serviceToVibMap[self.REMOVES].update(
            filesToVibMap(files, vib.id))

      upgrades = serviceadds & serviceremoves
      serviceadds -= upgrades
      serviceremoves -= upgrades

      if upgrades:
         cmd = ["/sbin/chkconfig", "-B", self._chkconfigdb, "-D",
               self._servicesdir, "-l"]
         try:
            out = RunCmdWithMsg(cmd, 'Getting chkconfig service list...')
         except ExecuteCommandError as e:
            msg = "Failed to get chkconfig service list:\n%s" % e
            raise Errors.InstallationError(None, None, msg)

         for line in out.splitlines():
            line = line.strip()
            if line.endswith(" on"):
               servicepath = os.path.join(self._servicesdir, line[:-3].strip())
               enabled.add(servicepath)
            elif line.endswith(" off"):
               servicepath = os.path.join(self._servicesdir, line[:-4].strip())
               disabled.add(servicepath)

         for script in upgrades:
            if script in enabled:
               self._servicestates["enabled"].add(script)
            elif script in disabled:
               self._servicestates["disabled"].add(script)
      self._servicestates["added"].update(serviceadds)
      self._servicestates["removed"].update(serviceremoves)

   def _GetEnabledServices(self):
      cmd = ["/sbin/chkconfig", "-B", self._chkconfigdb, "-D",
             self._servicesdir, "-i", "-o"]
      try:
         out = RunCmdWithMsg(cmd, 'Getting chkconfig service list...')
      except ExecuteCommandError as e:
         msg = "Failed to obtain chkconfig service list:\n%s" % e
         raise Errors.InstallationError(None, None, msg)
      return [line.strip() for line in out.splitlines()]

   def _ShutdownServices(self):
      '''Shutdown services according to the order in chkconfig.db. It is assumed
         that all the services are managed by chkconfig.
      '''
      tostop = self._servicestates["removed"] | self._servicestates["enabled"]
      for service in reversed(self._GetEnabledServices()):
         cmd = [service, self.STOP]
         if service in self._servicestates["upgradevibs"]:
            cmd.append("upgrade")
         else:
            cmd.append("remove")
         if service in tostop:
            try:
               RunCmdWithMsg(cmd, 'Stopping service %s...' % service)
               self._recovery["stoppedservices"].append(cmd)
            except ExecuteCommandError as e:
               self._RaiseServiceException(service, self.STOP, e)

   def _SetServiceEnabled(self, service, enabled):
      service = os.path.basename(service)
      status = "on" if enabled else "off"
      cmd = ["/sbin/chkconfig", "-B", self._chkconfigdb, "-D",
             self._servicesdir, service, status]
      try:
         RunCmdWithMsg(cmd, 'Toggle service %s to %s...' % (service, status))
      except ExecuteCommandError as e:
         action = "enable" if enabled else "disable"
         msg = "Unable to %s service %s\n%s" % (action, service, e)
         raise Errors.InstallationError(None, None, msg)

   def _StartServices(self):
      # Need to enable/disable any services which were previously explicitly
      # enabled/disabled.
      enabled = set(self._GetEnabledServices())
      for service in enabled & self._servicestates["disabled"]:
         self._SetServiceEnabled(service, False)

      for service in self._servicestates["enabled"] - enabled:
         self._SetServiceEnabled(service, True)

      tostart = self._servicestates["enabled"] | self._servicestates["added"]
      for service in self._GetEnabledServices():
         if service in tostart:
            cmd = [service, "start"]
            if service in self._servicestates["upgradevibs"]:
               cmd.append("upgrade")
            else:
               cmd.append("install")

            try:
               RunCmdWithMsg(cmd, 'Starting service %s...' % service)
               self._recovery["startedservices"].append(cmd)
            except ExecuteCommandError as e:
               self._RaiseServiceException(service, self.START, e)

   def _StartJumpStartPlugins(self, plugins):
      cmd = ["/sbin/jumpstart", "--method=start", "--invoke",
             "--plugin=%s" % ",".join(plugins)]
      RunCmdWithMsg(cmd, 'Activating Jumpstart plugins...',
                    raiseexception=False)

   def _RunRcScripts(self, scripts):
      for script in scripts:
         if os.stat(script).st_mode & stat.S_IXUSR:
            RunCmdWithMsg(script)
         else:
            log.warn("Script: %s is not executable. Skipping it." % script)

   def _UpdateTriggers(self, adds, removes):
      '''Activate trigger for VIB installation or removal
      '''
      for vib in adds:
         for trigger in self._triggers:
            trigger.Match(vib, 'add')
      for vib in removes:
         for trigger in self._triggers:
            trigger.Match(vib, 'remove')

   def _RunTriggers(self, ignoreErrors=False):
      '''Execute post installation triggers.
      '''
      for trigger in self._triggers:
         try:
            trigger.Run()
         except Exception as e:
            if ignoreErrors:
               log.warn("Error running %s: %s" % (trigger.NAME, str(e)))
            else:
               raise

   def _HandleLiveVibFailure(self, msg):
      '''Handle live vib installation failure by:
            * Shutdown the services from new vibs
            * Unmount the tardisks from new vibs
            * Mount the unmounted tardisks
            * Restore configuration
            * Start jumpstart and rc scripts from removed vibs
            * Start the services from removed vibs
            * Invoke triggers
         Caller needs to raise an exception after this method returns.
      '''
      log.warn("Handling Live Vib Failure: %s" % msg)

      # Shutdown the services from new vibs
      for (service, _, _) in reversed(self._recovery["startedservices"]):
         try:
            cmd = [service, self.STOP]
            if service in self._servicestates["upgradevibs"]:
               cmd.append("upgrade")
            else:
               cmd.append("remove")
            RunCmdWithMsg(cmd, raiseexception=False)
         except Exception as e:
            log.warn("Unable to shutdown service %s: %s" % (cmd, str(e)))

      # Unmount the tardisks from new vibs
      for localname in reversed(self._recovery["mountedtardisks"]):
         try:
            self._UnmountTardisk(localname)
         except Exception as e:
            log.warn("Unable to unmount tardisk %s: %s" % (localname, str(e)))

      # Mount the unmounted tardisks
      for vibid, name, localname in reversed(self._recovery["unmountedtardisks"]):
         try:
            src = os.path.join(self.TARDISK_BACKUP_DIR, localname)
            dest = os.path.join(self._stagedata, localname)
            shutil.move(src, dest)
            self._MountTardiskSecure(vibid, name, localname)
         except Exception as e:
            log.warn("Unable to mount tardisk %s: %s" % (localname, str(e)))

      # Restore the removed configs from /var/run/statebackup/local.tgz
      try:
         with tarfile.open(os.path.join(self._statebackupdir, 'local.tgz'),
                           'r:gz') as localtgz:
            for fn in self._recovery["removedconfigs"]:
               log.info('Extracting %s from local.tgz', fn)
               localtgz.extract(fn, '/')
      except Exception as e:
         log.warning("Error restoring removed configs: %s", str(e))

      ConfigStoreRefresh(raiseException=False)

      # Start jumpstart plugins and rc scripts
      try:
         if self._recovery["jumpstartplugins"]:
            self._StartJumpStartPlugins(self._recovery["jumpstartplugins"])
         if self._recovery["rcscripts"]:
            self._RunRcScripts(self._recovery["rcscripts"])
      except Exception as e:
         log.warn("Error %s" % str(e))

      # Start the stopped services
      for (service, _, _) in reversed(self._recovery["stoppedservices"]):
         try:
            cmd = [service, self.START]
            if service in self._servicestates["upgradevibs"]:
               cmd.append("upgrade")
            else:
               cmd.append("install")
            RunCmdWithMsg(cmd, raiseexception=False)
         except Exception as e:
            log.warn("Unable to start service %s: %s" % (cmd, str(e)))

      # Invoke triggers
      self._RunTriggers(ignoreErrors=True)

class LiveImageInstaller(Installer):
   '''LiveImageInstaller is the Installer class to live install/remove VIBs for
      live system.

      Attributes:
         * database - A Database.Database instance of the live system
   '''
   installertype = "live"
   priority = 5

   # Support locker VIB for PXE host so live install/remove of the tools
   # VIB is supported and the VIB will be kept in the image profile.
   if HostInfo.IsPxeBooting():
      SUPPORTED_VIBS = set([Vib.BaseVib.TYPE_BOOTBANK,
                            Vib.BaseVib.TYPE_LOCKER,])
   else:
      SUPPORTED_VIBS = set([Vib.BaseVib.TYPE_BOOTBANK,])

   SUPPORTED_PAYLOADS = set([Vib.Payload.TYPE_TGZ, Vib.Payload.TYPE_VGZ])
   BUFFER_SIZE = 8 * 1024

   def __init__(self, root = '/'):
      self.liveimage = LiveImage(root)
      self.problems = list()

   @property
   def database(self):
      return self.liveimage.database

   @property
   def stagedatabase(self):
      return self.liveimage.stagedatabase

   def StartTransaction(self, imgprofile, imgstate = None, preparedest = True,
                        forcebootbank = False, **kwargs):
      """Initiates a new installation transaction. Calculate what actions
         need to be taken.

         This method only works on staging directory

         Parameters:
            * imgprofile  - The ImageProfile instance representing the
                            target set of VIBs for the new image
            * imgstate    - The state of current HostImage, one of IMGSTATE_*
            * preparedest - Boolean, if True, then prepare the destination.
                            Set to false for a "dry run", to avoid changing
                            the destination.
            * forcebootbank - Boolean, if True, skip install of live image
                              even if its eligible for live install
         Returns:
            A tuple (installs, removes, staged), installs and removes are list
            of VIB IDs for HostImage.Stage() to install to the destination and
            to remove from the destination, in order to make it compliant
            with imgprofile. If LiveImage has already staged the imgprofile,
            staged is True.
            If there is nothing to do, (None, None, False) is returned.
         Exceptions:
            InstallationError
      """
      # Skip if reboot required VIBs have been installed
      from ..HostImage import HostImage
      if forcebootbank:
         msg = 'Nothing to do for live install - live installation has been ' \
               'disabled.'
         self.problems.append(msg)
         log.debug(msg)
         return (None, None, False)

      if imgstate not in (HostImage.IMGSTATE_FRESH_BOOT,
            HostImage.IMGSTATE_LIVE_UPDATED):
         msg = ('Only reboot-required installations are possible right now as '
                'a reboot-required installation has been done.')
         self.problems.append(msg)
         log.debug(msg)
         return (None, None, False)

      if self.database.profile is None:
         msg = 'No ImageProfile found for live image'
         raise Errors.InstallationError(None, None, msg)

      imgprofile = self.GetInstallerImageProfile(imgprofile)

      staged = False
      if self.stagedatabase is not None:
         if self.stagedatabase.profile and \
               self.stagedatabase.profile.vibIDs == imgprofile.vibIDs:
            staged = True
         else:
            self.liveimage.Cleanup()

      adds, removes = imgprofile.Diff(self.database.profile)

      if staged:
         return (adds, removes, staged)

      problems = self._CheckTransaction(imgprofile, adds, removes)
      if problems:
         log.debug('The transaction is not supported for live install:\n%s' % (problems))
         self.problems = problems
         return (None, None, False)

      imgsize = self.GetInstallationSize(imgprofile)

      if preparedest and (removes or adds):
         self.liveimage.StartTransaction(imgprofile, imgsize)

      return (adds, removes, staged)

   def VerifyPayloadChecksum(self, vibid, payload, **kwargs):
      """Verify the checksum of a given payload.

         Parameters:
            * vibid   - The Vib id containing the payload
            * payload - The Vib.Payload instance to read or write
         Returns:
            None if verification succeeds, Exception otherwise
         Exceptions:
            ChecksumVerificationError
            InstallationError
      """
      from vmware import vsi
      if payload.payloadtype in self.SUPPORTED_PAYLOADS:
         # TODO: we are currently only checking the SUPPORTED_PAYLOADS
         # for this installer. Once we have the ability to lookup
         # modules hashes from the kernel, we should enable checking
         # for other boot modules.
         checksumfound = False
         for checksum in payload.checksums:
            if checksum.checksumtype == "sha-1":
               checksumfound = True
               try:
                  if vibid in self.database.profile.vibstates:
                     vibstate = self.database.profile.vibstates[vibid]
                  else:
                     msg = ('Could not locate VIB %s in LiveImageInstaller'
                            % vibid)
                     raise Errors.InstallationError(None, [vibid], msg)
                  if payload.name in vibstate.payloads:
                     tardiskname = vibstate.payloads[payload.name]
                  else:
                     msg = "Payload name '%s' of VIB %s not in LiveImage DB" % \
                           (payload.name, vibid)
                     raise Errors.InstallationError(None, [vibid], msg)
                  tardiskchecksum = vsi.get('/system/visorfs/tardisks/%s/sha1hash' % tardiskname)
                  tardiskchecksum = ''.join(("%02x" % x) for x in tardiskchecksum)
                  if tardiskchecksum != checksum.checksum:
                     msg = ("Payload checksum mismatch. "
                            "Expected <%s>, kernel loaded <%s>" % (checksum.checksum, tardiskchecksum))
                     raise Exception(msg)
               except Exception as e:
                  msg = "Failed to verify checksum for payload %s: %s" % (payload.name, e)
                  log.error("%s" % msg)
                  raise Errors.ChecksumVerificationError(msg)
         if not checksumfound:
            msg = "Failed to find checksum for payload %s" % payload.name
            log.error("%s" % msg)
            raise Errors.ChecksumVerificationError(msg)

   def UpdateVibDatabase(self, newvib):
      """Update missing properties of vib metadata
         New vibs are always installed in the liveimage

         Parameters:
            * newvib   - The new vib to use as source
         Returns:
            None if the update succeeds, Exception otherwise
         Exceptions:
            VibFormatError
      """
      self.liveimage._UpdateVib(newvib)

   def OpenPayloadFile(self, vibid, payload, read = True, write = False,
                       fromBaseMisc=False):
      """Creates and returns a File-like object for either reading from
         or writing to a given payload.  One of read or write must be True, but
         ready and write cannot both be true.

         Parameters:
            * vibid   - The Vib id containing the payload
            * payload - The Vib.Payload instance to read or write
            * read    - Set to True to get a File object for reading
                        from the payload.
            * write   - Set to True to get a File object for writing
                        to the payload.
            * fromBaseMisc - Not used, defaults to False.
         Returns:
            A File-like object, must support read (for read), write (for
            write), close methods.
            None if the desired read/write is not supported.
         Exceptions:
            AssertionError    - neither read nor write is True, or both are true
            InstallationError - Cannot open file to write or read
      """
      if fromBaseMisc:
         raise NotImplementedError("Do not know how to handle fromBaseMisc")

      Installer.OpenPayloadFile(self, vibid, payload, read, write)

      if read:
         if vibid not in self.database.profile.vibstates:
            msg = 'Could not locate VIB %s in LiveImageInstaller' % vibid
            raise Errors.InstallationError(None, [vibid], msg)

         vibstate = self.database.profile.vibstates[vibid]
         if payload.name not in vibstate.payloads:
            msg = "Payload name '%s' of VIB %s not in LiveImage DB" % (
                  payload.name, vibid)
            raise Errors.InstallationError(None, [vibid], msg)

         if (payload.payloadtype in self.SUPPORTED_PAYLOADS or
             payload.payloadtype == Vib.Payload.TYPE_BOOT):

            filepath = os.path.join(self.liveimage.tardisksdir,
                  vibstate.payloads[payload.name])
            if os.path.isfile(filepath):
               # Tardisk is not compressed
               return open(filepath, 'rb')
            else:
               msg = "Payload '%s' of VIB %s at '%s' is missing" % (
                     payload.name, vibid, filepath)
               if HostInfo.HostOSIsSimulator():
                  log.info("HostSimulator: %s" % (msg))
                  return None
               else:
                  raise Errors.InstallationError(None, [vibid], msg)
         else:
            return None
      else:
         if payload.payloadtype not in self.SUPPORTED_PAYLOADS:
            log.debug("Payload %s of type '%s' in VIB '%s' is not supported by "
                      "LiveImageInstaller." % (payload.name, payload.payloadtype,
                         vibid))
            return None

         filepath = os.path.join(self.liveimage.stagedatadir, payload.localname)
         try:
            # Decompressed file stream is required when writing to this stream
            return open(filepath, 'wb')
         except EnvironmentError as e:
            msg = 'Can not open %s to write payload %s: %s' % (filepath,
                  payload.name, e)
            raise Errors.InstallationError(e, None, msg)

   def Cleanup(self):
      '''Cleans up the live image staging area.
      '''
      try:
         self.liveimage.Cleanup()
      except Exception:
         pass

   def CompleteStage(self):
      """Complete the staging of live image by writing out the database and
         staged indicator to staging directory.

         Exceptions:
            InstallationError
      """
      self.liveimage.CompleteStage()

   def Remediate(self, checkmaintmode=True, hasConfigDowngrade=False):
      """Live remove and install VIB payloads

         For each VIB to remove, shutdown scripts will be executed and payloads
         will be unmounted. For each VIB to install, payloads will be mounted
         and init scripts will be executed.

         Returns:
            A Boolean, always False, as a reboot is not needed.
         Exceptions:
            InstallationError -
            HostNotChanged    - If there is no staged ImageProfile
      """
      if not self.liveimage.isstaged:
         msg = 'LiveImage is not yet staged, nothing to remediate.'
         raise Errors.HostNotChanged(msg)
      if self.problems:
         msg = 'Live installation is not supported or has been disabled, ' \
               'skip remediation.'
         raise Errors.HostNotChanged(msg)

      self.liveimage.Remediate(checkmaintmode, hasConfigDowngrade)
      return False

   def _CheckTransaction(self, imageprofile, adds, removes):
      '''Check the transaction to see if there are any logical reasons that
         prevent live installation of the transaction.
            * For VIBs to be removed: require liveremoveok
            * For VIBs to be installed: require liveinstallok
            * No VIBs to be installed are overlaid by existing VIB
            * No VIBs to be removed are overlaid by existing VIB
      '''
      problems = []

      # Build a map from file path to the file states. File state is a bit map
      # to indicate which group the file belongs to.
      filestates = {}
      keepreg, keepoverlay, removereg, removeoverlay, addreg, addoverlay = \
         1, 2, 4, 8, 16, 32

      keeps = self.database.profile.vibIDs - set(removes)
      groups = ((keeps, self.database.vibs, (keepreg, keepoverlay)),
                (removes, self.database.vibs, (removereg, removeoverlay)),
                (adds, imageprofile.vibs, (addreg, addoverlay)))
      for vibids, vibs, flags in groups:
         for vibid in vibids:
            vib = vibs[vibid]
            for filepath in vib.filelist:
               if filepath == '' or filepath.endswith('/'):
                     continue

               filepath = PathUtils.CustomNormPath('/' + filepath)
               ind = vib.overlay and 1 or 0
               if filepath in filestates:
                  filestates[filepath] |= flags[ind]
               else:
                  filestates[filepath] = flags[ind]

      unsupported = {
         keepoverlay | removereg  : \
            "File to be removed is overlaid by existing VIB",
         keepoverlay | removereg | addreg : \
            "File to be removed/installed is overlaid by existing VIB",
         keepoverlay | addreg : \
            "File to be installed is overlaid by existing VIB",
         #XXX: visorFS to support resurrect?
         keepreg | removeoverlay : \
            "File to be removed overlays existing VIB",
         keepreg | removeoverlay | addoverlay : \
            "File to be removed/installed overlays existing VIB",
            }

      # Files overwritten are logged. This info is applicable
      # no matter reboot required or not.
      overlayed = {
         keepoverlay | addreg : \
            ("File to be installed is overlaid by existing VIB", []),
         keepreg | addoverlay : \
            ("File to be installed overlays existing VIB", []),
         addreg | addoverlay : \
            ("File to be installed overlays another VIB to be installed", []),
            }

      for filepath in filestates:
         if filestates[filepath] in unsupported:
            problem = "%s : %s" % (unsupported[filestates[filepath]], filepath)
            problems.append(problem)
         if filestates[filepath] in overlayed:
            overlayed[filestates[filepath]][1].append(filepath)

      for overlay in overlayed:
         if overlayed[overlay][1]:
            log.info("%s : %s" % (overlayed[overlay][0], overlayed[overlay][1]))

      for vibid in adds:
         # liveinstallok must be True for new VIB
         if not imageprofile.vibs[vibid].liveinstallok:
            problem = 'VIB %s cannot be live installed.' % (vibid)
            problems.append(problem)

      for vibid in removes:
         # liveremoveok must be True for VIB to be removed
         if not self.database.vibs[vibid].liveremoveok:
            problem = 'VIB %s cannot be removed live.' % (vibid)
            problems.append(problem)

      return problems

   def SaveDatabase(self):
      """Save live image database.
         Called in the base Installer class for image profile updates,
         regular transactions are handled by LiveImage.Remediate().
      """
      self.liveimage._UpdateDB(useStageDB=False)


def RunCmdWithMsg(cmd, title='', raiseexception=True):
   """Logs and runs a command.
      Raises ExecuteCommandError with raiseexception, or a warning is
      logged.
   """
   # runcommand takes both tuple or string.
   cmdStr = cmd if isString(cmd) else ' '.join(cmd)
   if title:
      log.info(title)
   else:
      log.info('Running [%s]...', cmdStr)

   try:
      rc, out = runcommand.runcommand(cmd)
      out = byteToStr(out)

      if rc != 0:
         msg = ('Error in running [%s]:\nReturn code: %s'
                '\nOutput: %s' % (cmdStr, rc, out))
         if raiseexception:
            raise ExecuteCommandError(msg)
         else:
            log.warning(msg)
      elif out:
         log.debug("Output: %s", out)
   except runcommand.RunCommandError as e:
      msg = 'Error in running [%s]: %s' % (cmdStr, str(e))
      if raiseexception:
         raise ExecuteCommandError(msg)
      else:
         log.warning(msg)

   return out

def RunCmdWithRetries(cmd, msg, numRetries=3):
   for i in range(1, numRetries + 1):
      try:
         out = RunCmdWithMsg(cmd, msg)
         return out
      except Exception as e:
         # Raise when the last try fails
         if i == numRetries:
            raise
         log.info("Received error: %s\nTrying again. Attempt #%d", e, i)
         time.sleep(i)
   return None
