#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''Скрипт для сравнения настроек инстанса mysql c конфигом.
'''

import ConfigParser
import fnmatch
import json
import optparse
import os
import socket
import string
import sys
import time
import re
import unittest
import MySQLdb
import logging
import logging.handlers
from collections import OrderedDict
from kazoo.client import KazooClient

reload(sys)
sys.setdefaultencoding('utf8')

try:
    with open('/etc/yandex-direct/db-config.json') as f:
        db_config = json.load(f)

    ZK_HOSTS = ','.join(db_config['db_config']['CHILDS']['zookeeper_ppcback']['host'])
except:
    ZK_HOSTS = "ppc-zk-1.da.yandex.ru:2181,ppc-zk-2.da.yandex.ru:2181,ppc-zk-3.da.yandex.ru:2181"

ZK_TIMEOUT = 10
ZK_RETRY = {"max_tries": -1, "ignore_expire": True}
ZK_LOCATION = "/direct/mysql_configs"
zkh = KazooClient(hosts=ZK_HOSTS, timeout=ZK_TIMEOUT, connection_retry=ZK_RETRY, command_retry=ZK_RETRY)

# время жизни кеша
CACHE_TIME = 3600
# ключи, не нужные для проверок или еще каких-нибудь целей
EXCLUDE_VARIABLES = [ 'pseudo_thread_id', 'gtid_purged', 'innodb_thread_sleep_delay' ]
# ключи, не нуждающиеся в проверке консистентности, но нужные для других целей
IGNORE_VARIABLES = [ 'timestamp', 'open_files_limit', 'auto_increment_offset', 'rpl_semi_sync_slave_enabled', 'rpl_semi_sync_master_enabled' ]
'''open_files_limit: if your OS limit is infinity value from the variable open_file_limit will be used, if OS limit not infinity, but larger than variable value: OS file limit will be used; if OS limit smaller than specified value; again OS file limit will be used. 
auto_increment_offset выставляется динамически PXC кластером.
'''

NODATA = "NODATA".lower()  # потому что все ключи сравниваем в lc

# проверяемые реплицированные ключи
SLAVE_STATUS_PARAMS = ['replicate_do_db', 'replicate_ignore_db', 'replicate_do_table', 'replicate_ignore_table', 
                       'replicate_wild_do_table', 'replicate_wild_ignore_table', 'master_user',
                       'master_port', 'replicate_ignore_server_ids', 'master_retry_count']
MASTER_STATUS_PARAMS = ['binlog_do_db', 'binlog_ignore_db']
# ключи, которые переименовываются в БД
RENAME_PARAMS = { 'tx_isolation': 'transaction_isolation',
                  'thread_cache': 'thread_cache_size',
                  'key_buffer': 'key_buffer_size',
                  'sort_buffer': 'sort_buffer_size',
                  'table_cache': 'table_open_cache' }
# в pxc57 по какойто причине не выводятся в show variables
WSREP_IGNORE = [ 'ist.recv_addr', 'gcs.fc_master_slave', 'gmcast.listen_addr' ]

USAGE = '''Скрипт проверяет разницу между конфигом mysql и запущенным инстансом,
указанным в нем. Для проверки следует задать имя конфига через -c. Примеры:
    {0} -c test.cnf 
    {0} -c ALL
    {0} -c /etc/mysql/ppc/test2.cnf
    {0} -d /etc/mysql -c ppc/test2.cnf
По умолчанию для -m/--monrun используется кэширование в /tmp/<instance_name>.cache, 
чтобы лишний раз не ходить в mysqld. Время живости кэша = {1}.'''.format(sys.argv[0], CACHE_TIME)

MULTY_PARAMS  = ( 'replicate-do-db', 'replicate-ignore-db', 'binlog-do-db',
                  'binlog-ignore-db', 'replicate-wild-ignore-table' )
CONFIG_IGNORS = ( 'user', 'bind', 'master-info-file', 'bind-address', '!includedir', 'plugin_load' )

class OrderedMultisetDict(OrderedDict):
    def __setitem__(self, key, value):
        multypar = [ i.lower().replace('-', '_') for i in MULTY_PARAMS ]
        ignors = [ i.lower().replace('-', '_') for i in opts.ignors ]
        key = key.lower().replace('-', '_')
        passKey = [ key.startswith(i) for i in ignors ]
        if opts.debug: print passKey, key, ignors
        if any(passKey): return
        try:
            item = self.__getitem__(key)
        except KeyError:
            super(OrderedMultisetDict, self).__setitem__(key, value)
            return

        if key in multypar:
            if isinstance(value, list):
                item.extend(value)
            item.sort()
        super(OrderedMultisetDict, self).__setitem__(key, item)

class Config(dict):
    def __setitem__(self, key, item):
        self.__dict__[key] = item

    def __getitem__(self, key):
        if self.__dict__.has_key(key):
            return self.__dict__[key]

    def __repr__(self): 
        return repr(self.__dict__)

    def __len__(self):
        return len(self.__dict__)

    def __hash__(self): 
        return hash(str(self.__dict__.values()))

    def __ne__(self, other):
        return not self.__eq__(other)

    def get(self, key, default):
        if self.__dict__.has_key(key):
            return self.__dict__[key]
        else:
            return default

    def keys(self):
        return self.__dict__.keys()

    def update(self, *args, **kwargs):
        result = list()
        for array in args:
            data = dict([ (RENAME_PARAMS.get(k, k.replace('-', '_').lower()), human2bytes(array[k])) for k in array ])
            [ data.pop(i, None) for i in kwargs.get('ignors', list()) ]
            result.append(data)
        return self.__dict__.update(*result, **dict())

class ConfigError(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return str(self.value)

class Status():
    def __init__(self, code, value, instance):
        self.value = value if value else 'OK'
        self.monrun = opts.monrun
        self.code = code
        self.instance = instance

    def __str__(self):
        #Для monrun длинные строки не красиво. Усекаем до 50 символов.
        data = '{0}; {1}'.format(self.code, self.value)[0:150] if opts.monrun else \
               '[{0}] {1}'.format(self.instance, self.value)
        return str(data)

#### BEGIN TEST ####

test_good_data = (
    ('sort_buffer_size', '8M', '8388608'),
    ('replicate-do-db', 'bmdata1,bmdata2', 'bmdata2,bmdata1'),
    ('log_slave_updates', None, 'ON'),
    ('binlog_rows_query_log_events', 'TRUE', 'ON'),
    ('log_bin', 'test-bin', 'ON'),
    ('explicit_defaults_for_timestamp', 'FALSE', 'OFF'),
    ('relay_log_index', 'test-relay.index', '/opt/mysql.test/test-relay.index'),
    ('key_buffer_size', '12345', '12345'),
    ('query_cache_type', '0', 'OFF'),
    ('query_cache_type', '1', 'ON'),
    ('replicate_ignore_db', 'distribution_logs,partner_logs,partner_logs_arc', 'partner_logs,distribution_logs,partner_logs_arc'),
    ('log_slow_verbosity', 'full', 'microtime,query_plan,innodb'),
    ('wsrep_provider_options','gmcast.listen_addr=tcp://[::]:17401; ist.recv_addr=ppc-xtradb-standby01i.yandex.ru:17501; gcache.size=5g; base_host=ppc-xtradb-standby01i.yandex.ru', 'base_host = ppc-xtradb-standby01i.yandex.ru; base_port = 17401; cert.log_conflicts = no; debug = no; evs.causal_keepalive_period = pt1s; evs.debug_log_mask = 0x1; evs.inactive_check_period = pt0.5s; evs.inactive_timeout = pt15s; evs.info_log_mask = 0; evs.install_timeout = pt7.5s; evs.join_retrans_period = pt1s; evs.keepalive_period = pt1s; evs.max_install_timeouts = 3; evs.send_window = 4; evs.stats_report_period = pt1m; evs.suspect_timeout = pt5s; evs.use_aggregate = true; evs.user_send_window = 2; evs.version = 0; evs.view_forget_timeout = p1d; gcache.dir = /opt/mysql.rbac2/; gcache.keep_pages_size = 0; gcache.mem_size = 0; gcache.name = /opt/mysql.rbac2//galera.cache; gcache.page_size = 128m; gcache.size = 5g; gcs.fc_debug = 0; gcs.fc_factor = 1.0; gcs.fc_limit = 16; gcs.fc_master_slave = no; gcs.max_packet_size = 64500; gcs.max_throttle = 0.25; gcs.recv_q_hard_limit = 9223372036854775807; gcs.recv_q_soft_limit = 0.25; gcs.sync_donor = no; gmcast.listen_addr = tcp://[::]:17401; gmcast.mcast_addr = ; gmcast.mcast_ttl = 1; gmcast.peer_timeout = pt3s; gmcast.time_wait = pt5s; gmcast.version = 0; ist.recv_addr = ppc-xtradb-standby01i.yandex.ru:17501; pc.announce_timeout = pt3s; pc.checksum = false; pc.ignore_quorum = false; pc.ignore_sb = false; pc.linger = pt20s; pc.npvo = false; pc.version = 0; pc.wait_prim = true; pc.wait_prim_timeout = p30s; pc.weight = 1; protonet.backend = asio; protonet.version = 0; repl.causal_read_timeout = pt30s; repl.commit_order = 3; ')
)

test_bad_data = (
    ('sort_buffer_size', '8M', '9388608'),
    ('log_slave_updates', None, 'OFF'),
    ('binlog_rows_query_log_events', 'TRUE', 'OFF'),
    ('replicate-do-db', 'bmdata1', 'bmdata2,bmdata1'),
    ('replicate-do-db2', 'bmdata2,bmdata1', 'bmdata1'),
    ('log_bin', 'test-bin', 'OFF'),
    ('query_cache_type', '2', 'OFF'),
    ('query_cache_type', '0', 'ON'),
    ('relay_log_index', 'test-relay.index', '/opt/mysql.test/test-relay1.index'),
    ('log_slow_verbosity', 'full', 'microtime,query_plan'),
    ('wsrep_provider_options', 'gmcast.listen_addr=tcp://[::]:17401; ist.recv_addr=ppc-xtradb-standby01i.yandex.ru:17501; gcache.size=15g; base_host=ppc-xtradb-standby01i.yandex.ru', 'base_host = ppc-xtradb-standby01i.yandex.ru; base_port = 17401; cert.log_conflicts = no; debug = no; evs.causal_keepalive_period = pt1s; evs.debug_log_mask = 0x1; evs.inactive_check_period = pt0.5s; evs.inactive_timeout = pt15s; evs.info_log_mask = 0; evs.install_timeout = pt7.5s; evs.join_retrans_period = pt1s; evs.keepalive_period = pt1s; evs.max_install_timeouts = 3; evs.send_window = 4; evs.stats_report_period = pt1m; evs.suspect_timeout = pt5s; evs.use_aggregate = true; evs.user_send_window = 2; evs.version = 0; evs.view_forget_timeout = p1d; gcache.dir = /opt/mysql.rbac2/; gcache.keep_pages_size = 0; gcache.mem_size = 0; gcache.name = /opt/mysql.rbac2//galera.cache; gcache.page_size = 128m; gcache.size = 5g; gcs.fc_debug = 0; gcs.fc_factor = 1.0; gcs.fc_limit = 16; gcs.fc_master_slave = no; gcs.max_packet_size = 64500; gcs.max_throttle = 0.25; gcs.recv_q_hard_limit = 9223372036854775807; gcs.recv_q_soft_limit = 0.25; gcs.sync_donor = no; gmcast.listen_addr = tcp://[::]:17401; gmcast.mcast_addr = ; gmcast.mcast_ttl = 1; gmcast.peer_timeout = pt3s; gmcast.time_wait = pt5s; gmcast.version = 0; ist.recv_addr = ppc-xtradb-standby01i.yandex.ru:17501; pc.announce_timeout = pt3s; pc.checksum = false; pc.ignore_quorum = false; pc.ignore_sb = false; pc.linger = pt20s; pc.npvo = false; pc.version = 0; pc.wait_prim = true; pc.wait_prim_timeout = p30s; pc.weight = 1; protonet.backend = asio; protonet.version = 0; repl.causal_read_timeout = pt30s; repl.commit_order = 3; ')
)

test_h2b_data = (
    ('8M', '8388608'),
    ('test-bin', 'test-bin'),
    (None, None)
)

test_config_data = (
    '/tmp/uniqtest', 
     ('/tmp/test1.cnf', '/tmp/uniqtest/mysql/test2.cnf', 'test3.cnf'),
     ('/tmp/uniqtest/test3.cnf', '/tmp/uniqtest/mysql/test2.cnf', '/tmp/test1.cnf'),
     ('/tmp/uniqtest', '/tmp/uniqtest/mysql')
)


class Test(unittest.TestCase):
    def setUp(self):
        self.ignore = opts.ignors
        self.clear_dirs = list()
        self.clear_files = list()

    def tearDown(self):
        if self.clear_files:
            [ os.remove(i) for i in self.clear_files ]
        if self.clear_dirs:
            self.clear_dirs.sort(reverse=True)
            [ os.rmdir(i) for i in self.clear_dirs ]
      
    def prepareData(self, data):
        raw = map(lambda x: ((x[0], x[1]), (x[0], x[2])), data)
        for cnf, cnv in raw:
            i, j = Config(), Config()
            i.update(dict((cnf,)))
            j.update(dict((cnv,)))
            yield i, j

    def printData(self, *value):
        if not opts.debug: return
        if len(value) > 1:
            sys.stdout.write('\n{0} {1} ... '.format(*value))
        else:
            sys.stdout.write('OK')
        sys.stdout.flush()

    #Проверяем работу по совпадающим параметрам.
    def test_checkKeys1(self):
        for cnf, cnv in self.prepareData(test_good_data):
            self.printData(cnf, cnv)
            self.assertEqual(checkKeys(cnf, cnv), None)
            self.printData('OK')

    #Проверяем работу по несовпадающим параметрам.
    def test_checkKeys2(self):
        for cnf, cnv in self.prepareData(test_bad_data):
            self.printData(cnf, cnv)
            with self.assertRaises(ConfigError): 
                checkKeys(cnf, cnv)
            self.printData('OK')

    #Провепяем функицию конвертации байтов.
    def test_human2bytes(self):
        for req, res in test_h2b_data:
            self.printData(req, res)
            self.assertEqual(human2bytes(req), res)
            self.printData('OK')     

    #Проверяем функциюю разбора конфигов mysql.
    def test_listConfigs(self):
        def createTmpDir(name):
            if os.path.isdir(name): return
            os.mkdir(name)
            self.clear_dirs.append(name)
        def createTmpFile(name):
            if os.path.exists(name): return
            os.mknod(name)
            self.clear_files.append(name)
        config_dir = test_config_data[0]
        config_rxp = test_config_data[1]
        config_files = list(test_config_data[2])
        [ createTmpDir(name) for name in test_config_data[3] ]
        [ createTmpFile(name) for name in test_config_data[2] ]
        self.assertListEqual(listConfigs(config_dir, *config_rxp), config_files)

    def test_cache(self):
        cache_file = '/tmp/test.cnf.cache'
        i = Config()
        self.clear_files.append(cache_file)
        data = map( lambda x: (unicode(x[0]), unicode(x[1]) ), test_good_data)
        i.update(dict(data))
        cacheWrite(cache_file, i)
        jdata = cacheRead(cache_file)
        self.assertEqual(len(set(jdata.items()) - set(i.__dict__.items())), 0)

#### END TEST #####

SYMBOLS = {
    'customary'     : ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'),
    'customary_ext' : ('byte', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa',
                       'zetta', 'iotta'),
    'iec'           : ('Bi', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'),
    'iec_ext'       : ('byte', 'kibi', 'mebi', 'gibi', 'tebi', 'pebi', 'exbi',
                       'zebi', 'yobi'),
}

'''Функция для преобразования человекоудобного представления байтов в машинный.
'''
def human2bytes(val):
    if not val: 
        return val #Если пустота, выходим из функции
    val = str(val)
    if val.count('.') > 1: 
        return val.lower() #Отсекаем варианты с 5.6.6
    rgxp = re.compile('^([\d.]+)(\w*)$')
    listVal = rgxp.findall(val)
    if not listVal:
        val = str(val).lower()
        val = val.replace('true', 'on')
        val = val.replace('false', 'off')
        val = val.rstrip('/')
        return str(val) #Не число с буквой.
    num, letter = listVal[0]
    if not letter: 
        return str(num).lower() #Число без буквы.
    num = float(num)
    for name, sset in SYMBOLS.items():
        if letter in sset:
            break
    else:
        if letter == 'k':
            sset = SYMBOLS['customary']
            letter = letter.upper()
        else:
            raise ConfigError("can't interpret %r" % init)
    prefix = {sset[0]:1}
    for i, s in enumerate(sset[1:]):
        prefix[s] = 1 << (i+1)*10
    return str(int(num * prefix[letter]))

'''Функция для парсинга mysql конфига.
'''
def parseMysqlConfig(config, result=Config()):
    if not os.path.isfile(config): return
    try:
        myval = dict()
        data = ConfigParser.ConfigParser(dict_type=OrderedMultisetDict, allow_no_value=True)
        data.read(config)
        for x, y in data.items('mysqld'):
            if y: y = ','.join(y)
            myval[x] = y
        result.update(myval)
    except ConfigParser.NoSectionError as err:
        if opts.debug: print 'Ignore config {0}: {1}'.format(config, err) #Игнорируем файлы без секций.
    with open(config, 'r') as f:
        for line in f.readlines():
            if line.count('!includedir') == 0: continue #Пропускаем записи.
            path = line.split()[1] #Берем параметр дирректории.
            [ parseMysqlConfig(os.path.join(path, c), result) for c in os.listdir(path) ]
    return result

'''Парсим параметры из SHOW VARIABLES.
'''
def parseMysqlVariables(config, result=Config()):
    db = MySQLdb.connect(read_default_file=config, user='root')
    cur = db.cursor()
    cur.execute("SHOW VARIABLES")
    #cur.execute("SELECT VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.session_variables") #SHOW VARIABLES
    result.update(dict(cur.fetchall()), ignors=EXCLUDE_VARIABLES)
    #cur.execute("SELECT VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME='wsrep_provider_options'") #SHOW GLOBAL VARIABLES
    cur.execute("SHOW GLOBAL VARIABLES LIKE 'wsrep_provider_options'")
    result.update(dict(cur.fetchall()), ignors=EXCLUDE_VARIABLES)
    for req, params_to_compare in (("SHOW SLAVE STATUS", SLAVE_STATUS_PARAMS), ("SHOW MASTER STATUS", MASTER_STATUS_PARAMS)):
        cur.execute(req)
        params = cur.fetchall()
        if params:
            description = map(lambda x: x[0].replace('-', '_').lower(), cur.description)
            deleted = list(set(description)-set(params_to_compare))
            result.update(dict(zip(description, params[0])), ignors=deleted)
        else:
            result.update({x: NODATA for x in params_to_compare})

    return result

EXTVARS = { 'log_slow_verbosity': { 'full':     ('microtime', 'query_plan', 'innodb'),
                                   'standard': ('microtime', 'innodb'),
                                   'minimal':  ('microtime',) }
}

'''Проверяем парметры конфигурационного файла БД и параметров в самом инстансе mysql. 
   Если есть неиспользованные ключи, и они не попадают в ignore - вызываем ошибку.
'''
def checkKeys(cnf, cnv):
    fails = list()
    keyf = cnf.keys() #значения из файла
    keyv = cnv.keys() #значения из бд

    #Отсекаем не используемые или ненайденные ключи.
    delta = set(keyf)-set(keyv)
    if len(delta) != 0:
        raise ConfigError(','.join(delta))

    round1 = []
    for i in list(set(keyf).intersection(keyv)):
        # пропускаем явно заигноренные ключи и ключи, команда для получения которых ничего не вернула (нет слейва совсем, например)
        if i in IGNORE_VARIABLES+SLAVE_STATUS_PARAMS or cnv[i] == NODATA:
            logger.debug("skip key %s with config value '%s' and mysqld process value '%s'" % (i, cnf[i], cnv[i]))
        elif cnf[i] != cnv[i]:
            round1.append(i)
        # ключи с одинаковыми значениями молча скипаем

    for key in round1:
        cnf_val, cnv_val = cnf[key], cnv[key]
        #Параметры типа ('log_slave_updates', None, 'ON') хорошие 
        if cnv_val in ('on', 'true') and cnf_val is None:
            continue
        #Отправляем в fails все что имеет None и не попало в предыдущий 'ON': ('log_slave_updates', None, 'OFF')
        if cnf_val is None or cnv_val is None:
            fails.append(key)
            continue
        #Параметры типа ('query_cache_type', '1', 'ON') хорошие
        #               ('log_bin', 'gorynych-bin', 'ON')
        if not (cnf_val in ('0', 'off', 'false')) and cnv_val in ('on', 'true'):
            continue
        #Параметры типа ('query_cache_type', '0', 'OFF') хорошие
        if cnf_val in ('0', 'off', 'false') and cnv_val in ('off', 'false'):
            continue
        #Отфильтровываем ('relay_log_index', 'gorynych-relay.index', '/opt/mysql.gorynych/gorynych-relay.index')
        if cnv_val.count(cnf_val) and cnv_val.count(',') == 0:
            continue
        #Разбираем параметры раскрывающиеся в БД: ('log_slow_verbosity': 'full'='microtime'+'query_plan'+'innodb') и
        #('replicate_ignore_db', 'distribution_logs,partner_logs,partner_logs_arc', 
        #                        'partner_logs,distribution_logs,partner_logs_arc'),
        extv = EXTVARS.get(key, dict())
        fval = extv.get(cnf_val, cnf_val.split(','))
        cval = cnv_val.split(',')
        if len( set(fval) ^ set(cval) ) == 0:
            continue
        #Остаток сплитим для парсинга значений со списком параметров. Например ключ 'wsrep_provider_options' 
        #имеет строку 'gmcast.listen_addr=tcp://[::]:17401; ist.recv_addr=ppc-xtradb-standby01i.yandex.ru:17501;...'
        list_cnf = cnf_val.strip('"').replace(' ', '').split(';')
        list_cnv = cnv_val.strip('"').replace(' ', '').split(';')
        diff = set(list_cnf)-set(list_cnv)
        if opts.debug: print "[DEBUG] multicount diff before WSREP_IGNORE: %s" % diff
        diff = [ i for i in list(diff) if i.split('=')[0] not in (WSREP_IGNORE) ]
        if opts.debug: print "[DEBUG] multicount diff after WSREP_IGNORE: %s" % diff
        if not diff: continue
        fails.append(key)
    if fails:
        raise ConfigError(','.join(fails))
    return

'''Для преобразования параметров запуска в список.
'''
def callList(option, opt, value, parser):
    setattr(parser.values, option.dest, value.split(','))

'''Парсим конфиги mysql.
'''
def listConfigs(directory, *configs):
    result = list()
    dictPath = dict()
    for _cnf in configs:
        if _cnf == 'ALL': #Для возможности проверить все конфиги.
            _cnf = '*' 
        if not _cnf.startswith('/'):
            _cnf = os.path.join(directory, _cnf) #Учитываем, что файл может быть задан от относительного пути.
        dir1, file1 = os.path.dirname(_cnf), os.path.basename(_cnf)
        if not dictPath.has_key(dir1): 
            dictPath[dir1] = list()
        dictPath[dir1].append(file1)
    for _dir in dictPath:
        listFiles = os.listdir(_dir)
        listConfigs = dictPath[_dir]
        _res = [ os.path.join(_dir, f) for c in listConfigs 
                                       for f in listFiles if fnmatch.fnmatch(f, c) ]
        _res = [ f for f in _res if os.path.isfile(f) ]
        result.extend(_res)
    if opts.debug: print "[DEBUG] list configs: {0}".format(result)
    return result

'''Читаем кэш данных из mysql, чтобы лишний раз не трогать БД. 
   skip_mtime=True - позволяет пропустить проверку на возраст 
   файла и проверить возраст относительно запуска mysql.
'''
def cacheRead(ccnv, skip_mtime=False):
    cache_data = dict()
    max_time = CACHE_TIME
    try: 
        if os.path.exists(ccnv):
            mtime_cache_file = time.time() - os.stat(ccnv).st_mtime
            if skip_mtime or mtime_cache_file < max_time:
                with open(ccnv) as cv:
                    cache_data = json.load(cv)
                pid_file = cache_data.get('pid_file')
                mtime_pid_file = time.time()-os.stat(pid_file).st_mtime
                if mtime_cache_file > mtime_pid_file:
                    cache_data = dict()
    except Exception as err:
        if opts.debug:
            raise ConfigError('cache {1} error: {0}'.format(err, ccnv))
    return cache_data

'''Записываем кэш параметров mysqld в файлик.'''
def cacheWrite(ccnv, data):
    try:
        with open(ccnv, 'w') as f:
           json.dump(data.__dict__, f)
    except Exception as err:
        if opts.debug:
           raise ConfigError('cache {1} error: {0}'.format(err, ccnv))

def writeZookeeper(instance, data):
    global zkh

    try:
        key = os.path.join(ZK_LOCATION, instance[:-4] if instance.endswith('.cnf') else instance, socket.getfqdn())
        jdata = json.dumps(data.__dict__)

        zkh.ensure_path(key)
        zkh.set(key, jdata)

    except Exception as err:
        if opts.debug:
            raise

def printDiff(cnv, defaults, instance, keys):
    if opts.monrun:
        messg = "found different settings. Use: '{0} -c {1}'".format(os.path.basename(__file__), instance)
    else:
        keys = keys.split(',')
        table = list()
        title = 'found different settings:'
        table.append(title)
        head = '|| {0:40.40} | {1:40.40} | {2:40.40} ||'.format('variable', 'current', 'config')
        sep = '='*len(head)
        table.append(sep)
        table.append(head)
        table.append(sep)
        body = ['|| {0:40.40} | {1:40.40} | {2:40.40} ||'.format(i, cnv.get(i, None), defaults.get(i, None)) for i in keys ]
        table.extend(body)
        table.append(sep)
        messg = '\n'.join(table)
    return messg

def main():
    try:
        if isinstance(opts.configs, str):
            instances = listConfigs(opts.directory, opts.configs)
        if isinstance(opts.configs, list):
            instances = listConfigs(opts.directory, *opts.configs)
    except Exception as err:
        print Status(1, str(err), "listConfigs")
        if opts.debug: raise
        sys.exit(1)

    zkh.start(timeout=ZK_TIMEOUT * len(ZK_HOSTS) * 2)

    for instance in instances:
        cnf, cnv, defaults = Config(), Config(), Config()
        short_name = os.path.basename(instance)
        cache_file = '/tmp/{0}.cache'.format(short_name)
        default_file = '/tmp/{0}.defaults'.format(short_name)
        try:
            if opts.debug:
                print '[INFO] Read config {0}'.format(instance)

            cnf = parseMysqlConfig(instance, Config())
            if not cnf:
                raise ConfigError('empty config {0}'.format(instance))
            if opts.monrun:
                cnv.update(cacheRead(cache_file))
            if not cnv:
                cnv = parseMysqlVariables(instance, Config())
                cacheWrite(cache_file, cnv)

            writeZookeeper(short_name, cnv) # Записываем текущую конфигурацию
            default_variables = dict()

            #берем данные, если pid не превыет время /tmp/{0}.defaults. 
            #Отлючено, т.к. часто ложно срабатывало в моменты установки пакета и старта mysql
            #if not opts.fsettings:
            #    default_variables = cacheRead(default_file, True)#, time_alive_mysql)

            #теперь сравниваем текущие настройки из файла и системные.
            default_variables = cacheRead(default_file, True)
            defaults.update(default_variables)
            if not defaults:
                cacheWrite(default_file, cnv)
            val = Config()
            if opts.only_default:
                #проверяем текущие в БД и дефолтные значения
                val.update(defaults.__dict__)
            else:
                #накладываем конфиг поверх дефолтных значений
                d = dict()
                d.update(defaults.__dict__)
                d.update(cnf.__dict__)
                val.update(d)
            logger.debug('instance {0}\nval {1}\ncnv {2}'.format(instance, val, cnv))
            checkKeys(val, cnv) #Проверяем, что из конфига используются все опции в mysql.
            print Status(0, "", short_name)
        except ConfigError as err:
            logger.exception(err)
            if opts.monrun:
                messg = "found different settings. Use: {0} -c {1}".format(os.path.basename(__file__), os.path.basename(instance))
            else:
                messg = printDiff(cnv, val, instance, str(err))
            print Status(1, messg, short_name)
        except Exception as err:
            err = 'unknown error {0}'.format(str(err).replace('\n', ' '))
            print Status(1, str(err), short_name)
            if opts.debug: raise

    zkh.stop()
    sys.exit(0)

if __name__ == '__main__':
    parser = optparse.OptionParser( usage=USAGE )
    parser.add_option( "-d", "--debug", action="store_true", dest="debug",
                                    help="вывод отладочной информации")
    parser.add_option( "--dir", "--directory", action="store", dest="directory",
                                    help="дирректория размещения конфигов mysql",
                                    default="/etc/mysql")
    parser.add_option( "-c", "--configs", action="callback", dest="configs",
                                    help="список конфигов для проверки",
                                    type="string", callback=callList, default=None) #list. default all str.
    parser.add_option( "-t", "--run-test", action="store_true", dest="test", 
                                    help="проверить скрипт на наличие ошибок")
    parser.add_option( "-i", "--ignors", action="callback", dest="ignors", 
                                    help="список из игнорируемых параметров в конфиге mysql",
                                    type="string", callback=callList, default=list())
    parser.add_option( "-m", "--monrun", action="store_true", dest="monrun",
                                    help="выводит провеку в формате monrun")
    parser.add_option( "-f", "--first", action="store_true", dest="fsettings",
                                    help="сохранить первую копию настроек mysql в файл")
    parser.add_option( "-o", "--only-default", action="store_true", dest="only_default",
                                    help="сверяем только значения при старте с текущими")
    (opts, args) = parser.parse_args()
    opts.ignors.extend(CONFIG_IGNORS)

    logger = logging.getLogger()
    handler = logging.StreamHandler(sys.stdout) if opts.debug else logging.NullHandler()

    logger.setLevel(logging.DEBUG)
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(logging.Formatter("[%(asctime)s]\t%(levelname)-8s\t%(filename)20s:%(funcName)-20s\t%(threadName)-15s\t%(name)-15s\t%(message)s"))
    logger.addHandler(handler)

    if opts.test:
        sys.argv[1:] = args
        unittest.main()
    elif opts.configs:
        main()
    else:
        print USAGE
