#!/usr/bin/python
# Copyright (c) 2023 VMware, Inc. All rights reserved. -- VMware Confidential

import logging
import os
import shutil

from vmware import runcommand, vsi

from .InstallerCommon import (AddRPs, applyVibSecPolicy,
   createTardiskMountRamdisk, CreateSingleTardiskRamdisk, DelRPs,
   ExecuteCommandError, LIVEINST_MARKER, MarkSystemLiveInstalled,
   RAMDISK_ROOT, RemediationResult, StartTransactionResult, SecPolicyTools)
from .LiveImageInstaller import FileState, LiveImageInstaller
from .. import Downloader, Errors, IS_ESXCLI, MIB, Vib, VibCollection
from ..ImageManager.Constants import (ERROR, INFO, QUICK_PATCH_NO_ACTIONS,
   QUICK_PATCH_REMEDIATION_DEFAULT_MSG, QP_SCRIPT_ERROR_ID, QP_SCRIPT_FAILED_ID,
   QP_SCRIPT_INFO_ID, QP_SCRIPT_STARTING_ID, QP_SCRIPT_SUCCEEDED_ID,
   QP_SCRIPT_WARNING_ID, WARNING)
from ..Utils import PartialVibDownloader, Ramdisk
from ..Utils.HostInfo import GetMaintenanceMode, IsTpmActive
from ..Utils.Misc import byteToStr

log = logging.getLogger('QuickPatchInstaller')

QUICK_PATCH_FAILURE_WARNING = '''Please follow the resolution(s) presented, \
if any, against each failed script or reboot the host to discard the \
unfinished update'''

QUICK_PATCH_INCOMPLETE_WARNING = '''The Quick Patch image was remediated. \
However, an error occurred while performing Quick Patch remediation actions. \
Please follow the resolution(s) presented, if any, or reboot the host to \
complete the remediation.'''

QUICKPATCH_MARKER = LIVEINST_MARKER

def _getMessageArgs(msgObj, scriptName):
   """Given msgObj, return the concatenated long string message.
      scriptName will only be used in the error message if an error
      is to be raised due to script processing failure.
   """
   from ..ImageManager.QuickPatchScriptLib import (LocalizableMessage,
                                       Notification as qplNotification)

   overallMessage = ""
   # Each element in non-empty "remediationActions" has fields "msgId",
   # "args", and "defaultMessage"; while each element in non-empty "info"/
   # "warnings"/"errors" has two fields (message and resolution), each of
   # which would contain fields "msgId", "args", and "defaultMessage" if
   # it is non-empty.
   if isinstance(msgObj, LocalizableMessage):
      # "remediationActions" case only
      msgArgs = msgObj.args
      defaultMessage = msgObj.defaultMessage
   elif isinstance(msgObj, qplNotification):
      # "info"/"warnings"/"errors" case
      msg = msgObj.message
      msgArgs = msg.args
      defaultMessage = msg.defaultMessage
   else:
      errMsg = ("Unexpected type of message '%s' for Notifications or "
                  "RemediationActions while processing script '%s'." % (
                  str(msgObj), scriptName))
      cause = Errors.QuickPatchScriptError(scriptName, errMsg)
      raise Errors.QuickPatchInstallationError(cause, None, errMsg)
   overallMessage = _formatStringArgs(defaultMessage, msgArgs)

   # For a notification, if the resolution exists, keep it the same line as
   # notification message.
   if isinstance(msgObj, qplNotification):
      resolution = msgObj.resolution
      if resolution:
         overallMessage += "  "
         errMsgArgs = resolution.args
         errDefaultMsg = resolution.defaultMessage
         overallMessage += _formatStringArgs(errDefaultMsg, errMsgArgs)
   # End each action/notification message + resolution with a newline
   overallMessage += "\n"
   return overallMessage

def _getNotifications(scriptOutput):
   """Get notifications object of each type given the scriptOutput dict
   """
   from ..ImageManager.QuickPatchScriptLib import \
      Notifications as qplNotifications

   INFO, WARNINGS, ERRORS = (qplNotifications.INFO, qplNotifications.WARNINGS,
                             qplNotifications.ERRORS)

   info = scriptOutput.notifications._notificationDict[INFO]
   warnings = scriptOutput.notifications._notificationDict[WARNINGS]
   errors = scriptOutput.notifications._notificationDict[ERRORS]
   return info, warnings, errors

def _formatStringArgs(msg, msgArgs):
   # If there are no args to format, simply return the input message.
   # Otherwise, when there is at least one arg to format, since the
   # string containing '{n}' where n starts with 1 not 0, we need to
   # manually add one more arg at the beginning when using .format().
   return msg.format('', *msgArgs) if len(msgArgs) else msg

def _getMessages(info, warnings, errors, scriptName, remediationActions=[]):
   """Get the messages for each type of notification from executing the script
      scriptName.
   """
   infoMessage = ""
   for infoMsg in info:
      infoMessage += _getMessageArgs(infoMsg, scriptName)

   warningMessage = ""
   for warning in warnings:
      warningMessage += _getMessageArgs(warning, scriptName)

   errorMessage = ""
   for error in errors:
      errorMessage += _getMessageArgs(error, scriptName)

   actionMessage = ""
   for action in remediationActions:
      actionMessage += _getMessageArgs(action, scriptName)

   return infoMessage, warningMessage, errorMessage, actionMessage

def processQpScanScriptNotification(vibId, results, continueOnFailure=False):
   """Parses the result notification from Quick Patch scripts.
      Parameters:
         * vibId - The ID of the VIB which is being Quick Patched.
         * results - A dictionary mapping each script name to its execution
                     result. See _runQuickPatchScripts().
         * continueOnFailure - If True, continue to process the next script even
                               if one script returned non-zero, invalid output,
                               or INCOMPATIBLE compliance status;
                               otherwise, raise QuickPatchScriptError on the
                               first instance of such a script.
      Returns:
         A list of final processed string for each script.
            * Each string concatenates all non-empty info/warning/
              remediatonAction messages, or an empty string if none is present.
            * If continueOnFailure=True and a script returned non-zero or
              INCOMPATIBLE, the processed string will be an error message.
   """

   # For the final output to return, it is a long string that combines all
   # RemediationActions and then info/warning notifications. Two script results
   # will be separated with an extra new line.
   # - See an example:
   # Service service0 will be restarted during the remediation.
   # Info msg with args: arg1 and arg2.  Info msg resolution with no arg.
   #
   # Service service1 will be restarted during the remediation.

   parseResult = {}
   for scriptName, result in results.items():
      try:
         overallMessage = ""
         scriptNameMsg = "Script '%s': " % scriptName
         rc, out, scriptOutput = result

         if scriptOutput is None:
            # Failed to execute or crashed.
            msg = ("Script '%s'%s did not return a valid JSON"
                   " structure. Output: %s" % (scriptName,
                   " with return code %d" % rc if rc is not None else "",
                   out))
            if continueOnFailure:
               parseResult[scriptName] = msg + '\n'
               continue
            else:
               cause = Errors.QuickPatchScriptError(scriptName, msg)
               raise Errors.QuickPatchInstallationError(cause, [vibId], msg)

         info, warnings, errors = _getNotifications(scriptOutput)

         # Scan results have compliance status and remediateActions fields.
         remediationActionStatus = scriptOutput.remediationActionStatus.name
         remediationActions = scriptOutput.remediationActions.actionList

         # Print results in the order of: remediationActions (if any), info
         # notifications/resolutions, warning notificaitons/resolutions.
         infoMessage, warningMessage, errorMessage, actionMessage = \
               _getMessages(info, warnings, errors, scriptName,
                            remediationActions=remediationActions)

         if infoMessage:
            log.debug("Script '%s' has info: %s", scriptName,
                      infoMessage.rstrip("\n"))
            infoMessage = "Info - %s" % infoMessage

         if warningMessage:
            log.debug("Script '%s' has warnings: %s", scriptName,
                      warningMessage.rstrip("\n"))
            warningMessage = "Warnings - %s" % warningMessage

         if errorMessage:
            log.debug("Script '%s' has errors: %s", scriptName,
                      errorMessage.rstrip("\n"))

         if actionMessage:
            log.debug("Script '%s' has remediationActions: %s", scriptName,
                      actionMessage.rstrip("\n"))
            actionMessage = "Remediation Action - %s" % actionMessage

         overallMessage += infoMessage + warningMessage + actionMessage

         # Scan scripts are not expected to return non-zero rc. In case that
         # happens, show the entire notification along with the error msg.
         if rc != 0:
            msg = ("Script '%s' has non-zero return code %d.\n" %
                   (scriptName, rc))
            if continueOnFailure:
               overallMessage = msg + overallMessage
            else:
               cause = Errors.QuickPatchScriptError(scriptName, msg)
               raise Errors.QuickPatchInstallationError(cause, [vibId], msg)
         # Script execution is successful but the remediation action status is
         # INCOMPATIBLE.
         elif rc == 0 and remediationActionStatus == 'INCOMPATIBLE':
            log.error("When running script '%s' in VIB '%s', "
                      "remediationActionStatus is INCOMPATIBLE with "
                      "error(s): %s", scriptName, vibId,
                      errorMessage.rstrip("\n"))
            # Only esxcli mode will trigger this message. We will use a
            # separate util function for vLCM mode.
            msg = ("Quick Patch cannot be performed due to the following "
                   "reason(s): \n%sPlease follow the resolution(s) above, if "
                   "available, or use 'esxcli software profile update' command "
                   "to perform a non-QuickPatch upgrade.\n" % errorMessage)
            if continueOnFailure:
               overallMessage = scriptNameMsg + msg + overallMessage
            else:
               cause = Errors.QuickPatchScriptError(scriptName, msg)
               raise Errors.QuickPatchInstallationError(cause, [vibId], msg)
         # If there are errors, append them to display and not raise an error.
         elif errorMessage:
            overallMessage = scriptNameMsg + overallMessage + errorMessage
         # No errors with non-empty notification and/or remediation action
         elif overallMessage != "":
               overallMessage = scriptNameMsg + overallMessage

         parseResult[scriptName] = overallMessage
      except Exception as e:
         msg = ("Getting '%s' script result failed with: %s" %
                (scriptName, str(e)))
         raise generateQuickPatchRemediationError(e, msg, vibs=[vibId])

   # If for each script, overallMessage is empty, meaning that
   # remediationActionStatus is "COMPLIANT", notifications/
   # remediationActions is empty, the combined string will also be empty.
   # Otherwise, combine all overallMessages to a new string as the output, add
   # an extra new line between two script messages, and skip the empty strings
   # that come from "remediationActionStatus is COMPLIANT" cases.
   msgList = [overallMessage for overallMessage in
              parseResult.values() if overallMessage != ""]

   # Note that here the combined string is just a raw string. It can be empty,
   # or can end with a newline. Specific format will be given in merge later.
   return "\n".join(msgList)

def MergeQuickPatchScriptNotification(msgList, apply=False):
   """Given a list of already processed Quick Patch script Notifications (each
      element in the list is from a single Quick Patch VIB), form a final long
      multi-line message that has processed script results from all scripts in
      all Quick Patch VIBs.
         Parameter:
            * msgList - a list of raw strings that each concatenates all
              non-empty info/warning/remediatonAction messages from a script,
              or a list of empty strings.
            * apply - boolean, true if being called to process apply script
              notification, false otherwise.
         Return:
            * A string that concatenates each element from the input string,
              or a human readable string for the case of compliant if the input
              is a list of empty strings representing no actions.
   """
   finalList = [msg for msg in msgList if msg != ""]

   # Only when all Quick Patch VIBs having no actions will we finally output "no
   # action is required". This is a human readable explanation for the case
   # where the final string is empty.
   # Othewise, skip the empty strings, combine and show those messages with
   # pending actions and/or notifications, and separate each other with an extra
   # new line.
   return ("\n".join(finalList)).rstrip("\n") if finalList else (
           QUICK_PATCH_NO_ACTIONS if not apply else
           QUICK_PATCH_REMEDIATION_DEFAULT_MSG)

def generateQuickPatchRemediationError(err, msg, vibs=None,
                                       isIncomplete=False):
   """Given an exception, wrap into QuickPatchInstallationError if the exception
      is not yet an instance of it. If the exception was raised due to an
      incomplete remediation, fill in the cause with a specific incomplete type
      reason.
      Parameters:
         * err - The error instance.
         * msg - The error message, which is usually str(err).
         * vibs - The VIB IDs of the VIBs that are being Quick Patch remediated,
                  if known when the error occurred.
         * isIncomplete - A boolean to indicate whether the error is raised due
                          to an incomplete remediation.
   """
   if not isinstance(err, Errors.QuickPatchInstallationError):
      err = Errors.QuickPatchInstallationError(err, vibs, msg)

   if isIncomplete:
      if err.cause and isinstance(err.cause,
                                  Errors.QuickPatchScriptError):
         # Quick Patch script error.
         err.cause = Errors.QuickPatchIncompleteScriptError(
            err.cause.scriptNames, str(err.cause))
      else:
         # Generic incomplete error.
         err.cause = Errors.QuickPatchIncompleteError(str(err.cause))
      # Append the incomplete warning message to the end of the error message.
      err.msg += "\n" + QUICK_PATCH_INCOMPLETE_WARNING

   return err

class QuickPatchInstaller(LiveImageInstaller):
   """Encapsulates attributes of QuickPatchInstaller

      Attributes:
         * qpActionOnlyVibs - A set of VIB IDs of all Quick Patch VIBs from
                              the new profile, only used by quickpatch
                              installer for incomplete remediation in case the
                              new profile is not staged.
         * qpAdds           - A set of Quick Patch eligible VIB IDs. It will be
                              used to filter the Quick Patch VIBs from mmode
                              check during remediation.
         * qpIncompleteRemediateError - An error instance used to save
                                        QuickPatchInstallationError for the
                                        incomplete remediation, i.e., any errors
                                        raised in apply-published scripts during
                                        regular Quick Patch remediation.
         * qpVibRemoveDict  - A dictionary of VIB IDs, each key value pair maps
                              a Quick Patch VIB to add -> one or more related
                              VIB(s) to remove due to being replaced by the
                              Quick Patch VIB.
         qpAdds is only for the Quick Patch VIBs to be mounted and remediated;
         while qpActionOnlyVibs is for all Quick Patch VIBs, which
         will only be assigned and used in the incomplete remediation case.
   """

   installertype = "quickpatch"
   priority = 4

   # A single ramdisk is used both as temporary storage and for mounting.
   # The directory must be present in tmp-vmxScan/ApplyDom.
   QP_TEMP_EXEC_RAMDISK_NAME = 'qptempexec'
   QP_TEMP_EXEC_RAMDISK_PATH = os.path.join(RAMDISK_ROOT,
                                            QP_TEMP_EXEC_RAMDISK_NAME)

   def __init__(self, root='/', task=None, enableQuickPatch=False,
                enforceQuickPatch=True):
      super().__init__(root, quickPatch=True, task=task)
      self.qpActionOnlyVibs = None
      self.qpAdds = set()
      self.qpIncompleteRemediateError = None
      self.qpVibRemoveDict = {}

      # Variable to say whether a reboot is required in case of Quick
      # Patch installation failure to get the system back to good state.
      self._qpRebootAdvised = False

      # Scan script execution result dict.
      self._qpScanScriptRes = None

      # When enableQuickPatch is False, QuickPatchInstaller will not work
      # functionally, and will skip operations like StartTransaction and
      # Remediate.
      self.enableQuickPatch = enableQuickPatch

      # vLCM scan and staging will set this to False to get results of all
      # scripts.
      self.enforceQuickPatch = enforceQuickPatch

      # Whether it is a staging operation, set in StartTransaction().
      self.stageOnly = False

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

   @property
   def useOsData(self):
      return self.liveimage._useOsdata

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

   def updateDB(self, createMountRamdisk=True):
      """Update the live image DB. See LiveImage._UpdateDB() in InstallerCommon
         for details.
      """
      return self.liveimage._UpdateDB(createMountRamdisk=createMountRamdisk)

   def GetQuickPatchPayloadSize(self, imgProfile):
      """Return total uncompressed size of Quick Patch payloads.
          Parameter:
             * imgProfile - An ImageProfile instance.
          Returns:
             * Total uncompressed size of Quick Patch payloads in bytes.
             * If a Quick Patch payload has None or zero uncompressedsize,
               the return value will be 0.
      """
      totalSize = 0
      for vibid in imgProfile.vibIDs:
         vib = imgProfile.vibs[vibid]

         # Skip the VIB in case of no platforms.
         if not vib.hasSystemSoftwarePlatform:
            continue

         # Iterate Quick Patch VIBs to get all Quick Patch payloads.
         if vib.isQuickPatchVib:
            for payload in vib.payloads:
               if payload.isQuickPatchRelevant():
                  if not payload.uncompressedsize:
                     return 0
                  totalSize += payload.uncompressedsize
      return totalSize

   def getQuickPatchParamsFromProfile(self, curProfile, newProfile):
      """Checks whether the given image profile can Quick Patch from the
         existing profile. For a profile to be Quick Patch eligible, the
         following conditions must be true:
         a) Baseimage of the new image profile is able to Quick Patch from the
            baseimage in the current one.
         b) The new image profile should be patch release. This can be
            indirectly verified by checking if a resource pool definition is
            available for the VIB.
         c) There should be no VIB/component in the profile that requires a
            reboot.
         d) The files of a Quick Patch VIB must not be overlaid by another VIB.

         Returns:
            * A tuple of (qpAdds, qpRemoves, runQpScanScripts).
            * qpAdds/qpRemoves can be a non-empty set of vibIds representing the
              Quick Patch VIBs to add/remove, or None; runQpScanScripts is a
              boolean that indicates whether Quick Patch scan scripts should be
              executed, which will be True if the baseimage on the host can
              Quick Patch to the target baseimage or it itself is a Quick Patch
              release.
            * If newProfile can Quick Patch from curProfile, qpAdds/qpRemoves is
              non-empty, runQpScanScripts is True -> return
              (qpAdds, qpRemoves, True);
              If a target image profile that is Quick Patch eligible but shares
              the same baseimage version with the source, qpAdds/qpRemoves will
              be empty, but runQpScanScripts is True -> return
              (None, None, True);
              Otherwise, runQpScanScripts is False -> return
              (None, None, False).
      """
      # Check if quickPatchCompatibleVersions of newProfile's baseimage contain
      # versionSpec of curProfile's baseimage.
      isMatched = False
      for versionSpec in newProfile.baseimage.quickPatchCompatibleVersions:
         if str(versionSpec.version) == \
               str(curProfile.baseimage.versionSpec.version):
            isMatched = True
            break

      # Verify if the target image profile is Quick Patch eligible.
      # Get Quick Patch VIB to add. We only get non-empty qpAdds.
      qpAdds = set(vib.id for vib in newProfile.vibs.values() if
                   vib.isQuickPatchVib)

      # No Quick Patch VIBs in newProfile.
      if not qpAdds:
         return None, None, False

      if not isMatched:
         # When the target image profile is Quick Patch eligible, check if
         # newProfile and curProfile share the same baseimage versionSpec.
         if str(newProfile.baseimage.versionSpec.version) == str(
                curProfile.baseimage.versionSpec.version):
            return None, None, True
         return None, None, False

      # Get VIBs to remove that are being updated by Quick Patch add for
      # further usage to modify VIBs to remove/keep.
      adds, removes = newProfile.Diff(curProfile)
      totalDict = {add : newProfile.vibs[add] for add in adds}
      # Merge two dicts.
      totalDict.update({remove : curProfile.vibs[remove] for remove in removes})
      # Get the relationship between VIBs to add and remove.
      vibCollection = VibCollection.VibCollection(totalDict)
      scanResults = vibCollection.Scan()

      # Get qpRemoves related to qpAdds.
      qpRemoves = set()
      for qpAdd in qpAdds:
         if qpAdd not in scanResults.vibs:
            # The Quick Patch VIB to add is not in the VibCollection scan
            # results.
            return None, None, False
         if not scanResults.vibs[qpAdd].replaces:
            # The Quick Patch VIB to add does not have related VIBs to remove
            # for update.
            continue
         # A Quick Patch add can have one or more remove(s).
         self.qpVibRemoveDict[qpAdd] = scanResults.vibs[qpAdd].replaces
         qpRemoves.update(self.qpVibRemoveDict[qpAdd])

      return qpAdds, qpRemoves, True

   def _runQuickPatchScripts(self, scripts, scriptType, prefix='/',
                             continueOnFailure=False):
      """Runs Quick Patch scripts.
         Parameters:
            * scripts - A list of Vib.QuickPatchScripts instances where everyone
                        shares the same script type.
            * scriptType - Type of the script.
            * prefix - Prefix of script path.
            * continueOnFailure - If continueOnFailure is True, executes all the
                                 scripts in the list irrespective of whether a
                                 script was able to execute; if False, raise an
                                 exception on the first script that failed to
                                 execute.

         Returns:
            A dictionary that maps each script name to a tuple of:
            (return code, output, Apply/ScanScriptOutput instance).

            When continueOnFailure=True, The Apply/ScanScriptOutput instance
            will be None if the output cannot be parsed properly.

         Raises:
            * ValueError - invalid scriptType input.
            With continueOnFailure=True:
               * QuickPatchScriptError - when a script failed to execute.
      """
      # Can only be imported in Python 3.7+; import locally to avoid upgrade
      # error from < 7.0.2.
      from ..ImageManager.QuickPatchScriptLib import (
         ApplyScriptOutput,
         ScanScriptOutput)

      results = {}

      if scriptType not in Vib.QuickPatchScript.SCRIPT_TYPES:
         errMsg = "Unknown script type %s found." % scriptType
         log.error(errMsg)
         raise ValueError(errMsg)

      for script in sorted(scripts, key=lambda s: os.path.basename(s.path)):
         scriptPath = os.path.join(prefix, script.path)
         timeout = script.timeout
         scriptName = os.path.basename(scriptPath)

         # PR 3271066, PR 3274352: We want to run the scripts in their own
         # process group (so that we can terminate any forked children), but
         # we must use `ForkExec()` (via `VisorPopen()`) to avoid memory
         # overhead. Since `VisorPopen()` cannot directly create a new session,
         # use `setsid` to wrap the script in a new session (and process group).
         scriptArgs = ['/bin/setsid', scriptPath]
         if IS_ESXCLI:
            scriptArgs.append('--esxcli')
         self.AddTaskNotification(QP_SCRIPT_STARTING_ID,
                                  msgArgs=[scriptType, scriptName])

         try:
            scriptProc = runcommand.VisorPopen(scriptArgs)
            log.info('Quick Patch script: type=%r pid=%d timeout=%s args=%r',
                     scriptType, scriptProc.pid, timeout, scriptArgs)
            try:
               rc, out = runcommand.waitProcessToComplete(
                  scriptProc,
                  args=scriptArgs,
                  timeout=timeout,
               )
            except runcommand.RunCommandError:
               # The process did not complete.
               # PR 3271066: Terminate the script's process group.
               try:
                  runcommand.terminateProcessGroup(scriptProc)
               except Exception as terminateError:
                  # The entire process group cannot be terminated. We cannot do
                  # much besides log and raise the original exception.
                  log.exception("Failed to terminate %s script '%s': %s",
                                scriptType, scriptName, str(terminateError))
               raise
         except Exception as e:
            self.AddTaskNotification(QP_SCRIPT_FAILED_ID,
                                     msgArgs=[scriptType, scriptName],
                                     type_=ERROR)
            errMsg = ("Failed to execute %s script '%s': %s" %
                     (scriptType, scriptName, str(e)))
            if continueOnFailure:
               log.exception(errMsg)
               results[scriptName] = (None, errMsg, None)
               continue
            raise Errors.QuickPatchScriptError(scriptName, errMsg) from e

         out = byteToStr(out)
         log.debug("Script '%s' returned %d with output: %s", scriptName, rc,
                   out)

         try:
            if scriptType == Vib.QuickPatchScript.QP_SCRIPT_TYPE_SCAN:
               scriptRes = ScanScriptOutput.fromJSON(out)
            else:
               scriptRes = ApplyScriptOutput.fromJSON(out)
         except Exception as e:
            # Return is either not a valid JSON structure or does not conform to
            # the script return specification.
            # The caller decides what exception to raise in this case.
            msg = ("Failed to parse the output of script '%s': %s"
                  % (scriptName, str(e)))
            log.exception(msg)
            scriptRes = None

         results[scriptName] = (rc, out, scriptRes)

         if rc != 0 or scriptRes is None:
            self.AddTaskNotification(QP_SCRIPT_FAILED_ID,
                                     msgArgs=[scriptType, scriptName],
                                     type_=ERROR)
         else:
            self.AddTaskNotification(QP_SCRIPT_SUCCEEDED_ID,
                                     msgArgs=[scriptType, scriptName])

         # If continueOnFailure is false, return without executing rest of the
         # scripts.
         if rc != 0 and not continueOnFailure:
            break
      return results

   def _addScriptNotificationsToTask(self, info, warnings, errors, scriptName,
                                     scriptType):
      """Adds Quick Patch script notifications to the vLCM task.
         Each script message is wrapped into a localizable one with the full
         message and resolution (if given) as an argument.
      """
      def addNotification(notification, notificationId, type_):
         """Adds one notification.
         """
         msg = _formatStringArgs(notification.message.defaultMessage,
                                 notification.message.args)
         if notification.resolution:
            # Concatenante message and resolution.
            msg += ' ' + _formatStringArgs(
               notification.resolution.defaultMessage,
               notification.resolution.args)
         self.AddTaskNotification(notificationId,
                                  msgArgs=[scriptType, scriptName, msg],
                                  type_=type_)

      for n in info:
         addNotification(n, QP_SCRIPT_INFO_ID, INFO)
      for n in warnings:
         addNotification(n, QP_SCRIPT_WARNING_ID, WARNING)
      for n in errors:
         addNotification(n, QP_SCRIPT_ERROR_ID, ERROR)

   def _processQpApplyScriptNotification(self, vibId, results, scriptType):
      """Parses the result notification from Quick Patch apply scripts.
         Parameters:
            * vibId - The ID of the VIB which is being Quick Patched.
            * results - A dictionary mapping each script name to its execution
                        result. See _runQuickPatchScripts().
            * scriptType - Type of apply script that was executed.
         Returns:
           A tuple of
                * final processed string for each script,
                  -  Each string concatenates all non-empty info/warning/
                     or an empty string if none is present.
               * scriptFailure
                  - Indicating whether any of the scripts failed.
               * cause
                  - A QuickPatchScriptError instance containing the first
                    failure.
         Side
      """
      parseResult = {}

      # Keep track of whether there is a script failure.
      scriptFailure = False

      # A QuickPatchScriptError will be raised if one or more scripts fail.
      cause = None

      # For the final output to return, it is a long string that combines all
      # info/warning/error notifications. Two script results
      # will be separated with an extra new line.
      # Example:
      # Script 'applyScript1.py' has non-zero return code 1. Output:
      # error message with arguments: arg1 and arg2  error message
      # resolution with no argument
      #
      # Script 'applyScript2.py': Warnings - warning message with arguments:
      # arg1 and arg2  warning message resolution with no argument

      failedScriptNames = []
      for scriptName, result in results.items():
         try:
            overallMessage = ""
            rc, out, scriptOutput = result

            if scriptOutput is None:
               # Failed to execute or crashed.
               msg = ("Script '%s'%s did not return a valid JSON"
                      " structure. Output: %s" % (scriptName,
                      " with return code %d" % rc if rc is not None else "",
                      out))
               overallMessage += msg + "\n"
               parseResult[scriptName] = overallMessage

               if not scriptFailure:
                  scriptFailure = True
                  failedScriptNames.append(scriptName)
               continue

            info, warnings, errors = _getNotifications(scriptOutput)
            self._addScriptNotificationsToTask(info, warnings, errors,
                                               scriptName, scriptType)

            # Print results in the order of:
            # info notifications/resolutions, warning notifications/resolutions.
            infoMessage, warningMessage, errorMessage, _ = \
               _getMessages(info, warnings, errors, scriptName)

            log.debug("Script '%s' has info: %s", scriptName,
                      infoMessage.rstrip("\n"))
            log.debug("Script '%s' has warnings: %s", scriptName,
                      warningMessage.rstrip("\n"))

            if rc != 0:
               msg = ("Script '%s' has non-zero return code %d. Output: %s"
                      % (scriptName, rc, errorMessage.rstrip("\n")))
               overallMessage += msg + "\n"
               if not scriptFailure:
                  scriptFailure = True
               failedScriptNames.append(scriptName)
            else:
               scriptNameMsg = "Script '%s': " % scriptName
               if infoMessage:
                  infoMessage = "Info - %s" % infoMessage
               if warningMessage:
                  warningMessage = "Warnings - %s" % warningMessage

               overallMessage = infoMessage + warningMessage
               if overallMessage != "":
                  overallMessage = scriptNameMsg + overallMessage

            parseResult[scriptName] = overallMessage
         except Exception as e:
            msg = ("Getting '%s' script result failed with: %s" %
                   (scriptName, str(e)))
            raise generateQuickPatchRemediationError(e, msg, vibs=[vibId])

      # If for each script, overallMessage is empty, meaning that
      # remediationActionStatus is "COMPLIANT", and notifications/
      # remediationActions is empty, the combined string will also be empty.
      # Otherwise, combine all overallMessages to a new string as the output,
      # add an extra new line between two script messages, and skip the empty
      # strings that come from "remediationActionStatus is COMPLIANT" cases.
      msgList = [overallMessage for overallMessage in
                 parseResult.values() if overallMessage != ""]

      # Note that here the combined string is just a raw string. It can be
      # empty, or can end with a newline. Specific format will be given in
      # the merge later.
      if failedScriptNames:
         causeMsg = ("One or more Quick Patch scripts failed: " +
                     ", ".join(failedScriptNames))
         cause = Errors.QuickPatchScriptError(failedScriptNames, causeMsg)

      return "\n".join(msgList), scriptFailure, cause

   def _quickPatchRemediateApplyPublishedOnly(self, qpVibs, imgprofile):
      """Run apply published scripts and apply VIB security policy for the
         incomplete remediation case.
         Parameters:
            * qpVibs     - The set of VIB IDs of all Quick Patch VIBs from
                           the new profile.
            * imgprofile - The ImageProfile instance representing the target
                           set of VIBs for the new image.
      """
      finalResults = []
      try:
         for vibid in qpVibs:
            if vibid not in self.database.vibs:
               msg = "Quick Patch VIB '%s' is not installed." % vibid
               raise Errors.QuickPatchInstallationError(None, [vibid], msg)
            vibXml = imgprofile.vibs[vibid]
            _, _, publishScripts = vibXml.GetQuickPatchScriptsByType()
            rpFiles = (os.path.join(os.path.sep, vibXml.respooldef),)
            AddRPs(rpFiles, None)
            try:
               secPolDir = os.path.join(os.path.sep, vibXml.secpolicydir)
               with SecPolicyTools(tmpPolicyDir=secPolDir).loadTempDoms():
                  results = self._runQuickPatchScripts(publishScripts,
                     Vib.QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PUBLISHED,
                     continueOnFailure=True)
               finalRes, scriptFailure, cause = \
                  self._processQpApplyScriptNotification(vibid, results,
                     Vib.QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PUBLISHED)
               finalResults.append(finalRes)
               if scriptFailure:
                  raise Errors.QuickPatchInstallationError(cause, [vibid],
                     MergeQuickPatchScriptNotification(finalResults,
                     apply=True))
            finally:
               DelRPs(rpFiles=rpFiles)
         applyVibSecPolicy()
      except Exception as e:
         log.error("Unexpected error occurred while attempting to run "
                   "Quick Patch apply-published scripts, or apply VIB security "
                   "policy: %s", str(e))
         raise generateQuickPatchRemediationError(e, str(e), vibs=[vibid],
                                                  isIncomplete=True)
      return MergeQuickPatchScriptNotification(finalResults, apply=True)

   def _checkPartialMModeConflict(self, checkMaintMode):
      """Check for multiple conflicting partial maintenance modes reported by
         Quick Patch scan scripts. Full maintenance mode is required in such a
         case.
         Raises:
            * MaintenanceModeError - If conflicting partial maintenance modes
                                     are found but full maintenance mode is not
                                     enabled.
      """
      if not checkMaintMode:
         log.info('Skipping partial MaintenanceMode conflict check...')
         return

      if not self._qpScanScriptRes:
         log.info('No scan script results, skipping partial MaintenanceMode '
                  'conflict check...')
         return

      pmmName = None
      for vibScriptResults in self._qpScanScriptRes.values():
         for _, _, scriptRes in vibScriptResults.values():
            if scriptRes is None:
               continue
            if (scriptRes.remediationActionStatus !=
                scriptRes.remediationActionStatus.NON_COMPLIANT or
                not scriptRes.maintenanceMode):
               # Only consider non-compliant with PMM.
               continue

            if pmmName is None:
               pmmName = scriptRes.maintenanceMode
            elif scriptRes.maintenanceMode != pmmName:
               log.info("Conflicting partial maintenance mode '%s' and '%s', "
                        "full maintenance mode is required.", pmmName,
                        scriptRes.maintenanceMode)
               if not GetMaintenanceMode():
                  qpVibs = sorted(self._qpScanScriptRes.keys())
                  raise Errors.MaintenanceModeError("MaintenanceMode is "
                     "required to perform Quick Patch remediation of VIB(s): "
                     "%s." % ', '.join(qpVibs))

   def _quickPatchRemediate(self, adds, checkmaintmode, qpAdds):
      """Quick Patch of vib payloads.
      """
      self.liveimage.prepareToRemediate(adds=adds,
         checkmaintmode=checkmaintmode, qpAdds=qpAdds)

      try:
         quickPatchRes = self._quickPatchAddVibs(adds)
         # applyVibSecPolicy() will always run regardless of whether an error is
         # raised by executing apply-published scripts in _quickPatchAddVibs().
         applyVibSecPolicy()
      except (ExecuteCommandError, Errors.QuickPatchInstallationError) as e:
         vibs = None
         cause = None
         msg = "Quick Patch remediation failed: %s" % str(e)
         if isinstance(e, Errors.QuickPatchInstallationError):
            vibs = e.vibs
            cause = e.cause
            if self._qpRebootAdvised:
               msg = e.msg + "\n " + QUICK_PATCH_FAILURE_WARNING
            else:
               msg = e.msg
         raise Errors.QuickPatchInstallationError(cause, vibs, msg)
      finally:
         # Delete state.tgz and extracted files.
         self.liveimage.removeStateBackup()

         # Mark system live installed
         markerfile = os.path.join(self.root, QUICKPATCH_MARKER)
         MarkSystemLiveInstalled(markerfile)
      return quickPatchRes

   def _quickPatchAddVibs(self, adds):
      '''Runs the Quick Patch apply scripts and publishes the
         new mount revision.
      '''
      def _updateEsxVersion(vib):
         """Updates the ESXi version in vsi node /system/version

            Sample /system/version:
            version {
            product:VMware ESXi
            productVersion:8.0.2
            buildVersion:DEBUGbuild-66468419
            buildType:DEBUG
            buildVersionNumeric:66468419
            releaseUpdate:2
            releasePatch:0
            buildDate:Jul 18 2023
            buildTime:06:05:05
            cpu architecture: 1 -> x86_64
            vmkernelBuild:66468419
            vmkcall:InterfaceVersion {
               major:45
               minor:0
            }
            releaseVersionStr:8.0.2-0.0.66468419
            }
         """
         try:
            _, patch, build = vib.version.release.versionstring.split('.')
            version = vsi.get('/system/version')
            version['buildVersion'] = version['buildType'] + 'build-' + build
            version['buildVersionNumeric'] = build
            version['releasePatch'] = patch
            version['releaseVersionStr'] = vib.version.versionstring
            version['vmkernelBuild'] = int(build)

            dateObj = vib.GetReleaseDatetimeObj()
            version['buildDate'] = dateObj.strftime("%b %d %Y")
            version['buildTime'] = dateObj.strftime("%H:%M:%S")

            vsi.set('/system/version', version)
         except Exception as e:
            msg = "Failed to set ESXi version in vsi node: %s" % str(e)
            cause = Errors.QuickPatchEsxVersionUpdateError(msg)
            raise Errors.QuickPatchInstallationError(cause, None, msg)

         log.info("ESXi version in vsi node updated to %s",
                  version['releaseVersionStr'])

      log.debug('Starting to enable Quick Patch VIBs: %s', ', '.join(adds))

      for vibid in adds:
         vib = self.stagedatabase.vibs[vibid]
         log.debug('Quick patching %s-%s', vib.name, vib.version)

         payloadsToMount = self.GetSupportedQuickPatchPayloads(vib)
         _, prepareScripts, publishScripts = vib.GetQuickPatchScriptsByType()

         # We need to create one more ramdisk to hold a copy of the lean
         # payload as they are being mounted twice - once using the staged
         # database and again within the MountRev context.
         sizeMib = 0
         # uncompressedSize will be used to mount the lean payload.
         uncompressedSize = 0
         for payload in vib.payloads:
            if payload.name in payloadsToMount and payload.isquickpatch:
               sizeMib = payload.size // MIB + 1
               # A lean payload is ensured to have the uncompressedsize
               # attribute.
               # Assume here we will not have xz compressed payloads.
               uncompressedSize = payload.uncompressedsize // MIB + 1
               break

         if not sizeMib:
            msg = "Unable to locate the lean payload in the vib %s" % vib.name
            log.error(msg)
            raise Errors.QuickPatchInstallationError(None, [vibid], msg)

         vibstates = self.stagedatabase.profile.vibstates
         payloadInfo = []
         for name, localname in vibstates[vibid].payloads.items():
            if name in payloadsToMount:
               payloadInfo.append((name, localname))

         # If staging happens in scratch, we need to create a ramdisk to
         # temporarily hold a tardisk before it is mounted. This is because
         # tardisk mounting requires the tardisk to be already in a ramdisk.
         tmpMountPath = None
         payloadsToMountLocalnames = [localname for _, localname in payloadInfo]
         try:
            if self.useOsData:
               CreateSingleTardiskRamdisk(payloadsToMountLocalnames,
                  self.stagedatadir, self.liveimage.TMP_TARDISK_NAME,
                  self.liveimage.TMP_TARDISK_DIR)
               tmpMountPath = self.liveimage.TMP_TARDISK_DIR
         except Exception as e:
            msg = ("Unexpected error occurred while creating tardisk mount "
                   "ramdisk: %s" % str(e))
            log.error(msg)
            raise Errors.QuickPatchInstallationError(None, None, msg)

         qpTempExecRamdiskName = self.QP_TEMP_EXEC_RAMDISK_NAME
         qpTempExecRamdiskPath = self.QP_TEMP_EXEC_RAMDISK_PATH

         try:
            createTardiskMountRamdisk(qpTempExecRamdiskName,
                                      qpTempExecRamdiskPath, sizeMib,
                                      uncompressedSize=uncompressedSize)
            srcFile = os.path.join(self.stagedatadir, payloadInfo[0][1])
            destFile = os.path.join(qpTempExecRamdiskPath, payloadInfo[0][1])
            shutil.copy2(srcFile, destFile)
         except Exception as e:
            Ramdisk.RemoveRamdisk(qpTempExecRamdiskName, qpTempExecRamdiskPath)
            log.error("Unexpected error occurred while creating a copy of "
                      "Quick Patch lean payload: %s", str(e))
            raise

         tardiskNames = []
         payloadName = ''
         finalResults = []
         # Only 1 resource-pool file per Quick Patch VIB, but need to make it
         # a tuple to iterate.
         rpFiles = (os.path.join(qpTempExecRamdiskPath, vib.respooldef),)
         try:
            for name, localname in payloadInfo:
               # We only need the script payload here and we can use the one in
               # the copy ramdisk created above.
               if name == payloadsToMount[0]:
                  log.debug('Trying to mount payload [%s]', name)
                  payloadName = name
                  # Mount the payload in the ramdisk created above.
                  Ramdisk.MountTardiskInRamdisk(vibid, name,
                     os.path.join(qpTempExecRamdiskPath, localname),
                     qpTempExecRamdiskName, qpTempExecRamdiskPath)
                  tardiskNames.append(localname)
                  break

            # When scan scripts were executed in StartTransaction(),
            # respooldef and secpolicydir were already checked and used.

            AddRPs(rpFiles, None)
            secPolDir = os.path.join(qpTempExecRamdiskPath,
                                     vib.secpolicydir)
            with SecPolicyTools(tmpPolicyDir=secPolDir).loadTempDoms():
               # Run the apply-prepare scripts. Stop at first failure.
               results = self._runQuickPatchScripts(prepareScripts,
                           Vib.QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PREPARE,
                           prefix=qpTempExecRamdiskPath)

            # Process the results from apply-prepare scripts to be used by
            # esxcli and vLCM workflows.
            finalRes, scriptFailure, cause = \
               self._processQpApplyScriptNotification(vibid, results,
                  Vib.QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PREPARE)
            finalResults.append(finalRes)

            if scriptFailure:
               raise Errors.QuickPatchInstallationError(cause, [vibid],
                  MergeQuickPatchScriptNotification(finalResults, apply=True))
         except Exception as e:
            msg = ("Unexpected error occurred while attempting to mount the "
                   "payload %s, or run Quick Patch apply-prepare scripts: %s"
                   % (payloadName, str(e)))
            log.error(msg)
            raise generateQuickPatchRemediationError(e, msg)
         finally:
            # Delete the resource pools
            DelRPs(rpFiles=rpFiles)

            # Unmount the tardisks
            for tardiskName in tardiskNames:
               Ramdisk.UnmountManualTardisk(tardiskName, raiseException=False)

            # Delete the ramdisks
            Ramdisk.RemoveRamdisk(qpTempExecRamdiskName, qpTempExecRamdiskPath)

         # Run the apply-prepare scripts and publish the new mount revision.
         log.info("Attempting to create a new MountRev")

         # INFO: MountRev uses Data classes which is available only in
         # Python 3.7. ESXi 6.7 runs on Python 3.5. Importing MountRev
         # locally to avoid import error when upgrading from 6.7 to 8.x.
         from ..Utils.MountRev import MountRev
         rpFiles = (os.path.join(os.path.sep, vib.respooldef),)

         # A boolean to indicate whether it is an incomplete remediation, which
         # is False by default.
         isIncompleteRemediation = False
         try:
            with MountRev.transact():
               # Secure mount the tardisks
               try:
                  for name, localname in payloadInfo:
                     if name in payloadsToMount:
                        log.debug('Trying to mount payload [%s]', name)
                        if tmpMountPath:
                           self.liveimage._MoveAndMountTardiskSecure(vibid,
                              name, localname, tmpMountPath)
                        else:
                           self.liveimage._MountTardiskSecure(vibid, name,
                                                              localname)
               except Exception as e:
                  msg = "Failed to mount the payload %s: %s" % (name, str(e))
                  log.error(msg)
                  raise Errors.QuickPatchInstallationError(e, None, msg)

            log.info("New mount revision published.")

            # Any failure from here on *may* require a reboot to bring the
            # system back to good state.
            self._qpRebootAdvised = True

            if vib.name == "esx-base":
               # Update the vsi node with the new version of ESXi
               _updateEsxVersion(vib)

            # Save Quick Patch image database before executing apply-published
            # scripts. In case of staging to OSData, since we already created
            # the ramdisk TMP_TARDISK_DIR, we do not need to re-create while
            # updating the database.
            self.updateDB(createMountRamdisk=not self.useOsData)

            # Till now, apply-prepare scripts are executed, vsi node and image
            # database have been updated successfully. We set this boolean to
            # True here. If apply-published scripts are executed successfully
            # later, we will set it back to False.
            isIncompleteRemediation = True

            AddRPs(rpFiles, None)
            # Run the apply-published scripts. Run all the scripts but
            # report a failure if any of them fails.
            secPolDir = os.path.join(os.path.sep, vib.secpolicydir)
            with SecPolicyTools(tmpPolicyDir=secPolDir).loadTempDoms():
               # Hold the result of each apply-published script in a dict.
               results = self._runQuickPatchScripts(publishScripts,
                           Vib.QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PUBLISHED,
                           continueOnFailure=True)

            # Process the results from apply-published scripts to be used by
            # esxcli and vLCM workflows.
            finalRes, scriptFailure, cause = \
               self._processQpApplyScriptNotification(vibid, results,
                  Vib.QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PUBLISHED)
            finalResults.append(finalRes)

            if scriptFailure:
               raise Errors.QuickPatchInstallationError(cause, [vibid],
                  MergeQuickPatchScriptNotification(finalResults, apply=True))

            # Set back to False since remediation completed successfully.
            isIncompleteRemediation = False
         except Exception as e:
            log.error("Unexpected error occurred while attempting to run "
                      "Quick Patch apply-published scripts: %s", str(e))

            err = generateQuickPatchRemediationError(e, str(e), vibs=[vibid],
                     isIncomplete=isIncompleteRemediation)
            # If an error happened during an incomplete remediation, use a
            # special incomplete error as the cause. Save the error now to be
            # raised later after iterating all other installers.
            if isIncompleteRemediation:
               self.qpIncompleteRemediateError = err
               return str(e)
            raise err
         finally:
            # Delete the RPs created for running the Quick Patch scripts.
            DelRPs(rpFiles=rpFiles)

            # Delete the temporary ramdisk created for securemount purpose.
            if tmpMountPath:
               Ramdisk.RemoveRamdisk(self.liveimage.TMP_TARDISK_NAME,
                                     self.liveimage.TMP_TARDISK_DIR,
                                     raiseException=True)

      log.debug('Quick Patch of VIBs is done.')
      return MergeQuickPatchScriptNotification(finalResults, apply=True)

   def Remediate(self, checkmaintmode=True, imgprofile=None, **kwargs):
      """Quick patch apply
         For each Quick Patch payload, its apply-prepare and apply-published
         scripts are run and a new MountRevision published.
         Parameters:
            * checkmaintmode - Whether to check the maintenance mode.
            * imgprofile     - The ImageProfile instance representing the
                               target set of VIBs for the new image. It will
                               only be used by the incomplete remediation case
                               as the image profile is not staged. Otherwise,
                               we will use staged* attributes in self.liveimage.
         Returns:
            A RemediationResult instance with all its attributes:
               * A boolean which is always False as a reboot is not needed
               * The result string from Quick Patch remediation.
         Exceptions:
            QuickPatchInstallationError
            HostNotChanged - if there is no staged image profile, or Quick
                             Patch policy is not enabled.
      """
      if not self.enableQuickPatch:
         msg = 'Quick Patch policy is not enabled, skip remediation.'
         raise Errors.HostNotChanged(msg)
      if self.problems:
         msg = 'Quick patch is not supported or has been disabled, ' \
               'skip remediation.'
         raise Errors.HostNotChanged(msg)
      if not self.isstaged:
         if self.qpActionOnlyVibs is None:
            msg = 'Quick patch is not yet staged, nothing to remediate.'
            raise Errors.HostNotChanged(msg)

         self._checkPartialMModeConflict(checkmaintmode)

         # qpActionOnlyVibs not None means that Quick Patch scan detects
         # an incomplete remediation from previous apply. In that case,
         # we still need to run apply-published scripts to know whether
         # there are any pending actions from apply results.
         res = self._quickPatchRemediateApplyPublishedOnly(
                        self.qpActionOnlyVibs, imgprofile)
         return RemediationResult(False, res)

      adds, _, _ = \
         self.GetImageProfileVibDiff(self.stagedimageprofile)

      self._checkPartialMModeConflict(checkmaintmode)

      res = self._quickPatchRemediate(adds, checkmaintmode, self.qpAdds)
      return RemediationResult(False, res)

   def PostRemediationCheck(self):
      """Check after remediation.
         For QuickPatchInstaller, if "qpIncompleteRemediateError" attribute is
         not None, we need to raise this error, otherwise a no-op.
      """
      if self.qpIncompleteRemediateError is not None:
         raise self.qpIncompleteRemediateError

   def _runQpScanScriptsAndProcessResults(self, vibXml, pathPrefix, rpFiles,
                                          continueOnFailure):
      """Core logic to run Quick Patch scripts and process results. Note that
         we must have Quick Patch VIBs installed/downloaded and mounted first.
         Parameters:
            * vibXml - a Quick Patch VIB metadata object.
            * pathPrefix - The prefix of the path for the resource pool and
                           Quick Patch scripts. For a Quick Patch VIB that is
                           already installed, pathPrefix is root; otherwise
                           if downloading and mounting is needed, pathPrefix is
                           the ramdisk path.
            * rpFiles - Paths to RP YAML definition files.
            * continueOnFailure - whether to continue to execute all scripts on
                                  failure; used in _runQuickPatchScripts().
         Returns:
            A tuple of the return from processQpScanScriptNotification()
            and the return from _runQuickPatchScripts().
         Raises:
            * InstallationError: When adding rps, rpFiles and initFiles are
                                 both missing, or failed to load/create rps.
            * QuickPatchInstallationError: Unknown script type before executing
                                          a Quick Patch script, or failed to
                                          process script results.
      """
      finalResults = ""
      AddRPs(rpFiles, None)
      try:
         # Run the scan scripts
         scanScripts, _, _ = vibXml.GetQuickPatchScriptsByType()
         scriptResults = self._runQuickPatchScripts(scanScripts,
               Vib.QuickPatchScript.QP_SCRIPT_TYPE_SCAN,
               prefix=pathPrefix, continueOnFailure=continueOnFailure)
         finalResults = processQpScanScriptNotification(vibXml.id,
                           scriptResults, continueOnFailure=continueOnFailure)
      finally:
         # Best effort cleanup
         DelRPs(rpFiles=rpFiles)
      return finalResults, scriptResults

   def _downloadPartialVib(self, vibXml, downloadPath, payloadsToDownload,
                           checkAcceptance):
      """Downloads a VIB partially based on VIB metadata. Instead of
         downloading the full VIB, it only downloads the quickpatch payloads
         specified in the payloadsToDownload parameter. The downloaded VIB will
         contain the descriptor, signature, and the specified quickpatch
         payloads.
         Parameters:
         * vibXml - VIB metadata instance that does not contain payload
                    objects.
         * downloadPath - the path where the VIB will be downloaded to
         * payloadsToDownload - The list of quickpatch payload names that need
                                to be downloaded.
         * checkAcceptance - If True, VIB acceptance levels will be validated.
      """

      vib = None
      log.info('Attempting to download VIB %s partially with Quick Patch '
               'payloads.', vibXml.name)
      Downloader.Downloader.setEsxupdateFirewallRule('true')
      try:
         for url in vibXml.remotelocations:
            try:
               partialDownloader = PartialVibDownloader.PartialVibDownloader(
                                      url, downloadPath, payloadsToDownload)
               vib = partialDownloader.GetPartialVib()
               break
            except Exception as e:
               log.warning('Unable to download from %s, error [%s]. Trying '
                           'next url...', url, str(e))
               # remove downloaded data if there is any
               if os.path.isfile(downloadPath):
                  os.unlink(downloadPath)
               continue
      finally:
         Downloader.Downloader.setEsxupdateFirewallRule('false')

      if not vib:
         urls = ', '.join(vibXml.remotelocations)
         raise Errors.VibDownloadError(urls, downloadPath,
                  "Unable to download VIB from any of the URLs %s" % urls)

      # Verify descriptor matches metadata:
      try:
         vib.MergeVib(vibXml)
         if checkAcceptance:
            vib.VerifyAcceptanceLevel()
      except Exception as e:
         vib.Close()
         raise Errors.InstallationError(e, [vib.id], str(e))

      return vib

   def _runScanScripts(self, newProfile, checkAcceptance, qpVibs,
                       continueOnFailure):
      """Runs the compliance scan scripts for Quick Patch.
            * Mounts the necessary payloads.
            * Runs the compliance scan scripts.
            * Collects the results of all scan scripts for higher layers to use.
            * Unmounts the tardisks before returning.
         Parameters:
            * newProfile - The ImageProfile instance for the new image.
            * checkAcceptance - If True, VIB acceptance levels will be
                                validated.
            * qpVibs - IDs of Quick Patch VIBs that have Quick Patch scripts
                       to execute.
            * continueOnFailure - See _runQpScanScriptsAndProcessResults().
         Returns:
            A tuple of:
               * A processed final string to display: a multi-line string
                 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".
               * 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.
      """

      # Both VIB download and mounting use the same ramdisk.
      qpStoreMountRamdiskName = self.QP_TEMP_EXEC_RAMDISK_NAME
      qpStoreMountRamdiskPath = self.QP_TEMP_EXEC_RAMDISK_PATH

      # List of final result strings from Quick Patch VIBs.
      finalResults = []
      # Map from VIB ID -> script name -> Apply/ScanScriptOutput instance.
      scriptResults = {}

      try:
         for vibid in qpVibs:
            vibXml = newProfile.vibs[vibid]
            # If the Quick Patch VIB is already installed, we do not need to
            # download again or do mount related stuff.
            needsDownloadAndMount = vibid not in self.database.vibs

            # Check for resource pool file and security policy dir.
            if not vibXml.respooldef:
               raise Errors.ResourcePoolFileNotFound(vibid, "Quick Patch "
                  "VIB '%s' is missing resource pool definition." % vibid)
            if not vibXml.secpolicydir:
               msg = ("Quick Patch VIB '%s' does not have a security "
                      "policy directory." % vibid)
               raise Errors.SecurityPolicyNotFound(vibid, msg)

            # Although we only have one resource pool definition for a VIB,
            # rpFiles needs to be iterable when loading, thus make it a tuple.
            if needsDownloadAndMount:
               rpFiles = (os.path.join(qpStoreMountRamdiskPath,
                                       vibXml.respooldef),)
               secPolDir = os.path.join(qpStoreMountRamdiskPath,
                                        vibXml.secpolicydir)
            else:
               rpFiles = (os.path.join(os.path.sep, vibXml.respooldef),)
               secPolDir = os.path.join(os.path.sep, vibXml.secpolicydir)

            # This path is for any check compliance after a complete or
            # incomplete remediation.
            if not needsDownloadAndMount:
               with SecPolicyTools(tmpPolicyDir=secPolDir).loadTempDoms():
                  finalRes, scriptRes = \
                     self._runQpScanScriptsAndProcessResults(
                        vibXml, '/', rpFiles, continueOnFailure)
               finalResults.append(finalRes)
               scriptResults[vibid] = scriptRes
               continue

            # We only mount and download the script payload.
            scanPayloads = self.GetSupportedQuickPatchPayloads(
                              vibXml, scriptsOnly=True)

            totalSize = 0
            # uncompressedSize is the total uncompressed size of all payloads
            # to mount of current VIB. These payloads are all Quick Patch
            # related (we get them from GetSupportedQuickPatchPayloads), and
            # all of them are ensured to have the uncompressedsize attribute.
            uncompressedSize = 0
            for payload in vibXml.payloads:
               if payload.name in scanPayloads:
                  totalSize += payload.size // MIB + 1
                  # Assume here we will not have xz compressed payloads.
                  uncompressedSize += payload.uncompressedsize // MIB + 1

            try:
               try:
                  # Total size used to create the ramdisk will be the sum of
                  # uncompressedSize and storageSize plus one, since we need
                  # both storage(for downloading) and extraction(for mounting).
                  createTardiskMountRamdisk(qpStoreMountRamdiskName,
                                            qpStoreMountRamdiskPath, totalSize,
                                            uncompressedSize=uncompressedSize,
                                            storageSize=totalSize * 2)
               except Exception as e:
                  log.error("Unexpected error occurred while creating "
                            "ramdisk: %s", str(e))
                  raise

               downloadPath = os.path.join(qpStoreMountRamdiskPath,
                                           vibXml.id + '.vib')
               vib = self._downloadPartialVib(vibXml,
                                              downloadPath,
                                              scanPayloads,
                                              checkAcceptance)

               tardiskNames = []
               try:
                  # Skip patch payloads if not downloaded.
                  for payload, fileObj in vib.IterPayloads():
                     if payload.name in scanPayloads:
                        destfPath = os.path.join(qpStoreMountRamdiskPath,
                                                 payload.name)
                        with open(destfPath, 'wb') as destfObj:
                           Vib.copyPayloadFileObj(payload, fileObj, destfObj,
                              decompress=True)
                        log.info('Trying to mount payload "%s"', payload.name)
                        Ramdisk.MountTardiskInRamdisk(downloadPath,
                           payload.name, destfPath, qpStoreMountRamdiskName,
                           qpStoreMountRamdiskPath)
                        tardiskNames.append(payload.name)

                  with SecPolicyTools(tmpPolicyDir=secPolDir).loadTempDoms():
                     finalRes, scriptRes = \
                        self._runQpScanScriptsAndProcessResults(
                           vibXml, qpStoreMountRamdiskPath, rpFiles,
                           continueOnFailure)
                  finalResults.append(finalRes)
                  scriptResults[vibid] = scriptRes
               finally:
                  for tardiskName in tardiskNames:
                     Ramdisk.UnmountManualTardisk(tardiskName,
                        raiseException=False)
                  vib.Close()
            finally:
               Ramdisk.RemoveRamdisk(qpStoreMountRamdiskName,
                                     qpStoreMountRamdiskPath)
      except Exception as e:
         log.exception("Unexpected error occurred while attempting to run "
                       "Quick Patch scan scripts: %s", str(e))
         if isinstance(e, Errors.InstallationError):
            raise
         raise Errors.QuickPatchInstallationError(e, [vibid], str(e))

      return MergeQuickPatchScriptNotification(finalResults), scriptResults

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

         Parameters:
            * imgprofile  - The ImageProfile instance representing the
                            target set of VIBs for the new image
            * imgstate    - The state of current HostImage, one of IMGSTATE_*
            * forcebootbank - Boolean, if True, skip install of live image
                              even if its eligible for live install
            * stageonly - Boolean, if True, only stages the contents, i.e.
                           changes are made only to stageliveimage
            * preparedest - Boolean, if True, then prepare the destination.
                            Set to false for a "dry run", to avoid changing
                            the destination.
            * checkAcceptance - If True, VIB acceptance levels will be
                                validated.
      """
      def formProblemMessage(problems):
         msg = ('Quick Patch is not supported for this operation due to '
                'the following issues:\n')
         for p in problems:
            msg += '- {}\n'.format(p)
         msg += ('Please use "esxcli software profile update" command '
                 'to perform this operation')
         return msg

      # Store the flag for _CheckTransaction().
      self.stageOnly = stageonly

      unsupportedRes = StartTransactionResult(None, None, False)

      # Quick Patch will be unsupported if enableQuickPatch is False.
      if not self.enableQuickPatch:
         return unsupportedRes

      # TPM check. When enforceQuickPatch=false, proceed to run checks and
      # scripts; the caller must execute the check.
      if IsTpmActive() and self.enforceQuickPatch:
         msg = ("Quick Patch is currently unsupported on a host with TPM "
                "enabled.")
         cause = Errors.QuickPatchTpmUnsupportedError(msg)
         raise Errors.QuickPatchInstallationError(cause, None, msg)

      self.VerifyPrerequisites(imgstate=imgstate, forcebootbank=forcebootbank)
      if self.problems:
         if self.enforceQuickPatch:
            raise Errors.QuickPatchUnsupportedError(
                                    formProblemMessage(self.problems))
         else:
            return unsupportedRes

      imgprofile = self.GetInstallerImageProfile(imgprofile)
      adds, removes, keeps = self.GetImageProfileVibDiff(imgprofile)

      # If there are VIBs on the host that are being Quick Patched, do not
      # consider them to be a removal. Adjust the "removes" set accordingly.
      # Add them to "keeps" to correctly detect overlays. See
      # getQuickPatchParamsFromProfile() for more info.

      quickPatchRes, quickPatchScriptRes = None, None
      # When runQpScanScripts is True, need to run all scan scripts even if
      # qpAdds may be empty.
      qpAdds, qpRemoves, runQpScanScripts = \
         self.getQuickPatchParamsFromProfile(self.database.profile, imgprofile)

      # Only modify removes/keeps/qpAdds when qpAdds and qpRemoves exist,
      # i.e., new profile can Quick Patch from current profile.
      # adds will keep unchanged, since we need the logic later in
      # VerifyTransaction to detect overlay.
      if qpAdds:
         self.qpAdds.update(qpAdds)

      if qpRemoves:
         removesCp = removes.copy()
         for qpRemove in qpRemoves:
            if qpRemove in removesCp:
               removes.remove(qpRemove)
               keeps.add(qpRemove)

      if runQpScanScripts:
         # Run scan scripts when curProfile can Quick Patch to newProfile or
         # they share the same baseimage version.
         # Get all Quick Patch VIBs to run scan scripts, not just the VIBs to
         # install (adds from newProfile.Diff(curProfile))
         qpVibs = set(vib.id for vib in imgprofile.vibs.values() if
                      vib.isQuickPatchVib)

         # If preparedest is False, it means we are either doing a dry-run or
         # running vLCM scan, we must get the results from all scan scripts
         # despite a failure.
         quickPatchRes, quickPatchScriptRes = \
            self._runScanScripts(imgprofile, checkAcceptance, qpVibs,
                                 not preparedest)
         self._qpScanScriptRes = quickPatchScriptRes
      else:
         # a) New profile cannot Quick Patch from current profile OR
         #
         # b) There is no action to be taken by the QuickPatchInstaller.
         # For eg: The Quick Patch policy is enforced but the source and the
         # target image profiles, say, both are 8.0U3 GA, are not Quick Patch
         # profiles i.e., they do not contain any quick patch vibs.
         #
         # In either case, no scan script will be executed. There may be other
         # vibs/components that need remediation via a different installer. So,
         # if there isn't a bootbank vib that needs remediation and Quick Patch
         # is not applicable/no action, return unsupported. Throw an exception
         # otherwise.
         if self.enforceQuickPatch:
            msg = ('Quick Patch is not supported because no Quick Patch VIBs '
                   'were found')
            if (adds or removes) and not qpAdds:
               raise Errors.QuickPatchUnsupportedError(msg)
         return unsupportedRes

      staged = self.isImgProfileStaged(imgprofile)
      res = StartTransactionResult(adds, removes, staged,
                                   quickPatchResult=quickPatchRes,
                                   quickPatchScriptResults=quickPatchScriptRes)

      # qpActionOnlyVibs is set only for the incomplete remediation case
      # where the image is not staged yet. It will be used in remediation.
      if not staged and res.isQuickPatchActionOnly:
         self.qpActionOnlyVibs = qpVibs
      if staged and quickPatchRes:
         return res

      haveProblems = self.VerifyTransaction(imgprofile, adds, removes, keeps)
      if haveProblems:
         if self.enforceQuickPatch:
            raise Errors.QuickPatchUnsupportedError(
                                    formProblemMessage(self.problems))
         else:
            return unsupportedRes

      imgsize = self.GetInstallationSize(imgprofile)
      qpPayloadSize = self.GetQuickPatchPayloadSize(imgprofile)
      if preparedest and adds:
         self.liveimage.StartTransaction(imgprofile, imgsize,
                                         stageonly=stageonly,
                                         qpPayloadSize=qpPayloadSize)

      return res

   def _CheckTransaction(self, imageprofile, adds, removes, keeps):
      """Check the transaction to see if there are any logical reasons that
         prevent Quick Patch installation of the transaction.
            * No VIB to be Quick Patched can have a file that is overlaid by
              an existing VIB or another new VIB, except the test certs.
         Parameter:
            * imageprofile - The ImageProfile instance for the new image.
            * adds/removes/keeps - A set of vibIds of vibs to add/remove/keep.
         Returns:
            * problems - A list of problem strings, each of which represents
                         a failure, i.e., a VIB cannot be Quick Patched due to
                         unsupported file operation.
      """
      # FileState groups for checking overlay problems.
      groups = ((keeps, self.database.vibs, (FileState.keepreg,
                                             FileState.keepoverlay)),
                (adds, imageprofile.vibs, (FileState.addreg,
                                           FileState.addoverlay)),
                (removes, self.database.vibs, (FileState.removereg,
                                               FileState.removeoverlay)))

      # Unsupported overlay for Quick Patch.
      # If a VIB is Quick Patched to a new version, its files will appear in
      # both keeps and adds, see StartTransaction().
      unsupported = {
         # New files from Quick Patch cannot be overlaid. Otherwise, either the
         # old overlay VIB's file will be overlaid by Quick Patch, or Quick
         # Patch would not have the intended effect (the overlay version is
         # instead effective).
         FileState.addreg | FileState.keepoverlay:
            "File to be installed is overlaid by an existing VIB",
         FileState.addreg | FileState.addoverlay:
            "File to be installed is overlaid by a new VIB",
         FileState.addreg | FileState.removeoverlay | FileState.addoverlay:
            "File to be installed is overlaid by an upgrading VIB",
         # Similarly, an updated file in Quick Patch VIB cannot be overlaid.
         FileState.keepreg | FileState.addreg | FileState.keepoverlay:
            "File to be updated is overlaid by an existing VIB",
         FileState.keepreg | FileState.addreg | FileState.addoverlay:
            "File to be updated is overlaid by a new VIB",
         FileState.keepreg | FileState.addreg | FileState.removeoverlay | \
            FileState.addoverlay:
            "File to be updated is overlaid by an upgrading VIB",
         # Removal cases that would cause a reboot in LiveImageInstaller. While
         # QuickPatchInstaller does not handle removal, we want to skip adding
         # live removal problems later if an overlay scenario is reboot
         # required.
         FileState.keepoverlay | FileState.removereg:
            "File to be removed is overlaid by an existing VIB",
         FileState.keepoverlay | FileState.removereg | FileState.addreg:
            "File to be updated is overlaid by an existing VIB",
         FileState.keepreg | FileState.removeoverlay:
            "File to be removed overlays an existing VIB",
         FileState.keepreg | FileState.removeoverlay | FileState.addoverlay:
            "File to be updated overlays an existing VIB",
      }


      # Overlay exceptions: particular files to their allowed operations.
      # Note: the overlay detection logic does not distinguish whether a file
      # is to be Quick Patched (in a paylaoad with overlay-order) or just to
      # be carried forward. These exceptions will cause problems if an
      # overlaid file is ever Quick Patched: remediation will reverse the
      # overlay and the version in the Quick Patch VIB will become effective;
      # then, on a reboot the overlay version will be restored.
      allowedOverlays = (
         {
            # Before Quick Patch, there can be test-cert installed. As long as
            # the test-cert VIB on the host is being kept and overlays existing
            # cert file (in both current and Quick Patch VIBs), it will not be
            # flagged as unsupported.
            '/usr/share/certs/vmpartner.cert',
            '/usr/share/certs/vmpartner.crl',
            '/usr/share/certs/vmware.cert',
            # gc VIB has an overlay copy of initSystemStorage.
            '/bin/initSystemStorage',
         },
         (
            FileState.keepreg | FileState.addreg | FileState.keepoverlay,
         )
      )
      problems = self._detectOverlayProblems(groups, unsupported,
                                             allowedOverlays=allowedOverlays)

      # Identify the files that cause overlay issues.
      overlayProblemFiles = {p.split(' : ')[-1] for p in problems}

      # Check Quick Patch staging/remediation will be performed. Regular live
      # or reboot remediation is not supported, i.e. any adds/removes not in
      # qpAdds/qpRemoves will cause a problem to be added.

      # In vLCM, enforceQuickPatch is set to false for check compliance
      # to not get an exception when Quick Patch is not possible. To distinguish
      # reboot required and no Quick Patch VIB scenarios from live VIB install/
      # remove scenarios, use the flag to not add live install/remove problems,
      # and let check compliance determine the scenario separately.
      # However, for staging, even though enforceQuickPatch is also false,
      # live VIB install/remove problems still need to be returned for the
      # installer to not run.
      addLiveProblems = self.enforceQuickPatch or self.stageOnly

      # Remove qpAdds from adds; qpRemoves should have already been deducted
      # from removes.
      adds = adds - self.qpAdds
      if adds or removes:
         for vibId in adds:
            vib = imageprofile.vibs[vibId]
            if vib.liveinstallok:
               if (overlayProblemFiles and
                   set(vib.filelist) & overlayProblemFiles):
                  # For a file being installed that has an overlay problem, the
                  # installation will likely not be live even in
                  # LiveImageInstaller. Skip adding a live install problem.
                  continue

               prob = ('VIB %s requires non-Quick Patch live remediation to '
                       'install.' % vibId)
               if addLiveProblems:
                  problems.append(prob)
               else:
                  log.debug('Skipping problem for vLCM check compliance: %s',
                            prob)
            else:
               problems.append('VIB %s requires reboot to install.' % vibId)

         for vibId in removes:
            vib = self.database.vibs[vibId]
            if vib.liveremoveok:
               if (overlayProblemFiles and
                   set(vib.filelist) & overlayProblemFiles):
                  # For a file being removed that has an overlay problem, the
                  # removal will likely not be live even in LiveImageInstaller.
                  # Skip adding a live remove problem.
                  continue

               prob = ('VIB %s requires non-Quick Patch live remediation to '
                       'remove.' % vibId)
               if addLiveProblems:
                  problems.append(prob)
               else:
                  log.debug('Skipping problem for vLCM check compliance: %s',
                            prob)
            else:
               problems.append('VIB %s requires reboot to remove.' % vibId)

      return problems
