# Copyright (c) 2019-2021 VMware, Inc.  All rights reserved. -- VMware Confidential
# coding: utf-8
"""
This module is the entry point for the Patch Runner.
Refer to
https://wiki.eng.vmware.com/VSphere2016/vSphere2016Upgrade/Inplace/Patch_Extensibility
"""

import logging
import os
import sys

"""
import 'VMWARE_PYTHON_PATH' since it's prerequisite of cis.tools
"""
for entry in os.environ['VMWARE_PYTHON_PATH'].split(":"):
    if entry not in sys.path:
        sys.path.append(entry)

from . import utils

from cis.tools import run_command
from l10n import localizedString, msgMetadata as _T
from extensions import Hook, extend
from patch_specs import RequirementsResult, PatchInfo, Requirements
from patch_errors import UserError
from reporting import getProgressReporter
from vcsa_utils import getComponentDiscoveryResult, isDisruptiveUpgrade
from service_manager import getServiceManager
from cis.defaults import get_component_home_dir
from fss_utils import getTargetFSS

SERVICE_NAME = "wcp"

logger = logging.getLogger(__name__)

# Import patches python directory where all individual python module resides.
sys.path.append(utils.PATCHES_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'))


@extend(Hook.Discovery)
def discover(ctx):  # pylint: disable=W0613
    """
    The function always return valid {DiscoveryResult}. This means the other
    patch hooks will always be invoked.

    :param ctx: Context given by the patch framework
    :type ctx: PatchContext
    :return: Always return {DiscoveryResult}.
    :rtype: DiscoveryResult
    """
    # Preserve the original wcp_version.txt to stage dir, since after rpm
    # installed there will be a new wcp_version.txt generated. So that we
    # need to preserve the old version for wcpsvc and do incremental patch.
    logger.info("Preserving version file")
    utils.preserveFile(ctx.stageDirectory)
    # Preserve vpxd-extension.xml to staged directory before VC patching
    # since there is re-register vpxd-extension.xml after VC patching when
    # there are changes made to the file after VC patching.
    logger.info("Preserving vpxd-extension.xml file")
    utils.preserveFile(ctx.stageDirectory, utils.__VPXD_EXTENSION_FILE__)

    if not isDisruptiveUpgrade(ctx):
        replicationConfig = {
            utils.SAVED_WCPSVC_YAML: os.path.join(
              utils.CONFIG_DIR, utils.WCPSVC_YAML),
            os.path.join(utils.CONFIG_DIR, "features.yaml"): None,
            "/etc/vmware-rhttpproxy/endpoints.conf.d/wcp-proxy.conf": None,
            os.path.join(utils.CONFIG_DIR, "guestclusters"): None,
            utils.VC_KUBERNETES_VERSIONS_FILEPATH: None,
            utils.WCP_RDU_IN_PROGRESS_SRC: utils.WCP_RDU_IN_PROGRESS,
            utils.__VERSION_FILE__: os.path.join(
              utils.CONFIG_DIR, utils.PRESERVED_VERSION_FILE_NAME)
        }
        result = getComponentDiscoveryResult(SERVICE_NAME,
            replicationConfig=replicationConfig)
        return result

    # Report how many applicable patches before really going into patching
    current_version = utils.getCurrentVersion(ctx.stageDirectory)
    # If there was a current version, it means WCP was already installed, so
    # if there is no applicable patches, we don't need to patch. However, if
    # there was no current version, it means WCP was not there, this will be
    # an install case and need to run firstboot during patch.
    if not current_version:
        logger.info("wcpsvc current version not found, \
            this is a fresh install")
    else:
        sorted_patches = utils.getApplicableSortedPatches()
        logger.info(
            "Applicable patches for wcpsvc: {}".format(sorted_patches))
        if not sorted_patches:
            logger.info("No applicable patches for wcpsvc {}. ".format(
                current_version))
            return None
    result = getComponentDiscoveryResult(SERVICE_NAME)
    return result


@extend(Hook.Requirements)
def collectRequirements(ctx):  # pylint: disable=W0613
    """
    Do prechecks for WCP service and collect requirements for VC patching.
    The results of prechecks are returned as mismatches which contain warnings
    and errors generated during prechecks.

    :param ctx: Context given by the patch framework.
    :type ctx: PatchContext
    :return: Always return {RequirementsResult}.
    :rtype: RequirementsResult
    """
    mismatches = []
    requirements = Requirements()
    patchInfo = PatchInfo()

    from . import precheck
    mismatches = precheck.doPrecheck(ctx.skipPrechecksIds)

    return RequirementsResult(requirements, patchInfo, mismatches)


@extend(Hook.Expand)
def expand(ctx):
    """
    Expand hook executed on the source.
    :param ctx: Context given by the patch framework.
    :type ctx: PatchContext
    """
    if not isDisruptiveUpgrade(ctx):
        stagedYaml = utils.getStagedWcpsvcYamlPath(ctx.stageDirectory)
        mod = __import__('wcpsvc_yaml_config')
        mod.doExpand(stagedYaml, utils.SAVED_WCPSVC_YAML)
        if not os.path.exists(os.path.dirname(utils.WCP_RDU_IN_PROGRESS_SRC)):
            os.makedirs(os.path.dirname(utils.WCP_RDU_IN_PROGRESS_SRC))
        open(utils.WCP_RDU_IN_PROGRESS_SRC, 'a').close()


@extend(Hook.Revert)
def revert(ctx):
    """
    Revert hook executed on the source
    """
    if not isDisruptiveUpgrade(ctx):
        mod = __import__('wcpsvc_yaml_config')
        mod.doRevert(utils.SAVED_WCPSVC_YAML)
        if os.path.exists(utils.WCP_RDU_IN_PROGRESS_SRC):
            os.remove(utils.WCP_RDU_IN_PROGRESS_SRC)


@extend(Hook.Contract)
def contract(ctx):
    """
    Contract hook executed on the target
    """
    if not isDisruptiveUpgrade(ctx):
      if os.path.exists(utils.WCP_RDU_IN_PROGRESS):
        os.remove(utils.WCP_RDU_IN_PROGRESS)


@extend(Hook.Prepatch)
def prePatch(ctx):
    """
    Prepatch component hook is executed right before the rpm is extracted.
    In this hook the component can stop the product service or memorize the
    product state.
    :type ctx: PatchContext.
    :throw UserError
    """
    # Since B2B patching also contains rpm installation and wcp-prestart which
    # include triggering patchingOrchestrator in day0 patching, there is a need
    # to suppress the patchingOrchestrator when B2B is being applied.
    # If now B2B patching is being applied, create a temporary marker file for
    # patchingOrchestrator.py triggered in post-rpm and wcp prestart to
    # consume.
    logger.info("B2B patching for WCP is being applied")
    if not os.path.exists(os.path.dirname(utils.WCP_B2B_PATCHING_IN_PROGRESS)):
        os.makedirs(os.path.dirname(utils.WCP_B2B_PATCHING_IN_PROGRESS))
    open(utils.WCP_B2B_PATCHING_IN_PROGRESS, 'a').close()


@extend(Hook.Patch)
def doPatching(ctx):
    """
    Do patching for wcp service. Run wcp firstboot if there is no wcp installed
    on VC. Then incrementally apply applicable patches.
    :param ctx: Context given by the patch framework.
    :type ctx: PatchContext.
    :throw UserError
    """
    if not isDisruptiveUpgrade(ctx):
        # Return if non-disruptive upgrade.
        # The patches run in this hook will either be executed in the expand
        # hook or as a startup script via the patchingOrchestrator to reduce
        # downtime and in case the Patch Hook is deprecated.
        # Any new file added here must adhere to either of the above approaches
        # for RDU.
        return

    # Check whether it is install or upgrade
    current_version = utils.getCurrentVersion(ctx.stageDirectory)
    if current_version is None:
        # If there was no original version file staged during pre-patch, then
        # wcp was not there before b2b, which means this is an install, so we
        # need to run wcp firstboot before applying incremental patches.
        logger.info("There was no WCP service, running firstboot.")
        _runFirstboot(ctx)

    logger.info("Prepare to do incremental patching.")
    doIncrementalPatching(current_version)


def _runFirstboot(ctx):
    """
    Run wcp firstboot for wcp service.
    :param ctx: Context given by the patch framework.
    :type ctx: PatchContext.
    :throw UserError.
    """

    logger.info("Running firstboot")
    for python_path in os.environ['VMWARE_PYTHON_PATH'].split(":"):
        if python_path not in sys.path:
            sys.path.append(python_path)

    # Command for running firstboot for wcp.
    # Logs are saved in /var/log/firstboot in format as
    # wcp-firstboot.py_$pid_stderr, wcp-firstboot.py_$pid_stdout
    # where $pid is the pid of the python process running the firstboot.
    action = "firstboot"
    white_list = "wcp-firstboot"
    cmd = [os.environ['VMWARE_RUN_FIRSTBOOTS'], "--action", action,
           "--subaction", action, "--fbWhiteList", white_list]
    rc, _, stderr = run_command(cmd)
    logger.info("Firstboot return code: {}".format(rc))
    if rc != 0:
        logger.error("firstboot error: {}".format(stderr))
        cause = localizedString(_T('wcp.patch.firstboot.fail', [str(stderr)]))
        raise UserError(cause)


def doIncrementalPatching(current_version):
    """
    Incrementally applies all applicable patches.
    :param current_version: Current running wcp version on VC before applying
    patches.
    :type current_version: String.
    :throw UserError.
    """
    user_error = None

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

    if current_version is None:
        logger.info("No current version provided, this means wcp was not "
                    "installed, returning all patches. ")
        # If wcp firstboot is run, stop the wcpsvc before incremental patching
        # since wcp_firstboot.py restarts wcpsvc in the end.
        serviceManager = getServiceManager()
        try:
            serviceManager.stop(SERVICE_NAME)
        except Exception:
            status = serviceManager.getStatus(SERVICE_NAME)
            err_msg = "Failed to stop WCP service! Status is %s"
            logger.error(err_msg % status)
            cause = localizedString(_T('wcp.patch.stop.fail', err_msg),
                                    [status])
            raise UserError(cause)
    else:
        logger.info("Start incremental patching on source wcpsvc version {}."
                    .format(current_version))

    length = len(applicable_patches)

    progress_reporter = getProgressReporter()
    progress_reporter.updateProgress(0, "Start WCP service patching, {} "
                                        "patches pending".format(length))

    logger.info("Applicable patches: {}".format(applicable_patches))
    cur_progress = 0
    percentage = 0
    for index in range(length):
        module_path = applicable_patches[index]

        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)))
            cause = localizedString(_T('wcp.patch.import.fail', err_msg),
                                    [module_path, str(e)])
            user_error = UserError(cause)
            break
        try:
            mod.doPatchingWithoutDependencies()
            mod.doPatchingWithDependencies()
        except Exception as e:
            err_msg = "Failed to apply patch %s! Error: %s."
            logger.error(err_msg)
            cause = localizedString(_T('wcp.patch.incrementalPatching.fail',
                                       err_msg), [module_path, str(e)])
            user_error = UserError(cause)
            break
        cur_progress = index + 1
        percentage = int((cur_progress / len(applicable_patches)) * 100)
        status_message = "Applied patch %s, changes are %s"
        status = localizedString(_T('wcp.patch.ongoing', status_message),
                                 [module_path, mod.getChanges()
                                  if hasattr(mod, "getChanges")
                                  else "undefined"])
        progress_reporter.updateProgress(percentage, status)
        logger.info("Applied patch {} for wcp.".format(module_path))

    if not user_error:
        logger.info("All patches applied successfully")
        progress_reporter.updateProgress(100, localizedString(_T(
            'wcp.patch.success',
            'Completed WCP service patching')))
        return

    elif cur_progress != 0:
        logger.error('Not all patches were applied. Latest applied patch is %s'
                     % cur_progress)
        progress_reporter.updateProgress(percentage, localizedString(_T(
            'wcp.patch.fail.partial',
            'WCP service patching completed to version %s'), cur_progress))
    else:
        logger.error("Failed to patch WCP service")
        progress_reporter.updateProgress(0, localizedString(_T(
            'wcp.patch.fail.all',
            'WCP service patching failed')))

    raise user_error


@extend(Hook.OnSuccess)
def onSuccess(ctx):
    """
    At OnSuccess phase the patch phase has finished successfully and the
    appliance is running on the new version. This phase is intended to be used
    to do any clean up or post patch configuration changes once all components
    are up-to-date.
    """

    # Clean up the temporary marker file to suppress day0 patching generated in
    # prePatch hook. This clean-up takes place in OnSuccess hook only when
    # b2b patching succeeded. If b2b patching fails, users have to revert back
    # the VCSA, which cleans up the temporary marker file as well.
    if os.path.exists(utils.WCP_B2B_PATCHING_IN_PROGRESS):
        logger.info('Clean up the temporary marker file generated in Prepatch')
        os.remove(utils.WCP_B2B_PATCHING_IN_PROGRESS)
        os.rmdir(os.path.dirname(utils.WCP_B2B_PATCHING_IN_PROGRESS))

    # Re-register vpxd-extensions.xml when B2B patching succeeded since there
    # may be new task-ids added to extensions.xml during patching.
    old_vpxd_extension_file = os.path.join(ctx.stageDirectory,
                                           utils.__VPXD_EXTENSION_FILE_NAME__)

    # Start re-register vpxd-extension.xml only when there is diff in
    # vpxd-extensions.xml before and after VC patching.
    if utils.cmpDiff(old_vpxd_extension_file, utils.__VPXD_EXTENSION_FILE__):
        import wcpconfigure
        try:
            logger.info("Re-register vpxd-extension.xml after VC patching")
            wcpconfigure.register_extension_with_vc()
        except Exception as ex:
            logger.error('Failed to register extension with VC. Err %s' % ex)
