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

"""Scanner
Implementation of host scan against the desired software spec.
"""

import json
import logging
import os
import sys
import traceback

from datetime import datetime

from com.vmware.esx.settings_daemon_client \
   import (AddOnCompliance, AddOnDetails, AddOnInfo, BaseImageCompliance,
           BaseImageDetails, BaseImageInfo, ComplianceImpact, ComplianceStatus,
           ComponentCompliance, ComponentDetails, ComponentInfo,
           ComponentSource, HardwareSupportPackageCompliance,
           HardwareSupportPackageInfo, HostCompliance, Notifications,
           SolutionCompliance, SolutionDetails, SolutionComponentDetails,
           SolutionComponentSpec, SolutionInfo)
from com.vmware.vapi.std_client import LocalizableMessage
from vmware.vapi.bindings.converter import TypeConverter
from vmware.vapi.data.serializers.jsonrpc import VAPIJsonEncoder

from .Constants import *
from .DepotMgr import DepotMgr
from .SoftwareSpecMgr \
   import (RESOLUTION_TYPE_ADDON, RESOLUTION_TYPE_BASEIMAGE,
           RESOLUTION_TYPE_MANIFEST, RESOLUTION_TYPE_SOLUTION,
           RESOLUTION_TYPE_USERCOMP, SOURCE_TYPE_MANIFEST, SoftwareSpecMgr)
from .Utils import (getCommaSepArg, getExceptionNotification,
                    getFormattedMessage, getNotification)

from .. import (ALLOW_DPU_OPERATION, IS_ESXIO, LIVEUPDATE_ENABLED, MIB,
   PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED, PYTHON_VER_STR,
   QUICKPATCH_STAGE1_ENABLED, QUICKPATCH_STAGE2_ENABLED)

from ..BaseImage import versionSpecListToDictOrStr
from ..Bulletin import ComponentCollection
from ..Errors import InstallerNotAppropriate, VibFormatError
from ..HostImage import HostImage
from ..Utils.HostInfo import getMaxMemAllocation, HostOSIsSimulator, IsTpmActive
from ..Version import VibVersion
from ..Vib import GetHostSoftwarePlatform, QuickPatchScript, SoftwarePlatform
from ..VibCollection import VibCollection

SCAN_TASK_ID = 'com.vmware.esx.settingsdaemon.software.scan'

# Order of component override.
# TODO: unify constant usage in two modules.
_RES_TYPE_TO_ENTITY = {
   RESOLUTION_TYPE_BASEIMAGE: BASE_IMAGE,
   RESOLUTION_TYPE_ADDON: ADDON,
   RESOLUTION_TYPE_MANIFEST: HARDWARE_SUPPORT,
   RESOLUTION_TYPE_USERCOMP: COMPONENT,
   RESOLUTION_TYPE_SOLUTION: SOLUTION,
}
# Removed component type is skipped as it cannot override.
ENTITY_OVERRIDE_ORDER = [_RES_TYPE_TO_ENTITY[s]
   for s in SoftwareSpecMgr.INTENT_RESOLUTION_MAP if s in _RES_TYPE_TO_ENTITY]

isNotNone = lambda x: x is not None

# Adapt an object or a list to an optional value where None is
# written when unset/empty.
getOptionalVal = lambda x: x if x else None

# Create compliance status related maps.
complianceStatus = [COMPLIANT, NON_COMPLIANT, INCOMPATIBLE, UNAVAILABLE]
complianceStatusToValue = {complianceStatus[i]: i
                           for i in range(len(complianceStatus))}
valueToComplianceStatus = {v:k for k, v in complianceStatusToValue.items()}

# Create impact related maps.
impacts = [IMPACT_NONE, IMPACT_PARTIAL_MMODE, IMPACT_MMODE, IMPACT_REBOOT,
           IMPACT_UNKNOWN]
impactToValue = {impacts[i]: i for i in range(len(impacts))}
valueToImpact = {v:k for k, v in impactToValue.items()}
impactToID = {IMPACT_PARTIAL_MMODE: PARTIAL_MAINTMODE_IMPACT_ID,
              IMPACT_MMODE: MAINTMODE_IMPACT_ID,
              IMPACT_REBOOT: REBOOT_IMPACT_ID}

# Whether task.py supports notification: this is for patch-the-patcher case.
taskHasNotification = lambda task: hasattr(task, 'updateNotifications')

# Locker components to be ignored while calculating staging status.
STAGE_BLACKLIST = ['VMware-VM-Tools']

def _addPythonPaths():
   """Add paths for Python libs in the patcher that are requried by scan.
   """
   importPaths = []
   # This script is in:
   # lib64/python<ver>/site-packages/vmware/esximage/ImageManager/
   modulePath = os.path.dirname(os.path.abspath(__file__))
   lib64Path = os.path.normpath(
                  os.path.join(modulePath, '..', '..', '..', '..', '..'))
   patcherRoot = os.path.normpath(os.path.join(lib64Path, '..'))

   # loadesxLive for QuickBoot precheck:
   # lib64/python<ver>/site-packages/loadesxLive, <ver> changes according to
   # the Python intepreter used as loadesx compiles pyc for supported versions.
   loadEsxSitePkgPath = os.path.normpath(
                           os.path.join(lib64Path, PYTHON_VER_STR,
                                        'site-packages'))
   importPaths.append(loadEsxSitePkgPath)

   # Weasel for hardware precheck:
   # usr/lib/vmware/weasel/
   usrLibVmwarePath = os.path.join(patcherRoot, 'usr', 'lib', 'vmware')
   importPaths.append(usrLibVmwarePath)

   # Precheck imports from 'esximage' rather than 'vmware.esximage'. For all
   # base image update cases patch the patcher sets it up when the new patcher
   # is mounted, but in other cases we do not have a new patcher and need to
   # make sure 'vmware' path is added.
   #
   # esximage module is in:
   # lib64/python<ver>/site-packages/vmware, <ver> is consistent with the path
   # of this module.
   vmwarePath = os.path.normpath(os.path.join(modulePath, '..', '..'))
   importPaths.append(vmwarePath)

   for path in importPaths:
      assert os.path.exists(path), 'Python lib path %s does not exist' % path
      if path not in sys.path:
         sys.path.insert(0, path)

def _checkAndUpdateStageStatus(compliance, stageStatus):
   """Checks the compliance class has stage_status attribute and updates it if
      it does.
   """
   if hasattr(compliance, 'stage_status'):
      compliance.stage_status = stageStatus

def _checkHostComplianceAttr(attribute):
   """Returns whether an attribute exists in the HostCompliance class in
      the settings_daemon Python binding.
   """
   return hasattr(HostCompliance(), attribute)

def _checkBaseImageDetailsAttr(attribute):
   """Returns whether an attribute exists in the BaseImageDetails class in
      the settings_daemon Python binding.
   """
   return hasattr(BaseImageDetails(), attribute)

def _importNewClassFromSettingsd(attrNames):
   """Imports new structure(s) from the settings_daemon Python binding,
      returns a boolean indicating whether import(s) was successful.
      During an upgrade with patch the patcher, a newly introduced structure
      will not be available due to the Python binding is outside of the patcher.
   """
   import com.vmware.esx.settings_daemon_client as settingsd
   imported = True
   for attrName in attrNames:
      cls = getattr(settingsd, attrName, None)
      if cls is None:
         imported = False
      globals()[attrName] = cls
   return imported

def _checkImpactDetailsAttr(attribute):
   """"Returns whether an attribute exists in the ImpactDetails class in
       settings_daemon Python binding.
   """
   if not _importNewClassFromSettingsd(('ImpactDetails',)):
      return False
   return hasattr(ImpactDetails(), attribute)

STAGING_SUPPORTED = _checkHostComplianceAttr('stage_status')

# Technically, both LiveUpdate and QuickPatch require structures defined under
# InfraPartialMMode. Since no ESXi functionality is under the FSS, for
# simplicity, only check LiveUpdate or QuickPatch FSS respectively.
LIVE_UPDATE_SUPPORTED = (LIVEUPDATE_ENABLED and
                         _checkHostComplianceAttr('impact_details') and
                         _checkImpactDetailsAttr('solution_impacts'))

# Stage2 Quick Patch requires Stage1 for installer functionalities.
QUICK_PATCH_SUPPORTED = (QUICKPATCH_STAGE1_ENABLED and
   QUICKPATCH_STAGE2_ENABLED and
   _checkBaseImageDetailsAttr('quick_patch_compatible_versions') and
   _checkHostComplianceAttr('compliance_status_details') and
   _checkHostComplianceAttr('impact_details') and
   _checkHostComplianceAttr('remediation_details') and
   _checkImpactDetailsAttr('solution_impacts'))


def vapiStructToJson(struct):
   """Convert a VAPI struct class object to JSON RPC.
   """
   dataValue = TypeConverter.convert_to_vapi(struct,
                                             type(struct).get_binding_type())

   return json.dumps(dataValue,
                     check_circular=False,
                     separators=(',', ':'),
                     cls=VAPIJsonEncoder)

def getBaseImageInfo(displayName, version, displayVersion, releaseDate,
                     quickPatchCompatibleVersions):
   """Get base image info from given parameters.
         Parameters:
            * displayName    - display name of the base image
            * version        - full version of the base image
            * displayVersion - human readable version of the base image
            * releaseDate    - release date of the base image
            * quickPatchCompatibleVersions - For base images this base image
                                             can quick patch from, map their
                                             full versions to display versions.
         Return:
            * BaseImageInfo instance that includes version and base image
              details formed with pass-in parameters
   """
   if QUICK_PATCH_SUPPORTED:
      details = BaseImageDetails(display_name=displayName,
                   display_version=displayVersion or version,
                   release_date=releaseDate,
                   quick_patch_compatible_versions=
                      versionSpecListToDictOrStr(quickPatchCompatibleVersions))
   else:
      details = BaseImageDetails(display_name=displayName,
                                 display_version=displayVersion or version,
                                 release_date=releaseDate)
   return BaseImageInfo(details=details, version=version)

def getAddOnInfo(name, displayName, version, displayVersion, vendor):
   details = AddOnDetails(display_name=displayName or name,
                          vendor=vendor,
                          display_version=displayVersion or version)
   return AddOnInfo(details=details, name=name, version=version)

def getComponentInfo(displayName, version, displayVersion, vendor):
   details = ComponentDetails(display_name=displayName,
                              vendor=vendor,
                              display_version=displayVersion)
   return ComponentInfo(version=version, details=details)

def getVersionCompliance(desiredVerStr, currentVerStr, entityName, entityType):
   """Compare desired and current version string and return a tuple of
      compliance result and a notification message struct for incompatible
      result.
   """
   desiredVersion = VibVersion.fromstring(desiredVerStr)
   currentVersion = VibVersion.fromstring(currentVerStr)
   if desiredVersion > currentVersion:
      return NON_COMPLIANT, None
   elif desiredVersion < currentVersion:
      notiId = DOWNGRADE_NOTIFICATION_ID[entityType]
      # Base image does not need name in the argument list.
      msgArgs = ([desiredVerStr, currentVerStr] if entityType == BASE_IMAGE
                 else [desiredVerStr, entityName, currentVerStr])
      msgDict = getNotification(notiId, notiId, msgArgs=msgArgs, type_=ERROR)
      return INCOMPATIBLE, msgDict
   else:
      return COMPLIANT, None

def _getQuickPatchIncompatNotifs(hostImg, imgProfile, adds, removes,
                                 checkLiveCompSolution=True):
   """Return one or more notifications for Quick Patch incompatible scenarios:
      * Presence of a managed DPU.
      * Presence of active TPM.
      * A component and/or solution will be modified live (non-Quick Patch)
        on the host. There can be two notifications if both a component and a
        solution are changed. This is by default checked, but can be disabled
        by setting checkLiveCompSolution to False.
      This function should be called only when Quick Patch is otherwise
      possible with the image.
   """
   notifications = []
   if ALLOW_DPU_OPERATION:
      notifications.append(
         getNotification(QP_DPU_UNSUPPORTED_ID, QP_DPU_UNSUPPORTED_ID,
                         type_=ERROR))

   if IsTpmActive():
      notifications.append(
         getNotification(QP_TPM_UNSUPPORTED_ID, QP_TPM_UNSUPPORTED_ID,
                         type_=ERROR))

   if not checkLiveCompSolution:
      return notifications

   liveAdds, liveRemoves = set(), set()
   for vId in adds:
      vib = imgProfile.vibs[vId]
      if (not (vib.isQuickPatchVib or vib.payloadupdateonly) and
          vib.liveinstallok):
         liveAdds.add(vId)

   curProfile = hostImg.GetProfile()
   for vId in removes:
      vib = curProfile.vibs[vId]
      # VIBs replaced by Quick Patch adds have already been excluded.
      if vib.liveremoveok:
         liveRemoves.add(vId)

   if not liveAdds and not liveRemoves:
      return notifications

   rmComps = curProfile.components.GetComponentsFromVibIds(
      liveRemoves, partialComponents=True)
   addComps = imgProfile.components.GetComponentsFromVibIds(
      liveAdds, partialComponents=True)
   allComps = rmComps + addComps

   solNames, compNames = set(), set()
   allSolutions = curProfile.solutions + imgProfile.solutions
   solutionComps = ComponentCollection()
   for solution in allSolutions.values():
      compsDict = solution.MatchComponents(allComps)
      if compsDict:
         solNames.add(solution.nameSpec.name)
         for compName, compList in compsDict.items():
            for comp in compList:
               solutionComps.AddComponent(comp)

   for comp in allComps.IterComponents():
      if solutionComps.HasComponent(compId=comp.id):
         continue
      compNames.add(comp.compNameStr)

   # Live orphan VIB removal. This can be a prior orphan VIB before
   # transitioning into vLCM image. Orphan VIB installation is not possible.
   removedOrphanVibs = liveRemoves & set(curProfile.GetOrphanVibs().keys())

   # Form a notification for changed live component, solution, and orphan VIB
   # respectively.
   if solNames:
      # Solution upgrade/remove may have MMode impact despite the VIBs
      # are liveinstall/removeok. This notification may need to be removed
      # later depending on the final impact.
      notifications.append(
         getNotification(QP_LIVE_SOLUTION_UNSUPPORTED_ID,
                         QP_LIVE_SOLUTION_UNSUPPORTED_ID,
                         msgArgs=[getCommaSepArg(solNames)],
                         type_=ERROR))
   if compNames:
      notifications.append(
         getNotification(QP_LIVE_COMP_UNSUPPORTED_ID,
                         QP_LIVE_COMP_UNSUPPORTED_ID,
                         msgArgs=[getCommaSepArg(compNames)],
                         type_=ERROR))

   if removedOrphanVibs:
      notifications.append(
         getNotification(QP_LIVE_ORPHAN_VIB_UNSUPPORTED_ID,
                         QP_LIVE_ORPHAN_VIB_UNSUPPORTED_ID,
                         msgArgs= [getCommaSepArg(removedOrphanVibs)],
                         type_=ERROR))

   return notifications

def _getLiveImpact(installer, hostImg, imgProfile, enableQuickPatch):
   """Returns the image impact of LiveImageInstaller and a list of notifications
      related to Quick Patch incompatibility if Quick Patch is enabled and live
      remediation will be performed.
      The image impact can be one of the following:
      - IMPACT_NONE: no maintenance mode VIBs.
      - IMPACT_MMODE: maintenance mode is required.
      - None: live install/remove is not possible.
      One or more Quick Patch incompatible notifications will be returned if:
      * Quick Patch is enabled.
      * The impact is live.
      This is due to Quick Patch cannot handle regular live remediation.
      DPU/TPM presence will cause additional notification(s) to be returned.
   """
   res = installer.StartTransaction(imgProfile, hostImg.imgstate,
                                    preparedest=False)
   if res.isUnSupported:
      return None, None

   adds, removes, _ = res.GetCommonAttributes()

   # Check if live VIB install/remove requires maintenance mode.
   mModeRemoves, mModeAdds = \
         installer.liveimage.GetMaintenanceModeVibs(imgProfile.vibs,
                                                    adds,
                                                    removes)
   if mModeRemoves or mModeAdds:
      return IMPACT_MMODE, None

   qpIncompatNotifs = None
   if enableQuickPatch:
      # If impact is live with enableQuickPatch, compliance must be incompatible
      # as a normal live remediation will fail when enforceQuickPatch is true
      # with no Quick Patch VIBs.
      qpIncompatNotifs = _getQuickPatchIncompatNotifs(hostImg, imgProfile, adds,
                                                      removes)
   return IMPACT_NONE, qpIncompatNotifs

def _getQuickPatchImpact(installer, hostImg, imgProfile, enableQuickPatch):
   """Returns a tuple of image impact related values for Quick Patch:
      * Image impact, can be one of the following:
         - IMPACT_NONE: Quick Patch is possible, no non-Quick Patch live,
           partial maintenance mode or maintenance mode VIBs.
         - IMPACT_PARTIAL_MMODE: Quick Patch is possible with PMM.
         - IMPACT_MMODE: multiple conflicting PMM names are found or MMode
                         VIBs are present.
         - IMPACT_REBOOT: pending reboot from previous incomplete Quick Patch
           remediation.
         - None: Quick Patch is not applicable due to either no Quick Patch
           VIB or reboot required VIBs. Or, the image contains Quick Patch
           VIB(s) and is already compliant.
      * Partial maintenance mode name. None if not applicable.
      * The script execution results dictionary from QuickPatchInstaller.
        The dictionary will be None if no Quick Patch scan script was
        executed. None if not applicable.
      * A Quick Patch information notification, currently this can only be
        the Quick Patch eligible notification when Quick Patch is possible.
        None if not applicable.
      * A Quick Patch pending reboot notification, when Quick Patch is
        disabled, but the prior Quick Patch remediation was incomplete. None
        if not applicable.
      * A list of error notifications for Quick Patch incompatibility, when
        Quick Patch is possible but cannot be performed. None if not
        applicable.
   """
   # Can import in Python 3.7+ only.
   from .QuickPatchScriptLib import RemediationActionStatus

   qpNotNeededRes = (None, None, None, None, None, None)

   # In order to get scan script results no matter the outcome of the prechecks,
   # set preparedest to False (same as dryrun=True in localcli)
   res = installer.StartTransaction(imgProfile, hostImg.imgstate,
                                    preparedest=False)

   if res.isUnSupported:
      return qpNotNeededRes

   qpEligibleInfo = getNotification(QP_INFO_ID, QP_INFO_ID)

   adds, removes, _ = res.GetCommonAttributes()
   scriptResults = res.quickPatchScriptResults
   addsCopy = adds.copy()

   # Quick Patch VIBs do not need full maintenance mode, get maintenance mode
   # VIBs from the rest of 'adds' and 'removes'.
   for vId in installer.qpAdds:
      # Quick Patched VIBs were already removed from 'removes' in
      # StartTransaction().
      adds.discard(vId)

   # If Quick Patch is enabled and the image has a Quick Patch VIB, always show
   # DPU/TPM unsupported errors.
   qpIncompatNotifs = None
   if enableQuickPatch:
      qpIncompatNotifs = _getQuickPatchIncompatNotifs(hostImg, imgProfile,
         addsCopy, removes, checkLiveCompSolution=False)

   mModeRemoves, mModeAdds = \
         installer.liveimage.GetMaintenanceModeVibs(imgProfile.vibs,
                                                    adds,
                                                    removes)

   if not scriptResults:
      # No script, tardisk-only update.

      if not enableQuickPatch:
         # Host already on a tardisk-only Quick Patch image with the policy
         # disabled. Fallback to live installer to tell the impact of the rest
         # of the image.
         return qpNotNeededRes

      if bool(addsCopy):
         # Image is non-compliant and requires remediation.
         # Re-check incompatibility and include live component/solution.
         qpIncompatNotifs = _getQuickPatchIncompatNotifs(hostImg, imgProfile,
                                                         addsCopy, removes)

         if qpIncompatNotifs:
            return None, None, None, None, None, qpIncompatNotifs

         logging.debug("No Quick Patch scan script is present. Quick Patch "
                       "will only update the tardisks.")
         return IMPACT_NONE, None, None, qpEligibleInfo, None, None

      # Host already on a tardisk-only Quick Patch image with the policy
      # enabled. Fallback to live installer to tell the impact of the rest
      # of the image.
      return qpNotNeededRes

   impact = IMPACT_NONE
   pmmName = None
   actionPending = False
   unableToQp = False
   for vibScriptResults in scriptResults.values():
      for rc, _, scriptRes in vibScriptResults.values():
         if rc is None or scriptRes is None:
            # Script did not succeed.
            unableToQp = True
            continue

         if scriptRes.remediationActionStatus in (
            RemediationActionStatus.INCOMPATIBLE,
            RemediationActionStatus.UNKNOWN):
            # Script reported Quick Patch is not possible.
            unableToQp = True
            continue

         if (not actionPending and scriptRes.remediationActionStatus ==
             RemediationActionStatus.NON_COMPLIANT):
            actionPending = True

         if scriptRes.maintenanceMode:
            if impact == IMPACT_NONE:
               impact = IMPACT_PARTIAL_MMODE
            if pmmName is None:
               pmmName = scriptRes.maintenanceMode
            elif scriptRes.maintenanceMode != pmmName:
               # Conflicting PMMs.
               logging.info("Conflicting partial maintenance mode '%s' and "
                            "'%s' found, using full maintenance mode.", pmmName,
                            scriptRes.maintenanceMode)
               pmmName = None
               impact = IMPACT_MMODE

   if enableQuickPatch:
      if unableToQp:
         # Failed script or incompatible/unknown compliance, notifications from
         # the scripts will be handled later.
         logging.debug("Quick Patch is not supported due to incompatible or "
                       "unknown scan script compliance.")
         return None, None, scriptResults, None, None, None

      if mModeRemoves or mModeAdds or impact == IMPACT_MMODE:
         # MMode remediation due to VIBs or conflicting PMMs.
         if mModeRemoves or mModeAdds:
            mModeAddStr = ("add VIBs: %s" % getCommaSepArg(mModeAdds)
                           if mModeAdds else "")
            mModeRmStr = ("remove VIBs: %s" % getCommaSepArg(mModeRemoves)
                          if mModeRemoves else "")
            msg = ("Maintenance mode is required to %s%s%s"
                   % (mModeAddStr,
                      ' and ' if mModeAddStr and mModeRmStr else '',
                      mModeRmStr))
            logging.debug(msg)

         # While QuickPatchInstaller does not support mixing Quick Patch and
         # MMode required VIBs, the enforceQuickPatch policy in remediation will
         # block the operation. Scan just needs to report the impact as MMode
         # normally.
         return IMPACT_MMODE, None, scriptResults, None, None, qpIncompatNotifs

      # Re-check incompatibility and include live component/solution.
      qpIncompatNotifs = _getQuickPatchIncompatNotifs(hostImg, imgProfile,
                                                      addsCopy, removes)

      # Quick Patch non-compliant/compliant or incompatible.
      needToRemediate = bool(removes) or bool(addsCopy) or actionPending
      # Add Quick Patch eligible notification if Quick Patch remediation is
      # expected and there is no incompatibility issue.
      qpInfo = (qpEligibleInfo if needToRemediate and not qpIncompatNotifs
                else None)
      return (impact, pmmName, scriptResults, qpInfo, None,
              qpIncompatNotifs or None)

   # Quick Patch disabled, VIBs already installed, but the prior remediation was
   # incomplete. Quick Patch scripts should have information about the
   # non-compliance.
   if actionPending and not addsCopy:
      rebootMsg = getNotification(QP_INCOMPLETE_REBOOT_ID,
                                  QP_INCOMPLETE_REBOOT_ID)
      return IMPACT_REBOOT, None, scriptResults, None, rebootMsg, None

   # Quick Patch image is either compliant or non-compliant. With Quick Patch
   # disabled, fallback to live installer for impact.
   # Quick Patch scripts may have information to show, return script results
   # for notification processing.
   return None, None, scriptResults, None, None, None

class _ImageImpactParameters:
   """A class that captures parameters for image impact.
      Members:
         * impact - the overall impact, one of no impact, partial maintenance
                    mode, maintenance mode or reboot required.
         * partialMModeName - the partial maintenance mode name, a non-empty
                              string when partial maintenance mode impact is
                              returned.
         * quickPatchScriptResults - the Quick Patch scan script result
                                     dictionary; None if Quick Patch is not
                                     applicable.
         * pendingRebootNotification - an information notification regarding
                                       a pending reboot, e.g. reboot is pending
                                       due to a prior image change or an
                                       incomplete Quick Patch remediation.
         * quickPatchInfo - an information notification about Quick Patch,
                            currently can only be the Quick Patch eligible info.
         * quickPatchIncompatNotifs - a list of error notifications about
                                      Quick Patch incompatibility. It can be
                                      one or more of: managed DPU, active TPM,
                                      live component remediation, and live
                                      solution remediation.
   """
   def __init__(self, impact, partialMModeName=None,
                quickPatchScriptResults=None, pendingRebootNotification=None,
                quickPatchInfo=None, quickPatchIncompatNotifs=None):
      self.impact = impact
      self.partialMModeName = partialMModeName
      self.quickPatchScriptResults = quickPatchScriptResults
      self.pendingRebootNotification = pendingRebootNotification
      self.quickPatchInfo = quickPatchInfo
      self.quickPatchIncompatNotifs = quickPatchIncompatNotifs

def _getImageImpactParameters(hostImg, imgProfile, enableQuickPatch,
                              doQuickPatchScan):
   """Returns the image impact parameters in an _ImageImpactParameters()
      instance.
   """
   # When the image state is bootbank updated, we have a pending
   # reboot, which takes precedence over all other cases.
   rebootPending = (hostImg.imgstate == hostImg.IMGSTATE_BOOTBANK_UPDATED)
   if rebootPending:
      n = getNotification(PENDING_REBOOT_ID, PENDING_REBOOT_ID)
      return _ImageImpactParameters(IMPACT_REBOOT, pendingRebootNotification=n)

   try:
      qpInstaller = None
      if doQuickPatchScan:
         # QuickPatchInstaller, if Quick Patch scan is needed, will run and
         # return Nones (unsupported) when:
         # 1. There is no Quick Patch VIB in the image profile.
         # 2. An overlay VIB interferes with a Quick Patch VIB.
         # 3. There are other VIB changes that are reboot required.
         # For case 1/2 we need to run the LiveImageInstaller again to get image
         # impact.
         qpInstaller = hostImg.installers['quickpatch']

      # LiveImageInstaller gives Nones (unsupported) when reboot is required:
      # 1. The host is already pending a reboot.
      # 2. VIB removed or VIB installed requires reboot.
      # 3. The transaction involves a change in overlay file.
      liveInstaller = hostImg.installers['live']
   except KeyError as e:
      raise InstallerNotAppropriate(str(e),
         '%s installer is not operational' % str(e).capitalize())

   (qpImpact, pmmName, qpScriptRes, qpInfo, pendingRebootNotif,
    qpIncompatNotifs) = None, None, None, None, None, None
   if qpInstaller:
      (qpImpact, pmmName, qpScriptRes, qpInfo, pendingRebootNotif,
       qpIncompatNotifs) = _getQuickPatchImpact(qpInstaller, hostImg,
         imgProfile, enableQuickPatch)

   if isNotNone(qpImpact):
      # Quick Patch live/PMM/conflicting PMMs, pending reboot for incomplete
      # Quick Patch remediation, or Quick Patch unsupported due to DPU/TPM or
      # live component/solution.
      # In cases of conflicting PMMs, full Mmode will be required, which means
      # remediation will fail if Quick Patch is enforced.
      # If there is no Quick Patch VIB or a reboot is required, qpImpact will be
      # None.
      return _ImageImpactParameters(
         qpImpact,
         partialMModeName=pmmName,
         quickPatchScriptResults=qpScriptRes,
         pendingRebootNotification=pendingRebootNotif,
         quickPatchInfo=qpInfo,
         quickPatchIncompatNotifs=qpIncompatNotifs)

   # Live impact can also return Quick Patch incompatible notification if the
   # remediation is live but Quick Patch is enabled.
   liveImpact, liveQpIncompatNotifs = _getLiveImpact(liveInstaller, hostImg,
                                                 imgProfile, enableQuickPatch)
   if isNotNone(liveImpact):
      # Live install/remove, MMode, or no-op; still need to return
      # quickPatchScriptResults for notifications from Quick Patch scripts
      # (if available).
      # There can be Quick Patch incompatible notifications when the image is
      # live with Quick Patch enabled.
      return _ImageImpactParameters(
         liveImpact,
         quickPatchScriptResults=qpScriptRes,
         quickPatchIncompatNotifs=liveQpIncompatNotifs)

   # Reboot is required; still need to return qpIncompatNotifs and
   # quickPatchScriptResults for Quick Patch notifications.
   return _ImageImpactParameters(
      IMPACT_REBOOT,
      quickPatchScriptResults=qpScriptRes,
      quickPatchIncompatNotifs=qpIncompatNotifs)

def getImageProfileImpact(hostImg, imgProfile):
   """Returns a tuple of the image impact of the target image profile and a
      boolean indicating whether a reboot is currently pending.
      Image impact could be one of:
      - IMPACT_NONE: no impact, live install/remove.
      - IMPACT_MMODE: full maintenance mode required.
      - IMPACT_REBOOT: reboot required (implies full maintenance mode required).
      The calculation is VIB-based with reboot, overlay and mmode metadata.
      This function should only be used for non-QuickPatch remediation, as it
      does not execute Quick Patch scan scripts and relies solely on the
      aforementioned VIB metadata.
   """
   params = _getImageImpactParameters(hostImg, imgProfile, False, False)
   return params.impact, params.pendingRebootNotification is not None

def getSolutionInfo(solution, solComps):
   """Form a SolutionInfo object from a Solution object and a list of
      matched solution components.
   """
   compDetails = list()
   for comp in solComps:
      solComp = SolutionComponentDetails(
                           component=comp.compNameStr,
                           display_name=comp.compNameUiStr,
                           display_version=comp.compVersionUiStr,
                           vendor=comp.vendor)
      compDetails.append(solComp)

   solDetails = SolutionDetails(display_name=solution.nameSpec.uiString,
                                display_version=solution.versionSpec.uiString,
                                components=compDetails)

   return SolutionInfo(details=solDetails,
                       version=solution.versionSpec.version.versionstring,
                       components=[SolutionComponentSpec(component=d.component)
                                   for d in compDetails])

def getHardwareSupportCompliance(status, curManifest, targetManifest,
                                 notifications):
   """For hardware support compliance object from the computed status,
      two manifest objects and notifications.
   """
   def _getOptionalInfo(manifest):
      if isNotNone(manifest):
         return HardwareSupportPackageInfo(
                  pkg=manifest.hardwareSupportInfo.package.name,
                  version=manifest.hardwareSupportInfo.package.version)
      else:
         return None

   assert curManifest or targetManifest
   curInfo = _getOptionalInfo(curManifest)
   targetInfo = _getOptionalInfo(targetManifest)
   notifications = notifications or Notifications()

   return HardwareSupportPackageCompliance(status=ComplianceStatus(status),
                                           current=curInfo,
                                           target=targetInfo,
                                           hardware_modules=dict(),
                                           notifications=notifications)

def _getNsxWcpComps(components):
   """Returns a tuple of NSX and WCP components if any.
   """
   nsx, wcp = None, None
   for comp in components.IterComponents():
      # Name of WCP component changes with every Kubernetes release.
      if comp.compNameStr.startswith(WCP_COMPONENT_PREFIX):
         wcp = comp
      elif comp.compNameStr == NSX_COMPONENT:
         nsx = comp
   return nsx, wcp

class HostScanner(object):
   """Host scanner.

   Scan the host compliance based on the desired software spec.
   """

   def __init__(self, swSpec, depotSpec, task, enableQuickPatch=False):
      """Constructor.
         Parameters:
            * swSpec - software spec dictionary.
            * depotSpec - depot spec dictionary.
            * task - Task instance to update progress.
            * enableQuickPatch - whether to enable Quick Patch.
      """
      self.swSpec = swSpec
      self.depotSpec = depotSpec
      self._depotMgr = None

      # Task should have been already started.
      self.task = task

      self._platform = GetHostSoftwarePlatform()

      self.hostImage = None
      self.hostImageProfile = None
      self.stagedImageProfile = None
      self.currentSoftwareScanSpec = None
      self.stagedSoftwareSpec = None
      self.desiredImageProfile = None

      self.hostCompSource = None
      self.stageCompSource = None
      self.desiredCompSource = None
      self.releaseUnitCompDowngrades = dict()
      self.stageUnitCompDowngrades = dict()
      self.userCompDowngrades = dict()
      self.stageCompDowngrades = dict()

      # A map from component name to version to UI name/version strings.
      self._compUiStrMap = dict()

      # A map of fully obsoleted reserved components on host:
      # component name -> (version, entity, replaced-by entity)
      self._hostObsoletedComps = dict()
      self._stageObsoletedComps = dict()

      self.overallNotifications = None

      self.dpusCompliance = None

      # Quick Patch related.
      # - Whether enableQuickPatch policy is set.
      self._enableQuickPatch = enableQuickPatch
      # - Whether to run Quick Patch installer. Even when True, Quick Patch
      # related fields may not get filled due to enableQuickPatch value and
      # script returns. Value to be set in scan().
      self._doQuickPatchScan = False
      # - Dictionary of Quick Patch script return. To be populated in
      # computeImageImpact().
      self._quickPatchScriptResults = None
      # - Whether Quick Patch compliance should be set to incompatible. To be
      # set in computeImageImpact().
      self._isQuickPatchIncompatible = False

      _addPythonPaths()

   def _formUnavailableResult(self, errMsg):
      """Form unavailable host compliance scan result.
      """
      errMsg = getNotification(UNAVAILABLE_ID,
                               UNAVAILABLE_ID, type_=ERROR)
      self.overallNotifications.errors.append(errMsg)

      baseImageInfo = getBaseImageInfo('', '', '', datetime.utcnow(), '')
      baseImage = BaseImageCompliance(status=ComplianceStatus(UNAVAILABLE),
                                      current=baseImageInfo,
                                      target=baseImageInfo,
                                      notifications=Notifications())
      addOn = AddOnCompliance(status=ComplianceStatus(UNAVAILABLE),
                              notifications=Notifications())

      # Re-process all notification lists to use optional when applicable.
      notifications = Notifications(
                  info=getOptionalVal(self.overallNotifications.info),
                  warnings=getOptionalVal(self.overallNotifications.warnings),
                  errors=getOptionalVal(self.overallNotifications.errors))

      return HostCompliance(impact=ComplianceImpact(IMPACT_NONE),
                               status=ComplianceStatus(UNAVAILABLE),
                               notifications=notifications,
                               scan_time=datetime.utcnow(),
                               base_image=baseImage,
                               add_on=addOn,
                               hardware_support=dict(),
                               components=dict(),
                               solutions=dict())

   def _getCompCompliance(self, status, current, target, currentSource,
                          targetSource, notifications=None):
      """Gets ComponentCompliance VAPI object.
      """
      notifications = notifications or Notifications()
      curSrcObj = (ComponentSource(currentSource)
                     if isNotNone(currentSource) else None)
      targetSrcObj = (ComponentSource(targetSource)
                        if isNotNone(targetSource) else None)
      return ComponentCompliance(status=ComplianceStatus(status),
                                 current=current, target=target,
                                 current_source=curSrcObj,
                                 target_source=targetSrcObj,
                                 notifications=notifications)

   def _compSummaryToInfo(self, compSummaries):
      """Convert a list of component summary and the components' versions
         to a component info dict indexed by component names.
      """
      compDict = dict()
      for comp in compSummaries:
         compName = comp[COMPONENT]
         compVersion = comp[VERSION]
         # Component display name and version are not mandated, name and
         # version are used in case they are not provided.
         compInfo = getComponentInfo(comp[DISP_NAME] or compName,
                                     compVersion,
                                     comp[DISP_VERSION] or compVersion,
                                     comp[VENDOR])
         compDict[compName] = compInfo
      return compDict

   def _getCompUiStrs(self, name, version):
      """Return a tuple of UI name, UI version and UI string of a component.
      """
      try:
         return self._compUiStrMap[name][version]
      except KeyError:
         raise KeyError('UI strings of component %s(%s) is not found in '
                        'cache' % (name, version))

   def reportErrorResult(self, errMsg, ex):
      """Form compliance result for an error from a general error message
         and an exception.
      """
      # Log error message and trace.
      logMsg = '%s: %s' % (errMsg, str(ex))
      logging.error(logMsg)
      logging.error(traceback.format_exc())
      if taskHasNotification(self.task):
         notif = getNotification(HOSTSCAN_FAILED, HOSTSCAN_FAILED)
         self.task.updateNotifications([notif])
      unavalCompliance = self._formUnavailableResult(errMsg)

      if ALLOW_DPU_OPERATION:
         self.mergeWithDpuResults(unavalCompliance)

      self.task.failTask(error=getExceptionNotification(ex),
                         result=vapiStructToJson(unavalCompliance))

      # XXX Need to be exception based, revisit it when changing the CLI
      # interfaces.
      sys.exit(1)

   def getLocalSolutionInfo(self, imageProfile):
      """Get a tuple of:
         1) A dict of SolutionComponentDetails for local solution components,
            indexed by solution name and then component name.
         2) Names of components that are installed as part of solutions.
      """
      solInfoDict = dict()
      ipSolDict, ipSolComps = imageProfile.GetSolutionInfo()
      for solId, solComps in ipSolDict.items():
         solution = imageProfile.solutions[solId]
         solInfoDict[solution.nameSpec.name] = getSolutionInfo(solution,
                                                               solComps)
      return solInfoDict, list(ipSolComps.keys())

   def scanLocalComponents(self, imageProfile, obsoletedComps, biComponents,
                           addOnComponents, addOnRemovedCompNames, hspComps,
                           hspRemovedCompNames, solutionCompNames):
      """Compute the local installed compoments against those in base image,
         addon, manifests and solution.
         Specifically record components that are:
         1) Remove/downgraded in base image and addon.
         2) Given by user to override or add to base image, addon and manifests.
         3) Provided by a solution.
         4) Provided by a hardware support package.
      """
      ADD, UPGRADE, DOWNGRADE = 'add', 'upgrade', 'downgrade'

      def _addCompInSpec(compName, comp, spec, subject, hostVer, subjectVer):
         """Triage a component to one of add, upgrade and downgrade categories
            according to host's and the spec piece's component versions.
            An addition in the spec dict means the spec piece (e.g. addon)
            adds the component, or upgrades/downgrades the component of another
            image piece (subject), e.g. base image.
         """
         if hostVer > subjectVer:
            spec[UPGRADE][subject][compName] = comp
         elif hostVer < subjectVer:
            spec[DOWNGRADE][subject][name] = comp
         else:
            # Even if multiple pieces claim the same version, the component
            # will be treated as "add" in all of them. This makes sure upgrade
            # and downgrade components are calculated correctly.
            spec[ADD][name] = comp

      def _getCompVersions(name):
         """Returns versions of a component in base image, addon and HSP,
            None is used whenever the component is not found in a release unit.
         """
         getCompVerObj = lambda d, n: (VibVersion.fromstring(d[n])
                                       if d and n in d else None)
         return [getCompVerObj(comps, name)
                 for comps in (biComponents, addOnComponents, hspComps)]

      addOnRemovedCompNames = addOnRemovedCompNames or []
      hspRemovedCompNames = hspRemovedCompNames or set()

      # All component names appear in the addon, base image and manifests.
      biAddonHspCompNames = set(biComponents.keys())
      if addOnRemovedCompNames:
         biAddonHspCompNames -= set(addOnRemovedCompNames)
      if addOnComponents:
         biAddonHspCompNames |= set(addOnComponents.keys())
      if hspRemovedCompNames:
         biAddonHspCompNames -= hspRemovedCompNames
      if hspComps:
         biAddonHspCompNames |= set(hspComps.keys())

      # Base image components.
      baseImageCompSpec = dict()
      # A breakdown of addon components regarding base image components.
      addonCompSpec = {
         ADD: dict(),
         UPGRADE: {
            BASE_IMG: dict(),
         },
         DOWNGRADE: {
            BASE_IMG: dict(),
         },
      }
      # A breakdown of hardware support components' relationship related to
      # base image and addon.
      hspCompSpec = {
         ADD: dict(),
         UPGRADE: {
            BASE_IMG: dict(),
            ADD_ON: dict()
         },
         DOWNGRADE: {
            BASE_IMG: dict(),
            ADD_ON: dict(),
         },
      }
      # A breakdown of user components regarding manifest components.
      userCompSpec = {
         ADD: dict(),
         UPGRADE: {
            BASE_IMG: dict(),
            ADD_ON: dict(),
            HARDWARE_SUPPORT: dict(),
         },
         DOWNGRADE: {
            BASE_IMG: dict(),
            ADD_ON: dict(),
            HARDWARE_SUPPORT: dict(),
         },
      }

      installedComps = imageProfile.ListComponentSummaries(removeDup=True)
      installedCompNames = set()
      for comp in installedComps:
         # Loop to figure out upgrade/downgrade relations.
         name = comp['component']
         installedCompNames.add(name)

         if name in solutionCompNames:
            # Solution component.
            continue

         if name not in biAddonHspCompNames:
            # Component introduced by the user, could be a new component,
            # or one that is removed by addon but re-added by user.
            userCompSpec[ADD][name] = comp
         else:
            # Find the provider of the component and if there is any
            # upgrade/downgrade took place.
            hostVersion = VibVersion.fromstring(comp[VERSION])
            biVersion, addonVersion, hspVersion = _getCompVersions(name)

            if isNotNone(biVersion):
               # Base image originally has the component.
               if biVersion == hostVersion:
                  # No override.
                  baseImageCompSpec[name] = comp
               elif isNotNone(addonVersion) and addonVersion == hostVersion:
                  # Addon override base image, it should be upgrade only, but
                  # we will check all cases nevertheless.
                  _addCompInSpec(name, comp, addonCompSpec, BASE_IMG,
                                 hostVersion, biVersion)
               elif isNotNone(hspVersion) and hspVersion == hostVersion:
                  # HSP overrides base image, it should be upgrade only, but
                  # we will check all cases just like addon.
                  _addCompInSpec(name, comp, hspCompSpec, BASE_IMG,
                                 hostVersion, biVersion)
               else:
                  # User component override base image, "add" was already
                  # handled, so here should only be upgrade/downgrade.
                  _addCompInSpec(name, comp, userCompSpec, BASE_IMG,
                                 hostVersion, biVersion)
            elif isNotNone(addonVersion):
               # Addon originally has the component.
               if addonVersion == hostVersion:
                  # Addon adds the component.
                  addonCompSpec[ADD][name] = comp
               elif isNotNone(hspVersion) and hspVersion == hostVersion:
                  # HSP overrides addon, it should be upgrade only, but
                  # we will check all cases just like addon.
                  _addCompInSpec(name, comp, hspCompSpec, ADD_ON,
                                 hostVersion, addonVersion)
               else:
                  # User component override addon, "add" was already
                  # handled, so here should only be upgrade/downgrade.
                  _addCompInSpec(name, comp, userCompSpec, ADD_ON,
                                 hostVersion, addonVersion)
            else:
               # HSP originally has the component.
               if hspVersion == hostVersion:
                  # Manifest adds the component.
                  hspCompSpec[ADD][name] = comp
               else:
                  # User component upgrades/downgrades HSP.
                  _addCompInSpec(name, comp, userCompSpec, HARDWARE_SUPPORT,
                                 hostVersion, hspVersion)

      # Component on host may have been reserved due to VIB obsolete rather than
      # same component name replacement or manual removal. A catch-up component
      # scanning is required so we do not report release unit component removal
      # when a component was obsoleted as a result of the last apply.
      reservedComps = set(imageProfile.reservedComponentIDs)
      if reservedComps:
         fullComps = self._depotMgr.componentsWithVibs
         hostComps = imageProfile.GetKnownComponents()

         # Scan requires VIBs to be present, a component reserved by pre-U2 7.0
         # may not have its VIBs present in the depot.
         missing = set()
         for name, ver in reservedComps:
            # PR 3021932: in case of dual component on host, the lower version
            # one would have been already removed from the image profile.
            if not fullComps.HasComponent(name, ver) and \
                  hostComps.HasComponent(name, ver):
               missing.add('%s(%s)' % (name, ver))
               hostComps.RemoveComponent(name, ver)

         if missing:
            logging.warning('Missing VIBs for reserved components %s, they '
                            'might be wrongfully reported as cause of release '
                            'unit non-compliance if they are obsoleted.',
                            getCommaSepArg(missing))

         # Need to look for obsoletes on all platforms.
         probs = hostComps.Validate(self._depotMgr.vibs)
         _, obsoletes = probs.GetErrorsAndWarnings()
         for obsolete in obsoletes.values():
            assert obsolete.reltype == obsolete.TYPE_OBSOLETES
            comp, replacesComp = (hostComps.GetComponent(obsolete.comp),
                                  hostComps.GetComponent(obsolete.replacesComp))
            name, ver = replacesComp.compNameStr, replacesComp.compVersionStr

            if (name, ver) in reservedComps:
               # PR3031405: if a component that replaces a reserved component
               # is itself not installed, there is no need to populate
               # obsoletedComps.
               if not self.hostImageProfile.components.HasComponent(
                      comp.compNameStr):
                  continue
               by = SOURCE_TO_ENTITY[self.hostCompSource[comp.compNameStr]]

               # Determine the source of the obsoleted component.
               biVer, addonVer, hspVer = _getCompVersions(name)
               src = SOLUTION # User component is not reserved when obsoleted.
               if hspVer:
                  src = HARDWARE_SUPPORT
               elif addonVer:
                  src = ADDON
               elif biVer:
                  src = BASE_IMAGE
               # Version, source of replacement, source of the obsoleted.
               obsoletedComps[name] = (ver, src, by)

      # Sets for the components of interest.
      removedBIComps = ((set(biComponents.keys()) - installedCompNames) -
                         set(addOnRemovedCompNames) - set(hspRemovedCompNames))
      downgradedBIComps = (set(addonCompSpec[DOWNGRADE][BASE_IMG].keys() |
                           set(hspCompSpec[DOWNGRADE][BASE_IMG].keys()) |
                            set(userCompSpec[DOWNGRADE][BASE_IMG].keys())) -
                            set(addOnRemovedCompNames) -
                            set(hspRemovedCompNames))

      removedAddonComps, downgradedAddonComps = set(), set()
      if addOnComponents:
         removedAddonComps = (set(addOnComponents.keys()) -
                              installedCompNames -
                              set(hspRemovedCompNames))
         downgradedAddonComps = (set(hspCompSpec[DOWNGRADE][ADD_ON].keys()) |
                                 set(userCompSpec[DOWNGRADE][ADD_ON].keys()) -
                                 set(hspRemovedCompNames))
      # All addon components
      allAddonComps = dict()
      allAddonComps.update(addonCompSpec[ADD])
      allAddonComps.update(addonCompSpec[UPGRADE][BASE_IMG])
      allAddonComps.update(addonCompSpec[DOWNGRADE][BASE_IMG])

      # All user components.
      allUserComps = dict()
      allUserComps.update(userCompSpec[ADD])
      allUserComps.update(userCompSpec[UPGRADE][HARDWARE_SUPPORT])
      allUserComps.update(userCompSpec[UPGRADE][ADD_ON])
      allUserComps.update(userCompSpec[UPGRADE][BASE_IMG])
      allUserComps.update(userCompSpec[DOWNGRADE][HARDWARE_SUPPORT])
      allUserComps.update(userCompSpec[DOWNGRADE][ADD_ON])
      allUserComps.update(userCompSpec[DOWNGRADE][BASE_IMG])

      compInfo = dict()

      # List of info dictionary.
      # TODO: refactor the usage to also use dictionary and not
      # convert back-and-forth between dictionary and list.
      compInfo[USER_COMPS_KEY] = list(allUserComps.values())

      # Name to info dictionaries.
      compInfo[BASEIMAGE_COMPS_KEY] = baseImageCompSpec
      compInfo[ADDON_COMPS_KEY] = allAddonComps

      # Lists of names.
      compInfo[DOWNGRADED_BI_COMP_KEY] = downgradedBIComps
      compInfo[REMOVED_BI_COMP_KEY] = removedBIComps
      compInfo[DOWNGRADED_ADDON_COMP_KEY] = downgradedAddonComps
      compInfo[REMOVED_ADDON_COMP_KEY] = removedAddonComps
      return compInfo

   def populateCompDowngradeInfo(self, imageProfile, unitCompDowngrades,
                                 userCompDowngrades):
      """Populate component downgrades info.
         For release unit downgrades where both source and destination are
         release units, get a map that is indexed with the from-image entity,
         the to-image entity, and component names, each value is a tuple of
         the versions and whether there is a config schema downgrade involved.
         For user components, get a name map where each value is a tuple of
         source/dest entities, versions, and if there is a config schema
         downgrade involved.
      """
      downgrades = \
         imageProfile.GetCompsDowngradeInfo(self.desiredImageProfile)
      for name, (v1, v2, src, dest, configDowngrade) in downgrades.items():
         # Convert source keywords to entity ones.
         srcEntity = SOURCE_TO_ENTITY[src]
         destEntity = SOURCE_TO_ENTITY[dest]
         if srcEntity == COMPONENT or destEntity == COMPONENT:
            # Release unit to user, user to release unit, or user to user
            # downgrades.
            userCompDowngrades[name] = v1, v2, src, dest, configDowngrade
         if srcEntity != COMPONENT or destEntity != COMPONENT:
            # Release unit to user, user to release unit, or release unit to
            # release unit downgrade.
            srcDict = unitCompDowngrades.setdefault(srcEntity, dict())
            srcDict.setdefault(destEntity, dict())[name] = (v1, v2,
                                                            configDowngrade)

   def getImageProfileOrphanVibs(self, imageProfile):
      """
      Get IDs of orphan VIBs on the host, i.e. VIBs on the host that are not a
      part of installed components.
      """
      return list(
         imageProfile.GetOrphanVibs(platform=self._platform).keys())

   def getImageProfileScanSpec(self, imageProfile, obsoletedComps,
                               isStage=False):
      """ For personality manager. Converts the Image Profile into
      a Software scan Specification.
      Core Logic:
      1. Get the BaseImage info from host
      2. Get the Addon info from host
      3. Get all Hardware Support components from host
      4. Scan for local component changes
      5. Get the orphan vibs

      Sample Spec with BaseImage/Addon/Solution info objects exploded:

      {"base_image":{"version": "6.8.7-1213313",
                     "display_name": "xyz",
                     "display_version": "xyz",
                     "release_date":"abc"},
       "add_on": {"version": "6.8.7-1213313",
                  "display_version":"xyz",
                  "name":"abc",
                  "display_name": "xyz",
                  "vendor":"vmw"},
       "base_image_components": {"test":{"component": "test",
                                         "version": "6.8.7-1213313",
                                         "display_name": "test",
                                         "display_version": "xyz",
                                         "vendor": "vmw"}},
       "addon_components": {"test":{"component": "test",
                                    "version": "6.8.7-1213313",
                                    "display_name": "test",
                                    "display_version": "xyz",
                                    "vendor": "vmw"}},
       "hardware_support": {
          "hardwareSupportManagerName": {
             "installedCompName": "1.0-1"
          }
       },
       "user_components": {"test":{"component": "test",
                                   "version": "6.8.7-1213313",
                                   "display_name": "test",
                                   "display_version": "xyz",
                                   "vendor": "vmw"}},
       "removed_or_downgraded_bi_components": {"test1"},
       "removed_or_downgraded_add_on_components": {"test2"},
       "orphan_vibs": ["Vendor_bootbank_vib1_1.0-1"],
       "solutions": [
         {
            "name": "solution-1",
            "version": "1.0-1",
            "display_name": "solution 1",
            "display_version": "1.0 release 1",
            "components": [
               {
                  "component": "component-1",
                  "version": "1.0-1",
                  "display_name": "component 1",
                  "display_version": "1.0 release 1",
                  "vendor": "VMware"
              }
            ]
         }
         ]
      }
      """
      scanSpec = dict()

      if not imageProfile:
         return scanSpec

      # Base Image info
      currentBI = imageProfile.baseimage
      if currentBI:
         # Base image object in esximage does not have UI name string,
         # initializing it with 'ESXi'
         uiName = BASEIMAGE_UI_NAME
         baseImageInfo = getBaseImageInfo(uiName,
                                  currentBI.versionSpec.version.versionstring,
                                  currentBI.versionSpec.uiString,
                                  currentBI.releaseDate,
                                  currentBI.quickPatchCompatibleVersions)
         baseImageComponents = currentBI.components
         if isStage:
            for key in STAGE_BLACKLIST:
               if key in baseImageComponents:
                  baseImageComponents.pop(key)
      else:
         # Base image info is required, use empty strings and current time.
         baseImageInfo = getBaseImageInfo('', '', '', datetime.utcnow(), '')
         baseImageComponents = dict()
      scanSpec[BASE_IMG] = baseImageInfo

      # Addon Info
      addOnInfo = None
      addOnComponents, addOnRemovedCompNames = None, None
      addon = imageProfile.addon
      if addon:
         addOnInfo = getAddOnInfo(addon.nameSpec.name,
                                  addon.nameSpec.uiString,
                                  addon.versionSpec.version.versionstring,
                                  addon.versionSpec.uiString,
                                  addon.vendor)
         addOnComponents = addon.components
         addOnRemovedCompNames = addon.removedComponents
      scanSpec[ADD_ON] = addOnInfo

      # Manifests info
      hspDict, allHspCompDict, allHspRmCompNames = \
         imageProfile.GetHardwareSupportInfo()

      scanSpec[HARDWARE_SUPPORT] = hspDict

      # Solution info
      solDict, solCompNames = self.getLocalSolutionInfo(imageProfile)
      scanSpec[SOLUTIONS] = solDict

      # Compute components info
      compInfo = self.scanLocalComponents(imageProfile,
                                          obsoletedComps,
                                          baseImageComponents,
                                          addOnComponents,
                                          addOnRemovedCompNames,
                                          allHspCompDict,
                                          allHspRmCompNames,
                                          solCompNames)
      scanSpec.update(compInfo)
      # Compute orphan vibs
      scanSpec[ORPHAN_VIBS] = self.getImageProfileOrphanVibs(imageProfile)
      return scanSpec

   def _processPrecheckResult(self, result):
      """Form notification messages for a precheck error/warning result.
         Input is a precheck Result object, the current error notifications,
         and the current warning notifications.
         Side effect: based on the type, an error/warning notification will
         be appended to the overall notifications.
      """
      def _formAndAddMsg(result, msgId, found, expected):
         # We need to tell which args are needed. For this purpose, we formulate
         # all messages to have formatter {1} for found and {2} for expected.
         msg = NOTIFICATION_MSG[msgId]
         msgArgs = []
         if msg.find('{1}') != -1:
            msgArgs.append(','.join(str(x) for x in found))
         if msg.find('{2}') != -1:
            msgArgs.append(','.join(str(x) for x in expected))
         notifType = ERROR if result.code == result.ERROR else WARNING
         notification = getNotification(msgId,
                                        msgId,
                                        msgArgs=msgArgs,
                                        type_=notifType)
         if result.code == result.ERROR:
            self.overallNotifications.errors.append(notification)
         elif result.isWarnType():
            self.overallNotifications.warnings.append(notification)

      msgId = PRECHECK_NOTIFICATION_ID[result.name]
      # Format message with found and expected. Not all string require
      # both in the string, but format() will not complain about it.
      if msgId == UNSUPPORTED_VENDOR_MODEL_ID:
         # Hardware mismatch can bring multiple errors, each in tuple
         # (vendor/model, image-value, host-value). Convert them to
         # multiple found != expected precheck errors.
         for match, image, host in result.found:
            realMsgId = msgId % match.capitalize()
            _formAndAddMsg(result, realMsgId, [host], [image])
      # We need different messageIds in the case of CPU Support for warning,
      # error and override cases respectively. Hence, based on the hard-coded
      # value, we are passing different messageIds. We should have a
      # generalized workflow in case more messageIds are added.
      elif msgId == UNSUPPORTED_CPU_ID:
         realMsgId = msgId
         if result.code == result.WARNING:
            realMsgId = UNSUPPORTED_CPU_WARN_ID
         elif result.code == result.OVERRIDEWARNING:
            realMsgId = UNSUPPORTED_CPU_OVERRIDE_ID
         _formAndAddMsg(result, realMsgId, result.found, result.expected)
      # Assign the warning ID for disk size warning else keep the error ID.
      elif msgId == DISK_TOO_SMALL_ID:
         realMsgId = msgId
         if result.code == result.WARNING:
            realMsgId = DISK_TOO_SMALL_WARN_ID
         _formAndAddMsg(result, realMsgId, result.found, result.expected)
      else:
         # Otherwise use found and expected to format one message.
         _formAndAddMsg(result, msgId, result.found, result.expected)

   def checkHardwareCompatibility(self):
      """Perform hardware compatibility precheck through weasel's
         upgrade_precheck.py.
         This method should be called only when base image is incompliant,
         otherwise we will be re-running checks that have already been
         done when the host was imaged.
         Returns: A True/False flag indicating if the system is compatible.
      """
      # Inline import as path is added when Scanner is initiated.
      from weasel.util import upgrade_precheck as precheck
      errors, warnings = precheck.imageManagerAction(self.hostImageProfile,
                                                     self.desiredImageProfile)

      # Warnings do not make the host incompatible, but they are sent up in
      # overall notifications.
      for result in errors + warnings:
         self._processPrecheckResult(result)
      return len(errors) == 0

   def checkQuickBootCompatibility(self):
      """Execute QuickBoot and SuspendToMemory prechecks and report
         compatibility by adding a notification.
      """
      if HostOSIsSimulator():
         # For simulators we will report QuickBoot not supported.
         logging.info('Host is a simulator, skipping Quick Boot precheck')
         n = getNotification(QUICKBOOT_UNSUPPORTED_ID, QUICKBOOT_UNSUPPORTED_ID)
         self.overallNotifications.info.append(n)
         return

      def isDriverComponent(comp, allVibs):
         vibs = comp.GetVibCollection(allVibs, platform=self._platform)
         for vib in vibs.values():
            for f in vib.filelist:
               if os.path.dirname(f) in DRIVER_MAP_DIRS:
                  return True
         return False

      if IS_ESXIO:
         # XXX: esxio does not have loadesxLive module. May ignore quick boot
         # for esxio.
         return

      # Inline import as path is added when Scanner is initiated.
      from loadesxLive.compCheck import printViolationMsg
      from loadesxLive.precheck import PrecheckStatus
      from loadesxLive.stmCompCheck import STMPrecheckStatus, stmPrecheck

      logging.info('Running Quick Boot and Suspend To Memory prechecks')
      result, hard, soft = stmPrecheck()

      if result == PrecheckStatus.ERROR:
         # It is rare to see an error, treat the host as unsupported.
         nId = QUICKBOOT_UNSUPPORTED_ID
         # Error is returned as a string in the place of hard error.
         logging.warning('Quick Boot precheck failed with error: %s', hard)
      elif result == PrecheckStatus.PASSED:
         nId = QUICKBOOT_SUPPORTED_ID
         printViolationMsg(logging.info, 'Suspend To Memory hard', hard)
      elif result == STMPrecheckStatus.PASSED:
         nId = QUICKBOOT_AND_SUSPEND_TO_MEMORY_SUPPORTED_ID
      else:
         nId = QUICKBOOT_UNSUPPORTED_ID
         printViolationMsg(logging.info, 'Quick Boot hard', hard)
         printViolationMsg(logging.info, 'Quick Boot soft', soft)

      # TODO: once we re-enable support of any form of downgrade, notification
      # QUICKBOOT_UNAVAILABLE_DRIVER_DG_ID should be added when:
      # 1) Quick Boot or Suspend to Moemory is supported.
      # 2) There is a driver downgrade.
      # 3) The overall scan compliance is non-compliant.
      # This is because downgrade is not internally tested with Quick Boot and
      # we want to stay on the safer side.

      n = getNotification(nId, nId)
      self.overallNotifications.info.append(n)

   def _getReleaseUnitCompDowngradeMsgs(self, newUnit, relType, oldUnit=None):
      """Checks component downgrade from old release unit and user components
         to the new release unit components, returns a list of error messages
         for unsupported component downgrades.
         Optional Parameter:
            oldUnit - A "comparable" old release unit to check same-name release
                      unit component downgrade, this means old baseimage, old
                      Addon of the same NameSpec or HSP of the same HSM and HSP
                      NameSpec.
      """
      def getSingleDestMsgs(newUnit, comps, srcType, destType):
         """Get downgrade notification messages from the host components to the
            new release unit.
            For the handled combinations, all components are included in one
            message. We don't want to panic when an unsual combination
            appears. As a fallback, there would be one generic message per
            component.
         """
         msgs = []
         try:
            msgId = COMP_DOWNGRADE_NOTIFICATION_ID[srcType][destType]
            # For all downgrades in desired Base Image, downgraded component
            # name(version) list is the only message argument; otherwise,
            # it is always downgraded name(version) list followed by the name
            # of the release unit.
            msgArgs = []
            compList = [self._getCompUiStrs(comp, v1)[2]
                        for comp, (v1, _, _) in comps.items()]
            msgArgs.append(getCommaSepArg(compList))
            if destType == HARDWARE_SUPPORT:
               msgArgs.append(newUnit.hardwareSupportInfo.package.name)
            elif destType in (ADDON, SOLUTION):
               msgArgs.append(newUnit.nameSpec.name)
            msgs.append(getNotification(msgId, msgId, msgArgs=msgArgs,
                                        type_=ERROR))
         except KeyError:
            logging.warning('Release unit component downgrades of %s from %s '
                            'to %s does not have a message, using the generic '
                            'one.', getCommaSepArg(comps.keys()), srcType,
                            destType)
            msgId = COMPONENT_DOWNGRADE_ID
            for comp, (v1, v2, _) in comps.items():
               uiName, uiVer2, _ = self._getCompUiStrs(comp, v2)
               uiVer1 = self._getCompUiStrs(comp, v1)[1]
               msgs.append(getNotification(msgId, msgId,
                                           msgArgs=[uiVer2, uiName, uiVer1],
                                           type_=ERROR))
         return msgs

      if relType == SOLUTION:
         # Different than other release units, solution can downgrade when a
         # host is moved around, and version (choice) of a solution component
         # can also downgrade when external component dependencies change.
         # Thus, disable version checks of downgrade with component when the
         # same solution changes in version. However, config downgrade check is
         # kept.
         #commonComps = set()

         # XXX: in 7.0 U1, still disallow all downgrades, populate all common
         #      solution components.
         newComps = set()
         commonComps = set()
         for c in newUnit.componentConstraints:
            name = c.componentName
            if self.desiredCompSource.get(name, None) == SOURCE_SOLUTION:
               newComps.add(name)
               if self.hostCompSource.get(name, None) == SOURCE_SOLUTION:
                  commonComps.add(name)
      else:
         # Comps in the new release unit that are going to be installed, and
         # comps in both old and new release units.
         newComps = set(newUnit.components.keys())
         oldComps = (set(oldUnit.components.keys()) if isNotNone(oldUnit)
                     else set())
         commonComps = oldComps & newComps

      errMsgs = []
      for srcType, srcDict in self.releaseUnitCompDowngrades.items():
         for destType, nameDict in srcDict.items():
            if destType != relType:
               # Only care about this release unit downgrading host components.
               continue

            typeDowngrades = dict()
            if srcType == destType:
               # Check component downgrades of the same release unit; specially,
               # all solution components are considered together.
               typeDowngrades = {n: nameDict[n]
                                 for n in commonComps & set(nameDict.keys())}
               if typeDowngrades:
                  errMsgs.extend(getSingleDestMsgs(newUnit, typeDowngrades,
                                                   relType, relType))

            # Check component downgrades from different release units.
            # XXX: in 7.0 U1, still disallow all downgrades, not only those
            #      with config schema.
            downgrades = dict()
            for n, v in nameDict.items():
               assert len(v) == 3
               if n not in newComps:
                  # Skip components outside the new release unit.
                  continue
               if n in typeDowngrades.keys():
                  # Skip same-name type downgrades that are flagged already.
                  continue

               downgrades[n] = v
               #if v[2]:
               #   # If it is a config schema downgrade.
               #   configDowngrades[n] = v
            if downgrades:
               errMsgs.extend(getSingleDestMsgs(newUnit, downgrades,
                                                srcType, relType))

      return errMsgs

   def _amendReleaseUnitDowngradeCompliance(self, newUnit, oldUnit, relType,
                                            curStatus, curErrs=None):
      """Finalizes release unit compliance by looking into unsupported
         downgrades. Returns a compliance status and a list of error messages.
         Parameters:
            newUnit - the new release unit being scanned
            oldUnit - a comparable release unit of the same type and name
                      that is present on host; use None when not present.
            relType - type of the release unit.
            curStatus - current compliance status from regular version check.
            curErrs - errors that are already present for this particular
                      release units, the list will be extended with new ones
                      generated by this method.
      """
      curErrs = curErrs or []
      errs = self._getReleaseUnitCompDowngradeMsgs(newUnit, relType,
                                                   oldUnit=oldUnit)
      if errs:
         curStatus = INCOMPATIBLE
         curErrs.extend(errs)

      return curStatus, curErrs

   def _isRemovedComponentMissing(self, obsoletedComps, compName, expVer,
                                  entType):
      """Returns if a component of BaseImage/Addon/Manifest that was removed
         on the host is missing and needs to be installed. This assmues the
         version of the release unit is compliant, and the component is not
         present on host.
         If the component of expected version is obsoleted by an overriding
         entity's component on host and is also not present in the desired
         image, persumably still being obsoleted, then this component needs
         not to be reported as missing.
         Otherwise it should be reported as missing.
      """
      assert entType != SOLUTION

      obsolete = obsoletedComps.get(compName, None)
      if (isNotNone(obsolete) and obsolete[0] == expVer and
          obsolete[1] == entType):
         expByTypes = ENTITY_OVERRIDE_ORDER[
            ENTITY_OVERRIDE_ORDER.index(entType) + 1:]
         if (obsolete[2] in expByTypes and
             not self.desiredImageProfile.components.HasComponent(
               compName, expVer)):
            logging.debug('Component %s(%s) in %s is obsoleted on host '
                          'and will not be treated as missing', compName,
                          expVer, entType)
            return False
      return True

   def computeBaseImageCompliance(self):
      """
      Computes the base image compliance by comparing the host image vs
      desired image and staged image vs desired image. It populates the staged
      status in the host image compliance and returns the host compliance
      status and staged status.
      """
      hostImageCompliance = self._computeBaseImageCompliance(
         self.hostImageProfile, self.currentSoftwareScanSpec,
         self._hostObsoletedComps)

      stagedImageCompliance = self._computeBaseImageCompliance(
         self.stagedImageProfile, self.stagedSoftwareSpec,
         self._stageObsoletedComps)

      (hostImageCompliance, stageStatus) = \
         self._computeComplianceHelper(hostImageCompliance,
                                       stagedImageCompliance)

      if hostImageCompliance.status == COMPLIANT:
         if self.stagedImageProfile:
            if self.stagedImageProfile.baseimageID != \
               self.hostImageProfile.baseimageID:
               stageStatus = NOT_STAGED

      return (hostImageCompliance, stageStatus)

   def _computeComplianceHelper(self, hostCompliance,
                                 stageCompliance):
      """
      Helper function to compute the hostCompliance
      along with its staging status and return the
      overall staging status
      """
      stageStatus = None
      if not STAGING_SUPPORTED:
         return (hostCompliance, stageStatus)

      if hostCompliance.status == INCOMPATIBLE \
         or hostCompliance.status == UNAVAILABLE \
         or hostCompliance.status == COMPLIANT:
         return (hostCompliance, stageStatus)

      if stageCompliance and stageCompliance.status == COMPLIANT:
         hostCompliance.stage_status = STAGED
         stageStatus = STAGED
      else:
         hostCompliance.stage_status = NOT_STAGED
         stageStatus = NOT_STAGED
      return (hostCompliance, stageStatus)

   def _computeBaseImageCompliance(self, imageProfile, softwareScanSpec,
                                   obsoletedComps):
      """
      Compute base image compliance data. In this function imageProfile
      is compared with the desired base image version
      and the compliance info based on the check will be returned. Compliance
      status will be decided based on the versions and if the status is
      compliant and if some of the base image components are removed by the
      user from the host then the status will be returned as NON-COMPLIANT
      with a warning
      :return: Base Image Compliance info
      """
      infoMsgs, errMsgs = [], []

      if not imageProfile or not softwareScanSpec:
         return None

      # Comparing base image version
      currentBIObj = imageProfile.baseimage
      currentBIInfo = softwareScanSpec[BASE_IMG]
      currentVersion = currentBIInfo.version
      desiredBIObj = self.desiredImageProfile.baseimage
      desiredVersion = desiredBIObj.versionSpec.version.versionstring
      if currentVersion:
         status, errMsg = getVersionCompliance(desiredVersion,
                                               currentVersion,
                                               "ESXi",
                                               BASE_IMAGE)
         if errMsg:
            errMsgs.append(errMsg)
      else:
         status = NON_COMPLIANT

      if status != INCOMPATIBLE:
         # Check the base image does not cause any component downgrade.
         status, errMsgs = self._amendReleaseUnitDowngradeCompliance(
                              desiredBIObj,
                              imageProfile.baseimage,
                              BASE_IMAGE,
                              status,
                              curErrs=errMsgs)

      if status == COMPLIANT:
         # Check if there are any missing base image components when
         # version is compliant.

         # Set of uiString of the missing components.
         missingComps = set()

         for comp in softwareScanSpec.get(REMOVED_BI_COMP_KEY, set()):
            biCompVer = currentBIObj.components[comp]
            if self._isRemovedComponentMissing(obsoletedComps, comp, biCompVer,
                                               BASE_IMAGE):
               if (PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED and
                   comp in self.desiredImageProfile.removedComponents):
                  # Exclude comps which are user removed components
                  continue
               else:
                  missingComps.add(self._getCompUiStrs(comp, biCompVer)[0])

         if missingComps:
            # Changing the status as NON_COMPLIANT
            status = NON_COMPLIANT
            msgArgs = [getCommaSepArg(missingComps)]
            infoMsg = getNotification(BASEIMAGE_COMPONENT_REMOVED_ID,
                                      BASEIMAGE_COMPONENT_REMOVED_ID,
                                      msgArgs=msgArgs)
            infoMsgs.append(infoMsg)

      # Base image object in esximage does not have UI name string,
      # initializing it with 'ESXi'
      uiName = BASEIMAGE_UI_NAME
      targetBIInfo = getBaseImageInfo(uiName,
                        desiredVersion,
                        desiredBIObj.versionSpec.uiString,
                        desiredBIObj.releaseDate,
                        desiredBIObj.quickPatchCompatibleVersions)

      notifications = Notifications(info=getOptionalVal(infoMsgs),
                                    errors=getOptionalVal(errMsgs))
      baseImageCompliance = BaseImageCompliance(status=ComplianceStatus(status),
                                                current=currentBIInfo,
                                                target=targetBIInfo,
                                                notifications=notifications)
      return baseImageCompliance

   def computeAddOnCompliance(self):
      """
      Computes the add on compliance by comparing the host image vs desired
      image and staged image vs desired image.It populates the staged status
      in the host image compliance and returns the overall staged status along
      with the host compliance
      """
      hostAddOnCompliance = self._computeAddOnCompliance(self.hostImageProfile,
         self.currentSoftwareScanSpec, self._hostObsoletedComps)
      stgAddOnCompliance = self._computeAddOnCompliance(self.stagedImageProfile,
         self.stagedSoftwareSpec, self._stageObsoletedComps)
      (hostAddOnCompliance, stageStatus) = self._computeComplianceHelper(
                                             hostAddOnCompliance,
                                             stgAddOnCompliance)
      if hostAddOnCompliance.status == COMPLIANT and self.stagedImageProfile:
         if self.hostImageProfile.addonID:
            if self.stagedImageProfile.addonID:
               stageStatus = NOT_STAGED if (self.stagedImageProfile.addonID \
                  != self.hostImageProfile.addonID) else None
         else:
            stageStatus = NOT_STAGED if self.stagedImageProfile.addonID else \
               None

      return (hostAddOnCompliance, stageStatus)

   def _computeAddOnCompliance(self, imageProfile, softwareScanSpec,
                               obsoletedComps):
      """
      Compute addon compliant status, return a result that is similar to
      baseimage compliance.
      In addition, if there's no addon installed and no addons specified in
      desired state then the status will be returrned as COMPLIANT
      :return: addon Compliance info
      """
      if not imageProfile or not softwareScanSpec:
         return None
      errMsgs, infoMsgs = [], []
      targetAddOnObj = self.desiredImageProfile.addon
      currentAddOnInfo = softwareScanSpec[ADD_ON]
      currentAddOnObj = imageProfile.addon


      # Default compliant result and None target addon.
      status = COMPLIANT
      targetAddOnInfo = None

      currentAddOnName = None
      targetAddOnName = None

      if targetAddOnObj:
         # Desired state has addon.
         targetAddOnName = targetAddOnObj.nameSpec.name
         targetAddOnVersion = targetAddOnObj.versionSpec.version.versionstring
         targetAddOnInfo = getAddOnInfo(targetAddOnName,
                                        targetAddOnObj.nameSpec.uiString,
                                        targetAddOnVersion,
                                        targetAddOnObj.versionSpec.uiString,
                                        targetAddOnObj.vendor)

         if currentAddOnInfo:
            # Both installed and desired state have addon.
            currentAddOnName = currentAddOnInfo.name
            currentAddOnVersion = currentAddOnInfo.version

            if currentAddOnName != targetAddOnName:
               # Installed addon and desired addon are different.
               status = NON_COMPLIANT
               msgArgs = [currentAddOnName, currentAddOnVersion]
               infoMsg = getNotification(ADDON_REMOVAL_ID,
                                         ADDON_REMOVAL_ID,
                                         msgArgs=msgArgs)
               infoMsgs.append(infoMsg)
            else:
               # Compare the version of installer/desired addon.
               status, errMsg = getVersionCompliance(targetAddOnVersion,
                                                     currentAddOnVersion,
                                                     targetAddOnName,
                                                     ADDON)

               if errMsg:
                  errMsgs.append(errMsg)
               if status == COMPLIANT:
                  # Check if there are any missing addon components when
                  # version is in compliant.

                  # Set of uiString of missing components
                  missingComps = set()

                  for comp in softwareScanSpec.get(REMOVED_ADDON_COMP_KEY,
                                                   set()):
                     addonCompVer = currentAddOnObj.components[comp]
                     if self._isRemovedComponentMissing(obsoletedComps,
                           comp, addonCompVer, ADDON):
                        if (PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED and
                            comp in self.desiredImageProfile.removedComponents):
                            # Exclude comps which are user removed components
                            continue
                        else:
                           missingComps.add(
                              self._getCompUiStrs(comp, addonCompVer)[0])

                  if missingComps:
                     status = NON_COMPLIANT
                     msgArgs = [getCommaSepArg(missingComps)]
                     infoMsg = getNotification(ADDON_COMPONENT_REMOVED_ID,
                                               ADDON_COMPONENT_REMOVED_ID,
                                               msgArgs=msgArgs)
                     infoMsgs.append(infoMsg)
         else:
            # There is an addon to install.
            status = NON_COMPLIANT

      elif currentAddOnInfo:
         # When addon is installed and is not present in the desired state,
         # we have tentative non-compliant.
         currentAddOnName = currentAddOnInfo.name
         status = NON_COMPLIANT
         msgArgs = [currentAddOnInfo.name, currentAddOnInfo.version]
         infoMsg = getNotification(ADDON_REMOVAL_ID,
                                   ADDON_REMOVAL_ID,
                                   msgArgs=msgArgs)
         infoMsgs.append(infoMsg)


      if targetAddOnInfo and status != INCOMPATIBLE:
         # Check the addon in the target image does not cause any component
         # downgrade.
         oldUnit = (currentAddOnObj if currentAddOnName == targetAddOnName
                    else None)
         status, errMsgs = self._amendReleaseUnitDowngradeCompliance(
                              targetAddOnObj,
                              oldUnit,
                              ADDON,
                              status,
                              curErrs=errMsgs)

      notifications = Notifications(info=getOptionalVal(infoMsgs),
                                    errors=getOptionalVal(errMsgs))
      addOnCompliance = AddOnCompliance(status=ComplianceStatus(status),
                                        current=currentAddOnInfo,
                                        target=targetAddOnInfo,
                                        notifications=notifications)
      return addOnCompliance

   def computeHardwareSupportCompliance(self):
      """Compute hardware support compliance status, return a map of
         HardwareSupportPackageCompliance objects indexed by hardware
         support manager names.
      """
      def _getManifestCompsCompliance(manifest):
         """Checks compliance of components in a manifest on the host, used
            when the desired manifest is already present on host.
            Returns a compliance status (one of compliant and non-compliant)
            and a list of info messages.
         """
         # Comp groups of the manifest related to the host.
         upComps, addComps = set(), set()

         # Set of user removed components
         removedComps = set()

         hsi = manifest.hardwareSupportInfo
         installedHsmComps = \
            self.currentSoftwareScanSpec[HARDWARE_SUPPORT][hsi.manager.name]

         for comp, version in manifest.components.items():
            if comp in installedHsmComps:
               status, _ = getVersionCompliance(version,
                                                installedHsmComps[comp],
                                                comp,
                                                COMPONENT)
               if status == NON_COMPLIANT:
                  # Component on host is a downgraded version.
                  upComps.add((comp, version))
               # If the host has a higher version of the component, it will
               # be classified as an user component and a compliance check would
               # be done separately.
            elif self._isRemovedComponentMissing(self._hostObsoletedComps,
                                                 comp, version,
                                                 HARDWARE_SUPPORT):
               # Component is missing, and it is not due to obsolete.
               addComps.add((comp, version))

            if PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED and \
                  comp in self.desiredImageProfile.removedComponents:
               # We do not really care about the version of removed components.
               # It is useful to do set operations below on upComps and addComps
               removedComps.add((comp, version))

         infoMsgs = []
         if upComps or addComps:
            status = NON_COMPLIANT

            if PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED:
               comps = (upComps | addComps) - removedComps
            else:
               comps = upComps | addComps

            msgArgs = [getCommaSepArg([self._getCompUiStrs(n, v)[0]
                                       for n, v in comps])]
            infoMsg = getNotification(HSP_COMPONENT_REMOVED_ID,
                                      HSP_COMPONENT_REMOVED_ID,
                                      msgArgs=msgArgs)
            infoMsgs.append(infoMsg)
         else:
            status = COMPLIANT

         return status, infoMsgs

      def _getFinalHspCompliance(hostManifest, targetManifest, curStatus,
                                 sameHsmHsp=False):
         """With a tentative compliant/non-compliant HSP status:
            1) Check component downgrade by the target manifest, if downgrade
               exists compliance will turn incompatible.
            2) If tentative status is compliant, and check 1 does not turn up
               an error, check whether the HSP is fully installed, if not
               compliance will turn non-compliant.
         """
         curStatus, errMsgs = self._amendReleaseUnitDowngradeCompliance(
                                 targetManifest,
                                 hostManifest if sameHsmHsp else None,
                                 HARDWARE_SUPPORT,
                                 curStatus)

         infoMsgs = []
         if curStatus == COMPLIANT:
            # Even with the same HSM-HSP, we could still have different
            # components across manifests. We will only verify the current
            # manifest on host is fully installed (ignore incompatible
            # component due to user component overwrite).
            curStatus, infoMsgs = _getManifestCompsCompliance(hostManifest)

         notifications = Notifications(info=getOptionalVal(infoMsgs),
                                       errors=getOptionalVal(errMsgs))
         return getHardwareSupportCompliance(curStatus,
                                             hostManifest,
                                             targetManifest,
                                             notifications)

      hspCompliance = dict()
      targetManifests = self.desiredImageProfile.manifests
      hostManifests = self.hostImageProfile.manifests

      targetManifestMap = {m.hardwareSupportInfo.manager.name: m
                           for m in targetManifests.values()}
      hostManifestMap = {m.hardwareSupportInfo.manager.name: m
                         for m in hostManifests.values()}
      targetHsms = set(targetManifestMap.keys())
      hostHsms = set(hostManifestMap.keys())

      for hsm in targetHsms - hostHsms:
         # New HSMs
         hspCompliance[hsm] = _getFinalHspCompliance(
                                 None,
                                 targetManifestMap[hsm],
                                 NON_COMPLIANT)

      for hsm in hostHsms - targetHsms:
         # HSMs pending removal. Downgrades are checked in other release units
         # and user components.
         hsiPkg = hostManifestMap[hsm].hardwareSupportInfo.package
         infoMsg = getNotification(HSP_REMOVAL_ID,
                                   HSP_REMOVAL_ID,
                                   msgArgs=[hsiPkg.name, hsiPkg.version])
         hspCompliance[hsm] = getHardwareSupportCompliance(
                                 NON_COMPLIANT,
                                 hostManifestMap[hsm],
                                 None,
                                 Notifications(info=[infoMsg]))

      for hsm in targetHsms & hostHsms:
         # Installed or partially installed HSMs.
         hostManifest, targetManifest = \
                                    hostManifestMap[hsm], targetManifestMap[hsm]
         hostHsp, targetHsp = (hostManifest.hardwareSupportInfo.package,
                               targetManifest.hardwareSupportInfo.package)
         if hostHsp.name == targetHsp.name:
            # Same HSM and HSP name.
            hspStatus, errMsg = getVersionCompliance(targetHsp.version,
                                                     hostHsp.version,
                                                     hostHsp.name,
                                                     HARDWARE_SUPPORT)

            if hspStatus == INCOMPATIBLE:
               notifications = Notifications(errors=[errMsg])
               hspCompliance[hsm] = getHardwareSupportCompliance(
                                       hspStatus,
                                       hostManifest,
                                       targetManifest,
                                       notifications)
            else:
               hspCompliance[hsm] = _getFinalHspCompliance(hostManifest,
                                                           targetManifest,
                                                           hspStatus,
                                                           sameHsmHsp=True)
         else:
            hspCompliance[hsm] = _getFinalHspCompliance(hostManifest,
                                                        targetManifest,
                                                        NON_COMPLIANT)

      return hspCompliance

   def computeSolutionCompliance(self):
      """Compute solution compliance and return a dict of SolutionCompliance
         objects indexed by solution names.
      """
      stageStatus = None
      hostSolutionCompliance = self._computeSolutionCompliance(
                                             self.hostImageProfile,
                                             self.currentSoftwareScanSpec,
                                             True)

      if not STAGING_SUPPORTED:
         return (hostSolutionCompliance, stageStatus)

      stageSolutionCompliance = self._computeSolutionCompliance(
                                             self.stagedImageProfile,
                                             self.stagedSoftwareSpec,
                                             False)

      imageStaged = bool(self.stagedImageProfile)

      for name in hostSolutionCompliance:
         if hostSolutionCompliance[name].status == NON_COMPLIANT:
            if not imageStaged:
               hostSolutionCompliance[name].stage_status = NOT_STAGED
               stageStatus = NOT_STAGED
            # If the solution is not present in stageSolutionCompliance
            # it means it isn't present in either staged image or
            # desired image making it implicitly staged
            elif not name in stageSolutionCompliance or \
               stageSolutionCompliance[name].status == COMPLIANT:
               hostSolutionCompliance[name].stage_status = STAGED
               if stageStatus == None:
                  stageStatus = STAGED
            else:
               hostSolutionCompliance[name].stage_status = NOT_STAGED
               stageStatus = NOT_STAGED
         else:
            if name in stageSolutionCompliance and \
               stageSolutionCompliance[name].status != COMPLIANT:
               stageStatus = NOT_STAGED

      if stageSolutionCompliance.keys() - hostSolutionCompliance.keys():
         stageStatus = NOT_STAGED

      return (hostSolutionCompliance, stageStatus)

   def _computeSolutionCompliance(self, imageProfile, softwareScanSpec,
                                  addNotifications):
      """Compute solution compliance for the given imageProfile and
         softwareScanSpec and return a dict of SolutionCompliance
         objects indexed by solution names. Notifications would
         be added only of addNotifications is True.
      """
      solCompliance = dict()

      if not imageProfile:
         return solCompliance

      getSolDict = lambda p: {s.nameSpec.name: s for s in p.solutions.values()}

      curSolutionsInfo = softwareScanSpec[SOLUTIONS]
      curSolutions = getSolDict(imageProfile)
      newSolutions = getSolDict(self.desiredImageProfile)
      newSolutionComps, _ = self.desiredImageProfile.GetSolutionInfo()
      allCurComps = imageProfile.components
      allNewComps = self.desiredImageProfile.components

      # Contains name, version tuples of solutions enabled/disabled.
      # This is for generating overall solution notifications.
      enableSols, disableSols = list(), list()

      curSols, newSols = set(curSolutions.keys()), set(newSolutions.keys())
      removes = curSols - newSols
      adds = newSols - curSols
      common = curSols & newSols

      for name in removes:
         curInfo = curSolutionsInfo[name]
         disableSols.append((curInfo.details.display_name,
                             curInfo.details.display_version))
         solCompliance[name] = SolutionCompliance(
                                       status=ComplianceStatus(NON_COMPLIANT),
                                       current=curInfo,
                                       notifications=Notifications())

      for name in adds:
         solution = newSolutions[name]
         newInfo = getSolutionInfo(solution,
                                   newSolutionComps[solution.releaseID])
         # Check if the new solution will downgrade another solution component.
         # This should not happen unless a solution is renamed.
         errors = self._getReleaseUnitCompDowngradeMsgs(
                     solution,
                     SOLUTION)
         if errors:
            status = INCOMPATIBLE
            notifications = Notifications(errors=errors)
         else:
            status = NON_COMPLIANT
            notifications = Notifications()
            enableSols.append((newInfo.details.display_name,
                               newInfo.details.display_version))

         solCompliance[name] = SolutionCompliance(
                                       status=ComplianceStatus(status),
                                       target=newInfo,
                                       notifications=notifications)

      for name in common:
         curInfo = curSolutionsInfo[name]
         newSolution = newSolutions[name]
         newInfo = getSolutionInfo(newSolution,
                                   newSolutionComps[newSolution.releaseID])

         # Whether version has changed will help us determine if there is a
         # metadata-only update, and that if solution disable/enable messages
         # will be shown. A pure version string comparison will suffice.
         solutionVerChanged = newInfo.version != curInfo.version
         status = NON_COMPLIANT if solutionVerChanged else COMPLIANT

         warnMsgs = []
         # Check downgrades of components.
         errMsgs = self._getReleaseUnitCompDowngradeMsgs(
                     newSolution,
                     SOLUTION)

         if errMsgs:
            status = INCOMPATIBLE
         else:
            # Look into compliance of each component. Components that are
            # newly added, removed or updated will cause the status to be
            # non-compliant. If solution version changes, we will still
            # need to separately report removed components.
            curComps = {c.component: allCurComps.GetComponent(c.component)
                        for c in curInfo.components}
            newComps = {c.component: allNewComps.GetComponent(c.component)
                        for c in newInfo.components}
            compAdded = bool(set(newComps.keys()) - set(curComps.keys()))
            rmCompArgs = set()
            compUpdated, compDowngraded = False, False
            for compName, comp in curComps.items():
               if compName not in newComps:
                  # Previous solution component removed.
                  rmCompArgs.add(comp.compUiStr)
               elif comp.compVersion > newComps[compName].compVersion:
                  compDowngraded = True
               elif comp.compVersion < newComps[compName].compVersion:
                  compUpdated = True

            if status == COMPLIANT:
               if compUpdated or compDowngraded or compAdded or rmCompArgs:
                  # Any component change when solution version is the same.
                  status = NON_COMPLIANT
               # As we don't have partial solution enable/disable message,
               # don't show anything that is confusing.
            else:
               # Solution version changed, both enable and disable messages
               # are shown.
               disableSols.append((curInfo.details.display_name,
                                   curInfo.details.display_version))
               enableSols.append((newInfo.details.display_name,
                                  newInfo.details.display_version))

            # Removed components of the soltuion.
            if rmCompArgs:
               warnMsgs.append(
                  getNotification(SOLUTIONCOMPONENT_REMOVAL_ID,
                                  SOLUTIONCOMPONENT_REMOVAL_ID,
                                  msgArgs=[getCommaSepArg(rmCompArgs),
                                           name],
                                  type_=WARNING))

         notifications = Notifications(warnings=getOptionalVal(warnMsgs),
                                       errors=getOptionalVal(errMsgs))
         solCompliance[name] = SolutionCompliance(
                                          status=ComplianceStatus(status),
                                          current=curInfo,
                                          target=newInfo,
                                          notifications=notifications)

      if disableSols and addNotifications:
         msgArgs = [getCommaSepArg(['%s %s' % (n, v) for n, v in disableSols])]
         infoMsg = getNotification(SOLUTION_DISABLE_ID,
                                   SOLUTION_DISABLE_ID,
                                   msgArgs=msgArgs)
         self.overallNotifications.info.append(infoMsg)
      if enableSols and addNotifications:
         msgArgs = [getCommaSepArg(['%s %s' % (n, v) for n, v in enableSols])]
         infoMsg = getNotification(SOLUTION_ENABLE_ID,
                                   SOLUTION_ENABLE_ID,
                                   msgArgs=msgArgs)
         self.overallNotifications.info.append(infoMsg)

      return solCompliance

   def _computeImpactForSolution(self, solutionImpacts):
      """WCP/NSX components requires host to be in maintenance mode in
         following scenarios.
         1. Removal of NSX components in desired image.
         2. Upgrade/downgrade of WCP/NSX components.
         Other scenarios like WCP/NSX components install or removal of
         WCP component has no impact.

         For LiveUpdate feature, WCP requires partial maintenance mode in the
         cases above; Add (solution ID, partial/full maintenance mode ID)
         key/value pairs into the out parameter 'solutionImpacts'.
      """

      def _getComponentSolutionName(componentName):
         """
         Get the solution name if the component is part of the desired image
         profile solutions else returns None.
         """
         solComponentDict, _ = \
             self.desiredImageProfile.GetSolutionInfo()
         for solId, compList in solComponentDict.items():
             for c in compList:
                 if c.compNameStr == componentName:
                     solution = self.desiredImageProfile.solutions[solId]
                     return solution.nameSpec.name
         return None

      hostNsx, hostWcp = _getNsxWcpComps(self.hostImageProfile.components)
      desiredNsx, desiredWcp = \
         _getNsxWcpComps(self.desiredImageProfile.components)

      impact, mmode = IMPACT_NONE, None

      # Computes NSX impact
      # NSX upgrade/downgrade/removal requires mmode
      if (hostNsx and desiredNsx and (hostNsx.compVersion
          != desiredNsx.compVersion)) or (hostNsx and not desiredNsx):
         if LIVE_UPDATE_SUPPORTED:
            solutionName = _getComponentSolutionName(hostNsx.compNameStr)
            if isNotNone(solutionName) and solutionName not in solutionImpacts:
               solutionImpacts[solutionName] = FULL_MAINTENANCE_MODE
         impact, mmode = IMPACT_MMODE, MAINTMODE_IMPACT_ID

      if hostWcp and desiredWcp and (hostWcp.compVersion
         != desiredWcp.compVersion):
         if LIVE_UPDATE_SUPPORTED:
            # WCP upgrade/downgrade requires partial mmode for LiveUpdate
            solutionName = _getComponentSolutionName(desiredWcp.compNameStr)
            if isNotNone(solutionName) and solutionName not in solutionImpacts:
               solutionImpacts[solutionName] = PARTIAL_MAINTENANCE_MODE_WCP
            if impact == IMPACT_NONE:
               return IMPACT_PARTIAL_MMODE, PARTIAL_MAINTMODE_IMPACT_ID
            return impact, mmode
         else:
            # WCP upgrade/downgrade requires mmode
            return IMPACT_MMODE, MAINTMODE_IMPACT_ID

      return impact, mmode

   def computeImageImpact(self):
      """Compute image impact and generate a notification for the impact.
         Returns a tuple of:
            * Overall impact, one of REBOOT_IMPACT_ID, MAINTMODE_IMPACT_ID,
              PARTIAL_MAINTMODE_IMPACT_ID and IMPACT_NONE.
            * A dictionary of solution impacts.
            * Partial maintenance mode name when impact is
              PARTIAL_MAINTMODE_IMPACT_ID.
         Side effects:
         * In case Quick Patch is incompatible due to DPU/TPM or live component/
           solution, add overall error notification(s).
         * An overall notification is added for the image impact.
         * In case the host is pending reboot from a prior software change,
           an info notification will be generated and added to overall
           notification.
         * For a Quick Patch desired image, the scan scripts' ScanScriptOutput
           instances are saved for later processing.
      """
      # Impact of the remediation.
      impactParams = _getImageImpactParameters(
         self.hostImage, self.desiredImageProfile, self._enableQuickPatch,
         self._doQuickPatchScan)
      impact = impactParams.impact
      pendingRebootNotification = impactParams.pendingRebootNotification

      # Below 2 will be None when Quick Patch is not enabled or impossible.
      pmmName = impactParams.partialMModeName
      self._quickPatchScriptResults = impactParams.quickPatchScriptResults

      impactId = None
      solutionImpacts = dict()

      if impact != IMPACT_NONE:
         impactId = (MAINTMODE_IMPACT_ID if impact == IMPACT_MMODE else
            PARTIAL_MAINTMODE_IMPACT_ID if impact == IMPACT_PARTIAL_MMODE else
            REBOOT_IMPACT_ID)
      if pendingRebootNotification:
         impactId = PENDING_REBOOT_ID
         self.overallNotifications.info.append(pendingRebootNotification)

      # Compute impact from NSX/WCP components.
      # XXX: if solutions return PMM names, need to check conflict with Quick
      # Patch.
      solImpact, solImpactId = \
            self._computeImpactForSolution(solutionImpacts)

      # Solutions can cause impact to become PMM or MMode.
      if (impact == IMPACT_NONE or
          (impact == IMPACT_PARTIAL_MMODE and solImpact == IMPACT_MMODE)):
         impact, impactId = solImpact, solImpactId

      if ALLOW_DPU_OPERATION:
         impact, impactId = self.mergeDPUImpacts(impact, impactId)

      if impactId and impactId != PENDING_REBOOT_ID:
         msgDict = getNotification(impactId, impactId)
         self.overallNotifications.info.append(msgDict)

      if pmmName:
         if impact != IMPACT_PARTIAL_MMODE:
            # PMM has been overwritten by solution or DPU.
            pmmName = None
         else:
            logging.info("Partial maintenance mode '%s' impact will be "
                         "returned", pmmName)

      if self._enableQuickPatch:
         # Handle Quick Patch eligible/incompatible notifications.

         # Solution may have changed the impact from live/PMM to MMode.
         liveOrPmm = impact in (IMPACT_NONE, IMPACT_PARTIAL_MMODE)

         if impactParams.quickPatchIncompatNotifs:
            # If the image is Quick Patch eligible but DPU/TPM or live
            # component/solution causes Quick Patch to be incompatible, add the
            # notifications as errors. DPU/TPM errors are added even when impact
            # is MMode, while live component/solution errors are only added with
            # live and PMM impact.

            # Reverse the order to get hardware/setup issues first before
            # software ones.
            for n in reversed(impactParams.quickPatchIncompatNotifs):
               if (not liveOrPmm and
                   n.id in (QP_LIVE_COMP_UNSUPPORTED_ID,
                            QP_LIVE_SOLUTION_UNSUPPORTED_ID)):
                  # In case a solution changed impact from live to MMode, skip
                  # the live component/solution warnings.
                  continue

               self.overallNotifications.errors.insert(0, n)

               # Image impact does not handle compliance, set the flag for Quick
               # Patch compliance to return incompatible.
               self._isQuickPatchIncompatible = True

         if liveOrPmm and impactParams.quickPatchInfo:
            # Place Quick Patch eligible at the front of information when the
            # impact is live or PMM and there is no live component/solution
            # incompatibility.
            self.overallNotifications.info.insert(
               0, impactParams.quickPatchInfo)

      return impact, solutionImpacts, pmmName

   def computeRemovedCompsCompliance(self):
      """
      Computes the removed components image compliance.
      Returns the overall staged status along with host compliance.
      """

      def _computeRemovedCompsCompliance(imageProfile, softwareScanSpec):
         """
         Compute user removed component compliance data by looking at the
         current and the desired image.
         Returns a dictionary of ComponentComplianceInfo indexed by component
         names.
         """
         removedCompsCompliance = dict()

         if not imageProfile:
            return removedCompsCompliance

         # List of component names to be removed.
         removedDesiredCompNames = self.desiredImageProfile.removedComponents
         if not removedDesiredCompNames:
            # No components to scan
            return removedCompsCompliance

         ipComponents = imageProfile.components
         hostComps = self._compSummaryToInfo(
                        imageProfile.ListComponentSummaries(removeDup=True))

         biComps = softwareScanSpec.get(BASEIMAGE_COMPS_KEY, set())
         addOnComps = softwareScanSpec.get(ADDON_COMPS_KEY, set())
         userComps = softwareScanSpec.get(USER_COMPS_KEY, set())

         for removedCompName in removedDesiredCompNames:
            if not ipComponents.HasComponent(removedCompName):
               # Component does not exist on the host
               notifications = None
               if removedCompName not in imageProfile.removedComponents:
                  status = NON_COMPLIANT

                  # Find component display name.
                  # Unless the removed component was a user component, it is in
                  # reserved components.
                  highResComps = self.desiredImageProfile.reservedComponents.\
                     GetHighestVerComps()
                  if highResComps.HasComponent(removedCompName):
                     compUiName = \
                        highResComps.GetComponent(removedCompName).compNameUiStr
                  elif COMPONENTS in self.swSpec and self.swSpec[COMPONENTS] \
                        and removedCompName in self.swSpec[COMPONENTS]:
                     # Fetch version from software spec.
                     rmComp = self._depotMgr.components.GetComponent(
                        removedCompName,
                        self.swSpec[COMPONENTS][removedCompName])
                     compUiName = rmComp.compNameUiStr
                  else:
                     # Should not happen, but fallback to use regular name.
                     compUiName = removedCompName

                  infoMsg = getNotification(USER_REMOVED_COMPONENT_ID,
                                            USER_REMOVED_COMPONENT_ID,
                                            msgArgs=[compUiName])
                  notifications = Notifications(info=[infoMsg])
               else:
                  status = COMPLIANT

               removedCompsCompliance[removedCompName] = \
                  self._getCompCompliance(status, None, None, None,
                                          SOURCE_USER_REMOVED, notifications)
               continue

            # User removed component is part of the image and is pending removal
            status = NON_COMPLIANT

            current = hostComps[removedCompName]
            compUiName = current.details.display_name

            # Maintain a separate instance for target
            from copy import deepcopy
            target = deepcopy(current)

            # Set the version and display_version of the target to None so we
            # do not show a target version for the removed component. The
            # notification added above will provide the needed info.
            target.version = None
            target.details.display_version = None

            # Set the current source of the component in the order of overriding
            # capability.
            # IMAGE_CUST_TODO: HSP should ideally be after user components and
            # before addon as it can override the addon and base image.
            if removedCompName in userComps:
               currSource = SOURCE_USER
               infoMsg = getNotification(USER_REMOVED_COMP_USER_ID,
                                         USER_REMOVED_COMP_USER_ID,
                                         msgArgs=[compUiName])
            elif removedCompName in addOnComps:
               currSource = SOURCE_ADDON
               infoMsg = getNotification(USER_REMOVED_COMP_ADDON_ID,
                                         USER_REMOVED_COMP_ADDON_ID,
                                         msgArgs=[compUiName])
            elif removedCompName in biComps:
               currSource = SOURCE_BASEIMAGE
               infoMsg = getNotification(USER_REMOVED_COMP_BI_ID,
                                         USER_REMOVED_COMP_BI_ID,
                                         msgArgs=[compUiName])
            else:
               currSource = SOURCE_HSP
               infoMsg = getNotification(USER_REMOVED_COMP_HSP_ID,
                                         USER_REMOVED_COMP_HSP_ID,
                                         msgArgs=[compUiName])

            notifications = Notifications(info=[infoMsg])
            removedCompsCompliance[removedCompName] = \
               self._getCompCompliance(status, current, target, currSource,
                                       SOURCE_USER_REMOVED, notifications)

         return removedCompsCompliance

      stageStatus = None
      hostComponentsCompliance = \
         _computeRemovedCompsCompliance(self.hostImageProfile,
                                        self.currentSoftwareScanSpec)

      stagedComponentsCompliance = \
         _computeRemovedCompsCompliance(self.stagedImageProfile,
                                        self.currentSoftwareScanSpec)

      for compName, compliance in hostComponentsCompliance.items():
         if compliance.status in (INCOMPATIBLE, UNAVAILABLE):
            stageStatus = None
            break
         if compliance.status == COMPLIANT:
            continue

         compStgCompliance = stagedComponentsCompliance.get(compName, None)

         if compStgCompliance and compStgCompliance.status == COMPLIANT:
            hostComponentsCompliance[compName].stage_status = STAGED
            # Mark as STAGED only if no other component has set the stageStatus
            # to NOT_STAGED. If it has, overall stageStatus should remain
            # NOT_STAGED.
            if stageStatus is None:
               stageStatus = STAGED
         else:
            hostComponentsCompliance[compName].stage_status = NOT_STAGED
            stageStatus = NOT_STAGED

      return (hostComponentsCompliance, stageStatus)

   def computeComponentsCompliance(self):
      """
      Computes the component image compliance by comparing the host image vs
      desired image and staged image vs desired image.It populates the staged
      status in the host image compliance and returns the overall staged
      status along with the host compliance
      """
      stageStatus = None
      hostComponentsCompliance = \
         self._computeComponentsCompliance(self.hostImageProfile,
            self.currentSoftwareScanSpec,
            self.userCompDowngrades,
            self.hostCompSource)
      if not STAGING_SUPPORTED:
         return (hostComponentsCompliance, stageStatus)

      stagedComponentsCompliance = \
         self._computeComponentsCompliance(self.stagedImageProfile,
            self.stagedSoftwareSpec,
            self.stageCompDowngrades,
            self.stageCompSource)

      imageStaged = bool(self.stagedImageProfile)

      # If any host component status is Incompatible or Unavailable, then do
      # not populate stage status entirely, else if any component is not
      # staged, mark the overall status as not staged
      for compname, compliance in hostComponentsCompliance.items():
         if compliance.status in (INCOMPATIBLE, UNAVAILABLE):
            stageStatus = None
            break
         if compliance.status == COMPLIANT:
            continue

         compStgCompliance = stagedComponentsCompliance.get(compname, None)
         if not imageStaged:
            hostComponentsCompliance[compname].stage_status = NOT_STAGED
            stageStatus = NOT_STAGED
         # If the component is not present in compStgCompliance it means it
         # it isn't present in either staged image or desired image making it
         # implicitly staged
         elif not compStgCompliance or compStgCompliance.status == COMPLIANT:
            hostComponentsCompliance[compname].stage_status = STAGED
            if stageStatus is None:
               stageStatus = STAGED
         else:
            hostComponentsCompliance[compname].stage_status = NOT_STAGED
            stageStatus = NOT_STAGED

      # If there are any extra components staged mark the overall stage status
      # as not staged
      if stagedComponentsCompliance.keys() - hostComponentsCompliance.keys():
         stageStatus = NOT_STAGED

      return (hostComponentsCompliance, stageStatus)

   def _computeComponentsCompliance(self, imageProfile, softwareScanSpec,
         userCompDowngrades, compSource):
      """Compute user component compliance data by looking at user overriden
         components in the current and the desired image. This excludes
         components that are a part of solutions.
         Returns a dictionary of ComponentComplianceInfo indexed by component
         names.
      """
      if not imageProfile or not softwareScanSpec:
         return dict()

      def _getCompVerStatus(name, hostCompInfo, targetCompInfo,
                            userCompDowngrades):
         """Gets component version compliance status for an user component
            that is present on host and in the desired image.
            Returns a compliance status (one of incompatible, non-compliant and
            compliant), and a Notifications object with message or None.
         """
         if name in userCompDowngrades:
            # User component downgrade, this excludes duplicate target
            # component that is in both a release unit and user component
            # section of the desired spec.
            v1, v2, _, dest, configDowngrade = userCompDowngrades[name]

            # XXX: in 7.0 U1, still disallow all downgrades, not only those
            #      with config schema.

            #if configDowngrade:

            # Unsupported downgrade - incompatible.
            if dest == SOURCE_USER:
               # However, notification is always in the destination entity to
               # avoid duplicate info.
               # Thus, only adding notification if the destination is an user
               # component.
               uiName, uiVer1, _ = self._getCompUiStrs(name, v1)
               uiVer2 = self._getCompUiStrs(name, v2)[1]
               msg = getNotification(COMPONENT_DOWNGRADE_ID,
                                     COMPONENT_DOWNGRADE_ID,
                                     msgArgs=[uiVer2, uiName, uiVer1],
                                     type_=ERROR)
               return INCOMPATIBLE, Notifications(errors=[msg])
            else:
               return INCOMPATIBLE, None

            #else:
            #   # Supported downgrade - non-compliant
            #   return NON_COMPLIANT, None

         # Upgrade or new addition - non-compliant;
         # if the target component is a downgrade but the same component is
         # present in both release unit and user component, we will catch it
         # here as incompatible;
         # otherwise - compliant.
         if hostCompInfo is None:
            return NON_COMPLIANT, None
         else:
            status, err = getVersionCompliance(targetCompInfo.version,
                                               hostCompInfo.version,
                                               name,
                                               COMPONENT)
            return status, Notifications(errors=[err]) if err else None

      desiredUserComps = dict()
      if COMPONENTS in self.swSpec and self.swSpec[COMPONENTS]:
         desiredUserComps = self.swSpec[COMPONENTS]

      hostUserComps = self._compSummaryToInfo(
                                 softwareScanSpec.get(USER_COMPS_KEY, ()))

      compCompliance = dict()
      if not hostUserComps and not desiredUserComps:
         # No components to scan.
         return compCompliance

      hostComps = self._compSummaryToInfo(
                           imageProfile.ListComponentSummaries(removeDup=True))
      hostSolComps = set([c.component for s in
                          softwareScanSpec[SOLUTIONS].values()
                          for c in s.components])
      desiredComps = self._compSummaryToInfo(
                           self.desiredImageProfile.ListComponentSummaries())
      desiredUserComps = {key: value for key, value in desiredComps.items()
                          if key in desiredUserComps}

      for name, comp in desiredUserComps.items():
         # Skip the components which are marked for removal by the user.
         # Compliance for such components will be done separately.
         if PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED and \
            name in self.desiredImageProfile.removedComponents:
            continue

         # Loop through ComponentInfo of desired components and find which
         # installed components are added/updated/downgraded/unchanged.
         if name in hostComps:
            if name in hostSolComps:
               continue
            # The user component is present on host, check version compliance.
            current = hostComps[name]
            currentSource = compSource[name]
            status, notifications = _getCompVerStatus(name, current, comp,
                                                      userCompDowngrades)
         else:
            # Component not present on host.
            current, currentSource, notifications = None, None, None
            status = NON_COMPLIANT

         compCompliance[name] = self._getCompCompliance(status,
                                                        current,
                                                        comp,
                                                        currentSource,
                                                        SOURCE_USER,
                                                        notifications)

      for name, comp in hostUserComps.items():
         # Loop through ComponentInfo of the installed user components and find
         # any components that will be removed/downgraded/unchanged by a release
         # unit.
         if name in hostSolComps:
            continue

         if name not in desiredComps:
            # The component does not appear in the desired component list
            # means it is removed.
            infoMsg = getNotification(COMPONENT_REMOVAL_ID,
                                      COMPONENT_REMOVAL_ID,
                                      msgArgs=[comp.details.display_name])
            notifications = Notifications(info=[infoMsg])
            compCompliance[name] = self._getCompCompliance(NON_COMPLIANT,
                                                           hostComps[name],
                                                           None,
                                                           SOURCE_USER,
                                                           None,
                                                           notifications)
         elif name in desiredComps and not name in desiredUserComps:
            # The component merges into base image, addon, HSP or solution.
            target = desiredComps[name]
            targetSource = self.desiredCompSource[name]

            status, notifications = _getCompVerStatus(name, comp, target,
                                                      userCompDowngrades)
            compCompliance[name] = self._getCompCompliance(status,
                                                           comp,
                                                           target,
                                                           SOURCE_USER,
                                                           targetSource,
                                                           notifications)

      return compCompliance

   def computeOrphanVibCompliance(self):
      """Compute orphan VIB compliance and returns a compliant status, which
         is one of compliant, non-compliant and incompatible.
         Side effect: a warning notification will be added for orphan VIBs
         to be removed or downgraded without config schema impact, an error
         notification will be added for orphan VIBs to be downgraded with
         cofig schema impact.
      """
      def _formVibMsg(msgId, vibDict, type_):
         vibDetails = ['%s(%s)' % (name, version) for name, version in
                       vibDict.items()]
         return getNotification(msgId, msgId,
                                msgArgs=[getCommaSepArg(vibDetails)],
                                type_=type_)

      hostOrphanVibIds = self.currentSoftwareScanSpec[ORPHAN_VIBS]
      if not hostOrphanVibIds:
         return COMPLIANT

      # The host is non-compliant as there must be at least some metadata
      # changes.
      status = NON_COMPLIANT

      hostVibs = self.hostImageProfile.vibs
      desiredVibs = self.desiredImageProfile.vibs

      # We only need to look deeper into orphan VIBs that are not in the
      # desired image to generate notifications.
      orphanVibs = [hostVibs[vibId] for vibId in hostOrphanVibIds
                    if not vibId in desiredVibs]
      if orphanVibs:
         allVibs = VibCollection()
         allVibs += desiredVibs
         for vib in orphanVibs:
            allVibs.AddVib(vib)

         dgVibs, removedVibs = dict(), dict()
         scanResult = allVibs.Scan()
         for vib in orphanVibs:
            if scanResult.vibs[vib.id].replaces:
               # VIB being downgraded.
               for replace in scanResult.vibs[vib.id].replaces:
                  # XXX: in 7.0.1, still disallow all downgrades, not only those
                  #      with config schema.
                  #if allVibs[replace].hasConfigSchema and vib.hasConfigSchema:
                  dgVibs[vib.name] = vib.versionstr
                  break
            elif not scanResult.vibs[vib.id].replacedBy:
               # VIB is not replaced and will be removed when remediating.
               removedVibs[vib.name] = vib.versionstr

         if dgVibs:
            status = INCOMPATIBLE
            self.overallNotifications.errors.append(
               _formVibMsg(VIB_DOWNGRADE_ID, dgVibs, ERROR))

         if removedVibs:
            self.overallNotifications.warnings.append(
               _formVibMsg(VIB_REMOVAL_ID, removedVibs, WARNING))

      return status

   def _getQuickPatchTardisksSize(self):
      """Returns the uncompressed sizes of Quick Patch script/contents tardisks
         to be mounted during the remediation.
      """
      # Assumes image impact was already calculated and PMM will be used.
      # Which also means QuickPatchInstaller should be present.
      qpInstaller = self.hostImage.installers['quickpatch']

      totalMib = 0
      adds, _, _ = \
         qpInstaller.GetImageProfileVibDiff(self.desiredImageProfile)
      for add in adds:
         vib = self.desiredImageProfile.vibs[add]
         if not vib.isQuickPatchVib:
            continue
         for payload in vib.payloads:
            if not payload.isQuickPatchRelevant():
               continue
            if payload.uncompressedsize is None:
               raise VibFormatError(vib.id, 'Uncompressed size is required '
                  'for payload %s in Live Patch VIB %s'
                  % (payload.name, vib.id))
            totalMib += payload.uncompressedsize // MIB + 1
      return totalMib

   def _isQuickPatchScanNeeded(self):
      """Checks whether Quick Patch scan and impact calculation are needed. We
         only check target base image and some attributes/classes required for
         Quick Patch.
         Returns True if so, otherwise False.
      """
      # ESXio check
      if IS_ESXIO:
         logging.debug('Quick Patch is not supported on DPU, skipping '
                       'Quick Patch compliance')
         return False

      # Enablement check.
      if not QUICK_PATCH_SUPPORTED:
         logging.debug('Quick Patch is disabled or not supported by the '
            'settings_daemon binding, skipping Quick Patch compliance')
         return False

      # We only check the target image as the source image is not available yet.
      targetBi = self.desiredImageProfile.baseimage

      # The target base image must support Quick Patch.
      if not targetBi.isQuickPatch:
         logging.debug('The target base image %s is not compatible with '
                       'Quick Patch.', targetBi.versionSpec.version)
         return False

      imported = _importNewClassFromSettingsd((
         'ImpactDetails', 'MemoryReservation', 'RemediationAction',
         'RemediationDetails'))
      if not imported:
         logging.debug('One or more required class for Quick Patch cannot be '
                       'imported, skipping Quick Patch compliance')
         return False

      return True

   def computeQuickPatchCompliance(self, impact, pmmName):
      """Forms Quick Patch related structures, returns a tuple of: Quick
         patch action compliance status, an ImpactDetails instance, and a
         RemediationDetails instance. If the image cannot be Quick Patched,
         Nones will be returned.
      """
      # Can import in Python 3.7+ only.
      from .QuickPatchScriptLib import RemediationActionStatus

      REMEDIATION_STATUS_TO_COMPLIANCE = {
         RemediationActionStatus.COMPLIANT: COMPLIANT,
         RemediationActionStatus.NON_COMPLIANT: NON_COMPLIANT,
         # When Quick Patch is not possible, remediation action compliance
         # is not applicable.
         RemediationActionStatus.INCOMPATIBLE: None,
         RemediationActionStatus.UNKNOWN: None,
      }

      def convertScriptLocalizableMessage(msg):
         """Converts a LocalizableMessage from a script to settings_daemon
            LocalizableMessage.
         """
         if msg is None:
            return None
         return LocalizableMessage(
            id=msg.msgId,
            # Similar to getNotification(), default_message needs to be
            # formatted.
            default_message=getFormattedMessage(msg.defaultMessage, msg.args),
            args=msg.args)

      def convertScriptNotification(scriptName, n, type_):
         """Converts a Notification from a script to settings_daemon
            Notification.
         """
         scriptMsg = getFormattedMessage(n.message.defaultMessage,
                                         n.message.args)
         scriptRes = getFormattedMessage(n.resolution.defaultMessage,
            n.resolution.args) if n.resolution else ''
         nId = QP_SCRIPTMSG_TYPE_TO_WRAPPER_ID[type_]
         args = [QuickPatchScript.QP_SCRIPT_TYPE_SCAN, scriptName,
                 scriptMsg + ' ' + scriptRes if scriptRes else scriptMsg]
         return getNotification(nId, nId, msgArgs=args, type_=type_)

      def addScriptNotifications(scriptName, scriptNotifs, type_):
         """Adds notifications of a Quick Patch scan script to overall
            notifications.
         """
         if not scriptNotifs:
            return
         notifsList = \
            (self.overallNotifications.errors if type_ == ERROR else
             (self.overallNotifications.warnings
              if type_ == WARNING else self.overallNotifications.info))
         for scriptNotif in scriptNotifs:
            notifsList.append(
               convertScriptNotification(scriptName, scriptNotif, type_))

      # Skip populating Quick Patch fields when Quick Patch is not possible/
      # disabled (exclude incomplete remediation case), or no scan scripts have
      # been executed.
      # However, compliance will be set accordingly if Quick Patch is
      # incompatible.
      if not self._doQuickPatchScan or not self._quickPatchScriptResults:
         status = INCOMPATIBLE if self._isQuickPatchIncompatible else None
         return status, None, None

      # Process remediation actions, notifications, upgradeActions and
      # memoryReservations.
      overallCompliance = RemediationActionStatus.COMPLIANT
      upgradeActions = set()
      failedScripts = []
      remediationActions = []
      # Fetch the max mem reserved for the settingsd-task-forks group.
      # This is done to avoid changing tempRes every time there is a
      # bump/dip required for settingsd-task-forks
      tempRes = getMaxMemAllocation('host/vim/vmvisor/settingsd-task-forks')
      permRes = 0
      for vibScriptResults in self._quickPatchScriptResults.values():
         for scriptName, (rc, _, scriptRes) in vibScriptResults.items():
            if rc is None or scriptRes is None:
               # The script failed to execute or returned invalid output.
               failedScripts.append(scriptName)
               overallCompliance = RemediationActionStatus.UNKNOWN
               continue

            # Accumulate upgrade actions, remediation actions and reservations.
            if scriptRes.upgradeActions:
               for action in scriptRes.upgradeActions:
                  upgradeActions.add(action)
            if (scriptRes.remediationActionStatus.value >
                overallCompliance.value):
               overallCompliance = scriptRes.remediationActionStatus
            if scriptRes.remediationActions:
               for action in scriptRes.remediationActions.actionList:
                  remediationActions.append(
                     RemediationAction(
                        action=convertScriptLocalizableMessage(action)))
            if scriptRes.memoryReservations:
               tempRes += scriptRes.memoryReservations.temporaryReservation
               permRes += \
                  scriptRes.memoryReservations.permanentReservationIncrease
            # Add notifications to overallNotifications when Quick Patch is
            # enabled.
            if self._enableQuickPatch:
               addScriptNotifications(scriptName,
                  scriptRes.notifications.getInfoNotifications(), INFO)
               addScriptNotifications(scriptName,
                  scriptRes.notifications.getWarningNotifications(), WARNING)
               addScriptNotifications(scriptName,
                  scriptRes.notifications.getErrorNotifications(), ERROR)

      if failedScripts:
         # Add an error notification for script failure, even if Quick Patch
         # is not enabled.
         n = getNotification(QP_SCANSCRIPT_FAILURE_ID, QP_SCANSCRIPT_FAILURE_ID,
                             msgArgs=[getCommaSepArg(failedScripts)],
                             type_=ERROR)
         self.overallNotifications.errors.append(n)

      if not self._enableQuickPatch:
         if remediationActions:
            # When Quick Patch is disabled but there are pending actions due to
            # incomplete remediation, do not populate Quick Patch fields, but
            # still return NON_COMPLIANT.
            # Note: we will not reach here if the host has not been remediated
            # with the target Quick Patch version.
            return NON_COMPLIANT, None, None

         # At Quick Patch target version and is compliant.
         return COMPLIANT, None, None

      impactDetails, remediationDetails = None, None
      if pmmName:
         # Only in PMM, memory reservation, upgrade actions and PMM name will
         # be filled in.
         # Tardisk sizes will be added to permanent reservation as mounting will
         # use memory.
         permRes += self._getQuickPatchTardisksSize()
         memRes = MemoryReservation(temporary_reservation=tempRes,
                                    permanent_reservation_increase=permRes)
         upgradeActions = sorted(list(upgradeActions))
         impactDetails = ImpactDetails(
            memory_reservation=memRes,
            partial_maintenance_mode_name=pmmName,
            partial_maintenance_mode_upgrade_actions=upgradeActions,
            # Non-optional, formHostComplianceResult() will fill it in if
            # LiveUpdate is enabled.
            solution_impacts={})

      if impact != IMPACT_REBOOT and remediationActions:
         # As long as impact is not reboot, remediation actions will be
         # reflected.
         remediationDetails = \
            RemediationDetails(remediation_actions=remediationActions)

      # Convert to the general compliance status.
      # If Quick Patch is incompatible, the compliance will be set accordingly
      # even if the scripts may have returned normal results.
      status = (INCOMPATIBLE if self._isQuickPatchIncompatible else
                REMEDIATION_STATUS_TO_COMPLIANCE[overallCompliance])

      return status, impactDetails, remediationDetails

   def formHostComplianceResult(self, baseImageCompliance, addOnCompliance,
                                hspCompliance, componentsCompliance,
                                solutionsCompliance, orphanVibStatus,
                                stageStatus, removedCompsCompliance=None):
      """Computes the overall host compliance based on base image, addon,
         solutions, user component, orphan VIB, and Quick Patch compliance info.
         If any of the compliance computation fails then the status will be
         returned as UNAVAILABLE.
      """
      def _getOverallCompliance(compStatus):
         """Returns the overall compliance status, determined with the below
            order:
            * Unavailable: if any image entity compliance is unavailable.
            * Incompatible: if any image entity compliance is incompatible, or
                            upgrade precheck reported an error.
            * Non-compliant: if any image entity compliance is non-compliant.
            * Compliant: if none of the above applies.
         """
         overallStatus = COMPLIANT
         if UNAVAILABLE in compStatus:
            overallStatus = UNAVAILABLE
         elif INCOMPATIBLE in compStatus:
            overallStatus = INCOMPATIBLE
         elif NON_COMPLIANT in compStatus:
            overallStatus = NON_COMPLIANT
         return overallStatus

      def _getComplianceDetailsNotification(beforeQpStatus, overallStatus):
         """Returns the notification for compliance status details.
         """
         if overallStatus == NON_COMPLIANT and beforeQpStatus == COMPLIANT:
            # Special notification for non-compliant due to pending action.
            nId = (DPU_COMPLIANCE_QPACTION_NONCOMPLIANT_ID if IS_ESXIO
                   else HOST_COMPLIANCE_QPACTION_NONCOMPLIANT_ID)
         else:
            nId = (DPU_COMPLIANCE_DETAILS_ID[overallStatus] if IS_ESXIO
                   else HOST_COMPLIANCE_DETAILS_ID[overallStatus])
         nType = COMPLIANCE_TO_NOTIFICATION_TYPE[overallStatus]
         return getNotification(nId, nId, type_=nType)


      # Execute hardware precheck.
      try:
         if HostOSIsSimulator():
            # Skip hardware compatibilty check in simulators as there are unmet
            # assumptions in the environment, e.g locker partition do not exist.
            precheckCompatible = True
         else:
            precheckCompatible = self.checkHardwareCompatibility()
      except Exception as e:
         self.reportErrorResult('Failed to check hardware compatibility', e)

      # QuickBoot precheck
      try:
         self.checkQuickBootCompatibility()
      except Exception as e:
         # Unhandled error in QuickBoot precheck.
         self.reportErrorResult('Failed to execute QuickBoot precheck', e)

      try:
         impact, solutionImpacts, pmmName = self.computeImageImpact()
      except InstallerNotAppropriate as e:
         self.reportErrorResult('Failed to compute impact', e)

      complianceStatus = set()
      if baseImageCompliance:
         complianceStatus.add(str(baseImageCompliance.status))
      if addOnCompliance:
         complianceStatus.add(str(addOnCompliance.status))
      if hspCompliance:
         for value in hspCompliance.values():
            complianceStatus.add(str(value.status))
      if componentsCompliance:
         for value in componentsCompliance.values():
            complianceStatus.add(str(value.status))
      if solutionsCompliance:
         for value in solutionsCompliance.values():
            complianceStatus.add(str(value.status))
      if removedCompsCompliance:
         for value in removedCompsCompliance.values():
            complianceStatus.add(str(value.status))
      if isNotNone(orphanVibStatus):
         complianceStatus.add(orphanVibStatus)
      if not precheckCompatible:
         complianceStatus.add(INCOMPATIBLE)

      # Quick Patch and solution impacts.
      impactDetails, remediationDetails, compDetails, beforeQpStatus = \
         None, None, None, None
      if (QUICK_PATCH_SUPPORTED and
          (self._enableQuickPatch or self._doQuickPatchScan)):
         # Populate Quick Patch details.
         try:
            qpStatus, impactDetails, remediationDetails = \
               self.computeQuickPatchCompliance(impact, pmmName)
         except Exception as e:
            msg = "Failed to compute Live Patch compliance for the host"
            # This will write unknown compliance and quit.
            self.reportErrorResult(msg, e)

         if isNotNone(qpStatus):
            # Consider Quick Patch compliance status in the overall compliance.
            if self._enableQuickPatch:
               # When Quick Patch is enabled, Quick Patch non-compliant is
               # specially considered as it can alter the compliance status
               # message.
               beforeQpStatus = _getOverallCompliance(complianceStatus)
            complianceStatus.add(qpStatus)

      if LIVE_UPDATE_SUPPORTED:
         if isNotNone(impactDetails):
            # Solution details is part of ImpactDetails.
            impactDetails.solution_impacts = solutionImpacts
         else:
            # Only live update, no Quick Patch.
            impactDetails = ImpactDetails(solution_impacts=solutionImpacts)

      # Overall compliance status and compliance details message.
      overallStatus = _getOverallCompliance(complianceStatus)
      if overallStatus != NON_COMPLIANT:
         # Stage status is irrelevant when overall status is not non-compliant.
         stageStatus = None

      # Overall notifications, use None when no notification has been added for
      # a type.
      notifications = Notifications(
                  info=getOptionalVal(self.overallNotifications.info),
                  warnings=getOptionalVal(self.overallNotifications.warnings),
                  errors=getOptionalVal(self.overallNotifications.errors))

      hostCompliance = HostCompliance(impact=ComplianceImpact(impact),
                            status=ComplianceStatus(overallStatus),
                            notifications=notifications,
                            scan_time=datetime.utcnow(),
                            base_image=baseImageCompliance,
                            add_on=addOnCompliance,
                            hardware_support=hspCompliance,
                            components=componentsCompliance,
                            solutions=solutionsCompliance)

      # Legacy settingsd does not understand removed_components. Do not pass
      # its compliance by default.
      if _checkHostComplianceAttr('removed_components'):
         hostCompliance.removed_components = removedCompsCompliance

      if STAGING_SUPPORTED:
         hostCompliance.stage_status = stageStatus

      if QUICK_PATCH_SUPPORTED:
         # Quick Patch is supported.

         # Compliance details message.
         compDetails = \
            _getComplianceDetailsNotification(beforeQpStatus, overallStatus)
         # These fields cannot be populated within patch the patcher when the
         # host does not have these members defined in the settings_daemon
         # binding.

         # ESX host and ESXio use the same scan code. The ESXio reports the
         # hostCompliance (Not data_processing_unit_compliance) as the ScanTask
         # result to the ESX host. In ESXio's hostCompliance result,
         # the compliance_status_details and remediation_details should not be
         # filled as data_processing_unit_compliance doesn't have these fields.
         if not IS_ESXIO:
            hostCompliance.compliance_status_details = compDetails
            hostCompliance.remediation_details = remediationDetails
         hostCompliance.impact_details = impactDetails
      elif LIVE_UPDATE_SUPPORTED:
         # Only live update, no Quick Patch.
         hostCompliance.impact_details = impactDetails

      return hostCompliance

   def mergeDPUImpacts(self, impact, impactId):
      """ Merge DPU impacts.
      """
      if (impact != IMPACT_UNKNOWN and self.dpusCompliance and
          self.dpusCompliance.compliance):
         maxImpact = max([impactToValue[d.impact]
                          for d in self.dpusCompliance.compliance.values()])
         impact = valueToImpact[max(maxImpact, impactToValue[impact])]
         impactId = impactToID.get(impact, None)
      return impact, impactId

   def mergeWithDpuResults(self, hostComplianceResult):
      """ Add dpu compliance in host compliance result if exists.

          Adjust overall status and impact based on DPU statuses and impacts.
      """
      if not self.dpusCompliance or not hostComplianceResult:
         return
      hostComplianceResult.data_processing_units_compliance = \
         self.dpusCompliance
      status = hostComplianceResult.status
      if status != UNAVAILABLE and self.dpusCompliance.compliance:
         allStatus = [d.status for d in self.dpusCompliance.compliance.values()]
         maxStatus = max([complianceStatusToValue[s] for s in allStatus])
         status = valueToComplianceStatus[max(maxStatus,
                                              complianceStatusToValue[status])]
         hostComplianceResult.status = status

   def scan(self):
      """Scan the host compliance.
      """
      def _getStageStatus(stageStatus, currentStageStatus):
         """
         Returns the updated stageStatus based on the global
         stageStatus till now and and the newly computed
         currentStageStatus (e.g. addon, component).
         """
         if currentStageStatus:
            if stageStatus is None or stageStatus == STAGED:
               stageStatus = currentStageStatus

         return stageStatus

      # This method assumes the task is already in progress, and complete
      # the task with increments.
      if taskHasNotification(self.task):
         notif = getNotification(HOSTSCAN_STARTED, HOSTSCAN_STARTED)
         self.task.updateNotifications([notif])

      stepCount = 22 if ALLOW_DPU_OPERATION else 15
      progressStep = (100 - self.task.progress) // stepCount

      self._depotMgr = DepotMgr(depotSpecs=self.depotSpec, connect=True)
      swMgr = SoftwareSpecMgr(softwareSpec=self.swSpec,
                              depotManager=self._depotMgr)

      # Populate UI string cache.
      for comp in self._depotMgr.components.IterComponents():
         name, ver = comp.compNameStr, comp.compVersionStr

         self._compUiStrMap.setdefault(name, dict())[ver] = \
            (comp.compNameUiStr, comp.compVersionUiStr, comp.compUiStr)

      # Notification lists must be explicitly initiated to empty lists to
      # allow additions.
      self.overallNotifications = Notifications(info=[], warnings=[], errors=[])
      self.task.setProgress(self.task.progress + progressStep)

      try:
         self.desiredImageProfile = swMgr.validateAndReturnImageProfile()
         self.desiredCompSource = \
            self.desiredImageProfile.GetComponentSourceInfo()

         if ALLOW_DPU_OPERATION:
            from ..ESXioImage import ImageOperations
            depotUrls = [depot['url'] for depot in self.depotSpec]
            profComps = self.desiredImageProfile.GetKnownComponents()
            relatedComponents = profComps.GetCompNameVersionPairs()
            self.dpusCompliance = ImageOperations.scanOnDPUs(self.swSpec,
                                                             depotUrls,
                                                             relatedComponents,
                                                             self.task)
            self.task.setProgress(self.task.progress + 7 * progressStep)

         self._doQuickPatchScan = self._isQuickPatchScanNeeded()
         # When either self._enableQuickPatch or self._doQuickPatchScan is True,
         # we will enable QuickPatchInstaller instance.
         # We need to do Quick Patch scan even if enableQuickPatch is false,
         # particularly when in incomplete remediation, the user may want a
         # coordinated reboot, rather than retrying the actions.
         # However, enforceQuickPatch must be False as the image may not be
         # Quick Patch eligible.
         self.hostImage = HostImage(
            enableQuickPatch=self._enableQuickPatch or self._doQuickPatchScan,
            enforceQuickPatch=False)

         # The current live image is what we really want to scan against
         # as any pending reboot image will be discarded by apply.
         self.hostImageProfile = self.hostImage.GetProfile(
                                             database=HostImage.DB_VISORFS)
         self.stagedImageProfile = self.hostImage.stagedimageprofile
         self.hostCompSource = self.hostImageProfile.GetComponentSourceInfo()
         self.stageCompSource = None
         if self.stagedImageProfile:
            self.stageCompSource \
               = self.stagedImageProfile.GetComponentSourceInfo()
            self.stagedSoftwareSpec = self.getImageProfileScanSpec(
                                          self.stagedImageProfile,
                                          self._stageObsoletedComps,
                                          isStage=True)
            self.populateCompDowngradeInfo(self.stagedImageProfile,
                                        self.stageUnitCompDowngrades,
                                        self.stageCompDowngrades)

         self.currentSoftwareScanSpec = self.getImageProfileScanSpec(
                                          self.hostImageProfile,
                                          self._hostObsoletedComps)

         self.populateCompDowngradeInfo(self.hostImageProfile,
                                        self.releaseUnitCompDowngrades,
                                        self.userCompDowngrades)

         self.task.setProgress(self.task.progress + progressStep)
      except Exception as e:
         msg = "Failed to validate/extract the softwareSpec"
         self.reportErrorResult(msg, e)

      # Compare logic for base image
      stageStatus = None
      try:
         baseImageCompliance, baseImageStageStatus = \
            self.computeBaseImageCompliance()
         if baseImageStageStatus:
            stageStatus = baseImageStageStatus
         self.task.setProgress(self.task.progress + progressStep)
      except Exception as e:
         msg = "Failed to compute base image compliance for the host"
         self.reportErrorResult(msg, e)

      # Compare logic for addon
      try:
         addOnCompliance, addOnComplianceStageStatus = \
            self.computeAddOnCompliance()
         stageStatus = _getStageStatus(stageStatus, addOnComplianceStageStatus)
         self.task.setProgress(self.task.progress + progressStep)
      except Exception as e:
         msg = "Failed to compute addon compliance for the host"
         self.reportErrorResult(msg, e)

      # Compare logic for components
      try:
         componentsCompliance, componentsStageStatus = \
            self.computeComponentsCompliance()
         stageStatus = _getStageStatus(stageStatus, componentsStageStatus)
         self.task.setProgress(self.task.progress + 2 * progressStep)
      except Exception as e:
         msg = "Failed to compute component compliance for the host"
         self.reportErrorResult(msg, e)

      # Compliance for hardware support packages.
      try:
         hspCompliance = self.computeHardwareSupportCompliance()
         self.task.setProgress(self.task.progress + 2 * progressStep)
      except Exception as e:
         msg = "Failed to compute hardware support compliance for the host"
         self.reportErrorResult(msg, e)

      # Compliance logic for solutions.
      try:
         solutionsCompliance, solutionsStageStatus = \
            self.computeSolutionCompliance()
         stageStatus = _getStageStatus(stageStatus, solutionsStageStatus)
         self.task.setProgress(self.task.progress + 2 * progressStep)
      except Exception as e:
         msg = "Failed to compute solution compliance for the host"
         self.reportErrorResult(msg, e)

      # Compliance for user removed components
      removedCompsCompliance = None
      if PERSONALITY_MANAGER_COMPONENT_REMOVAL_ENABLED:
         try:
            removedCompsCompliance, removedCompsStageStatus = \
               self.computeRemovedCompsCompliance()
            stageStatus = _getStageStatus(stageStatus, removedCompsStageStatus)
            self.task.setProgress(self.task.progress + 2)
         except Exception as e:
            msg = "Failed to compute removed components compliance for the host"
            self.reportErrorResult(msg, e)

      # Orphan VIBs compliance
      try:
         orphanVibCompStatus = self.computeOrphanVibCompliance()
         self.task.setProgress(self.task.progress + progressStep)
      except Exception as e:
         msg = "Failed to compute orphan VIB compliance for the host"
         self.reportErrorResult(msg, e)

      complianceResult = self.formHostComplianceResult(
                                 baseImageCompliance,
                                 addOnCompliance,
                                 hspCompliance,
                                 componentsCompliance,
                                 solutionsCompliance,
                                 orphanVibCompStatus,
                                 stageStatus,
                                 removedCompsCompliance=removedCompsCompliance)

      if ALLOW_DPU_OPERATION:
         self.mergeWithDpuResults(complianceResult)

      if taskHasNotification(self.task):
         notif = getNotification(HOSTSCAN_COMPLETED, HOSTSCAN_COMPLETED)
         self.task.updateNotifications([notif])

      self.task.completeTask(result=vapiStructToJson(complianceResult))
