~drizzle-trunk/drizzle/development

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