#!/usr/bin/python
# -*- coding: utf-8 -*-
# @brief   : 2018/10/10 16:56
# @File    : app ctrl
#  @Software: Software management infrastructure

import os
import sys
import shutil
import logging as log
import xml.etree.ElementTree as ETree
from patch import PatchMgr

MY_DIR = os.path.dirname(os.path.realpath(__file__))


LOG_PATH = '/OSM/log/cur_debug/omm/co.log'
INSTALL_DIR = '/OSM/coffer_bin/apollo_patch'
PATCH_CONF = os.path.join(INSTALL_DIR, 'patch.xml')
DATA_DIR = "/OSM/coffer_data"


RETURN_EXEC_SUCC = 0
RETURN_EXEC_FAILED = 1

RETURN_NO_NEED_UPGRADE = 1
RETURN_HOT_UPGRADE = 2
RETURN_COLD_UPGRADE = 3


class EulerFile(object):

    def __init__(self, node_type, board_type, path, script, desc=''):
        self.node_type = node_type
        self.board_type = board_type
        self.path = path
        self.script = script
        self.desc = desc


class Manifest(object):
    """
    Manifest config
    """
    def __init__(self, cfg_path):
        """

        :param cfg_path:
        """
        try:
            import yaml
            with open(cfg_path, 'r') as fp:
                self.cfg = yaml.safe_load(fp)
            self.version = self.cfg.get('Version', '')
        except:
            self.cfg = None
            self.version = ''
        return


class ApolloPatchAPP(object):
    """
    App control
    """

    def __init__(self):
        self.root_dir = os.path.dirname(MY_DIR)
        self.repo_dir = os.path.join(self.root_dir, 'repo')
        self.patch_conf = os.path.join(self.repo_dir, 'patch.xml')
        self.manifest = os.path.join(self.root_dir, 'manifest.yml')
        self._run_ver = ''
        self._pkg_ver = ''
        super(ApolloPatchAPP, self).__init__()
        return

    @property
    def pkg_ver(self):
        if not self._pkg_ver:
            self._pkg_ver = Manifest(self.manifest).version
        return self._pkg_ver

    @property
    def run_ver(self):
        if not self._run_ver:
            self._run_ver = Manifest(os.path.join(
                INSTALL_DIR, 'manifest.yml')).version
        return self._run_ver

    def _install_pkg(self, install_dir=INSTALL_DIR):
        """
        Install euler package
        :param install_dir:
        :return:
        """
        if os.path.isdir(install_dir):
            shutil.rmtree(install_dir)
        shutil.copytree(self.repo_dir, install_dir, symlinks=True)
        shutil.copy(os.path.join(self.root_dir, 'manifest.yml'), install_dir)
        log.info("Install package to dir(%s) successfully.", install_dir)
        return 

    def upgrade(self):
        """

        :return:
        """
        if not self._fast_upg_euler_file("upgrade"):
            log.error("Upgrade euler file failed.")
            return RETURN_EXEC_FAILED
        if not PatchMgr.upgrade(PATCH_CONF, os.path.join(
                self.repo_dir, 'patch.xml'), self.run_ver, self.pkg_ver):
            log.error("Upgrade patch failed.")
            return RETURN_EXEC_FAILED

        self._install_pkg()
        log.info("Run fast upgrade successfully.")
        return RETURN_EXEC_SUCC

    def install(self):
        """

        :return:
        """
        if not self._fast_upg_euler_file("install"):
            log.error("Upgrade euler file failed.")
            return RETURN_EXEC_FAILED
        if not PatchMgr.install(self.patch_conf, 2):
            log.error("Install patch failed.")
            return RETURN_EXEC_FAILED
        self._install_pkg()
        log.info("Install patch successfully.")
        return RETURN_EXEC_SUCC

    def start(self):
        """

        :return:
        """
        if not PatchMgr.install(self.patch_conf, 3):
            log.error("Install patch failed.")
            return RETURN_EXEC_FAILED
        log.info("Install patch successfully.")
        return RETURN_EXEC_SUCC

    def check_upgrade(self):
        """
        Apollo patch must support hot upgrade
        """

        if not self.run_ver or not self.pkg_ver:
            log.warn("Get pkg and running version failed(pkg: %s, run: %s),"
                     " just return cold upgrade.",
                     self.pkg_ver, self.run_ver)
            return RETURN_HOT_UPGRADE
        log.info("The running version is %s, and pkg version is %s.",
                 self.run_ver, self.pkg_ver)
        if self.run_ver == self.pkg_ver:
            return RETURN_NO_NEED_UPGRADE
        return RETURN_HOT_UPGRADE

    def pre_upgrade(self):
        """

        :return:
        """
        if not self._fast_upg_euler_file("pre_upgrade"):
            log.error("Prepare for upgrade failed.")
            return RETURN_EXEC_FAILED
        log.info("Prepare for upgrade successfully.")
        return RETURN_EXEC_SUCC

    @staticmethod
    def check_status():
        """

        :return:
        """
        log.info("Check status successfully.")
        return RETURN_EXEC_SUCC

    @staticmethod
    def online():
        log.info("Online successfully.")
        return RETURN_EXEC_SUCC

    def pre_rollback(self):
        """

        :return:
        """
        if not self._fast_upg_euler_file("pre_rollback"):
            log.error("Prepare for rollback failed.")
            return RETURN_EXEC_FAILED
        log.info("Prepare for rollback successfully.")
        return RETURN_EXEC_SUCC

    def rollback(self):
        """

        :return:
        """
        return self.upgrade()

    @staticmethod
    def get_cmpt_ver(file_path):
        cmd = "grep \"Version:\" {0} | cut -d' ' -f3".format(file_path)
        version = os.popen(cmd).read().strip()
        return version

    @staticmethod
    def parse_euler_upg_xml(file_path):
        """
        parse euler config.
        :return:
        """
        if not os.path.exists(file_path):
            log.error("The euler backup config(%s) is not exist.", file_path)
            return False, []
        root = ETree.parse(file_path)
        file_obj_list = []
        file_list = root.findall('item')
        for per_file in file_list:
            node_type = per_file.attrib['node_type']
            board_type = per_file.attrib['board_type']
            path = per_file.attrib.get('path', '')
            script = per_file.attrib.get('script', '')
            description = per_file.attrib.get('desc', '')
            file_obj_list.append(
                EulerFile(node_type, board_type, path, script, description))
        return True, file_obj_list

    def _backup_old_file(self, euler_file_list):
        """
        back up old euler file for rollback upgrade.
        :param euler_file_list:
        :return:
        """
        backup_dir = os.path.join(DATA_DIR, "euler_back_up")
        if not os.path.exists(backup_dir):
            os.mkdir(backup_dir)
        bak_ver_path = os.path.join(backup_dir, "manifest.yml")
        cur_version = self.get_cmpt_ver(os.path.join(DATA_DIR,
                                                     'update/pkg_upgrade_old_extract/apollo_patch-*/manifest.yml'))
        last_bak_verion = self.get_cmpt_ver(bak_ver_path)
        if last_bak_verion == cur_version:
            log.info("Back up euler version is same as cur version(%s).",
                     cur_version)
            return
        log.info("Back up upd file, last bak ver(%s), cur bak ver(%s).",
                 last_bak_verion, cur_version)
        shutil.rmtree(backup_dir, ignore_errors=True)
        for per_file in euler_file_list:
            if not os.path.exists(per_file.path):
                continue
            dst_path = backup_dir + os.path.dirname(per_file.path)
            if not os.path.isdir(dst_path):
                os.system("mkdir -p {0}".format(dst_path))
            shutil.copy(per_file.path, backup_dir + per_file.path)
            os.system('fsync {}'.format(backup_dir + per_file.path))
        src_ver_path = os.path.join(DATA_DIR,
                                    'update/pkg_upgrade_old_extract/apollo_patch-*/manifest.yml')
        os.system("cp {0} {1};fsync {1}".format(src_ver_path, bak_ver_path))
        return

    def _fast_upg_euler_file(self, run_type):
        """
        upgrade euler script.
        1. replace old script
        2. exec script to make file work
        :param run_type:
        :return:
        """
        log.info("Upg handle run type(%s)", run_type)
        euler_conf = os.path.join(self.root_dir, "action/upd_info.xml")
        if not os.path.exists(euler_conf):
            log.info("No need to handle euler files.")
            return True
        ret, euler_file_list = self.parse_euler_upg_xml(euler_conf)
        if not ret:
            return False
        if run_type == 'upgrade':
            self._backup_old_file(euler_file_list)
        for per_file in euler_file_list:
            if run_type in ['upgrade', 'rollback', 'install']:
                src_path = os.path.join(self.root_dir,
                                        'repo/recover_file' + per_file.path)
            else:
                src_path = os.path.join(DATA_DIR,
                                        "euler_back_up" + per_file.path)
            log.info("Copy src(%s) to dst(%s).", src_path, per_file.path)
            if not os.path.exists(os.path.dirname(per_file.path)):
                os.system("mkdir -p {0}".format(os.path.dirname(per_file.path)))
            if os.path.exists(src_path):
                shutil.copy(src_path, per_file.path)
            if per_file.script == '':
                continue
            script = os.path.join(self.root_dir,
                                  'repo/upd_script/' + per_file.script)
            cmd = "sh {0} {1}".format(script,
                                      'upgrade' if run_type in ['upgrade', 'install'] else 'rollback')
            ret = os.system(cmd)
            if ret:
                log.error("Exec %s failed.", cmd)
                return False
            else:
                log.info("Exec %s success.", cmd)
        return True


_USAGE = '''
Usage: {0} option
  Management euler component.
  Options are:
  install         Install the euler component
  start           Start the euler component, actually do nothing
  check_upgrade   Check the euler component is need upgrade
  pre_upgrade     Prepare for upgrade
  upgrade         Upgrade the euler component
  check_status    Check the euler component running status
  online          Online the euler component
  pre_rollback    Prepare for rollback
  rollback        Rollback the euler component
'''.format(os.path.basename(__file__))


def prepare_log(log_path):
    """

    :param log_path:
    :return:
    """
    fmt = log.Formatter(fmt='[%(asctime)s][%(levelname)s][APOLLO_PATCH:'
                            ' %(message)s][%(filename)s, %(lineno)d]',
                        datefmt='%Y-%m-%d %H:%M:%S')
    hdlr = log.StreamHandler()
    hdlr.setFormatter(fmt)
    log.root.setLevel(log.INFO)
    log.root.addHandler(hdlr)

    if not log_path:
        return
    log_dir = os.path.dirname(log_path)
    if not os.path.isdir(log_dir):
        os.makedirs(log_dir, 0o644)
    hdlr = log.FileHandler(log_path)
    hdlr.setFormatter(fmt)
    log.root.addHandler(hdlr)
    return


def exec_cmd():
    """

    :return:
    """
    args = sys.argv
    if len(args) != 2 or args[1] == '-h':
        print(_USAGE)
        return 0
    app = ApolloPatchAPP()
    prepare_log(LOG_PATH)

    if not hasattr(app, args[1]):
        log.error("Apollo patch app not support command(%s).", args[1])
        return 1
    try:
        return getattr(app, args[1])()
    except:
        log.exception("Exec cmd(%s) failed.", args[1])
        return 2


if __name__ == '__main__':
    sys.exit(exec_cmd())

