1#!@PERL_PATH@ 2 3# Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License, version 2.0, 7# as published by the Free Software Foundation. 8# 9# This program is also distributed with certain software (including 10# but not limited to OpenSSL) that is licensed under separate terms, 11# as designated in a particular file or component or in included license 12# documentation. The authors of MySQL hereby grant you an additional 13# permission to link the program and your derivative works with the 14# separately licensed software that they have included with MySQL. 15# 16# This program is distributed in the hope that it will be useful, 17# but WITHOUT ANY WARRANTY; without even the implied warranty of 18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19# GNU General Public License, version 2.0, for more details. 20# 21# You should have received a copy of the GNU Library General Public 22# License along with this library; if not, write to the Free 23# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, 24# MA 02110-1301, USA 25 26use strict; 27use Getopt::Long; 28use Data::Dumper; 29use File::Basename; 30use File::Path; 31use DBI; 32use Sys::Hostname; 33use File::Copy; 34use File::Temp qw(tempfile); 35 36=head1 NAME 37 38mysqlhotcopy - fast on-line hot-backup utility for local MySQL databases and tables 39 40=head1 SYNOPSIS 41 42 mysqlhotcopy db_name 43 44 mysqlhotcopy --suffix=_copy db_name_1 ... db_name_n 45 46 mysqlhotcopy db_name_1 ... db_name_n /path/to/new_directory 47 48 mysqlhotcopy db_name./regex/ 49 50 mysqlhotcopy db_name./^\(foo\|bar\)/ 51 52 mysqlhotcopy db_name./~regex/ 53 54 mysqlhotcopy db_name_1./regex_1/ db_name_1./regex_2/ ... db_name_n./regex_n/ /path/to/new_directory 55 56 mysqlhotcopy --method='scp -Bq -i /usr/home/foo/.ssh/identity' --user=root --password=secretpassword \ 57 db_1./^nice_table/ user@some.system.dom:~/path/to/new_directory 58 59WARNING: THIS PROGRAM IS STILL IN BETA. Comments/patches welcome. 60 61=cut 62 63# Documentation continued at end of file 64 65my $VERSION = "1.23"; 66 67my $opt_tmpdir = $ENV{TMPDIR} || "/tmp"; 68 69my $OPTIONS = <<"_OPTIONS"; 70 71$0 Ver $VERSION 72 73Usage: $0 db_name[./table_regex/] [new_db_name | directory] 74 75 -?, --help display this help-screen and exit 76 -u, --user=# user for database login if not current user 77 -p, --password=# password to use when connecting to server (if not set 78 in my.cnf, which is recommended) 79 -h, --host=# hostname for local server when connecting over TCP/IP 80 -P, --port=# port to use when connecting to local server with TCP/IP 81 -S, --socket=# socket to use when connecting to local server 82 --old_server connect to old MySQL-server (before v5.5) which 83 doesn't have FLUSH TABLES WITH READ LOCK fully implemented. 84 85 --allowold don\'t abort if target dir already exists (rename it _old) 86 --addtodest don\'t rename target dir if it exists, just add files to it 87 --keepold don\'t delete previous (now renamed) target when done 88 --noindices don\'t include full index files in copy 89 --method=# method for copy (only "cp" currently supported) 90 91 -q, --quiet be silent except for errors 92 --debug enable debug 93 -n, --dryrun report actions without doing them 94 95 --regexp=# copy all databases with names matching regexp 96 --suffix=# suffix for names of copied databases 97 --checkpoint=# insert checkpoint entry into specified db.table 98 --flushlog flush logs once all tables are locked 99 --resetmaster reset the binlog once all tables are locked 100 --resetslave reset the master.info once all tables are locked 101 --tmpdir=# temporary directory (instead of $opt_tmpdir) 102 --record_log_pos=# record slave and master status in specified db.table 103 --chroot=# base directory of chroot jail in which mysqld operates 104 105 Try \'perldoc $0\' for more complete documentation 106_OPTIONS 107 108sub usage { 109 die @_, $OPTIONS; 110} 111 112# Do not initialize user or password options; that way, any user/password 113# options specified in option files will be used. If no values are specified 114# at all, the defaults will be used (login name, no password). 115 116my %opt = ( 117 noindices => 0, 118 allowold => 0, # for safety 119 keepold => 0, 120 method => "cp", 121 flushlog => 0, 122); 123Getopt::Long::Configure(qw(no_ignore_case)); # disambiguate -p and -P 124GetOptions( \%opt, 125 "help", 126 "host|h=s", 127 "user|u=s", 128 "password|p=s", 129 "port|P=s", 130 "socket|S=s", 131 "old_server", 132 "allowold!", 133 "keepold!", 134 "addtodest!", 135 "noindices!", 136 "method=s", 137 "debug", 138 "quiet|q", 139 "mv!", 140 "regexp=s", 141 "suffix=s", 142 "checkpoint=s", 143 "record_log_pos=s", 144 "flushlog", 145 "resetmaster", 146 "resetslave", 147 "tmpdir|t=s", 148 "dryrun|n", 149 "chroot=s", 150) or usage("Invalid option"); 151 152# @db_desc 153# ========== 154# a list of hash-refs containing: 155# 156# 'src' - name of the db to copy 157# 't_regex' - regex describing tables in src 158# 'target' - destination directory of the copy 159# 'tables' - array-ref to list of tables in the db 160# 'files' - array-ref to list of files to be copied 161# 'index' - array-ref to list of indexes to be copied 162# 163 164my @db_desc = (); 165my $tgt_name = undef; 166 167print STDERR "Warning: $0 is deprecated and will be removed in a future version.\n"; 168 169usage("") if ($opt{help}); 170 171if ( $opt{regexp} || $opt{suffix} || @ARGV > 2 ) { 172 $tgt_name = pop @ARGV unless ( exists $opt{suffix} ); 173 @db_desc = map { s{^([^\.]+)\./(.+)/$}{$1}; { 'src' => $_, 't_regex' => ( $2 ? $2 : '.*' ) } } @ARGV; 174} 175else { 176 usage("Database name to hotcopy not specified") unless ( @ARGV ); 177 178 $ARGV[0] =~ s{^([^\.]+)\./(.+)/$}{$1}; 179 @db_desc = ( { 'src' => $ARGV[0], 't_regex' => ( $2 ? $2 : '.*' ) } ); 180 181 if ( @ARGV == 2 ) { 182 $tgt_name = $ARGV[1]; 183 } 184 else { 185 $opt{suffix} = "_copy"; 186 } 187} 188 189my %mysqld_vars; 190my $start_time = time; 191$opt_tmpdir= $opt{tmpdir} if $opt{tmpdir}; 192$0 = $1 if $0 =~ m:/([^/]+)$:; 193$opt{quiet} = 0 if $opt{debug}; 194$opt{allowold} = 1 if $opt{keepold}; 195 196# --- connect to the database --- 197my $dsn; 198$dsn = ";host=" . (defined($opt{host}) ? $opt{host} : "localhost"); 199$dsn .= ";port=$opt{port}" if $opt{port}; 200$dsn .= ";mysql_socket=$opt{socket}" if $opt{socket}; 201 202# use mysql_read_default_group=mysqlhotcopy so that [client] and 203# [mysqlhotcopy] groups will be read from standard options files. 204 205my $dbh = DBI->connect("dbi:mysql:$dsn;mysql_read_default_group=mysqlhotcopy", 206 $opt{user}, $opt{password}, 207{ 208 RaiseError => 1, 209 PrintError => 0, 210 AutoCommit => 1, 211}); 212 213# --- check that checkpoint table exists if specified --- 214if ( $opt{checkpoint} ) { 215 $opt{checkpoint} = quote_names( $opt{checkpoint} ); 216 eval { $dbh->do( qq{ select time_stamp, src, dest, msg 217 from $opt{checkpoint} where 1 != 1} ); 218 }; 219 220 die "Error accessing Checkpoint table ($opt{checkpoint}): $@" 221 if ( $@ ); 222} 223 224# --- check that log_pos table exists if specified --- 225if ( $opt{record_log_pos} ) { 226 $opt{record_log_pos} = quote_names( $opt{record_log_pos} ); 227 228 eval { $dbh->do( qq{ select host, time_stamp, log_file, log_pos, master_host, master_log_file, master_log_pos 229 from $opt{record_log_pos} where 1 != 1} ); 230 }; 231 232 die "Error accessing log_pos table ($opt{record_log_pos}): $@" 233 if ( $@ ); 234} 235 236# --- get variables from database --- 237my $sth_vars = $dbh->prepare("show variables like 'datadir'"); 238$sth_vars->execute; 239while ( my ($var,$value) = $sth_vars->fetchrow_array ) { 240 $mysqld_vars{ $var } = $value; 241} 242my $datadir = $mysqld_vars{'datadir'} 243 || die "datadir not in mysqld variables"; 244 $datadir= $opt{chroot}.$datadir if ($opt{chroot}); 245$datadir =~ s:/$::; 246 247 248# --- get target path --- 249my ($tgt_dirname, $to_other_database); 250$to_other_database=0; 251if (defined($tgt_name) && $tgt_name =~ m:^\w+$: && @db_desc <= 1) 252{ 253 $tgt_dirname = "$datadir/$tgt_name"; 254 $to_other_database=1; 255} 256elsif (defined($tgt_name) && ($tgt_name =~ m:/: || $tgt_name eq '.')) { 257 $tgt_dirname = $tgt_name; 258} 259elsif ( $opt{suffix} ) { 260 print "Using copy suffix '$opt{suffix}'\n" unless $opt{quiet}; 261} 262else 263{ 264 $tgt_name="" if (!defined($tgt_name)); 265 die "Target '$tgt_name' doesn't look like a database name or directory path.\n"; 266} 267 268# --- resolve database names from regexp --- 269if ( defined $opt{regexp} ) { 270 my $t_regex = '.*'; 271 if ( $opt{regexp} =~ s{^/(.+)/\./(.+)/$}{$1} ) { 272 $t_regex = $2; 273 } 274 275 my $sth_dbs = $dbh->prepare("show databases"); 276 $sth_dbs->execute; 277 while ( my ($db_name) = $sth_dbs->fetchrow_array ) { 278 next if $db_name =~ m/^information_schema$/i; 279 push @db_desc, { 'src' => $db_name, 't_regex' => $t_regex } if ( $db_name =~ m/$opt{regexp}/o ); 280 } 281} 282 283# --- get list of tables and views to hotcopy --- 284 285my $hc_locks = ""; 286my $hc_tables = ""; 287my $hc_base_tables = ""; 288my $hc_views = ""; 289my $num_base_tables = 0; 290my $num_views = 0; 291my $num_tables = 0; 292my $num_files = 0; 293 294foreach my $rdb ( @db_desc ) { 295 my $db = $rdb->{src}; 296 my @dbh_base_tables = get_list_of_tables( $db ); 297 my @dbh_views = get_list_of_views( $db ); 298 299 ## filter out certain system non-lockable tables. 300 ## keep in sync with mysqldump. 301 if ($db =~ m/^mysql$/i) 302 { 303 @dbh_base_tables = grep 304 { !/^(apply_status|schema|general_log|slow_log)$/ } @dbh_base_tables 305 } 306 307 ## generate regex for tables/files 308 my $t_regex; 309 my $negated; 310 if ($rdb->{t_regex}) { 311 $t_regex = $rdb->{t_regex}; ## assign temporary regex 312 $negated = $t_regex =~ s/^~//; ## note and remove negation operator 313 314 $t_regex = qr/$t_regex/; ## make regex string from 315 ## user regex 316 317 ## filter (out) tables specified in t_regex 318 print "Filtering tables with '$t_regex'\n" if $opt{debug}; 319 @dbh_base_tables = ( $negated 320 ? grep { $_ !~ $t_regex } @dbh_base_tables 321 : grep { $_ =~ $t_regex } @dbh_base_tables ); 322 323 ## filter (out) views specified in t_regex 324 print "Filtering tables with '$t_regex'\n" if $opt{debug}; 325 @dbh_views = ( $negated 326 ? grep { $_ !~ $t_regex } @dbh_views 327 : grep { $_ =~ $t_regex } @dbh_views ); 328 } 329 330 ## Now concatenate the base table and view arrays. 331 my @dbh_tables = (@dbh_base_tables, @dbh_views); 332 333 ## get list of files to copy 334 my $db_dir = "$datadir/$db"; 335 opendir(DBDIR, $db_dir ) 336 or die "Cannot open dir '$db_dir': $!"; 337 338 my %db_files; 339 340 while ( defined( my $name = readdir DBDIR ) ) { 341 $db_files{$name} = $1 if ( $name =~ /(.+)\.\w+$/ ); 342 } 343 closedir( DBDIR ); 344 345 unless( keys %db_files ) { 346 warn "'$db' is an empty database\n"; 347 } 348 349 ## filter (out) files specified in t_regex 350 my @db_files; 351 if ($rdb->{t_regex}) { 352 @db_files = ($negated 353 ? grep { $db_files{$_} !~ $t_regex } keys %db_files 354 : grep { $db_files{$_} =~ $t_regex } keys %db_files ); 355 } 356 else { 357 @db_files = keys %db_files; 358 } 359 360 @db_files = sort @db_files; 361 362 my @index_files=(); 363 364 ## remove indices unless we're told to keep them 365 if ($opt{noindices}) { 366 @index_files= grep { /\.(ISM|MYI)$/ } @db_files; 367 @db_files = grep { not /\.(ISM|MYI)$/ } @db_files; 368 } 369 370 $rdb->{files} = [ @db_files ]; 371 $rdb->{index} = [ @index_files ]; 372 my @hc_base_tables = map { quote_names("$db.$_") } @dbh_base_tables; 373 my @hc_views = map { quote_names("$db.$_") } @dbh_views; 374 375 my @hc_tables = (@hc_base_tables, @hc_views); 376 $rdb->{tables} = [ @hc_tables ]; 377 378 $hc_locks .= ", " if ( length $hc_locks && @hc_tables ); 379 $hc_locks .= join ", ", map { "$_ READ" } @hc_tables; 380 381 $hc_base_tables .= ", " if ( length $hc_base_tables && @hc_base_tables ); 382 $hc_base_tables .= join ", ", @hc_base_tables; 383 $hc_views .= ", " if ( length $hc_views && @hc_views ); 384 $hc_views .= join " READ, ", @hc_views; 385 386 @hc_tables = (@hc_base_tables, @hc_views); 387 388 $num_base_tables += scalar @hc_base_tables; 389 $num_views += scalar @hc_views; 390 $num_tables += $num_base_tables + $num_views; 391 $num_files += scalar @{$rdb->{files}}; 392} 393 394# --- resolve targets for copies --- 395 396if (defined($tgt_name) && length $tgt_name ) { 397 # explicit destination directory specified 398 399 # GNU `cp -r` error message 400 die "copying multiple databases, but last argument ($tgt_dirname) is not a directory\n" 401 if ( @db_desc > 1 && !(-e $tgt_dirname && -d $tgt_dirname ) ); 402 403 if ($to_other_database) 404 { 405 foreach my $rdb ( @db_desc ) { 406 $rdb->{target} = "$tgt_dirname"; 407 } 408 } 409 elsif ($opt{method} =~ /^scp\b/) 410 { # we have to trust scp to hit the target 411 foreach my $rdb ( @db_desc ) { 412 $rdb->{target} = "$tgt_dirname/$rdb->{src}"; 413 } 414 } 415 else 416 { 417 die "Last argument ($tgt_dirname) is not a directory\n" 418 if (!(-e $tgt_dirname && -d $tgt_dirname ) ); 419 foreach my $rdb ( @db_desc ) { 420 $rdb->{target} = "$tgt_dirname/$rdb->{src}"; 421 } 422 } 423 } 424else { 425 die "Error: expected \$opt{suffix} to exist" unless ( exists $opt{suffix} ); 426 427 foreach my $rdb ( @db_desc ) { 428 $rdb->{target} = "$datadir/$rdb->{src}$opt{suffix}"; 429 } 430} 431 432print Dumper( \@db_desc ) if ( $opt{debug} ); 433 434# --- bail out if all specified databases are empty --- 435 436die "No tables to hot-copy" unless ( length $hc_locks ); 437 438# --- create target directories if we are using 'cp' --- 439 440my @existing = (); 441 442if ($opt{method} =~ /^cp\b/) 443{ 444 foreach my $rdb ( @db_desc ) { 445 push @existing, $rdb->{target} if ( -d $rdb->{target} ); 446 } 447 448 if ( @existing && !($opt{allowold} || $opt{addtodest}) ) 449 { 450 $dbh->disconnect(); 451 die "Can't hotcopy to '", join( "','", @existing ), "' because directory\nalready exist and the --allowold or --addtodest options were not given.\n" 452 } 453} 454 455retire_directory( @existing ) if @existing && !$opt{addtodest}; 456 457foreach my $rdb ( @db_desc ) { 458 my $tgt_dirpath = "$rdb->{target}"; 459 # Remove trailing slashes (needed for Mac OS X) 460 substr($tgt_dirpath, 1) =~ s|/+$||; 461 if ( $opt{dryrun} ) { 462 print "mkdir $tgt_dirpath, 0750\n"; 463 } 464 elsif ($opt{method} =~ /^scp\b/) { 465 ## assume it's there? 466 ## ... 467 } 468 else { 469 mkdir($tgt_dirpath, 0750) or die "Can't create '$tgt_dirpath': $!\n" 470 unless -d $tgt_dirpath; 471 my @f_info= stat "$datadir/$rdb->{src}"; 472 chown $f_info[4], $f_info[5], $tgt_dirpath; 473 } 474} 475 476############################## 477# --- PERFORM THE HOT-COPY --- 478# 479# Note that we try to keep the time between the LOCK and the UNLOCK 480# as short as possible, and only start when we know that we should 481# be able to complete without error. 482 483# read lock all the tables we'll be copying 484# in order to get a consistent snapshot of the database 485 486if ( $opt{checkpoint} || $opt{record_log_pos} ) { 487 # convert existing READ lock on checkpoint and/or log_pos table into WRITE lock 488 foreach my $table ( grep { defined } ( $opt{checkpoint}, $opt{record_log_pos} ) ) { 489 $hc_locks .= ", $table WRITE" 490 unless ( $hc_locks =~ s/$table\s+READ/$table WRITE/ ); 491 } 492} 493 494my $hc_started = time; # count from time lock is granted 495 496if ( $opt{dryrun} ) { 497 if ( $opt{old_server} ) { 498 print "LOCK TABLES $hc_locks\n"; 499 print "FLUSH TABLES /*!32323 $hc_tables */\n"; 500 } 501 else { 502 # Lock base tables and views separately. 503 print "FLUSH TABLES $hc_base_tables WITH READ LOCK\n" 504 if ( $hc_base_tables ); 505 print "LOCK TABLES $hc_views READ\n" if ( $hc_views ); 506 } 507 508 print "FLUSH LOGS\n" if ( $opt{flushlog} ); 509 print "RESET MASTER\n" if ( $opt{resetmaster} ); 510 print "RESET SLAVE\n" if ( $opt{resetslave} ); 511} 512else { 513 my $start = time; 514 if ( $opt{old_server} ) { 515 $dbh->do("LOCK TABLES $hc_locks"); 516 printf "Locked $num_tables tables in %d seconds.\n", time-$start unless $opt{quiet}; 517 $hc_started = time; # count from time lock is granted 518 519 # flush tables to make on-disk copy up to date 520 $start = time; 521 $dbh->do("FLUSH TABLES /*!32323 $hc_tables */"); 522 printf "Flushed tables ($hc_tables) in %d seconds.\n", time-$start unless $opt{quiet}; 523 } 524 else { 525 # Lock base tables and views separately, as 'FLUSH TABLES <tbl_name> 526 # ... WITH READ LOCK' (introduced in 5.5) would fail for views. 527 # Also, flush tables to make on-disk copy up to date 528 $dbh->do("FLUSH TABLES $hc_base_tables WITH READ LOCK") 529 if ( $hc_base_tables ); 530 printf "Flushed $num_base_tables tables with read lock ($hc_base_tables) in %d seconds.\n", 531 time-$start unless $opt{quiet}; 532 533 $start = time; 534 $dbh->do("LOCK TABLES $hc_views READ") if ( $hc_views ); 535 printf "Locked $num_views views ($hc_views) in %d seconds.\n", 536 time-$start unless $opt{quiet}; 537 538 $hc_started = time; # count from time lock is granted 539 } 540 $dbh->do( "FLUSH LOGS" ) if ( $opt{flushlog} ); 541 $dbh->do( "RESET MASTER" ) if ( $opt{resetmaster} ); 542 $dbh->do( "RESET SLAVE" ) if ( $opt{resetslave} ); 543 544 if ( $opt{record_log_pos} ) { 545 record_log_pos( $dbh, $opt{record_log_pos} ); 546 $dbh->do("FLUSH TABLES /*!32323 $hc_tables */"); 547 } 548} 549 550my @failed = (); 551 552foreach my $rdb ( @db_desc ) 553{ 554 my @files = map { "$datadir/$rdb->{src}/$_" } @{$rdb->{files}}; 555 next unless @files; 556 557 eval { copy_files($opt{method}, \@files, $rdb->{target}); }; 558 push @failed, "$rdb->{src} -> $rdb->{target} failed: $@" 559 if ( $@ ); 560 561 @files = @{$rdb->{index}}; 562 if ($rdb->{index}) 563 { 564 copy_index($opt{method}, \@files, 565 "$datadir/$rdb->{src}", $rdb->{target} ); 566 } 567 568 if ( $opt{checkpoint} ) { 569 my $msg = ( $@ ) ? "Failed: $@" : "Succeeded"; 570 571 eval { 572 $dbh->do( qq{ insert into $opt{checkpoint} (src, dest, msg) 573 VALUES ( '$rdb->{src}', '$rdb->{target}', '$msg' ) 574 } ); 575 }; 576 577 if ( $@ ) { 578 warn "Failed to update checkpoint table: $@\n"; 579 } 580 } 581} 582 583if ( $opt{dryrun} ) { 584 print "UNLOCK TABLES\n"; 585 if ( @existing && !$opt{keepold} ) { 586 my @oldies = map { $_ . '_old' } @existing; 587 print "rm -rf @oldies\n" 588 } 589 $dbh->disconnect(); 590 exit(0); 591} 592else { 593 $dbh->do("UNLOCK TABLES"); 594} 595 596my $hc_dur = time - $hc_started; 597printf "Unlocked tables.\n" unless $opt{quiet}; 598 599# 600# --- HOT-COPY COMPLETE --- 601########################### 602 603$dbh->disconnect; 604 605if ( @failed ) { 606 # hotcopy failed - cleanup 607 # delete any @targets 608 # rename _old copy back to original 609 610 my @targets = (); 611 foreach my $rdb ( @db_desc ) { 612 push @targets, $rdb->{target} if ( -d $rdb->{target} ); 613 } 614 print "Deleting @targets \n" if $opt{debug}; 615 616 print "Deleting @targets \n" if $opt{debug}; 617 rmtree([@targets]); 618 if (@existing) { 619 print "Restoring @existing from back-up\n" if $opt{debug}; 620 foreach my $dir ( @existing ) { 621 rename("${dir}_old", $dir ) 622 or warn "Can't rename ${dir}_old to $dir: $!\n"; 623 } 624 } 625 626 die join( "\n", @failed ); 627} 628else { 629 # hotcopy worked 630 # delete _old unless $opt{keepold} 631 632 if ( @existing && !$opt{keepold} ) { 633 my @oldies = map { $_ . '_old' } @existing; 634 print "Deleting previous copy in @oldies\n" if $opt{debug}; 635 rmtree([@oldies]); 636 } 637 638 printf "$0 copied %d tables (%d files) in %d second%s (%d seconds overall).\n", 639 $num_tables, $num_files, 640 $hc_dur, ($hc_dur==1)?"":"s", time - $start_time 641 unless $opt{quiet}; 642} 643 644exit 0; 645 646 647# --- 648 649sub copy_files { 650 my ($method, $files, $target) = @_; 651 my @cmd; 652 print "Copying ".@$files." files...\n" unless $opt{quiet}; 653 654 if ($method =~ /^s?cp\b/) # cp or scp with optional flags 655 { 656 my $cp = $method; 657 # add option to preserve mod time etc of copied files 658 # not critical, but nice to have 659 $cp.= " -p" if $^O =~ m/^(solaris|linux|freebsd|darwin)$/; 660 661 # add recursive option for scp 662 $cp.= " -r" if $^O =~ /m^(solaris|linux|freebsd|darwin)$/ && $method =~ /^scp\b/; 663 664 # perform the actual copy 665 safe_system( $cp, (map { "'$_'" } @$files), "'$target'" ); 666 } 667 else 668 { 669 die "Can't use unsupported method '$method'\n"; 670 } 671} 672 673# 674# Copy only the header of the index file 675# 676 677sub copy_index 678{ 679 my ($method, $files, $source, $target) = @_; 680 681 print "Copying indices for ".@$files." files...\n" unless $opt{quiet}; 682 foreach my $file (@$files) 683 { 684 my $from="$source/$file"; 685 my $to="$target/$file"; 686 my $buff; 687 open(INPUT, "<$from") || die "Can't open file $from: $!\n"; 688 binmode(INPUT, ":raw"); 689 my $length=read INPUT, $buff, 2048; 690 die "Can't read index header from $from\n" if ($length < 1024); 691 close INPUT; 692 693 if ( $opt{dryrun} ) 694 { 695 print "$opt{method}-header $from $to\n"; 696 } 697 elsif ($opt{method} eq 'cp') 698 { 699 open(OUTPUT,">$to") || die "Can\'t create file $to: $!\n"; 700 if (syswrite(OUTPUT,$buff) != length($buff)) 701 { 702 die "Error when writing data to $to: $!\n"; 703 } 704 close OUTPUT || die "Error on close of $to: $!\n"; 705 } 706 elsif ($opt{method} =~ /^scp\b/) 707 { 708 my ($fh, $tmp)= tempfile('mysqlhotcopy-XXXXXX', DIR => $opt_tmpdir) or 709 die "Can\'t create/open file in $opt_tmpdir\n"; 710 if (syswrite($fh,$buff) != length($buff)) 711 { 712 die "Error when writing data to $tmp: $!\n"; 713 } 714 close $fh || die "Error on close of $tmp: $!\n"; 715 safe_system("$opt{method} $tmp $to"); 716 unlink $tmp; 717 } 718 else 719 { 720 die "Can't use unsupported method '$opt{method}'\n"; 721 } 722 } 723} 724 725 726sub safe_system { 727 my @sources= @_; 728 my $method= shift @sources; 729 my $target= pop @sources; 730 ## @sources = list of source file names 731 732 ## We have to deal with very long command lines, otherwise they may generate 733 ## "Argument list too long". 734 ## With 10000 tables the command line can be around 1MB, much more than 128kB 735 ## which is the common limit on Linux (can be read from 736 ## /usr/src/linux/include/linux/binfmts.h 737 ## see http://www.linuxjournal.com/article.php?sid=6060). 738 739 my $chunk_limit= 100 * 1024; # 100 kB 740 my @chunk= (); 741 my $chunk_length= 0; 742 foreach (@sources) { 743 push @chunk, $_; 744 $chunk_length+= length($_); 745 if ($chunk_length > $chunk_limit) { 746 safe_simple_system($method, @chunk, $target); 747 @chunk=(); 748 $chunk_length= 0; 749 } 750 } 751 if ($chunk_length > 0) { # do not forget last small chunk 752 safe_simple_system($method, @chunk, $target); 753 } 754} 755 756sub safe_simple_system { 757 my @cmd= @_; 758 759 if ( $opt{dryrun} ) { 760 print "@cmd\n"; 761 } 762 else { 763 ## for some reason system fails but backticks works ok for scp... 764 print "Executing '@cmd'\n" if $opt{debug}; 765 my $cp_status = system "@cmd > /dev/null"; 766 if ($cp_status != 0) { 767 warn "Executing command failed ($cp_status). Trying backtick execution...\n"; 768 ## try something else 769 `@cmd` || die "Error: @cmd failed ($?) while copying files.\n"; 770 } 771 } 772} 773 774sub retire_directory { 775 my ( @dir ) = @_; 776 777 foreach my $dir ( @dir ) { 778 my $tgt_oldpath = $dir . '_old'; 779 if ( $opt{dryrun} ) { 780 print "rmtree $tgt_oldpath\n" if ( -d $tgt_oldpath ); 781 print "rename $dir, $tgt_oldpath\n"; 782 next; 783 } 784 785 if ( -d $tgt_oldpath ) { 786 print "Deleting previous 'old' hotcopy directory ('$tgt_oldpath')\n" unless $opt{quiet}; 787 rmtree([$tgt_oldpath],0,1); 788 } 789 rename($dir, $tgt_oldpath) 790 or die "Can't rename $dir=>$tgt_oldpath: $!\n"; 791 print "Existing hotcopy directory renamed to '$tgt_oldpath'\n" unless $opt{quiet}; 792 } 793} 794 795sub record_log_pos { 796 my ( $dbh, $table_name ) = @_; 797 798 eval { 799 my ($file,$position) = get_row( $dbh, "show master status" ); 800 die "master status is undefined" if !defined $file || !defined $position; 801 802 my $row_hash = get_row_hash( $dbh, "show slave status" ); 803 my ($master_host, $log_file, $log_pos ); 804 if ( $dbh->{mysql_serverinfo} =~ /^3\.23/ ) { 805 ($master_host, $log_file, $log_pos ) 806 = @{$row_hash}{ qw / Master_Host Log_File Pos / }; 807 } else { 808 ($master_host, $log_file, $log_pos ) 809 = @{$row_hash}{ qw / Master_Host Relay_Master_Log_File Exec_Master_Log_Pos / }; 810 } 811 my $hostname = hostname(); 812 813 $dbh->do( qq{ replace into $table_name 814 set host=?, log_file=?, log_pos=?, 815 master_host=?, master_log_file=?, master_log_pos=? }, 816 undef, 817 $hostname, $file, $position, 818 $master_host, $log_file, $log_pos ); 819 820 }; 821 822 if ( $@ ) { 823 warn "Failed to store master position: $@\n"; 824 } 825} 826 827sub get_row { 828 my ( $dbh, $sql ) = @_; 829 830 my $sth = $dbh->prepare($sql); 831 $sth->execute; 832 return $sth->fetchrow_array(); 833} 834 835sub get_row_hash { 836 my ( $dbh, $sql ) = @_; 837 838 my $sth = $dbh->prepare($sql); 839 $sth->execute; 840 return $sth->fetchrow_hashref(); 841} 842 843sub get_list_of_tables { 844 my ( $db ) = @_; 845 846 my $tables = 847 eval { 848 $dbh->selectall_arrayref('SHOW FULL TABLES FROM ' . 849 $dbh->quote_identifier($db) . 850 ' WHERE Table_type = \'BASE TABLE\'') 851 } || []; 852 warn "Unable to retrieve list of tables in $db: $@" if $@; 853 854 return (map { $_->[0] } @$tables); 855} 856 857sub get_list_of_views { 858 my ( $db ) = @_; 859 860 my $views = 861 eval { 862 $dbh->selectall_arrayref('SHOW FULL TABLES FROM ' . 863 $dbh->quote_identifier($db) . 864 ' WHERE Table_type = \'VIEW\'') 865 } || []; 866 warn "Unable to retrieve list of views in $db: $@" if $@; 867 868 return (map { $_->[0] } @$views); 869} 870 871sub quote_names { 872 my ( $name ) = @_; 873 # given a db.table name, add quotes 874 875 my ($db, $table, @cruft) = split( /\./, $name ); 876 die "Invalid db.table name '$name'" if (@cruft || !defined $db || !defined $table ); 877 878 # Earlier versions of DBD return table name non-quoted, 879 # such as DBD-2.1012 and the newer ones, such as DBD-2.9002 880 # returns it quoted. Let's have a support for both. 881 $table=~ s/\`//g; 882 return "`$db`.`$table`"; 883} 884 885__END__ 886 887=head1 DESCRIPTION 888 889mysqlhotcopy is designed to make stable copies of live MySQL databases. 890 891Here "live" means that the database server is running and the database 892may be in active use. And "stable" means that the copy will not have 893any corruptions that could occur if the table files were simply copied 894without first being locked and flushed from within the server. 895 896=head1 OPTIONS 897 898=over 4 899 900=item --checkpoint checkpoint-table 901 902As each database is copied, an entry is written to the specified 903checkpoint-table. This has the happy side-effect of updating the 904MySQL update-log (if it is switched on) giving a good indication of 905where roll-forward should begin for backup+rollforward schemes. 906 907The name of the checkpoint table should be supplied in database.table format. 908The checkpoint-table must contain at least the following fields: 909 910=over 4 911 912 time_stamp timestamp not null 913 src varchar(32) 914 dest varchar(60) 915 msg varchar(255) 916 917=back 918 919=item --record_log_pos log-pos-table 920 921Just before the database files are copied, update the record in the 922log-pos-table from the values returned from "show master status" and 923"show slave status". The master status values are stored in the 924log_file and log_pos columns, and establish the position in the binary 925logs that any slaves of this host should adopt if initialised from 926this dump. The slave status values are stored in master_host, 927master_log_file, and master_log_pos, corresponding to the coordinates 928of the next to the last event the slave has executed. The slave or its 929siblings can connect to the master next time and request replication 930starting from the recorded values. 931 932The name of the log-pos table should be supplied in database.table format. 933A sample log-pos table definition: 934 935=over 4 936 937CREATE TABLE log_pos ( 938 host varchar(60) NOT null, 939 time_stamp timestamp(14) NOT NULL, 940 log_file varchar(32) default NULL, 941 log_pos int(11) default NULL, 942 master_host varchar(60) NULL, 943 master_log_file varchar(32) NULL, 944 master_log_pos int NULL, 945 946 PRIMARY KEY (host) 947); 948 949=back 950 951 952=item --suffix suffix 953 954Each database is copied back into the originating datadir under 955a new name. The new name is the original name with the suffix 956appended. 957 958If only a single db_name is supplied and the --suffix flag is not 959supplied, then "--suffix=_copy" is assumed. 960 961=item --allowold 962 963Move any existing version of the destination to a backup directory for 964the duration of the copy. If the copy successfully completes, the backup 965directory is deleted - unless the --keepold flag is set. If the copy fails, 966the backup directory is restored. 967 968The backup directory name is the original name with "_old" appended. 969Any existing versions of the backup directory are deleted. 970 971=item --keepold 972 973Behaves as for the --allowold, with the additional feature 974of keeping the backup directory after the copy successfully completes. 975 976=item --addtodest 977 978Don't rename target directory if it already exists, just add the 979copied files into it. 980 981This is most useful when backing up a database with many large 982tables and you don't want to have all the tables locked for the 983whole duration. 984 985In this situation, I<if> you are happy for groups of tables to be 986backed up separately (and thus possibly not be logically consistent 987with one another) then you can run mysqlhotcopy several times on 988the same database each with different db_name./table_regex/. 989All but the first should use the --addtodest option so the tables 990all end up in the same directory. 991 992=item --flushlog 993 994Rotate the log files by executing "FLUSH LOGS" after all tables are 995locked, and before they are copied. 996 997=item --resetmaster 998 999Reset the bin-log by executing "RESET MASTER" after all tables are 1000locked, and before they are copied. Useful if you are recovering a 1001slave in a replication setup. 1002 1003=item --resetslave 1004 1005Reset the master.info by executing "RESET SLAVE" after all tables are 1006locked, and before they are copied. Useful if you are recovering a 1007server in a mutual replication setup. 1008 1009=item --regexp pattern 1010 1011Copy all databases with names matching the pattern. 1012 1013=item --regexp /pattern1/./pattern2/ 1014 1015Copy all tables with names matching pattern2 from all databases with 1016names matching pattern1. For example, to select all tables which 1017names begin with 'bar' from all databases which names end with 'foo': 1018 1019 mysqlhotcopy --indices --method=cp --regexp /foo$/./^bar/ 1020 1021=item db_name./pattern/ 1022 1023Copy only tables matching pattern. Shell metacharacters ( (, ), |, !, 1024etc.) have to be escaped (e.g., \). For example, to select all tables 1025in database db1 whose names begin with 'foo' or 'bar': 1026 1027 mysqlhotcopy --indices --method=cp db1./^\(foo\|bar\)/ 1028 1029=item db_name./~pattern/ 1030 1031Copy only tables not matching pattern. For example, to copy tables 1032that do not begin with foo nor bar: 1033 1034 mysqlhotcopy --indices --method=cp db1./~^\(foo\|bar\)/ 1035 1036=item -?, --help 1037 1038Display help-screen and exit. 1039 1040=item -u, --user=# 1041 1042User for database login if not current user. 1043 1044=item -p, --password=# 1045 1046Password to use when connecting to the server. Note that you are strongly 1047encouraged *not* to use this option as every user would be able to see the 1048password in the process list. Instead use the '[mysqlhotcopy]' section in 1049one of the config files, normally /etc/my.cnf or your personal ~/.my.cnf. 1050(See the chapter 'my.cnf Option Files' in the manual.) 1051 1052=item -h, -h, --host=# 1053 1054Hostname for local server when connecting over TCP/IP. By specifying this 1055different from 'localhost' will trigger mysqlhotcopy to use TCP/IP connection. 1056 1057=item -P, --port=# 1058 1059Port to use when connecting to MySQL server with TCP/IP. This is only used 1060when using the --host option. 1061 1062=item -S, --socket=# 1063 1064UNIX domain socket to use when connecting to local server. 1065 1066=item --old_server 1067 1068Use old server (pre v5.5) commands. 1069 1070=item --noindices 1071 1072Don\'t include index files in copy. Only up to the first 2048 bytes 1073are copied; You can restore the indexes with isamchk -r or myisamchk -r 1074on the backup. 1075 1076=item --method=# 1077 1078Method for copy (only "cp" currently supported). Alpha support for 1079"scp" was added in November 2000. Your experience with the scp method 1080will vary with your ability to understand how scp works. 'man scp' 1081and 'man ssh' are your friends. 1082 1083The destination directory _must exist_ on the target machine using the 1084scp method. --keepold and --allowold are meaningless with scp. 1085Liberal use of the --debug option will help you figure out what\'s 1086really going on when you do an scp. 1087 1088Note that using scp will lock your tables for a _long_ time unless 1089your network connection is _fast_. If this is unacceptable to you, 1090use the 'cp' method to copy the tables to some temporary area and then 1091scp or rsync the files at your leisure. 1092 1093=item -q, --quiet 1094 1095Be silent except for errors. 1096 1097=item --debug 1098 1099Debug messages are displayed. 1100 1101=item -n, --dryrun 1102 1103Display commands without actually doing them. 1104 1105=back 1106 1107=head1 WARRANTY 1108 1109This software is free and comes without warranty of any kind. You 1110should never trust backup software without studying the code yourself. 1111Study the code inside this script and only rely on it if I<you> believe 1112that it does the right thing for you. 1113 1114Patches adding bug fixes, documentation and new features are welcome. 1115Please send these to internals@lists.mysql.com. 1116 1117=head1 TO DO 1118 1119Extend the individual table copy to allow multiple subsets of tables 1120to be specified on the command line: 1121 1122 mysqlhotcopy db newdb t1 t2 /^foo_/ : t3 /^bar_/ : + 1123 1124where ":" delimits the subsets, the /^foo_/ indicates all tables 1125with names beginning with "foo_" and the "+" indicates all tables 1126not copied by the previous subsets. 1127 1128'newdb' is either the name of the new database, or the full path name 1129of the new database file. The database should not already exist. 1130 1131Add option to lock each table in turn for people who don\'t need 1132cross-table integrity. 1133 1134Add option to FLUSH STATUS just before UNLOCK TABLES. 1135 1136Add support for other copy methods (e.g., tar to single file?). 1137 1138Add support for forthcoming MySQL ``RAID'' table subdirectory layouts. 1139 1140=head1 AUTHOR 1141 1142Tim Bunce 1143 1144Martin Waite - Added checkpoint, flushlog, regexp and dryrun options. 1145 Fixed cleanup of targets when hotcopy fails. 1146 Added --record_log_pos. 1147 RAID tables are now copied (don't know if this works over scp). 1148 1149Ralph Corderoy - Added synonyms for commands. 1150 1151Scott Wiersdorf - Added table regex and scp support. 1152 1153Monty - Working --noindex (copy only first 2048 bytes of index file). 1154 Fixes for --method=scp. 1155 1156Ask Bjoern Hansen - Cleanup code to fix a few bugs and enable -w again. 1157 1158Emil S. Hansen - Added resetslave and resetmaster. 1159 1160Jeremy D. Zawodny - Removed deprecated DBI calls. Fixed bug which 1161resulted in nothing being copied when a regexp was specified but no 1162database name(s). 1163 1164Martin Waite - Fix to handle database name that contains space. 1165 1166Paul DuBois - Remove end '/' from directory names. 1167