3
# Copyright (C) 2009 Giuseppe Maxia
4
# Contacts: http://datacharmer.org
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; version 2 of the License
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24
use English qw( -no_match_vars );
26
use Getopt::Long qw(:config no_ignore_case );
27
use File::Copy qw/cp/;
31
my $DEBUG = $MySQL::Sandbox::DEBUG;
33
my $sandbox_options_file = "my.sandbox.cnf";
34
my $sandbox_current_options = "current_options.conf";
36
my %supported_operations = (
37
ports => 'lists ports used by the Sandbox',
38
range => 'finds N consecutive ports not yet used by the Sandbox',
39
info => 'returns configuration options from a Sandbox',
40
tree => 'creates a replication tree',
41
copy => 'copies data from one Sandbox to another',
42
move => 'moves a Sandbox to a different location',
43
port => 'Changes a Sandbox port',
46
my %supported_formats = (
47
text => 'plain text dump of requested information',
48
perl => 'fully structured information in Perl code',
54
parse => 'o|operation=s',
56
accepted => \%supported_operations,
57
help => 'what task to perform',
61
parse => 's|source_dir=s',
63
help => 'source directory for move,copy',
67
parse => 'd|dest_dir=s',
69
help => 'destination directory for move,copy',
73
parse => 'n|new_port=s',
75
help => 'new port while moving a sandbox',
79
parse => 'u|only_used',
81
help => 'for "ports" operation, shows only the used ones',
85
parse => 'i|min_range=i',
87
help => 'minimum port when searching for available ranges',
91
parse => 'x|max_range=i',
93
help => 'maximum port when searching for available ranges',
97
parse => 'z|range_size=i',
99
help => 'size of range when searching for available port range',
103
parse => 'f|format=s',
105
accepted => \%supported_formats,
106
help => 'format for "ports" and "info"',
110
parse => 'p|search_path=s',
111
value => $ENV{SANDBOX_HOME},
112
help => 'search path for ports and info',
116
parse => 'a|all_info',
118
help => 'print more info for "ports" operation'
122
parse => 'tree_nodes=s',
124
help => 'description of the tree (x-x x x-x x|x x x|x x)',
128
parse => 'mid_nodes=s',
130
help => 'description of the middle nodes (x x x)',
134
parse => 'leaf_nodes=s',
136
help => 'description of the leaf nodes (x x|x x x|x x)',
140
parse => 'tree_dir=s',
142
help => 'which directory contains the tree nodes',
146
parse => 'v|verbose',
148
help => 'prints more info on some operations'
154
help => 'this screen',
158
my %options = map { $_, $parse_options{$_}{value} } keys %parse_options;
160
GetOptions( map { $parse_options{$_}{parse}, \$options{$_} }
161
keys %parse_options )
164
get_help() if $options{help} or ! $options{operation};
166
if ($options{verbose}) {
167
$DEBUG = $options{verbose} unless $DEBUG;
170
for my $op ( keys %parse_options ) {
171
if ( $parse_options{$op}{accepted} ) {
172
my %accepted = %{ $parse_options{$op}{accepted} };
173
for my $ak ( keys %accepted ) {
174
unless ( exists $accepted{ $options{$op} } ) {
175
croak "invalid value '$options{$op}' for option <$op>\n";
181
for my $dir (qw(source_dir dest_dir tree_dir search_path)) {
182
if ($options{$dir}) {
183
$options{$dir} =~ s/^\s*~/$ENV{HOME}/;
187
if ( $options{operation} eq 'ports' ) {
190
elsif ( $options{operation} eq 'info' ) {
191
$options{all_info} = 1;
192
$options{format} = 'perl';
195
elsif ( $options{operation} eq 'range' ) {
198
elsif ( $options{operation} eq 'tree' ) {
199
make_tree($options{tree_dir} )
201
elsif ( $options{operation} eq 'move' ) {
202
move_sandbox( $options{source_dir}, $options{dest_dir} );
204
elsif ( $options{operation} eq 'port' ) {
205
unless ($options{new_port}) {
206
croak "operation 'port' requires new_port option\n";
208
move_sandbox( $options{source_dir}, $options{source_dir}, 'alreday_moved' );
210
elsif ( $options{operation} eq 'copy' ) {
211
copy_single_sandbox( $options{source_dir}, $options{dest_dir} );
214
croak "unsupported operation ($options{operation})\n";
218
my ( $ports, $all_info ) = get_sb_info();
219
my $minimum_port = $options{min_range};
220
my $maximum_port = $options{max_range};
221
my $range_size = $options{range_size};
222
if ( $minimum_port >= $maximum_port ) {
223
croak "minimum range must be lower than the maximum range\n";
225
if ( ( $minimum_port + $range_size ) > $maximum_port ) {
226
croak "range too wide for given boundaries\n";
230
while ( !$range_found ) {
231
if ( $minimum_port >= $maximum_port ) {
232
croak "can't find a range of $range_size "
233
. "free ports between "
234
. "$options{min_range} and $options{max_range}\n";
236
for my $i ( $minimum_port .. $minimum_port + $range_size ) {
237
if ( exists $ports->{$i} or ( $i >= $maximum_port ) ) {
238
$minimum_port = $i + 1;
244
printf "%5d - %5d\n", $minimum_port , $minimum_port + $range_size;
248
my ( $ports, $all_info ) = get_sb_info();
250
if ( $options{format} eq 'perl' ) {
251
print Data::Dumper->Dump( [$ports], ['ports'] );
252
print Data::Dumper->Dump( [$all_info], ['all_info'] )
253
if $options{all_info};
255
elsif ( $options{format} eq 'text' ) {
256
for my $port ( sort { $a <=> $b } keys %$ports ) {
257
printf "%5d %2d\n", $port, $ports->{$port};
261
croak "unrecognized format -> $options{format}\n";
263
return ( $ports, $all_info );
267
my ($search_path) = @_;
276
if ( $seen_dir eq $File::Find::dir ) {
280
if ( $params = get_sandbox_params($File::Find::dir, 1) ) {
281
$seen_dir = $File::Find::dir;
282
my $port = $params->{opt}{port};
283
if ( -f $params->{opt}{pid_file}
284
&& -e $params->{opt}{socket} )
287
$all_info{$port} = $params if $options{all_info};
290
unless ( $options{only_used} ) {
292
$all_info{$port} = $params if $options{all_info};
298
$search_path || $options{search_path}
300
return ( \%ports, \%all_info );
303
sub get_sandbox_params {
304
my ($dir, $skip_strict) = @_;
305
confess "directory name required\n" unless $dir;
306
confess "directory $dir doesn't exist\n" unless -d $dir;
307
unless (is_a_sandbox($dir)) {
308
confess "directory <$dir> must be a sandbox\n" unless $skip_strict;
314
if ( -f "$dir/$sandbox_options_file" ) {
315
$params{opt} = get_option_file_contents("$dir/$sandbox_options_file");
318
# warn "options file $dir not found\n";
321
if ( -f "$dir/$sandbox_current_options" ) {
323
get_option_file_contents("$dir/$sandbox_current_options");
326
# warn "current conf file not found\n";
332
sub get_option_file_contents {
334
confess "file name required\n" unless $file;
335
confess "file $file doesn't exist\n" unless -f $file;
337
open my $RFILE, q{<}, $file
338
or confess "can't open file $file\n";
339
while ( my $line = <$RFILE> ) {
340
next if $line =~ /^\s*$/;
341
next if $line =~ /^\s*#/;
342
next if $line =~ /^\s*\[/;
344
my ( $key, $val ) = split /\s*=\s*/, $line;
346
$options{$key} = $val;
349
# print Dumper(\%options) ; exit;
357
print "\n", $msg, "\n";
361
print "usage: $PROGRAM_NAME [options] \n";
362
for my $op ( sort { $parse_options{$a}{so} <=> $parse_options{$b}{so} }
363
keys %parse_options )
365
my $val = $options{$op};
366
my $parse = $parse_options{$op}{parse};
368
if ( $parse =~ s/=(.+)// ) {
371
my ( $short, $long ) = split /\|/, $parse;
376
printf "\t%s%-5s --%-15s (%s) <%s> - %s\n",
382
$parse_options{$op}{help} || '';
383
if ( $parse_options{$op}{accepted} ) {
384
my %accepted = %{ $parse_options{$op}{accepted} };
385
for my $ao ( keys %accepted ) {
386
printf "\t\t %-10s %s\n", "'$ao'", $accepted{$ao};
396
croak "you must set the directory using the 'tree_dir' option\n";
399
croak "directory ($dir) does not exist\n";
402
if ($options{tree_nodes} ) {
403
my ($m, $mid, $leaf) = split /-/, $options{tree_nodes};
408
croak " master not defined in tree_nodes\n";
411
$options{mid_nodes} = $mid;
414
croak " middle nodes not defined in tree_nodes\n";
417
$options{leaf_nodes} = $leaf;
420
croak " leaf nodes not defined in tree_nodes\n";
424
my @MID_NODES = split ' ', $options{mid_nodes}
425
or croak "no mid nodes selected. Use the --mid_nodes option\n";
426
for my $mid (@MID_NODES) {
427
croak "middle nodes must be numeric" unless $mid =~ /^\d+$/;
431
my @chunks = split /\|/, $options{leaf_nodes}
432
or croak "no leaf nodes selected. Use the --leaf_nodes option\n";
433
# print Data::Dumper->Dump([\@chunks], ['chunks']);
434
for my $c (@chunks) {
435
my @leaf = split ' ', $c;
436
croak "empty leaf node\n" unless @leaf;
438
croak "leaf nodes must be numeric" unless $ln =~ /^\d+$/;
440
push @LEAF_NODES, [@leaf];
443
# print Data::Dumper->Dump([\@MID_NODES], ['MID_NODES']);
444
# print Data::Dumper->Dump([\@LEAF_NODES], ['LEAF_NODES']);
445
if ( @LEAF_NODES != @MID_NODES) {
446
croak "you must specify at least one leaf node for each middle node\n";
449
for my $node (( $master, @MID_NODES, map {@$_} @LEAF_NODES)) {
450
if ( ! -d "$dir/node$node" ) {
451
croak "node $node does not exist\n";
455
my ($N1INFO, $N1PORT)=get_node_info($dir, $master);
458
croak "can't get the port for node$master\n"
459
. "make sure the node is running\n";
462
my $CHANGE_MASTER_Q= "CHANGE MASTER TO master_host='127.0.0.1', "
463
. "master_user='msandbox', master_password='msandbox',"
466
print "$dir/stop_all\n";
467
system "$dir/stop_all";
469
print "node $master is master\n";
470
unless ( -e $N1INFO->{opt}{socket}) {
471
system "$dir/node$master/start"
474
system qq(echo "$dir/use_all 'stop slave'" > $dir/clear_all);
475
system qq(echo "$dir/use_all 'stop slave'" > $dir/stop_all);
476
system qq(echo "" > $dir/send_kill_all);
477
system qq(echo "$dir/node$master/start" > $dir/start_all);
478
for my $mid_node ( @MID_NODES ) {
480
my ($MID_NODE_INFO, $MID_NODE_PORT)=get_node_info($dir, $mid_node);
481
unless ( -e $MID_NODE_INFO->{opt}{socket}) {
482
system "$dir/node$mid_node/start";
484
my $HAS_UPDATES=`grep log_slave_updates $dir/node$mid_node/my.sandbox.cnf`;
485
my $HAS_REPORT=`grep "report-host" $dir/node$mid_node/my.sandbox.cnf`;
487
unless ($HAS_REPORT) {
488
system qq( echo "report-host=node$mid_node" >> $dir/node$mid_node/my.sandbox.cnf) ;
489
system qq( echo "report-port=$N1PORT" >> $dir/node$mid_node/my.sandbox.cnf) ;
491
unless ( $HAS_UPDATES) {
492
print "enabling node $mid_node to relay updates\n";
493
system qq(echo "log_slave_updates" >> $dir/node$mid_node/my.sandbox.cnf) ;
494
system qq($dir/node$mid_node/restart) ;
497
system qq($dir/n$mid_node -e "stop slave") ;
498
system qq($dir/n$mid_node -e "$CHANGE_MASTER_Q $N1PORT") ;
499
system qq($dir/n$mid_node -e "start slave") ;
500
print " node $mid_node is slave of node $master\n";
501
my $l_nodes = shift @LEAF_NODES;
502
system qq(echo "$dir/node$mid_node/start" >> $dir/start_all);
503
for my $leaf_node (@$l_nodes) {
504
my ($LEAF_NODE_INFO, $LN_PORT) = get_node_info($dir, $leaf_node);
505
unless ( -e $LEAF_NODE_INFO->{opt}{socket}) {
506
system "$dir/node$leaf_node/start";
508
check_report($dir,$leaf_node, 1, $MID_NODE_PORT);
509
system qq($dir/n$leaf_node -e "stop slave");
510
system qq($dir/n$leaf_node -e "$CHANGE_MASTER_Q $MID_NODE_PORT");
511
system qq($dir/n$leaf_node -e "start slave");
512
print " node $leaf_node is slave of node $mid_node\n";
513
system qq(echo "$dir/node$leaf_node/stop" >> $dir/stop_all);
514
system qq(echo "$dir/node$leaf_node/clear" >> $dir/clear_all);
515
system qq(echo "$dir/node$leaf_node/send_kill" >> $dir/send_kill_all);
516
system qq(echo "$dir/node$leaf_node/start" >> $dir/start_all);
518
system qq(echo "$dir/node$mid_node/stop" >> $dir/stop_all);
519
system qq(echo "$dir/node$mid_node/clear" >> $dir/clear_all);
520
system qq(echo "$dir/node$mid_node/send_kill" >> $dir/send_kill_all);
522
system qq(echo "$dir/node$master/stop" >> $dir/stop_all);
523
system qq(echo "$dir/node$master/clear" >> $dir/clear_all);
524
system qq(echo "$dir/node$master/send_kill" >> $dir/send_kill_all);
528
my ($dir, $node, $restart, $master_port) = @_;
529
my $HAS_REPORT=`grep "report-host" $dir/node$node/my.sandbox.cnf`;
530
unless ($HAS_REPORT) {
531
system qq(echo "report-host=node$node" >> $dir/node$node/my.sandbox.cnf);
532
system qq(echo "report-port=$master_port" >> $dir/node$node/my.sandbox.cnf);
534
system qq($dir/node$node/restart);
540
my ($dir, $node) = @_;
541
my ( $info ) = get_sandbox_params("$dir/node$node");
542
# print Dumper($ports, $all_info);
543
confess "can't read port for node $node" unless $info;
544
return ($info, $info->{opt}{port});
548
my ($source, $dest) = @_;
550
croak "Need a source directory (--source_dir)\n";
553
croak "Need a destination directory (--dest_dir)\n";
559
$source =~ s/^\s*~/$ENV{HOME}/;
560
$dest =~ s/^\s*~/$ENV{HOME}/;
561
unless (($source =~ m{^/}) && ($dest =~ m{^/}) ) {
562
croak "Source and destination directories must be absolute paths.\n";
564
unless ( -d $source ) {
565
croak "directory $source does not exist\n";
567
if ( -x "$source/start") {
568
if (( $source eq $dest) and $options{new_port}) {
569
move_single_sandbox($source, $dest, "already_moved");
572
move_single_sandbox($source, $dest);
575
elsif ( -x "$source/start_all") {
576
move_multiple_sandbox($source, $dest);
579
croak "directory $source does not seem to be a sandbox\n";
583
sub move_multiple_sandbox {
584
my ($old_dir, $new_dir) = @_;
585
unless ( -d $old_dir ) {
586
croak " directory $old_dir doesn't exist\n";
589
croak "directory $new_dir already exists\n";
591
if ( -x "$old_dir/stop_all" ) {
592
system "$old_dir/stop_all";
594
while ( file_exists($old_dir, '\.pid$')) {
600
croak "$old_dir does not seem to contain a multiple sandbox\n";
602
my @old_subdirs = grep { -d $_ } glob("$old_dir/*/");
603
for my $od (@old_subdirs) {
604
unless ( -x "$od/change_paths" ) {
605
croak "directory $od is not a sandbox created with version 2.0.15+\n";
608
my $result = system "mv $old_dir $new_dir";
610
croak "unable to move sandbox $old_dir to $new_dir ($OS_ERROR)\n";
612
my @new_subdirs = ();
613
for my $od (@old_subdirs) {
615
if (($nd =~ s/$old_dir/$new_dir/ ) && ( -d $nd )) {
616
push @new_subdirs, [$od, $nd];
619
# reverting to old directory
620
system "mv $new_dir $old_dir";
621
croak "can't move directory $od to $nd\n";
624
for my $sd (@new_subdirs) {
625
move_single_sandbox( $sd->[0], $sd->[1], "already_moved" );
628
unless ($old_dir =~ m{/$} ) {
631
unless ($new_dir =~ m{/$} ) {
634
my @nodes = glob("n[0-9]*");
635
my @slaves = glob("s[0-9]*");
636
my @scripts = qw(m start_all stop_all clear_all send_kill_all
637
check_slaves use_all initialize_slaves);
638
for my $script (( @nodes, @slaves, @scripts ) ) {
640
system q(perl -i.bak -pe 'BEGIN{$old=shift;$new=shift};s/$old/$new/g')
641
. " $old_dir $new_dir $script " ;
647
my ($dir,$pattern) = @_;
651
$file_count++ if $File::Find::name =~ /$pattern/;
658
sub move_single_sandbox {
659
my ($old_dir, $new_dir, $already_moved) = @_;
660
unless ( $already_moved) {
661
unless ( -d $old_dir ) {
662
croak " directory $old_dir doesn't exist\n";
665
if ( -d $new_dir && (! $already_moved ) ) {
666
croak "directory $new_dir already exists\n";
668
unless ( (-e "$old_dir/change_paths")
669
or ( $already_moved && -e "$new_dir/change_paths") ) {
670
croak "script 'change_paths' not found. "
671
. "Please get it from any Sandbox installed with version 2.0.15+\n";
673
if ($already_moved) {
674
if (is_running($new_dir)) {
675
stop_sandbox($new_dir);
679
stop_sandbox($old_dir);
680
my $result = system qq(mv $old_dir $new_dir) ;
682
croak "unable to move sandbox $old_dir to $new_dir ($OS_ERROR)\n";
686
unless ( $old_dir eq $new_dir) {
687
system "./change_paths $old_dir $new_dir";
689
if ($options{new_port}) {
690
unless ($options{new_port} =~ /^\d+$/) {
691
croak "new port must be numerical ($options{new_port})\n";
693
if (($options{new_port} <= 1024) or ( $options{new_port} > 32000)) {
694
croak "new port out of range ($options{new_port}) - "
695
. "it must be between 1025 and 32000\n";
697
unless ( -e "$new_dir/change_ports" ) {
698
croak "script 'change_ports' not found. "
699
. "Please get it from any Sandbox installed with version 2.0.18+\n";
701
system "./change_ports $options{new_port}";
707
my ($info) = get_sandbox_params($sbdir);
708
if ( -x "$sbdir/stop" ) {
709
system "$sbdir/stop";
711
while ($timeout && ( -e $info->{opt}{socket} )) {
715
if ( -e $info->{opt}{socket} ) {
716
croak "sandbox in $sbdir is still running. Unable to stop it\n";
720
croak "$sbdir does not seem to contain a sandbox\n";
724
sub clone_sandbox_data {
725
my ($source_dir, $dest_dir) = @_;
726
croak "source directory missing\n" unless $source_dir;
727
croak "destination directory missing\n" unless $dest_dir;
728
$source_dir =~ s{/\s*$}{};
729
unless (-d $source_dir) {
730
croak "<$source_dir> is not a valid directory\n";
732
unless (-d $dest_dir) {
733
croak "<$dest_dir> is not a valid directory\n";
735
# checking if it is a valid data directory
736
unless (-d "$source_dir/mysql") {
737
croak "<$source_dir> is not a valid data directory\n"
739
my @pids = glob( "$source_dir/*.pid" );
741
croak "it seems that your sandbox is running. Please stop it and try again\n";
743
my @skip_files = map {qr/$_/} (
749
'-relay-bin\.index+$',
755
my $dir = $File::Find::dir;
756
my $fname = $File::Find::name;
760
# print "<$File::Find::name><$File::Find::dir> [$dir] [$fname]\n";
761
return if $dir =~ /^\./;
762
return if $File::Find::name eq $source_dir;
763
for my $skip (@skip_files) {
764
return if $fname =~ $skip ;
766
if ( -d $File::Find::name ) {
767
if ( -d "$dest_dir/$fname" ) {
770
elsif ( -f "$dest_dir/$fname" ) {
771
croak "<$dest_dir/$fname> already exists and it is not a directory\n";
773
print_debug( "creating $dest_dir/$fname\n");
774
my $result = mkdir "$dest_dir/$fname";
776
croak "error creating directory ($!)\n";
779
elsif ( -f $File::Find::name ) {
780
# print "$Find::File::dir eq $source_dir\n";
781
if ((! $File::Find::dir) or ($File::Find::dir eq $source_dir)) {
784
print_debug( "$File::Find::name -> $dest_dir/$dir/$fname\n");
785
my $result = cp $File::Find::name, "$dest_dir/$dir/$fname";
787
croak "error copying file $!\n";
791
croak "unhandled file $File::Find::name\n";
800
my ($msg, $level) = @_;
802
if ($DEBUG >= $level) {
809
unless ( -d $sandbox ) {
810
confess "Can't see if it's running. <$sandbox> is not a sandbox\n";
812
my $sboptions = get_sandbox_params($sandbox);
813
unless ($sboptions->{opt}
814
&& $sboptions->{opt}{'pid_file'}
815
&& $sboptions->{opt}{'socket'}) {
816
# print Dumper($sboptions);
817
confess "<$sandbox> is not a single sandbox\n";
819
if ( ( -f $sboptions->{opt}{'pid_file'} )
820
&& ( -e $sboptions->{opt}{'socket'}) ) {
821
return (1, $sboptions);
824
return (0, $sboptions);
828
sub copy_single_sandbox {
829
my ($source_dir, $dest_dir) = @_;
830
if ($options{new_port}) {
831
croak "option 'new_port' is not supported with 'copy'\n";
833
unless ( $source_dir) {
834
croak " source directory missing\n";
836
unless ( -d $source_dir) {
837
croak " <$source_dir> not found\n";
839
unless ( $dest_dir) {
840
croak " destination directory missing\n";
842
unless ( -d $dest_dir) {
843
croak " destination directory <$dest_dir> not found\n";
845
my ($srunning, $ssboptions) = is_running($source_dir);
846
my ($drunning, $dsboptions) = is_running($dest_dir);
847
unless ($ssboptions->{conf}{install_version}) {
848
croak "unable to determine version for <$source_dir>\n";
850
unless ($dsboptions->{conf}{install_version}) {
851
croak "unable to determine version for <$dest_dir>\n";
853
if ($ssboptions->{conf}{install_version} ne $dsboptions->{conf}{install_version}) {
854
croak "can't copy from $source_dir to $dest_dir. Not the same major version\n";
857
system "$source_dir/stop";
858
if ( -e $ssboptions->{opt}{'pid_file'} ) {
859
system "$source_dir/send_kill";
863
system "$dest_dir/stop";
864
if ( -e $dsboptions->{opt}{'pid_file'} ) {
865
system "$dest_dir/send_kill";
868
clone_sandbox_data( "$source_dir/data", "$dest_dir/data");
874
confess "directory missing\n";
877
my %sandbox_files = map {s{.*/}{}; $_, 1 } glob("$dir/*");
878
my @required = (qw(data start stop send_kill clear use restart),
879
$sandbox_current_options, $sandbox_options_file );
880
for my $req (@required) {
881
unless (exists $sandbox_files{$req}) {