# Copyright (c) 2019-2021 VMware, Inc.  All rights reserved. -- VMware Confidential
# coding: utf-8

import logging.handlers
import os
import sys
import argparse
import shutil

import utils

SERVICE_NAME = "wcp"
LOG_FILENAME = '/var/log/vmware/applmgmt/wcp_patching_orchestrator.log'

logger = logging.getLogger()
logger.setLevel(logging.INFO)
formatter = logging.Formatter('[%(asctime)s] - %(name)s - %(levelname)s - \
%(message)s')
# Add the log message handler to the logger, make logging file in rotation
# when each file reaches 10MB size. Tha max number of backed-up log files is 5.
handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=10485760,
                                               backupCount=5)
handler.setFormatter(formatter)

logger.addHandler(handler)

sys.path.append(os.environ['VMWARE_PYTHON_PATH'])
from cis.defaults import get_component_home_dir
# Import directory where wcpsvc_configure lies in since we need to re-register
# extensions.xml to vpxd after patching.
sys.path.append('/usr/lib/vmware-wcp/py-modules')
sys.path.append(os.path.join(get_component_home_dir('cm'), 'bin'))
# Import patches python directory where all individual python modules reside.
sys.path.append(utils.PATCHES_DIR)


def doPatching(patching_in_post_rpm):
    """
    Do day0 patching for WCP service on VC.
    :param patching_in_post_rpm: Flag of whether patchingOrchestrator is now
           triggered in post-rp script or wcpsvc pre-start script.
    :type patching_in_post_rpm: bool
    """
    logger.info("Do patching in %s script." %
                ("rpm post-install" if patching_in_post_rpm
                 else "wcp pre-start"))

    # Get the wcp version
    wcp_version = utils.getCurrentVersion(utils.CONFIG_DIR,
                                          utils.PRESERVED_VERSION_FILE_NAME)

    if wcp_version is None:
        # day0 patching only brings service from 1 to n instead of instroducing
        # new service to VC.
        # If there was WCP version file staged during pre-patch, then
        # wcp was not installed on VC.
        # Thus, day0 patching stops if there is no WCP service on VC.
        logger.info("There was no WCP service, stop patching.")
        return

    logger.info("Prepare to do incremental patching in %s script." %
                ("rpm post-install" if patching_in_post_rpm
                 else "wcp pre-start"))
    doIncrementalPatching(patching_in_post_rpm)


def doIncrementalPatching(patching_in_post_rpm):
    """
    Incrementally applies all applicable patches listed in version2patch
    :param patching_in_post_rpm: Flag of whether patchingOrchestrator is now
           triggered in post-rp script or wcpsvc pre-start script.
    :type patching_in_post_rpm: bool
    """

    applicable_patches = utils.getApplicableSortedPatches()

    if applicable_patches is None:
        logger.info("No applicable patches specified")
        return

    logger.info("There are %d applicable patches during %s script." %
                (len(applicable_patches), "post-rpm install"
                 if patching_in_post_rpm else "wcp pre-start"))

    for module_path in applicable_patches:
        logger.info("Applying patch %s." % (module_path))
        try:
            mod = __import__(module_path)
        except Exception as e:
            err_msg = "Failed to import wcp patch module %s! Error: %s."
            logger.error(err_msg % (module_path, str(e)))
            raise Exception(err_msg % (module_path, str(e)))
        try:
            if patching_in_post_rpm:
                mod.doPatchingWithoutDependencies()
            else:
                mod.doPatchingWithDependencies()
        except Exception as e:
            err_msg = "Failed to patch %s! Error: %s"
            logger.error(err_msg % (module_path, str(e)))
            raise Exception(err_msg % (module_path, str(e)))
        status_message = "Applied patch %s, changes: %s"
        logger.info(status_message % (module_path, mod.getChanges()
                    if hasattr(mod, "getChanges") else "undefined."))

    logger.info("All applicable patches during %s applied successfully." %
                ("post-rpm install" if patching_in_post_rpm
                 else "wcp pre-start"))


def suppressPatch(patching_in_post_rpm):
    """
    Function to suppress the day0 patchingOrchestrator when:
    1. It is in normal wcp service start/restart so that the patching
       orchestrator cannot be triggered by wcp service pre-start script.
    2. The ongoing patching is being performed by another entity,
       e.g. B2B patching.
    :param patching_in_post_rpm: Flag of whether patchingOrchestrator is now
           triggered in post-rp script or wcpsvc pre-start script.
    :type patching_in_post_rpm: bool
    :return: Whether patchingOrchestrator needs to be suppressed or not.
    :rtype: bool
    """
    # Determine if B2B patching is in progress by checking if a marker file
    # exists on file system.
    # Note:
    # - Both B2B and day0 patching invoke same patching scripts.
    # - Day0 patching trigger is in wcp rpm install and wcp service
    #   startup scripts.
    # - B2B invokes rpm install, service startup too.
    # To avoid double patching, we suppress Day0 patching code path during B2B
    # patching.
    if os.path.exists(utils.WCP_B2B_PATCHING_IN_PROGRESS):
        logger.info('B2B patching is applied, suppress patchingOrchestrator.')
        return True

    # Determine if day0 patching is in progress by checking if a marker file
    # exists on file system generated by patchingOrchestrator triggered in
    # post-rpm script.
    # When wcp service pre-start script is triggered, if there is a temp
    # marker file generated by post-rpm script, it means day0 patching is in
    # progress. Otherwise, day0 patching is suppressed since it is a normal
    # wcp service start-up.
    if patching_in_post_rpm:
        logger.info("Day0 patching is in progress.")
        return False
    else:
        if os.path.exists(utils.WCP_DAY0_PATCHING_IN_PROGRESS) or \
          os.path.exists(utils.WCP_RDU_IN_PROGRESS):
            return False
        else:
            return True


def generateMarkerFile(patching_in_post_rpm):
    """
    Function to generate temporary marker file when day0 patching is in
    progress.
    The temporary marker file is generated by patchingOrchestrator triggered
    during post-rpm install script. In order to distinguish between wcp
    pre-start during day0 patching and normal wcp service starts, this
    temporary marker file will be consumed by patchingOrchestrator triggered
    in wcp pre-start script.
    :param patching_in_post_rpm: Flag of whether patchingOrchestrator is now
           triggered in post-rp script or wcpsvc pre-start script.
    :type patching_in_post_rpm: bool
    """

    if patching_in_post_rpm:
        if not os.path.exists(
          os.path.dirname(utils.WCP_DAY0_PATCHING_IN_PROGRESS)):
            os.makedirs(os.path.dirname(utils.WCP_DAY0_PATCHING_IN_PROGRESS))
        open(utils.WCP_DAY0_PATCHING_IN_PROGRESS, 'a').close()


def reRegisterExtension():
    """
    There may be some task ids added to extensions.xml during VC patching
    so that we need to re-register extensions.xml to vpxd.
    Re-register extensions.xml to vpxd after upon patching in WCP service
    pre-start since it relies on vpxd started before.
    """
    import wcpconfigure
    try:
        logger.info("Re-register WCP extensions.xml to vpxd upon re-starting"
                    "WCP service")
        wcpconfigure.register_extension_with_vc()
    except Exception as ex:
        logger.error('Failed to register extension with VC. Err %s' % ex)


def cleanupMarkerFile(patching_in_post_rpm):
    """
    Clean up wcp_version_preserved.txt file after patching in wcp prestart.
    On successful patching, remove marker file.
    On unsuccessful patching, marker file is not removed and patching will
    be retried if called again.
    :param patching_in_post_rpm: Flag of whether patchingOrchestrator is now
           triggered in post-rp script or wcpsvc pre-start script.
    :type patching_in_post_rpm: bool
    """

    if not patching_in_post_rpm:
        preserved_version_file = os.path.join(utils.CONFIG_DIR,
                                              utils.PRESERVED_VERSION_FILE_NAME)
        if os.path.exists(preserved_version_file):
            os.remove(preserved_version_file)

        if os.path.exists(utils.WCP_DAY0_PATCHING_IN_PROGRESS):
            logger.debug("Clean up marker file generated in day0 patching.")
            os.remove(utils.WCP_DAY0_PATCHING_IN_PROGRESS)


def createWcpSymlink():
    """
    Function for creating symlinks for WCP OVF files and spherelet vibs after
    moving the installed location from /storage/core/vmware-wcp to
    /storage/lifecycle/vmware-wcp, symlinks to be created are:
    1. The old installed directory for WCP AGENT OVF files since there may be
    some services still consuming WCP OVF in old location.
    2. The old directory for webserver since there may still be some services
    accessing WCP service from the old webserver link.
    3. The new directory where wcp webserver need to be placed in case we have
    changes accessing WCP service from the prospect new webserver link.
    Moreinfo:
    https://confluence.eng.vmware.com/pages/viewpage.action?spaceKey=~fdeng
    &title=Create+WCP+symlinks+during+wcpsvc+pre-start
    TODO: Cleaning up some symlinks, tracked in:
    https://jira.eng.vmware.com/browse/VKAL-3198?src=confmacro
    """
    # Location where WCP AGENT OVF files and spherelet vibs are installed
    _WCP_INST_DIR = "/storage/lifecycle/vmware-wcp"
    # The old installed directory for WCP AGENT OVFs and spherelet vibs
    _OLD_WCP_INST_DIR = "/storage/core/vmware-wcp"
    # The old directory for webserver
    _OLD_WEBSERVER_DIR = "/opt/vmware/share/htdocs"
    # The new directory where wcp webserver need to be placed
    _NEW_WEBSERVER_DIR = "/usr/lib/vmware-wcp"

    logger.info("Setting up symlinks for wcp agent ovfs and spherelet vibs")
    # If the installed location on VC hasn't been moved to /storage/lifecycle
    # skip this symlink creation patch.
    if not os.path.exists(_WCP_INST_DIR):
       logger.info("There is no WCP AGENT OVFs and spherelet vibs installed "
                   "in {}, skip patching.".format(_WCP_INST_DIR))
       return

    wcp_comps = ["wcpagent", "spherelet"]
    wcp_symlink_dirs = [_OLD_WCP_INST_DIR, _OLD_WEBSERVER_DIR,
                        _NEW_WEBSERVER_DIR]

    for wcp_comp in wcp_comps:
        symlink_source = os.path.join(_WCP_INST_DIR, wcp_comp)
        if not os.path.exists(symlink_source):
            logger.info("There is no {0} installed in {1}, skip symlink."
                        "creation".format(wcp_comp, symlink_source))
            return
        for wcp_symlink_dir in wcp_symlink_dirs:
            symlink_target = os.path.join(wcp_symlink_dir, wcp_comp)
            logger.info("Setting up symlink {}".format(symlink_target))

            # Since patchingOrchestrator is triggered every time wcp service
            # starts, there are cases that target symlinks are already created
            # before. Skip the symlink creation if it exists already.
            # There is cases when VC patching from M9 to main where old
            # wcpovf.rpm is removed from VC, leaving empty directory in symlink
            # target location. Clear it if exists.
            if os.path.exists(symlink_target):
                if os.path.islink(symlink_target) and\
                   os.path.realpath(symlink_target) == symlink_source:
                    logger.info("Symlink {} exists.".format(symlink_target))
                    continue
                else:
                    logger.info("Try removing existing file {}, since a "
                                "new symlink with same name will be created."
                                .format(symlink_target))

                    try:
                        shutil.rmtree(symlink_target)
                    except Exception as e:
                        logger.exception("Failed to remove file: {0}: {1}."
                                         .format(symlink_target, e))
                        raise

            # Create the directory for new symlink if not exist.
            if not os.path.exists(os.path.dirname(symlink_target)):
                logger.info("Try creating directory {0} for symlink {1}"
                            .format(os.path.dirname(symlink_target),
                                    symlink_target))
                try:
                    os.mkdir(os.path.dirname(symlink_target))
                except Exception as e:
                    logger.exception("Failed to create directory {0}: {1}"
                                     .format(os.path.dirname(symlink_target),
                                             e))
                    raise

            # Create symlink
            try:
                logger.info("Try creating symlink from {0} to {1}."
                            .format(symlink_source, symlink_target))
                os.symlink(symlink_source, symlink_target)
            except Exception as e:
                logger.exception("Failed to create symlink from {0} to "
                                 "{1}: {2}"
                                 .format(symlink_source, symlink_target, e))
                raise


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Option for whether patching\
 needs dependency. If option set, patching happens in wcp prestart script.\
 Otherwise, patching happens in rpm post install script.')
    parser.add_argument("-w", "--withDependency", action='store_true')

    args = parser.parse_args()

    # patchingOrchestrator is triggered in rpm post-install script and wcpsvc
    # pre-start script. If args.withDependency is set to true it means now we
    # are in wcpsvc prestart, otherwise, we're patching in post-rpm.
    patching_in_post_rpm = False if args.withDependency else True

    # When patchingOrchestrator is triggered during wcp pre-start script,
    # create symlinks for wcp agent ovf files and spherelet vibs if there are
    # no symlinks generated after moving the installed location from
    # /storage/core/vmware-wcp to /storage/lifecycle/vmware-wcp
    # Moreinfo:
    # https://confluence.eng.vmware.com/pages/viewpage.action?spaceKey=~fdeng
    # &title=Create+WCP+symlinks+during+wcpsvc+pre-start
    if patching_in_post_rpm is False:
        createWcpSymlink()

    if suppressPatch(patching_in_post_rpm):
        sys.exit(0)

    generateMarkerFile(patching_in_post_rpm)
    doPatching(patching_in_post_rpm)
    # Re-register extension.xml to vpxd during patching in WCP service
    # pre-start during VC patching since there may be new task-ids added
    if patching_in_post_rpm is False:
        reRegisterExtension()
    cleanupMarkerFile(patching_in_post_rpm)
