The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl 
# make_replication_sandbox
#    The MySQL Sandbox
#    Copyright (C) 2006-2010 Giuseppe Maxia
#    Contacts: http://datacharmer.org
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 of the License
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

use strict;
use warnings;
use English qw( -no_match_vars ); 
use Data::Dumper;
use Getopt::Long qw(:config no_ignore_case );
use MySQL::Sandbox qw(get_ranges runs_as_root use_env sbinstr);
use MySQL::Sandbox::Scripts;

my $DEBUG = $MySQL::Sandbox::DEBUG;

runs_as_root();

my %defaults = (
    replication_base_port => $MySQL::Sandbox::default_base_port{replication},
    circular_base_port    => $MySQL::Sandbox::default_base_port{circular},
    replication_directory => 'rsandbox',
);

unless ( $ENV{SANDBOX_HOME} ) { 
    $ENV{SANDBOX_HOME} = "$ENV{HOME}/sandboxes";
}
# 
# $SANDBOX_HOME must be set BEFORE loading MyScripts
#
#if ( -d "$ENV{HOME}/sandboxes" ) {
#    $ENV{SANDBOX_HOME} = $ENV{SANDBOX_HOME} || "$ENV{HOME}/sandboxes";
#}
#
unless ( -d $ENV{'SANDBOX_HOME'} ) {
    mkdir $ENV{'SANDBOX_HOME'} 
        or die "can't create $ENV{'SANDBOX_HOME'} ($CHILD_ERROR) \n";
}

#my $install_dir;
#$install_dir = $PROGRAM_NAME;
#$install_dir =~ s{/\w+(\.pl)?$}{};
# eval "use lib q($install_dir)";

my $msb = MySQL::Sandbox->new();

$msb->parse_options( MySQL::Sandbox::Scripts::parse_options_replication() ); 

GetOptions (
    map { $msb->{parse_options}{$_}{parse}, \$msb->{options}{$_} }        
        grep { $msb->{parse_options}{$_}{parse}}  keys %{$msb->{parse_options}}  
) or $msb->get_help();

$msb->get_help() if $msb->{options}{'help'};

unless ($msb->{options}{server_version}) {
    $msb->{options}{server_version} = shift
        or $msb->get_help('server version required');
}

if ($msb->{options}{master_options})
{
    $ENV{MASTER_OPTIONS} = $msb->{options}{master_options};
}
if ($msb->{options}{node_options})
{
    $ENV{NODE_OPTIONS} = $msb->{options}{node_options};
}
if ($msb->{options}{slave_options})
{
    $ENV{SLAVE_OPTIONS} = $msb->{options}{slave_options};
}


if ($msb->{options}{one_node_options})
{
    for my $node_opt (@{ $msb->{options}{one_node_options}} )
    {
        if ($node_opt =~ /^(\d+):\s*(\S.+)/)
        {
            $ENV{"NODE${1}_OPTIONS"} = $2;
        }
        else
        {
            get_help("invalid format for --one_node_option ($node_opt)");    
        }
    }
}
if ($msb->{options}{one_slave_options})
{
    for my $node_opt (@{ $msb->{options}{one_slave_options}} )
    {
        if ($node_opt =~ /^(\d+):\s*(\S.+)/)
        {
            $ENV{"SLAVE${1}_OPTIONS"} = $2;
        }
        else
        {
            get_help("invalid format for --one_slave_option ($node_opt)");    
        }
    }
}


for my $opt (qw(repl_user repl_password remote_access)) {
    if (defined $msb->{options}{$opt}) {
        $MySQL::Sandbox::default_users{$opt} = $msb->{options}{$opt};
    }
}

my $base_version ;
my $release_no;
my $create_gtid_script=0;
my $dashed_version ;
my $replication_port = 0;
my $prefix='';
my $temp_path = $msb->{options}{server_version};
$temp_path =~ s{.*/}{};
if ( $temp_path =~ /^(\D+)\d+/ ) {
    $prefix = $1;
}
if ( $temp_path =~ /(\d+)\.(\d+)\.(\d+)/ ) {
    my ($major, $minor, $rev) = ($1, $2, $3);
    $base_version = "${major}${minor}${rev}";
    $release_no = $rev;
    $dashed_version = "${prefix}${major}_${minor}_${rev}";
    $base_version =~ s/^0+//;
    $base_version += $major + ($minor * 10) + ($rev * 100);
    if (($major > 5) or (($major == 5) && ($minor >= 6) && ($rev >= 9 )))
    {
        $create_gtid_script=1;
    } 
}
else {
    $msb->get_help("No recognizable version pattern in $msb->{options}{server_version}\n");
}

my @supported_topologies = qw(standard circular);
#
# deals with circular replication
#

if ($msb->{options}{master_master}) {
    $msb->{options}{topology} = 'circular';
    $msb->{options}{how_many_slaves} = 2;
}

if ($msb->{options}{circular} && ($msb->{options}{circular} =~ /^\d+$/)) {
    $msb->{options}{topology} = 'circular';
    $msb->{options}{how_many_slaves} = $msb->{options}{circular};
}

if ($msb->{options}{'topology'} ) {
    if ($msb->{options}{'topology'} eq 'standard') {
        # do nothing. default behavior
    }
    elsif ($msb->{options}{'topology'} eq 'circular') {
        make_circular_replication();
    }
    else {
        die qq(unsupported topology "$msb->{options}{topology}". Allowed options: {@supported_topologies}\n);
    }
}

# print Dumper(\%{$msb->{options}});

my $replication_directory ;

if ( $msb->{options}{replication_directory}) {
    $replication_directory = "$msb->{options}{upper_directory}/$msb->{options}{replication_directory}";
}
else {
    $replication_directory = "$msb->{options}{upper_directory}/$defaults{replication_directory}_$dashed_version";
}

if ( -d $replication_directory ) {
    if ( -x "$replication_directory/clear_all") {
        system "$replication_directory/clear_all";
    }
    system "rm -rf $replication_directory/*";
}
else {
    print "creating replication directory $replication_directory\n" if $msb->{options}{verbose};
    mkdir $replication_directory;
    unless ( -d $replication_directory ) {
        die "error creating replication directory n";
    }
}

for my $dir ( 1 .. $msb->{options}{how_many_slaves}) {
    if ( -d "$replication_directory/node$dir" ) {
        system "rm -rf $replication_directory/node$dir" 
            and die "unable to clean $replication_directory/node$dir\n";
    }
}

if ( -d "$replication_directory/master" ) {
    system "rm -rf $replication_directory/master"
        and die "unable to clean $replication_directory/master\n";
}

if ( $msb->{options}{sandbox_base_port}) {
    $replication_port = $msb->{options}{sandbox_base_port};
}
else {
    $replication_port = $defaults{replication_base_port} + $base_version + ($release_no * 100);
}

if ( $msb->{options}{check_base_port}) {
    $replication_port = get_ranges({
                min_range   => $replication_port,
                range_size  => $msb->{options}{how_many_slaves},
                max_range   => 64000,
                search_path => $ENV{SANDBOX_HOME},
            }, 1);
}

print "installing and starting master\n";
my $additional_master_options = $ENV{MASTER_OPTIONS} || '';
my $additional_slave_options = $ENV{SLAVE_OPTIONS} || '';
if ($ENV{NODE_OPTIONS}) {
    $additional_master_options .= q{ } . $ENV{NODE_OPTIONS};
    $additional_slave_options  .= q{ } . $ENV{NODE_OPTIONS};
}
$additional_master_options .=  q{ };
$additional_slave_options  .=  q{ };
my $install_master='';
my $install_master_command  = qq(
   make_sandbox $msb->{options}{server_version} -- \\
    --datadir_from=script \\
    --no_confirm \\
    --no_check_port \\
    --upper_directory=$replication_directory \\
    --sandbox_directory=master \\
    --sandbox_port=$replication_port \\
    --repl_user=$msb->{options}{repl_user} \\
    --repl_password=$msb->{options}{repl_password} \\
    --remote_access=$msb->{options}{remote_access} \\
    --load_grants \\
    --prompt_prefix=master \\
    -c log-bin=mysql-bin \\
    -c server-id=1 $additional_master_options
    );
if ($msb->{options}{interactive} 
       or ($additional_master_options =~ /\binteractive\b/)) {
    my $result = system($install_master_command);
}
else {
    my $install_master = qx($install_master_command);
    print $install_master if $msb->{options}{verbose};
}
    if ($CHILD_ERROR) {
        print "error installing the master\n";
        print "$install_master\n($CHILD_ERROR $OS_ERROR)\n";
        exit(1)
    }

# sleep 1 ;

for my $slave (1 .. $msb->{options}{how_many_slaves}) {
   my $slave_port = $replication_port+ $slave;
   my $slave_id = 100 + $slave;
   print "installing slave $slave\n";
   my $this_slave_options = $ENV{"SLAVE${slave}_OPTIONS"} || '';
   my $install_slave_command = qq(
    make_sandbox $msb->{options}{server_version} -- \\
    --datadir_from=script \\
    --upper_directory=$replication_directory \\
    --sandbox_directory=node$slave \\
    --no_confirm \\
    --no_load_grants \\
    --prompt_prefix=slave$slave \\
    --sandbox_port=$slave_port \\
    --repl_user=$msb->{options}{repl_user} \\
    --repl_password=$msb->{options}{repl_password} \\
    --remote_access=$msb->{options}{remote_access} \\
    -c server-id=$slave_id \\
    -c report-host=SBslave$slave \\
    -c report-port=$slave_port \\
    -c log-bin=mysql-bin $additional_slave_options $this_slave_options
    );
    my $install_slave ='';
    if ($msb->{options}{interactive} 
       or ($additional_slave_options =~ /\binteractive\b/)) {
        system($install_slave_command);
    }
    else {
        $install_slave = qx($install_slave_command);
        print $install_slave if $msb->{options}{verbose};
    }
    if ($CHILD_ERROR) {
        print "error installing slave $slave\n";
        print "$install_slave\n ($CHILD_ERROR)\n";
        exit(1)
    }
    # sleep 1;
}

my $current_dir = $ENV{PWD};

chdir $replication_directory;

for my $cmd ( qw(start stop clear send_kill ) ) {
    my $cmd_fname = $cmd . '_all';
    $msb->write_to($cmd_fname, '>', '#!/bin/sh');
    if ($cmd eq 'start') {
        $msb->write_to ($cmd_fname, '>>', qq(echo 'executing "$cmd" on master') );
        $msb->write_to ($cmd_fname, '>>', qq($replication_directory/master/$cmd "\$@") );
    }
    for my $slave (1 .. $msb->{options}{how_many_slaves} ) {
        $msb->write_to ($cmd_fname, '>>', qq(echo 'executing "$cmd" on slave $slave'));
        $msb->write_to ($cmd_fname, '>>', qq($replication_directory/node$slave/$cmd "\$@"));
    }
    if ($cmd ne 'start') {
        $msb->write_to ($cmd_fname, '>>', qq(echo 'executing "$cmd" on master') );
        $msb->write_to ($cmd_fname, '>>', qq($replication_directory/master/$cmd "\$@"));
    }
    chmod 0755, $cmd_fname;
}

my $rw_splitting = '/usr/local/share/mysql-proxy/rw-splitting.lua';
my $mysql_proxy = '/usr/local/sbin/mysql-proxy';
if ( ( -x $mysql_proxy ) &&  ( -f $rw_splitting) ) {
    my $port = $replication_port;
    $msb->write_to('run_proxy',  '>',  '#!/bin/sh'); 
    $msb->write_to('run_proxy',  '>>', "$mysql_proxy \\" ); 
    $msb->write_to('run_proxy',  '>>', "  --proxy-backend-addresses=127.0.0.1:$port \\" ); 
    for my $slave ( 1 .. $msb->{options}{how_many_slaves}) {
        $port++;
        $msb->write_to('run_proxy',  '>>', "  --proxy-read-only-backend-addresses=127.0.0.1:$port \\" ); 
    }
    $msb->write_to('run_proxy',  '>>', "  --proxy-lua-script=$rw_splitting" ); 
    chmod 0755, 'run_proxy';
}

if ($create_gtid_script)
{
    $msb->write_to('enable_gtid', '>', '#!/bin/bash');
    $msb->write_to('enable_gtid', '>>', "cd $replication_directory");
    $msb->write_to('enable_gtid', '>>', './restart_all --master-info-repository=table --relay-log-info-repository=table  --gtid_mode=ON  --log-slave-updates --enforce-gtid-consistency');
    chmod 0755, 'enable_gtid';
}
$msb->write_to('clear_all', '>>', "date > $replication_directory/needs_initialization");

$msb->write_to('start_all', '>>', "if [ -f $replication_directory/needs_initialization ] ");
$msb->write_to('start_all', '>>', "then");
$msb->write_to('start_all', '>>', "    $replication_directory/initialize_slaves");
$msb->write_to('start_all', '>>', "    rm -f $replication_directory/needs_initialization");
$msb->write_to('start_all', '>>', "fi");


$msb->write_to('m',    '>',    "#!/bin/sh");
$msb->write_to('m',    '>>',   qq($replication_directory/master/use "\$@") );
chmod 0755, 'm';

$msb->write_to('restart_all', '>',  '#!/bin/sh');
$msb->write_to('restart_all', '>>', qq($replication_directory/stop_all));
$msb->write_to('restart_all', '>>', qq($replication_directory/start_all "\$@"));

chmod 0755, 'restart_all';
$msb->write_to('use_all', '>', '#!/bin/sh');
my $slave_list = join(' ', (1 .. $msb->{options}{how_many_slaves}) );

$msb->write_to('use_all', '>>', 'if [ "$1" = "" ]');
$msb->write_to('use_all', '>>', 'then');
$msb->write_to('use_all', '>>', '  echo "syntax: $0 command"');
$msb->write_to('use_all', '>>', '  exit 1');
$msb->write_to('use_all', '>>', 'fi');
$msb->write_to('use_all', '>>', '');
$msb->write_to('use_all', '>>', 'echo "# master  " ' );
$msb->write_to('use_all', '>>', 'echo "$@" | ' . $replication_directory . '/master/use  ');
$msb->write_to('use_all', '>>', '');
$msb->write_to('use_all', '>>', 'for SNUM in ' . $slave_list);
$msb->write_to('use_all', '>>', 'do ' );
$msb->write_to('use_all', '>>', '  echo "# server: $SNUM: " ' );
$msb->write_to('use_all', '>>', '  echo "$@" | ' . $replication_directory . '/node$SNUM/use $MYCLIENT_OPTIONS ');
$msb->write_to('use_all', '>>', 'done ' );
chmod 0755, 'use_all';

$msb->write_to('status_all', '>', '#!/bin/sh');
my $rdir = $replication_directory;
$rdir =~ s{.*/}{};
$msb->write_to('status_all', '>>', "echo REPLICATION $rdir");
$msb->write_to('status_all', '>>', $replication_directory . '/master/status');
$msb->write_to('status_all', '>>', '');
$msb->write_to('status_all', '>>', 'for SNUM in ' . $slave_list);
$msb->write_to('status_all', '>>', 'do ' );
$msb->write_to('status_all', '>>', $replication_directory . '/node$SNUM/status');
$msb->write_to('status_all', '>>', 'done ' );
chmod 0755, 'status_all';


$msb->write_to('check_slaves', '>', '#!/bin/sh' );
$msb->write_to('check_slaves', '>>', 'for NNUM in ' . $slave_list );
$msb->write_to('check_slaves', '>>', 'do' );
$msb->write_to('check_slaves', '>>', '  echo "slave # $NNUM"');
$msb->write_to('check_slaves', '>>', "  $replication_directory/node\$NNUM/use -e "
            . qq( 'show slave status\\G' | grep "\\(Running:\\|Master_Log_Pos\\|\\<Master_Log_File\\)") );
$msb->write_to('check_slaves', '>>', 'done ' );
chmod 0755, 'check_slaves';

for my $slave (1 .. $msb->{options}{how_many_slaves} ) {
    $msb->write_to("s$slave", '>', "#!/bin/sh");
    $msb->write_to("s$slave", '>', qq($replication_directory/node$slave/use "\$@"));
    chmod 0755, "s$slave";
}

$msb->write_to('initialize_slaves', '>', "#!/bin/sh");
$msb->write_to('initialize_slaves', '>>', "");
$msb->write_to('initialize_slaves', '>>', "# Don't use directly.");
$msb->write_to('initialize_slaves', '>>', "# This script is called by 'start_all' when needed");
$msb->write_to('initialize_slaves', '>>', "");
for my $slave (1 .. $msb->{options}{how_many_slaves} ) {
    print "starting slave $slave\n";
    system "./node$slave/start"
        and die "can't execute ./node$slave/start ($CHILD_ERROR $OS_ERROR)";

    $msb->write_to('initialize_slaves', '>>', qq(echo "initializing slave $slave"));

    $msb->write_to('initialize_slaves', '>>',  
      qq( echo 'CHANGE MASTER TO )
           . qq( master_host="127.0.0.1", )
           . qq( master_port=$replication_port, )
           . qq( master_user="$MySQL::Sandbox::default_users{'repl_user'}", )
           . qq( master_password="$MySQL::Sandbox::default_users{'repl_password'}" )
           . qq(' | $replication_directory/node$slave/use -u root  ) );

    $msb->write_to('initialize_slaves', '>>', 
            qq( echo 'START SLAVE' )
           . qq( | $replication_directory/node$slave/use -u root ) );
    system "perl -i -pe 's/^password/#password/' ./node$slave/my.sandbox.cnf"
}

#
# Creates the replication user
#
##system qq(./m -u root -e 'grant replication slave on *.* )
##    . qq( to "$MySQL::Sandbox::default_users{'repl_user'}"\@"127.%" ) 
##    . qq( identified by "$MySQL::Sandbox::default_users{'repl_password'}"')
##        and die "can't create slave user";

chmod 0755, "initialize_slaves";

system "./initialize_slaves"
        and die "can't initialize slave";

# 
# For the first run, there is root without password. 
# After the first run, a password is assigned
#
for my $slave (1 .. $msb->{options}{how_many_slaves} ) {
    system "perl -i -pe 's/^#password/password/' ./node$slave/my.sandbox.cnf"
}
# system q( perl -i -pe 's/(-u root) (--password=)/$1/g' initialize_slaves );

chdir $current_dir;
print "replication directory installed in ", use_env($replication_directory), "\n";

#
# instrumentation
#
sbinstr( "replication_directory installed in  <"
        . use_env($replication_directory) . ">");

sub make_circular_replication {
    my $cmd =  "make_multiple_sandbox --circular";
    if ($msb->{options}{how_many_slaves}) {
        $cmd .= ' --how_many_nodes=' . $msb->{options}{how_many_slaves};
    }
    if ($msb->{options}{replication_directory} ) {
        $cmd .= ' --group_directory=' . $msb->{options}{replication_directory};
    }        
    else {
        $cmd .= ' --group_directory=rcsandbox_' . $dashed_version 
    }
    if ($msb->{options}{sandbox_base_port} ) {
        $cmd .= ' --sandbox_base_port=' . $msb->{options}{sandbox_base_port}
    }
    else {
        $cmd .= ' --sandbox_base_port=' . ($defaults{circular_base_port} + ($release_no * 100));
    }
    exec "$cmd $msb->{options}{server_version}" ;
}

__END__