#!/usr/bin/python
# Copyright 2015 VMware, Inc.  All rights reserved. -- VMware Confidential
#
"""
PatchRunner is a standalone console application targeted to
orchestrate the patching process of existing vCSA. In addition to that, it
1. aggregates the vCSA patching script results and provide single accumulated results.
2. respects the inter-component script dependencies and call every script in right time
"""
import sys
import os
import logging
import argparse
import traceback
import json

rootDir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(os.path.join(rootDir, 'py'))
sys.path.append(os.path.join(rootDir, 'libs'))
sys.path.append(os.path.join(rootDir, 'libs', 'sdk'))
sys.path.append(os.path.join(rootDir, 'libs', 'feature-state'))
stageDir = os.path.abspath(os.path.join(rootDir, os.pardir, os.pardir))
sys.path.append(os.path.join(stageDir, 'scripts'))
import update_utils  # pylint: disable=E0401
import product_utils  # pylint: disable=E0401


if update_utils.isGateway():
    platformFssPath = "/usr/lib/vmware-vmcg/site-packages"
    transition_marker_file = os.path.join(stageDir, 'decoupled_update')
    # create a marker to indicate transition
    if not os.path.exists(platformFssPath):
        from pathlib import Path
        Path(transition_marker_file).touch()
    # if gateway platform update (and not transitioning) then change
    # the python path to use platform's FSS file
    if os.path.exists(platformFssPath) and not os.path.exists(transition_marker_file) \
            and product_utils.getUpdateProduct() is None:
        os.environ["VMWARE_PYTHON_PATH"] = platformFssPath + ":" \
                                           + os.environ["VMWARE_PYTHON_PATH"]


pythonVerLib = "python{0}".format(sys.version_info[0])
sys.path.insert(0, os.path.join(rootDir, 'libs', pythonVerLib, 'site-packages'))

import vmware_b2b.patching.utils.resource_utils as resources
resources.setRootDirectory(rootDir)


from os_utils import createDirectory
from vmware_b2b.patching.utils import logging_utils
from vmware_b2b.patching.config import settings

from vmware_b2b.patching.phases import discoverer
from vmware_b2b.patching.phases import validator
from vmware_b2b.patching.phases import system_preparator
from vmware_b2b.patching.phases import prepatcher
from vmware_b2b.patching.phases import patcher

from vmware_b2b.patching.phases import expander
from vmware_b2b.patching.phases import switcher
from vmware_b2b.patching.phases import contracter
from vmware_b2b.patching.phases import reverter

from patch_specs import PatchContext
logger = logging.getLogger(__name__)

def usage(argv):
    applName = os.path.basename(argv[0])
    print(__doc__ % applName)

def _setupLogging(logOnScreen):
    '''Setups the logging facility.
    '''

    loggingData = settings.loggingData

    # Creates log dir if does not exist
    createDirectory(loggingData.directory)

    # Setup the logging
    logFilePath = os.path.join(loggingData.directory, loggingData.rootName)
    logging_utils.setupLogging(logFilePath,
                               getattr(logging, loggingData.level),
                               logOnScreen)

def _loadAnswers(answerFile):
    '''Loads the customer answers from a file.

    @param answerFile: Location of a file where all customers answers are persited
      in json format
    @type answerFile: str

    @return: The customer answers as dictionary
    @rtype: dict
    '''
    if not answerFile:
        return {}

    if not os.path.exists(answerFile):
        raise ValueError("Given answer file '%s' does not exist" % answerFile)

    if not os.path.isfile(answerFile):
        raise ValueError("Given answer file '%s' is not a regular file" %
                         answerFile)

    try:
        with open(answerFile) as fp:
            return json.load(fp)
    except:
        logger.exception("Invalid answer file input")
        raise ValueError("Given answer file '%s' is not a regular json file" %
                         answerFile)

def _addPhaseSubParser(subParsers,
                       parserName,
                       parserHelp,
                       defaultOutputFile,
                       defaultStageDir,
                       stageDirAction,
                       callbackFunc,
                       operation):
    ''' Adds an argument parser to the given subParsers object which
    can handle the following commands:
    --userData
    --outputFile
    --stageDir
    --disableStdoutLogging

    @param subParsers: the subparsers action object returned by calling add_subparsers
    @type subParsers: argparse._SubParsersAction

    @param parserName: the name of the parser
    @type parserName: str

    @param parserHelp: The help string for the parser
    @type parserHelp: str

    @param defaultOutputFile: The default value for --outputFile argument
    @type defaultOutputFile: str

    @param defaultStageDir: The default value for --stageDir argument
    @type defaultStageDir: str

    @param stageDirAction: The action used for the --stageDir argument
    @type argparse.Action

    @param callbackFunc: The callback function which will be assigned
    to the parser
    @type callbackFunc: function

    @param operation: The operation string which will be assigned
    to the parser
    @type str
    '''

    phaseParser = subParsers.add_parser(parserName,
                                        help=parserHelp)

    phaseParser.add_argument('-u', '--userData',
                                help="Path to a file which contains user answers in "
                                     "json format",
                                type=_loadAnswers,
                                default={})
    phaseParser.add_argument('-o', '--outputFile',
                                help="Specifies the output file where the output will be stored into",
                                default=defaultOutputFile)
    phaseParser.add_argument('-d', '--stageDir',
                                help="Specifies the temporary directory where the intermediate data will be stored into",
                                default=defaultStageDir,
                                action=stageDirAction)
    phaseParser.add_argument('--disableStdoutLogging', help="Specifies whether to log on stdout",
                                action="store_true")

    phaseParser.set_defaults(callback=callbackFunc,
                                 operation=operation)

def _createCommandLineArgumentParser():
    '''Creates the command line arguments. The tool support several sub-commands:
      discovery, validate, prepatch and patch.
    '''

    defaultStageDir = os.path.abspath(os.path.join(rootDir, os.pardir, os.pardir, settings.stageDir))
    defaultOutputFile = os.path.join(defaultStageDir, settings.outputFile)
    rootParser = argparse.ArgumentParser(prog="PatchRunner")
    subParsers = rootParser.add_subparsers(title="sub-commands", dest='')
    subParsers.required = True

    class StageDirAction(argparse.Action):
        '''Action that set correct default value to outputFile if none is provided
        '''
        # adapted from documentation
        def __call__(self, parser, namespace, values, option_string=None):
            setattr(namespace, self.dest, values)
            dOutputFile = getattr(namespace, 'outputFile')
            if dOutputFile == defaultOutputFile:
                dOutputFile = os.path.join(values, settings.outputFile)
            setattr(namespace, 'outputFile', dOutputFile)

    # discovery action parser
    discoveryParser = subParsers.add_parser("discovery",
                                        help="Discovery of vCSA patching components")

    discoveryParser.add_argument('-o', '--outputFile',
                           help="Specifies the output file where the output will be stored into",
                           default=defaultOutputFile)
    discoveryParser.add_argument('-d', '--stageDir',
                           help="Specifies the temporary directory where the intermediate data will be stored into",
                           default=defaultStageDir,
                           action=StageDirAction)
    discoveryParser.add_argument('-c', '--components',
                           help="Specifies which components should take part of "
                           "the patch process. If not specified, all of them will "
                           "take part.")
    discoveryParser.add_argument('-l', '--locale',
                           help="Specifies the client locale",
                           default='en')
    discoveryParser.add_argument('--disableStdoutLogging', help="Specifies whether to log on stdout",
                             action="store_true")
    discoveryParser.add_argument('-s', '--skipPrechecksIds', nargs='+',
                                 help='Specifies by id prechecks to be skipped. if id is unknown will be ignored',
                                 default=[])
    discoveryParser.add_argument('-t', '--upgradeType',
                                 help='The type of upgrade being performed',
                                 choices=[PatchContext.DISRUPTIVE_UPGRADE, PatchContext.NONDISRUPTIVE_UPGRADE],
                                 default=PatchContext.DISRUPTIVE_UPGRADE)
    discoveryParser.set_defaults(callback=discoverer.discover,
                                 operation="Discovery of vCSA patching components")

    # validator action parser
    _addPhaseSubParser(subParsers,
                       "validate",
                       "Validate the patching of vCSA",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       validator.validate,
                       "Validate vCSA components")

    # expand action parser
    _addPhaseSubParser(subParsers,
                       "expand",
                       "Execute the component's expand functionality.",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       expander.expand,
                       "Expand the state of vCSA components")

    # switchover action parser
    _addPhaseSubParser(subParsers,
                       "switchover",
                       "Execute the component's switchover functionality.",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       switcher.switchOver,
                       "Switchover for vCSA components")

    # system preparation action parser
    _addPhaseSubParser(subParsers,
                       "system-prepare",
                       "Prepare the system for the patching of vCSA",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       system_preparator.prepare,
                       "System prepare for vCSA components")

    # prepatch action parser
    _addPhaseSubParser(subParsers,
                       "prepatch",
                       "Orchestrate the pre-patching of vCSA",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       prepatcher.prepatch,
                       "Pre-patch vCSA")

    # patch action parser
    _addPhaseSubParser(subParsers,
                       "patch",
                       "Orchestrate the patching of vCSA",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       patcher.patch,
                       "Patch vCSA")

    # contract action parser
    _addPhaseSubParser(subParsers,
                       "contract",
                       "Execute the component's contract functionality.",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       contracter.contract,
                       "Contract the state of vCSA components")

    # revert action parser
    _addPhaseSubParser(subParsers,
                       "revert",
                       "Execute the component's revert functionality.",
                       defaultOutputFile,
                       defaultStageDir,
                       StageDirAction,
                       reverter.revert,
                       "Revert for vCSA components")

    return rootParser

def main(args):
    # Parse the command line arguments
    cmdParser = _createCommandLineArgumentParser()
    parsedArgs = cmdParser.parse_args(args)

    # Setup logging
    _setupLogging(not parsedArgs.disableStdoutLogging)

    # Extract the callback arguments
    callback, operation = parsedArgs.callback, parsedArgs.operation
    callbackArgs = parsedArgs.__dict__
    del callbackArgs["callback"], callbackArgs["operation"], callbackArgs["disableStdoutLogging"], callbackArgs[""]

    # Call the operation callback
    logger.info("Start executing %s with following arguments 'PatchRunner.py %s'",
                operation, " ".join(args))

    succeed = False
    try:
        succeed = callback(**callbackArgs)

        if succeed:
            logger.info("%s succeeded", operation)
        else:
            logger.error("%s failed", operation)
    except Exception:  # pylint: disable=W0703
        logger.exception("%s got unhandled exception", operation)

    return 0 if succeed else 1

if __name__ == "__main__":
    try:
        sys.exit(main(sys.argv[1:]))
    except Exception:  # pylint: disable=W0703
        rootLogger = logging.getLogger('')
        if not rootLogger.handlers:
            sys.stderr.write('Caught unhandled exception. Error: %s\n' % traceback.format_exc())
        else:
            logger.exception('Caught unhandled exception.')

        sys.exit(1)
