#!/usr/bin/env python2
# -*- encoding: utf-8 -*-

import os
import sys
import argparse
import logging
import mysql_backup_mds.mds as mds
import mysql_backup_mds.support as support
import json
import MySQLdb

from subprocess import Popen

USAGE = '''Программа для подготовки работы и запуска mysql на продакшен средах Директа.
1. Проверяет есть ли запущенные инстансы. При наличии таковых - завершается.
2. Испрявляет UUID у указанных инстансов.
3. Обновляет информацию о gtid из xtrabackup_binlog_info.
4. Обновляет информацию о репликации(данные о мастер хосте берется из db-config.json.
5. Запускает репликацию.
Пример использования:

%(prog)s ppcdata0 ppcdata1
'''

MYSQLSOCK = "/var/run/mysqld.{0}/mysqld.sock"
MYSQLPID = "/var/run/mysqld.{0}/mysqld.pid"
ZKCONF = "/etc/yandex-direct/db-config.json"
REPLICATION_TOKEN = "/etc/direct-tokens/mysql_rplcat2"
REPLICATION_USER = "rplcat2"
XTRABACKUP_INFO = "/opt/mysql.{0}/data/xtrabackup_binlog_info"
CONNECT_HOST = "localhost"
CONNECT_USER = "root"
CONNECT_PASSWD = ""
CONNECT_DBNAME = "mysql"

global instances, host, user, passwd, dbname

#логирование осуществляется в STDOUT/STDERR
def startLogging(level='DEBUG'):
    logger = logging.getLogger('stream logs to console')
    logger.setLevel(level=getattr(logging, level))
    formatter = logging.Formatter('%(asctime)s %(message)s')
    logch = logging.StreamHandler()
    logch.setLevel(level=getattr(logging, level))
    logch.setFormatter(formatter)
    logger.addHandler(logch)
    return logger, logch

#Запустить БД в указанных инстансах
def startMysqlProcess():
    code = 0
    for instance in instances:
        logger.info("starting mysql instance {0}".format(instance))
        cmd = "/usr/local/bin/lm {0} server-start --skip-slave-start --force".format(instance)
        status, err = runMysqlShellCommand(cmd)
        if status:
            logger.info("success start mysql instance {0}".format(instance))
        else:
            logger.info("failed start mysql instance {0}: {1}".format(instance, err))
            code = 1
    return code

#Остановить БД в указанных инстансах
def stopMysqlProcess():
    code = 0
    for instance in instances:
        logger.info("stoping mysql instance {0}".format(instance))
        cmd = "/usr/local/bin/lm {0} server-stop --force".format(socket)
        status, err = runMysqlShellCommand(cmd)
        if status:
            logger.info("success stop mysql instance {0}".format(instance))
        else:
            logger.info("failed stop mysql instance {0}: {1}".format(instance, err))
            code = 1
    return code

#Принимает команду на выполнение.
#В результате выводит статус выполнения(true/false) и ошибку(error).
def runMysqlShellCommand(command):
    try:
        logger.debug("run command {0}".format(command))
        fileno_logfile = logch.stream.fileno()
        p1 = Popen(command, shell=True, bufsize=0, stderr=fileno_logfile, stdout=fileno_logfile)
        while True:
            rcode1 = p1.poll()
            if rcode1 is not None and rcode1 != 0:
                raise ValueError("error run command {0} code {1}".format(command, rcode1))
            if rcode1 == 0:
                break
            p1.wait()
    except Exception as err:
        return False, ValueError(err)
    return True, None

#По указанным instance находим запущенные старые БД и возвращаем pids в виде списка.
#Если к указанному pid нет процесса, удаляем старый pid файл.
def checkRunProcess():
    pids = []
    for instance in instances:
       pidfile = MYSQLPID.format(instance)
       if os.path.exists(pidfile):
           pid = open(pidfile).read().strip()
           if os.path.exists('/proc/{0}'.format(pid)):
               pids.append(pid)
           else:
               logger.info("not found running process from {0}. Remove file {1}".format(pid, pidfile))
               os.remove(pidfile)
    return pids

#Исправляет UUID указанных инстансов
def fixMysqlUUID():
    code = 0
    for instance in instances:
        logger.info("start fix mysql uuid for {0}".format(instance))
        cmd = "/usr/local/bin/lm -force {0} fix-uuid".format(instance)
        status, error = runMysqlShellCommand(cmd)
        if status:
            logger.info("success fixMysqlUUID for {0}".format(instance))
        else:
            logger.info("failed fixMysqlUUID for {0}".format(instance))
            code = 2
    return code

#Ищем текущего мастера в структурах данных dbconf
def findCurrentMaster(jdata, instance):
    if not isinstance(jdata, dict):
        return
    if jdata.has_key("instance") and jdata["instance"] == instance:
        logger.debug("zkconf: {0}".format(jdata))
        return jdata["host"]
    for key in jdata:
        if isinstance(jdata[key], dict):
            master = findCurrentMaster(jdata[key], instance)
            if master is not None:
                return master
    return

#Разово устанавливает соединение и выполняет команду. Выводит результат stdout. Ошибки в вызывают Exception.
def mysqlExecute(instance, command):
    mysocket = MYSQLSOCK.format(instance)
    logger.debug("run command {1} to socket {0}".format(mysocket, command))
    if not os.path.exists(mysocket):
        raise ValueError("socket {0} dosnt exists".format(mysocket))
    db = MySQLdb.connect(host=host, user=user, passwd=passwd, db=dbname, unix_socket=mysocket)
    cur = db.cursor()
    logger.debug("execute: {0}".format(command))
    cur.execute(command)
    data = cur.fetchall()
    cur.close()
    db.close()
    return data

#Вычитываем из xtrabackup info позиции по репликации на время бекапа. Выводит результат в виде словаря:
#{"name": "ppcdata11-bin.037243", "position": 31666298, "gtid": "139e4662-096d-586d-3830-2b80a400:1-100, ...}
def readXtrabackupInfo(instance):
    xtrabackup = XTRABACKUP_INFO.format(instance)
    result = dict()
    if os.path.exists(xtrabackup):
        meta = open(xtrabackup).read().replace("\n", "").split("\t")
        result["name"] = meta[0] if len(meta) >= 1 else ""
        result["position"] = meta[1] if len(meta) >= 2 else 0
        result["gtid"] = meta[2] if len(meta) >= 3 else ""
    else:
        raise ValueError("not found xtradb file {0}".format(xtrabackup))
    return result

#Обновляет gtid позицию репликации y mysql на основании xtrabackup_binlog_info.
def updateMasterPositions():
    code = 0
    for instance in instances:
        if instance == "ppcdict":
            logger.info("ignore ppcdict pxc: RESET MASTER not allowed when node is in cluster")
            continue
        try:
            xtrameta = readXtrabackupInfo(instance)
            print xtrameta
            gtids = xtrameta.get("gtid", "")
            if len(gtids) == 0:
                raise ValueError("empty gtid section")
            logger.info("start update GTID position for {0}".format(instance))
            mysqlExecute(instance, "RESET MASTER")
            mysqlExecute(instance, "SET GLOBAL gtid_purged='{0}'".format(gtids))
            logger.info("success update GTID position for {0}".format(instance))
        except Exception as err:
            logger.info("failed update GTID position for {0}: {1}".format(instance, err))
            code = 3
    return code

#Обновляет мастер позицию репликации y mysql, после чего запускает ее.
def updateReplicationPosition():
    code = 0 
    for instance in instances:
        logger.info("start replication for {0}".format(instance))
        rquery = dict()
        if instance == "ppcdict":
            logger.info("ignore ppcdict pxc: CHANGE MASTER not used in PXC")
            continue
        try:
            zkdata = open(ZKCONF, 'r').read().strip()
            rquery["port"] = mysqlExecute(instance, "SHOW VARIABLES LIKE 'port'")[0][1]
            rquery["passwd"] = open(REPLICATION_TOKEN, 'r').read().strip()
            rquery["user"] = REPLICATION_USER
            rquery["host"] = findCurrentMaster(json.loads(zkdata), instance)
            req = '''CHANGE MASTER TO MASTER_HOST='{host}', MASTER_USER='{user}', MASTER_PASSWORD='{passwd}', MASTER_PORT={port}, MASTER_AUTO_POSITION=1'''.format(**rquery)
            logger.debug("execute sql: {0}".format(req))
            try:
                mysqlExecute(instance, req)
            except Exception as err:
                logger.warning("warning start replication: {0}".format(err))
            mysqlExecute(instance, "START SLAVE")
        except Exception as err:
            logger.info("failed start replication for {0}: {1}".format(instance, err))
            raise
            code = 4
    return code

def run():
    pids = checkRunProcess() #проверяем есть ли запущенные инстансы
    if len(pids) != 0:
        raise ValueError("found running mysql instances with pids: {0}!".format(pids))
    fixMysqlUUID() #исправляем uuid у инстансов
    status = startMysqlProcess() #запускает mysql
    if status != 0:
        raise ValueError("not all mysql instances starting")
    updateMasterPositions() #обновляем gtid позиции у инстансов
    updateReplicationPosition() #обновляем данные у репликации и запускаем ее

    return
    
if __name__ == '__main__':
    parser = argparse.ArgumentParser(usage=USAGE)
    parser.add_argument("instances", nargs='*', type=str, action='store',
        help="command for execute. Example: [instance1] [instance2]")
    parser.add_argument("-o", "--mysql-directory", nargs='?', type=str, action='store',
        dest="mysql_directory", help="directory for save/restore mysql backup")
    parser.add_argument("-d", "--debug", action="store_true",
        dest='debug', help="включить debug режим")
    parser.add_argument("-u", "--user", action="store_true", default=CONNECT_USER,
        dest="user", help="пользователь для подключения к mysql")
    parser.add_argument("-H", "--host", action="store_true", default=CONNECT_HOST,
        dest="host", help="хост для подключения к БД")
    parser.add_argument("-p", "--password", action="store_true", default=CONNECT_PASSWD,
        dest="passwd", help="пароль для подключения к БД")
    parser.add_argument("-n", "--dbname", action="store_true", default=CONNECT_DBNAME,
        dest="dbname", help="имя базы для подключения к mysql")

    opts = parser.parse_args()

    host = opts.host
    user = opts.user
    passwd = opts.passwd
    dbname = opts.dbname
    log_level = "DEBUG" if opts.debug else "INFO"
    logger, logch = startLogging(level=log_level)
    instances = opts.instances

    run()
