~drizzle-trunk/drizzle/development

« back to all changes in this revision

Viewing changes to scripts/mysqlhotcopy.sh

  • Committer: Brian Aker
  • Date: 2008-07-08 21:36:11 UTC
  • mfrom: (77.1.34 codestyle)
  • Revision ID: brian@tangent.org-20080708213611-b0k2zy8eldttqct3
Merging up Monty's changes

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