# Copyright 2019-2021,2023 VMware, Inc.
# All rights reserved. -- VMware Confidential

"""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, Notification,
           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, SoftwareSpecMgr)
from .Utils import getExceptionNotification, getFormattedMessage, getCommaSepArg

from .. import PYTHON_VER_STR, LIVEUPDATE_ENABLED

from ..Errors import InstallerNotAppropriate
from ..HostImage import HostImage
from ..Version import VibVersion
from ..VibCollection import VibCollection
from ..Utils.HostInfo import HostOSIsSimulator

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,
}
ENTITY_OVERRIDE_ORDER = [_RES_TYPE_TO_ENTITY[s]
                         for s in SoftwareSpecMgr.INTENT_RESOLUTION_MAP]

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

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 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 getNotification(notificationId, msgId, msgArgs=None, resArgs=None):
   defMsg = getFormattedMessage(NOTIFICATION_MSG[msgId], msgArgs)
   msg = LocalizableMessage(id=msgId, default_message=defMsg,
                            args=msgArgs or [])
   resMsg = getFormattedMessage(RESOLUTION_MSG.get(notificationId, ''), resArgs)
   if resMsg:
      # Populate the optional resolution when there is actually a message.
      resId = msgId + RESOLUTION_SUFFIX if resMsg else ''
      resolution = LocalizableMessage(id=resId,
                                      default_message=resMsg,
                                      args=resArgs or [])
   else:
      resolution = None
   return Notification(id=notificationId,
                       time=datetime.utcnow(),
                       message=msg,
                       resolution=resolution)

def getBaseImageInfo(displayName, version, displayVersion, releaseDate):
   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)
      return INCOMPATIBLE, msgDict
   else:
      return COMPLIANT, None

def getImageProfileImpact(hostImg, imgProfile):
   """Get the impact of the target image profile, could be one of: no impact,
      maintenance mode required, and reboot required (implies maintenance
      mode required).
      The calculation is VIB based where reboot, overlay and mmode metadata
      are available.
      Returns the impact and a boolean indicating if the host is pending reboot
      from a previous software change, which automatically cause a reboot
      required impact.
   """
   if not 'live' in hostImg.installers:
      raise InstallerNotAppropriate('live', 'Live installer is not operational')

   # 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 an change in overlay file.
   liveInstaller = hostImg.installers['live']
   adds, removes, _ = liveInstaller.StartTransaction(
                                                imgProfile,
                                                hostImg.imgstate,
                                                preparedest=False)
   rebootRequired = (adds == removes == None)
   if rebootRequired:
      # When the image state is bootbank updated, we have a pending reboot.
      return (IMPACT_REBOOT,
              hostImg.imgstate == hostImg.IMGSTATE_BOOTBANK_UPDATED)

   # Check if live VIB removal/addition requires maintenance mode.
   mmoderemoves, mmodeadds = \
         liveInstaller.liveimage.GetMaintenanceModeVibs(imgProfile.vibs,
                                                        adds,
                                                        removes)
   if mmoderemoves or mmodeadds:
      return IMPACT_MMODE, False

   return IMPACT_NONE, False

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):
      """Constructor.
      """
      self.swSpec = swSpec
      self.depotSpec = depotSpec
      self._depotMgr = None

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

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

      self.hostCompSource = None
      self.desiredCompSource = None
      self.releaseUnitCompDowngrades = None
      self.userCompDowngrades = None

      # 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.overallNotifications = None

      _addPythonPaths()

   def _formUnavailableResult(self, errMsg):
      """Form unavailable host compliance scan result.
      """
      errMsg = getNotification(UNAVAILABLE_ID,
                               UNAVAILABLE_ID)
      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))

      if LIVEUPDATE_ENABLED:
           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(),
                                 partial_maintenance_modes=dict())
      else:
           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 _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())
      unavalCompliance = self._formUnavailableResult(errMsg)
      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):
      """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 = self.hostImageProfile.GetSolutionInfo()
      for solId, solComps in ipSolDict.items():
         solution = self.hostImageProfile.solutions[solId]
         solInfoDict[solution.nameSpec.name] = getSolutionInfo(solution,
                                                               solComps)
      return solInfoDict, list(ipSolComps.keys())

   def scanLocalComponents(self, 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 = \
         self.hostImageProfile.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(self.hostImageProfile.reservedComponentIDs)
      if reservedComps:
         fullComps = self._depotMgr.componentsWithVibs
         hostComps = self.hostImageProfile.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:
            if not fullComps.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))

         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:
               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.
               self._hostObsoletedComps[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[REMOVED_DG_BI_COMP_KEY] = removedBIComps | downgradedBIComps
      compInfo[REMOVED_DG_ADDON_COMP_KEY] = (removedAddonComps |
                                             downgradedAddonComps)
      return compInfo

   def populateCompDowngradeInfo(self):
      """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.
      """
      self.releaseUnitCompDowngrades = dict()
      self.userCompDowngrades = dict()
      downgrades = \
         self.hostImageProfile.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.
            self.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 = self.releaseUnitCompDowngrades.setdefault(srcEntity,
                                                                dict())
            srcDict.setdefault(destEntity, dict())[name] = (v1, v2,
                                                            configDowngrade)

   def getHostOrphanVibs(self):
      """
      Get IDs of orphan VIBs on the host, i.e. VIBs on the host that are not a
      part of installed components.
      """
      compVibs = set()
      profile = self.hostImageProfile
      for bulletin in profile.bulletins.values():
         compVibs = compVibs.union(bulletin.vibids)
      return list(profile.vibIDs - compVibs)

   def getImageProfileScanSpec(self):
      """ For personality manager. Converts the host 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()

      # Base Image info
      currentBI = self.hostImageProfile.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)
         baseImageComponents = currentBI.components
      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 = self.hostImageProfile.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 = \
         self.hostImageProfile.GetHardwareSupportInfo()

      scanSpec[HARDWARE_SUPPORT] = hspDict

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

      # Compute components info
      compInfo = self.scanLocalComponents(baseImageComponents, addOnComponents,
                                          addOnRemovedCompNames, allHspCompDict,
                                          allHspRmCompNames, solCompNames)
      scanSpec.update(compInfo)

      # Compute orphan vibs
      scanSpec[ORPHAN_VIBS] = self.getHostOrphanVibs()
      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))
         notification = getNotification(msgId,
                                        msgId,
                                        msgArgs=msgArgs)
         if result.code == result.ERROR:
            self.overallNotifications.errors.append(notification)
         elif result.code == result.WARNING:
            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)
      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)
         for vib in vibs.values():
            for f in vib.filelist:
               if os.path.dirname(f) in DRIVER_MAP_DIRS:
                  return True
         return False

      # 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))
         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]))
         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, 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 = self._hostObsoletedComps.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):
      """
      Compute base image compliance data. In this function the installed
      baseimage version 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 = [], []

      # Comparing base image version
      currentBIObj = self.hostImageProfile.baseimage
      currentBIInfo = self.currentSoftwareScanSpec[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,
                              self.hostImageProfile.baseimage,
                              BASE_IMAGE,
                              status,
                              curErrs=errMsgs)

      if status == COMPLIANT:
         # Check if there are any missing base image components when
         # version is in compliant.
         missingComps = set()
         for comp in self.currentSoftwareScanSpec[REMOVED_DG_BI_COMP_KEY]:
            biCompVer = currentBIObj.components[comp]
            if self._isRemovedComponentMissing(comp, biCompVer, BASE_IMAGE):
               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,
                                      releaseDate=desiredBIObj.releaseDate)

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

   def computeAddOnCompliance(self):
      """
      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
      """
      errMsgs, infoMsgs = [], []
      targetAddOnObj = self.desiredImageProfile.addon
      currentAddOnInfo = self.currentSoftwareScanSpec[ADD_ON]
      currentAddOnObj = self.hostImageProfile.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.
                  missingComps = set()
                  for comp in self.currentSoftwareScanSpec[
                     REMOVED_DG_ADDON_COMP_KEY]:
                     addonCompVer = currentAddOnObj.components[comp]
                     if self._isRemovedComponentMissing(
                           comp, addonCompVer, ADDON):
                        missingComps.add(
                           self._getCompUiStrs(comp, addonCompVer)[0])

                  if missingComps:
                     # Changing the status as NON-COMPLIANT.
                     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()
         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(comp, version,
                                                 HARDWARE_SUPPORT):
               # Component is missing, and it is not due to obsolete.
               addComps.add((comp, version))

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

            msgArgs = [
               getCommaSepArg([self._getCompUiStrs(n, v)[0]
                  for n, v in (upComps | addComps)])]
            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.
      """
      getSolDict = lambda p: {s.nameSpec.name: s for s in p.solutions.values()}

      curSolutionsInfo = self.currentSoftwareScanSpec[SOLUTIONS]
      curSolutions = getSolDict(self.hostImageProfile)
      newSolutions = getSolDict(self.desiredImageProfile)
      newSolutionComps, _ = self.desiredImageProfile.GetSolutionInfo()
      allCurComps = self.hostImageProfile.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

      solCompliance = dict()

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

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

      if disableSols:
         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:
         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 _computePartialMmodeRequirementsForWCP(self, partialMmodes):
      """Check the need for partial maintenance mode during
         upgrade/downgrade of WCP components.
      """
      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

      # Check for WCP components in the host and desired image profile.
      _, hostWcp = _getNsxWcpComps(self.hostImageProfile.components)
      _, desiredWcp = \
         _getNsxWcpComps(self.desiredImageProfile.components)

      # Note: WCP upgrade/update requires partial maintenance mode.
      # Removal does not require Partial Mmode.
      if (hostWcp and desiredWcp and hostWcp.compVersion
          != desiredWcp.compVersion):
          solutionName = _getComponentSolutionName(desiredWcp.compNameStr)
          if isNotNone(solutionName) and solutionName not in partialMmodes:
              partialMmodes[solutionName] = PARTIAL_MAINTENANCE_MODE_WCP

   def computePartialMmodeRequirements(self):
      """Compute the partial maintenance modes requirement for component
         updates to achieve the desired image state. Currently, we shall
         support this only for WCP component.
      """
      partialMmodes = dict()
      self._computePartialMmodeRequirementsForWCP(partialMmodes)
      return partialMmodes

   def _computeImpactForNSXWCP(self):
      """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.
      """
      impact, impactId = IMPACT_NONE, None

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

      # WCP/NSX upgrade/downgrade requires mmode
      if ((hostNsx and desiredNsx and (hostNsx.compVersion
          != desiredNsx.compVersion)) or
          (hostWcp and desiredWcp and (hostWcp.compVersion
          != desiredWcp.compVersion))):
         impact, impactId = IMPACT_MMODE, MAINTMODE_IMPACT_ID

      # NSX removal requires mmode
      elif hostNsx and not desiredNsx:
         impact, impactId = IMPACT_MMODE, MAINTMODE_IMPACT_ID

      return impact, impactId

   def computeImageImpact(self):
      """Compute image impact, partial maintenance mode requirement and
         generate a notification for the impact.
         In case the host is pending reboot from a prior software change,
         another info notification will be generated to reflect it.
      """
      # Reboot/MMode impact of the remediation.
      impact, rebootPending = getImageProfileImpact(self.hostImage,
                                                    self.desiredImageProfile)

      impactId = None
      partialMmode = None

      if impact != IMPACT_NONE:
         impactId = (MAINTMODE_IMPACT_ID if impact == IMPACT_MMODE else
                     REBOOT_IMPACT_ID)
      if rebootPending:
         impactId = PENDING_REBOOT_ID

      # Compute impact from NSX/WCP components
      if impact == IMPACT_NONE:
         impact, impactId = self._computeImpactForNSXWCP()
         # If the impact is not none then either NSX/WCP requires
         # maintenance mode.
         if LIVEUPDATE_ENABLED and impact != IMPACT_NONE:
            # Append the partial Mmode requirements
            partialMmode = self.computePartialMmodeRequirements()

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

      return impact, partialMmode


   def computeComponentsCompliance(self):
      """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.
      """
      def _getCompVerStatus(name, hostCompInfo, targetCompInfo):
         """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 self.userCompDowngrades:
            v1, v2, _, dest, configDowngrade = self.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])
               return INCOMPATIBLE, Notifications(errors=[msg])
            else:
               return INCOMPATIBLE, None

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

         # Upgrade or new addition - non-compliant, otherwise - compliant.
         if hostCompInfo is None:
            status = NON_COMPLIANT
         else:
            status, _ = getVersionCompliance(targetCompInfo.version,
                                             hostCompInfo.version,
                                             name,
                                             COMPONENT)
            assert status in (COMPLIANT, NON_COMPLIANT)
         return status, None

      def _getCompCompliance(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(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

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

      hostUserComps = _compSummaryToInfo(
                                 self.currentSoftwareScanSpec[USER_COMPS_KEY])

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

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

      for name, comp in desiredUserComps.items():
         # 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 = self.hostCompSource[name]
            status, notifications = _getCompVerStatus(name, current, comp)
         else:
            # Component not present on host.
            current, currentSource, notifications = None, None, None
            status = NON_COMPLIANT

         compCompliance[name] = _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] = _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)
            compCompliance[name] = _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):
         vibDetails = ['%s(%s)' % (name, version) for name, version in
                       vibDict.items()]
         return getNotification(msgId, msgId,
                                msgArgs=[getCommaSepArg(vibDetails)])

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

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

      return status

   def formHostComplianceResult(self, baseImageCompliance, addOnCompliance,
                                hspCompliance, componentsCompliance,
                                solutionsCompliance, orphanVibStatus):
      """
      Compute the overall host compliance based on base image, addon, solutions
      and user components compliance info, as well orphan VIB compliance
      status.
      If any of the compliance computation fails then the status will be
      returned as UNAVAILABLE.
      """

      # 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, partialMmode = 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 isNotNone(orphanVibStatus):
         complianceStatus.add(orphanVibStatus)
      if not precheckCompatible:
         complianceStatus.add(INCOMPATIBLE)

      # Overall compliance status.
      # Unavailable: if any image piece reports unavailable.
      # Incompatible: if any image piece reports incompatible, or the
      #               precheck result contains an error.
      # non-compliant: if any image piece reports non-compliant.
      # compliant: if none of the above applies.
      overallStatus = COMPLIANT
      if UNAVAILABLE in complianceStatus:
         overallStatus = UNAVAILABLE
      elif INCOMPATIBLE in complianceStatus:
         overallStatus = INCOMPATIBLE
      elif NON_COMPLIANT in complianceStatus:
         overallStatus = NON_COMPLIANT

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

      if LIVEUPDATE_ENABLED:
         return 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,
                            partial_maintenance_modes = \
                                getOptionalVal(partialMmode))
      else:
         return 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)

   def scan(self):
      """Scan the host compliance.
      """
      # This method assumes the task is already in progress, and complete
      # the task with increments.
      progressStep = (100 - self.task.progress) // 12

      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()
         self.hostImage = HostImage()
         # 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.hostCompSource = self.hostImageProfile.GetComponentSourceInfo()
         self.currentSoftwareScanSpec = self.getImageProfileScanSpec()

         self.populateCompDowngradeInfo()
         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
      try:
         baseImageCompliance = self.computeBaseImageCompliance()
         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 = self.computeAddOnCompliance()
         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 = self.computeComponentsCompliance()
         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 = self.computeSolutionCompliance()
         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)

      # 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)
      self.task.completeTask(result=vapiStructToJson(complianceResult))
