#!/usr/bin/perl -w

# $Id$

=head1 NAME
       
    lm - выполнение операций с локальным mysql

=head1 DESCRIPTION

    Скрипт подключается к локальной базе по сокету
    /var/run/mysqld.NAME/mysqld.sock
    NAME - первый параметр команодной строки
    Пользователь подключения - всегда root, 
    пароль можно указать в файлe /etc/mysql/NAME-root

    Для lm может существовать отдельный конфиг /etc/mysql/NAME-lm.json
    Возможные ключи:
    root_password - пароль для root-а
    default_db - бд по-умолчанию

    После NAME могут идти названия команд для выполнения, если команд нет - 
    запускается mysql с удобным prompt-ом

    Опции:
    --help - справка
    --force - игнорировать предупреждения
    --complete - вывести список для дополнения команды в шелле
    -A - при запуске mysql передать -A (ускоряет запуск)
    --skip-slave-start - запускать/перезапускать mysql с опцией skip-slave-start
    --resolve-uuid - попытаться соотнести mysql uuid с именами реплик (для status-gtid)
    -x, --use-extra-port - подключиться к mysqld через extra-port

=head1 SYNOPSIS

    lm ppcdata1 status
    clus PPCDATA1,-ppcdata01b exec 'lm ppcdata1 status change-master XXX sleep 1 status'

=head1 COMMANDS

=cut

use strict;
use warnings;

use Getopt::Long;
use File::Slurp;
use Data::Dumper;
use POSIX qw/strftime/;
use Term::ANSIColor;
use Term::Prompt;
use JSON qw/to_json from_json/;

use DBI;

my %CMDS = (
    'status' => {
        func => \&cmd_status,
    },
    'status-gtid' => {
        func => \&cmd_status_gtid,
    },
    'status-json' => {
        func => \&cmd_status_json,
    },
    'change-master' => {
        func => \&cmd_change_master, 
        argc => 1,
    },
    'change-master-safe' => {
        func => \&cmd_change_master_safe, 
        argc => 2,
    },
    'slave-stop' => {
        func => \&cmd_slave_stop, 
    },
    'slave-start' => {
        func => \&cmd_slave_start, 
    },
    'slave-restart' => {
        func => \&cmd_slave_restart, 
    },
    'slave-start-until' => {
        func => \&cmd_slave_start_until, 
        argc => 1,
    },
    'slave-skip' => {
        func => \&cmd_slave_skip,
    },
    'slave-show-error' => {
        func => \&cmd_slave_show_error,
    },
    'slave-show-pos' => {
        func => \&cmd_slave_show_pos,
    },
    'sql' => {
        func => \&cmd_sql,
        argc => 1,
    },
    'mysql' => {
        func => \&cmd_mysql,
        argc => 1,
    },
    'server-stop' => {
        func => \&cmd_server_stop,
        argc => 0,
    },
    'server-start' => {
        func => \&cmd_server_start,
        argc => 0,
    },
    'server-restart' => {
        func => \&cmd_server_restart,
        argc => 0,
    },
    'dump' => {
        func => \&cmd_dump,
        argc => 0,
    },
    'killall' => {
        func => \&cmd_killall,
    },
    'dirty' => {
        func => \&cmd_dirty,
        argc => 1,
    },
    'dirty-status' => {
        func => \&cmd_dirty_status,
    },
    'innotop' => {
        func => \&cmd_innotop,
    },
    'sleep' => {
        func => \&cmd_sleep,
        argc => 1,
    },
    'myq' => {
        func => \&cmd_myq,
        argc => 1,
    },
    'fix-uuid' => {
        func => \&cmd_fix_uuid,
    },
    'uuid-decode' => {
        func => \&cmd_uuid_decode,
    },
    'installdb' => {
        func => \&cmd_installdb,
    },
    );

my ($FORCE, $COMPLETE, $NO_AUTO_REHASH, $SKIP_SLAVE_START, $RESOLVE_UUID, $EXTRA_PORT, $DRY_RUN);
GetOptions(
    help => \&usage_full,
    force => \$FORCE,
    complete => \$COMPLETE,
    'A' => \$NO_AUTO_REHASH,
    'skip-slave-start' => \$SKIP_SLAVE_START,
    'resolve-uuid' => \$RESOLVE_UUID,
    'use-extra-port' => \$EXTRA_PORT,
    'x' => \$EXTRA_PORT,
    'dry-run' => \$DRY_RUN,
    );

chomp(my $full_host = qx(hostname -f));
chomp(my $host = qx(hostname -s));

# QLOUD_DISCOVERY_INSTANCE=db-1.db.test.mysql56.bm.stable.qloud-d.yandex.net
if ($ENV{QLOUD_DISCOVERY_INSTANCE}) {
    $full_host = $ENV{QLOUD_DISCOVERY_INSTANCE};
    ($host) = join ".", ((split(/\./, "$full_host-n.o.n.e."))[0 .. 3]);
}

if ($COMPLETE) {
    complete();
    exit(0);
} elsif (!@ARGV) {
    usage();
}
my $dbname = shift @ARGV;
if ($dbname !~ /^[\w\.-]+$/) {
    die "Incorrect NAME: '$dbname'";
}

my %uuid_to_host;
fill_uuid_to_hostname($dbname);

# пароль умеем читать из файлв
my $dbpass = '';
my $dbpass_file = "/etc/mysql/$dbname-root";
if (-f $dbpass_file) {
    $dbpass = read_file($dbpass_file);
    $dbpass =~ s/\s+//g;
}
# дополнительный конфиг для lm
my $lm_conf = {};
my $lm_conf_file = "/etc/mysql/$dbname-lm.json";
if (-f $lm_conf_file) {
    $lm_conf = from_json(scalar read_file($lm_conf_file));
    $dbpass = $lm_conf->{root_password} if exists $lm_conf->{root_password};
}

my $mysqld_conf_file = "/etc/mysql/$dbname.cnf";
my $socket = mysql_socket($dbname);
my $extra_port = get_mysqld_extra_port();

if (($EXTRA_PORT) && not defined($extra_port)) {
   die "not found extra_port in $mysqld_conf_file. Exit";
}

my @protocol;
if ($EXTRA_PORT) {
   @protocol = (
      "--port=$extra_port",
      "--protocol=TCP",
      "--host=127.0.0.1"
   )
} else {
   @protocol = (
      "--protocol=SOCKET",
      "--socket=$socket",
   )
}

my @MYSQL_CONNECT_ARGS = (
    "--user=root"
    , ($dbpass ne '' ? ("--password=$dbpass") : ())
    );
push(@MYSQL_CONNECT_ARGS, @protocol);

if (!@ARGV) {
    my $default_db = dbh_default_db(db_connect());
    exec "mysql"
        , "--prompt=$host/$dbname:\\d> "
        , ($NO_AUTO_REHASH ? "-A" : ())
        , @MYSQL_CONNECT_ARGS
        , ($default_db ? $default_db : ());
    die $!;
}

while(my $cmd = shift @ARGV) {
    if (exists $CMDS{$cmd}) {
        my @argv;
        if ($CMDS{$cmd}->{argc}) {
            if (@ARGV < $CMDS{$cmd}->{argc}) {
                die "Command $cmd requires $CMDS{$cmd}->{argc} params";
            }
            @argv = splice @ARGV, 0, $CMDS{$cmd}->{argc};
        }
        $CMDS{$cmd}->{func}->(@argv);
    } else {
        die "Command $cmd not exists";
    }
}

=head2 status

    вывести master/slave статусы

=cut
sub cmd_status {
    my $dbh = db_connect();
    my $vars = dbh_vars($dbh);
    my $status = dbh_status($dbh);

    my $proc_list = dbh_processlist($dbh);
    my %stat;
    for my $p (@$proc_list) {
        $stat{$p->{Command}}++;
    }
    my $proc_str = join ", ", map {"$_:$stat{$_}"} sort keys %stat;
    $proc_str ||= "no connections";

    my $master_status = $dbh->selectrow_hashref("SHOW MASTER STATUS");
    my $master_str = master_status_str($master_status, $vars);
    my $master_add_str = is_gtid_on($vars) ? "gtid_mode: ON" : "";

    my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    my $slave_str = slave_status_str($slave_status);
    my @slave_vars = qw/Slave_IO_Running Slave_SQL_Running Seconds_Behind_Master/;
    push(@slave_vars, 'Auto_Position') if is_gtid_on($vars);
    my $slave_add_str = defined $slave_status
        ? join(":", 
               map {defined $_ ? $_ : 'NULL'}
               map {$slave_status->{$_}} @slave_vars
              )
        : '';

    my $vars_str = join ", ",
                "sql_log_bin=".(lc($vars->{sql_log_bin}) eq 'on' ? $vars->{sql_log_bin} : colored(['red'], $vars->{sql_log_bin})),
                "server_id=$vars->{server_id}";
    $vars_str .= ", uuid=$vars->{server_uuid}" if is_gtid_on($vars);
    
    print colored(["green"], "$host/$dbname").", ".strftime("%Y-%m-%d %H:%M:%S", localtime).", $vars_str: $proc_str\n";
    print "    ".sprintf(colored(["red"],"Master").": %-60s    %s", $master_str, $master_add_str)."\n";
    print "    ".sprintf(colored(["blue"],"Slave").":  %-60s    %s", $slave_str, $slave_add_str)."\n";
    if ($vars->{wsrep_cluster_name}) {
        if ($status->{wsrep_cluster_size}) {
            my $cluster_status = "$vars->{wsrep_cluster_name}($status->{wsrep_cluster_status}, $status->{wsrep_cluster_size} hosts)";
            my $node_status = "node: $status->{wsrep_ready}/$status->{wsrep_local_state_comment}/w=$vars->{wsrep_provider_options}->{'pc.weight'}?";
            my $queue_status = "queue send/recv: $status->{wsrep_local_send_queue}($status->{wsrep_local_send_queue_avg})/$status->{wsrep_local_recv_queue}($status->{wsrep_local_recv_queue_avg})";
            my $pxc_status = "$cluster_status, $node_status, $queue_status";
            print "    ".colored(["blue"],"PXC").": $pxc_status\n";
        } else {
            print "    ".colored(["blue"],"PXC").": disabled\n";
        }
    }
}

=head2 status-json

    вывести master/slave статусы в формате json

=cut
sub cmd_status_json {
    my $dbh = db_connect();

    my $vars = dbh_vars($dbh);
    my $status = dbh_status($dbh);

    my $proc_list = dbh_processlist($dbh);
    my $proc_num = grep {$_->{User} !~ /^(?:system user|root)$/ && $_->{Command} !~ '^Binlog Dump'} @$proc_list;

    my $master_status = $dbh->selectrow_hashref("SHOW MASTER STATUS");
    $master_status->{str} = master_status_str($master_status, $vars) if $master_status;
    $master_status->{str_auto} = master_status_str($master_status, $vars, 1) if $master_status;

    my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    $slave_status->{str} = slave_status_str($slave_status) if $slave_status;

    my $pxc;
    if ($vars->{wsrep_cluster_name}) {
        $pxc = {
            cluster_name => $vars->{wsrep_cluster_name},
            cluster_status => $status->{wsrep_cluster_status},
            cluster_size => $status->{wsrep_cluster_size},
            ready => $status->{wsrep_ready},
            local_state_comment => $status->{wsrep_local_state_comment},
            queue_stats => { map { ($_ => $status->{"wsrep_$_"}) } qw/local_send_queue local_send_queue_avg local_recv_queue local_recv_queue_avg/ },
        };
    }

    my $status_json = {
        host => $full_host,
        port => $vars->{port},
        proc_num => $proc_num,
        vars => {
            map {$_ => $vars->{$_}} qw/server_id sql_log_bin gtid_mode/
        },
        pxc => $pxc,
        master_status => $master_status,
        slave_status => $slave_status,
        };
    print to_json($status_json), "\n";
}

=head2 status-gtid

    вывести информацию по gtid транзакциям

=cut
sub cmd_status_gtid {
    cmd_status();

    my $dbh = db_connect();
    my $vars = dbh_vars($dbh, 1);
    if (not is_gtid_on($vars)) {
        return;
    }

    my $master_status = $dbh->selectrow_hashref("SHOW MASTER STATUS");
    my $master_str = master_status_str($master_status, $vars);
    $master_str =~ s/:[^:]+:[^:]+$/:auto:auto/;

    print "\n";
    print "    ".sprintf(colored(["blue"],"GTID Master").": %-60s", $master_str)."\n";
    for my $var (qw(gtid_executed gtid_purged)) {
        my $data = $vars->{$var} // '-';
        $data =~ s/\n//g;
        if ($RESOLVE_UUID) {
            map { $data =~ s/$_/$uuid_to_host{$_}/g } keys %uuid_to_host;
        }
        printf("    %20s: %s\n", $var, $data);
    }

    print colored(["blue"], "    GTID Slave:\n");
    my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    for my $var (qw(Retrieved_Gtid_Set Executed_Gtid_Set)) {
        my $data = $slave_status->{$var} // '-';
        $data =~ s/\n//g;
        if ($RESOLVE_UUID) {
            map { $data =~ s/$_/$uuid_to_host{$_}/g } keys %uuid_to_host;
        }
        printf("    %20s: %s\n", $var, $data);
    }
}

=head2 sql

    выполнить запрос sql
    принимает один параметр - строку запроса
    выводит результат выполнения запроса в формате tab-separated

=cut
sub cmd_sql {
    my ($sql) = @_;

    my $dbh = db_connect();

    # выбираем умолчательную БД
    my $default_db = dbh_default_db($dbh);
    $dbh->do("use ".$dbh->quote_identifier($default_db)) if $default_db;

    for my $row (@{$dbh->selectall_arrayref($sql)||[]}) {
        print join("\t", map {defined $_ ? $_ : 'NULL'} @$row)."\n";
    }
}

=head2 mysql

    выполнить запросы, используя стандартный клиент mysql
    принимает один параметр - строку запроса

=cut
sub cmd_mysql {
    my ($sql) = @_;

    my $dbh = db_connect();

    my $default_db = dbh_default_db(db_connect());
    exec "mysql", @MYSQL_CONNECT_ARGS
        , ($default_db ? $default_db : ())
        , "-e" => $sql;
    die "Can't exec mysql: $!";
}

=head2 server-stop

    остановить сервер mysqld (выполнить /etc/init.d/mysql.instance stop)
    - в интерактивном режиме переспрашивает, уверены ли вы
    - в неинтерактивном работает, только если указать --force

=cut
sub cmd_server_stop {
    if (!$FORCE) {
        if (-t *STDOUT) {
            exit 1 unless prompt("y", "Are you sure you want to STOP mysqld?", "", 0);
        } else {
            print STDERR "Use --force to STOP mysqld in non-interactive mode\n";
            exit 1;
        }
    }
    system("/etc/init.d/mysql.$dbname", "stop");
}


=head2 server-start

    запустить сервер mysqld (выполнить /etc/init.d/mysql.instance start)
    - в интерактивном режиме переспрашивает, уверены ли вы
    - в неинтерактивном работает, только если указать --force

=cut
sub cmd_server_start {
    if (!$FORCE) {
        if (-t *STDOUT) {
            exit 1 unless prompt("y", "Are you sure you want to START mysqld?", "", 0);
        } else {
            print STDERR "Use --force to START mysqld in non-interactive mode\n";
            exit 1;
        }
    }
    if ($SKIP_SLAVE_START) {
        system(q(perl -i -lpe 's/^(.*\[mysqld\].*)$/$1\nskip-slave-start/' ) . $mysqld_conf_file);
    }
    system("/etc/init.d/mysql.$dbname", "start");
    if ($SKIP_SLAVE_START) {
        system(q(perl -i -lne 'print if ! (/^skip-slave-start$/ && $ms && ($. == $ms + 1)); if ( ! $ms && /^(.*\[mysqld\].*)$/) { $ms = $. }' ) . $mysqld_conf_file);
    }
}


=head2 server-restart

    запустить сервер mysqld (выполнить /etc/init.d/mysql.instance restart)
    - в интерактивном режиме переспрашивает, уверены ли вы
    - в неинтерактивном работает, только если указать --force

=cut
sub cmd_server_restart {
    if (!$FORCE) {
        if (-t *STDOUT) {
            exit 1 unless prompt("y", "Are you sure you want to RESTART mysqld?", "", 0);
        } else {
            print STDERR "Use --force to RESTART mysqld in non-interactive mode\n";
            exit 1;
        }
    }
    if ($SKIP_SLAVE_START) {
        system(q(perl -i -lpe 's/^(.*\[mysqld\].*)$/$1\nskip-slave-start/' ) . $mysqld_conf_file);
    }
    system("/etc/init.d/mysql.$dbname", "restart");
    if ($SKIP_SLAVE_START) {
        system(q(perl -i -lne 'print if ! (/^skip-slave-start$/ && $ms && ($. == $ms + 1)); if ( ! $ms && /^(.*\[mysqld\].*)$/) { $ms = $. }' ) . $mysqld_conf_file);
    }
}


=head2 dump

    запустить mysqldump для выбранного инстанса:
     - дампятся все базы данны, кроме системных
     - в начало дампа дописывается отключение бинлогов и остановка репликации
     - указываются флаги --quick --routines --master-data=1 --single-transaction

    дамп выводится в stdout

=cut
sub cmd_dump {
    my $dbh = db_connect();
    my $vars = dbh_vars($dbh);
    my @databases = grep {!/^(?:mysql|information_schema|performance_schema)$/i}
                    @{$dbh->selectcol_arrayref("SHOW DATABASES")||[]};
    die "No non-system databases" if !@databases;
    print "#####################\n";
    print "# lm dump for ".join(', ', @databases)."\n";
    print "SET SQL_LOG_BIN=0;\n";
    print "STOP SLAVE;\n";
    print "CHANGE MASTER TO MASTER_HOST='$full_host', MASTER_PORT=$vars->{port};\n";
    print "#####################\n";
    print "\n";
    exec("mysqldump",
         @MYSQL_CONNECT_ARGS,
         "--master-data=1", "--single-transaction",
         "--quick", "--routines",
         "--database", @databases,
    );
    die "Can't exec mysqldump: $!";
}

=head2 slave-stop

    остановить репликацию

=cut
sub cmd_slave_stop {
    dbh_do(db_connect(), "STOP SLAVE");
}

=head2 slave-start

    запустить репликацию

=cut
sub cmd_slave_start {
    dbh_do(db_connect(), "START SLAVE");
}

=head2 slave-restart

    перезапустить репликацию

=cut
sub cmd_slave_restart {
    my $dbh = db_connect();
    dbh_do($dbh, "STOP SLAVE");
    dbh_do($dbh, "START SLAVE");
}

=head2 slave-start-until

    запустить репликацию до определённой позиции.
    позиция задаётся в виде имя_лога:позиция

=cut
sub cmd_slave_start_until {
    my ($param) = @_;
    if (!defined $param || $param !~ /^([\w\.\d\-]+):(\d+)$/) {
        die "Usage: lm NAME slave-start-until logfile:logpos";
    }
    my $dbh = db_connect();
    if (is_gtid_on(dbh_vars($dbh)) && !$FORCE) {
        die "Use --force option for binlog-based 'start slave until' on GTID enabled instance";
    }
    dbh_do($dbh, "START SLAVE UNTIL MASTER_LOG_FILE = '$1', MASTER_LOG_POS = $2");
}

=head2 slave-skip

    пропустить одну команду при выполнении бинлога

=cut
sub cmd_slave_skip {
    my $dbh = db_connect();
    if (! is_gtid_on(dbh_vars($dbh))) {
        dbh_do($dbh,
               "STOP SLAVE",
               "SET GLOBAL sql_slave_skip_counter = 1",
               "START SLAVE");
    } else {
        my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
        my $exec_set = $slave_status->{Executed_Gtid_Set};
        $exec_set =~ s/\n|\s+//gs;

        (my $last_trx_num) = ($exec_set =~ /$slave_status->{Master_UUID}:[\d:-]*?\d+-(\d+)(?=,|$)/);
        die "Can't find last executed trx num for $slave_status->{Master_UUID} in $exec_set" if ! $last_trx_num;

        $last_trx_num++;
        my $skip_trx = "$slave_status->{Master_UUID}:$last_trx_num";
        if (!$FORCE) {
            die "Use --force option to skip $skip_trx\n";
        }

        dbh_do($dbh,
               "STOP SLAVE",
               "SET GTID_NEXT='$skip_trx'",
               "BEGIN",
               "COMMIT",
               "SET GTID_NEXT='AUTOMATIC'",
               "START SLAVE");
    }
}

=head2 slave-show-error

    показать ошибки IO/SQL тредов реплики

=cut
sub cmd_slave_show_error {
    my $dbh = db_connect();
    my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    map { print "$_: $slave_status->{$_}\n" } qw(Last_SQL_Error Last_SQL_Error_Timestamp Last_IO_Error Last_IO_Error_Timestamp); 
}

=head2 slave-show-pos

    показать позиции bin/relay логов для IO/SQL тредов

=cut
sub cmd_slave_show_pos {
    my $dbh = db_connect();
    my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    my $master_status = $dbh->selectrow_hashref("SHOW MASTER STATUS");

    print "I/O thread has read up to: master_binlog:pos --> relay logs total size\n";
    printf "    %s --> %s bytes\n",
        "$slave_status->{Master_Log_File}:$slave_status->{Read_Master_Log_Pos}",
        "$slave_status->{Relay_Log_Space}";
    print "SQL thread has read and executed up to: master_binlog:pos --> relay_log:pos --> binlog:pos\n";
    printf "    %s --> %s --> %s\n",
        "$slave_status->{Relay_Master_Log_File}:$slave_status->{Exec_Master_Log_Pos}",
        "$slave_status->{Relay_Log_File}:$slave_status->{Relay_Log_Pos}",
        "$master_status->{File}:$master_status->{Position}";
        
}

=head2 change-master

    перенастроить реплику на другого мастера

    принимает один параметр вида
      server.ru:3306:logbin-00000384:234234[:repl_user:repl_pass]
    или, для gtid master_auto_position
      server.ru:3306:auto:auto[:repl_user:repl_pass]

=cut
sub cmd_change_master {
    my $master_pos = get_valid_master_pos(@_);

    my $pos = $master_pos->[0];
    if ($pos->{master_host} eq $host || $pos->{master_host} eq $full_host) {
        die "Incorrect use of change-master: attempt to connect to same host";
    }
    my $dbh = db_connect();

    # проверка, не догоняет и слейв сейчас
    if (!$FORCE && (my $slave = $dbh->selectrow_hashref("SHOW SLAVE STATUS"))) {
        if ($slave->{Slave_SQL_Running} eq 'Yes' && $slave->{Read_Master_Log_Pos} != $slave->{Exec_Master_Log_Pos}) {
            die "Please, Wait while slave catch up master (or try --force option)";
        }
    }
    my $change_cmd = get_change_master_cmd($pos, is_gtid_on(dbh_vars($dbh)));

    dbh_do($dbh, "STOP SLAVE", $change_cmd);
    dbh_do($dbh, "START SLAVE") if ! $SKIP_SLAVE_START;
}

=head2 change-master-safe

    перенастроить реплику на другого мастера(второй параметр), причём только в том случае,
    если текущее состояние совпадает с первым параметром

    принимает два параметра вида
      server.ru:3306:logbin-00000384:234234

=cut
sub cmd_change_master_safe {
    my $master_pos = get_valid_master_pos(@_);
    my $dbh = db_connect();

    my ($pos_old, $pos_new) = @{$master_pos};
    if ($pos_new->{master_host} eq $host || $pos_new->{master_host} eq $full_host) {
        die "Incorrect use of change-master: attempt to connect to same host";
    }

    # проверка, не отстаёт ли слейв сейчас
    my $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    my $slave_status_str = slave_status_str($slave_status);
    if ($slave_status_str ne $pos_old->{joined}) {
        die "Old slave status is not valid: $slave_status_str";
    }

    dbh_do($dbh, "STOP SLAVE");

    # ещё одна проверка
    $slave_status = $dbh->selectrow_hashref("SHOW SLAVE STATUS");
    $slave_status_str = slave_status_str($slave_status);
    if ($slave_status_str ne $pos_old->{joined}) {
        die "Old slave status is not valid: $slave_status_str";
    }

    my $change_cmd = get_change_master_cmd($pos_new, is_gtid_on(dbh_vars($dbh)));
    dbh_do($dbh, $change_cmd);
    dbh_do($dbh, "START SLAVE") if ! $SKIP_SLAVE_START;
}

=head2 killall

    убить все сессии, кроме системных

=cut
sub cmd_killall {
    my $dbh = db_connect();
    my $proc_list = dbh_processlist($dbh);
    local $dbh->{RaiseError} = undef;
    local $dbh->{PrintError} = undef;
    for my $id (map {$_->{Id}} @$proc_list) {
        if (!$dbh->do("kill $id")) {
            if ($dbh->err == 1094) {
                # коннект уже отвалился самостоятельно
            } else {
                die "Can't kill $id: ".$dbh->err." ".$dbh->errstr;
            }
        }
    }
}

=head2 dirty

    установить innodb_max_dirt_pages_pct, принимает один параметр - целое число
    от 0 до 100 или строку "default"(в этом случае пытаемся получить
    настройку из конфига или ставим 90)

=cut
sub cmd_dirty {
    my ($proc) = @_;
    if (!defined $proc || $proc !~ /^(\d{1,2}|100|default)$/) {
        die "Usage: lm NAME dirty (0-100|defaut)";
    }
    if ($proc eq 'default') {
        my $config_file = $mysqld_conf_file;
        if (-f $config_file && scalar(read_file($config_file)) =~ /innodb_max_dirty_pages_pct\s*=\s*(\d+)/) {
            $proc = $1;
        }
    }
    dbh_do(db_connect(), "set global innodb_max_dirty_pages_pct=$proc");
}

=head2 dirty-status

    раз в секунду выводить Innodb_buffer_pool_pages_dirty

=cut
sub cmd_dirty_status {
    my $dbh = db_connect();
    while(1) {
        my (undef, $val) = $dbh->selectrow_array("show status like ?", {}, "Innodb_buffer_pool_pages_dirty");
        print "Innodb_buffer_pool_pages_dirty $val\n";
        sleep 1;
    }
}

=head2 innotop

    запустить innotop

=cut
sub cmd_innotop {
    my $filename = "/tmp/.lm-$dbname-innotop-$<";
    umask 0077;
    # определяем версию innotop
    my ($ver) = scalar(`innotop --version`) =~ /Ver ([\d\.]+)/;

    my $protocol;

    if ($EXTRA_PORT) {
        $protocol = "DBI:mysql:;port=$extra_port;host=127.0.0.1";
    } else {
        $protocol = "DBI:mysql:;mysql_socket=/var/run/mysqld.$dbname/mysqld.sock";
    }

    my $conf = {
        version => $ver || '1.6.0',
        general => {
            interval => 1,
            mode => 'Q',
        },
        active_filters => {
            processlist => 'hide_self hide_inactive',
        },
        connections => {
            $dbname => "user=root pass=$dbpass dsn=$protocol savepass=1 dl_table=",
        },
    };
    my @lines;
    for my $key (sort keys %$conf) {
        if (ref $conf->{$key}) {
            my $sect = $conf->{$key};
            push @lines, "[$key]";
            for my $subkey (sort keys %$sect) {
                push @lines, "$subkey=$sect->{$subkey}";
            }
            push @lines, "[/$key]";
        } else {
            push @lines, "$key=$conf->{$key}";
        }
    }
    write_file($filename, map {"$_\n"} @lines);
    exec("innotop", "--config=$filename");
    die "Can't start innotop: $!";
}

=head2 myq
    
    запустить myq_status

=cut
sub cmd_myq {
    my ($arg) = @_;
    if ($arg !~ /^\w+$/) {
        die "Usage: myq help";
    }
    exec("myq_status", "--socket=$socket", "$arg");
    die "Can't start myq_status: $!";
}

=head2 sleep

    пауза на указанное число секунд
    принимает один параметр

=cut
sub cmd_sleep {
    my ($timeout) = @_;
    if ($timeout !~ /^\d+$/) {
        die "Usage: sleep NUM";
    }
    sleep $timeout;
}

=head2 fix-uuid

    генерирует gtid uuid из hostname и прописывает в auto.cnf

=cut
sub cmd_fix_uuid {
    my $uuid_file = get_mysqld_datadir() . '/auto.cnf';

    my $old_uuid = '';
    if (-f $uuid_file && scalar(read_file($uuid_file)) =~ /\bserver-uuid\s*=\s*(\S+)/) {
        $old_uuid = $1;
    }

    my $new_uuid = str_to_uuid($full_host, $dbname);
    print "old uuid: $old_uuid\nnew uuid: $new_uuid\n";
    if ($old_uuid eq $new_uuid || $DRY_RUN) {
        print "Nothing to do\n";
        exit 0;
    }

    if (!$FORCE) {
        if (-t *STDOUT) {
            exit 1 unless prompt("y", "Are you sure you want to fix $uuid_file?", "", 0);
        } else {
            print STDERR "Use --force to fix $uuid_file\n";
            exit 1;
        }
    }
    write_file($uuid_file, "[auto]\nserver-uuid=$new_uuid\n");
    print "$uuid_file fixed\n";
}


=head2 uuid-decode

    определяет метод, по которому сгеренирован gtid uuid: 
    LM_UUID_OLD -- только по hostname, LM_UUID_NEW -- по hostname и dbname

=cut
sub cmd_uuid_decode {
    my $uuid_file = get_mysqld_datadir() . '/auto.cnf';

    my $uuid = '';
    if (-f $uuid_file && scalar(read_file($uuid_file)) =~ /\bserver-uuid\s*=\s*(\S+)/) {
        $uuid = $1;
    }

    my %possible_uuid = ();
    for my $f ( qw/LM_UUID_OLD LM_UUID_NEW/ ){
        local $ENV{$f} = 1; 
        $possible_uuid{$f} = str_to_uuid($full_host, $dbname);
    }

    my @format = map { $possible_uuid{$_} eq $uuid ? $_ : () } keys %possible_uuid;
    @format = ('unknown') unless scalar @format;

    print join(", ", @format)."\n";
}


=head2 installdb

    инициализирует пустую базу правильной командой
    в зависимости от версии mysqld

=cut
sub cmd_installdb {
    my $ver = get_mysqld_ver();
    my $cmd = ''; 
    if ($ver =~ /^5\.6/ || $ver =~ /^5\.5/) {
        $cmd = qq(mysql_install_db --defaults-file=$mysqld_conf_file --skip-name-resolve);
    } elsif ($ver =~ /^5\.7/) {
        $cmd = qq(mysqld --defaults-file=$mysqld_conf_file --initialize-insecure --user=mysql);
    }
    print "mysql installdb cmd: $cmd && lm $dbname fix-uuid --force\n";

    if ($FORCE) {
        my $datadir = get_mysqld_datadir();
        if (is_dir_empty($datadir)) {
            system($cmd) == 0 or die "mysql installdb failed: $?\n";
            system("lm $dbname fix-uuid --force") == 0 or die "mysql installdb failed: $?\n";
        } else {
            print "$datadir is not empty\n";
            exit 1;
        }
    } else {
        print "Use --force to apply commands\n";
    }
}

sub is_dir_empty {
    my $dirname = shift;
    if (opendir(my $dh, $dirname)) {
        return scalar(grep { ! /^\.+$/ } readdir($dh)) == 0;
    } else {
        print "Can't open dir $dirname: $!";
        return '';
    }
}

sub mysql_socket {
    "/var/run/mysqld.$_[0]/mysqld.sock";
}

sub db_connect {
    if ($EXTRA_PORT) {
        return DBI->connect("DBI:mysql:;port=$extra_port;host=127.0.0.1;mysql_enable_utf8=1", "root", $dbpass, {RaiseError => 1});
    } else {
        return DBI->connect("DBI:mysql:;mysql_socket=/var/run/mysqld.$dbname/mysqld.sock;mysql_enable_utf8=1", "root", $dbpass, {RaiseError => 1});
    };
}

sub dbh_do {
    my ($dbh, @sql) = @_;
    for my $s (@sql) {
        print $s, "\n";
        $dbh->do($s);
    }
}

sub dbh_processlist {
    my ($dbh) = @_;
    my ($con_id) = $dbh->selectrow_array("select connection_id()");
    return [grep {$_->{User} ne 'system user' && $_->{Id} != $con_id } @{$dbh->selectall_arrayref("show processlist", {Slice => {}})}];
}

sub dbh_vars {
    my ($dbh, $global) = @_;
    my $vars = { map {$_->[0] => $_->[1]} @{$dbh->selectall_arrayref($global ? "SHOW GLOBAL VARIABLES" : "SHOW VARIABLES")} };
    if (exists $vars->{wsrep_provider_options}) {
        # show variables может обрезать длинные значения, поэтому получим значение wsrep_provider_options другим способом
        $vars->{wsrep_provider_options} = $dbh->selectrow_hashref('SELECT @@wsrep_provider_options')->{'@@wsrep_provider_options'};
        $vars->{wsrep_provider_options} = {
            map {/^(.*?)\s*=\s*(.*)/s ? ($1 => $2) : ($_ => '')}
            split /;\s*/, $vars->{wsrep_provider_options}
        };
    }
    return $vars;
}

sub dbh_status {
    my ($dbh) = @_;
    return { map {$_->[0] => $_->[1]} @{$dbh->selectall_arrayref("SHOW STATUS")} };
}

sub dbh_default_db {
    my ($dbh) = @_;

    return $lm_conf->{default_db} if exists $lm_conf->{default_db};

    my %db_order = (lc($dbname) => -1, mysql => 1, information_schema => 2, performance_schema => 3);
    my @databases = 
        sort {
            ($db_order{lc($a)}||0) <=> ($db_order{lc($b)}||0)
            ||
            $a cmp $b
        }
        grep {/^\w+$/}
        @{$dbh->selectcol_arrayref("show databases")};
    return @databases ? $databases[0] : undef;
}

sub get_change_master_cmd {
    my ($pos, $gtid) = @_;
    my $change_cmd = "CHANGE MASTER TO master_host='$pos->{master_host}', master_port=$pos->{master_port}";

    if ($pos->{master_log_pos} ne 'auto') {
        $change_cmd .= ", master_log_file='$pos->{master_log_file}', master_log_pos=$pos->{master_log_pos}";
    }
    elsif ($gtid) {
        $change_cmd .= ", master_auto_position = " . ($pos->{master_log_pos} eq 'auto');
    }
    else {
        die "Incorrect use of change-master: can't use master_auto_position with gtid_mode = OFF";
    }

    if ($pos->{master_user} && $pos->{master_password}) {
        $change_cmd .= ", master_user = '$pos->{master_user}', master_password = '$pos->{master_password}'";
    }

    return $change_cmd;
}


sub get_valid_master_pos {
    my @master_pos_list;
    for my $pos (@_) {
        my @master_pos = ($pos =~ /^([\w\.-]+):(\d+):([\w\.-]+):(\d+|auto)(?::([^:]+):([^:]+))?$/);
        if (scalar(@master_pos) != 6) {
            die "Incorrect master position: '$pos', " . join ' ', map { $_ // 'undef' } @master_pos;
        }
        $master_pos[2] = $master_pos[3] if $master_pos[3] eq 'auto';

        my %master_pos;
        my $i = 0;
        for my $key (qw(master_host master_port master_log_file master_log_pos master_user master_password)) {
           $master_pos{$key} = $master_pos[$i];
           $i++;
        }
        $master_pos{joined} = join ":", @master_pos[0..3];
        push(@master_pos_list, \%master_pos);
    }
    return \@master_pos_list;
}

sub slave_status_str {
    my $slave_status = shift;
    if (!defined $slave_status) {
        return "-";
    } else {
        return join ":", map {$slave_status->{$_}} qw/Master_Host Master_Port Relay_Master_Log_File Exec_Master_Log_Pos/;
    }
}

sub master_status_str {
    my ($master_status, $vars, $auto_pos) = @_;
    if (!defined $master_status) {
        return "-";
    } else {
        return "$full_host:$vars->{port}:$master_status->{File}:$master_status->{Position}" if ! $auto_pos;
        return "$full_host:$vars->{port}:auto:auto";
    }
}

sub is_gtid_on {
    my ($vars) = @_;
    return (exists($vars->{gtid_mode}) && lc($vars->{gtid_mode}) eq 'on');
}

sub get_mysqld_datadir {
    my $datadir = "/opt/mysql.$dbname";
    if (-f $mysqld_conf_file && scalar(read_file($mysqld_conf_file)) =~ /\bdatadir\s*=\s*(\S+)/) {
        $datadir = $1;
    }
    return $datadir;
}

sub get_mysqld_extra_port {
    if (-f $mysqld_conf_file && scalar(read_file($mysqld_conf_file)) =~ /\bextra_port\s*=\s*(\S+)/) {
        $extra_port = $1;
    }
    return $extra_port;
}

sub get_mysqld_ver {
    chomp(my $mysqld_ver = qx(mysqld -V 2>/dev/null));
    if ($mysqld_ver =~ /^mysqld\s+Ver\s+([\d.-]+)/) {
        $mysqld_ver = $1;
    } else {
        $mysqld_ver = 'undef';
    }
    return $mysqld_ver;
}

sub str_to_uuid {
    my ($hostname, $instance) = @_;
    $instance //= '';
    # Переменными окружения LM_UUID_OLD и LM_UUID_NEW можно заставить сгенерироваться старый или новый uuid,
    # Глобальный дефолт -- старый формат, за исключением некоторых инстансов, для которых -- новый
    if (!$ENV{LM_UUID_OLD} && ($ENV{LM_UUID_NEW} || $instance =~ /^(ppcdata18|ppcdata19|ppcdata21|ppcdata20)$/ )){
        # new
        chomp(my $md5_str = qx(echo "$hostname$instance" | md5sum));
        my @md5 = ($md5_str =~ /(.{8})(.{4})(.{4})(.{4})(.{12})/);
        return join("-", @md5);
    } else {
        chomp(my $md5_str = qx(echo "$hostname" | md5sum));
        my @md5 = ($md5_str =~ /(.{8})(.{4})(.{4})(.{4})(.{12})/);
        return join("-", @md5);
    }
}

sub fill_uuid_to_hostname {
    my ($instance) = @_;
    # в qloud все проще
    if ($ENV{QLOUD_DISCOVERY_COMPONENT}) {
        my @hosts = map { /^.*\s+(\S+)\.$/ && $1 } split /\n/, (qx(host -tSRV "\$QLOUD_DISCOVERY_COMPONENT"));
        map { $uuid_to_host{str_to_uuid($_)} = $_ } @hosts;
    } else {
        # подбираем имена реплик, чтобы не ходить в кондуктор и тд =(
        my ($prefix_full, $suffix) = ($full_host =~ /^([a-z0-9-]+)[0-9][a-z](\..*)$/);
        my ($prefix_full_double, $suffix_double) = ($full_host =~ /^([a-z0-9-]+)([0-9]*)[a-z](\..*)$/);
        return if !$prefix_full || !$suffix;
        my ($prefix) = ($prefix_full =~ /^([a-z]+)/);
        return if !$prefix;
    
        for (my $num_part = 0; $num_part < 10; $num_part++) {
            for my $dc (qw(e f g h i k m)) {
                my $fake_hostname = "${prefix_full}${num_part}${dc}${suffix}";
                $uuid_to_host{str_to_uuid($fake_hostname)} = $fake_hostname; 

                $fake_hostname = "${prefix_full}${num_part}${dc}${suffix}$instance";
                $uuid_to_host{str_to_uuid($fake_hostname)} = "${prefix_full}${num_part}${dc}${suffix}($instance)"; 
    
                $fake_hostname = "$dbname-0${num_part}${dc}${suffix}";
                $uuid_to_host{str_to_uuid($fake_hostname)} = $fake_hostname; 

                $fake_hostname = "$dbname-0${num_part}${dc}${suffix}$instance";
                $uuid_to_host{str_to_uuid($fake_hostname)} = "$dbname-0${num_part}${dc}${suffix}($instance)"; 
    
                $fake_hostname = "${prefix}-standby0${num_part}${dc}${suffix}";
                $uuid_to_host{str_to_uuid($fake_hostname)} = $fake_hostname; 

                $fake_hostname = "${prefix}-standby0${num_part}${dc}${suffix}$instance";
                $uuid_to_host{str_to_uuid($fake_hostname)} = "${prefix}-standby0${num_part}${dc}${suffix}($instance)"; 
    
                $fake_hostname = "ppcstandby0${num_part}${dc}${suffix}";
                $uuid_to_host{str_to_uuid($fake_hostname)} = $fake_hostname; 

                $fake_hostname = "ppcstandby0${num_part}${dc}${suffix}$instance";
                $uuid_to_host{str_to_uuid($fake_hostname)} = "ppcstandby0${num_part}${dc}${suffix}($instance)";  
            }
        }
        for my $dc (qw(e f g h i k m)) {
	    my $fake_hostname = "${prefix_full_double}${dc}${suffix}$instance";
            $uuid_to_host{str_to_uuid($fake_hostname)} = "${prefix_full_double}${dc}${suffix}($instance)";
	}
    }
}

sub usage {
    system("podselect -section NAME -section DESCRIPTION $0 | pod2text-utf8 >&2");
    exit(0);
}

sub usage_full {
    system("pod2text-utf8 <$0 | less -F");
    exit(0);
}

# более-менее универсальная функция, претендует на Yandex::...
sub complete {
    my $line = $ENV{COMP_LINE} || '';
    my @params = split /\s+/, $line;
    my $is_new_param = $line =~ /\s$/;
    # удаляем команду
    shift @params;
    # находим начало подсказки
    my $startwith = !$is_new_param ? pop(@params) : '';
    $startwith = '' if !defined $startwith;
    # удаляем опции если додсказки зависят от позиции
    @params = grep {!/^-/} @params;

    my @words = _get_complete_words($startwith, \@params);

    print join("", map {"$_\n"} sort grep {/^\Q$startwith\E/} @words), "\n";
}

# скриптозависимая функция - выдаёт список слов
sub _get_complete_words {
    my ($startwith, $params) = @_;
    if (defined $startwith && $startwith =~ /^-/) {
        return qw/--force --help --skip-slave-start --resolve-uuid -A/;
    } elsif (!@$params) {
        return _dbnames();
    } else {
        return keys %CMDS;
    }
}

# список инстансов, установленных на сервере
sub _dbnames {
    opendir(my $dh, "/etc/init.d") || die "Can't open /etc/init.d: $!";
    my @dbnames = map {/^mysql.(.*)$/ ? ($1) : ()} readdir $dh;
    closedir($dh);
    return @dbnames;
}

