| |
| # Copyright (c) 2021, PostgreSQL Global Development Group |
| |
| # Verify that various forms of corruption are detected by pg_verifybackup. |
| |
| use strict; |
| use warnings; |
| use Cwd; |
| use Config; |
| use File::Path qw(rmtree); |
| use PostgresNode; |
| use TestLib; |
| use Test::More tests => 44; |
| |
| my $primary = get_new_node('primary'); |
| $primary->init(allows_streaming => 1); |
| $primary->start; |
| |
| # Include a user-defined tablespace in the hopes of detecting problems in that |
| # area. |
| my $source_ts_path = TestLib::tempdir_short(); |
| my $source_ts_prefix = $source_ts_path; |
| $source_ts_prefix =~ s!(^[A-Z]:/[^/]*)/.*!$1!; |
| |
| $primary->safe_psql('postgres', <<EOM); |
| CREATE TABLE x1 (a int); |
| INSERT INTO x1 VALUES (111); |
| CREATE TABLESPACE ts1 LOCATION '$source_ts_path'; |
| CREATE TABLE x2 (a int) TABLESPACE ts1; |
| INSERT INTO x1 VALUES (222); |
| EOM |
| |
| my @scenario = ( |
| { |
| 'name' => 'extra_file', |
| 'mutilate' => \&mutilate_extra_file, |
| 'fails_like' => |
| qr/extra_file.*present on disk but not in the manifest/ |
| }, |
| { |
| 'name' => 'extra_tablespace_file', |
| 'mutilate' => \&mutilate_extra_tablespace_file, |
| 'fails_like' => |
| qr/extra_ts_file.*present on disk but not in the manifest/ |
| }, |
| { |
| 'name' => 'missing_file', |
| 'mutilate' => \&mutilate_missing_file, |
| 'fails_like' => |
| qr/pg_xact\/0000.*present in the manifest but not on disk/ |
| }, |
| { |
| 'name' => 'missing_tablespace', |
| 'mutilate' => \&mutilate_missing_tablespace, |
| 'fails_like' => |
| qr/pg_tblspc.*present in the manifest but not on disk/ |
| }, |
| { |
| 'name' => 'append_to_file', |
| 'mutilate' => \&mutilate_append_to_file, |
| 'fails_like' => qr/has size \d+ on disk but size \d+ in the manifest/ |
| }, |
| { |
| 'name' => 'truncate_file', |
| 'mutilate' => \&mutilate_truncate_file, |
| 'fails_like' => qr/has size 0 on disk but size \d+ in the manifest/ |
| }, |
| { |
| 'name' => 'replace_file', |
| 'mutilate' => \&mutilate_replace_file, |
| 'fails_like' => qr/checksum mismatch for file/ |
| }, |
| { |
| 'name' => 'bad_manifest', |
| 'mutilate' => \&mutilate_bad_manifest, |
| 'fails_like' => qr/manifest checksum mismatch/ |
| }, |
| { |
| 'name' => 'open_file_fails', |
| 'mutilate' => \&mutilate_open_file_fails, |
| 'fails_like' => qr/could not open file/, |
| 'skip_on_windows' => 1 |
| }, |
| { |
| 'name' => 'open_directory_fails', |
| 'mutilate' => \&mutilate_open_directory_fails, |
| 'cleanup' => \&cleanup_open_directory_fails, |
| 'fails_like' => qr/could not open directory/, |
| 'skip_on_windows' => 1 |
| }, |
| { |
| 'name' => 'search_directory_fails', |
| 'mutilate' => \&mutilate_search_directory_fails, |
| 'cleanup' => \&cleanup_search_directory_fails, |
| 'fails_like' => qr/could not stat file or directory/, |
| 'skip_on_windows' => 1 |
| }); |
| |
| for my $scenario (@scenario) |
| { |
| my $name = $scenario->{'name'}; |
| |
| SKIP: |
| { |
| skip "unix-style permissions not supported on Windows", 4 |
| if $scenario->{'skip_on_windows'} && $windows_os; |
| |
| # Take a backup and check that it verifies OK. |
| my $backup_path = $primary->backup_dir . '/' . $name; |
| my $backup_ts_path = TestLib::tempdir_short(); |
| # The tablespace map parameter confuses Msys2, which tries to mangle |
| # it. Tell it not to. |
| # See https://www.msys2.org/wiki/Porting/#filesystem-namespaces |
| local $ENV{MSYS2_ARG_CONV_EXCL} = $source_ts_prefix; |
| $primary->command_ok( |
| [ |
| 'pg_basebackup', '-D', $backup_path, '--no-sync', |
| '--target-gp-dbid', '123', |
| '-T', "${source_ts_path}=${backup_ts_path}" |
| ], |
| "base backup ok"); |
| command_ok([ 'pg_verifybackup', $backup_path ], |
| "intact backup verified"); |
| |
| # Mutilate the backup in some way. |
| $scenario->{'mutilate'}->($backup_path); |
| |
| # Now check that the backup no longer verifies. |
| command_fails_like( |
| [ 'pg_verifybackup', $backup_path ], |
| $scenario->{'fails_like'}, |
| "corrupt backup fails verification: $name"); |
| |
| # Run cleanup hook, if provided. |
| $scenario->{'cleanup'}->($backup_path) |
| if exists $scenario->{'cleanup'}; |
| |
| # Finally, use rmtree to reclaim space. |
| rmtree($backup_path); |
| } |
| } |
| |
| sub create_extra_file |
| { |
| my ($backup_path, $relative_path) = @_; |
| my $pathname = "$backup_path/$relative_path"; |
| open(my $fh, '>', $pathname) || die "open $pathname: $!"; |
| print $fh "This is an extra file.\n"; |
| close($fh); |
| return; |
| } |
| |
| # Add a file into the root directory of the backup. |
| sub mutilate_extra_file |
| { |
| my ($backup_path) = @_; |
| create_extra_file($backup_path, "extra_file"); |
| return; |
| } |
| |
| # Add a file inside the user-defined tablespace. |
| sub mutilate_extra_tablespace_file |
| { |
| my ($backup_path) = @_; |
| my ($tsoid) = |
| grep { $_ ne '.' && $_ ne '..' } slurp_dir("$backup_path/pg_tblspc"); |
| my ($catvdir) = grep { $_ ne '.' && $_ ne '..' } |
| slurp_dir("$backup_path/pg_tblspc/$tsoid"); |
| my ($tsdboid) = grep { $_ ne '.' && $_ ne '..' } |
| slurp_dir("$backup_path/pg_tblspc/$tsoid/$catvdir"); |
| create_extra_file($backup_path, |
| "pg_tblspc/$tsoid/$catvdir/$tsdboid/extra_ts_file"); |
| return; |
| } |
| |
| # Remove a file. |
| sub mutilate_missing_file |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/pg_xact/0000"; |
| unlink($pathname) || die "$pathname: $!"; |
| return; |
| } |
| |
| # Remove the symlink to the user-defined tablespace. |
| sub mutilate_missing_tablespace |
| { |
| my ($backup_path) = @_; |
| my ($tsoid) = |
| grep { $_ ne '.' && $_ ne '..' } slurp_dir("$backup_path/pg_tblspc"); |
| my $pathname = "$backup_path/pg_tblspc/$tsoid"; |
| if ($windows_os) |
| { |
| # rmdir works on some windows setups, unlink on others. |
| # Instead of trying to implement precise rules, just try one and then |
| # the other. |
| unless (rmdir($pathname)) |
| { |
| my $err = $!; |
| unlink($pathname) || die "$pathname: rmdir: $err, unlink: $!"; |
| } |
| } |
| else |
| { |
| unlink($pathname) || die "$pathname: $!"; |
| } |
| return; |
| } |
| |
| # Append an additional bytes to a file. |
| sub mutilate_append_to_file |
| { |
| my ($backup_path) = @_; |
| append_to_file "$backup_path/global/pg_control", 'x'; |
| return; |
| } |
| |
| # Truncate a file to zero length. |
| sub mutilate_truncate_file |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/global/pg_control"; |
| open(my $fh, '>', $pathname) || die "open $pathname: $!"; |
| close($fh); |
| return; |
| } |
| |
| # Replace a file's contents without changing the length of the file. This is |
| # not a particularly efficient way to do this, so we pick a file that's |
| # expected to be short. |
| sub mutilate_replace_file |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/PG_VERSION"; |
| my $contents = slurp_file($pathname); |
| open(my $fh, '>', $pathname) || die "open $pathname: $!"; |
| print $fh 'q' x length($contents); |
| close($fh); |
| return; |
| } |
| |
| # Corrupt the backup manifest. |
| sub mutilate_bad_manifest |
| { |
| my ($backup_path) = @_; |
| append_to_file "$backup_path/backup_manifest", "\n"; |
| return; |
| } |
| |
| # Create a file that can't be opened. (This is skipped on Windows.) |
| sub mutilate_open_file_fails |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/PG_VERSION"; |
| chmod(0, $pathname) || die "chmod $pathname: $!"; |
| return; |
| } |
| |
| # Create a directory that can't be opened. (This is skipped on Windows.) |
| sub mutilate_open_directory_fails |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/pg_subtrans"; |
| chmod(0, $pathname) || die "chmod $pathname: $!"; |
| return; |
| } |
| |
| # restore permissions on the unreadable directory we created. |
| sub cleanup_open_directory_fails |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/pg_subtrans"; |
| chmod(0700, $pathname) || die "chmod $pathname: $!"; |
| return; |
| } |
| |
| # Create a directory that can't be searched. (This is skipped on Windows.) |
| sub mutilate_search_directory_fails |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/base"; |
| chmod(0400, $pathname) || die "chmod $pathname: $!"; |
| return; |
| } |
| |
| # rmtree can't cope with a mode 400 directory, so change back to 700. |
| sub cleanup_search_directory_fails |
| { |
| my ($backup_path) = @_; |
| my $pathname = "$backup_path/base"; |
| chmod(0700, $pathname) || die "chmod $pathname: $!"; |
| return; |
| } |