# Copyright 2017 Vmware, Inc. All rights reserved. -- VMware Confidential
#
'''This script defines extension points where patching process can be extended.
Each plugin/component should implement those extension-points it wants to hook
to. The sample of the plugin module structure can be:

@extend(Hook.Discovery)
def discover(context):
    pass

@extend(Hook.Requirements)
def collectRequirements(context):
    pass

@extend(Hook.Validation)
def validate(context):
    pass

@extend(Hook.SystemPrepare)
def prepare(context):
    pass

@extend(Hook.Patch)
def patch(context):
    pass


'''

import logging
from shutdown_executor import shutdownHookExecutor, ShutdownhookPriority
from status_reporting_sdk.json_utils import pruneSensitiveData

logger = logging.getLogger(__name__)

# =============================================================================
# Extensions
# =============================================================================

class Extension(object):
    def __init__(self, module, hook):
        ''' Creates a new instance of a plug-in implementing a patch extension
        point.

        @param module: A module representing plug-in (e.g. component) which
          implements an extension-points
        @type module: module

        @param hook: Name of the hook representing the extension-point.
        @type hook: str

        @raise ValueError:  If module or hook are invalid.
        '''
        if module is None:
            raise ValueError('Illegal module: %s' % module)
        if hook is None:
            raise ValueError('Illegal hook: %s' % hook)

        self.module = module
        self.hook = hook
        self.extension = self._findExtension(module, hook)

    @staticmethod
    def _findExtension(module, hook):
        ''' Get the extension of this plug-in by the given hook representing
        the actual extension-point.

        @param module: A module representing plug-in (e.g. component) which
          implements an extension-points
        @type module: module

        @param hook: Name of the hook representing the extension-point.
        @type hook: str
        '''
        # Get all attributes from the module
        attributes = [getattr(module, x) for x in dir(module)]
        # Get all extensions from the module
        extCallables = [a for a in attributes if callable(a) and hasattr(a, 'hook')]
        # Get the first extensions implementing given extension-point represented
        # by the hook
        for extCallable in extCallables:
            if extCallable.hook == hook:
                logger.info("Found patch hook %s:%s'", module, hook)
                return extCallable
        # Return nothing if there is no extension implementing extension-point
        # with given hook name
        logger.info("Unable to find upgrade hook %s:%s'", module, hook)
        return None

    def __call__(self, *args):
        ''' Invoke the extension implementing the actual extension-point
        represented by the hook.

        @param args: Arguments needed to the extension.
        '''
        if self.extension is None:
            # just do logging since some extensions won't implement all
            # extension-points/hooks
            logger.info("Hook '%s' is not defined in component '%s'. Skip calling it.",
                        self.hook, self.module)
            return

        # Call the extension-point
        logger.info("Executing patch hook '%s:%s' with context %s.",
                    self.module, self.hook, pruneSensitiveData(*args)) #pylint: disable=E1120
        result = self.extension(*args)
        logger.info("The component script returned '%s'", result)
        return result

# =============================================================================
# Extension Points
# =============================================================================

def extend(name):
    '''Decorator for extension functions that adds the extension point name
    attribute to the function so that it is recognized as an extension by the
    patching process.

    @param name: The hook name.
    '''
    def decorator(func):
        def _func(*args):
            return func(*args)
        res = _func
        res.hook = name
        res.__name__ = func.__name__
        return res
    return decorator

#pylint: disable=W0105
class Hook(object):
    ''' Definition of patch extension points.
    '''
    '''The discovery phase is the first patch hook which is executed, and the
    components can decide if it needs to take part in the patching process or
    not. If a component returns valid DiscoveryResult, then next patching hooks
    will be executed for that component.
    The extension has following signature:
      DiscoveryResult discover(PatchContext sharedCtx) throw UserError
    '''
    Discovery = "ComponentDiscovery"

    '''The requirements is the phase where the components can define their
    patching requirements. In this hook, the components are usually connecting
    to their service, validate product data consistency, etc. In this phase
    a customer can be asked for an input.
    The extension has following signature:
      RequirementsResult collectRequirements(PatchContext sharedCtx)
    '''
    Requirements = "CollectRequirements"

    '''At validation phase a component can validate the customer input, and
    also execute more complex, time-consuming validation. This will be the last
    validation phase, before the actual patching begins.
    The extension has following signature:
      ValidationResult validate(PatchContext sharedCtx)
    '''
    Validation = "Validation"

    '''At Expand phase a component has to extend its state on the source machine
    to be compatible with the old and the new version of the binaries.
    The extension has following signature:
      void expand(PatchContext sharedCtx)
    '''
    Expand = "Expand"

    '''At SwitchOver phase the switchover has already been executed and a component can
    execute any switchover logic before the services are started.
    The extension has following signature:
      void switchOver(PatchContext sharedCtx)
    '''
    SwitchOver = "SwitchOver"

    '''At system prepare phase a component can run any code that need to prepare the system
    for installation of the rpms.
    The extension has following signature:
      void prepare(PatchContext sharedCtx)
    '''
    SystemPrepare = "SystemPrepare"

    '''At prepatch phase a component prepare its service for patch such as
    stop the service, exports its data, etc.
    The extension has following signature:
      void prePatch(PatchContext sharedCtx) throw UserError
    '''
    Prepatch = "Prepatch"

    '''At patch phase the latest rpm bits have been already deployed, and
    now a component can patch the product configuration, database, etc before
    its service be started. The component dependencies defined in Discovery
    phase will be respected, and dependent services will be patched and started
    before the actual component's patch hook is executed.
    The extension has following signature:
      void patch(PatchContext sharedCtx) throw UserError
    '''
    Patch = "Patch"

    '''At Contract phase a component has to remove any leftover state that is no longer
    needed by its new version.
    The extension has following signature:
      void contract(PatchContext sharedCtx)
    '''
    Contract = "Contract"

    ''' 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. It, like other phases, honours the order of the components defined
    in the Discovery phase
    '''
    OnSuccess = "OnSuccess"

    '''In the event of any failure during a nondisruptive upgrade the revert hook is executed.
    Each component has to revert the changes done during the extend phase.
    The extension has following signature:
      void revert(PatchContext sharedCtx)
    '''
    Revert = "Revert"


def addShutdownHook(callback, context=None, timeout=60):
    '''Adds custom shutdown hook. The callback will be invoked in case the
    shutdown signal from external source is received and the context will
    be passed to the callback. This callback will not be invoked if the
    program exits normally, when the last non-daemon thread exits or when
    the exit (equivalently, system.exit) method is invoked. If the callback
    has been already registered its context and timeout would be updated.
      If there are no provided custom shutdown hooks, Upgrade infrastructure by
    default will find and kill every external program which has been started
    but it is still running.
      If the callback complete, the Upgrade infrastructure will check if there
    are still processes which has been started but not completed. If there are
    such, Upgrade infrastructure will go and kill them.
      If the callback does not complete in <timeout> seconds then, the Upgrade
    infrastructure will interrupt the callback and will go to default shutdown
    behavior: go and kill every external command which is still running.

    @param callback: Shutdown callback handler
    @type callback: Function having the follow definition:
      def _myShutdownHook(context):
          pass

    @param context: Shutdown context passed to the shutdown hook
    @type context: Any object

    @param timeout: Timeout in seconds is the window in which the shutdown
      hook must complete
    @type timeout: int

    @raise ValueError: In case the callback is None
    @raise KeyError: If that callback has been already registered
    '''
    shutdownHookExecutor.addShutdownHook(callback, context, timeout,
                                     ShutdownhookPriority.COMPONENT_HOOK)

def removeShutdownHook(callback):
    '''Removes the already registered shutdown hook.

    @param callback: Already registered shutdown callback handler
    @type callback: Function

    @raise KeyError: In case the callback has not been registered or already
    unregistered.
    '''
    shutdownHookExecutor.removeShutdownHook(callback)
