# Copyright 2015 VMware, Inc.  All rights reserved. -- VMware Confidential
#
'''
Script responsible to setup environment and execute the patching hook of vCSA
components. The script calls only those components which have been discovered
in prior discovery phase. The patch phase respect the component dependencies
defined in @Discovery hook, and execute the component patch hook in the right
order as pay attention to start the dependent services, before the hook is
executed.
'''
import os
import logging

from extensions import Hook
from os_utils import getCommandExitCode
from service_manager import getServiceManager, ServiceStarttype
from rpm_utils import removeRpms
from l10n import msgMetadata as _T, localizedString as _
from reporting import getMessageReporter, getProgressReporter
from patch_errors import UserError, ComponentError

from vmware.patching.config import settings
from vmware.patching.data.model import PatchPhaseContext
from vmware.patching.executor.execution_facade import executeComponentHook
from vmware.patching.utils.reporting_utils import setupReporting, configureReportingFactory, \
    FRAMEWORK_COMPONENT, reportProgressError, configureLocalization

logger = logging.getLogger(__name__)

CANNOT_REMOVE_RPM_ERROR_TEXT = _T('ur.rpm.removal.error.text',
                           'Cannot remove automatically deprecated rpms %s from the system.')
CANNOT_REMOVE_RPM_ERROR_RESOLUTION = _(_T('ur.rpm.removal.error.resolution',
                                 'There are deprecated rpms that cannot be removed automatically.'
                                 ' To ensure that there will be no problems with subsequence patches'
                                 ' remove the rpms manually.'))

FAILED_TO_START_SERVICES_TEXT = _(_T('ur.services.start.text',
                                 'Failed to start all services after successful patching.'))

def _startDependentServices(c):
    '''Start a component dependent services. Assume those services are either
    new or already patched.

    @param c: A component, which dependent services has to be started
    @type c: vmware.patching.data.model.Component
    '''
    depServices = c.discoveryResult.dependentServices
    logger.info("Start component %s dependent services - %s", c.name,
                ", ".join(depServices))

    serviceManager = getServiceManager()
    for depService in depServices:
        if serviceManager.getStarttype(depService) not in [ServiceStarttype.MANUAL, ServiceStarttype.DISABLED]:
            serviceManager.start(depService)
        else:
            logger.info('Skip starting service %s as its starttype is manual/disabled.', depService)

def _patchComponents(ctx, userData, reportingQueue):
    '''Execute the patching workflow. The patching workflow first start all
    component dependent services and then call the component Patch hook.

    @param ctx: Global CPO patch phase context.
    @type ctx: vmware.patching.data.model.PatchPhaseContext

    @param userData: Customer input, as result of component questions raised
      in @Discovery patching hook.
    @type userData: dict

    @param reportQueue: A queue to be used for reporting purposes by the patching
        hook
    @type reportQueue: multiprocessor.Queue

    @return: True if patch process complete properly, and False otherwise
    @rtype bool

    @raise Exception: if patch hook execution fails.
    '''
    for c in ctx.getPatchableComponents(sort=True):
        logger.info("Patch component %s", c.name)

        # Start all dependent services before the patch hook is being called out
        _startDependentServices(c)

        executeComponentHook(Hook.Patch, ctx, c, userData, reportingQueue)

    return True

def _runOnSuccess(ctx, userData, reportingQueue):
    '''Execute the patching workflow. The patching workflow first start all
    component dependent services and then call the component Patch hook.

    @param ctx: Global CPO patch phase context.
    @type ctx: vmware.patching.data.model.PatchPhaseContext

    @param userData: Customer input, as result of component questions raised
      in @Discovery patching hook.
    @type userData: dict

    @param reportQueue: A queue to be used for reporting purposes by the patching
        hook
    @type reportQueue: multiprocessor.Queue

    @return: True if patch process complete properly, and False otherwise
    @rtype bool

    @raise Exception: if patch hook execution fails.
    '''
    for c in ctx.getPatchableComponents(sort=True):
        logger.info("OnSuccess component %s", c.name)

        try:
            # Start all dependent services before the patch hook is being called out
            executeComponentHook(Hook.OnSuccess, ctx, c, userData, reportingQueue)
        except ComponentError:
            logger.exception("Patch hook %s got ComponentWrapperError.", Hook.OnSuccess)
        except UserError:
            logger.exception("Patch hook %s got UserError.", Hook.OnSuccess)
        except Exception:
            logger.exception("PostPatch hook %s got unhandled exception.", Hook.OnSuccess)
    return True

def _startAllVMwareServices():
    '''Starts all enabled VMware services.

    @return: True if the operation succeed and False otherwise
    @rtype: bool
    '''
    command = ['/bin/service-control',
                   '--start',
                   '--all',
                   '--vmon-profile',
                   'ALL']

    logDir = settings.loggingData.directory
    outFileName = os.path.join(logDir, 'startAllServices.out.log')
    errFileName = os.path.join(logDir, 'startAllServices.err.log')

    logger.info("Starting all VMware services... The immediate command stdout "
                "is redirected to file %s and stderr is redirected to file %s",
                outFileName, errFileName)
    exitCode = getCommandExitCode(command,
                                  localStdoutFile=outFileName,
                                  localStderrFile=errFileName)
    with open(outFileName) as fp:
        out = fp.read()

    with open(errFileName) as fp:
        err = fp.read()

    logger.info("Start All VMware services: command=%s, exit-code=%s, "
                "stdout=%s, stderr=%s", command, exitCode, out, err)

    if exitCode != 0:
        raise UserError(FAILED_TO_START_SERVICES_TEXT)

def _removeDeprecatedRpms(ctx):
    ''' Remove deprecated rpms from the system

    @param ctx: Global CPO patch phase context.
    @type ctx: vmware.patching.data.model.PatchPhaseContext

    @param outputFile: File where the output of the command needs to be written
      to
    @type outputFile: str
    '''
    rpmList = set()
    for component in ctx.getAllComponents():
        rpmList = rpmList.union(component.discoveryResult.deprecatedProducts)
    rpmList=list(rpmList)  # list is needed for reporting
    failingRpms = removeRpms(rpmList)
    if failingRpms:
        getMessageReporter().postWarning(_(CANNOT_REMOVE_RPM_ERROR_TEXT, failingRpms), CANNOT_REMOVE_RPM_ERROR_RESOLUTION)

def patch(stageDir, userData, outputFile):
    '''The entry point of patch CPO phase.

    The patch phase is responsible to setup environment and execute the patching
    hook of vCSA components. The patch phase calls only those components which
    have been discovered in prior discovery phase. The patch phase respect the
    component dependencies defined in @Discovery hook, and execute the component
    patch hook in the right order as pay attention to start the dependent
    services, before the hook is executed.

    @param stageDir: global CPO stage directory. All components stage
      directories will be created as sub-directories of global stage directory.
    @type stageDir: str

    @param userData: Customer input, as result of component questions raised
      in @Discovery patching hook.
    @type userData: dict

    @param outputFile: File where the output of the command needs to be written
      to
    @type outputFile: str

    @return: True if the patch phase succeed and False otherwise
    @rtype: bool
    '''
    statusAggregator = None
    try:
        ctx = PatchPhaseContext.load(stageDir)
        configureLocalization(ctx.locale)
        # add extra component for the framework as we might throw exception from
        # framework bit and this will mess the reporting
        statusAggregator = setupReporting(outputFile, len(2 * ctx.getPatchableComponents()) + 1)
        configureReportingFactory(FRAMEWORK_COMPONENT, statusAggregator.reportingQueue)

        getProgressReporter().updateProgress(0)
        _patchComponents(ctx, userData, statusAggregator.reportingQueue)

        # TODO iradev: Move the code below to a common patching hook when
        # all components plugin the framework. Otherwise the dependencies
        # cannot be specified up front.
        _startAllVMwareServices()

        _removeDeprecatedRpms(ctx)

        _runOnSuccess(ctx, userData, statusAggregator.reportingQueue)
        getProgressReporter().success()
        return True
    except ComponentError as wrapperError:
        logger.exception("Patch hook %s got ComponentWrapperError.", Hook.Patch)
        if isinstance(wrapperError.baseError, UserError):
            reportProgressError(wrapperError.baseError, wrapperError.componentKey)
        else:
            reportProgressError(identifier=wrapperError.componentKey)
        return False
    except UserError as userError:
        logger.exception("Patch hook %s got UserError.", Hook.Patch)
        reportProgressError(userError)
        return False
    except Exception:
        logger.exception("Patch hook %s got unhandled exception.", Hook.Patch)
        reportProgressError()
        return False
    finally:
        if statusAggregator:
            statusAggregator.stop()
