1 # Copyright (c) 2009, 2010 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc, 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19 use Test::More tests => 74;
21 use lib "@amperldir@";
24 use Amanda::Constants;
26 use Amanda::Debug qw( debug );
30 use Installcheck::Application;
31 use Installcheck::Config;
32 use Installcheck::Run;
35 Amanda::Debug::dbopen("installcheck");
36 Installcheck::log_test_output();
37 my $debug = !exists $ENV{'HARNESS_ACTIVE'};
40 my $reason = shift @_;
42 skip($reason, Test::More->builder->expected_tests);
47 skip_all("GNU tar is not available")
48 unless ($Amanda::Constants::GNUTAR and -x $Amanda::Constants::GNUTAR);
50 my $postgres_prefix = $ENV{'INSTALLCHECK_POSTGRES_PREFIX'};
51 skip_all("Set INSTALLCHECK_POSTGRES_PREFIX to run tests") unless $postgres_prefix;
54 my $verout = Installcheck::Run::run_get("$postgres_prefix/bin/psql", "-X", "--version");
55 my @lines = split(/\n/, $verout);
56 my ($maj, $min, $pat) = ($lines[0] =~ / ([0-9]+)\.([0-9]+)\.([0-9]+)$/);
57 return $maj * 10000 + $min * 100 + $pat;
59 my $pg_version = get_pg_version();
62 my $DB_NAME = "installcheck";
63 my $root_dir = "$Installcheck::TMP/ampgsql";
64 my $data_dir = "$root_dir/data";
65 my $config_file = "$data_dir/postgresql.conf";
66 my $recovery_conf_file = "$data_dir/recovery.conf";
67 my $recovery_done_file = "$data_dir/recovery.done";
68 my $socket_dir = "$root_dir/sockets";
69 my $archive_dir = "$root_dir/archive";
70 my $tmp_dir = "$root_dir/tmp";
71 my $log_dir = "$Installcheck::TMP/ampgsql";
72 my $state_dir = "$root_dir/state";
83 # run a command with output sent to the debug log
85 my ($in_str, $prog, @args) = @_;
88 debug("running $prog " . join(" ", @args));
89 debug(".. with input '$in_str'") if $in_str;
91 my $dbfd = Amanda::Debug::dbfd();
92 my $pid = open3(\*IN, ">&$dbfd", ">&$dbfd", $prog, @args);
97 debug("..exit status $status");
102 # run a sub and report on the result; mostly used for setup/teardown
104 my ($desc, $code, @args) = @_;
106 $err_str = "$@" unless eval {$code->(@args); 1;};
107 ok(!$err_str, $desc) or diag($err_str);
110 sub write_config_file {
111 my ($filename, $cfg) = @_;
113 unlink $filename if -f $filename;
114 Amanda::Util::burp($filename, $cfg);
117 # run $code while the postmaster is started, shutting down the postmaster afterward,
118 # logging the result to our debug file
121 my $pidfile = "$data_dir/postmaster.pid";
124 die "postmaster already running"
127 dbg("starting postmaster..");
129 my $dbfd = Amanda::Debug::dbfd();
130 my $pid = open3(\*IN, ">&$dbfd", ">&$dbfd",
131 "$postgres_prefix/bin/postmaster", "-D", $data_dir);
134 # busy-wait for the pidfile to be created, for up to 120s
136 while (!-f $pidfile) {
137 die "postmaster did not start"
138 if ($ticks++ > 120 or !kill 0, $pid);
139 dbg("waiting for postmaster to write its pid");
143 # and finish out those 120 seconds waiting for the db to actually
144 # be ready to roll, using psql -l just like pg_ctl does
145 while ($ticks++ < 120) {
148 my $psqlpid = open3(\*IN, ">&$dbfd", ">&$dbfd",
149 "$postgres_prefix/bin/psql", "-X", "-h", $socket_dir, "-l");
151 waitpid($psqlpid, 0);
152 last if (($? >> 8) == 0);
157 die("postmaster never started");
160 # use eval to be careful to shut down postgres
161 eval { $code->() if $code };
164 # kill the postmaster and wait for it to die
167 my $status = $? >> 8;
168 dbg("postmaster stopped");
170 die "postmaster pid file still exists"
176 # count the WAL files. Note that this may count some extra stuff, too, but
177 # since it's only used to see the number of WALs *increase*, that's OK.
179 my @files = glob("$archive_dir/*");
181 debug("WAL files in archive_dir: " . join(" ", @files));
183 debug("No WAL files in archive_dir");
185 return scalar @files;
189 my ($level, $backup) = @_;
191 my $tmpdir = "$Installcheck::TMP/backup_data";
192 -d $tmpdir && rmtree $tmpdir;
196 debug("contents of level-$level backup:");
197 Amanda::Util::burp("$tmpdir/backup.tar", $backup->{'data'});
198 run_and_log("", $Amanda::Constants::GNUTAR, "-tvf", "$tmpdir/backup.tar");
200 debug("contents of level-0 backup:");
201 Amanda::Util::burp("$tmpdir/backup.tar", $backup->{'data'});
202 run_and_log("", $Amanda::Constants::GNUTAR, "-C", $tmpdir, "-xvf", "$tmpdir/backup.tar");
203 debug(".. archive_dir.tar contains:");
204 run_and_log("", $Amanda::Constants::GNUTAR, "-tvf", "$tmpdir/archive_dir.tar");
205 debug(".. data_dir.tar looks like:\n" . `ls -l "$tmpdir/data_dir.tar"`);
211 # set up all of our dirs
212 try_eval("emptied root_dir", \&rmtree, $root_dir);
213 try_eval("created archive_dir", \&mkpath, $archive_dir);
214 try_eval("created data_dir", \&mkpath, $data_dir);
215 try_eval("created socket_dir", \&mkpath, $socket_dir);
216 try_eval("created log_dir", \&mkpath, $log_dir);
217 try_eval("created state_dir", \&mkpath, $state_dir);
219 # create an amanda config for the application
220 my $conf = Installcheck::Config->new();
221 $conf->add_client_param('property', "\"PG-DATADIR\" \"$data_dir\"");
222 $conf->add_client_param('property', "\"PG-ARCHIVEDIR\" \"$archive_dir\"");
223 $conf->add_client_param('property', "\"PG-CLEANUPWAL\" \"yes\"");
224 $conf->add_client_param('property', "\"PG-HOST\" \"$socket_dir\"");
225 $conf->add_client_param('property', "\"PSQL-PATH\" \"$postgres_prefix/bin/psql\"");
228 # set up the database
229 dbg("creating initial database");
230 run_and_log("", "$postgres_prefix/bin/initdb", "-D", "$data_dir")
231 and die("error running initdb");
233 # enable archive mode for 8.3 and higher
234 my $archive_mode = '';
235 if ($pg_version >= 80300) {
236 $archive_mode = 'archive_mode = on';
239 # write the postgres config file
240 write_config_file $config_file, <<EOF;
241 listen_addresses = ''
242 unix_socket_directory = '$socket_dir'
243 archive_command = 'test ! -f $archive_dir/%f && cp %p $archive_dir/%f'
245 log_destination = 'stderr'
246 # checkpoint every 30 seconds (this is the minimum)
247 checkpoint_timeout = 30
248 # and don't warn me about that
249 checkpoint_warning = 0
250 # and keep 50 segments
251 checkpoint_segments = 50
252 # and bundle commits up to one minute
256 my $app = new Installcheck::Application("ampgsql");
257 $app->add_property('statedir', $state_dir);
258 $app->add_property('tmpdir', $tmp_dir);
260 # take three backups: a level 0, level 1, and level 2. The level 2 has
261 # no database changes, so it contains no WAL files.
262 my ($backup, $backup_incr, $backup_incr_empty);
263 sub setup_db_and_backup {
266 run_and_log("", "$postgres_prefix/bin/createdb", "-h", $socket_dir, $DB_NAME);
269 run_and_log(<<EOF, "$postgres_prefix/bin/psql", "-X", "-h", $socket_dir, "-d", $DB_NAME);
270 CREATE TABLE foo (bar INTEGER, baz INTEGER, longstr CHAR(10240));
271 INSERT INTO foo (bar, baz) VALUES (1, 2);
273 pass("created test data (table and a row)");
275 $backup = $app->backup('device' => $data_dir, 'level' => 0, 'config' => 'TESTCONF');
276 ls_backup_data(0, $backup);
277 is($backup->{'exit_status'}, 0, "backup error status ok");
278 ok(!@{$backup->{'errors'}}, "..no errors")
279 or diag(@{$backup->{'errors'}});
280 ok(grep(/^\/PostgreSQL-Database-0$/, @{$backup->{'index'}}), "..contains an index entry")
281 or diag(@{$backup->{'index'}});
282 ok(length($backup->{'data'}) > 0,
283 "..got at least one byte");
285 # add a database that should be big enough to fill a WAL, then wait for postgres
287 my $n_wals = count_wals();
288 run_and_log(<<EOF, "$postgres_prefix/bin/psql", "-X", "-h", $socket_dir, "-d", $DB_NAME);
289 INSERT INTO foo (bar, baz) VALUES (1, 2);
290 CREATE TABLE wal_test AS SELECT * FROM GENERATE_SERIES(1, 500000);
293 for ($i = 0; $i < 10; $i++) {
294 last if (count_wals() > $n_wals);
295 dbg("still $n_wals WAL files in archive directory; sleeping");
298 die "postgres did not archive any WALs" if $i == 10;
299 $n_wals = count_wals();
301 $backup_incr = $app->backup('device' => $data_dir, 'level' => 1, 'config' => 'TESTCONF');
302 ls_backup_data(1, $backup_incr);
303 is($backup_incr->{'exit_status'}, 0, "incr backup error status ok");
304 ok(!@{$backup_incr->{'errors'}}, "..no errors")
305 or diag(@{$backup_incr->{'errors'}});
306 ok(grep(/^\/PostgreSQL-Database-1$/, @{$backup_incr->{'index'}}), "..contains an index entry")
307 or diag(@{$backup_incr->{'index'}});
308 ok(length($backup_incr->{'data'}) > 0,
309 "..got at least one byte");
311 die "more WALs appeared during backup (timing error)"
312 if count_wals() > $n_wals;
313 ok(count_wals() == $n_wals,
314 "ampgsql did not clean up the latest bunch of WAL files (as expected)");
316 # (no more transactions here -> no more WAL files)
318 $backup_incr_empty = $app->backup('device' => $data_dir, 'level' => 2, 'config' => 'TESTCONF');
319 ls_backup_data(2, $backup_incr_empty);
320 is($backup_incr_empty->{'exit_status'}, 0, "incr backup with no changes: error status ok");
321 ok(!@{$backup_incr_empty->{'errors'}}, "..no errors")
322 or diag(@{$backup_incr_empty->{'errors'}});
323 ok(grep(/^\/PostgreSQL-Database-2$/, @{$backup_incr_empty->{'index'}}),
324 "..contains an index entry")
325 or diag(@{$backup_incr_empty->{'index'}});
326 ok(length($backup_incr_empty->{'data'}) > 0,
327 "..got at least one byte");
329 ok(count_wals() == $n_wals,
330 "ampgsql still did not clean up the latest bunch of WAL files");
333 do_postmaster(\&setup_db_and_backup);
334 pass("finished setting up db");
339 $sc = $app->selfcheck('device' => $data_dir, 'config' => 'TESTCONF');
340 is($sc->{'exit_status'}, 0, "selfcheck error status ok");
341 ok(!@{$sc->{'errors'}}, "no errors reported");
342 ok(@{$sc->{'oks'}}, "got one or more OK messages");
344 $app->set_property('statedir', "$state_dir/foo");
345 $sc = $app->selfcheck('device' => $data_dir, 'config' => 'TESTCONF');
346 is($sc->{'exit_status'}, 0, "selfcheck error status ok");
347 ok(grep(/STATEDIR/, @{$sc->{'errors'}}), "got STATEDIR error");
349 my $test_state_dir_par = "$root_dir/parent-to-strip";
350 my $test_state_dir = "$test_state_dir_par/state";
351 $app->set_property('statedir', $test_state_dir);
352 try_eval("created state_dir", \&mkpath, $test_state_dir);
353 my @par_stat = stat($test_state_dir_par);
354 my $old_perms = $par_stat[2] & 0777;
355 ok(chmod(0, $test_state_dir_par), "stripped permissions from parent of statedir");
356 $sc = $app->selfcheck('device' => $data_dir, 'config' => 'TESTCONF');
357 is($sc->{'exit_status'}, 0, "selfcheck error status ok");
358 ok(grep(/STATEDIR/, @{$sc->{'errors'}}), "got STATEDIR error");
359 ok(grep(/$test_state_dir_par\/ /, @{$sc->{'errors'}}), "got perms error for parent of statedir");
361 ok(chmod($old_perms, $test_state_dir_par), "restored permissions on parent of statedir");
362 $app->set_property('statedir', $state_dir);
365 do_postmaster(\&try_selfcheck);
370 my ($expected_foo_count, @backups) = @_;
372 dbg("*** try_restore from level $#backups");
374 try_eval("emptied data_dir", \&rmtree, $data_dir);
375 try_eval("emptied archive_dir", \&rmtree, $archive_dir);
376 try_eval("recreated data_dir", \&mkpath, $data_dir);
378 my $orig_cur_dir = POSIX::getcwd();
379 ok($orig_cur_dir, "got current directory");
381 ok(chdir($root_dir), "changed working directory (for restore)");
383 for my $level (0 .. $#backups) {
384 my $backup = $backups[$level];
386 my $restore = $app->restore('objects' => ['./'], level => $level,
387 'data' => $backup->{'data'});
388 is($restore->{'exit_status'}, 0, "..level $level restore error status ok");
390 ok(-f "$data_dir/PG_VERSION", "..data dir has a PG_VERSION file");
391 ok(-d $archive_dir, "..archive dir exists");
393 my $pidfile = "$data_dir/postmaster.pid";
394 ok(! -f $pidfile, "..pidfile is not restored")
399 ok(chdir($orig_cur_dir), "changed working directory (back to original)");
400 is(system('chmod', '-R', 'go-rwx', $archive_dir, $data_dir) >> 8, 0, 'chmod restored files');
402 write_config_file $recovery_conf_file, <<EOF;
403 restore_command = 'echo restore_cmd invoked for %f >&2; cp $archive_dir/%f %p'
407 like(Installcheck::Run::run_get("$postgres_prefix/bin/psql", "-X",
409 "-h", $socket_dir, "-d", $DB_NAME,
410 "-c", "SELECT count(*) FROM foo;"),
411 qr/^$expected_foo_count/,
412 "..got $expected_foo_count rows from recovered database");
415 do_postmaster($get_data);
416 unlink($recovery_conf_file);
417 unlink($recovery_done_file);
420 # try a level-0, level-1, and level-2 restore
421 try_restore(1, $backup);
422 try_restore(2, $backup, $backup_incr);
423 try_restore(2, $backup, $backup_incr, $backup_incr_empty);
425 try_eval("emptied root_dir", \&rmtree, $root_dir);