#!/usr/bin/perl eval 'exec /usr/bin/perl -S $0 ${1+"$@"}' if 0; # not running under some shell # Copyright (C) 2009 Darren Oldag # # 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; use MySQL::Sandbox::Scripts; use Graph::Reader::Dot; use Graph; use File::Path qw(rmtree); use Sys::Hostname; $Graph::Reader::Dot::UseNodeAttr = 'yes'; $Graph::Reader::Dot::UseEdgeAttr = 'yes'; my $DEBUG = $MySQL::Sandbox::DEBUG; my $DEFAULT_PORT=22000; unless ( $ENV{SANDBOX_HOME} ) { $ENV{SANDBOX_HOME} = "$ENV{HOME}/sandboxes"; } my $msb = MySQL::Sandbox->new(); my %defaults = ( circular_base_port => $MySQL::Sandbox::default_base_port{multiple}, group_directory => 'multi_msb', ); $msb->parse_options ( MySQL::Sandbox::Scripts::parse_options_many() ); 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'); } my $dotfile = shift or die("usage: $PROGRAM_NAME version topo.dot"); print "using topology in $dotfile\n"; ##################################### # Load the topology from the dot file my $reader = Graph::Reader::Dot->new(); my $g = $reader->read_graph($dotfile); my $topology_name = $g->get_graph_attribute('name'); print "found topoloy named $topology_name \n"; my $replication_directory = "$msb->{options}{upper_directory}/$topology_name"; # See if the topology defines a base port my $next_port = $g->get_graph_attribute('baseport') || $DEFAULT_PORT; # iterate nodes and annotate them my @nodeNames = $g->vertices(); my @mysqlNames = (); my @nodes = (); my @mysqls = (); my @proxies = (); my $NAME = 'name'; my $CLASS = 'class'; my $ATTRS = 'attributes'; my $PORT = 'port'; my $OPTS = 'options'; my $MASTER = 'master'; my $MYSQL = 'mysql'; my $PROXY = 'proxy'; my $PROXY_ADDR = 'proxy-address'; my $HOST = 'hostname'; my $PLUGINS = 'plugins'; my $BACKENDS = 'proxy-backend-addresses'; my $INSTANCES = 'mysqld-instance-dir'; sub attributes_to_options { my ($node) = @_; my $attrs = $node->{$ATTRS}; my $opts = $node->{$OPTS}; my $class = $node->{$CLASS}; while ((my $key,my $value) = each %$attrs) { if ($key =~ /^$class\./) { $key =~ s/^$class\.(.*)$/$1/; # allow a node-local value to win $opts->{$key} = $value if not exists $attrs->{$key}; } elsif ($key =~ /^[^\.]+\./) { # ignore other classes } else { $opts->{$key} = $value; } } } sub init_mysql_node { my ($node) = @_; $node->{$HOST} = hostname; if (not exists $node->{$ATTRS}->{$PORT}) { $node->{$OPTS}->{$PORT} = $next_port++; } else { $node->{$OPTS}->{$PORT} = $node->{$ATTRS}->{$PORT}; } attributes_to_options($node); # clone it here, so it is in the same spot as proxy $node->{$PORT} = $node->{$OPTS}->{$PORT}; push @mysqls, $node; push @mysqlNames, $node->{$NAME}; } sub init_proxy_node { my ($node) = @_; my $host; my $port; if (not exists $node->{$ATTRS}->{$PROXY_ADDR}) { $port = $next_port++; $node->{$OPTS}->{$PROXY_ADDR} = ":" . $port; $host = hostname; } else { my $pa = $node->{$ATTRS}->{$PROXY_ADDR}; ($host, $port) = split(/:/, $pa); $node->{$OPTS}->{$PROXY_ADDR} = $pa; } my $plugins = $node->{$ATTRS}->{$PLUGINS} || 'proxy'; $node->{$OPTS}->{$PLUGINS} = $plugins; attributes_to_options($node); $node->{$PORT} = $port; $node->{$HOST} = $host; $node->{'is_proxy'} = $plugins =~ /proxy/; $node->{'is_agent'} = $plugins =~ /agent/; if ($node->{'is_agent'}) { my $uuid = `uuidgen` or die "unable to generate uuid\n"; $node->{$OPTS}->{'agent-uuid'} = lc $uuid; } push @proxies, $node; } my %init_funcs = ( $MYSQL => \&init_mysql_node, $PROXY => \&init_proxy_node ); # OPTS are what are passed directly to the mysql command line via sandbox # so, for any attributes we do NOT want (specialty ones we recognize, we # will remove them. for my $nodeName (sort @nodeNames) { my $class = $g->get_vertex_attribute($nodeName, $CLASS) || $MYSQL; my $node = {}; print "Found node $nodeName of class $class\n"; $node->{$NAME} = $nodeName; $node->{$CLASS} = $class; my $attrs = $g->get_vertex_attributes($nodeName); my $init = $init_funcs{$class} || die "unknown class $class"; delete $attrs->{$CLASS}; $node->{$ATTRS} = $attrs; # $node->{$OPTS} = $attrs; $init->($node); push @nodes, $node; } my %node_map; for my $node (@nodes) { $node_map{$node->{$NAME}} = $node; } # Now that we've initialized some data, let's look for master/slave linkages. # We don't do it in the first loop, because the 'classes' might not # be setup yet. for my $node (@mysqls) { my $name = $node->{$NAME}; my @predecessors = $g->predecessors($node->{$NAME}); for my $pred_name (@predecessors) { my $pred = $node_map{$pred_name}; if ($pred->{$CLASS} =~ $MYSQL) { die ("$node->{$NAME} has more than one master.") if exists $node->{$MASTER}; my %master = ( "master_host" => qq("$pred->{$HOST}"), "master_port" => $pred->{$PORT}, # TODO: these need to be parameterized. "master_user" => qq("msandbox"), "master_password" => qq("msandbox") ); $node->{$MASTER} = \%master; } } my @successors = $g->successors($name); for my $slave (@successors) { my $s = $node_map{$slave}; die "Master/Slave edges cannot be proxied ($name->$slave)" if (not $s->{$CLASS} =~ $MYSQL); } if ($node->{$MASTER} && @successors) { # node is a slave, and has successors (is a master), so we'll automagic add love-slave-updates $node->{$OPTS}->{'log-slave-updates'}=1; } } for my $node (@proxies) { my @successors = $g->successors($node->{$NAME}); my @backends = (); for my $backend_name (@successors) { my $backend = $node_map{$backend_name}; my $host = $backend->{$HOST}; my $port = $backend->{$PORT}; push @backends, "$host:$port"; } $node->{$OPTS}->{$BACKENDS} = join(',',@backends); $node->{$OPTS}->{$INSTANCES} = qq($replication_directory/$node->{$NAME}/instances) if $node->{'is_agent'}; $node->{$OPTS}->{'pid-file'} = qq($replication_directory/$node->{$NAME}/chassis.pid); $node->{$OPTS}->{'log-file'} = qq($replication_directory/$node->{$NAME}/chassis.log); } ######################################### # Try to locate the proxy binary my $proxy_home; my $proxy; if (@proxies) { $proxy_home = $ENV{PROXY_HOME} || die "Proxies defined, with no PROXY_HOME specified"; if (-x "$proxy_home/bin/mysql-proxy") { $proxy = "$proxy_home/bin/mysql-proxy"; } elsif (-x "$proxy_home/sbin/mysql-proxy") { $proxy = "$proxy_home/sbin/mysql-proxy"; } else { die "mysql-proxy not found in \$PROXY_HOME/bin or \$PROXY_HOME/sbin"; } } ######################################### # Initialize physical sandbox directories if ( -d $replication_directory ) { if ( -x "$replication_directory/clear_all") { system "$replication_directory/clear_all"; } # experiment in portability ... system "rm -rf $replication_directory/*"; rmtree($replication_directory, { error => \my $err_list, result => \my $list }); } print "creating replication directory $replication_directory\n" if $msb->{options}{verbose}; mkdir $replication_directory; # we just recusrive deleted the parent replication_directory, # so we shouldn't need to iteratlively delete the nodes. sub hashToString { my $h = shift @_; my $prefix = shift @_ || ''; my $s = ''; for my $key (keys %$h) { $s .= "$prefix$key=$h->{$key} "; } return $s; } ############################################ # Deploy the nodes according to the topology for my $node (@mysqls) { my $name = $node->{$NAME}; my $port = $node->{$OPTS}->{$PORT}; my $opts = hashToString($node->{$OPTS}, '-c '); my $additional_node_options = $opts; print "Deploying node $name\n"; my $install_node = qx( make_sandbox $msb->{options}{server_version} \\ --datadir_from=script \\ --upper_directory=$replication_directory \\ --sandbox_directory=$name \\ --no_confirm \\ --no_check_port \\ --prompt_prefix=$name \\ --sandbox_port=$port \\ -c server-id=$port \\ -c log-bin=mysql-bin $additional_node_options ); print $install_node if $msb->{options}{verbose}; if ($CHILD_ERROR) { print "error installing node $node\n"; print "$install_node\n"; exit(1) } } ######################################################################## # Add the convenience [cmd]_all scripts to operate on the whole topology my $current_dir = $ENV{PWD}; chdir $replication_directory; my $op = '>'; for my $cmd ( qw(start stop clear send_kill) ) { my $cmd_fname = $cmd . '_all'; for my $node (@nodes) { my $name = $node->{$NAME}; $msb->write_to($cmd_fname, $op, '#!/bin/sh'); $op = '>>'; $msb->write_to ($cmd_fname, $op, qq(echo 'executing "$cmd" on "$name"') ); $msb->write_to ($cmd_fname, $op, "$replication_directory/$name/$cmd"); } chmod 0755, $cmd_fname; } # convenience 'start_proxies' script $msb->write_to('start_proxies', '>', '#!/bin/sh'); for my $node (@proxies) { my $name = $node->{$NAME}; $msb->write_to ('start_proxies', '>>', qq(echo 'executing "start" on "$name"') ); $msb->write_to ('start_proxies', '>>', "$replication_directory/$name/start"); } chmod 0755, 'start_proxies'; $msb->write_to('use_all', '>', '#!/bin/sh'); my $node_list = join(' ', sort @nodeNames); $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', '>>', 'for NAME in ' . $node_list); $msb->write_to('use_all', '>>', 'do ' ); $msb->write_to('use_all', '>>', ' echo "# server: $NAME: " ' ); $msb->write_to('use_all', '>>', ' echo "$@" | ' . $replication_directory . '/$NAME/use $MYCLIENT_OPTIONS '); $msb->write_to('use_all', '>>', 'done ' ); chmod 0755, 'use_all'; for my $node (@nodes) { my $name = lc $node->{$NAME}; $name = 'my-'.$name; $msb->write_to("$name", '>', "#!/bin/sh"); $msb->write_to("$name", '>>', qq($replication_directory/$node->{$NAME}/use "\$@")); chmod 0755, "$name"; } ######################################################################## # Proxy configs # current directory is still $replication_directory; for my $node (@proxies) { my $name = $node->{$NAME}; my $ini = "$name/chassis.ini"; mkdir "$name"; $msb->write_to($ini, '>', '[mysql-proxy]'); while ((my $key,my $value) = each %{$node->{$OPTS}}) { $msb->write_to($ini, '>>', "$key=$value"); } if ($node->{'is_agent'} == 1) { my $instances = $node->{$OPTS}->{$INSTANCES}; mkdir $instances; my @successors = $g->successors($node->{$NAME}); for my $backend_name (@successors) { my $dir = "$instances/$backend_name"; mkdir $dir; my $backend = $node_map{$backend_name}; my $host = $backend->{$HOST}; my $port = $backend->{$PORT}; my $ini = "$dir/agent-instance.ini"; $msb->write_to($ini, '>', '[mysqld]'); $msb->write_to($ini, '>>', "displayname=$backend_name"); $msb->write_to($ini, '>>', "hostname=$host"); $msb->write_to($ini, '>>', "port=$port"); # TODO: user support $msb->write_to($ini, '>>', "user=msandbox"); $msb->write_to($ini, '>>', "password=msandbox"); # TODO: socket support } } # TODO: use Scripts.pm for these my $start = "$name/start"; $msb->write_to($start, '>', "#!/bin/sh"); $msb->write_to($start, '>>', "echo starting proxy $name"); $msb->write_to($start, '>>', "$proxy --daemon --defaults-file=$replication_directory/$ini"); chmod 0755, $start; my $stop = "$name/stop"; $msb->write_to($stop, '>', "#!/bin/sh"); $msb->write_to($stop, '>>', "echo stopping proxy $name"); $msb->write_to($stop, '>>', "$replication_directory/$name/send_kill"); chmod 0755, $stop; $stop = "$name/send_kill"; $msb->write_to($stop, '>', "#!/bin/sh"); $msb->write_to($stop, '>>', "echo killing proxy $name"); $msb->write_to($stop, '>>', qq(kill `cat $node->{$OPTS}->{'pid-file'}`)); chmod 0755, $stop; my $use = "$name/use"; $msb->write_to($use, '>', "#!/bin/sh"); $msb->write_to($use, '>>', qq(#TODO BASEDIR=???) ); $msb->write_to($use, '>>', qq(mysql --protocol=tcp --host=$node->{$HOST} --port=$node->{$PORT} \$MYCLIENT_OPTIONS "\$@" )); chmod 0755, $use; my $clear = "$name/clear"; $msb->write_to($clear, '>', "#!/bin/sh"); $msb->write_to($clear, '>>', qq(#TODO) ); chmod 0755, $clear; } ######################################################################## # setup one-time slave initialization $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('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 $node (@mysqls) { if (not exists $node->{$MASTER}) { next; } my $name = $node->{$NAME}; my $master = $node->{$MASTER}; print "starting slave $name\n"; system "./$name/start" and die "can't execute ./$name/start ($CHILD_ERROR)"; $msb->write_to('initialize_slaves', '>>', qq(echo "initializing slave $name")); my $change_master = join(', ', split(/ +/, hashToString($master))); $change_master = qq( echo 'CHANGE MASTER TO ) . $change_master; $msb->write_to('initialize_slaves', '>>', $change_master . qq(' | $replication_directory/$name/use -u root ) ); $msb->write_to('initialize_slaves', '>>', qq( echo 'START SLAVE' ) . qq( | $replication_directory/$name/use -u root ) ); } chmod 0755, "initialize_slaves"; system "./initialize_slaves" and die "can't initialize slave"; system "./start_proxies" and die "can't start proxies"; # return to original dir chdir $current_dir;