#!/usr/bin/python
# -*- coding: utf-8 -*-
# @brief   : 2018/10/10 16:56
# @File    : app ctrl
#  @Software: Software management infrastructure
import logging
import os
import re
import shutil
import signal
import subprocess
import sys
import time
from distutils.sysconfig import get_python_lib

import yaml

logging.basicConfig(level=logging.INFO,
                    filename="/OSM/log/cur_debug/startup.log",
                    format='[%(asctime)s][%(levelname)s][%(message)s][%(filename)s, %(lineno)d]',
                    datefmt='%Y-%m-%d %H:%M:%S')

TOP_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
MY_NAME = os.path.basename(__file__)
REPO_DIR = os.path.join(TOP_DIR, "repo")

DISK_RUN_DIR = "/OSM/coffer_bin/swm"
MEMORY_RUN_DIR = "/OSM/component/swm"
RUN_DIR = MEMORY_RUN_DIR

CMD = 'python {cmd:s} {param:s}'
START_CMD = CMD.format(cmd=os.path.join(RUN_DIR, 'deploy.py'), param='start')
STOP_CMD = CMD.format(cmd=os.path.join(RUN_DIR, 'deploy.py'), param='stop')
RESTART_CMD = 'nohup ' + CMD.format(cmd=os.path.join(RUN_DIR, 'deploy.py'), param='restart &')
SWM_RPM_DB_PATH = '/var/lib/swm_rpm'
SIMU = os.path.exists('/.dockerenv')
FIRMWARE_HOOK_PATH = '/startup_disk/image/pkg_cur/firmware/install_hook/'
MOUNT_IMAGE_RW = 'mount -o remount,rw,async /startup_disk/image/'
PKG_CUR = '/startup_disk/image/pkg_cur'
OSM_CONF_MANIFEST = '/OSM/conf/manifest.yml'
UPGRADE_FLAG_PATH = '/OSM/coffer_data/swm/upgrade_flag'
UNROOT_FLAG_PATH_IMAGE = '/startup_disk/image/pkg_cur/unroot_flag'
DATA_NODE_FLAG_FILE = '/startup_disk/conf/conf_local/data_node'
UNROOT_FLAG_PATH = '/OSM/conf/unroot_flag'
THRIFT_LIB_PATH = '/OSM/component/swm/lib/libthrift-0.13.0.so'
USR_LIB_PATH = '/usr/lib64/'
LIB_BAK_DIR = '/OSM/coffer_data/swm'
SELINUX_PATH = os.path.join(TOP_DIR, 'action', 'selinux')
PYTHON_2 = '2' == sys.version[0]

# INSTALL 加载执行的脚本
INSTALL_EXEC_CMD = [
    'sh /OSM/script/parse_initrd_config.sh',
    'sh /OSM/script/product_pre_handle.sh',
    'sh /OSM/script/product_mk_disk_dir.sh',
    'mkdir -p /OSM/script/setup.hook/before',
    'mkdir -p /OSM/script/setup.hook/after',

    'sh /OSM/script/os_modify_sftp_group_user_dir.sh',
    'sh /OSM/script/patch_stage1.sh',
    'cp -af %s/manifest.yml %s/conf' % (TOP_DIR, RUN_DIR)
]
# start 加载执行的脚本
START_EXEC_CMD = [
    '/OSM/bin/daemon',
]

USAGE = '''
Usage:
{name:s} [option]

Options:
    start
    stop
    online
    offline   
    install
    uninstall
    check_status
    restart
    pre_upgrade
    upgrade
    check_upgrade  
    '''.format(name=MY_NAME)


def show_help():
    print(USAGE)
    return


def symlink(src, link):
    """

    :param src:
    :param link:
    :return:
    """
    if os.path.islink(link):
        os.unlink(link)
    os.symlink(src, link)
    return


def unlink(link):
    """

    :param link:
    :return:
    """
    os.unlink(link)
    return


class PkgModel(object):
    """

    """
    PTH_FILE = os.path.join(get_python_lib(), "swm.pth")
    DIAGNOSE_LINK = "/usr/local/bin/swm_diagnose"
    DIAGNOSE_CLIENT = os.path.join(RUN_DIR, "infra/diagnose/diag_client.py")

    @classmethod
    def add_pth_2_path(cls, pkg_dir):
        """

        :return:
        """
        with open(cls.PTH_FILE, "w+") as f:
            f.write(pkg_dir)
        return

    @classmethod
    def remove_pth_from_path(cls):
        """

        :return:
        """
        if os.path.isfile(cls.PTH_FILE):
            os.remove(cls.PTH_FILE)
        return

    @classmethod
    def link_diagnose(cls):
        """

        :return:
        """
        cls.unlink_diagnose()
        symlink(cls.DIAGNOSE_CLIENT, cls.DIAGNOSE_LINK)
        return

    @classmethod
    def unlink_diagnose(cls):
        """

        :return:
        """
        if os.path.exists(cls.DIAGNOSE_LINK):
            unlink(cls.DIAGNOSE_LINK)
        return

    @classmethod
    def install(cls):
        """

        :return:
        """
        # Link to pkg dir
        try:
            cls.add_pth_2_path(RUN_DIR)
            cls.link_diagnose()
        except Exception as e:
            logging.error("install package model excepted %s.", e)
            return False
        logging.info("Install package model successfully.")
        return True

    @classmethod
    def uninstall(cls):
        """

        :return:
        """
        cls.remove_pth_from_path()
        cls.unlink_diagnose()

        # Delete history installed files
        if not os.path.exists(RUN_DIR):
            logging.warn("The pkg has not been installed.")
            return True
        logging.info("Uninstall pkg model successfully.")
        return True


# set apollo version to /OSM/conf/manifest.yml
def set_apollo_version():
    apollo_link = os.path.join(PKG_CUR, 'apollo')
    if not os.path.islink(apollo_link):
        return
    apollo_dir = os.path.realpath(apollo_link)
    apollo_version = apollo_dir.split('-')[-1]
    content = "ApolloVersion: {0}"
    with open(OSM_CONF_MANIFEST, 'a+') as fp:
        fp.write(content.format(apollo_version))
    return


def copy_lib_to_aimgt():
    bak_lib_path = os.path.join(LIB_BAK_DIR, 'libthrift-0.13.0.so')
    if not os.path.exists(bak_lib_path):
        return
    if not os.path.exists(USR_LIB_PATH):
        return
    ret_code = os.system('cp -af {0} {1}'.format(bak_lib_path, USR_LIB_PATH))
    if ret_code:
        logging.error('Copy thrift lib to aimgt work path failed, ret(%s).', ret_code)
    return


def start():
    copy_lib_to_aimgt()
    for cmd in START_EXEC_CMD:
        ret_code = os.system(cmd)
        if ret_code:
            logging.error("START: exec script (%s) failed", cmd)
    time.sleep(1)
    # temp use for start auto test
    cmd = "cd /OSM/conf; mv versions.conf versions_tmp.conf; mv tmp_use_versions.conf versions.conf"
    os.system(cmd)
    cmd = "cat /OSM/conf/versions_tmp.conf >> /OSM/conf/versions.conf; rm -rf /OSM/conf/versions_tmp.conf"
    os.system(cmd)
    cmd = 'cp -af {0}/manifest.yml /OSM/conf/'
    os.system(cmd.format(PKG_CUR))
    set_apollo_version()
    ret_code = os.system(START_CMD)
    if ret_code:
        logging.error("start swm service failed")
        return 1
    if not restart_esyslog_sync_service(["/opt/esyslog/swm_esyslog_sync_coffer_execute.py", "start_sync"]):
        return 1
    return 0


def stop():
    ret_code = os.system(STOP_CMD)
    if ret_code:
        logging.error("stop swm service failed")
        return 1
    return 0


def restart():
    ret_code = os.system(RESTART_CMD)
    if ret_code:
        logging.error("restart swm service failed")
        return 1
    return 0


def online():
    return 0


def offline():
    return 0


def create_rpm_dir():
    """
    创建rpm的db目录
    :return:
    """
    if not os.path.exists(SWM_RPM_DB_PATH):
        cmd = "mkdir -p %s" % SWM_RPM_DB_PATH
        os.system(cmd)


def exec_firmware_hook():
    """
    install hook in firmware dir
    :return:
    """
    if os.path.exists(FIRMWARE_HOOK_PATH):
        os.system(MOUNT_IMAGE_RW)
        for script in os.listdir(FIRMWARE_HOOK_PATH):
            cmd = 'cd {0};dos2unix {1};chmod +x {1};./{1}'.format(FIRMWARE_HOOK_PATH, script)
            ret_code = os.system(cmd)
            if ret_code:
                logging.error("exec firmware hook failed, script is (%s).", script)
    return


def exec_new_cmpt_hook():
    """
    install hook if has new cmpt
    :return:
    """
    apollo_link = os.path.join(PKG_CUR, 'apollo')
    if not os.path.islink(apollo_link):
        return
    apollo_dir = os.path.realpath(apollo_link)
    apollo_version = apollo_dir.split('-')[-1]
    if apollo_version != '1.0.0.1':
        return
    os.system('mkdir -p /OSM/script/startup.hook/after.start')
    os.system('chmod +xr /OSM/script/resume_new_cmpt_in_pkg_upd.py')
    os.system('cp -af /OSM/script/resume_new_cmpt_in_pkg_upd.py'
              ' /OSM/script/startup.hook/after.start/')
    return


def set_unroot_flag():
    if os.path.exists(DATA_NODE_FLAG_FILE):
        return
    if os.path.exists(UNROOT_FLAG_PATH_IMAGE):
        shutil.copy(UNROOT_FLAG_PATH_IMAGE, "/OSM/conf")
        os.system("chmod 644 /OSM/conf/unroot_flag")
        logging.info("INSTALL: set unroot_flag finish.")
        os.system(MOUNT_IMAGE_RW)
        system_dir = os.path.realpath(PKG_CUR)
        ver_dir = os.path.dirname(system_dir)
        os.chmod(system_dir, 0o755)
        os.chmod(ver_dir, 0o755)
        os.system("mkdir -p /OSM/conf/thrift_rpc")
        os.system("chmod 770 /OSM/conf/thrift_rpc")
        if os.path.exists("/OSM/script/addcommuser.sh"):
            os.system("sh /OSM/script/addcommuser.sh")
            logging.info("INSTALL: add common user for unroot scenes.")
    return


def install():
    create_rpm_dir()
    # 到repo目录下取出rpm包来进行安装
    for rpm_file in os.listdir(REPO_DIR):
        if not rpm_file.endswith('.rpm'):
            continue
        cmd = "rpm -ivh --nodeps --force --dbpath=%s %s" % (SWM_RPM_DB_PATH, os.path.join(REPO_DIR, rpm_file))
        ret_code = os.system(cmd)
        if ret_code:
            logging.error("install swm service failed")
            return 1
    if os.path.exists(SELINUX_PATH):
        ret_code = os.system("cd %s && ./install.sh && cd -" % SELINUX_PATH)
        logging.info("Install selinux result: %s.", ret_code)
    PkgModel.install()
    dev_type = os.getenv('DEVICE_TYPE', None)
    if dev_type is not None:
        logging.info("INSTALL: DEVICE_TYPE is %s", dev_type)
        sas_fw_file = os.path.join("/startup_disk/image/pkg_cur/firmware", dev_type, "Quark_fw_pkg.bin")
        sas_hda_file = os.path.join("/startup_disk/image/pkg_cur/firmware", dev_type, "Quark_HDA_Boot.bin")
        if not os.path.exists("/OSM/modules/Quark_fw_pkg.bin") and \
                os.path.exists(sas_fw_file):
            shutil.copy(sas_fw_file, "/OSM/modules")
        if not os.path.exists("/OSM/modules/Quark_HDA_Boot.bin") and \
                os.path.exists(sas_hda_file):
            shutil.copy(sas_hda_file, "/OSM/modules")
            logging.info("INSTALL: copy hda file")
    else:
        logging.error("INSTALL: board type is None")
    for cmd in INSTALL_EXEC_CMD:
        ret_code = os.system(cmd)
        if ret_code:
            logging.error("INSTALL:exec script (%s) failed", cmd)
            # return 1
    exec_firmware_hook()
    exec_new_cmpt_hook()
    set_unroot_flag()
    return 0


def uninstall():
    create_rpm_dir()
    PkgModel.uninstall()
    # 到repo目录下取出rpm包来卸载
    for rpm_file in os.listdir(REPO_DIR):
        rpm_name = rpm_file[0:rpm_file.index(".rpm")]
        cmd = "rpm -e --nodeps --dbpath=%s %s" % (SWM_RPM_DB_PATH, rpm_name)
        ret_code = os.system(cmd)
        if ret_code:
            logging.error("uninstall swm service failed")
            return -1
    return 0


def check_status():
    return 0


# 检查组件升级模式
def check_upgrade():
    old_cfg_path = os.path.join(MEMORY_RUN_DIR, 'conf', 'manifest.yml')
    if not os.path.exists(old_cfg_path):
        old_cfg_path = os.path.join(DISK_RUN_DIR, 'conf', 'manifest.yml')

    new_cfg_path = os.path.join(TOP_DIR, 'manifest.yml')
    old_cfg = None
    new_cfg = None
    with open(old_cfg_path, 'r') as fp:
        content = fp.read()
        old_cfg = yaml.safe_load(content)
    with open(new_cfg_path, 'r') as fp:
        content = fp.read()
        new_cfg = yaml.safe_load(content)
    if not (old_cfg and new_cfg):
        logging.error("Get yaml info failed(old: %s, new: %s).", old_cfg, new_cfg)
        return 0
    old_ver = old_cfg.get('Version')
    new_ver = new_cfg.get('Version')
    if old_ver == new_ver:
        logging.info("The old version is same(%s) with new version, no need upgrade.", old_ver)
        return 1
    logging.info("The old version(%s) is different with new version(%s), need upgrade.", old_ver, new_ver)
    return 2


def restart_ssh():
    script_path = "/OSM/script/modify_sshd.sh"
    if not os.path.exists(script_path):
        logging.warn("Failed to get script(%s).", script_path)
        return True
    ret_code = os.system(script_path)
    logging.info("Return code of script is (%s).", ret_code)
    return True


def bak_lib_for_aimgt():
    # 为aimgt备份so
    if not os.path.exists(THRIFT_LIB_PATH):
        return
    ret_code = os.system("cp -af {0} {1}".format(THRIFT_LIB_PATH, LIB_BAK_DIR))
    if ret_code:
        logging.error("Copy libthrift to coffer_data failed, ret(%s).", ret_code)
    if not os.path.exists(USR_LIB_PATH):
        return
    ret_code = os.system("cp -af {0} {1}".format(THRIFT_LIB_PATH, USR_LIB_PATH))
    if ret_code:
        logging.error("Copy libthrift to aimgt failed, ret(%s).", ret_code)
    return


def upgrade():
    bak_lib_for_aimgt()
    create_rpm_dir()
    if os.path.exists("/OSM/conf/product.ini"):
        cmd = "mv /OSM/conf/product.ini /OSM/conf/product_backups.ini"
        os.system(cmd)
    # 到repo目录下取出rpm包来进行安装
    for rpm_file in os.listdir(REPO_DIR):
        if not rpm_file.endswith('.rpm'):
            continue
        cmd = "rpm -ivh --nodeps --force --dbpath=%s %s" % (SWM_RPM_DB_PATH, os.path.join(REPO_DIR, rpm_file))
        ret_code = os.system(cmd)
        if ret_code:
            logging.error("upgrade swm service failed")
            if os.path.exists("/OSM/conf/product_backups.ini"):
                cmd = "mv /OSM/conf/product_backups.ini /OSM/conf/product.ini;dos2unix /OSM/conf/product.ini"
                os.system(cmd)
            return 1
    if os.path.exists("/OSM/conf/product_backups.ini"):
        cmd = "mv /OSM/conf/product_backups.ini /OSM/conf/product.ini;dos2unix /OSM/conf/product.ini"
        os.system(cmd)
    if not SIMU:
        restart_ssh()
        ret_code = os.system('sh /OSM/script/parse_initrd_config.sh')
        if ret_code:
            logging.error("Parse initrd config failed.")
            return 1
    if os.path.exists(UNROOT_FLAG_PATH):
        ret_code = os.system('sh /OSM/script/addcommuser.sh')
        if ret_code:
            logging.error("Add comm user failed ret(%s).", ret_code)
            return 1
    # 启动服务
    if not restart_esyslog_sync_service(["/opt/esyslog/swm_esyslog_sync_coffer_execute.py", "start_sync"]):
        return 1
    # 拷贝manifest文件须在进程重拉前最后一步执行
    ret_code = os.system('cp -af %s/manifest.yml %s/conf' % (TOP_DIR, RUN_DIR))
    if ret_code:
        logging.error("copy manifest.yml file failed.")
        return 1
    return restart()


def rollback():
    return upgrade()


def get_esyslog_sync_service_pid_list():
    """
    获取 esyslog 同步服务的进程ID
    :return:进程ID列表
    """
    cmd = "ps -ef |grep esyslog_sync_coffer_execute | grep -v grep|awk {print($2)}"
    ret_code, output = safe_pipe_cmd(cmd)
    if ret_code:
        logging.error("find eyslog sync service exec failed, ret(%s), output(%s).", ret_code, output)
        return False, []
    return True, output.split("\n")


def restart_esyslog_sync_service(cmd):
    """
    开启esyslog同步服务
    为了保证日志同步服务必须存在（不然环境会丢日志），新服务拉起成功之后再杀掉老的服务
    1、获取已经存在的同步进程
    2. 重新开启日志同步进程
    3. 杀掉第一步获取的进程
    """
    result, pid_list = get_esyslog_sync_service_pid_list()  # 获取之前已存在的同步服务
    if not result:
        logging.error("Get eyslog sync service pid failed, output(%s).", pid_list)
        return False
    if not start_service(cmd):
        return False
    for pid in pid_list:
        if not pid:
            continue
        os.kill(int(pid), signal.SIGKILL)
    return True


def start_service(cmd_list):
    try:
        work_dir = "/opt/esyslog/"
        subprocess.Popen(cmd_list, cwd=work_dir, close_fds=True)
        return True
    except Exception as exception:
        logging.error("Start esyslog service failed, exception(%s)", exception)
        return False


def safe_pipe_cmd(cmd, timeout=60, out_handle=subprocess.PIPE, **kwargs):
    """
    通过subprocess.Popen执行管道命令
    :param cmd: cmd为一个shell命令的字符串,或者为一个二维列表，每个列表表示要执行命令
    :param timeout: timeout为超时时间
    :param out_handle: 输出处理，默认重定向到管道中
    :return: 返回一个元组，第一个元素为最后命令的退出码，第二个元素为标准输出。
    """
    if not isinstance(cmd, list):
        cmd = [re.split(r'\s+', command.strip()) for command in cmd.strip().split('|')]
    try:
        ret_code, out = _call_pipe_cmd(cmd, timeout, out_handle, **kwargs)
        logging.info("Exec cmd(%s) success, ret(%d), output(%s).", cmd, ret_code, out)
        return ret_code, out
    except Exception:
        logging.exception("Call pipe cmd failed.")
        return -1, ''


def decode_str(value):
    if not PYTHON_2 and isinstance(value, bytes):
        return value.decode()
    return value


def _call_pipe_cmd(cmd, timeout, out_handle=subprocess.PIPE, **kwargs):
    """
    调用管道的内部命令
    :param cmd: 命令字符串
    :param timeout: 超时时间
    :param output_handle: 输出处理，默认重定向到管道中
    :param kwargs: Popen关键字参数
    :return: 错误码和输出的元组
    """
    process_list = []
    for idx, child in enumerate(cmd):
        next_process = subprocess.Popen(child, stdin=process_list[-1].stdout if idx != 0 else None, close_fds=True,
                                        stdout=subprocess.PIPE if idx != (len(cmd) - 1) else out_handle, **kwargs)
        process_list.append(next_process)
    timeout_para = {} if PYTHON_2 else {"timeout": timeout}
    out, _ = map(decode_str, process_list[-1].communicate(**timeout_para))
    return process_list[-1].poll(), out


def pre_upgrade():
    return start_old_esyslog_service()


def pre_rollback():
    return start_old_esyslog_service()


def start_old_esyslog_service():
    """
    在回滚或降级到615及其之前版本需要停止py同步服务，开启sh同步服务
    616及其之后的升级和补丁场景理论上必然存在日志配额文件，所以升级场景不会拉起sh形式的日志同步服务
    存在日志配额配置文件，表示回滚或降级到的版本支持py形式日志同步，不需要拉起之前的服务
    不存在日志配额文件表明已经回退到老版本(回滚和降级swm组件都是最后执行)，需要重新拉起shell形式的日志同步服务
    """
    log_quota_conf = "/OSM/conf/log_quota.conf"
    if os.path.exists(log_quota_conf):
        return 0
    if not restart_esyslog_sync_service(["/opt/esyslog/esyslog_sync_coffer_execute.sh"]):
        logging.error("Esyslog: start esyslog_sync_coffer_execute.sh service failed.")
        return 1
    logging.info("Esyslog: Start esyslog_sync_coffer_execute.sh service success.")
    return 0


def exec_cmd(args):
    """

    :param args:
    :return:
    """
    if 2 != len(args):
        show_help()
        return -1
    func_name = args[1]
    this_module = sys.modules[__name__]
    try:
        func = getattr(this_module, func_name)
    except Exception as e:
        logging.exception("param is error %s", e)
        return -1
    if func is None:
        return -1
    try:
        return func()
    except:
        logging.exception("swm run cmd(%s) failed.", func_name)
        return 1


if __name__ == "__main__":
    ret = exec_cmd(sys.argv)
    sys.exit(ret)
