#!/usr/bin/python
########################################################################
# Copyright (c) 2023-2024 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
########################################################################

'''
This file provides a common interface for all Installer types along with
utility functions.
'''

from contextlib import contextmanager
import fnmatch
import json
import logging
import os
import shutil
import socket
import stat
import tarfile
import time

from vmware import runcommand, vsi

from . import LIFECYCLE_DIR, RAMDISK_ROOT
from .. import Database, Errors, MIB, VibCollection, XZ_PAYLOADS
from ..ImageManager.Constants import QUICK_PATCH_NO_ACTIONS
from ..Utils import HostInfo, LockFile, PathUtils, Ramdisk
from ..Utils.Misc import byteToStr, configStoreLogInit, extractTar, isString

log = logging.getLogger('InstallerCommon')

LIVEINST_MARKER = 'var/run/update_altbootbank'
QUICKPATCH_MARKER = LIVEINST_MARKER

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

class StartTransactionResult:
   """Class that represents the result of an installer's StartTransaction call.
   """
   # Possible values of installer status. Details in installerStatus().
   NO_CHANGE = 0
   QUICK_PATCH_ACTION_ONLY = 1
   UNSUPPORTED = 2
   OTHERS = 3

   def __init__(self, adds, removes, staged, quickPatchResult=None,
                quickPatchScriptResults=None):
      """Constructor.

         Parameters:
         * adds             - IDs of VIBs to install.
         * removes          - IDs of VIBs to remove.
         * staged           - A boolean indicating whether the image has been
                              staged.
         * quickPatchResult - A multi-line string that has remediation actions
                              and info/warning/error messages from Quick Patch
                              scripts if any (single line if there is only one
                              message/action), or a single line mentioning that
                              "no action is needed".
         * quickPatchScriptResults - A dictionary that maps each Quick Patch VIB
                                     ID to names of its scripts, to each
                                     script's return code, output and
                                     Apply/ScanScriptReturn instance.
      """
      self.adds = adds
      self.removes = removes
      self.staged = staged
      self.quickPatchResult = quickPatchResult
      self.quickPatchScriptResults = quickPatchScriptResults

   def GetCommonAttributes(self):
      """Returns a tuple of mandatory attributes.
      """
      return self.adds, self.removes, self.staged

   @property
   def installerStatus(self):
      """Get the installer status based on self.adds/removes/quickPatchResult.
      """
      if self.adds is None and self.removes is None:
         # adds and removes are both None
         return self.UNSUPPORTED

      if self.adds is not None and not self.adds and \
            self.removes is not None and not self.removes:
         # adds and removes are both empty
         # Not QuickPatchInstaller -> quickPatchResult is None.
         # QuickPatchInstaller without actions -> QUICK_PATCH_NO_ACTIONS.
         # QuickPatchInstaller with actions -> a string with action details.
         return self.NO_CHANGE if self.quickPatchResult in (None,
            QUICK_PATCH_NO_ACTIONS) else self.QUICK_PATCH_ACTION_ONLY

      # The rest cases will be adds and removes are both non-empty regardless
      # of installer types.
      return self.OTHERS

   @property
   def isUnSupported(self):
      return self.installerStatus == self.UNSUPPORTED

   @property
   def isNoChange(self):
      return self.installerStatus == self.NO_CHANGE

   @property
   def isQuickPatchActionOnly(self):
      return self.installerStatus == self.QUICK_PATCH_ACTION_ONLY

class RemediationResult:
   """Class that represents the result of an installer's remediate call.
   """

   def __init__(self, rebootRequired, quickPatchResult=None):
      """Constructor

         Parameters:
         * rebootRequired        -  boolean, True if reboot is required post
                                    remediation. False otherwise.
         * quickPatchResult      -  A dictionary that maps Quick Patch script
                                    names to results.
      """
      self.rebootRequired = rebootRequired
      self.quickPatchResult = quickPatchResult

   def GetAttributes(self):
      """Returns a tuple of all attributes.
      """
      return self.rebootRequired, self.quickPatchResult

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.
   '''
   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
   '''

   from libconfigstorepy import RefreshConfigStore
   errMsg = ('Failed to refresh ConfigStore. '
             'Please check syslog for ConfigStore error.')
   try:
      configStoreLogInit()
      newList = newVibs or []
      schemaIds = []
      # Fetch schema IDs from VIBs' ConfigSchema tags (if any).
      for vibid in newList:
         vib = stagedProfile.vibs[vibid]
         schemaTag = vib.GetConfigSchemaTag(filterEsxioSchemas=False)
         if schemaTag:
            schemaIds.append(schemaTag.schemaId)
      if RefreshConfigStore(schemaIds, 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.warning(errMsg)
      if raiseException:
         raise

class InstallTrigger(object):
   '''Base class for system wide pre installation and 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 instance, which will be installed or removed.
            * operation - VIB operation, add or remove
      '''
      raise NotImplementedError('Must instantiate a subclass of '
                                'InstallTrigger')

   def PreInstallAction(self):
       '''Trigger pre-install action. This should be invoked before VIBs have been
          live installed/removed.

          NOTE: sub-class needs to implement _preInstallAction() method or
          override PreInstallAction method.
       '''
       if self.matchingvibs:
         log.info("Executing pre inst trigger : '%s'", self.NAME)
         self._preInstallAction()

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

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

   def _preInstallAction(self):
       '''Function to call pre-installation actions.'''
       # Not all InstallTrigger subclasses have a pre-install action
       return

   def _postInstallAction(self):
      '''Function to call post-installation actions.'''
      raise NotImplementedError('Must instantiate a subclass of '
                                'InstallTrigger')

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 = '/bin/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 = '/bin/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(InstallTrigger):
   '''FirewallRefreshTrigger is the trigger to refresh firewall settings
   '''
   NAME = 'Firewall Refresh Trigger'

   def Match(self, vib, operation):
      if 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 _postInstallAction(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(InstallTrigger):
   '''HostServiceRefreshTrigger is the trigger to refresh host service system
   '''
   NAME = 'Service System Refresh Trigger'

   def Match(self, vib, operation):
      if 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 _postInstallAction(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(InstallTrigger):
   '''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 GetFilesFromVib(vib,
            lambda x: fnmatch.fnmatch(x, provider_path)):
         self.matchingvibs.append(vib)

   def _postInstallAction(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(InstallTrigger):
   '''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 _postInstallAction(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)

class VAAIServiceRefreshTrigger(InstallTrigger):
   '''VAAIServiceRefreshTrigger is the trigger to refresh vaai-nasd service
   '''
   NAME = 'VAAI Service Refresh Trigger'
   SERVICE_COMMAND = '/etc/init.d/vaai-nasd %s'

   def Match(self, vib, operation):
      '''Add the VIB instance to matchingvibs list if NAS plugin
         folder path is being updated.

         Parameters:
            * vib - A VIB instance, which will be installed or removed.
            * operation - VIB operation, add or remove
      '''
      self.initialServiceStatus = False
      # Trigger on vib file path
      pluginFolderPath = "usr/lib/vmware/nas_plugins/lib64/*"
      if GetFilesFromVib(vib,
            lambda x: fnmatch.fnmatch(x, pluginFolderPath)):
         self.matchingvibs.append(vib)
         log.info('%s is required to install/remove %s', self.NAME, vib.id)

   def _isServiceRunning(self):
      '''Returns True if vaai-nasd service is running else returns False.
      '''
      SERVICE_RUNNING = 'ESX VAAI-NAS Daemon is running'
      try:
         rc, out = runcommand.runcommand(self.SERVICE_COMMAND % ('status'))
         out = out.decode('utf-8')
         # Returns True if service is running else False
         return SERVICE_RUNNING in out
      except Exception as e:
         raise Errors.InstallationError(None, None, str(e))

   def _installAction(self, action):
      '''Helper function to start or stop vaai-nasd service.
         action takes one of the two values: 'start', 'stop'
      '''
      try:
         cmd =  self.SERVICE_COMMAND % (action)
         if action == 'start':
            # Start the service
            RunCmdWithRetries(cmd, 'Starting NAS VAAI service...')
         elif action == 'stop':
            # Stop the service
            RunCmdWithRetries(cmd, 'Stopping NAS VAAI service...')
      except Exception as e:
         raise Errors.InstallationError(None, None, str(e))

   def _preInstallAction(self):
      '''Stores the initial status of the service and
         stops the vaai-nasd service before vib operation.
      '''
      try:
         self.initialServiceStatus = self._isServiceRunning()
         if self.initialServiceStatus:
            self._installAction('stop')
      except Exception as e:
         raise Errors.InstallationError(None, None, str(e))

   def _postInstallAction(self):
      '''Start the vaai-nasd service if applicable after vib operation.
      '''
      try:
         currentServiceStatus = self._isServiceRunning()
         if self.initialServiceStatus and not currentServiceStatus:
            self._installAction('start')
      except Exception as e:
         raise Errors.InstallationError(None, None, str(e))

class SecPolicyTools:
   """Class that interacts with secpolicytools.
   """
   SEC_POLICY_TOOLS = '/sbin/secpolicytools'

   def __init__(self, tmpPolicyDir=None, raiseExceptionOnLoad=True):
      """Constructor.
         Parameters:
            * tmpPolicyDir - path to the directory that contains temporary
                             security domain files. When given, the domains
                             will be unloaded after use.
            * raiseExceptionOnLoad - if True, raise an exception when
                                     secpolicytools fails to load policies.
                                     Unload is always best-effort.
      """
      self._tmpPolicyDir = tmpPolicyDir
      self._raiseExceptionOnLoad = raiseExceptionOnLoad

   def _run(self, args, msg, isLoad=True):
      """Runs secpolicytools with the given arguments.
      """
      try:
         RunCmdWithMsg((self.SEC_POLICY_TOOLS,) + args, title=msg)
      except ExecuteCommandError as e:
         if isLoad and self._raiseExceptionOnLoad:
            dirPath = self._tmpPolicyDir or '/etc/vmware/secpolicy'
            raise Errors.SecurityPolicyError(dirPath,
               'Failed to load security policy in {}: {}'.format(dirPath, e))
         # Unload is always best-effort.
         log.warning('Failed to run secpolicytools: {}'.format(e))

   def loadDefaultPolicy(self):
      """Loads the default policies.
      """
      self._run(('--load-policy',), 'Applying VIB security policies...')

   @contextmanager
   def loadTempDoms(self):
      """Loads the temporary domains.
      """
      if not self._tmpPolicyDir:
         raise ValueError('tmpPolicyDir is not set')

      args = ('--load-policy-doms', self._tmpPolicyDir)
      msg = 'Loading security domains in {}...'.format(self._tmpPolicyDir)
      self._run(args, msg)

      try:
         yield
      finally:
         # Unload regardless of whether load succeeded when
         # _raiseExceptionOnLoad=False.
         msg = 'Unloading security domains in {}'.format(self._tmpPolicyDir)
         self._run(('--unload-policy-doms', self._tmpPolicyDir), msg,
                   isLoad=False)


def RunCmdWithMsg(cmd, title='', raiseexception=True, timeout=0):
   """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, timeout=timeout)
      out = byteToStr(out)

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

def AddRPs(rpFiles, initFiles):
   """Add resource pools required by a VIB via resource pool files and init
      scripts.
      Parameters:
         * rpFiles - Paths to RP YAML definition files.
         * initFiles - Paths to init scripts to create resource pools for.
      Returns:
         * A list of created resource pools.
      Raises:
         * Errors.ResourcePoolFileInvalid: if rpFiles/initFiles fail to load.
         * Errors.ResourcePoolError: if an RP cannot be created.
   """
   # Only present on 8.0+
   from configRP import loadRPs

   if not rpFiles and not initFiles:
      raise Errors.InstallationError(None, None, 'At least one of rpFiles and '
                                     'initFiles must be provided')

   try:
      rpsToAdd = loadRPs(rpFiles=rpFiles, fatal=True, initFiles=initFiles)
   except Exception as e:
      msg = "Failed to load RPs to add: {}".format(e)
      rpFilesStr = ','.join(list(rpFiles or []) + list(initFiles or []))
      cause = Errors.ResourcePoolFileInvalid(rpFilesStr, msg)
      raise Errors.InstallationError(cause, None, msg)

   addedRPs = []
   for rp in rpsToAdd:
      try:
         rp.create()
         addedRPs.append(rp)
      except Exception as e:
         if addedRPs:
            # Best effort cleanup of already created RPs.
            DelRPs(rpsToDel=addedRPs)
         msg = "Failed to create RP '{}': {}".format(rp.name, e)
         cause = Errors.ResourcePoolError(rp.name, msg)
         raise Errors.InstallationError(cause, None, msg)

   return addedRPs

def DelRPs(rpFiles=None, initFiles=None, rpsToDel=None, raiseException=False):
   """Delete resource pools associated with a VIB via resource pool files and
      init scripts. Alternatively, a standalone list of RPs can also be given
      to delete.
      Parameters:
         * rpFiles - Paths to RP YAML definition files.
         * initFiles - Paths to init scripts to delete resource pools for.
         * rpsToDel - Standalone list of RPs to delete, cannot be used with
                      rpFiles or initFiles.
         * raiseException - If True, raise an exception when an error occurs.
      Returns:
         * A list of deleted resource pools.
      Raises:
         * Errors.ResourcePoolFileInvalid: if rpFiles/initFiles fail to load.
         * Errors.ResourcePoolError: if an RP cannot be deleted.
   """
   def _handleException(cause, msg):
      """Helper function for DelRPs to raise an exception or just log the error
         message depending on the value of the boolean raiseException.
      """
      if raiseException:
         raise Errors.InstallationError(cause, None, msg)
      else:
         log.error(msg)
         return []

   # Only present on 8.0+
   from configRP import loadRPs

   if rpsToDel and (rpFiles or initFiles):
      msg = 'rpsToDel cannot be used with rpFiles or initFiles.'
      return _handleException(None, msg)
   if not rpsToDel and not rpFiles and not initFiles:
      msg = ('At least one of rpFiles, initFiles and rpsToDel must be '
                'provided.')
      return _handleException(None, msg)

   if rpFiles or initFiles:
      try:
         rpsToDel = reversed(loadRPs(rpFiles=rpFiles, fatal=True,
                                     initFiles=initFiles))
      except Exception as e:
         msg = "Failed to load RPs to delete: {}".format(e)
         rpFilesStr = ','.join(list(rpFiles or []) + list(initFiles or []))
         cause = Errors.ResourcePoolFileInvalid(rpFilesStr, msg)
         return _handleException(cause, msg)
   else:
      rpsToDel = sorted(rpsToDel, key=lambda x: x.name, reverse=True)

   removedRPs = []
   for rp in rpsToDel:
      # In total try for at most 3 times, as sometimes the resource pool
      # might be busy and is not able to delete.
      for i in range(3):
         try:
            rp.delete()
            removedRPs.append(rp)
            break
         except Exception as e:
            log.warning("Failed to delete RP '%s': %s, #%d retry left",
                        rp.name, str(e), 2 - i)
            # only sleep for the first two times
            if i < 2:
               time.sleep(0.5)
            elif raiseException:
               msg = "Failed to delete RP '{}': {}".format(rp.name, e)
               cause = Errors.ResourcePoolError(rp.name, msg)
               raise Errors.InstallationError(cause, None, msg)

   return removedRPs

def StartJumpStartPlugins(plugins):
   log.info("Jumper2: Starting jumpstart plugins.")
   cmd = ["/sbin/jumper2", "--plugin=%s" % ",".join(plugins)]
   RunCmdWithMsg(cmd, 'Activating Jumpstart plugins...',
                 raiseexception=False)

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

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

def createTardiskMountRamdisk(name, path, compressedSize,
                              uncompressedSize=0, storageSize=0,
                              xzedTardisk=False):
   '''Creating a Ramdisk to mount a tardisk.
   '''
   # Live image uses uncompressed payload, while installation size is
   # compressed size.
   # Exact ramdisk size is hard to figure, we assume compression ratio will be
   # at most 4 by default, at most 15 for XZ'ed payloads. RAM beyond compression
   # ratio 1 will be consumed as needed with this max.
   # If uncompressedSize is not 0, we will use it instead of
   # compressedSize * ratio.
   # In addition to compressedSize and uncompressedSize, storageSize will be
   # used when a ramdisk is used as a source for both storage and mounting.

   ratio = 15 if xzedTardisk else 4
   totalSize = (compressedSize * ratio if uncompressedSize == 0 else
                uncompressedSize) + storageSize + 1
   Ramdisk.CreateRamdisk(totalSize, name, path,
                         reserveSize=compressedSize + storageSize)

def CreateSingleTardiskRamdisk(tardisks, basePath, ramdiskName, ramdiskDir):
   '''Creating a ramdisk to hold the largest of the tardisks in
      the staging area in the given basePath.
   '''
   xzedTardisk = False
   maxTardiskSize = 1
   for tardisk in tardisks:
      maxTardiskSize = max(maxTardiskSize,
                           round(GetTarDiskSize(tardisk,
                                 basePath) / MIB))
      if tardisk in XZ_PAYLOADS:
         xzedTardisk = True

   createTardiskMountRamdisk(ramdiskName, ramdiskDir, maxTardiskSize,
                             xzedTardisk=xzedTardisk)

def GetTarDiskSize(tardisk, basePath):
   '''Getting the file size of the given tardisk
      in the staging area in the given basePath.
   '''
   filePath = os.path.join(basePath, tardisk)
   return os.path.getsize(filePath)


def MarkSystemLiveInstalled(markerfile):
   # mark system live installed
   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(msg)

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

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 VerifyLiveTardiskChecksum(tardiskname, payload):
   """Verify live tardisk checksum against payload checksums.

      Parameters:
         * tardiskname - The live-mounted tardisk's name
         * payload     - The Vib.Payload instance to verify
      Returns:
         None if verification succeeds, Exception otherwise
      Exceptions:
         ChecksumVerificationError
   """
   tardiskchecksum = \
      vsi.get('/system/visorfs/tardisks/%s/sha256hash' % tardiskname)
   tardiskchecksum = ''.join(map('{:02x}'.format, tardiskchecksum))

   checksumFails = []
   checksumType = 'sha-256'   # VMkernel only reports SHA-256 hashes.
   for checksum in payload.checksums:
      if checksum.checksumtype != checksumType:
         continue
      if tardiskchecksum == checksum.checksum:
         return   # All good - at least one matching checksum.
      checksumFails.append(checksum.checksum)

   if checksumFails:
      raise Errors.ChecksumVerificationError(
         "Payload checksum mismatch. "
         "Expected one of <{}>, kernel loaded <{}>"
         .format(', '.join(checksumFails), tardiskchecksum)
      )
   raise Errors.ChecksumVerificationError(
      "No {} checksums found".format(checksumType)
   )

def applyVibSecPolicy():
   """Applies VIB security policies.
   """
   SecPolicyTools(raiseExceptionOnLoad=False).loadDefaultPolicy()

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 an image staged
                           by live or quickpatch installer depending on the
                           indicator. Under the directory self._stageroot, the
                           file 'staged' indicates the image is staged by live
                           installer, or the file 'quickPatchStaged' for
                           quickpatch staged.
   '''
   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.

   STAGEBOOTBANK_DIRECTORY = 'stageliveimage'
   STAGE_RAMDISK_NAME = HostInfo.GetContainerId() + STAGEBOOTBANK_DIRECTORY
   TMP_STAGE_DIR = os.path.join(RAMDISK_ROOT, STAGE_RAMDISK_NAME)
   DISK_STAGING_PATH = os.path.join(LIFECYCLE_DIR, STAGEBOOTBANK_DIRECTORY)
   STAGE_DIR = TMP_STAGE_DIR

   # In case the staging happens to osdata
   # A temporary tardisk would be created to run securemount command
   # before moving the tardisks to altbootbank
   TMP_TARDISK_STAGE_DIR = 'tmpstagetardisk'
   TMP_TARDISK_NAME = HostInfo.GetContainerId() + TMP_TARDISK_STAGE_DIR
   TMP_TARDISK_DIR = os.path.join(RAMDISK_ROOT, TMP_TARDISK_NAME)

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

   def __init__(self, root='/', stagedir=None, quickPatch=False):
      self._root = root
      self._useOsdata = False
      if os.path.exists(self.DISK_STAGING_PATH) and \
         HostInfo.IsDiskBacked(self.DISK_STAGING_PATH):
         self.STAGE_DIR = self.DISK_STAGING_PATH
         self._useOsdata = True
      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')
      # A boolean to indicate whether the live image belongs to live installer
      # (quickPatch=False) or quickpatch installer (quickPatch=True).
      self._quickPatch = quickPatch
      # Separate the indicators for live and quickpatch installers.
      self._stageIndicatorName = ('staged' if not self._quickPatch else
                                  'quickPatchStaged')
      self._stageindicator = os.path.join(self._stageroot,
                                          self._stageIndicatorName)
      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()}
      self._triggers = []

      # Map from init.d script to VIB IDs that are removed/added.
      self._serviceToVibMap = {
         self.ADDS: {},
         self.REMOVES: {},
      }
      # Map from VIB ID to component name(version) info.
      self._vibToCompMap = {}

      # Quick patch does not support any post install config triggers.
      # Initialize them only for LiveImageInstaller
      if not self._quickPatch:
         self.INST_TRIGGER_CLASSES = (FirewallRefreshTrigger,
                                      HostServiceRefreshTrigger,
                                      ToolsRamdiskRefreshTrigger,
                                      WBEMServiceEnableTrigger,
                                      VAAIServiceRefreshTrigger)

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

      # 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()
            if self.stagedatabase and self.stagedatabase.profile is not None:
               self.stagedatabase.profile.PopulateWithDatabase(
                  self.stagedatabase)
         except Exception as e:
            msg = ('Found a copy of staged image, but unable to load the '
                   'database: %s. Cleaning the live image staging area' % (e))
            log.warning(msg)
            self.Cleanup()
            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)

   def StartTransaction(self, imgprofile, imgsize, stageonly=False,
                        qpPayloadSize=0):
      """Reset staging directory to empty.
         Parameters:
            * imgprofile - The ImageProfile instance representing the
                           target set of VIBs for the new image.
            * imgsize - The installation size of the image.
            * stageonly - Boolean, if True, only stages the contents, i.e.,
                          changes are made only to stageliveimage.
            * qpPayloadSize - Quick Patch payload size of the image, i.e.,
                              the overall uncompressed size of Quick Patch
                              payloads of all Quick Patch VIBs in the image.
      """
      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, stageonly=stageonly, qpPayloadSize=qpPayloadSize)
      # 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, qpAdds=set()):
      '''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
            * qpAdds  - List of Quick Patch VIB IDs which will be added during
                        the transaction. This is a subset of adds.
         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()

      # Exclude Quick Patch eligible VIBs from mmode check.
      adds = adds - qpAdds

      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 _UpdateTriggers(self, adds=None, removes=None):
      '''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 _RunPreInstallTriggers(self, ignoreErrors=False):
      '''Execute pre installation triggers.
      '''
      for trigger in self._triggers:
         try:
            trigger.PreInstallAction()
         except Exception as e:
            if ignoreErrors:
               log.warning("Error running preinst trigger %s: %s", trigger().NAME, str(e))
            else:
               raise

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

   def Remediate(self, adds, removes, checkmaintmode, hasConfigDowngrade,
                 installerCls):
      '''Live remove VIBs and enable new VIBs
      '''
      self.prepareToRemediate(adds, removes, checkmaintmode)

      vibstoadd = [self.stagedatabase.vibs[vibid] for vibid in adds]
      vibstoremove = [self.database.vibs[vibid] for vibid in removes]

      # 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 = 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:

         #Invoke pre install triggers
         self._RunPreInstallTriggers()

         self._RemoveVibs(removes, hasConfigDowngrade, installerCls)
         self._AddVibs(adds, hasConfigDowngrade, installerCls)

         # 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.
         applyVibSecPolicy()

         # Start daemons in order
         self._StartServices()

         # Invoke post inst triggers
         self._RunPostInstallTriggers()

         # 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.
         self.removeStateBackup()

      # mark system live installed
      markerfile = os.path.join(self.root, LIVEINST_MARKER)
      MarkSystemLiveInstalled(markerfile)

   def removeStateBackup(self):
      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)

   def Cleanup(self):
      '''Remove data in staging area.
      '''
      if self._useOsdata:
         if os.path.exists(self.DISK_STAGING_PATH):
            shutil.rmtree(self.DISK_STAGING_PATH)
         self._useOsdata = False
         self.STAGE_DIR = self.TMP_STAGE_DIR
         return

      # 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, installerCls):
      log.debug('Starting to live remove VIBs: %s', ', '.join(removes))

      shutdownscripts = set()
      rpFiles = set()
      initFiles = set()

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

         # get resource pool definition files
         rpFiles.update(GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "etc/rp.d/*")))

         if not HostInfo.HostOSIsSimulator():
            initFiles.update(GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "etc/init.d/*")))

      self._ShutdownServices()

      if rpFiles or initFiles:
         self._recovery["removedRPs"] = DelRPs(rpFiles, initFiles)

      # 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 |= installerCls.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 = 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 = GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "usr/libexec/jumnpstart/plugins/*"))
         for fn in plugins:
            self._recovery["jumpstartplugins"].add(os.path.basename(fn))

         scripts = 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, installerCls):
      log.debug('Starting to enable VIBs: %s', ', '.join(adds))

      jumpstartplugins = set()
      rcscripts = set()
      rpFiles = set()
      initFiles = 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 = GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "usr/libexec/jumpstart/plugins/*"))
         for fn in plugins:
            jumpstartplugins.add(os.path.basename(fn))

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

         rpFiles.update(GetFilesFromVib(vib,
               lambda x: fnmatch.fnmatch(x, "etc/rp.d/*.yml")))

         if not HostInfo.HostOSIsSimulator():
            initFiles.update(GetFilesFromVib(vib,
                  lambda x: fnmatch.fnmatch(x, "etc/init.d/*")))

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

         # If the staging happens in scratch then we need to
         # create a ramdisk to temporarily hold a tardisk before it is mounted.
         # This is because Securemount requires payloads/tardisks to
         # be available on the ramdisk
         tmpMountPath = None
         try:
            payloadsToMount = installerCls.GetSupportedVibPayloads(vib)
            vibstates = self.stagedatabase.profile.vibstates
            if self._useOsdata:
               CreateSingleTardiskRamdisk\
                  (vibstates[vibid].payloads.values(), self._stagedata,
                   self.TMP_TARDISK_NAME, self.TMP_TARDISK_DIR)
               tmpMountPath = self.TMP_TARDISK_DIR

            for name, localname in vibstates[vibid].payloads.items():
               if name in payloadsToMount:
                  log.debug('Trying to mount payload [%s]', name)
                  if tmpMountPath is not None:
                     self._MoveAndMountTardiskSecure(vibid, name, \
                           localname, tmpMountPath)
                  else:
                     self._MountTardiskSecure(vibid, name, localname)
                  self._recovery["mountedtardisks"].append(localname)
         finally:
            # The ramdisk created above is used only for securemount purpose
            # Remove it post usage
            if tmpMountPath is not None:
               Ramdisk.RemoveRamdisk(self.TMP_TARDISK_NAME,
                                     self.TMP_TARDISK_DIR)

         # 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'),
                    mode='r:gz', format=tarfile.GNU_FORMAT) 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(list(adds), list(self._cfgupgrademodules),
                         self.stagedatabase.profile)

      if rpFiles or initFiles:
         self._recovery["addedRPs"] = AddRPs(rpFiles, initFiles)

      if jumpstartplugins:
         StartJumpStartPlugins(jumpstartplugins)

      if rcscripts:
         RunRcScripts(rcscripts)

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

   def _UpdateDB(self, useStageDB=True, createMountRamdisk=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.
            * createMountRamdisk: Check details in _MountImgdbSecure().
      """
      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,
                                createMountRamdisk=createMountRamdisk)
      except Exception:
         if os.path.exists(newDbPath):
            os.remove(newDbPath)
         if os.path.exists(backupDbPath):
            self._MountImgdbSecure(backupDbPath,
                                   createMountRamdisk=createMountRamdisk)
         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, stageonly=False, qpPayloadSize=0):
      """Set up the staging area.
         Check parameter details in StartTransaction().
      """
      self.Cleanup()
      isSpaceAvailableOnDisk = False
      if self._quickPatch:
         if not qpPayloadSize:
            raise ValueError("The total uncompressed size of Quick Patch "
                             "payloads cannot be 0 for Quick Patch staging.")
         # Quick Patch stages only the relevant payloads whose combined
         # uncompressed size is given in qpPayloadSize.
         # Add 10 MB buffer for imgdb, etc.
         requiredSpace = round(qpPayloadSize / MIB) + 10
      else:
         # TODO: live staging can also need smaller required space.
         requiredSpace = 4 * round(imgsize / MIB)
      # Keeping aside at least 1GB for logging, core dumps etc
      isSpaceAvailableOnDisk = HostInfo.IsDiskBacked(LIFECYCLE_DIR) and \
                              HostInfo.IsFreeSpaceAvailable(LIFECYCLE_DIR,
                              requiredSpace, reserveRatio=0.1,
                              minimumReserveSpace=1024)
      try:
         # Stage to ramdisk if part of apply and previously not staged.
         if not stageonly:
            self._useOsdata = False
         elif not self._useOsdata:
            if isSpaceAvailableOnDisk:
               self._useOsdata = True
               self.STAGE_DIR = self.DISK_STAGING_PATH
               self._stageroot = os.path.join(self.root, self.STAGE_DIR)
               self._stagedata = os.path.join(self._stageroot, 'data')
               self._stagedbpath = os.path.join(self._stageroot, 'imgdb')
               self._stageindicator = os.path.join(self._stageroot,
                                                   self._stageIndicatorName)
         if not self._useOsdata:
            tardiskMb = round(imgsize / MIB)
            createTardiskMountRamdisk(self.STAGE_RAMDISK_NAME,
                                      self._stageroot,
                                      tardiskMb)
         if not os.path.exists(self._stagedata):
            os.makedirs(self._stagedata)
      except Errors.InstallationError as e:
         msg = "No space available in scratch and ramdisk"
         if isSpaceAvailableOnDisk:
            self._useOsdata = True
            self.STAGE_DIR = self.DISK_STAGING_PATH
            self._stageroot = os.path.join(self.root, self.STAGE_DIR)
            self._stagedata = os.path.join(self._stageroot, 'data')
            self._stagedbpath = os.path.join(self._stageroot, 'imgdb')
            self._stageindicator = os.path.join(self._stageroot,
                                                self._stageIndicatorName)

         if not self._useOsdata:
            if hasattr(e, 'cause') and \
               isinstance(e.cause, Errors.MemoryReserveError):
               raise Errors.ResourceError(msg)
            else:
               raise e
      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):
         with open(self._stageindicator, 'w'):
            pass

   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, createMountRamdisk=True):
      '''Mount imgdb tar with SecureMount to update the live database.
         SecureMount verifies that the tardisk contains only esximg database.
         Parameters:
            * path - The path to update the live database.
            * createMountRamdisk - The boolean to indicate whether we need to
                                   create/remove the new ramdisk, which is
                                   default to be True. If staging happened in
                                   scratch in QuickPatchInstaller, we already
                                   created the same one before updating the
                                   database, thus no need to create and remove
                                   it here.
      '''
      if self._useOsdata:
         if createMountRamdisk:
            tardiskSize = round(os.path.getsize(path) / MIB)
            createTardiskMountRamdisk(self.TMP_TARDISK_NAME,
                                      self.TMP_TARDISK_DIR,
                                      tardiskSize)
         shutil.move(path, self.TMP_TARDISK_DIR)
         fileName = os.path.basename(path)
         newPath = os.path.join(self.TMP_TARDISK_DIR, fileName)
         args = [SECURE_MOUNT_SCRIPT, newPath]
         RunCmdWithMsg(args, 'Updating imgdb using %s...' % (newPath))
         if createMountRamdisk:
            Ramdisk.RemoveRamdisk(self.TMP_TARDISK_NAME, self.TMP_TARDISK_DIR)
      else:
         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.
      '''
      if self._useOsdata:
         tardiskpath = os.path.join(self.TMP_TARDISK_DIR, tardisk)
      else:
         tardiskpath = os.path.join(self._stagedata, tardisk)
      args = [SECURE_MOUNT_SCRIPT, vibid, payload, tardiskpath]
      RunCmdWithMsg(args, 'Mounting tardisk %s...' % tardisk)


   def _MoveAndMountTardiskSecure(self, vibid, payload, tardisk, movedir):
      '''Move the tardisk from a file system to Ramdisk and then run
         securemount to verify and mount the tardisk.
      '''
      srcfile = os.path.join(self._stagedata, tardisk)
      destfile = os.path.join(movedir, tardisk)
      shutil.move(srcfile, destfile)
      self._MountTardiskSecure(vibid, payload, tardisk)

   def _UnmountTardisk(self, tardisk, vibId=None):
      cmd = '/bin/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('/bin/lsof > %s' % LSOF_DUMP_FILE,
                             raiseexception=False)
               log.debug('Open file descriptors have been dumped to %s',
                         LSOF_DUMP_FILE)
            except Exception as ex:
               log.warning('Dumping file descriptors to %s failed: %s',
                        LSOF_DUMP_FILE, str(ex))

            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

   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 = GetUpgradeVibs(adds, removes)

      filterfn = lambda x: fnmatch.fnmatch(x, "etc/init.d/*")
      for vib in adds:
         files = 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 = 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 _HandleLiveVibFailure(self, msg):
      '''Handle live vib installation failure by:
            * Shutdown the services from new vibs
            * Delete RPs added by new VIBs
            * Unmount the tardisks from new vibs
            * Mount the unmounted tardisks
            * Restore configuration
            * Restore RPs defined by removed vibs
            * 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.warning("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.warning("Unable to shutdown service %s: %s", cmd, str(e))

      # Destroy the resource groups
      for rp in reversed(self._recovery["addedRPs"]):
         try:
            rp.delete()
         except Exception as e:
            log.warning("%s", e)

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

      # If the staging happens in scratch then we need to
      # create a ramdisk and copy the vibs there
      # because securemount command fails on filesystem locations
      tmpMountPath = None
      try:
         if self._useOsdata:
            tardisks = [tardiskDetails[2] for tardiskDetails in
               self._recovery["unmountedtardisks"]]
            CreateSingleTardiskRamdisk(tardisks, self.TARDISK_BACKUP_DIR,
                  self.TMP_TARDISK_NAME, self.TMP_TARDISK_DIR)
            tmpMountPath = self.TMP_TARDISK_DIR

         # 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)
               if tmpMountPath is not None:
                  self._MoveAndMountTardiskSecure(vibid, name,
                        localname, tmpMountPath)
               else:
                  self._MountTardiskSecure(vibid, name, localname)
            except Exception as e:
               log.warning("Unable to mount tardisk %s: %s", localname, str(e))
      # The ramdisk created above is used only for securemount purpose
      # Remove it post usage
      finally:
         if tmpMountPath is not None:
            Ramdisk.RemoveRamdisk(self.TMP_TARDISK_NAME, self.TMP_TARDISK_DIR)

      # Restore the removed configs from /var/run/statebackup/local.tgz
      try:
         with tarfile.open(os.path.join(self._statebackupdir, 'local.tgz'),
                           mode='r:gz', format=tarfile.GNU_FORMAT) 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)

      # Recreate resource pools
      for rp in reversed(self._recovery["removedRPs"]):
         try:
            rp.create()
         except Exception as e:
            log.error("recreate %s: %s", rp.name, e)

      # Start jumpstart plugins and rc scripts
      try:
         if self._recovery["jumpstartplugins"]:
            StartJumpStartPlugins(self._recovery["jumpstartplugins"])
         if self._recovery["rcscripts"]:
            RunRcScripts(self._recovery["rcscripts"])
      except Exception as e:
         log.warning("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.warning("Unable to start service %s: %s", cmd, str(e))

      # Invoke triggers
      self._RunPostInstallTriggers(ignoreErrors=True)

   def prepareToRemediate(self, adds=set(), removes=set(),
                          checkmaintmode=False, qpAdds=set()):
      """Setup/Prepare the live image for remediation. This is used by both
         LiveImageInstaller and QuickPatchInstaller.
      """
      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)

      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}

      # Post install triggers are applicable only for LiveImageInstaller. Skip
      # for QuickPatchInstaller
      if not self._quickPatch:
         self._UpdateServiceStates(vibstoadd, vibstoremove)

         for cls in self.INST_TRIGGER_CLASSES:
            self._triggers.append(cls())

         self._UpdateTriggers(vibstoadd, vibstoremove)

      # Verify that maintenance mode is not required for the transaction.
      mmoderemove, mmodeinstall = self.GetMaintenanceModeVibs(
                                    self.stagedatabase.vibs, adds, removes,
                                    qpAdds=qpAdds)
      if mmoderemove or mmodeinstall:
         msg = ('MaintenanceMode is required to '
                'remove: [%s]; install: [%s].'
                % (', '.join(mmoderemove),
                   ', '.join(mmodeinstall)))
         log.debug(msg)
         if checkmaintmode:
            if not HostInfo.GetMaintenanceMode():
               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)

      # 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)

         extractTar(dest, self._statebackupdir, mode='r:gz',
                    exception=ValueError)

         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))
