Imported Upstream version 3.3.3
[debian/amanda] / installcheck / ampgsql.pl
1 # Copyright (c) 2009-2012 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use Test::More tests => 74;
21
22 use lib "@amperldir@";
23 use strict;
24 use warnings;
25 use Amanda::Constants;
26 use Amanda::Paths;
27 use Amanda::Debug qw( debug );
28 use Amanda::Util;
29 use File::Path;
30 use Installcheck;
31 use Installcheck::Application;
32 use Installcheck::Config;
33 use Installcheck::Run;
34 use IPC::Open3;
35
36 Amanda::Debug::dbopen("installcheck");
37 Installcheck::log_test_output();
38 my $debug = !exists $ENV{'HARNESS_ACTIVE'};
39
40 sub skip_all {
41     my $reason = shift @_;
42     SKIP: {
43         skip($reason, Test::More->builder->expected_tests);
44     }
45     exit 0;
46 }
47
48 skip_all("GNU tar is not available")
49     unless ($Amanda::Constants::GNUTAR and -x $Amanda::Constants::GNUTAR);
50
51 my $postgres_prefix = $ENV{'INSTALLCHECK_POSTGRES_PREFIX'};
52 skip_all("Set INSTALLCHECK_POSTGRES_PREFIX to run tests") unless $postgres_prefix;
53
54 sub get_pg_version {
55     my $verout = Installcheck::Run::run_get("$postgres_prefix/bin/psql", "-X", "--version");
56     my @lines = split(/\n/, $verout);
57     my ($maj, $min, $pat) = ($lines[0] =~ / ([0-9]+)\.([0-9]+)\.([0-9]+)$/);
58     return $maj * 10000 + $min * 100 + $pat;
59 }
60 my $pg_version = get_pg_version();
61
62 my $SIGINT = 2;
63 my $DB_NAME = "installcheck";
64 my $root_dir = "$Installcheck::TMP/ampgsql";
65 my $data_dir = "$root_dir/data";
66 my $config_file = "$data_dir/postgresql.conf";
67 my $recovery_conf_file = "$data_dir/recovery.conf";
68 my $recovery_done_file = "$data_dir/recovery.done";
69 my $socket_dir = "$root_dir/sockets";
70 my $archive_dir = "$root_dir/archive";
71 my $tmp_dir = "$root_dir/tmp";
72 my $log_dir = "$Installcheck::TMP/ampgsql";
73 my $state_dir = "$root_dir/state";
74
75 sub dbg {
76     my ($msg) = @_;
77     if ($debug) {
78         diag($msg);
79     } else {
80         debug($msg);
81     }
82 }
83
84 # run a command with output sent to the debug log
85 sub run_and_log {
86     my ($in_str, $prog, @args) = @_;
87     local *IN;
88
89     debug("running $prog " . join(" ", @args));
90     debug(".. with input '$in_str'") if $in_str;
91
92     my $dbfd = Amanda::Debug::dbfd();
93     my $pid = open3(\*IN, ">&$dbfd", ">&$dbfd", $prog, @args);
94     print IN $in_str;
95     close(IN);
96     waitpid($pid, 0);
97     my $status = $? >> 8;
98     debug("..exit status $status");
99
100     return $status;
101 }
102
103 # run a sub and report on the result; mostly used for setup/teardown
104 sub try_eval {
105     my ($desc, $code, @args) = @_;
106     my $err_str;
107     $err_str = "$@" unless eval {$code->(@args); 1;};
108     ok(!$err_str, $desc) or diag($err_str);
109 }
110
111 sub write_config_file {
112     my ($filename, $cfg) = @_;
113
114     unlink $filename if -f $filename;
115     Amanda::Util::burp($filename, $cfg);
116 }
117
118 # run $code while the postmaster is started, shutting down the postmaster afterward,
119 # logging the result to our debug file
120 sub do_postmaster {
121     my $code = shift @_;
122     my $pidfile = "$data_dir/postmaster.pid";
123     local *IN;
124
125     die "postmaster already running"
126         if -f $pidfile;
127
128     dbg("starting postmaster..");
129
130     my $dbfd = Amanda::Debug::dbfd();
131     my $pid = open3(\*IN, ">&$dbfd", ">&$dbfd",
132             "$postgres_prefix/bin/postmaster", "-D", $data_dir);
133     close(IN);
134
135     # busy-wait for the pidfile to be created, for up to 120s
136     my $ticks = 0;
137     while (!-f $pidfile) {
138         die "postmaster did not start"
139             if ($ticks++ > 120 or !kill 0, $pid);
140         dbg("waiting for postmaster to write its pid");
141         sleep(1);
142     }
143
144     # and finish out those 120 seconds waiting for the db to actually
145     # be ready to roll, using psql -l just like pg_ctl does
146     while ($ticks++ < 120) {
147         local *IN;
148
149         my $psqlpid = open3(\*IN, ">&$dbfd", ">&$dbfd",
150             "$postgres_prefix/bin/psql", "-X", "-h", $socket_dir, "-l");
151         close IN;
152         waitpid($psqlpid, 0);
153         last if (($? >> 8) == 0);
154         sleep(1);
155     }
156
157     if ($ticks == 120) {
158         die("postmaster never started");
159     }
160
161     # use eval to be careful to shut down postgres
162     eval { $code->() if $code };
163     my $err = $@;
164
165     # kill the postmaster and wait for it to die
166     kill $SIGINT, $pid;
167     waitpid($pid, 0);
168     my $status = $? >> 8;
169     dbg("postmaster stopped");
170
171     die "postmaster pid file still exists"
172         if -f $pidfile;
173
174     die $err if $err;
175 }
176
177 # count the WAL files.  Note that this may count some extra stuff, too, but
178 # since it's only used to see the number of WALs *increase*, that's OK.
179 sub count_wals {
180     my @files = glob("$archive_dir/*");
181     if (@files) {
182         debug("WAL files in archive_dir: " . join(" ", @files));
183     } else {
184         debug("No WAL files in archive_dir");
185     }
186     return scalar @files;
187 }
188
189 sub ls_backup_data {
190     my ($level, $backup) = @_;
191
192     my $tmpdir = "$Installcheck::TMP/backup_data";
193     -d $tmpdir && rmtree $tmpdir;
194     mkpath $tmpdir;
195
196     if ($level > 0) {
197         debug("contents of level-$level backup:");
198         Amanda::Util::burp("$tmpdir/backup.tar", $backup->{'data'});
199         run_and_log("", $Amanda::Constants::GNUTAR, "-tvf", "$tmpdir/backup.tar");
200     } else {
201         debug("contents of level-0 backup:");
202         Amanda::Util::burp("$tmpdir/backup.tar", $backup->{'data'});
203         run_and_log("", $Amanda::Constants::GNUTAR, "-C", $tmpdir, "-xvf", "$tmpdir/backup.tar");
204         debug(".. archive_dir.tar contains:");
205         run_and_log("", $Amanda::Constants::GNUTAR, "-tvf", "$tmpdir/archive_dir.tar");
206         debug(".. data_dir.tar looks like:\n" . `ls -l "$tmpdir/data_dir.tar"`);
207     }
208
209     rmtree $tmpdir;
210 }
211
212 # set up all of our dirs
213 try_eval("emptied root_dir", \&rmtree, $root_dir);
214 try_eval("created archive_dir", \&mkpath, $archive_dir);
215 try_eval("created data_dir", \&mkpath, $data_dir);
216 try_eval("created socket_dir", \&mkpath, $socket_dir);
217 try_eval("created log_dir", \&mkpath, $log_dir);
218 try_eval("created state_dir", \&mkpath, $state_dir);
219
220 # create an amanda config for the application
221 my $conf = Installcheck::Config->new();
222 $conf->add_client_param('property', "\"PG-DATADIR\" \"$data_dir\"");
223 $conf->add_client_param('property', "\"PG-ARCHIVEDIR\" \"$archive_dir\"");
224 $conf->add_client_param('property', "\"PG-CLEANUPWAL\" \"yes\"");
225 $conf->add_client_param('property', "\"PG-HOST\" \"$socket_dir\"");
226 $conf->add_client_param('property', "\"PSQL-PATH\" \"$postgres_prefix/bin/psql\"");
227 $conf->write();
228
229 # set up the database
230 dbg("creating initial database");
231 run_and_log("", "$postgres_prefix/bin/initdb", "-D", "$data_dir")
232     and die("error running initdb");
233
234 # enable archive mode for 8.3 and higher
235 my $archive_mode = '';
236 if ($pg_version >= 80300) {
237     $archive_mode = 'archive_mode = on';
238 }
239
240 # write the postgres config file
241 write_config_file $config_file, <<EOF;
242 listen_addresses = ''
243 unix_socket_directory = '$socket_dir'
244 archive_command = 'test ! -f $archive_dir/%f && cp %p $archive_dir/%f'
245 $archive_mode
246 log_destination = 'stderr'
247 # checkpoint every 30 seconds (this is the minimum)
248 checkpoint_timeout = 30
249 # and don't warn me about that
250 checkpoint_warning = 0
251 # and keep 50 segments
252 checkpoint_segments = 50
253 # and bundle commits up to one minute
254 commit_delay = 60
255 EOF
256
257 my $app = new Installcheck::Application("ampgsql");
258 $app->add_property('statedir', $state_dir);
259 $app->add_property('tmpdir', $tmp_dir);
260
261 # take three backups: a level 0, level 1, and level 2.  The level 2 has
262 # no database changes, so it contains no WAL files.
263 my ($backup, $backup_incr, $backup_incr_empty);
264 sub setup_db_and_backup {
265     my $i;
266
267     run_and_log("", "$postgres_prefix/bin/createdb", "-h", $socket_dir, $DB_NAME);
268     pass("created db");
269
270     run_and_log(<<EOF, "$postgres_prefix/bin/psql", "-X", "-h", $socket_dir, "-d", $DB_NAME);
271 CREATE TABLE foo (bar INTEGER, baz INTEGER, longstr CHAR(10240));
272 INSERT INTO foo (bar, baz) VALUES (1, 2);
273 EOF
274     pass("created test data (table and a row)");
275
276     $backup = $app->backup('device' => $data_dir, 'level' => 0, 'config' => 'TESTCONF');
277     ls_backup_data(0, $backup);
278     is($backup->{'exit_status'}, 0, "backup error status ok");
279     ok(!@{$backup->{'errors'}}, "..no errors")
280         or diag(@{$backup->{'errors'}});
281     ok(grep(/^\/PostgreSQL-Database-0$/, @{$backup->{'index'}}), "..contains an index entry")
282         or diag(@{$backup->{'index'}});
283     ok(length($backup->{'data'}) > 0,
284         "..got at least one byte");
285
286     # add a database that should be big enough to fill a WAL, then wait for postgres
287     # to archive it.
288     my $n_wals = count_wals();
289     run_and_log(<<EOF, "$postgres_prefix/bin/psql", "-X", "-h", $socket_dir, "-d", $DB_NAME);
290 INSERT INTO foo (bar, baz) VALUES (1, 2);
291 CREATE TABLE wal_test AS SELECT * FROM GENERATE_SERIES(1, 500000);
292 EOF
293     sleep(1);
294     for ($i = 0; $i < 10; $i++) {
295         last if (count_wals() > $n_wals);
296         dbg("still $n_wals WAL files in archive directory; sleeping");
297         sleep(1);
298     }
299     die "postgres did not archive any WALs" if $i == 10;
300     $n_wals = count_wals();
301
302     $backup_incr = $app->backup('device' => $data_dir, 'level' => 1, 'config' => 'TESTCONF');
303     ls_backup_data(1, $backup_incr);
304     is($backup_incr->{'exit_status'}, 0, "incr backup error status ok");
305     ok(!@{$backup_incr->{'errors'}}, "..no errors")
306         or diag(@{$backup_incr->{'errors'}});
307     ok(grep(/^\/PostgreSQL-Database-1$/, @{$backup_incr->{'index'}}), "..contains an index entry")
308         or diag(@{$backup_incr->{'index'}});
309     ok(length($backup_incr->{'data'}) > 0,
310         "..got at least one byte");
311
312     die "more WALs appeared during backup (timing error)"
313         if count_wals() > $n_wals;
314     ok(count_wals() == $n_wals,
315         "ampgsql did not clean up the latest bunch of WAL files (as expected)");
316
317     # (no more transactions here -> no more WAL files)
318
319     $backup_incr_empty = $app->backup('device' => $data_dir, 'level' => 2, 'config' => 'TESTCONF');
320     ls_backup_data(2, $backup_incr_empty);
321     is($backup_incr_empty->{'exit_status'}, 0, "incr backup with no changes: error status ok");
322     ok(!@{$backup_incr_empty->{'errors'}}, "..no errors")
323         or diag(@{$backup_incr_empty->{'errors'}});
324     ok(grep(/^\/PostgreSQL-Database-2$/, @{$backup_incr_empty->{'index'}}),
325         "..contains an index entry")
326         or diag(@{$backup_incr_empty->{'index'}});
327     ok(length($backup_incr_empty->{'data'}) > 0,
328         "..got at least one byte");
329
330     ok(count_wals() == $n_wals,
331         "ampgsql still did not clean up the latest bunch of WAL files");
332 }
333
334 do_postmaster(\&setup_db_and_backup);
335 pass("finished setting up db");
336
337 sub try_selfcheck {
338     my $sc;
339
340     $sc = $app->selfcheck('device' => $data_dir, 'config' => 'TESTCONF');
341     is($sc->{'exit_status'}, 0, "selfcheck error status ok");
342     ok(!@{$sc->{'errors'}}, "no errors reported");
343     ok(@{$sc->{'oks'}}, "got one or more OK messages");
344
345     $app->set_property('statedir', "$state_dir/foo");
346     $sc = $app->selfcheck('device' => $data_dir, 'config' => 'TESTCONF');
347     is($sc->{'exit_status'}, 0, "selfcheck error status ok");
348     ok(grep(/STATEDIR/, @{$sc->{'errors'}}), "got STATEDIR error");
349
350     my $test_state_dir_par = "$root_dir/parent-to-strip";
351     my $test_state_dir = "$test_state_dir_par/state";
352     $app->set_property('statedir', $test_state_dir);
353     try_eval("created state_dir", \&mkpath, $test_state_dir);
354     my @par_stat = stat($test_state_dir_par);
355     my $old_perms = $par_stat[2] & 0777;
356     ok(chmod(0, $test_state_dir_par), "stripped permissions from parent of statedir");
357     $sc = $app->selfcheck('device' => $data_dir, 'config' => 'TESTCONF');
358     is($sc->{'exit_status'}, 0, "selfcheck error status ok");
359     ok(grep(/STATEDIR/, @{$sc->{'errors'}}), "got STATEDIR error");
360     ok(grep(/$test_state_dir_par\/ /, @{$sc->{'errors'}}), "got perms error for parent of statedir");
361     # repair
362     ok(chmod($old_perms, $test_state_dir_par), "restored permissions on parent of statedir");
363     $app->set_property('statedir', $state_dir); 
364 }
365
366 do_postmaster(\&try_selfcheck);
367
368 ## full restore
369
370 sub try_restore {
371     my ($expected_foo_count, @backups) = @_;
372
373     dbg("*** try_restore from level $#backups");
374
375     try_eval("emptied data_dir", \&rmtree, $data_dir);
376     try_eval("emptied archive_dir", \&rmtree, $archive_dir);
377     try_eval("recreated data_dir", \&mkpath, $data_dir);
378
379     my $orig_cur_dir = POSIX::getcwd();
380     ok($orig_cur_dir, "got current directory");
381
382     ok(chdir($root_dir), "changed working directory (for restore)");
383
384     for my $level (0 .. $#backups) {
385         my $backup = $backups[$level];
386
387         my $restore = $app->restore('objects' => ['./'], level => $level,
388                             'data' => $backup->{'data'});
389         is($restore->{'exit_status'}, 0, "..level $level restore error status ok");
390         if ($level == 0) {
391             ok(-f "$data_dir/PG_VERSION", "..data dir has a PG_VERSION file");
392             ok(-d $archive_dir, "..archive dir exists");
393
394             my $pidfile = "$data_dir/postmaster.pid";
395             ok(! -f $pidfile, "..pidfile is not restored")
396                 or unlink $pidfile;
397         }
398     }
399
400     ok(chdir($orig_cur_dir), "changed working directory (back to original)");
401     is(system('chmod', '-R', 'go-rwx', $archive_dir, $data_dir) >> 8, 0, 'chmod restored files');
402
403     write_config_file $recovery_conf_file, <<EOF;
404 restore_command = 'echo restore_cmd invoked for %f >&2; cp $archive_dir/%f %p'
405 EOF
406
407     my $get_data = sub {
408         like(Installcheck::Run::run_get("$postgres_prefix/bin/psql", "-X",
409                         "-q", "-A", "-t",
410                         "-h", $socket_dir, "-d", $DB_NAME,
411                         "-c", "SELECT count(*) FROM foo;"),
412             qr/^$expected_foo_count/,
413             "..got $expected_foo_count rows from recovered database");
414     };
415
416     do_postmaster($get_data);
417     unlink($recovery_conf_file);
418     unlink($recovery_done_file);
419 }
420
421 # try a level-0, level-1, and level-2 restore
422 try_restore(1, $backup);
423 try_restore(2, $backup, $backup_incr);
424 try_restore(2, $backup, $backup_incr, $backup_incr_empty);
425
426 try_eval("emptied root_dir", \&rmtree, $root_dir);