"""
Copyright 2016-2021 VMware, Inc.  All rights reserved. -- VMware Confidential
"""

import logging
import os
import sys
import lxml.etree as etree
import pwd
import subprocess
import datetime

sys.path.append(os.environ['VMWARE_PYTHON_PATH'])
sys.path.append(os.path.dirname("/usr/lib/vmware-cm/bin/"))

from cis.cisreglib import (
    _get_syscfg_info, LookupServiceClient, SsoClient)
from cis.vecs import vmafd_machine_id
from cloudvmcisreg import (
    cloudvm_sso_cm_register, VecsKeyStore)
from pyVmomi import lookup
from identity.vmkeystore import VmKeyStore

# The list of patches for B2B and NDU upgrade.
PATCHES = [("1.0", "patch1_0"),
           ("3.0", "patch3_0"),
           ("4.0", "patch4_0"),
           ("5.0", "patch5_0"),
           ("6.0", "patch6_0"),
           ("7.0", "patch7_0"),
           ("8.0", "patch8_0"),
           ("9.0", "patch9_0"),
           ("10.0", "patch10_0"),
           ("11.0", "patch11_0"),
           ("12.0", "patch12_0"),
           ("13.0", "patch13_0"),
           ("14.0", "patch14_0"),
           ("15.0", "patch15_0"),
           ("15.5", "patch15_5"),
      ]

logger = logging.getLogger(__name__)
VCINTEGRITY_LOGGING_FOLDER = os.path.join(os.sep, "var", "log",
                                            "vmware", "vmware-updatemgr")

HOME_DIR = os.path.join(os.sep, 'usr', 'lib', 'vmware-updatemgr')
B2B_DIR = os.path.join(os.sep, 'usr', 'lib', 'vmware-updatemgr-root', 'b2b')
CONFIG_VERSION_FILE = os.path.join(HOME_DIR, 'config', 'version.txt')

BIN_DIR = os.path.join(HOME_DIR, 'bin')
CONFIG_DIR = BIN_DIR
INTEGRITY_XML = os.path.join(BIN_DIR, "vci-integrity.xml")
INSTALLUTILS_CONFIG_XML = os.path.join(BIN_DIR, "vciInstallUtils_config.xml")
INSTALLUTIL = os.path.join(BIN_DIR, "vmware-vciInstallUtils")
VUM_CONFIG_TXT = os.path.join(BIN_DIR, "configvalues.txt")
JETTY_VUM_SSL_XML = os.path.join(BIN_DIR, "jetty-vum-ssl.xml")

RPMSAVE_FILE_SUFFIX = ".rpmsave"
RPMNEW_FILE_SUFFIX = ".rpmnew"
UPDATED_INTEGRITY_XML = INTEGRITY_XML + RPMNEW_FILE_SUFFIX
UPDATED_JETTY_VUM_SSL_XML = JETTY_VUM_SSL_XML + RPMNEW_FILE_SUFFIX

CONFIG_DB_USERNAME_KEY = 'db_user'
CONFIG_DB_PASSWORD_KEY = 'db_password'
CONFIG_DB_TYPE_KEY = 'db_type'
CONFIG_DSN_NAME_KEY = 'db_dsn'

VERSION_SEPARATOR = "."
PATH_SEPARATOR = "/"
PROTOCOLS_TAG = "protocols"
SSL_PROTOCOLS_TAG_PATH = "./vmacore/ssl/protocols"
SSL_TAG_PATH = "./vmacore/ssl"
WEB_SSL_PORT_TAG = './plugins/ufa_agent/webSslPort'
SOAP_PORT_TAG = './plugins/ufa_agent/soapPort'
WEB_SERVER_PORT = './plugins/ufa_agent/webServerPort'

# paths and files used in the patch
METADATA_DIR = os.path.join(HOME_DIR, 'etc')
FLEX_VUM_PRODUCT_ID = 'com.vmware.vcIntegrity'
FLEX_VUM_TYPE_ID = 'vcIntegrity'
H5_VUM_PRODUCT_ID = 'com.vmware.vum'
H5_VUM_TYPE_ID = 'client'
H5_PROP_FILE_NAME = "updatemgr-h5.properties"
H5_PROP_FILE = os.path.join(HOME_DIR, 'firstboot', H5_PROP_FILE_NAME)
H5_PROP_FILE_SAVED = H5_PROP_FILE + RPMSAVE_FILE_SUFFIX

# Since updatemgr.properties is deleted from rpm specification
# It is saved as updatemgr.properties.rpmsave
FLEX_PROP_FILE_NAME = "updatemgr.properties"
FLEX_PROP_FILE = os.path.join(HOME_DIR, 'firstboot', FLEX_PROP_FILE_NAME)
FLEX_PROP_FILE_SAVED = FLEX_PROP_FILE + RPMSAVE_FILE_SUFFIX

# B2B vci-integrity.xml generic patch tags.
# Note: The relative tag paths starting with "." are not supported
#       and the paths should not include root's tag.
# The elements to be updated/set in vci-integrity.xml
MIN_HOST_VERSION_TAG_PATH = "HostUpgradeConfig/MinHostVersion"
PACKAGE_VERSIONS_TAG_PATH = "HostUpgradeConfig/PackageVersions"
VIM_NAME_SPACE_TAG_PATH = "plugins/vci_vcIntegrity/vimNameSpace"
HOST_INCLUDE_TAG_PATH = "contentSettings/hostInclude"
VAPI_PLUGIN_ENABLE_TAG_PATH = "plugins/vci_vapi/enabled"
VAPI_PLUGIN_PORT_TAG_PATH = "plugins/vci_vapi/port"

class Version:
    def __init__(self, versionStr):
        intermediateResult = versionStr.split(VERSION_SEPARATOR)
        self.__versionNums = list(map(int, intermediateResult))

    def __lt__(self, other):
        result = False
        for index, verNo in enumerate(self.__versionNums):
            if verNo < other.__versionNums[index]:
                result = True
                break
            if verNo > other.__versionNums[index]:
                break
        return result

    def __str__(self):
        return VERSION_SEPARATOR.join(str(verNo) for verNo in self.__versionNums)


class XmlMerger:
    """
    The class provides a framework to merge config from one XML file to the other
    """
    def __init__(self, oldFilePath, newFilePath):
        """
        Initialize XmlMerger with old and new XML files

        @param oldFilePath: Path to old XML file
        @param newFilePath: Path to new XML file
        """
        parser = etree.XMLParser(remove_blank_text=True)
        self.__oldFilePath = oldFilePath
        self.__newFilePath = newFilePath
        self.__oldTree = etree.parse(oldFilePath, parser)
        self.__newTree = etree.parse(newFilePath, parser)
        self.__etree = etree

    def incrementVersion(self, tagPath):
        """
        Update the element of given tag from the new file to the old file,
        where the element's text should be a version existing in both files
        and the new version should be greater than the old one.

        @param tagPath: Path of tags to the version element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return
        oldVersion = self.__oldTree.find(tagPath)
        newVersion = self.__newTree.find(tagPath)
        if (oldVersion is not None and newVersion is not None
            and Version(oldVersion.text) < Version(newVersion.text)):
            oldVersion.text = newVersion.text

    def update(self, tagPath):
        """
        Update the element of given tag from the new file to the old file,
        where the element should exist in both files.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return
        oldElem = self.__oldTree.find(tagPath)
        newElem = self.__newTree.find(tagPath)
        if oldElem is not None and newElem is not None:
            oldElem.text = newElem.text

    def addOrUpdateRootAttribute(self, attributeName):
        """
        Update the given attribute of root from the new file to the old file,
        where the attribute should exist at least in new file. If the attribute
        exists in old file it is updated, otherwise new attribute is added.

        @param attributeName: Name of the attribute which should be added or
                              the value of which should be updated
        """
        if (self.__oldTree is None or self.__newTree is None):
            return

        oldRoot = self.__oldTree.getroot()
        newRoot = self.__newTree.getroot()

        if (oldRoot is None or newRoot is None):
            return

        newAttributeValue = newRoot.get(attributeName)

        if (newAttributeValue is not None):
            oldRoot.set(attributeName, newAttributeValue)

    def getParentPath(self, tagPath):
        """
        Retrieve the path to parent element by removing the last "/" and
        the following word. If no "/", then the parent path string is empty.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        words = tagPath.split(PATH_SEPARATOR)
        # Remove the last word and its preceding "/"
        parentPath = PATH_SEPARATOR.join(words[0:-1])
        return parentPath

    def getTag(self, tagPath):
        """
        Retrieve the name of a tag (i.e., the last word) given the path to it.
        If no word found in the path, then the tag string is empty.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        words = tagPath.split(PATH_SEPARATOR)
        # Return the last word
        return words[-1]

    def replace(self, tagPath):
        """
        Copy all instances of the element (including all of its subelements)
        of given tag from the new file to the old file,
        where at least one instance of the elemet should exist in both files.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        oldElems = self.__oldTree.findall(tagPath)
        newElems = self.__newTree.findall(tagPath)
        if len(oldElems) != 0 and len(newElems) != 0:
            for elem in oldElems:
                oldParent.remove(elem)
            for elem in newElems:
                oldParent.append(elem)

    def add(self, tagPath, attribKey=None, attribVal=None):
        """
        Add a new element (including all of its subelements)
        of given tag from the new file to the old file,
        where the elemet should exist in the new file only.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        @param attribKey: Key of the attribute of the tag
        @param attribVal: Value of the attribute of the tag
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        if not attribKey and not attribVal:
            oldElem = self.__oldTree.find(tagPath)
            newElem = self.__newTree.find(tagPath)
        elif attribKey and attribVal:
            # Find oldElem
            # Return if we find exactly the same elem in oldTree
            tag = self.getTag(tagPath)
            oldElem = None
            for elem in self.__oldTree.iter(tag=tag):
                if len(elem) and elem.attrib[attribKey] == attribVal:
                    return
            # Find newElem
            # Return if we can't find the elem in newTree
            newElem = None
            for elem in self.__newTree.iter(tag=tag):
                if len(elem) and elem.attrib[attribKey] == attribVal:
                    newElem = elem
                    break
            if newElem is None:
                return
        else:
            return

        if oldElem is None and newElem is not None:
            oldParent.append(newElem)

    def set(self, tagPath, text):
        """
        Set the text of an element of given tag in the old file only.
        If the element does not exist in the old file, then create it first.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or tagPath.startswith(".") or text is None):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        tag = self.getTag(tagPath)
        if len(tag) == 0:
            return

        oldElem = self.__oldTree.find(tagPath)
        if oldElem is None:
            oldElem = self.__etree.SubElement(oldParent, tag)
        oldElem.text = text

    def delete(self, tagPath, force=False):
        """
        Delete a element (including all of its subelements)
        of given tag from the old file,
        where the elemet should exist in the old file only.

        @param tagPath: Path of tags to the element from root
                        (excluding root's tag or relative path)
        """
        tagPath = tagPath.strip()
        if (self.__oldTree is None or self.__newTree is None
            or tagPath.startswith(".")):
            return

        oldParent = self.__oldTree.getroot()
        parentPath = self.getParentPath(tagPath)
        if len(parentPath) != 0:
            oldParent = self.__oldTree.find(parentPath)

        oldElem = self.__oldTree.find(tagPath)
        newElem = self.__newTree.find(tagPath)
        if oldElem is not None and (newElem is None or force):
            oldParent.remove(oldElem)

    def addOrUpdateElements(self, path):
        """
        Replaces all the sub tags present inside the parent tag.
        @param path: path of parent tag
        """

        oldParent = self.__oldTree.xpath(path)
        newParent = self.__newTree.xpath(path)

        if oldParent[0].text is not None:
            oldParent[0].text = None
        for element in oldParent[0]:
            oldParent[0].remove(element)
        for element in newParent[0]:
            oldParent[0].append(element)

    def saveToFile(self):
        """
        Save changes into the old XML file
        """
        self.__oldTree.write(self.__oldFilePath, encoding="utf-8", xml_declaration=True, pretty_print=True)


def GetConfigVersion(filePath):
    """
    Gets current config version number from filePath.

    :return: Version of Config files
    """
    # Read version from filePath
    if os.path.exists(filePath):
        with open(filePath) as fp:
            version = Version(fp.read())
    else:
        version = Version("0.0")

    return version


def SetConfigVersion(filePath, versionNo):
    """
    Persists the latest applied patch version.

    @param filePath: Path to the component version file.

    :return: None
    """
    with open(filePath, "w") as fp:
        fp.write(str(versionNo))

def verifyConfigValues(configDict):
    configSet = set([CONFIG_DSN_NAME_KEY, CONFIG_DB_USERNAME_KEY,
        CONFIG_DB_PASSWORD_KEY, CONFIG_DB_TYPE_KEY])

    if not configSet.issubset(configDict.keys()):
        logging.error("Verification of parsed values failed. Check %s, %s, %s,"
            " %s", CONFIG_DSN_NAME_KEY, CONFIG_DB_USERNAME_KEY,
            CONFIG_DB_PASSWORD_KEY, CONFIG_DB_TYPE_KEY)
        raise Exception("Verification of parsed values failed. "
            "configvalues.txt may be corrupted")
    logging.info("Verification of configvalues.txt values successful")

def LoadConfigFile():
    """
    Load data from config file
    """
    configDict = {}
    with open(VUM_CONFIG_TXT, 'r') as f:
        lines = [line.rstrip('\n') for line in f]
        for line in lines:
            key = line[0:line.find('=')]
            val = line[line.find('=') + 1:]
            configDict[key] = val
    return configDict

def updateOwnership(path, mode=0o660):
    """
    Update the file ownership to updatemgr:updatemgr
    """
    pwnam = pwd.getpwnam('updatemgr')
    uid = pwnam.pw_uid
    gid = pwnam.pw_gid
    try:
        if os.path.exists(path):
            os.chmod(path, mode)
            os.chown(path, uid, gid)
    except Exception as e:
        logger.error("Update of ownership and permission for file %s"
                     " failed : err %s", path, str(e))
        raise Exception("Update of file ownership and persmission failed")

def CreateUpgradeDataBaseSchemaCmd(logDir):
    """
    Create command to run install util for DB upgrade

    @param logDir: directory for log file
    """
    configDict = LoadConfigFile()
    verifyConfigValues(configDict)
    command = [INSTALLUTIL,
        '-C', CONFIG_DIR,
        '-L', logDir,
        '-I', BIN_DIR,
        '-D', configDict[CONFIG_DSN_NAME_KEY],
        '-U', configDict[CONFIG_DB_USERNAME_KEY],
        '-P', configDict[CONFIG_DB_PASSWORD_KEY],
        '-T', configDict[CONFIG_DB_TYPE_KEY],
        '-O', 'dbupgrade']
    return command

def upgradeVUMTLSConfig():
    """
    This function will modify vci-integrity.xml and jetty-vum-ssl.xml to support
    most secure TLS protocols configuration (as per security best practice doc.).
    TODO: Remove the code related to jetty-vum-ssl.xml config file, once we
          decide replace file upload use-case with VAPI
    """
    import lxml.etree as etree
    parser = etree.XMLParser(remove_blank_text=True)
    # Update vci-integrity.
    # Remove unsecure tls/ssl protocols as part of b2b upgrade.
    integrityXml = etree.parse(INTEGRITY_XML, parser)
    protocols = integrityXml.find(SSL_PROTOCOLS_TAG_PATH)
    if protocols is None:
       # If protocols tag is missing in config file due any reason, add it.
       ssl = integrityXml.find(SSL_TAG_PATH)
       protocols = etree.SubElement(ssl, PROTOCOLS_TAG)

    # By default, VUM should be configured to have only TLSv1.2 support.
    protocols.text = 'tls1.2'
    with open(INTEGRITY_XML, 'wb') as outfile:
        outfile.write(etree.tostring(integrityXml, pretty_print=True))

    # Update jetty-vum-ssl.
    # Add unsecure ssl/tls protocols to "excluded protocols" list.
    # TODO: remove this code once the jetty file upload use-case is replaced with VAPI
    jettyConfigTree = etree.parse(JETTY_VUM_SSL_XML, parser)
    array = jettyConfigTree.getroot().find(".//*[@name='ExcludeProtocols']/Array")
    protocolList = {"TLSv1" : False, "TLSv1.1" : False}
    for item in array:
        if item.text in protocolList:
            protocolList[item.text] = True

    for protocol in protocolList:
        if protocolList[protocol] is False:
            item = etree.SubElement(array, 'Item')
            item.text = protocol

    with open(JETTY_VUM_SSL_XML, 'wb') as outfile:
        outfile.write(etree.tostring(jettyConfigTree, xml_declaration=True, pretty_print=True, encoding="UTF-8"))


class LSPatch(object):
    """
    Patches the changes to the vum service properties file.
    Re-registers vum with the new properties file with Lookup service.
    """

    def __init__(self):
        """
        Initializes the data used for patching
        """
        ls_url, domain_name = _get_syscfg_info()
        self.soluser = "vpxd-extension"
        self.sol_id = "{}-{}".format(self.soluser, vmafd_machine_id())
        self.ls_obj = LookupServiceClient(ls_url, retry_count=1)
        self.sts_url, self.sts_cert_data = self.ls_obj.get_sts_endpoint_data()
        self.init_sso_client()

    def init_sso_client(self):
        """
        Initializes SSO client
        """
        with VmKeyStore('VKS') as ks:
            ks.load(self.soluser)
            cert = ks.get_certificate(self.soluser)
            key = ks.get_key(self.soluser)
        self.sso_client = SsoClient(self.sts_url, self.sts_cert_data,
                              None, None, cert=cert, key=key)
        self.ls_obj.set_sso_client(self.sso_client)

    def _get_service_ports(self):
        """
        Get the service ports.
        """
        parser = etree.XMLParser(remove_blank_text=True)
        integrityXml = etree.parse(INTEGRITY_XML, parser)

        webSslPort = integrityXml.find(WEB_SSL_PORT_TAG).text
        if webSslPort is None:
            logging.error('Unable to find webSslPort in vci-integrity.xml')
            webSslPort = '9087'

        soapPort = integrityXml.find(SOAP_PORT_TAG).text
        if soapPort is None:
            logging.error('Unable to find soapPort in vci-integrity.xml')
            soapPort = '8084'

        webServerPort = integrityXml.find(WEB_SERVER_PORT).text
        if webServerPort is None:
            logging.error('Unable to find webServerPort in vci-integrity.xml')
            webServerPort = '9084'

        return (soapPort, webServerPort, webSslPort)

    def _find_service_registration(self, product, pType):
        """
        Retrieves service information given product and type.
        """
        filterSpec = lookup.ServiceRegistration.Filter()
        filterSpec.serviceType = \
            lookup.ServiceRegistration.ServiceType(
                                           product=product,
                                           type=pType)
        svcInfoList = \
            self.ls_obj.get_service_info_list(search_filter=filterSpec)
        if not svcInfoList:
            logger.error("Service registration for %s not found in Lookup service" % product)
            raise Exception("Service registration for %s not found in Lookup service" % product)
        logger.info("Found service registration for %s in Lookup service" % product)
        return svcInfoList[0]

    def _cm_sso_register(self, op, product, pType, propFile):
        """
        Helper function to peform cloudvm_sso_cm_register with a
        given operation.
        """
        svcInfo = self._find_service_registration(product, pType)
        svcId = svcInfo.serviceId

        # Get the ports from integrity.xml
        # vapiPort and soapTcpPort are internal and they remain the same.
        (soapPort, webServerPort, webSslPort) = self._get_service_ports()
        svcSpecProps = {
            'solution-user.name' : self.sol_id,
            'unsupported.action' : 'SET_STARTUPTYPE_DISABLED',
            'control.script': 'service-control-default-vmon',
            'metadata.dir': METADATA_DIR,
            'updatemgr.ext.soapPort': soapPort,
            'updatemgr.ext.webSslPort': webSslPort,
            'updatemgr.ext.webServerPort': webServerPort,
            'updatemgr.int.vapiPort': '8083',
            'updatemgr.int.soapTcpPort': '8086',
            'updatemgr.int.webServerTcpPort': '9085'
        }
        keystore = VecsKeyStore(self.soluser)

        # Invoke cisreg to perform the operation
        try:
            logger.info("Invoking Lookup Service %s operation." % op)
            svc_info = cloudvm_sso_cm_register(keystore, propFile,
                           self.soluser, dynVars = svcSpecProps, regOp = op,
                           isPatch = True)
            logger.info("Lookup Service %s operation successful." % op)
        except Exception as e:
            logger.error(e)
            logger.error("Lookup Service %s operation failed." % op)
            raise Exception("VMware Update Manager patch failed")

    def re_register_h5_endpoints(self):
        """
        Re-register VUM with Lookup service entry to update
        the H5 endpoints.
        """
        self._cm_sso_register('tryupgrade',
            H5_VUM_PRODUCT_ID,
            H5_VUM_TYPE_ID,
            H5_PROP_FILE)

        # M5 release marks the H5 properties file as config, so
        # rpmsave file would be present.
        if os.path.isfile(H5_PROP_FILE_SAVED):
            os.remove(H5_PROP_FILE_SAVED)

    def unregister_flex_endpoints(self):
        """
        Re-register VUM with Lookup service entry to remove
        the Flex endpoints.
        """
        # Find the FLEX svcId
        try:
            svcInfo = self._find_service_registration(FLEX_VUM_PRODUCT_ID,
                FLEX_VUM_TYPE_ID)
        except Exception as e:
            logger.error("Flex registration is not found. Skipping.")
            return

        svcId = svcInfo.serviceId
        try:
            self.ls_obj.unregister_service(svcId)
        except Exception as e:
            logger.error("Failed to un-register Flex Endpoint.")
            raise Exception("Failed to un-register VUM Flex Endpoint.")

        logger.info("Unregistered VUM Flex Endpoint.")

        # If the FLEX properties file exists, delete it.
        # M5 release marks the FLEX properties file as config, so
        # rpmsave file would be present.
        if os.path.isfile(FLEX_PROP_FILE_SAVED):
            os.remove(FLEX_PROP_FILE_SAVED)

    def cleanup(self):
        self.sso_client.cleanup()

############################################################################
def getApplicablePatches(curVersion, applicablePatches):
    '''
    Get the list of applicable patches
    '''
    if not PATCHES:
        return

    for ver, modulePath in PATCHES:
        version = Version(ver)
        logger.info("Patch version = %s", str(version))
        if curVersion < version:
            logger.info("Add patch %s", modulePath)
            applicablePatches.append((ver, modulePath))

def _fixupMetadataZips():
    """Normalize and filter configSchemas"""
    try:
        from . import fix_metadata
        fix_metadata.fixMetadataZips()
        logger.info("Successfully fixed config schemas")
    except ImportError:
        logger.exception("Failed to import patching module. Possibly running on"
                         " an older system.")
    except:
        logger.exception("Failed to filter config schemas")

def _updateIntegrityConfig():
    # If VUM RPM in source and target build are from same build, then RPM
    # upgrade become a no-op and no new config files copies are generated. If no
    # new config files are present (implying VUM RPM is not updated), then will
    # not execute logic to update the package version.
    if not os.path.exists(UPDATED_INTEGRITY_XML):
        logger.info('%s does not exist. Skip updating integrity Config'
                    % UPDATED_INTEGRITY_XML)
        return

    # Update vci-integrity.xml from vci-integrity.xml.rpmnew
    xmlMerger = XmlMerger(INTEGRITY_XML, UPDATED_INTEGRITY_XML)

    # Update PackageVersions and MinHostVersion
    xmlMerger.incrementVersion(PACKAGE_VERSIONS_TAG_PATH)
    xmlMerger.incrementVersion(MIN_HOST_VERSION_TAG_PATH)

    # Update VIM API version
    xmlMerger.update(VIM_NAME_SPACE_TAG_PATH)

    # Replace all hostInclude instances with those from vci-integrity.xml.rpmnew
    xmlMerger.replace(HOST_INCLUDE_TAG_PATH)

    # Update VAPI tags from vci-integrity.rpmnew
    xmlMerger.update(VAPI_PLUGIN_ENABLE_TAG_PATH)
    xmlMerger.update(VAPI_PLUGIN_PORT_TAG_PATH)

    # Save changes into vci-integrity.xml
    xmlMerger.saveToFile()

def _upgradeDatabaseSchema():
    # upgrade database schema to the latest version
    before = datetime.datetime.now()
    # Check if file vciInstallUtils_config.xml exits
    if not os.path.exists(INSTALLUTILS_CONFIG_XML):
        logger.info("InstallUtils config file doesn't exist. Skipping database"
                     " schema upgrade.")
        return

    # Run Install Util with dbupgrade
    command = CreateUpgradeDataBaseSchemaCmd(VCINTEGRITY_LOGGING_FOLDER)

    logger.info("Run Database schema upgrade ....")
    rc = subprocess.call(command)
    if rc:
        msg = "Failed to upgrade database schema. return code:"
        logger.error(msg + " %d" % rc)
        raise Exception(msg + " {0}".format(rc))
    else:
        delta = datetime.datetime.now() - before
        logger.info("DB upgrade took : %s" % (delta))
        logger.info("Database schema upgraded successfully.")

def _upgradeSecurityConfig():
    # upgrade security config
    # 1) TLS protocols in configuration files (vci-integrity, jetty-vum-ssl)

   upgradeVUMTLSConfig()
   logger.info("Security configuration upgraded successfully")


def _updateLSRegistrations():
    """
    This will update the lookup service registrations.
    """
    lsPatcher = LSPatch()

    # Update the H5 endpoint registration
    lsPatcher.re_register_h5_endpoints()

    # Cleanup
    lsPatcher.cleanup()

def applyIncrementalPatches(applicablePatches):
    '''
    Run b2b patch scripts in increments.
    '''
    if len(applicablePatches) == 0:
        return

    logger.info('Running incremental patch')
    # load the patch directory
    sys.path.append(os.path.join(B2B_DIR, "patches"))

    logger.info("Applying Patching ...")
    totalPatch = len(applicablePatches)
    for ver, modulePath in sorted(applicablePatches,
                                    key=lambda x: Version(x[0])):
        logger.info("Applying patch %s for version %s" % (modulePath, ver))
        mod = __import__(modulePath)
        mod.doPatching('NDU_B2B')
        logger.info("Patch %s applied" % (modulePath))

def doB2BPatching(srcVersion):
    '''
    The will run the incremental patches from srcVersion.
    The routine is called from the service startup to
    upgrade for NDU upgrades.
    '''
    applicablePatches = list()
    getApplicablePatches(srcVersion, applicablePatches)

    applyIncrementalPatches(applicablePatches)

    logger.info('Incremental patching completed.')
    _updateIntegrityConfig()
    logger.info('Completed updating the vci-integrity.xml file.')
    _upgradeDatabaseSchema()
    logger.info('Completed DB upgrade')
    _upgradeSecurityConfig()
    logger.info('Completed Jetty SSL update.')
    _updateLSRegistrations()
    logger.info('Completed update of Lookup service registration.')
    # Update the configuration schemas.
    fixMetadata = __import__('fix_metadata')
    fixMetadata.fixMetadataZips()

    logger.info('Completed config schemas fix for patchstore.')
    logger.info('Done applying B2B patches.')
