~drizzle-trunk/drizzle/development

« back to all changes in this revision

Viewing changes to scripts/mysqlhotcopy.sh

  • Committer: brian
  • Date: 2008-06-25 05:29:13 UTC
  • Revision ID: brian@localhost.localdomain-20080625052913-6upwo0jsrl4lnapl
clean slate

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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