#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
:file:      script/gspylib/common/OperationStep.py
:date:      2019-03-12
:license:   2012-2019, Huawei Tech. Co., Ltd.
:brief:     OM operation step file management module.
:details:   Record OM operation and related information, provide reentry and rollback credentials.
"""

# adapt to python3
from __future__ import print_function, absolute_import, division

# export module
__all__ = ["OperationStepManager"]

# system import
import os
import sys
import socket

# local import.
try:
    from gspylib.common.GaussLog import Logger as _Logger
    from gspylib.common.ErrorCode import ErrorCode as _ErrorCode
    from gspylib.common.ErrorCode import OmError as _OmError
    from gspylib.common.Common import DefaultValue as _DefaultValue
    from gspylib.common.OMCommand import OMCommand as _OMCommand
    from gspylib.threads.SshTool import SshTool as _SshTool
    from local.LocalOperationRecord import OperationStepOpener as _OperationStepOpener
    from local.LocalOperationRecord import OperationStep as _OperationStep
    from local.LocalOperationRecord import OperationRecordManager as _OperationRecordManager
    from local.LocalOperationRecord import CHECK_FAILED as _CHECK_FAILED
except ImportError as ie:
    sys.exit("[GAUSS-52200] : Unable to import module: %s." % str(ie))

# global variable.
_logger = _Logger.getLogger()
_key = "license"


class OperationStepManager(_OperationStepOpener):
    """
    The OM component operation steps management.
    """
    # Set the maximum timeout for step file distribution operation.
    _OPERATION_STEP_MAX_TIMEOUT = 60 * 5

    def __init__(self, _stepFile, _hosts, _user, _localLog=""):
        """
        Initialize the OM component operation steps management.

        :param _stepFile:   The operation step file that need to be locked.
        :param _hosts:      List of hosts to which action step files need to be distributed.
        :param _user:       The cluster user name.
        :param _localLog:   The local script log file path.

        :type _stepFile:    str
        :type _hosts:       List[str]
        :type _user:        str
        :type _localLog:    str
        """
        if not os.path.exists(_stepFile):
            os.mknod(_stepFile, _DefaultValue.KEY_FILE_PERMISSION)
        elif os.path.exists(_stepFile) and not os.path.isfile(_stepFile):
            raise _OmError(_ErrorCode.GAUSS_502["GAUSS_50210"] % _stepFile)

        _OperationStepOpener.__init__(self, _stepFile)

        # Store the host list.
        self._hosts = _hosts
        # Store the cluster user name.
        self._user = _user
        # Store the ssh tool instance.
        self._sshTool = _SshTool(_hosts, _key, timeout=self._OPERATION_STEP_MAX_TIMEOUT)
        # Store the local script log file path.
        self._localLog = _localLog

    @property
    def stepFile(self):
        """
        Get the operation step file path.

        :return:    Return the operation step file path.
        :rtype:     str
        """
        return self._file

    def initializeOperationStep(self, *args):
        """
        Create the directory that will be used to store the operation step file, and initialize the operation step file.

        :param args:    The input steps.
        :type args:     str

        :rtype: None
        """
        _logger.debug("Create the operation step directory, and initialize the operation step file.")

        # Initialize the operation step file.
        self._registerOperationStep(*args)
        # Distribute operation step file to the given hosts.
        self._distributeOperationStepFile()

        _logger.debug("Create the operation step directory, and initialize the operation step file successful.")

    def cleanOperationStep(self):
        """
        Clean the operation step file and remove the operation step directory.

        :rtype: None
        """
        _logger.debug("Clean the operation step file and remove the operation step directory.")

        # Remove the operation step directory.
        cmd = "if [ -f \"%(file)s\" ]; then rm -rf \"%(file)s\"; fi" % {"file": self._file}
        _logger.debug("Remove the operation step file on all nodes, execute command: %s" % cmd)
        status, output = self._sshTool.getSshStatusOutput(cmd)
        if self.__parseSSHResult(status, output) is None:
            raise _OmError(_ErrorCode.GAUSS_514["GAUSS_51400"] + "\nOutput:\n", cmd, output)

        # Get the directory of the operation step files.
        operationStepDir = os.path.dirname(self._file)
        # Remove the operation step directory.
        cmd = "if [ -d \"%(path)s\" ] && [ \"$(ls %(path)s)\"x == \"\"x ]; then rm -rf \"%(path)s\"; fi" % \
              {"path": operationStepDir}
        _logger.debug("Remove the operation step directory on all nodes, execute command: %s" % cmd)
        status, output = self._sshTool.getSshStatusOutput(cmd)
        if self.__parseSSHResult(status, output) is None:
            raise _OmError(_ErrorCode.GAUSS_514["GAUSS_51400"] + "\nOutput:\n", cmd, output)

        _logger.debug("Successfully clean the operation step file and remove the operation step directory.")

    def checkOperationLock(self, _hosts=None):
        """
        Check whether file locks exist in the current cluster.

        :param _hosts:      List of hosts to which action step files need to be distributed.
        :type _hosts:       List[str]

        :rtype: None
        """
        _logger.debug("Check whether file locks exist in the current cluster.")

        # Set default host list.
        if not _hosts:
            _hosts = self._hosts

        cmd = _OMCommand.getLocalScript("Local_Operation_Record")
        cmd += " -t %s" % _OperationRecordManager.ACTION_STEP_CHECK_LOCK
        cmd += " -f %s" % self._file
        cmd += " -H %s" % socket.gethostname()
        if self._localLog:
            cmd += " -l %s" % self._localLog

        _logger.debug("Execute the shell command to check the operation lock: %s", cmd)
        status, output = self._sshTool.getSshStatusOutput(cmd, _hosts)
        results = self.__parseSSHResult(status, output)
        if results is None:
            _logger.error("Execute the shell command failed: %s", output)
            raise _OmError(_ErrorCode.GAUSS_514["GAUSS_51400"], cmd)

        for host, result in results.items():
            if result.find(_CHECK_FAILED) >= 0:
                raise _OmError(_ErrorCode.GAUSS_516["GAUSS_51646"], host)

        _logger.debug("Successfully check whether file locks exist in the current cluster.")

    def checkReEntry(self):
        """
        Check whether the current operation is re-entrant.

        Note that this method cannot check whether the re-entrant operation parameters are the same as the last
         failed operation.

        :return:    Return whether the current operation is re-entrant.
        :rtype:     bool
        """
        _logger.debug("Check whether the current operation is re-entrant.")

        cmd = _OMCommand.getLocalScript("Local_Operation_Record")
        cmd += " -t %s" % _OperationRecordManager.ACTION_STEP_GET
        cmd += " -f %s" % self._file
        if self._localLog:
            cmd += " -l %s" % self._localLog

        _logger.debug("Execute the shell command to check the re-entrant flag: %s", cmd)
        status, output = self._sshTool.getSshStatusOutput(cmd)
        results = self.__parseSSHResult(status, output)
        if results is None:
            raise _OmError(_ErrorCode.GAUSS_514["GAUSS_51400"] + "\nOutput:\n", cmd, output)

        for host, result in results.items():
            if self._step - result < -1 or self._step - result > 1:
                raise _OmError(_ErrorCode.GAUSS_516["GAUSS_51647"] % str(results))
            elif self._step < result and self._step.hostName == host:
                _logger.debug("Possibly the distribution step file was not completed at rollback.")
                self._step.setStep(result)
            elif self._step > result and self._step.hostName == host:
                _logger.debug("Possibly the distribution step file was not completed at process.")
                self._step.setStep(result)

        _logger.debug("Successfully check whether the current operation is re-entrant.")

        return not self._step.isStart

    @property
    def currentStep(self):
        """
        Get the current operation step string.

        :return:    Return the current operation step instance.
        :rtype:     OperationStep
        """
        if self._step is None:
            raise _OmError(_ErrorCode.GAUSS_500["GAUSS_50013"], "Operation Step")

        return self._step

    def nextStep(self):
        """
        Go to the next step and save it in the step file.

        :return:    Return current step after moving.
        :rtype:     str
        """
        _logger.debug("Go to the next step and save it in the step file.")

        # Go to the next step.
        self._step.nextStep()
        # Save current step instance into the step file.
        self._dumpStepInstance()
        # Distribute the operation step file to the cluster nodes.
        self._distributeOperationStepFile()

        if not self._step.isEnd:
            _logger.debug("Running step: %s", str(self._step))
        else:
            self.cleanOperationStep()

        return self._step.step

    def rollbackStep(self):
        """
        Rollback to the previous step and save it in the step file.

        :return:    Return current step after moving.
        :rtype:     str
        """
        _logger.debug("Rollback to the previous step and save it in the step file.")

        # Go to the previous step.
        self._step.rollbackStep()
        # Save current step instance into the step file.
        self._dumpStepInstance()
        # Distribute the operation step file to the cluster nodes.
        self._distributeOperationStepFile()

        if not self._step.isStart:
            _logger.debug("Rollback step: %s", str(self._step))

        return self._step.step

    def setOperationStepAttr(self, **kwargs):
        """
        Set the operation step instance attribute, and dump it to operation step file.

        :param kwargs:  The additional attribute of operation step instance.
        :type kwargs:   str | List[str] | List[int]

        :rtype: None
        """
        _logger.debug("Set the operation step instance attribute, and dump it to operation step file.")

        # Set the attributes of the operation step instance.
        for attr, value in kwargs.items():
            if hasattr(self._step, attr):
                setattr(self._step, attr, value)

        # Dump the operation step instance to the operation step file.
        self._dumpStepInstance()
        # Distribute the operation step file to the cluster nodes.
        self._distributeOperationStepFile()

    def _registerOperationStep(self, *args):
        """
        Register operation step to a step instance.

        :param args:    The input steps.
        :type args:     str

        :rtype:     None
        """
        if os.path.exists(self._file):
            try:
                self._loadStepInstance()
            except EOFError:
                if os.path.getsize(self._file) == 0:
                    pass
                else:
                    raise
            else:
                # If the step file is already registered.
                if self._step.steps != list(args):  # pylint: disable=access-member-before-definition
                    raise _OmError(_ErrorCode.GAUSS_502["GAUSS_50222"], self._file)
                # If last operation was successfully completed, but not cleaned up.
                if self._step.isEnd:  # pylint: disable=access-member-before-definition
                    _logger.debug("The last operation was successfully completed, but not cleaned up.")
                else:
                    _logger.debug("The operation step file already exist, initialize the operation step file"
                                  " successful.")
                    return

        # If step file does not exist.
        # If last operation was successfully completed, but not cleaned up.
        _logger.debug("Create a new operation step file.")
        self._step = _OperationStep()
        self._step.registerOperationStep(*args)
        self._dumpStepInstance()

    def _distributeOperationStepFile(self):
        """
        Distribute operation step file to the given hosts.

        :rtype: None
        """
        _logger.debug("Distribute the operation step file to the hosts: %s", str(self._hosts))
        if not self._hosts or len(self._hosts) == 1:
            return

        # Get the directory of the operation step files.
        operationStepDir = os.path.dirname(self._file)
        # Ensure that step directory exists.
        cmd = "mkdir -p -m %s '%s'" % (_DefaultValue.KEY_DIRECTORY_MODE, operationStepDir)
        _logger.debug("Create operation step directory on all nodes, execute command: %s", cmd)
        status, output = self._sshTool.getSshStatusOutput(cmd)
        if self.__parseSSHResult(status, output) is None:
            raise _OmError(_ErrorCode.GAUSS_514["GAUSS_51400"] + "\nOutput:\n", cmd, output)

        # distribute file to all nodes
        self._sshTool.scpFiles(self._file, operationStepDir)

    @classmethod
    def __parseSSHResult(cls, _statuses, _results):
        """
        Analytical SSH results.

        :param _statuses:   The SSH operation status on each node.
        :param _results:    SSH operation results.

        :type _statuses:    Dict[str, str]
        :type _results:     str

        :return:    Returns a list of operation results.
            If the execution result contains an error, return None.
        :rtype:     Dict[str, str] | None
        """
        for host, status in _statuses.items():
            if status != _DefaultValue.SUCCESS:
                return None

        results = {}
        for result in [line.strip() for line in _results.split("[SUCCESS]") if line and line.strip()]:
            item = [line.strip() for line in result.split("\n", 1) if line and line.strip()]
            if len(item) == 1:
                host = item[0]
                value = ""
            else:
                host = item[0]
                value = item[1]

            try:
                results.setdefault(host[:-1], eval(value))
            except (NameError, TypeError, SyntaxError, EOFError):
                results.setdefault(host[:-1], value)

        return results
