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

description = """
Скрипт, чтобы не запутаться в порядке выкладки репликатора (релиз с реимпортом)

Скорее всего, можно будет приспособить и к зеркализатору.

Список релизов:
    dt-mysql2yt-release-order list

Инициализировать новый релиз (подставить номер релизного тикета):
    dt-mysql2yt-release-order init-release -t DIRECT-NNNN

Запомнить версии кода и данных:
   dt-mysql2yt-release-order -t DIRECT-NNNN set-versions --code-version 1.MMMMM.LLLLL-1
   dt-mysql2yt-release-order -t DIRECT-NNNN set-versions --data-version v.Y
   dt-mysql2yt-release-order -t DIRECT-NNNN set-versions --prev-data-version v.Z
   dt-mysql2yt-release-order -t DIRECT-NNNN set-versions --code-version 1.MMMMM.LLLLL-1 --data-version v.Y --prev-data-version v.Z

Посмотреть состояние релиза и рекомендации:
    dt-mysql2yt-release-order -t DIRECT-101099

Отметить очередной шаг выполненным:
    dt-mysql2yt-release-order -t DIRECT-NNNNNN done -c seneca-xxx -d prev_data_removed

Также см. документацию https://docs.yandex-team.ru/direct-dev/guide/jeri/deploy-b2yt
"""

import sys
import os
import yaml
import json
import argparse

from kazoo.client import KazooClient
from kazoo.exceptions import NoNodeError

ZK_HOSTS = "ppcback01f.yandex.ru,ppcback01e.yandex.ru,ppcback01i.yandex.ru"
ZK_TIMEOUT = 10
ZK_RETRY = {"max_tries": 1, "delay": 1, "max_jitter": 1, "backoff": 1, "ignore_expire": False}
zkh = KazooClient(hosts=ZK_HOSTS, timeout=ZK_TIMEOUT, connection_retry=ZK_RETRY, command_retry=ZK_RETRY)

CLUSTERS = [
        'seneca-vla',
        'seneca-man',
        'seneca-sas',
        ]
RELEASE_STAGES = [
    'initial_import_started', 
    'initial_import_completed', 
    'current_link_switched', 
    'prev_import_stopped', 
    'stable_upgraded', 
    'unstable_import_stopped', 
    'prev_data_removed', 
    ]

def recommendations( stage, state ):
    text = "??? %s" % stage
    if stage == 'initial_import_started':
        text = '(direct_java_sync_binlog_yt_2)# sudo apt-get install yandex-direct-binlog-to-yt-sync-java=%s && sudo sv start binlog-to-yt-sync' % state['code_version']
    elif stage == 'initial_import_completed':
        text = "see on dashboard https://grafana.yandex-team.ru/d/l2DJadUWz/direct-mysql-to-yt?orgId=1&from=now-12h&to=now&var-sync_state=prestable"
    elif stage == 'current_link_switched':
        text = 'ppcback# YT_TOKEN_PATH=/etc/direct-tokens/yt_robot-direct-yt yt --proxy %s link -f //home/direct/mysql-sync/%s //home/direct/mysql-sync/current' % ('seneca-XXX', state['data_version'])
        pass
    elif stage == 'prev_import_stopped':
        text = '(direct_java_sync_binlog_yt_1)# sv stop binlog-to-yt-sync'
    elif stage == 'stable_upgraded':
        text = '(direct_java_sync_binlog_yt_1)# sudo apt-get install yandex-direct-binlog-to-yt-sync-java=%s && sudo sv start binlog-to-yt-sync' % state['code_version']
    elif stage == 'unstable_import_stopped':
        text = '(direct_java_sync_binlog_yt_2)# sv stop binlog-to-yt-sync'
    elif stage == 'prev_data_removed':
        text = 'ppcback# YT_TOKEN_PATH=/etc/direct-tokens/yt_robot-direct-yt yt --proxy %s remove //home/direct/mysql-sync/%s --recursive' % ("seneca-xxx", state['prev_data_version'])
    return text

def parse_options():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-t", dest="ticket", help="release ticket id", type=str)
    parser.add_argument("-c", dest="cluster", help="yt cluster", type=str)
    parser.add_argument("-d", dest="stage_done", help="completed stage", type=str)
    parser.add_argument("--code-version", dest="code_version", help="code version", type=str)
    parser.add_argument("--data-version", dest="data_version", help="new data version", type=str)
    parser.add_argument("--prev-data-version", dest="prev_data_version", help="prev data version", type=str)
    parser.add_argument("-h", "--help", dest="help", help="Справка", action="store_true")
    opts, extra = parser.parse_known_args()

    if opts.help:
        print description
        print parser.format_help()
        exit(0)

    opts.cmd = ''
    if len(extra) > 0:
        opts.cmd = extra.pop(0)

    opts.extra = extra

    if not opts.ticket and opts.cmd not in ['list']:
        exit("expecting -t <ticket_id>" 
                + "\nRun 'dt-mysql2yt-release-order list' to view known releases." 
                + "\nTo initialise new release see https://docs.yandex-team.ru/direct-dev/guide/jeri/deploy-b2yt"
                )

    return opts

def normalize_state(st):
    if 'clusters' not in st:
        st['clusters'] = {}
    if 'code_version' not in st:
        st['code_version'] = '<code version>'
    if 'data_version' not in st:
        st['data_version'] = '<data version>'
    if 'prev_data_version' not in st:
        st['prev_data_version'] = '<previous data version>'
    for dc in CLUSTERS:
        if not dc in st['clusters']:
            st['clusters'][dc] = {}
        for stage in RELEASE_STAGES:
            if not stage in st['clusters'][dc]:
                st['clusters'][dc][stage] = '-'
        for k in st['clusters'][dc].keys():
            if not k in RELEASE_STAGES:
                print "!!! unknown stage %s" % k 
                #del(st['clusters'][dc][k])
    return

def what_to_do(st):
    to_do = []
    for dc in CLUSTERS:
        for stage in RELEASE_STAGES:
            if st['clusters'][dc][stage] == '-':
                to_do.append( (dc, stage) )
                break
    return to_do


def print_to_do(state):
    to_do = what_to_do(state)
    print ""
    if not len(to_do) > 0:
        print "TODO: nothing"
        return
    print "TODO %s / %s" % (state['data_version'], state['code_version'])
    for c in to_do:
        print "  %s waits for %s; todo: %s" % (c[0], c[1], recommendations(c[1], state))
    return


def print_state(state):
    print ""
    print "current state %s / %s" % (state['data_version'], state['code_version'])
    for dc in CLUSTERS:
        print "%s" % dc
        for stage in RELEASE_STAGES:
            print "  %-27s %s" % (stage, state['clusters'][dc][stage])
    return


def cmd_default():
    zk_path = '/direct/state/release-java-b2yt/%s' % opts.ticket
    print "zk_path: %s" % zk_path

    state_json = zkh.get(zk_path)[0]
    state = json.loads(state_json)

    normalize_state(state)
    #print "%s" % [ state ]

    if opts.cluster and opts.stage_done:
        if opts.cluster not in CLUSTERS:
            die("unknown cluster %s, stop" % opts.cluster)
        if opts.stage_done not in RELEASE_STAGES:
            die("unknown stage %s, stop" % opts.stage_done)
        state['clusters'][opts.cluster][opts.stage_done] = 'V'

    print_state(state)
    print_to_do(state)

    state_json = json.dumps(state, indent=4, sort_keys=True)
    zkh.set(zk_path, state_json)

    return

def cmd_done():
    zk_path = '/direct/state/release-java-b2yt/%s' % opts.ticket
    print "zk_path: %s" % zk_path

    if not opts.cluster:
        exit("expecting -c <cluster code>")
    if not opts.stage_done:
        exit("expecting -d <stage code>")

    state_json = zkh.get(zk_path)[0]
    state = json.loads(state_json)

    normalize_state(state)
    #print "%s" % [ state ]

    if opts.cluster not in CLUSTERS:
        die("unknown cluster %s, stop" % opts.cluster)
    if opts.stage_done not in RELEASE_STAGES:
        die("unknown stage %s, stop" % opts.stage_done)

    state['clusters'][opts.cluster][opts.stage_done] = 'V'

    print_state(state)
    print_to_do(state)

    state_json = json.dumps(state, indent=4, sort_keys=True)
    zkh.set(zk_path, state_json)

    return


def cmd_set_versions():
    zk_path = '/direct/state/release-java-b2yt/%s' % opts.ticket
    print "zk_path: %s" % zk_path

    state_json = zkh.get(zk_path)[0]
    state = json.loads(state_json)

    normalize_state(state)
    #print "%s" % [ state ]

    if opts.code_version:
        state['code_version'] = opts.code_version
    if opts.data_version:
        state['data_version'] = opts.data_version
    if opts.prev_data_version:
        state['prev_data_version'] = opts.prev_data_version

    state_json = json.dumps(state, indent=4, sort_keys=True)
    zkh.set(zk_path, state_json)

    print_state(state)

    return


def cmd_init_release():
    zk_path = '/direct/state/release-java-b2yt/%s' % opts.ticket

    if zkh.exists(zk_path):
        exit("node '%s' already exists, stop\nTo remove it (not recommended): direct-zkcli -H <one of ppcback*> rm <node path>" % zk_path)

    print "zk_path: %s" % zk_path
    state = {}

    normalize_state(state)

    state_json = json.dumps(state, indent=4, sort_keys=True)
    zkh.create(zk_path, state_json)

    return


def cmd_list():
    zk_path = '/direct/state/release-java-b2yt'
    print "zk_path: %s" % zk_path

    releases = zkh.get_children(zk_path)

    print "releases: %s" % releases
    # для сортировки вытаскиваем номер тикета как число и сортируем по нему
    for r in sorted(releases, key = lambda x: int(''.join(filter(unicode.isdigit, x)))): 
        path = "%s/%s" % (zk_path, r)
        state_json = zkh.get(path)[0]
        state = json.loads(state_json)
        normalize_state(state)
        print "  %-13s: %-14s / %-19s (%s)" % (r, state['data_version'], state['code_version'], path)

    return


def run():
    global zkh
    global opts

    opts = parse_options()

    zkh.start()

    if not opts.cmd:
        cmd_default()
    elif opts.cmd == 'list':
        cmd_list()
    elif opts.cmd == 'init-release':
        cmd_init_release()
    elif opts.cmd == 'set-versions':
        cmd_set_versions()
    elif opts.cmd == 'done':
        cmd_done()
    else:
        exit("unknown cmd '%s', stop" % opts.cmd)
    
    zkh.stop()
    return


def die(message=''):
    sys.stderr.write("%s\n" % message)
    exit(1)


if __name__ == '__main__':
    run()


