#!/usr/bin/env python
# Copyright 2016-2021 VMware, Inc.
# All rights reserved. -- VMware Confidential
"""
This script is executed by the `software-packages` command or Patching APIs.
It has the logic to decide what type of upgrade is done - onprem or cloud, and
on what type - vc or gateway, and executes logic corresponding to the needs.
"""
import os
import sys
import logging
import tempfile
import shutil
import json
from os.path import join, exists, abspath, dirname, isdir, basename

# This is needed to import FSS which is imported in some of the other files
sys.path.append(join(
    dirname(__file__),
    "patches",
    "libs",
    "feature-state"))

from update_utils import (runCommand, DEPLOYMENT_ROOT, MANIFEST_FILE,
        getRpmIgnoreList, RPM_MANIFEST_FILE, get_rpm_manifest,
        isRootPasswdExpired, isB2BUpgrade, isGateway, ALLOWLIST_FILE,
        isB2BComponentAllowed, getComponentsForProduct,
        getGwPlatformComponents, getAllProductComps, getSourceFSS,
        getServiceIgnoreList)
from fss_utils import getTargetFSS
from vmware.update.json_utils import prettyJsonDump, JsonFileSerializer, JsonSerializer
from vmware.update.extensions import extend, Hook
from vmware.update.specs import RequirementsResult, Requirements, \
        ValidationResult, Mismatch, InternalError, StatusInfo, ErrorInfo, \
        PatchInfo, Question
from vmware.update.l10n import msgMetadata as _T, localizedString as _
from update_wcp_utils import performWcpPrechecks
from update_nsx_utils import performNsxPrechecks
from product_utils import getUpdateProduct, isProductEnabled, getAllProducts


logger = logging.getLogger(__name__)

PASSWD_EXPIRED_TEXT = {'id': 'com.vmware.password.expired',
                       'localized': 'Appliance (OS) root password expired.',
                       'args': []}
PASSWD_RESOLUTION_TEXT = {'id': 'com.vmware.password.resolution',
                          'localized': 'Please change appliance (OS) root'
                          ' password before attempting an update',
                          'args': []}

INTERNAL_ERROR_TEXT = _(_T('ur.internal.text',
                           'Internal error occurs during execution'
                            'of update process.'))
INTERNAL_ERROR_RESOLUTION = _(_T('ur.internal.resolution',
                                 'Send upgrade log files to VMware technical support '
                                 'team for further assistance.'))

VERSION_ERROR_TEXT = {'id': 'vcsa.version.mismatch.error.text',
                   'localized': 'The patch is incompatible with the current '
                   'vCenter Server version.',
                   'args': []}
VERSION_ERROR_RESOLUTION = {'id': 'vcsa.version.mismatch.error.resolution',
                   'localized': 'Select a patch compatible with your '
                   'vCenter Server version.',
                   'args': []}
UPDATE_CONF = '/etc/applmgmt/appliance/update.conf'

PATCH_SUMMARY = _(_T('patch.summary',
                     'In-place upgrade for vCenter appliances.'))
TIME_TO_INSTALL= 0

WCP_COMPONENT_PATH = join(DEPLOYMENT_ROOT, "scripts","patches",
                          "payload", "components-script", "wcp")

WCP_TEMP_COMPONENT_PATH = join(DEPLOYMENT_ROOT, "scripts", "patches",
                               "payload", "wcp-temp")

NSX_COMPONENT_PATH = join(DEPLOYMENT_ROOT, "scripts","patches",
                          "payload", "components-script", "nsx")

NSX_TEMP_COMPONENT_PATH = join(DEPLOYMENT_ROOT, "scripts", "patches",
                               "payload", "nsx-temp")

def _executePatchRunnerAndSupplyUserData(command, stageDir, userData, outputFile,
                                         reportCompletion):
    '''Execute PatchRunner subcommand and supply user data to executed process.
    The routine will store the user data into a file and will pass the file
    to PatchRunner subcommand, then the file will be destroyed as soon as
    the PatchRunner loads it.

    @param command: PatchRunner command. Could be validate, prepatch, patch
    @type command: str

    @param stageDir: PatchRunner stage directory
    @type stageDir: str

    @param userData: Supplied user data as lookup of user data id and answer
    @type userData: dict

    @param outputFile: Output file passed to PatchRunner subcommand. This will
      be the file where the PatchRunner will store its results
    @type outputFile: str

    @param reportCompletion: Whether the routine needs to report the status of
      the command based on the PatchRunner subcommand exit code
    @type reportCompletion: bool
    '''
    patchRunner = join(DEPLOYMENT_ROOT, "scripts", "patches", "PatchRunner.py")

    with tempfile.NamedTemporaryFile(mode="w+") as fp:
        # Dump answers to a file}
        cnt = JsonSerializer().serialize(userData)
        fp.write(cnt)
        # Flush all the data, so the other process can read it.
        fp.flush()

        cmd = ['/usr/bin/python3', '"%s"' % patchRunner, command, "-u", '"%s"' % fp.name,
               "-o", '"%s"' % outputFile, '-d', '"%s"' % stageDir, '--disableStdoutLogging']
        out, err, rc = runCommand(["/bin/bash",  "--login", "-c", ' '.join(cmd)],
                                   progress=True,
                                   message="Running %s script." % command)

    logger.debug("Patch command %s: \n out=%s\nerror=%s\nrc=%d\n" %
                 (command, out, err, rc))

    if reportCompletion:
        if rc:
            logger.error("Patch command %s failed", command)
            _reportFailure(outputFile)
        else:
            logger.debug("Patch command %s completed successfully", command)
            _reportSuccess(outputFile)

    return rc == 0

def _reportSuccess(outputFile):
    if exists(outputFile):
        serializer = JsonFileSerializer(outputFile)
        data = serializer.deserialize()
        if data["status"] == StatusInfo.State.SUCCESS:
            logger.debug("Success status has been already reported")
        elif data["status"] == StatusInfo.State.ERROR:
            logger.warn("Unable to report success, because failure has been "
                        "already reported.")
        else:
            logger.debug("Report success")
            data["status"], data["progress"] = StatusInfo.State.SUCCESS, 100
            prettyJsonDump(outputFile, data)
    else:
        logger.debug("Report success")
        result = StatusInfo(StatusInfo.State.SUCCESS, progress=100)
        prettyJsonDump(outputFile, result)

def _reportFailure(outputFile):
    error=ErrorInfo(detail=[INTERNAL_ERROR_TEXT], resolution=INTERNAL_ERROR_RESOLUTION)
    if exists(outputFile):
        serializer = JsonFileSerializer(outputFile)
        data = serializer.deserialize()
        if data["status"] == StatusInfo.State.ERROR:
            logger.debug("Error status has been already reported")
            return

        if data["status"] == StatusInfo.State.SUCCESS:
            logger.warn("Wrong success status has been already reported. "
                        "Overwriting it.")

        logger.debug("Report error")
        data["status"] = StatusInfo.State.ERROR
        data["error"] = error
        prettyJsonDump(outputFile, data)
    else:
        logger.debug("Report error")
        result = StatusInfo(StatusInfo.State.ERROR, error=error)
        prettyJsonDump(outputFile, result)

def _reportProgress(outputFile, progress):
    result = StatusInfo(StatusInfo.State.RUNNING, progress=progress)
    prettyJsonDump(outputFile, result)

def _transformRequirementsRpmIngoreList(rpmIgnoreList):
    ''' Transforms the rpm ignore list recieved from Discovery hook execution to match
    the format expected by the upstream. Receive list of names, transform to list of
    names-version-build.rpm from the manifest
    Example:
    _transformRequirementsRpmIngoreList(['vmware-studio-vami-lighttpd', 'vmware-vmrc'])
    ['vmware-studio-vami-lighttpd_3.0.0.2-4446656.x86_64.rpm', 'vmware-vmrc-6.6.0-8842685.x86_64.rpm']


    @param rpmIgnoreList: Rpms to ignore during patching
    @type rpmIgnoreList: list of str

    @rtype: list of str
    '''
    if not rpmIgnoreList:
        return []

    #Load the manifest file so that we can do mapping between name and name with versions
    from lxml import etree
    manifest = etree.parse(MANIFEST_FILE)
    ignoreList = []
    for rpmName in rpmIgnoreList:
        rpmXml = manifest.xpath("//package[@name='%s']" % rpmName)
        if rpmXml:
           #location is always including 'package-pool/'
           fullName = basename(rpmXml[0].find('location').text.strip())
           ignoreList.append(fullName)
        else:
           logger.debug('Could not find matching rpm for name %s', rpmName)
    return ignoreList

def isLeafServiceUpdate():
    ''' Indicates if it is a leaf service update or not
    '''
    header = get_rpm_manifest().get("header", {})
    return "true" == header.get("leaf_service", "false")


def calculateRebootRequired(requirements):
    ''' Indicates if the update requires a reboot or not. Leaf service
    update never allows reboot. It reads the B2B and manifest and if one
    of them requires it, it sets it.
    '''
    if isLeafServiceUpdate():
       logger.debug("Not validating if reboot is required as it is leaf service")
       return False
    b2bReboot = requirements['rebootRequired']
    manifestReboot = get_rpm_manifest().get("header", {}).get("rebootrequired", False)
    rebootRequired = b2bReboot or manifestReboot
    logger.debug("The system needs restart: %s, due to B2B: %s and Manifest: %s", \
       rebootRequired, b2bReboot, manifestReboot)
    return rebootRequired


def _transformRequirements(requirements, defaultRpmIgnoreList, serviceIgnoreList):
    '''Transforms requirements from PatchRunner to requirements defined in
    patching spec. Add rpmIgnoreList based on the deployment type, on top of
    rpmIgnoreList defined by the components

    @param requirements: Aggregated component requirements:
    @type requirements: dict representing Requirements object
     Example:
         {
            "rebootRequired": false,
            "requiredDiskSpace": {
                "core": 0.1
            },
            "rpmIgnoreList": [],
            "questions": [
                {
                    "defaultAnswer": null,
                    "kind": "password",
                    "description": {
                        "id": "sso.password.desc",
                        "localized": "SSO root password is ..",
                        "translatable": "SSO root password is .."
                    },
                    "text": {
                        "id": "sso.password.text",
                        "localized": "What is the SSO root password",
                        "translatable": "What is the SSO root password"
                    },
                    "allowedValues": [],
                    "userDataId": "sso.root.password"
                }
            ]
        }
    @param defaultRpmIgnoreList: Default rpm ignore list which needs to be
      added on top of component requirements
    @type defaultRpmIgnoreList: list

    @return: Patch requirements
    @rtype update_specs.Requirements
    '''
    requiredDiskSpace = requirements['requiredDiskSpace']
    rpmIgnoreList = _transformRequirementsRpmIngoreList(requirements['rpmIgnoreList']) + defaultRpmIgnoreList
    questions = []
    rebootRequired = calculateRebootRequired(requirements)

    for q in requirements['questions']:
        question = Question(userDataId = q['userDataId'],
                            text = q['text'],
                            description = q['description'],
                            kind = q['kind'],
                            allowedValues = q['allowedValues'],
                            defaultAnswer = q['defaultAnswer'])
        questions.append(question)
    # Because all this logic should be on B2B side but it is not.
    # Requirements definition can be outdated
    try:
        patchingFrameworkRequirements = Requirements(
            requiredDiskSpace=requiredDiskSpace,
            rpmIgnoreList=rpmIgnoreList,
            serviceIgnoreList=serviceIgnoreList,
            questions=questions,
            rebootRequired=rebootRequired)
    except TypeError:
        patchingFrameworkRequirements = Requirements(
            requiredDiskSpace=requiredDiskSpace,
            rpmIgnoreList=rpmIgnoreList,
            questions=questions,
            rebootRequired=rebootRequired)
    return patchingFrameworkRequirements

def _transformPatchInfo(patchInfo):
    '''Transforms patch info returned by PatchRunner to patch info defined in
    patching spec. Replace patch summary with patch summary defined on top
    level.

    @param patchInfo: Aggregated patch info.
    @type patchInfo: dict representing PatchInfo object
     Example:
         {
            "patchSummary": null,
            "timeToInstall": 5
         },

    @return: Patch information
    @rtype update_specs.PatchInfo
    '''
    return PatchInfo(timeToInstall=patchInfo['timeToInstall'],
                     patchSummary=PATCH_SUMMARY)

def _transformMismatches(mismatches):
    """Transforms mismatches from PatchRunner to mismatches defined
    in patching spec. Add given mismatches on top of mismatches returned by
    PatchRunner

    @param mismatches: Patch Runner mismatches
    @type mismatches: array of dict
    """
    result = []
    for mismatch in mismatches:
        result.append(Mismatch(
                text = mismatch['text'],
                description = mismatch['description'],
                resolution = mismatch['resolution'],
                problemId = mismatch['problemId'],
                severity = mismatch['severity'],
                relatedQuestionId = mismatch['relatedUserDataId'])
        )
    return result

def _getPatchRunnerStageDir(rootStageDir):
    return join(rootStageDir, "patch_runner")

def _filterB2BScripts(scriptsDir, allowlistComponents=None, ignorelistComponents=None):
    ''' Now we packages all B2B scripts into special service repo and execution
    of these scripts without actual installation of the new binaries will lead
    to crash. This function removes those that are not part of th allowlist
    from the OS or are specificly ignorelisted. first_component and
    last_component are never removed. If a component is both allowlist and
    ignorelisted. The ignorelist takes priority.
    '''
    ignorelistComponents = ignorelistComponents or []
    dirs = os.listdir(scriptsDir)
    for i in dirs:
        if i in ['first_component', 'last_component']:
            continue

        if isB2BComponentAllowed(i, allowlistComponents, ignorelistComponents):
            continue

        filepath = os.path.join(scriptsDir, i)
        logger.debug('Payload does not allow component %s to be run, removing it', i)
        _cleanComponent(filepath)

def _getManifestAllowlistComponents(scriptsDir):
    ''' Returns the allowlist components for this env. If no such filtering is
    present None is return and all are allowed.
    '''
    with open(RPM_MANIFEST_FILE) as f:
        rpmManifestJson = json.load(f)['header']

    if 'allowed_b2b_components' not in rpmManifestJson or not exists(scriptsDir):
       logger.debug('No components header present on the manifest.')
       return None
    return rpmManifestJson['allowed_b2b_components']

def _getPayloadAllowlistComponents(scriptsDir):
    ''' Returns the allowlist components for this environment and chosen upgrade
    option. if all are allowed None is returned.
    '''
    with open(ALLOWLIST_FILE) as f:
        allowlist = json.load(f)

    typeAllowlist = allowlist['gateway'] if isGateway() else allowlist['vcenter']
    allowedComponents = typeAllowlist.get('b2b', None) if isB2BUpgrade() else typeAllowlist.get('day0', None)
    return allowedComponents

def _ensureComponentIsPreserved(originalComponentPath, tempComponentPath):
    ''' Ensure that component is present. Return True if it is preserved and
    false otherwise.
    '''
    # If both are not present that means that we should not have preserve it.
    if not exists(tempComponentPath) and not exists(originalComponentPath):
        logger.debug("Requested component should not be available.")
        return False
    if not exists(tempComponentPath) and exists(originalComponentPath):
        logger.debug("Copying to temp location the requested component.")
        shutil.copytree(originalComponentPath, tempComponentPath)
    if exists(tempComponentPath) and not exists(originalComponentPath):
        logger.debug("Regenerating the requested component from the temp location.")
        shutil.copytree(tempComponentPath, originalComponentPath)
    return True

def _cleanComponent(componentPath):
    ''' Cleans component if exists
    '''
    if not exists(componentPath):
        return
    if os.path.isfile(componentPath):
        os.remove(componentPath)
    else:
        shutil.rmtree(componentPath)

def _executeWcpPrechecks(requirementsSpec, allowlist, ignorelist):
    ''' Executes wcp prechecks if the component won't be executed
    '''
    if isGateway() or isB2BUpgrade():
        logger.debug("WCP precheck is only appicable for non-cloud environments")
        return []

    if isB2BComponentAllowed("wcp", allowlist, ignorelist):
        logger.debug("WCP precheck is not required to be run.")
        return []

    if not _ensureComponentIsPreserved(WCP_COMPONENT_PATH, WCP_TEMP_COMPONENT_PATH):
        logger.debug("WCP is not preserved thus it should not be checked.")
        return []

    logger.debug("Performing WCP check onprem")
    wcpMismatches = performWcpPrechecks(requirementsSpec)
    _cleanComponent(WCP_COMPONENT_PATH) # Should not stay in components folder
    logger.debug("WCP check return errors: %s", len(wcpMismatches))
    return wcpMismatches


def _executeNsxPrechecks(requirementsSpec, allowlist, ignorelist):
    ''' Executes nsx prechecks if the component won't be executed
    '''
    if not (getSourceFSS('NSX_Integrated') and getTargetFSS('NSX_Integrated')):
        logger.debug("NSX FSS is not enabled for this environment."
                     "Skipping this checks")
        return []

    if isGateway() or isB2BUpgrade():
        logger.debug("Found either a cloud gateway or b2b environment hence "
                     "skipping NSX day0 patching prechecks")
        return []

    if isB2BComponentAllowed("nsx", allowlist, ignorelist):
        logger.debug("NSX precheck is not required to be run.")
        return []

    if not _ensureComponentIsPreserved(NSX_COMPONENT_PATH, NSX_TEMP_COMPONENT_PATH):
        logger.debug("NSX is not preserved thus it should not be checked.")
        return []

    logger.debug("Performing NSX check onprem")
    nsxMismatches = performNsxPrechecks(requirementsSpec)
    _cleanComponent(NSX_COMPONENT_PATH) # Should not stay in components folder
    logger.debug("NSX check return errors: %s", len(nsxMismatches))
    return nsxMismatches

def _filterComponents(requirementsSpec, patchRunnerComponentsDir, updateProduct=None):
    ''' Calculates which of the components should be kept and remove rest. It also
    will perform wcp prechecks if this is needed, even if the script should be removed.
    '''
    allowlist = _getManifestAllowlistComponents(patchRunnerComponentsDir)

    if allowlist is None:
        logger.debug("No manifest allowlist present, it is not leaf service upgrade.")
        allowlist = _getPayloadAllowlistComponents(patchRunnerComponentsDir)
        if not isGateway() and not isB2BUpgrade() and allowlist is not None:
            if "wcp" not in allowlist:
                logger.debug("Ensure WCP is available")
                # the wcp script is removed during staging and not thus we need to
                # ensure it is there for the precheck after staging
                _ensureComponentIsPreserved(WCP_COMPONENT_PATH, WCP_TEMP_COMPONENT_PATH)
            if "nsx" not in allowlist:
                logger.debug("Ensure NSX is available")
                # the nsx script is removed during staging and not thus we need to
                # ensure it is there for the precheck after staging
                _ensureComponentIsPreserved(NSX_COMPONENT_PATH, NSX_TEMP_COMPONENT_PATH)

    ignorelist = []

    if isGateway():
        # decoupled updates of product (like HLM) and gateway platform is
        # only applicable if FSS is enabled
        if getSourceFSS('GW_PLATFORM_UPDATE'):
            allProductComps = getAllProductComps()
            # if product only update then remove platform components
            if updateProduct:
                gatewayComps = getGwPlatformComponents(patchRunnerComponentsDir)
                ignorelist.extend(gatewayComps)

                updateProductComps = getComponentsForProduct(updateProduct)
                otherProductComps = [ele for ele in allProductComps
                                     if ele not in updateProductComps]
                ignorelist.extend(otherProductComps)
            # if gateway/platform only updates then remove product components
            else:
                ignorelist.extend(allProductComps)
        else:
            # continue support for current way of updates using the combined HLM
            # and gateway repo if FSS is not enabled. If product not installed
            # then remove its components irrespective of update type
            products = getAllProducts()
            for product in products:
                if not isProductEnabled(product):
                    ignorelist.extend(getComponentsForProduct(product))

    if allowlist is not None or ignorelist:
        logger.debug("Filtering components as not all are allowed to run. "
                    "Allowlist %s\n Ignorelist %s", allowlist, ignorelist)
        _filterB2BScripts(patchRunnerComponentsDir,
                          allowlistComponents=allowlist,
                          ignorelistComponents=ignorelist)
    return allowlist, ignorelist

@extend(Hook.Requirements)
def collectRequirements(requirementsSpec):
    mismatches = []

    try:
        if not (isGateway() or isB2BUpgrade()):
            logger.debug("Checking verisons")
            with open(UPDATE_CONF, "r") as updateConf:
                updateConfJson = json.load(updateConf)
                src_version = updateConfJson["version"]
                logger.debug("Source VCSA version = %s" %src_version)

            manifest = get_rpm_manifest().get("header", {})
            target_version = manifest.get("version", None)
            logger.info("Target VCSA version = %s" %target_version)
            src_version_split = src_version.split('.')
            target_version_split = target_version.split('.')
            if src_version_split[0] != target_version_split[0] or \
                    src_version_split[1] != target_version_split[1]:
                logger.error("Major versions do not match. Cannot patch.")
                mismatch = Mismatch(VERSION_ERROR_TEXT,
                           description= VERSION_ERROR_TEXT,
                           resolution= VERSION_ERROR_RESOLUTION,
                           severity= Mismatch.ERROR)
                mismatches = [mismatch]
                return RequirementsResult(mismatches=mismatches)
    except Exception as e:
        logger.error("Version check failed. Exception %s" %e)
        mismatch = Mismatch(INTERNAL_ERROR_TEXT,
                   description=INTERNAL_ERROR_TEXT,
                   resolution=INTERNAL_ERROR_RESOLUTION,
                   severity=Mismatch.ERROR)
        mismatches.append(mismatch)
        return RequirementsResult(mismatches=mismatches)

    if not isGateway():
        if isRootPasswdExpired():
            logger.error("Appliance (OS) root password expired. "
                         "Please change appliance (OS) root password "
                         "before attempting an update")
            mismatch = Mismatch(PASSWD_EXPIRED_TEXT,
                        description=PASSWD_EXPIRED_TEXT,
                        resolution=PASSWD_RESOLUTION_TEXT,
                        severity=Mismatch.ERROR)
            mismatches = [mismatch]
            return RequirementsResult(mismatches=mismatches)
    else:
        logger.info("Not performing the root password expired check as it is Gateway.")

    patchRunnerStageDir = _getPatchRunnerStageDir(requirementsSpec.stageDirectory)
    patchRunner = join(DEPLOYMENT_ROOT, "scripts", "patches", "PatchRunner.py")
    patchRunnerComponentsDir = join(DEPLOYMENT_ROOT, "scripts", "patches", "payload", "components-script")
    if not exists(patchRunnerStageDir):
        os.makedirs(patchRunnerStageDir)

    if requirementsSpec.userData.get('thirdPartyPatch'):
        #This is 3rd party update and only system scripts need to be executed
        for dr in os.listdir(patchRunnerComponentsDir):
            fullPath = join(patchRunnerComponentsDir, dr)
            if isdir(fullPath) and 'first_component' not in dr \
                and 'last_component' not in dr:
                logger.debug('Third party patching, thus removing components directory %s', fullPath)
                shutil.rmtree(fullPath)
    mismatches = []

    # check for product update, only applicable to gateway
    updateProduct = None
    if isGateway():
        try:
            updateProduct = getUpdateProduct()
        except Exception as e:
            logger.error("Error while checking product update %s" %e)
            mismatch = Mismatch(INTERNAL_ERROR_TEXT,
                       description=INTERNAL_ERROR_TEXT,
                       resolution=INTERNAL_ERROR_RESOLUTION,
                       severity=Mismatch.ERROR)
            mismatches = [mismatch]
            return RequirementsResult(mismatches=mismatches)

    #Some payloads require that some scripts are not executed
    allowlist, ignorelist = _filterComponents(requirementsSpec,
                                             patchRunnerComponentsDir, updateProduct)

    # Not all frameworks have that property set yet so we need to check this way
    skipPrechecksIds = []
    if hasattr(requirementsSpec, 'skipPrechecksIds'):
        skipPrechecksIds = requirementsSpec.skipPrechecksIds

    # TODO Once this is enabled onprem remove this specific prechecks
    mismatches.extend(_executeWcpPrechecks(requirementsSpec, allowlist, ignorelist))

    # TODO Once this is enabled onprem remove this specific prechecks
    mismatches.extend(_executeNsxPrechecks(requirementsSpec, allowlist, ignorelist))

    with tempfile.NamedTemporaryFile() as fp:
        cmd = ['/usr/bin/python3', '"%s"' % patchRunner, 'discovery',
               "-o", '"%s"' % fp.name,
               "-d", '"%s"' % patchRunnerStageDir,
               '--disableStdoutLogging']

        if skipPrechecksIds: # cannot pass empty list
            cmd.extend(["--skipPrechecksIds",
                        " ".join(skipPrechecksIds)])

        out, err, rc = runCommand(["/bin/bash",  "--login", "-c", ' '.join(cmd)],
                                   progress=True,
                                   message="Running requirements script.")
        logger.debug("Patch command %s: \n out=%s\nerror=%s\nrc=%d\n" %
             (cmd, out, err, rc))

        # There is python bugs on the appliance and the code below does not work
        # properly
        #fp.seek(0)
        #requirementsResult = fp.read()
        with open(fp.name) as tempFp:
            requirementsResult = tempFp.read()

    rpmIgnoreList = getRpmIgnoreList(allowlist, ignorelist)
    serviceIgnoreList = getServiceIgnoreList()

    if requirementsResult:
        logger.debug("Transform patch runner requirements results: %s",
             requirementsResult)
        patchRunnerRes = JsonSerializer().deserialize(requirementsResult)
        requirements = _transformRequirements(patchRunnerRes['requirements'],
                                              rpmIgnoreList, serviceIgnoreList)
        patchInfo = _transformPatchInfo(patchRunnerRes['patchInfo'])
        mismatches.extend(_transformMismatches(patchRunnerRes['mismatches']))
        return RequirementsResult(requirements=requirements, patchInfo=patchInfo,
                                  mismatches=mismatches)
    else:
        if rc:
            logger.error("Requirements failed. Generate error result")
            mismatch = Mismatch(INTERNAL_ERROR_TEXT,
                    description=INTERNAL_ERROR_TEXT,
                    resolution=INTERNAL_ERROR_RESOLUTION,
                    severity=Mismatch.ERROR)
            mismatches.append(mismatch)
        else:
            logger.debug("Requirements done. Generate result")
        requirements = Requirements(rpmIgnoreList=rpmIgnoreList)
        patchInfo = PatchInfo(TIME_TO_INSTALL, PATCH_SUMMARY)
        return RequirementsResult(requirements=requirements, patchInfo=patchInfo,
                                     mismatches=mismatches)
@extend(Hook.Validation)
def validate(validationSpec):
    if 'Validation' in get_rpm_manifest().get('header', {}).get('skip_b2b_scripts', ''):
        logger.debug("Not executing Validation hook as manifest indicates it should be skipped.")
        return ValidationResult()
    patchRunnerStageDir = _getPatchRunnerStageDir(validationSpec.stageDirectory)
    userData = validationSpec.userData or {}
    with tempfile.NamedTemporaryFile() as fp:
        succ = _executePatchRunnerAndSupplyUserData("validate",
                                                    patchRunnerStageDir,
                                                    userData,
                                                    fp.name,
                                                    reportCompletion=False)

        # There is python bugs on the appliance and the code below does not work
        # properly
        #fp.seek(0)
        #validationResult = fp.read()
        with open(fp.name) as tempFp:
            validationResult = tempFp.read()

    if validationResult:
        logger.debug("Transform patch runner validation results: %s",
                     validationResult)
        patchRunnerRes = JsonSerializer().deserialize(validationResult)
        mismatchResult = _transformMismatches(patchRunnerRes['mismatches'])
        result = ValidationResult(mismatches=mismatchResult)
    elif not succ:
        logger.debug("Validate logic failed. Generate default error")
        mismatch = Mismatch(INTERNAL_ERROR_TEXT,
                            description=INTERNAL_ERROR_TEXT,
                            resolution=INTERNAL_ERROR_RESOLUTION,
                            severity=Mismatch.ERROR)
        result = ValidationResult([mismatch])
    else:
        logger.debug("Validate succeeded. Generate success result")
        result = ValidationResult()

    return result

@extend(Hook.Prepare)
def prepare(prepareSpec):
    # This hook is designed to be used in exceptional cases when rpms conflicts
    # need to be sorted out before the other rpms being install
    # if you change the system in this hook you need to WARN the user for that
    # as on failure the customer will need to revert via snapshot
    if 'SystemPrepare' in get_rpm_manifest().get('header', {}).get('skip_b2b_scripts', ''):
        logger.debug("Not executing SystemPrepare hook as manifest indicates it should be skipped.")
        return
    patchRunnerStageDir = _getPatchRunnerStageDir(prepareSpec.stageDirectory)
    userData = prepareSpec.userData or {}
    _executePatchRunnerAndSupplyUserData("system-prepare",
                                         patchRunnerStageDir,
                                         userData,
                                         prepareSpec.outputFile,
                                         reportCompletion=True)

@extend(Hook.Prepatch)
def prepatch(prepatchSpec):
    patchRunnerStageDir = _getPatchRunnerStageDir(prepatchSpec.stageDirectory)
    userData = prepatchSpec.userData or {}
    _executePatchRunnerAndSupplyUserData("prepatch",
                                         patchRunnerStageDir,
                                         userData,
                                         prepatchSpec.outputFile,
                                         reportCompletion=True)

@extend(Hook.Patch)
def patch(patchSpec):
    patchRunnerStageDir = _getPatchRunnerStageDir(patchSpec.stageDirectory)
    userData = patchSpec.userData or {}
    _executePatchRunnerAndSupplyUserData("patch",
                                         patchRunnerStageDir,
                                         userData,
                                         patchSpec.outputFile,
                                         reportCompletion=True)
