#!/usr/bin/python
# -*- coding: utf8 -*-

description = """
Скрипт для работы с релизами 

Примеры: 

Список приложений:
direct-release list-apps

Собрать новый релиз:
direct-release -a java-intapi -s testing create-release

Посмотреть, что сейчас можно/рекомендуется сделать:
direct-release -a java-intapi -s testing what-to-do

Посмотреть текущее состояние релиза (хранится в ZK):
direct-release -a java-intapi -s testing show-state
"""

"""
Частные команды:
direct-release -a java-intapi -s testing dmove

direct-release -a java-intapi -s testing new-release
direct-release -a java-intapi -s testing create-sandbox-task
direct-release -a java-intapi -s testing wait-packages
direct-release -a java-intapi -s testing dmove
direct-release -a java-intapi -s testing test-update

direct-release -a java-intapi -s testing track

direct-release -a java-intapi -s testing hotfix NNNN,MMMMM
direct-release -a java-intapi -s testing merge NNNN
direct-release -a java-intapi -s testing slide NNNN
"""

"""
TODO
 + проверять исходное состояние: не делать dmove, если не сделано packages, не делать tracker, если нет packages и т.п.
 + вовремя выкидывать done от предыдущей итерации 
 + состояние хранить в ZK
 + определиться, где хранить apps_conf
 + уменьшить логирование от ZK
 + what-to-do и внутри два списка: recommended, possible (=> "приоритеты" действий)
 + "дождаться индексации" (перед dmove)
 + блокировки против одновременной работы с одним релизом
 + группировки действий: "сделать релиз", "сделать хотфикс"
 - манипуляции с состоянием -- в кр. случае есть direct-zkcli vim
 + тесты последовательности действий: рекомендуемое всегда возможно, рекомендации всегда исчерпываются 
 + list-apps
 + в help -- человеческий текст
 + в what-to-do -- краткие описания
 + нельзя slide после hotfix
 + кастомные функции проверки для prerequisites
 + тест на негативные сценарии
 + перед созданием релиза проверять, что предыдущий уже протестирован

Очень надо
 + хотя бы ручную инициализацию stable-релиза, чтобы делать хотфиксы
 + в open-release сверять компоненту релиза и приложение 
 + finish только если статус релиза подходящий
 * мульти-стейдж действия (объявить готовым к выкладке, объявить продакшеновым)
 ! если составное действие прервано/сломалось -- уметь "resume" (доделать что оставалось)
 * везде где требуется версия -- проверять, что она действительно есть
 ! действие next -- сделать самое приоритетное из рекомендованного
 * show-state и другое читающее -- без лока

Просто надо
 * уведомления "готово" (джаббер/телеграм/почта?)
 * действие "переименовать" (+ возможно "задать название заранее")
 * check-version; если не совпадает -- снимать done.test-update
 * для dmove/test-update уметь брать версию из тикета?
 * упростить систему проверки предусловий
 * овервью всех записанных релизов для всех приложений
 * если не проходят проверки предусловий -- диагностику выдавать
 * уметь выдавать команде разные приоритеты по результатам проверки предусловий
"""

import os
import re
import sys

sys.path.insert(0, '/opt/direct-py/startrek-python-client-sni-fix')
sys.path.insert(0, '/var/lib/direct-release/')
if not re.match(r'^/usr/', os.path.dirname(os.path.abspath(__file__))):
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) + '/../var/lib/direct-release/')

import argparse
import copy
import datetime
import getpass
import json
import logging
import requests
import socket
import subprocess
import time
import yaml
import tempfile
from collections import defaultdict
from random import randint

from requests.packages.urllib3.exceptions import InsecureRequestWarning
# чтобы не было ворнингов при обращении к tabula/yamb_send
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

from project_specific import ProjectSpecificSettings

# делается в init_state только если надо
#import zookeeper
#from external_state import zk_sync_init
release_type = None
cmds = None
apps_conf_file = os.environ['APPS_CONF'] if 'APPS_CONF' in os.environ else '/etc/yandex-direct/direct-apps.conf.yaml'
apps_conf = None
state = None
state_location = None 
#state_storage_type = 'fs'
state_storage_type = 'zk'
zk_servers = ['ppctest-zookeeper01i.sas.yp-c.yandex.net:2181', 'ppctest-zookeeper01f.myt.yp-c.yandex.net:2181', 'ppctest-zookeeper01v.vla.yp-c.yandex.net:2181']
zkh = None
project_specific = ProjectSpecificSettings()
reqid = randint(100000000, 110000000)

ALLOWED_PRIORITY_TYPES = ['recommended', 'useful', 'possible',]

logger = logging.getLogger('direct-release')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s {0},%(name)s/%(funcName)s,{1}:0:{1} %(levelname)s %(message)s".format(socket.getfqdn(), reqid) ,datefmt="%Y-%m-%d:%H:%M:%S")
rootLogger = logging.getLogger()
fh = logging.FileHandler('/var/log/yandex/direct-production/messages.%s' % datetime.datetime.today().strftime('%Y%m%d'))
fh.setLevel(logging.INFO)
fh.setFormatter(formatter)
logger.addHandler(fh)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)

# данные, которые хочется передавать внутри процесса между шагами, работающими с разными стейджами
inter_state_data = {}

external_programs = {
        'create-master-connection': 'create-master-connection',
        'get-prod-version-zk': 'direct-prod-version-zk',
        'get-prod-version-yadeploy': 'dt-get-yadeploy-app-versions',
        'create-sandbox-task': 'sandbox-ya-package.pl',
        'sandbox-resource-release': 'dt-sandbox-resource-release.py',
        'wait-dist': 'wait-package-on-dist',
        'wait-packages': 'sandbox-ya-package.pl',
        'dmove_stable': 'beta-update',
        'dmove_testing': 'dt-dist',
        'update-java-ts': 'direct-java-test-update',
        'update-java-dev': 'java-test-update',
        'update-yadeploy-ts': 'dt-update-yadeploy-stage',
        'write-tracker': 'java-release.pl',
        'write-tracker-perl': 'direct-release-tracker-perl.pl', 
        'ready-for-test': 'dt-release-rft.py',
        'change-issue-status-or-comment': 'dt-tracker-change-status-comment.py',
        'hotfix-merge': 'arcadia-hotfix.pl',
        'hotfix-merge-perl': 'svn-hotfix',
        'release-ticket-report': 'release-ticket-report',
        'find-packages-to-dmove': 'find-packages-to-dmove.pl',
        'build-perl-direct': 'build-direct.pl', 
        'update-ts-perl': 'direct-test-update',
        'release-beta-up': 'release-beta-up-direct.sh',
        'release-flag-perl': 'beta-update',
        'check-release': 'dt-check-release-consistency',
        'dmove-dependencies': 'direct-mass-dmove',
        'dt-deps-manager': 'dt-deps-manager',
        'get-last-changed-rev': 'get-last-changed-rev.sh',
        'translations-for-release-branch-perl': 'upd-release-i18n.sh',
        'get-changelog': 'release-changelog',
        'wait-changelog': 'wait-changelog',
        'svn-info': ['svn', 'info'],
        #'translations-for-release-branch-perl': '/home/lena-san/c/direct-utils/direct-release-tools/bin/upd-release-i18n.sh',
        }

empty_current_build = {
        "type": None,
        "done": {},
        "sandbox_task": "", 
        "deploy_tickets": {},
        "source": "", 
        "version": "",
        "version-to-be": "",
        "branch": "",
        "head-rev": "",
        "hotfix_revisions": [],
        } 
empty_state = {
    "_format_": 1, 
    "current_build": copy.deepcopy(empty_current_build),
    "ticket": "",
    "build_type": "",
    "finished": 0,
    }


def init_common_cmds():
    global cmds
    cmds = {
            'sleep': {
                'code': cmd_sleep,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'пауза на указанное кол-во секунд; для отладки',
                'notify': True,
                'can_work_on_finished_release': True,
                },
            'create-master-connection': {
                'code': cmd_master_connection,
                'can_work_on_finished_release': True,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'создать мастер-соединение',
                },
            'ensure-screen-tmux': {
                'code': cmd_ensure_screen_tmux,
                'can_work_on_finished_release': True,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'проверить, что direct-release запущен из под screen или tmux',
                },
            'zookeeper-info': {
                'code': cmd_zookeeper_info,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'адрес ZK и ноды с состоянием; для отладки',
                'can_work_on_finished_release': True,
                },
            'list-apps': {
                'code': cmd_list_apps,
                'can_work_without_app': True,
                'can_work_without_stage': True,
                'priority': {'type': 'possible', 'priority': 40},
                'description': 'список приложений',
                },
            'what-to-do': {
                'code': cmd_what_to_do,
                'can_work_on_finished_release': True,
                'priority': {'type': 'useful', 'priority': 70},
                'description': 'возможные действия при текущем состоянии',
                },
            'open-release': {
                'code': cmd_open_release,
                'can_work_on_finished_release': True,
                'priority': check_open_release_priority,
                'description': 'открыть релиз по номеру тикета',
                'accepts_params_from_main_cmd': True,
                },
            'declare-release': {
                'code': cmd_declare_release,
                'starts_build': True,
                'can_work_on_finished_release': True,
                'description': 'объявить намерение собрать новый релиз; лучше пользоваться create-release',
                'priority': check_declare_release_priority,
                },
            'ready-for-test': {
                'code': cmd_ready_for_test,
                'requires_ticket': True,
                'description': 'если переменная окружения DIRECT_RELEASE_ALLOW_AUTO_RFT непустая, переведет релиз в RFT, а иначе ничего',
                'priority': check_ready_for_test_priority,
                },
            'show-state': {
                'code': cmd_show_state,
                'can_work_on_finished_release': True,
                'priority': {'type': 'possible', 'priority': 10},
                'description': 'показать текущее состояние; для отладки',
                },
            'slide-plan': {
                'code': cmd_slide_plan,
                'starts_build': True,
                'requires_ticket': True,
                'description': 'начать сборку пакетов от свежего транка; лучше пользоваться slide',
                'priority': check_slide_priority,
                'accepts_params_from_main_cmd': True,
                },
            'finish': {
                'code': cmd_finish,
                'requires_ticket': False,
                'can_work_on_finished_release': True,
                'priority': check_finish_release_priority,
                'description': 'закончить работу с релизом (= "дотестирован", или "начать все сначала")',
                },
            'overview': {
                'can_work_without_stage': True,
                'work_with_multiple_stages': True,
                'priority': {'type': 'useful', 'priority': 58},
                'can_work_on_finished_release': True,
                'description': 'вывести основную информацию по всем релизным тикетам приложения',
                'complex': True,
                'steps': [
                     {'cmd': 'inspect-issue',
                      'stage': 'testing'},
                     {'cmd': 'inspect-issue',
                      'stage': 'stable'},
                     {'cmd': 'inspect-issue',
                      'stage': 'waiting'},
                ],
                },
            'inspect-issue': {
                'code': cmd_inspect_issue,
                'priority': {'type': 'useful', 'priority': 59},
                'can_work_on_finished_release': True,
                'description': 'вывести информацию о релизном тикете приложения',
                },
            'testing-done': {
                    #'priority': 54,
                'complex': True,
                'can_work_on_finished_release': False,
                'priority': check_finish_release_priority,
                'can_work_on_finished_release': True,
                'steps': [
                    'testing-done-for-ticket',
                    'push-to-queue',
                    'finish',
                ],
                'description': 'завершить тестирование и отправить релиз в очередь',
                },
            'testing-done-for-ticket': {
                'code': cmd_testing_done_for_ticket,
                'priority': 53,
                'prerequisites_check': check_testing_done_for_ticket_ok,
                'can_work_on_finished_release': True,
                'description': 'переводит релизный тикет из статуса "Тестируется" в "RM Acceptance", если можно, для ещё не протестированных релизов выдаёт ошибку, для уже протестированных ничего не делает',
                },
            'push-to-queue': {
                'code': cmd_push_to_queue,
                'priority': 53,
                'prerequisites_check': check_push_to_queue_ok,
                'can_work_on_finished_release': True,
                'description': 'добавляет релиз из testing в конец очереди',
                },
            'return-to-queue': {
                'code': cmd_return_to_queue,
                'priority': check_return_to_queue_priority,
                'can_work_on_finished_release': True,
                'description': 'передвигает релиз из stable а начало очереди',
                },
            'next-waiting': {
                'code': cmd_next_waiting,
                'priority': 52,
                'prerequisites_check': check_next_waiting_ok,
                'can_work_on_finished_release': True,
                'description': 'берет первый ожидающий релиз, открывает его в stable и сдвигает очередь',
                },
            'rollback': {
                'complex': True,
                'priority': check_rollback_priority,
                'can_work_on_finished_release': True,
                'steps': [
                    'return-to-queue',
                    'open-release',
                ],
                'description': 'бывший релиз из stable переезжает в начало в очереди, релиз из параметров открывается в stable',
                },
            'check': {
                'code': cmd_check_release,
                'can_work_on_finished_release': True,
                'requires_ticket': True,
                'description': 'проверить консистентность релиза',
                'priority': {'type': 'possible', 'priority': 11},
                },
            'get-changelog': {
                'code': cmd_get_changelog,
                'priority': 150,
                'description': "запускает получение changelog'а для конкретного приложения в фоне и сохраняет в аркадию",
                'prerequisites_check': check_get_changelog_ok,
                },
            'wait-changelog': {
                'code': cmd_wait_changelog,
                'priority': 150,
                'description': "дождаться получение changelog'а",
                'prerequisites_check': check_wait_changelog_ok,
                },
            'continue-create-release': {
                'code': None,
                'priority': check_continue_create_release_priority,
                'description': "продолжить упавшую сборку релиза; внутри проверит, что ситуация похожа на прерванную сборку релиза",
                },
            'continue-hotfix': {
                'code': None,
                'priority': check_continue_hotfix_priority,
                'description': "продолжить упавшую сборку хотфикса; внутри проверит, что ситуация похожа на прерванную сборку хотфикса",
                },
            'continue-slide': {
                'code': None,
                'priority': check_continue_slide_priority,
                'description': "продолжить упавший slide",
                },
           }
    return

def init_cmds_common_deb():
    global cmds

    cmds.update({
            'gpg-cache': {
                'code': cmd_gpg_cache,
                'can_work_on_finished_release': True,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'прогреть кеш gpg-агента',
                },
            'wait-dist': {
                'code': cmd_wait_dist,
                'description': 'дождаться индексации пакетов на dist-е',
                'priority': check_wait_dist_priority,
                'notify': True,
                },
            'dmove': {
                'code': cmd_dmove,
                'description': 'запросить перемещение пакетов на dist-е',
                'priority': check_dmove_priority,
                'notify': True,
                },
            'dmove-dependencies': {
                'code': cmd_dmove_dependencies,
                'description': 'запросить перемещение зависимостей',
                'priority': check_dmove_dependencies_priority,
                'notify': True,
                },
            'to_dmove': {
                'code': cmd_to_dmove,
                'priority': 56,
                'description': 'ищет недопередвинутые зависимости для текущего собранного пакета (использовать с direct-mass-dmove)',
                'prerequisites_check': check_to_dmove_ok,
                },
            })
    return


def init_cmds_java_anyplatform():
    global cmds

    cmds.update({
            'clear-sandbox-task': {
                'code': cmd_clear_sandbox_task,
                'priority': 61,
                'prerequisites_check': check_clear_sandbox_task_ok,
                'description': 'нужна на случай, если таска в сэндбоксе не выполнилась',
                },
            'rename': {
                'code': cmd_rename,
                'priority': 45,
                'can_work_on_finished_release': True,
                'requires_ticket': True,
                'description': 'переименовать релиз',
                'prerequisites_check': check_rename_ok,
                },
            'track': {
                'code': cmd_track,
                'priority': 110,
                'description': 'записать все что нужно в трекер',
                'prerequisites_check': check_track_ok,
                },
            'hotfix-plan': {
                'code': cmd_hotfix_plan,
                'starts_build': True,
                'requires_ticket': True,
                'priority': 15,
                'description': 'объявить намерение добавить хотфикс; лучше пользоваться hotfix',
                'prerequisites_check': check_hotfix_plan_ok,
                'accepts_params_from_main_cmd': True,
                },
            'hotfix-merge': {
                'code': cmd_hotfix_merge,
                'requires_ticket': True,
                'priority': 150,
                'description': 'замержить хотфикс в релизный бранч',
                'prerequisites_check': check_hotfix_merge_ok,
                'notify': True,
                },
            'create-sandbox-task': {
                'code': cmd_create_sandbox_task,
                'priority': 150,
                'description': 'создать сандбоксовый таск на сборку пакетов',
                'prerequisites_check': check_create_sandbox_task_ok,
                },
            'wait-packages': {
                'code': cmd_wait_packages,
                'priority': 150,
                'description': 'дождаться сборки пакетов',
                'prerequisites_check': check_wait_packages_ok,
                'notify': True,
                },
            'test-update': {
                'code': cmd_test_update,
                'description': 'обновить ТС',
                'priority': check_test_update_priority,
                'notify': True,
                'can_work_on_finished_release': True,
                },
            'devtest-update': {
                'code': cmd_devtest_update,
                'description': 'обновить devtest',
                'priority': check_devtest_update_priority,
                'notify': True,
                'can_work_on_finished_release': True,
                },
            'dev7-update': {
                'code': cmd_dev7_update,
                'description': 'обновить dev7',
                'priority': check_dev7_update_priority,
                'notify': True,
                'can_work_on_finished_release': True,
                },
            })
    return


def init_cmds_java_deb():
    global cmds

    init_common_cmds()
    init_cmds_common_deb()
    init_cmds_java_anyplatform()

    cmds.update({
# TODO under construction
#            'next': {
#                'code': cmd_next,
#                'skip_prerequisites_check': True,
#                'priority': 80,
#                'description': 'выполнить одно самое приоритетное из рекомендованных действий (если они есть)',
#                },
            'create-release': {
                'starts_build': True,
                'can_work_on_finished_release': True,
                'complex': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'declare-release',
                    'create-sandbox-task',
                    'get-changelog',
                    'wait-packages',
                    'wait-dist',
                    'dmove',
                    'dmove-dependencies',
                    'test-update',
                    'devtest-update',
                    'dev7-update',
                    'wait-changelog',
                    'track',
                    'ready-for-test',
                    ],
                'description': 'собрать релиз',
                'priority': check_declare_release_priority,
                'notify': True,
                },
            'slide': {
                'starts_build': True,
                'requires_ticket': True,
                'complex': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'slide-plan',
                    'create-sandbox-task',
                    'get-changelog',
                    'wait-packages',
                    'wait-dist',
                    'dmove',
                    'dmove-dependencies',
                    'test-update',
                    'devtest-update',
                    'dev7-update',
                    'wait-changelog',
                    'track',
                    ],
                'description': 'сдвинуть релиз вперед по транку',
                'priority': check_slide_priority,
                'notify': True,
                },
            'test-update': {
                #TODO научить test-update работать без done.dmove
                'code': cmd_test_update,
                'description': 'обновить ТС',
                'priority': check_test_update_priority,
                'notify': True,
                'can_work_on_finished_release': True,
                },
            'devtest-update': {
                'code': cmd_devtest_update,
                'description': 'обновить devtest',
                'priority': check_devtest_update_priority,
                'notify': True,
                'can_work_on_finished_release': True,
                },
            'dev7-update': {
                'code': cmd_dev7_update,
                'description': 'обновить dev7',
                'priority': check_dev7_update_priority,
                'notify': True,
                'can_work_on_finished_release': True,
                },
            'testing-to-stable': {
                'can_work_without_stage': True,
                'work_with_multiple_stages': True,
                'priority': {'type': 'useful', 'priority': 57},
                'description': 'передвинуть релиз из testing в stable',
                'complex': True,
                'steps': [
                     {'cmd': 'finish',
                      'stage': 'testing'},
                     {'cmd': 'open-release', 
                      'stage': 'stable'}
                ],
                },
            'hotfix': {
                'starts_build': True,
                'priority': 60,
                'complex': True,
                'requires_ticket': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'hotfix-plan',
                    'hotfix-merge',
                    'create-sandbox-task',
                    'get-changelog',
                    'wait-packages',
                    'wait-dist',
                    'dmove',
                    'dmove-dependencies',
                    'test-update',
                    'devtest-update',
                    'dev7-update',
                    'wait-changelog',
                    'track',
                    ],
                'description': 'добавить хотфикс к релизу (с отведением бранча)',
                'prerequisites_check': check_hotfix_ok,
                'notify': True,
                },
            })
    return

def init_cmds_java_yadeploy():
    global cmds

    init_common_cmds()
    init_cmds_common_deb()
    init_cmds_java_anyplatform()

    cmds.update({
            'create-release': {
                'starts_build': True,
                'can_work_on_finished_release': True,
                'complex': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'declare-release',
                    'create-sandbox-task',
                    'get-changelog',
                    'wait-packages',
                    'sandbox-release-task',
                    'sandbox-wait-release',
                    'test-update',
                    'devtest-update',
                    'dev7-update',
                    'test-update-wait',
                    'devtest-update-wait',
                    'dev7-update-wait',
                    'wait-changelog',
                    'track',
                    'ready-for-test',
                    ],
                'description': 'собрать релиз',
                'priority': check_declare_release_priority,
                'notify': True,
                },
            'slide': {
                'starts_build': True,
                'requires_ticket': True,
                'complex': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'slide-plan',
                    'create-sandbox-task',
                    'get-changelog',
                    'wait-packages',
                    'sandbox-release-task',
                    'sandbox-wait-release',
                    'test-update',
                    'devtest-update',
                    'dev7-update',
                    'test-update-wait',
                    'devtest-update-wait',
                    'dev7-update-wait',
                    'wait-changelog',
                    'track',
                    ],
                'description': 'сдвинуть релиз вперед по транку',
                'priority': check_slide_priority,
                'notify': True,
                },
            'hotfix': {
                'starts_build': True,
                'priority': 60,
                'complex': True,
                'requires_ticket': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'hotfix-plan',
                    'hotfix-merge',
                    'create-sandbox-task',
                    'get-changelog',
                    'wait-packages',
                    'sandbox-release-task',
                    'sandbox-wait-release',
                    'test-update',
                    'devtest-update',
                    'dev7-update',
                    'test-update-wait',
                    'devtest-update-wait',
                    'dev7-update-wait',
                    'wait-changelog',
                    'track',
                    ],
                'description': 'добавить хотфикс к релизу (с отведением бранча)',
                'prerequisites_check': check_hotfix_ok,
                'notify': True,
                },
            'sandbox-release-task': {
                'code': cmd_sandbox_release_task,
                'priority': 150,
                'description': 'зарелизить сандбоксовый ресурс',
                'prerequisites_check': check_sandbox_release_task_ok,
                },
            'clear-sandbox-release': {
                'code': cmd_clear_sandbox_release,
                'priority': 61,
                'description': 'сбросить состояние "запущен релиз задачи в Sandbox", чтобы попытаться заново',
                'prerequisites_check': check_clear_sandbox_release_ok,
                },
            'sandbox-wait-release': {
                'code': cmd_sandbox_wait_release,
                'priority': 150,
                'description': 'дождаться релиза сандбоксового ресурса',
                'prerequisites_check': check_sandbox_wait_release_ok,
                },
            'test-update-wait': {
                'code': cmd_test_update_wait,
                'description': 'дождаться обновления ТС',
                'priority': check_test_update_wait_priority,
                'can_work_on_finished_release': True,
                },
            'devtest-update-wait': {
                'code': cmd_devtest_update_wait,
                'description': 'дождаться обновления devtest',
                'priority': check_devtest_update_wait_priority,
                'can_work_on_finished_release': True,
                },
            'dev7-update-wait': {
                'code': cmd_dev7_update_wait,
                'description': 'дождаться обновления dev7',
                'priority': check_dev7_update_wait_priority,
                'can_work_on_finished_release': True,
                },
            })
    return


def init_cmds_perl_deb():
    global cmds

    init_common_cmds()
    init_cmds_common_deb()

    cmds.update({
            'flag-set': {
                'code': cmd_flag_set_perl,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'установить релизный флаг на указанное кол-во минут',
                'can_work_on_finished_release': True,
                'notify': True,
                },
            'flag-drop': {
                'code': cmd_flag_drop_perl,
                'priority': {'type': 'possible', 'priority': 2},
                'description': 'сбросить релизный флаг',
                'can_work_on_finished_release': True,
                'notify': True,
                },
            'rename': {
                'code': cmd_rename_perl,
                'priority': 45,
                'can_work_on_finished_release': True,
                'requires_ticket': True,
                'description': 'переименовать релиз',
                'prerequisites_check': check_rename_ok,
                },
            'build-packages': {
                'code': cmd_build_packages_perl,
                'requires_ticket': False,
                'priority': 60,
                'description': 'собрать пакеты',
                'prerequisites_check': check_build_packages_perl_ok,
                'notify': True,
                },
            'test-update': {
                'code': cmd_test_update_perl,
                'requires_ticket': False,
                'priority': 60,
                'description': 'обновить ТС',
                'prerequisites_check': check_test_update_perl_ok,
                'can_work_on_finished_release': True,
                'notify': True,
                },
            'release-beta-up': {
                'code': cmd_release_beta_up_perl,
                'requires_ticket': False,
                'priority': 60,
                'description': 'обновить ТС',
                'prerequisites_check': check_release_beta_up_perl,
                'can_work_on_finished_release': True,
                'notify': True,
                },
            'slide': {
                'starts_build': True,
                'requires_ticket': True,
                'complex': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'gpg-cache',
                    'slide-plan',
                    'build-packages',
                    'wait-dist',
                    'dmove',
                    'track', 
                    ],
                'description': 'сдвинуть релиз вперед по транку',
                'priority': check_slide_priority,
                'notify': True,
                },
            'create-release': {
                'starts_build': True,
                'can_work_on_finished_release': True,
                'complex': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'gpg-cache',
                    #'flag-set',
                    'declare-release',
                    'build-packages',
                    'wait-dist',
                    'dmove',
                    'dmove-dependencies',
                    'track',
                    'test-update',
                    'release-beta-up',
                    ],
                'description': 'собрать релиз',
                'priority': check_declare_release_priority,
                'notify': True,
                },
            'hotfix': {
                'starts_build': True,
                'priority': 60,
                'complex': True,
                'requires_ticket': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'gpg-cache',
                    'hotfix-plan',
                    'hotfix-merge',
                    'build-packages',
                    'wait-dist',
                    'dmove',
                    'track', 
                    ],
                'description': 'добавить хотфикс к релизу (с отведением бранча)',
                'prerequisites_check': check_hotfix_ok,
                'notify': True,
                },
            'hotfix-plan': {
                'code': cmd_hotfix_plan,
                'starts_build': True,
                'requires_ticket': True,
                'priority': 15,
                'description': 'объявить намерение добавить хотфикс; лучше пользоваться hotfix',
                'prerequisites_check': check_hotfix_plan_ok,
                'accepts_params_from_main_cmd': True,
                },
            'hotfix-merge': {
                'code': cmd_hotfix_merge_perl,
                'requires_ticket': True,
                'priority': 150,
                'description': 'замержить хотфикс в релизный бранч',
                'prerequisites_check': check_hotfix_merge_ok,
                'notify': True,
                },
            'hotfix-merge-manual-complete': {
                'code': cmd_hotfix_merge_complete_perl,
                'requires_ticket': True,
                'priority': 1,
                'description': 'записать результаты ручного мержа хотфикса',
                'prerequisites_check': check_hotfix_merge_ok,
                'notify': False,
                },
            'track': {
                'code': cmd_track_perl,
                'priority': 110,
                'description': 'записать все что нужно в трекер',
                'prerequisites_check': check_track_ok_perl,
                },
            'update-translations-front': {
                'starts_build': True,
                'priority': 60,
                'complex': True,
                'requires_ticket': True,
                'steps': [
                    'ensure-screen-tmux',
                    'create-master-connection',
                    'gpg-cache',
                    'hotfix-plan',
                    'get-translations-from-tanker-front',
                    'build-packages',
                    'wait-dist',
                    'dmove',
                    'track', 
                    ],
                'description': 'добавить хотфикс к релизу (с отведением бранча)',
                'prerequisites_check': check_hotfix_ok,
                'notify': True,
                },
            'get-translations-from-tanker-front': {
                'code': cmd_get_translations_from_tanker_frontend_perl,
                'priority': check_update_translations_frontend_priority,
                'description': 'Привезти в релизную ветку переводы фронтенда из Танкера',
                },
            'testing-to-stable': {
                    # отдельная команда из-за того, что сюда надо срочно внедрить проверку недомерженных продакшен-хотфиксов
                    # если она сможет работать и для java -- можно потом объединить обратно
                'can_work_without_stage': True,
                'work_with_multiple_stages': True,
                'priority': {'type': 'useful', 'priority': 57},
                'description': 'передвинуть релиз из testing в stable',
                'complex': True,
                'steps': [
                    # TODO здесь должен быть check
                     {'cmd': 'finish',
                      'stage': 'testing'},
                     {'cmd': 'open-release', 
                      'stage': 'stable'}
                ],
                },
        })

    return


def apply_test_conf(conf_file):
    global apps_conf_file
    global state_storage_type
    global external_programs
    with open(conf_file) as fh:
        test_conf = yaml.load(fh)
    apps_conf_file = test_conf['apps_conf_file']
    if state_storage_type == 'zk':
        state_storage_type = 'fs'
    elif state_storage_type == 'empty':
        pass
    else:
        die()
    os.environ['STATE_DIR'] = test_conf['state_dir']
    external_programs = test_conf['external_programs']
    return


def init_apps_conf():
    global apps_conf
    with open(apps_conf_file) as fh:
        apps_conf = yaml.load(fh)
    return


def init_state(opts):
    global state_location
    global state_location_zk
    global lock_location_zk
    global zkh

    if state_storage_type == 'fs':
        if not opts.app in apps_conf['apps']:
            die("unknown app '%s'" % opts.app)

        if 'direct-release' in apps_conf['apps'][opts.app].get("ignore-features", []):
            die("unsupported app '%s'" % opts.app)

        prefix = os.environ['STATE_DIR']
        match_result = re.match(ur"^waiting-([0-9]+)$", opts.stage) if opts.stage else None
        if match_result:
            state_location = {opts.stage: prefix + '/' + 'waiting/' + opts.app + '/' + match_result.group(1)}

        elif 'work_with_multiple_stages' in cmds[opts.cmd] and cmds[opts.cmd]['work_with_multiple_stages']:
            queue_location = get_queue_location(opts.app)
            items = list_queue(queue_location)

            state_location = {'testing': prefix + '/' + 'testing' + '/' + opts.app,
                              'stable':  prefix + '/' + 'stable' + '/' + opts.app}
            state_location.update({"waiting-%s" % item: "%s/%s" % (queue_location, item) for item in items})
        else:
            state_location = {opts.stage: prefix + '/' + opts.stage + '/' + opts.app}

    elif state_storage_type == 'zk':
        if not opts.app in apps_conf['apps']:
            die("unknown app '%s'" % opts.app)

        if 'direct-release' in apps_conf['apps'][opts.app].get("ignore-features", []):
            die("unsupported app '%s'" % opts.app)
        # zookeeper-специфические модули загружаем только если надо
        # -- удобно для тестирования с state_storage_type = "fs"
        exec "import zookeeper" in globals()
        exec "from external_state import zk_sync_init" in globals()
        zkh = zk_sync_init(",".join(zk_servers), None, 1000)
        
        prefix = os.environ['STATE_ZK_NODE'] if 'STATE_ZK_NODE' in os.environ else '/direct/release-state'

        match_result = re.match(ur"^waiting-([0-9]+)$", opts.stage) if opts.stage else None
        if match_result:
            state_location_zk = {opts.stage: prefix + '/' + 'waiting/' + opts.app + '/' + match_result.group(1)}
            lock_location_zk = {opts.stage: prefix + '/' + 'waiting/' + opts.app + '.lock'}
       
        elif 'work_with_multiple_stages' in cmds[opts.cmd] and cmds[opts.cmd]['work_with_multiple_stages']:
            queue_location = get_queue_location(opts.app)
            items = list_queue(queue_location)

            state_location_zk = {'testing': prefix + '/' + 'testing' + '/' + opts.app,
                                 'stable':  prefix + '/' + 'stable' + '/' + opts.app}
            state_location_zk.update({"waiting-%s" % item: "%s/%s" % (queue_location, item) for item in items})

            lock_location_zk = {'testing': prefix + '/' + 'testing' + '/' + opts.app + '.lock',
                                'stable':  prefix + '/' + 'stable' + '/' + opts.app + '.lock',
                                'waiting': queue_location + '.lock'}
            
        else:
            state_location_zk = {opts.stage: prefix + '/' + opts.stage + '/' + opts.app}
            lock_location_zk = {opts.stage: prefix + '/' + opts.stage + '/' + opts.app + '.lock'}

        #die("unsupported state storage '%s'" % state_storage_type)
        #init_zk()
    elif state_storage_type == 'empty':
        pass
    else:
        die("unsupported state storage '%s'" % state_storage_type)


def init_zk():
    return


class external_state:
    def __init__(self, stage=None):
        self.stage = stage
    def __enter__(self):
        if state_storage_type == 'fs':
            read_state_file(state_location[self.stage])
        elif state_storage_type == 'zk':
            read_state_zk(state_location_zk[self.stage])
        elif state_storage_type == 'empty':
            pass
        else:
            die("unsupported state storage '%s'" % state_storage_type) 
        return
    def __exit__(self, type, value, traceback):
        if type != None or value != None or traceback != None:
            return False
        if state_storage_type == 'fs':
            write_state_file(state_location[self.stage])
        elif state_storage_type == 'zk':
            write_state_zk(state_location_zk[self.stage])
        elif state_storage_type == 'empty':
            pass
        else:
            die("unsupported state storage '%s'" % state_storage_type) 
        return False


class external_state_lock:
    def __init__(self, custom_lock_location=None, info_only=False):
        # по флагу info_only не будем падать если не получилось взять лок, будем только возвращать список существующих локов
        self.info_only = info_only
        if state_storage_type == 'zk':
            self.lock_location = custom_lock_location if custom_lock_location else lock_location_zk

    def __enter__(self):
        current_locks = []
        if state_storage_type == 'fs':
            pass
        elif state_storage_type == 'zk':
            for stage in self.lock_location:
                try:
                    ZOO_OPEN_ACL_UNSAFE = [{"perms":0x1f, "scheme":"world", "id" :"anyone"}]
                    lock_info = "host %s\nlogin %s\ntime %s" % (socket.getfqdn(), getpass.getuser(), time.ctime() )
                    zookeeper.create(zkh, self.lock_location[stage], lock_info, ZOO_OPEN_ACL_UNSAFE, zookeeper.EPHEMERAL)
                except zookeeper.NodeExistsException:
                    cont, _ = zookeeper.get(zkh, self.lock_location[stage])
                    if self.info_only:
                        current_locks.append({'stage': stage, 'info': cont})
                    else: 
                        die("can't get lock for stage %s, stop\n\ncurrent lock info:\n%s" % (stage, cont))
                except:
                    logger.error("can't get lock for stage %s, stop" % stage)
                    logger.error("Unexpected error: %s / %s" % (sys.exc_info()[0], sys.exc_info()[1]))
                    die("")
            return current_locks
        elif state_storage_type == 'empty':
            pass
        else:
            die("unsupported state storage '%s'" % state_storage_type) 
        return current_locks
    def __exit__(self, type, value, traceback):
        if self.info_only:
            return False
        if type != None or value != None or traceback != None:
            return False
        if state_storage_type == 'fs':
            pass
        elif state_storage_type == 'zk':
            for stage in self.lock_location:
                zookeeper.delete(zkh, self.lock_location[stage])
        elif state_storage_type == 'empty':
            pass
        else:
            die("unsupported state storage '%s'" % state_storage_type) 
        return False


def die(message=''):
    logger.error("%s" % message)
    exit(1)


def my_system(cmd, verbose=False):
    if verbose:
        logger.info("going to exec: %s" % cmd)
    exit_code = subprocess.call(cmd)
    if exit_code != 0:
        die("cmd failed, stop (%s)" % cmd)
    return


def my_tee(cmd, verbose=False):
    proc = my_popen(cmd, verbose)
    lines = []
    while True:
        line = proc.stdout.readline()
        if not line:
            break
        line = line.rstrip()
        lines.append(line)
        logger.info(line)
    proc.wait()
    if proc.returncode != 0:
        die("cmd failed, stop (%s)" % cmd)
    return lines


def my_qx(cmd, verbose=False):
   if verbose:
       logger.info("going to exec: %s" % cmd)

   try:
       res = subprocess.check_output(cmd)
   except:
       die("cmd failed, stop (%s)" % cmd)

   return res


def my_popen(cmd, verbose=False, not_kill=False):
    if verbose:
        logger.info("going to exec: %s" % cmd)

    stderr = open(os.devnull, 'w') if not_kill else subprocess.PIPE
    return subprocess.Popen(
        (['nohup'] if not_kill else []) + cmd, stdout=subprocess.PIPE, stderr=stderr, close_fds=True
    )


def ext_program(name):
    if name not in external_programs:
        die("no external program registered for %s, stop" % name)
    return external_programs[name]


def component2app(c):
    apps = [a for a in apps_conf['apps'].keys() if apps_conf['apps'][a]['tracker-component'] == c ]
    if len(apps) == 1:
        return apps[0]
    die("can't find app for component %s" % c)


def set_release_type4app(app):
    global release_type
    global apps_conf

    if app == None:
        # для list-apps
        init_cmds_java_deb()
        release_type = 'java_deb'
        pass
    elif app == 'direct':
        init_cmds_perl_deb()
        release_type = 'perl_deb'
    elif apps_conf['apps'][app].get('type') == 'arcadia-java':
        if apps_conf['apps'][app].get('deploy_type') == 'yadeploy':
            init_cmds_java_yadeploy()
            release_type = 'java_yadeploy'
        if apps_conf['apps'][app].get('deploy_type') == 'deb':
            init_cmds_java_deb()
            release_type = 'java_deb'
    else: #Оставил для совместимости, т.к. есть dna, logshatter, для них выставляется java_deb, можно дополнительно проверять deploy_type
        init_cmds_java_deb()
        release_type = 'java_deb'


def parse_options():
    global state_storage_type
    my_name = os.path.basename( sys.argv[0] )
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-s", dest="stage", help="stable|testing|waiting", type=str)
    parser.add_argument("-a", "--app", dest="app", help="Приложение", type=str)
    parser.add_argument("-t", "--test-conf", dest="test_conf", help="Конфиг для тестирования", type=str)
    parser.add_argument("-w", "--show-steps", dest="show_steps", help="Ничего не делать, показать список команд к выполнению (полезно для составных команд)", action="store_true")
    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)

    if len(extra) <= 0:
        die("expecting action, for suggestions run: direct-release -s %s -a %s what-to-do" % (opts.stage or '<stage>', opts.app or '<app>'))

    if opts.test_conf:
        apply_test_conf( opts.test_conf )
    init_apps_conf()

    set_release_type4app(opts.app)

    opts.cmd = extra.pop(0)

    if not opts.cmd in cmds:
        die("unknown action '%s'" % opts.cmd)

    if not opts.stage and not('can_work_without_stage' in cmds[opts.cmd] and cmds[opts.cmd]['can_work_without_stage']):
        die("expecting -s testing|stable|waiting")

    if not opts.app and not ('can_work_without_app' in cmds[opts.cmd] and cmds[opts.cmd]['can_work_without_app']):
        die("expecting -a <app>, see direct-release list-apps for list")

    if 'can_work_without_app' in cmds[opts.cmd] and cmds[opts.cmd]['can_work_without_app']:
        state_storage_type = 'empty'

    opts.extra = extra

    return opts


def read_state_file(file_path):
    global state
    state = defaultdict(str, read_state_from_file(file_path))
    return


def write_state_file(file_path=None):
    with open(file_path, 'w') as fh:
        fh.write(state_to_str(state))
    return


def read_state_zk(zk_path):
    global state
    state = defaultdict(str, read_state_from_zk(zk_path))
    return


def write_state_zk(zk_path):
    zookeeper.set(zkh, zk_path, state_to_str(state))
    return


def read_state_from_file(file_path):
    with open(file_path) as fh:
        return json.load(fh)


def read_state_from_zk(zk_path):
    global zkh
    cont, _ = zookeeper.get(zkh, zk_path)
    return json.loads(cont)


def state_to_str(state):
    return json.dumps(state, indent=4, sort_keys=True)


def get_current_base_revision_from_tracker(ticket):
    output = my_qx([ ext_program('release-ticket-report'), ticket, '-j'])
    r = json.loads("".join(output))
    m = re.match(r'1.([0-9]+)[\.-]', r['version'])
    if m:
        return m.group(1)
    die("can't get base revision for release branch, stop. output: %s" % output)
    # после die return не нужен, но для порядка пусть будет
    return

def get_current_base_revision_from_zk(app):
    output = my_tee([ ext_program('get-prod-version-zk'), app])
    m = re.match(r'1.([0-9]+)[\.-]', output[0])
    if m:
        return m.group(1)
    die("can't get base revision from ZK, stop. output: %s" % output)
    return 

def get_current_base_revision_from_yadeploy(app):
    output = my_tee([ ext_program('get-prod-version-yadeploy'), app, 'production'])
    m = re.match(r'1.([0-9]+)[\.-]', output[0].split()[-1])
    if m:
        return m.group(1)
    die("can't get base revision from Ya.Deploy, stop. output: %s" % output)
    return 

def app_short_name(app_name):
    m = re.match(r'java-(.*)$', app_name)
    if m:
        short_name = m.group(1)
    else: 
        short_name = app_name
    return short_name


def get_cmd_steps(cmd, app):
    steps = []
    if not 'complex' in cmds[cmd] or not cmds[cmd]['complex']:
        steps = [ cmd ]
    else:
        steps = cmds[cmd]['steps']

        if app and 'work_with_multiple_stages' in cmds[cmd] and cmds[cmd]['work_with_multiple_stages']:
            # раскрываем stage waiting
            items = sorted(list_queue(get_queue_location(app)))
            steps = []

            for step in cmds[cmd]['steps']:
                if step['stage'] != 'waiting':
                    steps.append(step)
                else:
                    steps.extend([{'cmd': step['cmd'], 'stage': "waiting-%s" % item} for item in items])

            # для составных команд добавляем виртуальный первый шаг только из проверки приоритета (если в этой составной команде есть проверка)
            if 'priority' in cmds[cmd]:
                steps.insert(0, {'cmd': "check-priority:%s" % cmd, 'stage': 'testing'})
        else:
            if 'priority' in cmds[cmd]:
                steps.insert(0, "check-priority:%s" % cmd)

    return steps


def get_continue_hotfix_steps():
    return get_continue_steps('hotfix')

def get_continue_slide_steps():
    return get_continue_steps('slide')

def get_continue_create_release_steps():
    return get_continue_steps('create-release')

def get_continue_steps(complex_step_name):
    '''
    для продолжения сборки релиза вычисление шагов особенное: надо знать текущий стейт
    стейт получаем снаружи, чтобы вся работа с ним была на верхнем уровне
    '''

    steps = list(cmds[complex_step_name]['steps'])
    for s in [ 'ensure-screen-tmux', 'create-master-connection', 'gpg-cache' ]:
        if s in steps:
            steps.remove(s)

    done_steps = dict(state['current_build']['done'])

    # некоторые команды записывают в done не свои имена, 
    # т.к. в java-релизах и в perl-релизах 
    # разные действия приводят к одинаковому результату: собрались пакеты
    special_done_names = {
            'wait-packages': 'packages',
            'build-packages': 'packages',
            }

    # пока есть сделанные шаги -- проверяем, что первый в списке релизных шагов уже сделан
    # и выкидываем первый релизный шаг и из списка релизных шагов, и из дикта сделанного.
    # Если после цикла дикт пустой -- успех, был сделан какой-то префикс создания релиза
    while len(done_steps) > 0:
        if len(steps) <= 0:
            die("too many done steps")
        s = steps[0]
        if s in special_done_names:
            s = special_done_names[s]
        if s in done_steps:
            steps.pop(0)
            del done_steps[s]
        else:
            break

    if len(done_steps) > 0:
        die("unexpected done steps: %s" % done_steps.keys())

    if len(steps) <= 0:
        die("everything is done already")

    steps.insert(0, 'create-master-connection')
    steps.insert(0, 'ensure-screen-tmux')

    return steps


def calc_priority(app, cmd, stage=None, verbose=False, extra=None):
    '''
    Считает, можно ли выполнять сейчас команду cmd и с каким приортитетом ее рекомендовать
    '''
    # Сначала -- запрещающие факторы: требуется тикет, требуется незафинишенный релиз
    if 'requires_ticket' in cmds[cmd] and cmds[cmd]['requires_ticket'] and not state['ticket']:
        if verbose:
            logger.warn("prerequisites for %s: ticked required" % (cmd))
        return {'type': 'denied', 'priority': 0}

    # state может отсутствовать, если команда вообще не работает со state (list-apps, например)
    if state and state['finished']:
        if 'can_work_on_finished_release' not in cmds[cmd] or not cmds[cmd]['can_work_on_finished_release']:
            if verbose:
                logger.warn("prerequisites for %s: can't work on finished release" % (cmd))
            return {'type': 'denied', 'priority': 0}

    # Если приоритет константный -- отдаем его
    if 'priority' in cmds[cmd] and type(cmds[cmd]['priority']) is dict:
        return cmds[cmd]['priority']

    # Если есть функция для вычисления приоритета -- приоритет вычисляет она
    if 'priority' in cmds[cmd] and callable(cmds[cmd]['priority']):
        return cmds[cmd]['priority'](stage=stage, cmd=cmd, verbose=verbose, app=app, extra=extra)

    # Совместимость со старой системой "отдельно применимость, отдельно константный приоритет"
    # TODO Когда ни у одной команды не останется prerequisites_check -- этот блок убрать
    if 'prerequisites_check' in cmds[cmd]:
        allowed = cmds[cmd]['prerequisites_check'](stage=stage, cmd=cmd, verbose=verbose)
        if not allowed: 
            return {'type': 'denied', 'priority': 0}
        if cmds[cmd]['priority'] < 50:
            return {'type': 'possible', 'priority': cmds[cmd]['priority']}
        elif cmds[cmd]['priority'] < 100: 
            return {'type': 'useful', 'priority': cmds[cmd]['priority']}
        else:
            return {'type': 'recommended', 'priority': cmds[cmd]['priority']}

    # Если попали сюда, значит, не смогли расчитать приоритет, это ошибка, запрещаем
    if verbose:
        logger.warn("prerequisites for %s: can't calculate command priority" % (cmd))
    return {'type': 'denied', 'priority': 0}


def do_one_cmd(cmd, app, stage, extra, main_cmd):
    global state
    logger.info("### current cmd: %s" % cmd)

    # бывают команды специального вида, только для проверки прав (приоритета) на составных команадх
    # в них надо только проверить приоритет и больше ничего не делать 
    check_only = None
    cmd_for_priority_check = cmd
    match_result = re.match(ur"^check-priority:(.*)$", cmd)
    if match_result:
        cmd_for_priority_check = match_result.group(1)
        check_only = 1

    p = calc_priority(app, cmd_for_priority_check, stage=stage, verbose=True, extra=extra)
    if not p['type'] in ALLOWED_PRIORITY_TYPES:
        die("prerequisites failed for %s" % cmd);

    if check_only:
        return

    if 'starts_build' in cmds[cmd] and cmds[cmd]['starts_build']:
        state["current_build"]= defaultdict(str, copy.deepcopy(empty_current_build))

    filtered_extra = []
    if main_cmd == cmd or 'accepts_params_from_main_cmd' in cmds[cmd] and cmds[cmd]['accepts_params_from_main_cmd']:
        filtered_extra = extra

    cmds[cmd]['code'](app=app, stage=stage, extra=filtered_extra, main_cmd=main_cmd)
    return


def send_message(to, msg):
    logger.info("message to send: %s" % msg)
    if not isinstance(to, basestring) or not re.match(r'^[a-z0-9\-_]+$', to):
        logger.warn("bad login for notifications: '%s', sending nothing" % (to))
        return
    payload = {'to': to, 'msg': msg}
    r = requests.get('https://direct-dev.yandex-team.ru/yamb_send', params=payload, verify=False, timeout=20)
    return


def make_observer( app, stage, cmd ):
    start_time = time.time()
    p = os.fork()
    if p != 0:
        # здесь -- родитель: дождаться завершения чайлда и послать нотификацию
        pid, status = os.waitpid( p, 0 )

        end_time = time.time()
        elapsed_time = time.time() - start_time
        elapsed_time_str = time.strftime("%H:%M:%S", time.gmtime(elapsed_time))

        child_exit_status = status >> 8
        signal = status & 127

        if signal == 0 and child_exit_status == 0:
            msg = "\n== direct-release ==\n%s/%s/%s: SUCCESS\nDuration: %s" % (app, stage, cmd, elapsed_time_str)
        else:
            msg = "\n== direct-release ==\n%s/%s/%s: FAIL\nSignal %s, exit code %s\nDuration: %s" % (app, stage, cmd, signal, child_exit_status, elapsed_time_str)
        user = getpass.getuser()
        send_message(user, msg)

        exit(child_exit_status)
    # чайлд продолжается как был
    return 


def run():
    opts = parse_options()

    # если это не тестирование и команда требует уведомления: 
    # форкаемся и основная работа продолжается в чайлде, а отец дожидается окончания и отправляет сообщения
    # важно создать наблюдателя достаточно рано, пока не наплодилось тредов; 
    # например, после init_state -- поздно, т.к. zookeeper-библиотека использует треды  
    # и манипуляций с форками не переживает
    send_notification = not os.environ.get("DIRECT_RELEASE_NO_NOTIFICATIONS", "") and not opts.test_conf and not opts.show_steps and 'notify' in cmds[opts.cmd] and cmds[opts.cmd]['notify']
    if send_notification: 
        make_observer( app=opts.app, stage=opts.stage, cmd=opts.cmd )

    init_state(opts)

    # если приложение переехало в NewCI
    if apps_conf['apps'][opts.app].get('newci_graph', ''):
        die("App '%s' has been migrated to NewCI %s\nHow to build release in NewCI: %s" % (
            opts.app,
            apps_conf['apps'][opts.app].get('newci_graph'),
            'https://docs.yandex-team.ru/direct-dev/guide/releases/release-newci'
        ))

    #print("%s\n%s\n%s\n" % (cmds, opts.cmd, apps_conf))
    no_lock = False
    if opts.cmd in ['overview', 'show-state']:
        no_lock = True

    steps = []
    if opts.cmd == 'continue-create-release':
        with external_state(stage=opts.stage):
            steps = get_continue_create_release_steps()
    elif opts.cmd == 'continue-hotfix':
        with external_state(stage=opts.stage):
            steps = get_continue_hotfix_steps()
    elif opts.cmd == 'continue-slide':
        with external_state(stage=opts.stage):
            steps = get_continue_slide_steps()
    else:
        steps = get_cmd_steps(opts.cmd, opts.app)

    if opts.show_steps: 
        print("### Planned steps:")
        print("\n".join(steps))
        exit(0)

    with external_state_lock(info_only=no_lock):
        print('RunID: %s' % reqid)
        if opts.stage:
            with external_state(stage=opts.stage):
                if state and 'ticket' in state and state['ticket']:
                    my_system([ ext_program('release-ticket-report'), state['ticket']])

        for c in steps:
            if 'work_with_multiple_stages' in cmds[opts.cmd] and cmds[opts.cmd]['work_with_multiple_stages']:
                with external_state(stage=c['stage']):
                    do_one_cmd(c['cmd'], opts.app, c['stage'], opts.extra, opts.cmd)
            else:
                with external_state(stage=opts.stage):
                    do_one_cmd(c, opts.app, opts.stage, opts.extra, opts.cmd)
    logger.info("%s: done" % opts.cmd)
    exit(0)

############################

def require_and_allow_steps(required_steps=[], allowed_steps=[], cmd=None, verbose=None):
    for s in required_steps:
        if s not in state['current_build']['done'] or not state['current_build']['done'][s]:
            if verbose:
                logger.warn("prerequisites for %s: step %s is required before %s" % (cmd, s, cmd ))
            return False

    if allowed_steps:
        for s in state['current_build']['done']:
            if s not in allowed_steps and s not in required_steps:
                if verbose:
                    logger.warn("prerequisites for %s: step %s is not allowed before %s" %(cmd, s, cmd ))
                return False

    return True

############################

def check_next_waiting_ok(stage=None, cmd=None, verbose=None):
    return stage == 'stable'


def check_rollback_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if stage == 'stable':
        return {'type': 'possible', 'priority': 2}
    else:
        return {'type': 'denied', 'priority': 0}

def check_testing_done_for_ticket_ok(stage=None, cmd=None, verbose=None):
    return stage == 'testing'

def check_push_to_queue_ok(stage=None, cmd=None, verbose=None):
    return stage == 'testing'


def check_return_to_queue_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if stage == 'stable':
        return {'type': 'possible', 'priority': 2}
    else:
        return {'type': 'denied', 'priority': 0}


def check_dmove_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    # Если нет версии -- то нельзя dmove
    if 'version' not in state['current_build'] or state['current_build']['version'] == '':
        return {'type': 'denied', 'priority': 0}
    # Если делалась сборка и еще не делался dmove -- рекомендуем dmove с большим приоритетом
    # иначе -- разрешаем, но не вытаскиваем в рекомендуемые
    if 'wait-dist' in state['current_build']['done'] and state['current_build']['done']['wait-dist'] and not 'dmove' in state['current_build']['done']:
        return {'type': 'recommended', 'priority': 150}
    else: 
        return {'type': 'possible', 'priority': 2}


# аналогиично check_dmove_priority
def check_dmove_dependencies_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    # Если нет версии -- то нельзя dmove-dependencies
    if 'version' not in state['current_build'] or state['current_build']['version'] == '':
        return {'type': 'denied', 'priority': 0}
    # Если делалася dmove и еще не делался dmove-dependencies -- рекомендуем dmove-dependencies с большим приоритетом
    # иначе -- разрешаем, но не вытаскиваем в рекомендуемые
    if 'dmove' in state['current_build']['done'] and state['current_build']['done']['dmove'] and not 'dmove-dependencies' in state['current_build']['done']:
        return {'type': 'recommended', 'priority': 150}
    else:
        return {'type': 'possible', 'priority': 2}


def check_slide_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    # Если java-релиз уже тестируется -- запрещаем slide, т.к. с ним тикеты слишком легко проваливаются мимо регрессии
    # в perl-релизах на slide-ы смотрят внимательно, так что там оставляем все на усмотрение пользователя
    # Upd: все-таки slide полезно, разрешаем обратно. Надо еще думать
    if False and state['ticket'] != '' and apps_conf['apps'][app]['type'] == 'arcadia-java' :
        output = my_qx([ ext_program('release-ticket-report'), state['ticket'], '-j'])
        r = json.loads("".join(output))
        if r['status'] not in ['new']:
            if verbose:
                logger.error("can't slide release of app '%s' when ticket in status '%s'" % (app, r['status']))
            return {'type': 'denied', 'priority': 0}

    if state['build_type'] == 'branch':
        return {'type': 'denied', 'priority': 0}
    if cmd == 'slide-plan':
        return {'type': 'possible', 'priority': 15}
    return {'type': 'useful', 'priority': 65}


def check_declare_release_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if stage == 'stable':
        # в stable релизы не создаем
        return {'type': 'denied', 'priority': 0}
    if state['_format_'] and not state['finished']:
        # полноценный релиз, но незафинишенный
        return {'type': 'denied', 'priority': 0}
    # предыдущий релиз закончен -- можно новый
    # самый первый релиз тоже можно (= в ZK только '{}')
    if cmd == 'create-release':
        # у create-release приоритет больше, чем у declare-release
        return {'type': 'recommended', 'priority': 60}
    else:
        return {'type': 'possible', 'priority': 2}
        
def check_ready_for_test_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    return {'type': 'possible', 'priority': 2}

def check_open_release_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if stage == 'stable':
        return {'type': 'useful', 'priority': 50}
    else: 
        if not state['_format_'] or state['finished']: 
            return {'type': 'possible', 'priority': 2}
        else: 
            return {'type': 'denied', 'priority': 0}
    return {'type': 'denied', 'priority': 0}

def check_finish_release_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if stage == 'stable':
        return {'type': 'possible', 'priority': 2}

    if stage.startswith('waiting'):
        return {'type': 'denied', 'priority': 0}
    # если тикета еще нет -- разрешаем финишировать релиз
    # Полезно для случаев, когда первичная сборка упала и хочется начать все начисто сначала
    if state['ticket'] == '':
        return {'type': 'useful', 'priority': 2}

    output = my_qx([ ext_program('release-ticket-report'), state['ticket'], '-j'])
    r = json.loads("".join(output))
    # Если исправляется список -- править и тест
    # первая скобка -- для продакшена, вторая -- для тестов
    if (r['status'] in ['closed', 'needAcceptance', 'rmAcceptance', 'readyToDeploy', 'testing' ]) or (re.search(r'allow_finish', r['status'])):
        if state['finished']: 
            return {'type': 'possible', 'priority': 2}
        elif cmd == 'testing-done':
            return {'type': 'recommended', 'priority': 105}
        else:
            # сюда попадает finish
            return {'type': 'possible', 'priority': 2}
    if verbose:
        logger.info("can't finish release when ticket in status '%s'" % r['status'])
    return {'type': 'denied', 'priority': 0}


def check_continue_create_release_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    return {'type': 'possible', 'priority': 2}

def check_continue_hotfix_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    return {'type': 'possible', 'priority': 2}

def check_continue_slide_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    return {'type': 'possible', 'priority': 2}

def check_clear_sandbox_task_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['create-sandbox-task'], allowed_steps=['hotfix-plan', 'hotfix-merge', 'get-changelog', 'declare-release'], cmd=cmd, verbose=verbose):
        return False
    return True

def check_clear_sandbox_release_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['sandbox-release-task'], cmd=cmd, verbose=verbose):
        return False
    return True

def check_create_sandbox_task_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=[], allowed_steps=['hotfix-plan', 'hotfix-merge', 'get-changelog', 'wait-changelog', 'declare-release'], cmd=cmd, verbose=verbose):
        return False

    return True


def check_wait_packages_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['create-sandbox-task'], allowed_steps=['hotfix-plan', 'hotfix-merge', 'get-changelog', 'wait-changelog', 'declare-release'], cmd=cmd, verbose=verbose):
        return False

    return True


def check_sandbox_release_task_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['create-sandbox-task', 'packages'], allowed_steps=['hotfix-plan', 'hotfix-merge', 'get-changelog', 'wait-changelog', 'declare-release'], cmd=cmd, verbose=verbose):
        return False

    return True


def check_sandbox_wait_release_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['sandbox-release-task'], allowed_steps=['hotfix-plan', 'hotfix-merge', 'get-changelog', 'wait-changelog', 'declare-release', 'packages', 'create-sandbox-task'], cmd=cmd, verbose=verbose):
        return False

    return True


def check_wait_dist_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(required_steps=['packages'], allowed_steps=['hotfix-plan', 'hotfix-merge', 'create-sandbox-task', 'get-changelog', 'wait-changelog', 'declare-release'], cmd=cmd, verbose=verbose):
        return {'type': 'denied', 'priority': 0}

    return {'type': 'recommended', 'priority': 150}


def check_test_update_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(
        required_steps=[],
        allowed_steps=[
            'hotfix-plan', 'hotfix-merge', 'packages', 'dmove', 'wait-dist', 'get-changelog', 'wait-changelog',
            'create-sandbox-task', 'track', 'dmove-dependencies', 'test-update', 'devtest-update', 'dev7-update',
            'declare-release', 'ready-for-test', 'test-update-wait', 'devtest-update-wait', 'dev7-update-wait',
            'sandbox-wait-release',
            'sandbox-release-task',
            'placeholder-comment',
        ],
        cmd=cmd,
        verbose=verbose
    ):
        return {'type': 'denied', 'priority': 0}
    if 'test-update' in state['current_build']['done'] and state['current_build']['done']['test-update']:
        # повторный test-update
        if state['release_type'] != 'java_yadeploy':
            return {'type': 'useful', 'priority': 1}
        else:
            return {'type': 'denied', 'priority': 0}
    else:
        # test-update еще не делали
        return {'type': 'recommended', 'priority': 150}


def check_devtest_update_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(
        required_steps=[],
        allowed_steps=[
            'hotfix-plan', 'hotfix-merge', 'packages', 'dmove', 'wait-dist', 'get-changelog', 'wait-changelog',
            'create-sandbox-task', 'track', 'dmove-dependencies', 'test-update', 'devtest-update', 'dev7-update',
            'declare-release', 'ready-for-test', 'test-update-wait', 'devtest-update-wait', 'dev7-update-wait',
            'sandbox-wait-release',
            'sandbox-release-task',
            'placeholder-comment',
        ],
        cmd=cmd,
        verbose=verbose
    ):
        return {'type': 'denied', 'priority': 0}
    if 'devtest-update' in state['current_build']['done'] and state['current_build']['done']['devtest-update']:
        # повторный devtest-update
        if state['release_type'] != 'java_yadeploy':
            return {'type': 'useful', 'priority': 1}
        else:
            return {'type': 'denied', 'priority': 0}
    else:
        # devtest-update еще не делали
        return {'type': 'recommended', 'priority': 150}


def check_dev7_update_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(
        required_steps=[],
        allowed_steps=[
            'hotfix-plan', 'hotfix-merge', 'packages', 'dmove', 'wait-dist', 'get-changelog', 'wait-changelog',
            'create-sandbox-task', 'track', 'dmove-dependencies', 'test-update', 'devtest-update', 'dev7-update',
            'declare-release', 'ready-for-test', 'test-update-wait', 'devtest-update-wait', 'dev7-update-wait',
            'sandbox-wait-release',
            'sandbox-release-task',
            'placeholder-comment',
        ],
        cmd=cmd,
        verbose=verbose
    ):
        return {'type': 'denied', 'priority': 0}
    if 'dev7-update' in state['current_build']['done'] and state['current_build']['done']['dev7-update']:
        # повторный dev7-update
        if state['release_type'] != 'java_yadeploy':
            return {'type': 'useful', 'priority': 1}
        else:
            return {'type': 'denied', 'priority': 0}
    else:
        # dev7-update еще не делали
        return {'type': 'recommended', 'priority': 150}


def check_test_update_wait_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(required_steps=['test-update'], cmd=cmd, verbose=verbose):
        return {'type': 'denied', 'priority': 0}
    elif 'test-update-wait' in state['current_build']['done'] and state['current_build']['done']['test-update-wait']:
        return {'type': 'denied', 'priority': 0}
    else:
        return {'type': 'recommended', 'priority': 150}


def check_devtest_update_wait_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(required_steps=['devtest-update'], cmd=cmd, verbose=verbose):
        return {'type': 'denied', 'priority': 0}
    elif 'devtest-update-wait' in state['current_build']['done'] and state['current_build']['done']['devtest-update-wait']:
        return {'type': 'denied', 'priority': 0}
    else:
        return {'type': 'recommended', 'priority': 150}


def check_dev7_update_wait_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if not require_and_allow_steps(required_steps=['dev7-update'], cmd=cmd, verbose=verbose):
        return {'type': 'denied', 'priority': 0}
    elif 'dev7-update-wait' in state['current_build']['done'] and state['current_build']['done']['dev7-update-wait']:
        return {'type': 'denied', 'priority': 0}
    else:
        return {'type': 'recommended', 'priority': 150}


def check_get_changelog_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['create-sandbox-task'], cmd=cmd, verbose=verbose):
        return False

    return True


def check_wait_changelog_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['get-changelog'], cmd=cmd, verbose=verbose):
        return False

    return True


def check_track_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(
        required_steps=['packages', 'wait-changelog'],
        allowed_steps=[
            'hotfix-plan', 'hotfix-merge', 
            'create-sandbox-task', 'get-changelog', 'wait-dist', 
            'test-update','devtest-update','dev7-update', 
            'test-update-wait', 'devtest-update-wait', 'dev7-update-wait',
            'dmove', 'dmove-dependencies', 'declare-release', 
            'sandbox-wait-release', 'sandbox-release-task'
        ],
        cmd=cmd,
        verbose=verbose
    ):
        return False

    return True

# версия для perl отличается отсутствием changelog в required_steps (в perl-релизах get-/wait-changelog не происходит); отдельная функция для perl задумана как временный фикс проблемы
def check_track_ok_perl(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(
        required_steps=['packages'],
        allowed_steps=['hotfix-plan', 'hotfix-merge', 'create-sandbox-task', 'get-changelog', 'wait-dist', 'test-update', 'dmove', 'dmove-dependencies', 'declare-release'],
        cmd=cmd,
        verbose=verbose
    ):
        return False

    return True

def check_hotfix_merge_ok(stage=None, cmd=None, verbose=None):
    if not require_and_allow_steps(required_steps=['hotfix-plan'], allowed_steps=['hotfix-plan', 'create-sandbox-task', 'get-changelog', 'wait-changelog', 'packages'], cmd=cmd, verbose=verbose):
        return False

    return True

def check_rename_ok(stage=None, cmd=None, verbose=None):
    return True

def check_hotfix_plan_ok(stage=None, cmd=None, verbose=None):
    return True

def check_hotfix_ok(stage=None, cmd=None, verbose=None):
    return True

def check_to_dmove_ok(stage=None, cmd=None, verbose=None):
    return True

def check_update_translations_frontend_priority(stage=None, cmd=None, verbose=None, app=None, extra=None):
    if state['build_type'] == 'branch':
        return {'type': 'useful', 'priority': 50}
    return {'type': 'denied', 'priority': 0 }

############################

def check_build_packages_perl_ok(stage=None, cmd=None, verbose=None):
    return True

def check_test_update_perl_ok(stage=None, cmd=None, verbose=None):
    return True

def check_release_beta_up_perl(stage=None, cmd=None, verbose=None):
    return True

############################

def cmd_sleep(app, stage, extra, main_cmd):
    time.sleep(int(extra[0]))
    return

def cmd_gpg_cache(app, stage, extra, main_cmd):
    if os.environ.get("DIRECT_RELEASE_NO_GPG_AGENT", ""):
        return
    subprocess.call('echo | gpg --sign >/dev/null', shell=True)
    return

def cmd_zookeeper_info(app, stage, extra, main_cmd):
    if not state_storage_type == 'zk':
        return
    print "\nservers: %s\nnode: %s" % (zk_servers, state_location_zk)
    return

def cmd_show_state(app, stage, extra, main_cmd):
    print json.dumps(state, indent=4, sort_keys=True)
    return

def cmd_list_apps(app, stage, extra, main_cmd):
    for app in apps_conf['apps']:
        print " * %s" % app
    return

def cmd_finish(app, stage, extra, main_cmd):
    global state
    global inter_state_data
    inter_state_data['ticket'] = state['ticket']
    state['finished'] = 1
    return

def cmd_clear_sandbox_task(app, stage, extra, main_cmd):
    global state
    
    del state['current_build']['done']['create-sandbox-task']
    state['current_build']['sandbox_task'] = ''
    return

def read_waiting_state(location):
    if state_storage_type == 'fs':
        read_state_file(location)

    elif state_storage_type == 'zk':
        read_state_zk(location)


def read_waiting_state_from_location(location):
    if state_storage_type == 'fs':
        return read_state_from_file(location)

    elif state_storage_type == 'zk':
        return read_state_from_zk(location)


def read_waiting_state_str(location):
    if state_storage_type == 'fs':
        with open(location, 'r') as fh:
            return fh.read()

    elif state_storage_type == 'zk':
        new_state, _ = zookeeper.get(zkh, location)
        return new_state

def write_waiting_state_str(location, state_str):
    if state_storage_type == 'fs':
        with open(location, 'w') as fh:
            fh.write(state_str)

    elif state_storage_type == 'zk':
        zookeeper.set(zkh, location, state_str)

def create_waiting_state(location, content=""):
    if state_storage_type == 'fs':
        with open(location, 'w') as fd:
            fd.write(content)

    elif state_storage_type == 'zk':
        zookeeper.create(zkh, location, content, [{"perms":0x1f, "scheme":"world", "id" :"anyone"}])

def rm_waiting_state(location):
    if state_storage_type == 'fs':
        os.remove(location)

    elif state_storage_type == 'zk':
        zookeeper.delete(zkh, location)

def get_waiting_location(queue_location, number):
    return queue_location + '/%d' % number

def cmd_next_waiting(app, stage, extra, main_cmd):
    queue_location = get_queue_location(app)

    # берем лок на очередь
    with external_state_lock(custom_lock_location={'waiting': queue_location + '.lock'}):
        try:
            read_waiting_state(queue_location + '/1')
            state['finished'] = 0

            children = list_queue(queue_location)

            number = max(map(lambda x: int(x), children))

            for i in xrange(2, number + 1):
                new_state = read_waiting_state_str(get_waiting_location(queue_location, i))
                write_waiting_state_str(get_waiting_location(queue_location, i - 1), new_state)

            rm_waiting_state(get_waiting_location(queue_location, number))
        except:
            die("Unexpected error: %s / %s" % (sys.exc_info()[0], sys.exc_info()[1]))

    return


def cmd_return_to_queue(app, stage, extra, main_cmd):
    queue_location = get_queue_location(app)

    # берем лок на очередь
    with external_state_lock(custom_lock_location={'waiting': queue_location + '.lock'}):
        try:
            children = list_queue(queue_location)
            if children:
                number = max(map(lambda x: int(x), children))

                create_waiting_state(
                    get_waiting_location(queue_location, number + 1),
                    read_waiting_state_str(get_waiting_location(queue_location, number))
                )
                for i in xrange(number - 1, 0, -1):
                    new_state = read_waiting_state_str(get_waiting_location(queue_location, i))
                    write_waiting_state_str(get_waiting_location(queue_location, i + 1), new_state)
            else:
                create_waiting_state(get_waiting_location(queue_location, 1), "")

            state['finished'] = 0
            dump_state_to_queue(get_waiting_location(queue_location, 1))
        except:
            die("Unexpected error: %s / %s" % (sys.exc_info()[0], sys.exc_info()[1]))

    return

def list_queue(queue_location):
    if state_storage_type == 'fs':
        return os.listdir(queue_location)

    elif state_storage_type == 'zk':
        return zookeeper.get_children(zkh, queue_location)

def dump_state_to_queue(location):
    if state_storage_type == 'fs':
        write_state_file(location)
 
    elif state_storage_type == 'zk':
        write_state_zk(location)

        
def get_queue_location(app):
    if state_storage_type == 'fs':
        return os.environ['STATE_DIR'] + '/' + 'waiting' + '/' + app

    elif state_storage_type == 'zk':
        prefix = os.environ['STATE_ZK_NODE'] if 'STATE_ZK_NODE' in os.environ else '/direct/release-state'
        return prefix + '/' + 'waiting' + '/' + app

def cmd_testing_done_for_ticket(app, stage, extra, main_cmd):
    global state
    to_run = []
    #java-release.pl --test-passed --ticket <ticket> -do
    to_run += [ ext_program('write-tracker') ]
    to_run += [ '--test-passed' ]
    to_run += [ '--ticket', state['ticket'] ]
    to_run += [ '-do' ]
    
    my_system(to_run)


def cmd_push_to_queue(app, stage, extra, main_cmd):
    queue_location = get_queue_location(app)

    # берем лок на очередь
    with external_state_lock(custom_lock_location={'waiting': queue_location + '.lock'}):
        try:
            children = list_queue(queue_location)

            if not children:
                new_location = get_waiting_location(queue_location, 1)
            else:
                last_el = max(map(lambda x: int(x), children))

                cur_state_str = state_to_str({key: value for key, value in state.items() if key != "finished"})
                last_waiting_state_str = state_to_str({
                    key: value
                    for key, value in read_waiting_state_from_location(get_waiting_location(queue_location, last_el)).items()
                    if key != "finished"
                })
                if cur_state_str == last_waiting_state_str:
                    die("current state is already in queue")

                new_location = get_waiting_location(queue_location, last_el + 1)

            create_waiting_state(new_location, "")
            dump_state_to_queue(new_location)
        except:
            die("Unexpected error: %s / %s" % (sys.exc_info()[0], sys.exc_info()[1]))

    return

def cmd_what_to_do(app, stage, extra, main_cmd):
    priority = {
            'denied': {},
            }
    for t in ALLOWED_PRIORITY_TYPES:
        priority[t] = {}

    for c in cmds:
        if c == 'what-to-do':
            continue
        try:
            p = calc_priority(app, c, stage, verbose=False)
            #print("%s: %s" % (c, p))
            if p['type'] not in priority:
                priority[p['type']] = {}
            priority[p['type']][c] = p['priority']
        except: 
            # если проверка падает -- значит, нельзя
            pass

    for n in ('recommended', 'useful', 'possible'):
        group = priority[n].keys()
        group.sort( reverse=True, key = lambda c: priority[n][c] )
    
        print "%s: " % n
        for c in group:
            print "    %-20s %s" % (c, cmds[c]['description']);
        print ""

    return

def cmd_next(app, stage, extra, main_cmd):
    recommended_cmds = []
    for c in cmds:
        if c == 'next':
            continue
        try:
            if prerequisites_ok(c) and cmds[c]['priority'] >= 100:
                recommended_cmds += [c]
        except: 
            pass
    if len(recommended_cmds) <= 0:
        die("no recommended (high-priority) actions, stop")
    recommended_cmds.sort(reverse=True, key = lambda c: cmds[c]['priority'])
    next_cmd = recommended_cmds[0]
    logger.info("performing %s ..." % next_cmd)
    # возможность остановить, если вдруг делается не то, что хотелось
    time.sleep(3)
    
    steps = get_cmd_steps(next_cmd)
    for c in steps:
        with external_state():
            do_one_cmd(c, app, stage, extra)
    return


def cmd_open_release(app, stage, extra, main_cmd):
    global state
    global inter_state_data

    if main_cmd == 'testing-to-stable':
        extra.append(inter_state_data['ticket'])
    if len(extra) != 1:
        die("expecting exactly 1 parameter, %s given; stop" % ", ".join(extra))
    ticket = extra[0]
    output = my_tee([ ext_program('release-ticket-report'), ticket, '-j'])
    r = json.loads("".join(output))
    build_type = ''
    if re.match(r'1.([0-9]+)\.', r['version']):
        build_type = 'branch'
    elif re.match(r'1.([0-9]+)-', r['version']): 
        build_type = ''
    else:
        die("can't parse release status, stop. %s" % output[0])

    # не открываем "не свои" тикеты
    ticket_app = component2app(r['components'][0])
    if ticket_app != app:
        die("component %s doesn't match app %s, stop" % (r['components'][0], app))

    state = defaultdict(str, copy.deepcopy(empty_state))
    state['release_type'] = release_type
    state['ticket'] = ticket
    state['build_type'] = build_type
    state['current_build']['version'] = r['version']
    return


def current_head_revision():
    to_run = ext_program('svn-info') + ['svn+ssh://arcadia.yandex.ru/arc/trunk/arcadia/direct']
    output = my_qx(to_run)
    return re.search(r'Revision: (.+)', output, re.I).group(1)


def cmd_declare_release(app, stage, extra, main_cmd):
    global state
    global release_type
    if stage != 'testing':
        die("declaring new release allowed only for testing stage")
    state = defaultdict(str, copy.deepcopy(empty_state))
    if release_type == None:
        die('release_type cannot be None, stop')
    state['release_type'] = release_type

    # аналогично slide: можно указать ревизию
    # в create-release не хотим протаскивать, сценарий редкий
    if len(extra) == 0:
        # если ревизия не указана, получаем head ревизию из аркадии
        state['current_build']['head-rev'] = current_head_revision()
    elif len(extra) == 1: 
        #объявляем релиз с указанной ревизией
        state['current_build']['head-rev'] = extra[0]
    elif len(extra) > 1:
        # слишком много ревизий, ошибка
        die("too many parameters for slide")

    state['current_build']['done']['declare-release'] = 1
    return


def cmd_slide_plan(app, stage, extra, main_cmd):
    global state
    if 'build_type' in state and state['build_type'] == 'branch':
        die("can't slide after hotfix-merge")
    state["current_build"] = defaultdict(str, copy.deepcopy(empty_current_build))

    if len(extra) == 0:
        # если ревизия не указана, получаем head ревизию из аркадии
        state['current_build']['head-rev'] = current_head_revision()
    elif len(extra) == 1: 
        #slide с указанной ревизией
        state['current_build']['head-rev'] = extra[0]
    elif len(extra) > 1:
        # слишком много ревизий, ошибка
        die("too many parameters for slide")

    return


def cmd_ready_for_test(app, stage, extra, main_cmd):
    global state
    if not state['ticket']:
        die("expected non-empty ticket in state")

    if not os.environ.get("DIRECT_RELEASE_ALLOW_AUTO_RFT", ""):
        logger.info("no DIRECT_RELEASE_ALLOW_AUTO_RFT variable, doing nothing")
        state['current_build']['done']['ready-for-test'] = 1
        return

    to_run = []
    to_run += [ ext_program('change-issue-status-or-comment') ]
    to_run += ['--ticket', state['ticket']]
    to_run += ['--transition', 'ready_for_test']

    my_system( to_run )

    state['current_build']['done']['ready-for-test'] = 1

    return


def cmd_create_sandbox_task(app, stage, extra, main_cmd):
    global state

    to_run = []
    to_run += [ ext_program('create-sandbox-task') ]
    #to_run += ['--no-use-aapi-fuse']
    to_run += ['--verbose', '--no-wait']

    if state['release_type'] == 'java_yadeploy':
        to_run += ['--package']
        to_run += [ apps_conf['apps'][app]['package-json-yadeploy'] ]
        to_run += ['--yadeploy']
        to_run += ['--yadeploy-resource-type']
        to_run += [ apps_conf['apps'][app]['yadeploy-resource-type'] ] 
    else: 
        to_run += ['--package']
        to_run += [ apps_conf['apps'][app]['package-json'] ]

    if 'patch' in apps_conf['apps'][app]:
        to_run += [ '--patch', apps_conf['apps'][app]['patch'] ]
    if state['current_build']['type'] == 'branch':
        to_run += [ '--branch', state['current_build']['branch'] ]
        to_run += [ '--revision', str(state['current_build']['head-rev']) ]
        to_run += [ '--version', state['current_build']['version-to-be'] ]
        # FORGIVEME
        # steps собирается с патчем, и по умолчанию версия нестандартного вида
        # надеемся, что steps научится собираться без патчика, и тогда этот if можно удалить
        if app == 'steps':
            to_run += [ '--very-special-app-steps' ]
    elif 'head-rev' in state['current_build'] and state['current_build']['head-rev']:
        # create-release и slide с указанной ревизией
        to_run += [ '--revision', str(state['current_build']['head-rev']) ]
        # FORGIVEME
        # steps собирается с патчем, и по умолчанию версия нестандартного вида
        # надеемся, что steps научится собираться без патчика, и тогда этот if можно удалить
        if app == 'steps':
            to_run += [ '--version', '1.%s-1' % state['current_build']['head-rev']]
            to_run += [ '--very-special-app-steps' ]

    output = my_qx(to_run)

    task_search = re.search(r'^task_id: ([0-9]+)$', output, re.M)
    if not task_search:
        die("can't find task_id, stop")

    task_id = task_search.group(1)

    version_search = re.search(r'^package version: ([a-z~0-9\.-]+)$', output, re.M)
    if not version_search:
        die("can't find version, stop")

    version = version_search.group(1)

    state['current_build']['version'] = version
    state['current_build']['sandbox_task'] = task_id
    state['current_build']['done']['create-sandbox-task'] = 1

    return


def cmd_sandbox_release_task(app, stage, extra, main_cmd):
    global state
    # dt-sandbox-resource-release.py -t 750370752 -s stable
    to_run = []
    to_run += [ ext_program('sandbox-resource-release') ]
    to_run += ['--task']
    to_run += [ state['current_build']['sandbox_task'] ]
    # релиз ресурса в stable стоит делать роботом аналогично dmove stable
    to_run += ['--subject']
    to_run += ['testing']

    need_restore_SANDBOX_TOKEN_FILE = False
    if 'SANDBOX_TOKEN_FILE' in os.environ:
        old_SANDBOX_TOKEN_FILE = os.environ['SANDBOX_TOKEN_FILE']
        need_restore_SANDBOX_TOKEN_FILE = True
    os.environ['SANDBOX_TOKEN_FILE'] = '/etc/direct-tokens/sandbox_oauth_token_ppc'
    my_system( to_run )
    if need_restore_SANDBOX_TOKEN_FILE:
        os.environ['SANDBOX_TOKEN_FILE'] = old_SANDBOX_TOKEN_FILE
    else:
        del(os.environ['SANDBOX_TOKEN_FILE'])

    state['current_build']['done']['sandbox-release-task'] = 1

    return

def cmd_clear_sandbox_release(app, stage, extra, main_cmd):
    global state
    del state['current_build']['done']['sandbox-release-task']

    return

def cmd_sandbox_wait_release(app, stage, extra, main_cmd):
    global state
    # sandbox-ya-package.pl --verbose --timeout 2700 --task-id 750370752 -e RELEASED
    to_run = []
    to_run += [ ext_program('wait-packages') ]
    to_run += ['--verbose']
    to_run += ['--timeout', "600"]
    to_run += ['--task-id']
    to_run += [ state['current_build']['sandbox_task'] ]
    to_run += ['--expected-status']
    to_run += ['RELEASED']

    my_system( to_run )

    state['current_build']['done']['sandbox-wait-release'] = 1

    return

def cmd_master_connection(app, stage, extra, main_cmd):
    if os.environ.get("DIRECT_RELEASE_NO_MASTER_CONNECTION", ""):
        # для автоматических запусков мастер-соединения не требуются, пропускаем
        return
    if app == 'direct':
        # perl-Директ + Аркадия пока не стыкуются с мастер-соединениями, нужна отладка
        print "TEMPORARY: DOING NOTHING"
        return
    params = []
    params.append(["svn", [ext_program('create-master-connection'), "--svn"]])
    params.append(["arcadia", [ext_program('create-master-connection'), "--arcadia"]])
    params.append(["dist", [ext_program('create-master-connection'), "-H", "dupload.dist.yandex.ru"]])
    params.append(["ppctest-scripts-ts.ppc.yandex.ru", [ext_program('create-master-connection'), "-H", "ppctest-scripts-ts.ppc.yandex.ru"]])
    params.append(["ppctest-sandbox1-front.ppc.yandex.ru", [ext_program('create-master-connection'), "-H", "ppctest-sandbox1-front.ppc.yandex.ru", "-u", "updater"]])
    params.append(["ppcdev1.yandex.ru", [ext_program('create-master-connection'), "-H", "ppcdev1.yandex.ru", '-u', 'updater']])
    try:
        # дополнительное ограничение на то, как может вызываться ext_program('update-java-ts')
        java_hosts_to_update = subprocess.check_output([ext_program('update-java-ts'), app, '--list-hosts']).rstrip().split('\n')
        for h in java_hosts_to_update:
            params.append([h, [ext_program('create-master-connection'), "-H", h]])
    except subprocess.CalledProcessError:
        pass

    for name, cmd in params:
        norm_name = name.replace(".", "_").replace("-", "_").upper()
        dirpath = tempfile.mkdtemp(dir="/tmp/temp-ttl/ttl_2d")
        filename = "%s/%s" % (dirpath, norm_name)
        my_system(cmd + ["-f", filename])

        os.environ['SSH_MASTER_CONN_%s' % norm_name] = filename


def cmd_ensure_screen_tmux(app, stage, extra, main_cmd):
    if os.environ.get("DIRECT_RELEASE_NO_TMUX", ""):
        return
    if not (os.environ.get("TMUX", "") or os.environ.get("TERM").find("screen") != -1):
        die("you should run '%s' under screen or tmux" % main_cmd)
    logger.info("!!! You can safely disconnect from session only after completing commands create-master-connection and gpg-cache !!!")
    return


def cmd_wait_dist(app, stage, extra, main_cmd):
    global state
    ### проверять, что версия непустая!
    to_run = []
    to_run += [ ext_program('wait-dist') ]
    to_run += ['--timeout', '300']
    to_run += [ apps_conf['apps'][app]['package'] + '_' + state['current_build']['version'] ]

    my_system( to_run )

    state['current_build']['done']['wait-dist'] = 1

    return


def cmd_wait_packages(app, stage, extra, main_cmd):
    global state
    to_run = []
    to_run += [ ext_program('wait-packages') ]
    to_run += ['--verbose']
    to_run += ['--timeout', os.environ.get("DIRECT_RELEASE_SANDBOX_TASK_TIMEOUT", "2700")]
    to_run += ['--task-id']
    to_run += [ state['current_build']['sandbox_task'] ]

    if state['release_type'] == 'java_yadeploy':
        to_run += ['--resource-type-with-version']
        to_run += [ apps_conf['apps'][app]['yadeploy-resource-type'] ]

    output = my_tee( to_run )
    #ready version: 1.2885666-1
    pattern = r'^ready version: ([a-z~0-9\.-]+)$'
    versions = [m.group(1) for m in (re.search(pattern, str(l)) for l in output) if m]
    if len(versions) != 1:
        die("can't find version, stop")

    state['current_build']['version'] = versions[0]

    if 'packages' not in state['current_build']['done']:
        state['current_build']['done']['packages'] = 1

    return


def cmd_dmove(app, stage, extra, main_cmd):
    global state

    if len(extra) == 0:
        targets = ['testing']
    elif len(extra) > 1:
        die("too many parameters for dmove")
    else:
        if extra[0] == 'stable':
            targets = ['stable']
        elif extra[0] == 'testing':
            targets = ['testing']
        else:
            die("unknown target for dmove: %s, stop" % extra[0])

    for target in targets:
        dmove_param = []
        dmove_param += ['dmove_%s' % target]
        dmove_param += ['-r', 'direct-trusty']
        dmove_param += ['%s=%s' % (apps_conf['apps'][app]['package'], state['current_build']['version'])]
        if target == 'testing':
            dmove_param += ['--no-ssh']

        my_system([ext_program('dmove_%s' % target)] + dmove_param)

    state['current_build']['done']['dmove'] = 1

    return


def cmd_dmove_dependencies(app, stage, extra, main_cmd):
    global state

    if main_cmd != 'dmove-dependencies':
        delay = 5
        logger.info("Sleeping for %d seconds before checking dependencies..." % delay)
        time.sleep(delay)

    dependencies = cmd_to_dmove(app, stage, extra, main_cmd)

    to_run = [ext_program('dmove-dependencies')]
    proc = subprocess.Popen(to_run, stdin=subprocess.PIPE)
    proc.communicate(dependencies)

    if proc.returncode != 0:
        die("ERROR: %s exited with non-zero code %d" % (" ".join(to_run), proc.returncode))
    else:
        logger.info("Seems ok")

    state['current_build']['done']['dmove-dependencies'] = 1

    return


def cmd_to_dmove(app, stage, extra, main_cmd):
    global state

    if stage == 'testing':
        targets = [('unstable', 'testing')]
    elif stage == 'stable':
        targets = [('unstable', 'stable'), ('prestable', 'stable'), ('testing', 'stable')]
    elif re.match(r'^waiting-[0-9]+$', stage):
        targets = [('unstable', 'stable'), ('prestable', 'stable'), ('testing', 'stable')]
    else:
        die("to_dmove: don't know how to process stage '%s', stop" % stage)

    result = ""

    delimiter = ">>>>>>>>>>>>>"
    result += "\n%s\n" % delimiter

    packages = [ apps_conf['apps'][app]['package'] ]
    if app == 'direct':
        packages += [ 'yandex-direct-scripts-switchman', 'yandex-direct-python-deps', 'yandex-direct-soap' ]
    for target in targets:
        result += "# %s -> %s\n" % (target[0], target[1])
        for repo in ['direct-trusty', 'direct-common']:
            for package in packages:
                result += "### %s:\n" % repo
                to_run = []
                to_run += [ext_program('find-packages-to-dmove')]
                to_run += ['--repository', 'http://%s.dist.yandex.ru/%s' % (repo, repo)]
                to_run += ['--from', target[0]]
                to_run += ['--package', "%s=%s" % (package, (state['current_build']['version']) if 'version' in state['current_build'] else "")]

                output = my_qx(to_run)
                result += output

    result += delimiter

    logger.info(result)

    return result


# TODO direct-apps.conf.yaml
DEVTEST_DEV7_UPDATE_APPS = [
    'java-web',
]


def need_test_update(app, stage, main_cmd, is_dev):
    # временно, пока не узнаем у buhter@, как правильно выкладывать это приложение
    if app in [ 'java-b2yt' ]:
        return False

    if is_dev and app not in DEVTEST_DEV7_UPDATE_APPS:
        return False

    # при хотфиксе и слайде в стабильный релиз не пытаться обновить ТС
    if main_cmd == 'hotfix' or main_cmd == 'slide':
        if stage == 'stable' or re.match(r'^waiting-[0-9]+$', stage):
           logger.info("App won't be deployed - hotfix or slide to stable release")
           return False

    return True


def test_update(app, stage, app_env, main_cmd, wait=True):
    """
    Обновить тестовый стенд

    :param app: приложение, которое обновляем
    :param stage: testing/waiting-N/stable
    :param app_env: testing/devtest/dev7
    :param main_cmd: main_cmd
    :param wait: True, если надо дождаться выкладки
    """
    global state

    is_dev = app_env != 'testing'

    if not need_test_update(app, stage, main_cmd, is_dev):
       return

    if state['release_type'] == 'java_yadeploy':
        stages = apps_conf['apps'][app]['yadeploy-stages']
        if not app_env in stages:
            logger.info('No %s stage for app %s in direct-apps.conf.yaml' % (app_env, app))
            return

        to_run = [
            ext_program('update-yadeploy-ts'),
            '--stage', stages[app_env],
            '--sandbox-task', state['current_build']['sandbox_task'],
            '--commit',
            '-m', 'release %s : sb:%s ,  version %s' % (app, state['current_build']['sandbox_task'], state['current_build']['version']),
        ]

        if wait:
            to_run += [ '--wait' ]

        output = my_qx(to_run)

        deploy_ticket_match = re.search(r'^committed: ([\w\d-]+)$', output, re.M)
        if not deploy_ticket_match:
            die("can't find deploy_ticket, stop")

        deploy_ticket = deploy_ticket_match.group(1)

        state['current_build']['deploy_tickets'][app_env] = deploy_ticket
    elif app_env == 'testing':
        # direct-java-test-update logviewer 1.2882462-1
        to_run = [
            ext_program('update-java-ts'),
            app,
            state['current_build']['version'],
        ]
        my_system(to_run)
    else:
        # java-test-update devtest/dev7 java-web 1.2882462-1
        to_run = [
            ext_program('update-java-dev'),
            app_env,
            app,
            state['current_build']['version'],
        ]
        my_system(to_run)


def test_update_wait(app, stage, app_env, main_cmd):
    """Дождаться обновления тестового стенда"""
    global state

    if state['release_type'] != 'java_yadeploy':
        die('Error: cant wait for non-deploy stage update')

    is_dev = app_env != 'testing'

    if not need_test_update(app, stage, main_cmd, is_dev):
        return

    stages = apps_conf['apps'][app]['yadeploy-stages']
    if not app_env in stages:
        logger.info('No %s stage for app %s in direct-apps.conf.yaml' % (app_env, app))
        return

    deploy_tickets = state['current_build']['deploy_tickets']
    if not app_env in deploy_tickets:
        exit("No deploy_ticket in state for app %s stage %s" % (app, app_env))

    to_run = [
        ext_program('update-yadeploy-ts'),
        '--stage', stages[app_env],
        '--deploy-ticket', deploy_tickets[app_env],
        '--wait',
    ]

    my_system(to_run)


def cmd_test_update(app, stage, extra, main_cmd):
    global state

    wait = main_cmd == 'test-update'
    test_update(app, stage, 'testing', main_cmd, wait)
    state['current_build']['done']['test-update'] = 1


def cmd_devtest_update(app, stage, extra, main_cmd):
    global state

    wait = main_cmd == 'devtest-update'
    test_update(app, stage, 'devtest', main_cmd, wait)
    state['current_build']['done']['devtest-update'] = 1


def cmd_dev7_update(app, stage, extra, main_cmd):
    global state
    
    wait = main_cmd == 'dev7-update'
    test_update(app, stage, 'dev7', main_cmd, wait)
    state['current_build']['done']['dev7-update'] = 1


def cmd_test_update_wait(app, stage, extra, main_cmd):
    global state

    test_update_wait(app, stage, 'testing', main_cmd)
    state['current_build']['done']['test-update-wait'] = 1


def cmd_devtest_update_wait(app, stage, extra, main_cmd):
    global state

    test_update_wait(app, stage, 'devtest', main_cmd)
    state['current_build']['done']['devtest-update-wait'] = 1


def cmd_dev7_update_wait(app, stage, extra, main_cmd):
    global state

    test_update_wait(app, stage, 'dev7', main_cmd)
    state['current_build']['done']['dev7-update-wait'] = 1


def cmd_get_changelog(app, stage, extra, main_cmd):
    global state

    to_run = [
        ext_program('get-changelog'),
        app,
        state['current_build']['version'],
        '--commit'
    ]
    if state['ticket']:
        to_run += ['--ticket', state['ticket']]

    proc = my_popen(to_run, not_kill=True)
    svn_url = proc.stdout.readline().strip()
    proc.stdout.close()
    logger.info(svn_url)

    state['current_build']['changelog-url'] = svn_url
    state['current_build']['done']['get-changelog'] = 1

    return


def cmd_wait_changelog(app, stage, extra, main_cmd):
    global state

    to_run = [ext_program('wait-changelog'), state['current_build']['changelog-url']]
    my_tee(to_run)

    state['current_build']['done']['wait-changelog'] = 1


def cmd_track(app, stage, extra, main_cmd):
    global state
    dmove_param = ""

    to_run = []
    #java-release.pl -v 1.2777128-1 -n 'Переименование в KeywordsResearch, тюнинг GC' --app api5 -do
    #java-release.pl --app $app_name --version $version --ticket \$DIRECT_JAVA_RELEASE_TICKET
    to_run += [ ext_program('write-tracker') ]
    to_run += [ '-v', state['current_build']['version'] ]
    to_run += [ '--app', app ]
    to_run += ['--changelog-url', state['current_build']['changelog-url']]
    to_run += [ '--sandbox-task', state['current_build']['sandbox_task']]
    if state['ticket']:
        to_run += [ '--ticket', state['ticket'] ]

        # переоткрывать релизный тикет при хотфиксе или слайде в продакшен 
        # релиз из waiting-1 может быть закрыт, если был откат из production
        if stage == 'stable' or stage == 'waiting-1':
            to_run += [ '--reopen' ]
    else:
        today = datetime.datetime.today().strftime('%Y-%m-%d')
        default_release_name = "Сборка от %s" % today
        to_run += [ '-n', default_release_name ]
    # TODO просмотр и подтверждение?
    # TODO заранее давать имя
    to_run += [ '-do' ]

    output = my_tee( to_run )
    if not state['ticket']:
        # new ticket: $res
        pattern = r'^new ticket: ([a-zA-Z0-9-]+)$'
        tickets = [m.group(1) for m in (re.search(pattern, str(l)) for l in output) if m]
        if len(tickets) != 1:
            die("can't find tickets, stop")
        state['ticket'] = tickets[0]

    if state['current_build']['type'] == 'branch':
        state['build_type'] = 'branch'

    # возможно, вместо этого сбросить current_build
    state['current_build']['done']['track'] = 1

    return


def cmd_inspect_issue(app, stage, extra, main_cmd):
    global state

    title = 'app: %s, stage: %s' % (app, stage)
    if state['finished']:
        title += ', FINISHED'
    logger.info(title)
    if 'ticket' in state and state['ticket']:
        my_system([ ext_program('release-ticket-report'), state['ticket']])
    else:
        print '-'
    
    return


def cmd_rename(app, stage, extra, main_cmd):
    global state
    if len(extra) <= 0:
        die("expecting name for release %s, stop" % state['ticket'])
    name = extra[0]

    to_run = []
    #java-release.pl --rename --ticket <ticket> -n 'Новое название' -do
    to_run += [ ext_program('write-tracker') ]
    to_run += [ '--rename' ]
    to_run += [ '--ticket', state['ticket'] ]
    to_run += [ '-n', name ]
    to_run += [ '-do' ]
    
    my_system(to_run)
    
    return


def cmd_hotfix_plan(app, stage, extra, main_cmd):
    global state

    # TODO Почему не в prerequisites_check? 
    # Если подробное сообщение выводить из проверки, то было бы ok
    # тогда надо 2 режима работы проверок: молча для what-to-do и подробно для единичного вызова
    if stage == 'stable' and app != 'java-b2yt':    # для b2yt пока нет версии в продакшене, поэтому пропускаем
        # для продакшена-релиза проверяем, что релиз, в который добавляем хотфикс -- действительно тот, который сейчас выложен (на самом деле -- записан в ZK)
        base_revision_tracker = get_current_base_revision_from_tracker(state['ticket'])
        if state['release_type'] == 'java_yadeploy':
            logger.info('Getting app version from Ya.Deploy...')
            base_revision_yadeploy = get_current_base_revision_from_yadeploy(app)
            if base_revision_tracker != base_revision_yadeploy:
                die("version from tracker (base rev %s) doesn't match version from Ya.Deploy (base rev %s). Suggestion: open-release ..." % (base_revision_tracker, base_revision_yadeploy))
        else:
            base_revision_zk = get_current_base_revision_from_zk(app)
            # TODO искать и подсказывать, какой релиз открыть
            # TODO сделать force? (например, если откатываемся и нужен сначала хотфикс?)
            if base_revision_tracker != base_revision_zk:
                die("version from tracker (base rev %s) doesn't match version from ZK (base rev %s). Suggestion: open-release ..." % (base_revision_tracker, base_revision_zk))
    elif stage == 'testing':
        # для RC проверяем, что тикет еще не закрыт (=действительно еще тестируется)
        output = my_qx([ ext_program('release-ticket-report'), state['ticket'], '-j'])
        r = json.loads("".join(output))
        if r['status'] in ['closed', 'readyToDeploy' ]:
            die("Unexpected status '%s' for hotfix in stage 'testing', stop. Suggestion: move release to 'stable' stage (testing-to-stable)" % r['status'])

    state['current_build'] = copy.deepcopy(empty_current_build)
    # ??? можно не заменять, а добавлять новые ревизии к предыдущим
    if len(extra) <= 0: 
        die('expecting one or more revisions to hotfix, stop')
    if len(extra) == 1 and extra[0] == 'empty':
        # если явно попросили -- соглашаемся на пустой список. Надо например для переводов сразу в релизном бранче
        extra = []
    for rev in extra:
        if not re.match(r'^[0-9]+$', rev):
            die('bad revision format, expexting int: "%s"' % rev)

    state['current_build']['hotfix_revisions'] = extra
    state['current_build']['done']['hotfix-plan'] = 1
    state['current_build']['type'] = 'branch'

    return


def cmd_hotfix_merge(app, stage, extra, main_cmd):
    global state

    base_revision = get_current_base_revision_from_tracker(state['ticket'])
    
    # проверяем, что ревизии хотфиксов достаточно большие. Ловим случаи "подставили номер ревью вместо коммита"
    for r in state['current_build']['hotfix_revisions']:
        if not (int(r) > int(base_revision)):
            die('VERY suspicious: hotfix revision (%s) <= base release revision (%s), stop' % (r, base_revision))

    # arcadia-hotfix.pl api5 2505659 2546708 2546694 2546129 2545252
    to_run = []
    to_run += [ ext_program('hotfix-merge') ]
    to_run += [ app ]
    to_run += [ str(base_revision) ]
    to_run += [str(r) for r in state['current_build']['hotfix_revisions']]
    logger.info(to_run)

    output = my_tee( to_run )


    #print "ready branch\@rev: $release_branch\@$head_rev, version-to-be: $version\n";
    pattern = r'^ready branch@rev: (.*)@([0-9]+), version-to-be: ([a-z~0-9\.-]+)$'
    results = [m.groups() for m in (re.search(pattern, str(l)) for l in output) if m]

    if len(results) != 1:
        die("can't find branch, revision, version-to-be, stop")

    state['current_build']['branch'] = results[0][0]
    state['current_build']['head-rev'] = results[0][1]
    state['current_build']['version-to-be'] = results[0][2]
    state['current_build']['done']['hotfix-merge'] = 1

    return


def cmd_flag_set_perl(app, stage, extra, main_cmd):
    duration = 300
    if len(extra) > 0 :
        duration = extra[0]
    my_system([ ext_program('release-flag-perl'), '--ppcdev-all', 'direct-release-flag', '--set', str(duration)])
    return

def cmd_flag_drop_perl(app, stage, extra, main_cmd):
    my_system([ ext_program('release-flag-perl'), '--ppcdev-all', 'direct-release-flag', '--drop' ])
    return


def cmd_rename_perl(app, stage, extra, main_cmd):
    global state
    if len(extra) <= 0:
        die("expecting name for release %s, stop" % state['ticket'])
    name = extra[0]

    to_run = []
    #direct-release-tracker-perl.pl --ticket DIRECT-NNNNN --name 'Две мега-фичи' --rename -do
    to_run += [ ext_program('write-tracker-perl') ]
    to_run += [ '--rename' ]
    to_run += [ '--ticket', state['ticket'] ]
    to_run += [ '-n', name ]
    to_run += [ '-do' ]
    
    my_system(to_run)
    
    return


def cmd_build_packages_perl(app, stage, extra, main_cmd):
    global state
    to_run = []
    to_run += [ ext_program('build-perl-direct') ]
    to_run += ['--dupload']
    if state['current_build']['type'] == 'branch':
        to_run += ['--svn-url', state['current_build']['branch']]
        to_run += ['--rev', str(state['current_build']['head-rev'])]
    else:
        to_run += ['--svn-url', project_specific.svn_trunk_url()]
        if 'head-rev' in state['current_build'] and state['current_build']['head-rev']:
            # slide с указанной ревизией
            to_run += [ '--rev', str(state['current_build']['head-rev']) ]

    if os.environ.get('DIRECT_RELEASE_NO_GPG_AGENT',""):
        to_run += [ '--passphrase-file', os.environ['DIRECT_RELEASE_GPG_PASSPHRASE_FILE'] ]
        to_run += [ '--no-gpg-agent' ]
    logger.info("%s" % to_run) # !!!

    output = my_tee( to_run )
    pattern = r'source version *([^\n]*)'
    versions = [m.group(1) for m in (re.search(pattern, str(l)) for l in output) if m]
    if len(versions) != 1:
        die("can't find version, stop")

    state['current_build']['version'] = versions[0]

    if 'packages' not in state['current_build']['done']:
        state['current_build']['done']['packages'] = 1

    return

def cmd_test_update_perl(app, stage, extra, main_cmd):
    my_system([ ext_program('update-ts-perl'), 'test-all', state['current_build']['version'] ])
    return 

def cmd_release_beta_up_perl(app, stage, extra, main_cmd):
    if len(extra) > 0 :
        betas_to_update = extra
    else: 
        betas_to_update = [8080, 8999, 9090, 9091]
    for b in betas_to_update:
        logger.debug(b)
        to_run = []
        to_run += [ ext_program('release-beta-up') ]
        to_run += [ str(b) ]
        my_system(to_run)
    return

def cmd_hotfix_merge_complete_perl(app, stage, extra, main_cmd):
    #svn+ssh://svn.yandex.ru/direct/releases/release-164412 165000
    release_branch_re = re.escape(project_specific.svn_release_branch_url('PLACEHOLDER'))
    release_branch_re = release_branch_re.replace('PLACEHOLDER', '[0-9]+')
    release_branch_re = '^' + release_branch_re + '$'
    if not re.match(release_branch_re, extra[0]):
        die("unexpected svn branch '%s'" % extra[0])

    if not re.match(r'[0-9]+$', extra[1]):
        die("uexpected head revision '%s'" % extra[1])

    if my_qx([ext_program('get-last-changed-rev'), extra[0]]).strip() != extra[1]:
        die("not head revision provided '%s'" % extra[1])
    state['current_build']['branch'] = extra[0]
    state['current_build']['head-rev'] = extra[1]
    state['current_build']['done']['hotfix-merge'] = 1
    return

def cmd_hotfix_merge_perl(app, stage, extra, main_cmd):
    global state
    base_revision = get_current_base_revision_from_tracker(state['ticket'])

    # svn-hotfix --non-interactive --ignore-externals --tmp-dir /tmp/temp-ttl/ttl_7d svn+ssh://svn.yandex.ru/direct 160966 161184,161197,161215
    to_run = []
    to_run += [ ext_program('hotfix-merge-perl') ]
    to_run += [ '--non-interactive', '--ignore-externals', '--tmp-dir', '/tmp/temp-ttl/ttl_7d' ]
    to_run += [project_specific.svn_root_url()]
    to_run += [ str(base_revision) ]
    to_run += [",".join(str(r) for r in state['current_build']['hotfix_revisions'])]
    logger.info(to_run)

    output = my_tee( to_run )

    #print "ready branch\@rev: $RELEASE_URL\@$ready_rev"
    pattern = r'^ready branch@rev: (.*)@([0-9]+)$'
    results = [m.groups() for m in (re.search(pattern, str(l)) for l in output) if m]

    if len(results) != 1:
        die("can't find branch, revision, version-to-be, stop")

    state['current_build']['branch'] = results[0][0]
    state['current_build']['head-rev'] = results[0][1]
    state['current_build']['done']['hotfix-merge'] = 1

    return


def cmd_track_perl(app, stage, extra, main_cmd):
    global state
    dmove_param = ""

    to_run = []
    #java-release.pl -v 1.2777128-1 -n 'Переименование в KeywordsResearch, тюнинг GC' --app api5 -do
    #java-release.pl --app $app_name --version $version --ticket \$DIRECT_JAVA_RELEASE_TICKET
    # direct-release-tracker-perl.pl -version 1.161372-1 -do 
    to_run += [ ext_program('write-tracker-perl') ]
    to_run += [ '-v', state['current_build']['version'] ]
    if state['ticket']:
        to_run += [ '--ticket', state['ticket'] ]
        
        # переоткрывать релизный тикет при хотфиксе или слайде в продакшен 
        #if stage == 'stable' or stage == 'waiting-1':
        #    to_run += [ '--reopen' ]
    # TODO просмотр и подтверждение?
    # TODO заранее давать имя
    to_run += [ '-do' ]

    output = my_tee( to_run )
    if not state['ticket']:
        # new ticket: $res
        pattern = r'^new ticket: ([a-zA-Z0-9-]+)$'
        tickets = [m.group(1) for m in (re.search(pattern, str(l)) for l in output) if m]
        if len(tickets) != 1:
            die("can't find tickets, stop")
        state['ticket'] = tickets[0]

    if state['current_build']['type'] == 'branch':
        state['build_type'] = 'branch'

    # возможно, вместо этого сбросить current_build
    state['current_build']['done']['track'] = 1

    return


#Committed revision 172876.
def cmd_get_translations_from_tanker_frontend_perl( app, stage, extra, main_cmd ):
    # создать бранч:
    #svn-hotfix --non-interactive --ignore-externals --tmp-dir /tmp/temp-ttl/ttl_7d svn+ssh://arcadia.yandex.ru/arc 4973730
    # записать версию в тикет
    # ^^ оформить как отдельную команду "сделать релизный бранч"
    if state['build_type'] != 'branch':
        die("can't work without release branch")
    base_revision = get_current_base_revision_from_tracker(state['ticket'])
    to_run = [ext_program('translations-for-release-branch-perl'), "release-" + base_revision  ]
    logger.info(str(to_run))
    output = my_tee( to_run )
    # в выводе Committed revision 173449.
    pattern = r'^Committed revision ([0-9]+)\. *'
    results = [m.groups() for m in (re.search(pattern, str(l)) for l in output) if m]

    if len(results) != 1:
        die("can't find new revision, stop")

    state['current_build']['head-rev'] = results[0][0]
    return


def cmd_check_release(app, stage, extra, main_cmd):
    to_run = []
    to_run += [ ext_program('check-release'), state['ticket'] ]

    logger.info("#### Checking release consistency")
    #print " ".join(to_run)

    try:
        my_system(to_run)
    except:
        logger.info("FOUND ERRORS!")

    logger.info("#### Checking release dependencies")
    to_run = [ext_program('dt-deps-manager'), "-cr", state['ticket']]
    try:
        res = subprocess.check_output(to_run)
        logger.info("dependencies OK")
    except:
        logger.warn("UNSATISFIED DEPENDENCIES, run `%s` for details!" % (" ".join(to_run)))

    return


if __name__ == '__main__':
    run();

