1#!/usr/bin/perl 2# 3# xpostgres 4# 5# Author:: Apple Inc. 6# Documentation:: Apple Inc. 7# Copyright (c) 2013 Apple Inc. All Rights Reserved. 8# 9# IMPORTANT NOTE: This file is licensed only for use on Apple-branded 10# computers and is subject to the terms and conditions of the Apple Software 11# License Agreement accompanying the package this file is a part of. 12# You may not port this file to another platform without Apple's written consent. 13# License:: All rights reserved. 14# 15# This tool is a wrapper for postgres. 16# Its function is to launch a postgres process and manage WAL archiving. 17# perltidy options: -pbp -l=100 18 19use strict; 20use warnings; 21use Errno qw(EAGAIN); 22use File::Basename qw(basename); 23use File::Basename qw(dirname); 24use File::Path qw(rmtree); 25use Time::Local; 26use Getopt::Std; 27use Fcntl; 28use Fcntl qw(:flock); 29use FileHandle; 30use POSIX; 31use Cwd qw(); 32 33# Constants 34my $WAIT4PATH = '/bin/wait4path'; 35my $GNUTAR_PATH = '/usr/bin/gnutar'; 36my $PLIST_BUDDY = '/usr/libexec/PlistBuddy'; 37my $ARCHIVE_COMMAND_PATH 38 = '/Applications/Server.app/Contents/ServerRoot/usr/libexec/xpg_archive_command'; 39my $PG_BASEBACKUP_PATH = '/Applications/Server.app/Contents/ServerRoot/usr/bin/pg_basebackup'; 40my $PSQL_PATH = '/Applications/Server.app/Contents/ServerRoot/usr/bin/psql'; 41my $PG_CTL_PATH = '/Applications/Server.app/Contents/ServerRoot/usr/bin/pg_ctl'; 42my $POSTGRES_PATH = '/Applications/Server.app/Contents/ServerRoot/usr/bin/postgres_real'; 43my $TMUTIL_PATH = '/usr/bin/tmutil'; 44my $DEFAULT_SOCKET_DIR = '/var/pgsql_socket'; 45my $RESTORE_ON_ABSENCE_FILE = '.NoRestoreNeeded'; 46my $MY_NAME = 'xpostgres'; 47my $BACKUP_DIRECTORY_NAME = 'base_backup'; 48my $BACKUP_ZIP_FILE_NAME = 'base_complete.tar.gz'; 49my $BACKUP_TEMP_FILE_NAME = 'base.tar.gz'; 50my $NUM_COMPLETE_BACKUPS_PER_DAY = 4; 51my $SECONDS_PER_DAY = 86400; 52my $HEARTBEAT_SECS = 10; # seconds to wait between heartbeat intervals 53my $MAX_WAL_SENDERS = 2; # for postgresql.conf, value for 'max_wal_senders' preference 54 55# Global vars 56my $g_debug_enabled = 0; 57my $g_archived_logs_directory = q{}; # empty string 58my $g_postgres_data_directory = q{}; 59my $g_postgres_socket_directory = q{}; 60my $g_postgres_log_directory = q{}; 61my $g_kill_signal = q{}; 62my $g_apple_configuration = q{}; 63my $g_postgres_proccess_pid; 64my @g_argv; 65my $g_child_status; 66 67# Signal handlers 68$SIG{HUP} = \&handle_signal; 69$SIG{TERM} = \&handle_signal; 70$SIG{INT} = \&handle_signal; 71$SIG{CHLD} = \&reaper; 72 73############################################################################# 74# PostgreSQL process management 75############################################################################# 76 77############################################################################# 78sub start_postgres { 79 if ( defined $g_postgres_proccess_pid && $g_postgres_proccess_pid != 0 ) { 80 log_message( 'postgres appears to already be running with pid: ' 81 . $g_postgres_proccess_pid 82 . '. Not starting a new instance.' ); 83 return 1; 84 } 85 86 if ( !-e $g_postgres_data_directory ) { 87 log_message('Waiting for data path to exit...'); 88 system $WAIT4PATH, $g_postgres_data_directory; 89 if ( $g_child_status != 0 ) { 90 log_message("Wait4path returned error: $g_child_status"); 91 return 1; 92 } 93 } 94 95 # Snip the shared memory block out of the lockfile if no process is running 96 # that matches the PID in the file. Otherwise postgres will fail to start 97 # if it wasn't shut down properly and another process is now using that memory 98 # block. 99 my $postgres_pid_path = $g_postgres_data_directory . '/postmaster.pid'; 100 if ( -e $postgres_pid_path ) { 101 my $FILE; 102 if ( !open $FILE, '+<', $postgres_pid_path ) { 103 log_message("Error opening lock file: $!"); 104 } 105 else { 106 my @lines = <$FILE>; 107 if ( $lines[0] =~ / \A (\d+) \n* \z /xms ) { 108 my $old_pid = $1; 109 system 'kill', '-0', $old_pid; 110 if ( $g_child_status != 0 ) { 111 112 # Process is not running 113 log_message('Clearing shared memory block from lock file'); 114 if ( $lines[$#lines] =~ / \A \s* \d+ \s+ \d+ \s* \n* \z /xms ) { 115 my $out = q{}; 116 for ( my $i = 0; $i < $#lines - 1; $i++ ) { 117 $out .= $lines[$i]; 118 } 119 if ( !seek $FILE, 0, 0 ) { 120 log_message("Error, seek: $!"); 121 return 1; 122 } 123 print $FILE $out; 124 truncate $FILE, tell($FILE); 125 } 126 } 127 } 128 close $FILE; 129 } 130 } 131 132 # Clean up any stale socket files 133 my $SOCK_DIR; 134 if ( !opendir $SOCK_DIR, $g_postgres_socket_directory ) { 135 log_message("Cannot open socket directory: $!"); 136 return 1; 137 } 138 my @files = readdir $SOCK_DIR; 139 closedir $SOCK_DIR; 140 141 foreach my $file (@files) { 142 if ( $file eq '.' || $file eq '..' ) { 143 next; 144 } 145 log_message("Found stale file in socket directory, removing it: $file"); 146 if ( !unlink $g_postgres_socket_directory . "/$file" ) { 147 log_message("Failed to delete stale file: $!"); 148 } 149 } 150 151FORK: { 152 my $pid; 153 if ( $pid = fork ) { 154 $g_postgres_proccess_pid = $pid; 155 } 156 elsif ( defined $pid ) { 157 exec $POSTGRES_PATH, @g_argv; 158 } 159 elsif ( $! == EAGAIN ) { 160 sleep 5; 161 redo FORK; 162 } 163 else { 164 log_message("Fork error: $!"); 165 return 1; 166 } 167 } 168 my $success = 0; 169 for ( 1 .. 30 ) { 170 if ( !opendir $SOCK_DIR, $g_postgres_socket_directory ) { 171 log_message("Cannot open socket directory: $!"); 172 return 1; 173 } 174 my @files = readdir $SOCK_DIR; 175 closedir $SOCK_DIR; 176 177 my $arr_size = @files; 178 if ( $arr_size > 2 ) { # more than just '.' and '..' 179 $success = 1; 180 last; 181 } 182 if ($g_debug_enabled) { 183 log_message('Waiting for postgres socket file to be created...'); 184 } 185 sleep 1; 186 } 187 if ( !$success ) { 188 log_message('Could not determine if postgres is running successfully, giving up'); 189 return 1; 190 } 191 192 touch_dotfile(); 193 194 return 0; 195} 196 197############################################################################# 198sub stop_postgres { 199 my $signal = shift; 200 if ( !defined $signal ) { 201 $signal = 'TERM'; 202 } 203 204 # Disconnect any connected clients before sending a kill signal 205 my $sql = 'SELECT pid, (SELECT pg_terminate_backend(pid)) as killed from ' 206 . "pg_stat_activity WHERE state LIKE 'idle';"; 207 system $PSQL_PATH, '-q', '-h', $g_postgres_socket_directory, '-d', 'postgres', '-c', $sql; 208 209 if ( ( !defined $g_postgres_proccess_pid ) || $g_postgres_proccess_pid == 0 ) { 210 log_message('postgres appears to already be stopped.'); 211 return; 212 } 213 214 if ( defined $g_postgres_proccess_pid && $g_postgres_proccess_pid != 0 ) { 215 kill $signal, $g_postgres_proccess_pid; 216 } 217 218 my $postgres_pid_path = $g_postgres_data_directory . '/postmaster.pid'; 219 my $success = 0; 220 221 # launchd is configured to wait 60 seconds before sending a SIGKILL to us, 222 # so allow up to 50 seconds for a "smart" shutdown, but send a SIGINT for 223 # a "fast" shutdown if that fails. 224 for ( 1 .. 50 ) { 225 if ( $g_postgres_proccess_pid == 0 ) { 226 last; 227 } 228 229 my $pid = waitpid $g_postgres_proccess_pid, WNOHANG; 230 if ( $pid == $g_postgres_proccess_pid ) { 231 $g_postgres_proccess_pid = 0; 232 last; 233 } 234 235 sleep 1; 236 } 237 238 if ( $g_postgres_proccess_pid > 0 ) { 239 kill 'INT', $g_postgres_proccess_pid; 240 $g_postgres_proccess_pid = 0; 241 } 242 243 return; 244} 245 246############################################################################# 247sub sighup_postgres { 248 if ( defined $g_postgres_proccess_pid && $g_postgres_proccess_pid != 0 ) { 249 kill 'HUP', $g_postgres_proccess_pid; 250 } 251} 252 253############################################################################# 254# Returns 1 if postgres is running, 0 if not running. 255sub postgres_is_running { 256 my $postgres_pid_path = $g_postgres_data_directory . '/postmaster.pid'; 257 my $FILE; 258 if ( !-e $postgres_pid_path ) { 259 return 0; 260 } 261 262 if ( !open $FILE, '+<', $postgres_pid_path ) { 263 log_message("Error opening lock file: $!"); 264 return -1; 265 } 266 267 my @lines = <$FILE>; 268 if ( $lines[0] =~ / \A (\d+) \n* \z /xms ) { 269 my $old_pid = $1; 270 close $FILE; 271 system 'kill', '-0', $old_pid; 272 if ( $g_child_status != 0 ) { 273 274 # Process is not running 275 return 0; 276 } 277 else { 278 return 1; 279 } 280 } 281} 282 283############################################################################# 284# Authentication configuration file operations 285############################################################################# 286 287############################################################################# 288# Restrict postgres connections via pg_hba.conf 289sub enable_connection_restriction { 290 291 # Disable any non-replication line in pg_hba.conf 292 my $FILE; 293 if ( !open $FILE, '+<', $g_postgres_data_directory . '/pg_hba.conf' ) { 294 log_message("Error opening pg_hba.conf to restrict connections: $!"); 295 return 1; 296 } 297 if ( !flock $FILE, LOCK_EX ) { 298 log_message("Error getting lock for file pg_hba.conf: $!"); 299 return 1; 300 } 301 my $updated_file = 0; 302 my $out; 303 while (<$FILE>) { 304 if ( $_ =~ / \# /xms ) { 305 $out .= $_; 306 } 307 elsif ( $_ =~ / \A (\S+) \s+ (\S+) \s+ (\S+) \s+ (\S+) \s* (\S*) \s* \n \z /xms ) { 308 my ( $type, $database, $user ) = ( $1, $2, $3 ); 309 my ( $address, $method ); 310 if ( $5 ne q{} ) { 311 ( $address, $method ) = ( $4, $5 ); 312 } 313 else { 314 $method = $4; 315 } 316 if ( $database eq 'replication' ) { 317 $out .= $_; 318 next; 319 } 320 else { 321 my $line = $_; 322 chomp $line; 323 $out .= '#' . $line . " # UPDATED BY xpostgres\n"; 324 $updated_file = 1; 325 } 326 } 327 else { 328 $out .= $_; 329 } 330 } 331 332 if ($updated_file) { 333 if ( !seek $FILE, 0, 0 ) { 334 log_message("Error writing pg_hba.conf: $!"); 335 return 1; 336 } 337 print $FILE $out; 338 truncate $FILE, tell($FILE); 339 } 340 close $FILE; 341 342 return 0; 343} 344 345############################################################################# 346sub disable_connection_restriction { 347 348 # Enable any lines in pg_hba.conf that we previously disabled 349 my $FILE; 350 if ( !open $FILE, '+<', $g_postgres_data_directory . '/pg_hba.conf' ) { 351 log_message("Error opening pg_hba.conf to allow connections: $!"); 352 return 1; 353 } 354 if ( !flock $FILE, LOCK_EX ) { 355 log_message("Error getting lock for file pg_hba.conf: $!"); 356 return 1; 357 } 358 my $updated_file = 0; 359 my $out; 360 while (<$FILE>) { 361 if ( $_ =~ / \A \# \s* .+ \s* \# \s UPDATED \s BY \s xpostgres \n \z /xms ) { 362 my $line = $_; 363 $line =~ s/ \A \# //xms; 364 $line =~ s/\s*# UPDATED BY xpostgres//ms; 365 $out .= $line; 366 $updated_file = 1; 367 } 368 else { 369 $out .= $_; 370 } 371 } 372 if ($updated_file) { 373 if ( !seek $FILE, 0, 0 ) { 374 log_message("Error writing pg_hba.conf: $!"); 375 return 1; 376 } 377 print $FILE $out; 378 truncate $FILE, tell($FILE); 379 } 380 close $FILE; 381 382 return 0; 383} 384 385############################################################################# 386# Takes one argument: 0 to disable archiving, 1 to enable archiving. h 387sub toggle_wal_archiving_state { 388 my $enable_wal_archiving = shift; 389 if ( !( ( $enable_wal_archiving == 0 ) || ( $enable_wal_archiving == 1 ) ) ) { 390 return 1; 391 } 392 393 if ($enable_wal_archiving) { 394 395 # Enable local connections for replication (update pg_hba.conf) 396 my $FILE; 397 if ( !open $FILE, '+<', $g_postgres_data_directory . '/pg_hba.conf' ) { 398 log_message("Error opening pg_hba.conf to enable replication connections: $!"); 399 return 1; 400 } 401 if ( !flock $FILE, LOCK_EX ) { 402 log_message("Error getting lock for file pg_hba.conf: $!"); 403 return 1; 404 } 405 my $replication_enabled = 0; 406 while (<$FILE>) { 407 if ( $_ =~ /^ \# /xms ) { 408 next; 409 } 410 if ( $_ =~ /\A (\S+) \s+ (\S+) \s+ (\S+) \s+ (\S+) \s* (\S*) \z/xms ) { 411 my ( $type, $database, $user ) = ( $1, $2, $3 ); 412 my ( $address, $method ) = q{}; # empty string 413 if ( $5 ne q{} ) { 414 ( $address, $method ) = ( $4, $5 ); 415 } 416 else { 417 $method = $4; 418 } 419 if ( $type eq 'local' 420 && $database eq 'replication' 421 && $address eq q{} 422 && $method eq 'trust' ) 423 { 424 $replication_enabled = 1; 425 next; 426 } 427 } 428 } 429 if ( !$replication_enabled ) { 430 if ( !seek $FILE, 0, 2 ) { 431 log_message("Error updating file pg_hba.conf: $!"); 432 return 1; 433 } 434 print $FILE "local replication _postgres trust\n"; 435 } 436 close $FILE; 437 } 438 439 # Update postgresql.conf 440 # postgres must be restarted for the changes to go into effect. 441 my $out; 442 my $FILE; 443 if ( !open $FILE, '+<', $g_postgres_data_directory . '/postgresql.conf' ) { 444 log_message("Error opening file at path $g_postgres_data_directory/postgresql.conf: $!"); 445 return 1; 446 } 447 if ( !flock $FILE, LOCK_EX ) { 448 log_message("Error getting lock for file postgresql.conf: $!"); 449 return 1; 450 } 451 my $archive_mode; 452 my $archive_command; 453 my $max_wal_senders; 454 my $wal_level; 455 while (<$FILE>) { 456 my $line = $_; 457 if ( $line =~ / \A \s* (\#*) archive_mode \s* = \s* (.*) (.*\n) \z /xms ) { 458 if ( defined $archive_mode ) { 459 log_message( 460 'Warning: found multiple occurrences of "archive_mode" in postgresql.conf'); 461 next; 462 } 463 $archive_mode = $2; 464 my $trailer = $3; 465 my $enabled; 466 if ( $1 =~ / \# /xms ) { 467 $enabled = 0; 468 } 469 else { 470 $enabled = 1; 471 } 472 473 if ( ( $enabled == 0 ) && $enable_wal_archiving ) { 474 $out .= 'archive_mode = on' . $trailer; 475 } 476 elsif ( ( $enabled == 1 ) && ( !$enable_wal_archiving ) ) { 477 $out .= '#archive_mode = off' . $trailer; 478 } 479 else { 480 $out .= $line; 481 } 482 } 483 elsif ( $line =~ / \A \s* (\#*) archive_command \s* = \s* ['"] (.*) ['"] (.*\n) \z /xms ) { 484 if ( defined $archive_command ) { 485 log_message( 'Warning: found multiple occurrences of ' 486 . '"archive_command" in postgresql.conf' ); 487 next; 488 } 489 $archive_command = $2; 490 my $trailer = $3; 491 my $enabled; 492 if ( $1 =~ / \# /xms ) { 493 $enabled = 0; 494 } 495 else { 496 $enabled = 1; 497 } 498 499 if ( ( $enabled == 0 ) && $enable_wal_archiving ) { 500 my $postgres_data_directory = escape_string_for_shell($g_postgres_data_directory); 501 my $archived_logs_directory = escape_string_for_shell($g_archived_logs_directory); 502 my $command = "\'$ARCHIVE_COMMAND_PATH -D \"$postgres_data_directory\" " 503 . "-w \"$archived_logs_directory\" -f %f\'"; 504 505 $out .= 'archive_command = ' . $command . $trailer; 506 } 507 elsif ( ( $enabled == 1 ) && ( !$enable_wal_archiving ) ) { 508 $out .= '#archive_command = \'\'' . $trailer; 509 } 510 else { 511 $out .= $line; 512 } 513 } 514 elsif ( $line =~ / \A \s* (\#*) max_wal_senders \s* = \s* (\d+) (.*\n) \z /xms ) { 515 if ( defined $max_wal_senders ) { 516 log_message( 'Warning: found multiple occurrences of ' 517 . '"max_wal_senders" in postgresql.conf' ); 518 next; 519 } 520 $max_wal_senders = $2; 521 my $trailer = $3; 522 my $enabled; 523 if ( $1 =~ / \# /xms ) { 524 $enabled = 0; 525 } 526 else { 527 $enabled = 1; 528 } 529 530 if ( ( $enabled == 0 ) && $enable_wal_archiving ) { 531 $out .= 'max_wal_senders = ' . $MAX_WAL_SENDERS . $trailer; 532 } 533 elsif ( ( $enabled == 1 ) && ( !$enable_wal_archiving ) ) { 534 $out .= '#max_wal_senders = 0' . $trailer; 535 } 536 else { 537 $out .= $line; 538 } 539 } 540 elsif ( $line =~ / \A \s* (\#*) wal_level \s* = \s* (\S+) (.*\n) \z /xms ) { 541 if ( defined $wal_level ) { 542 log_message( 543 'Warning: found multiple occurrences of "wal_level" in postgresql.conf'); 544 next; 545 } 546 $wal_level = $2; 547 my $trailer = $3; 548 my $enabled; 549 if ( $1 =~ / \# /xms ) { 550 $enabled = 0; 551 } 552 else { 553 $enabled = 1; 554 } 555 556 if ( ( $enabled == 0 ) && $enable_wal_archiving ) { 557 $out .= 'wal_level = archive' . $trailer; 558 } 559 elsif ( ( $enabled == 1 ) && ( !$enable_wal_archiving ) ) { 560 $out .= '#wal_level = minimal' . $trailer; 561 } 562 else { 563 $out .= $line; 564 } 565 } 566 else { 567 $out .= $line; 568 } 569 } 570 571 if (!( defined $archive_command 572 && defined $max_wal_senders 573 && defined $wal_level 574 && defined $archive_mode 575 ) 576 ) 577 { 578 log_message( 'Error: could not successfully parse postgresql.conf ' 579 . 'in order to enable WAL archiving' ); 580 return 1; 581 } 582 583 if ( !seek $FILE, 0, 0 ) { 584 log_message("Error writing postgresql.conf: $!"); 585 return 1; 586 } 587 588 print $FILE $out; 589 truncate $FILE, tell($FILE); 590 close $FILE; 591 592 return 0; 593} 594 595############################################################################# 596sub touch_dotfile { 597 my $dotfile = $g_postgres_data_directory . q{/} . $RESTORE_ON_ABSENCE_FILE; 598 if ( -e $dotfile ) { 599 my $now = time; 600 utime $now, $now, $dotfile; 601 } 602 else { 603 my $FILE; 604 if ( !open $FILE, '>', $dotfile ) { 605 log_message("Error opening dotfile for creation: $!"); 606 } 607 else { 608 close $FILE; 609 system $TMUTIL_PATH, 'addexclusion', $dotfile; 610 if ( $g_child_status != 0 ) { 611 log_message("Warning: tmutil appears to have failed: $g_child_status"); 612 } 613 } 614 } 615} 616 617############################################################################# 618sub do_backup { 619 my $backup_zip_file 620 = $g_archived_logs_directory . q{/} . $BACKUP_DIRECTORY_NAME . q{/} . $BACKUP_ZIP_FILE_NAME; 621 622 # Determine if it is time to do a new complete backup 623 if ( -e $backup_zip_file ) { 624 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime; 625 my $now = timelocal( $sec, $min, $hour, $mday, $mon, $year ); 626 my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, 627 $size, $atime, $mtime, $ctime, $blksize, $blocks 628 ) = stat $backup_zip_file; 629 my $maximum_file_age_secs = $SECONDS_PER_DAY / $NUM_COMPLETE_BACKUPS_PER_DAY; 630 if ( ( $now - $mtime ) < $maximum_file_age_secs ) { 631 632 # It's too soon for another complete backup 633 return 0; 634 } 635 } 636 637 if ( !-e $g_postgres_data_directory ) { 638 log_message('Error: missing postgres data directory for backup'); 639 return 1; 640 } 641 642 # If a temp file already exists, get rid of it, or pg_basebackup will fail 643 my $temp_backup_file 644 = $g_archived_logs_directory . q{/} 645 . $BACKUP_DIRECTORY_NAME . q{/} 646 . $BACKUP_TEMP_FILE_NAME; 647 if ( -e $temp_backup_file ) { 648 if ( !unlink $temp_backup_file ) { 649 log_message("Error unlinking temporary backup zip file: $!"); 650 return 1; 651 } 652 } 653 654 # Delete the outdated file, otherwise pg_basebackup will fail. 655 if ( -e $backup_zip_file ) { 656 if ( !unlink $backup_zip_file ) { 657 log_message("Error unlinking backup zip file: $!"); 658 return 1; 659 } 660 } 661 662 # Make a list of WAL files to delete after backup succeeds. 663 # XXX Instead of basing the list on file creation times, it would be 664 # best to untar the base backup, check the backup_label file, and base 665 # deletion on the START WAL LOCATION. However, this could use a lot of 666 # space. 667 my $LOG_DIR; 668 if ( !opendir $LOG_DIR, $g_archived_logs_directory ) { 669 log_message("Cannot open transaction log directory: $!"); 670 return 1; 671 } 672 my @files = readdir $LOG_DIR; 673 closedir $LOG_DIR; 674 675 my %file_create_times; 676 foreach my $file (@files) { 677 678 # Ignore directories - this skips '.' and '..' as well as backup directories created 679 # by pg_basebackup 680 if ( -d "$g_archived_logs_directory/$file" ) { 681 next; 682 } 683 684 # Delete all but the past few log files. When pg_basebackup is run, 685 # postgres may request to archive the most recent file, at least. 686 my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, 687 $size, $atime, $mtime, $ctime, $blksize, $blocks 688 ) = stat "$g_archived_logs_directory/$file"; 689 $file_create_times{$file} = $ctime; 690 } 691 692 system $PG_BASEBACKUP_PATH, '-Ft', '-z', '-h', $g_postgres_socket_directory, '-D', 693 "$g_archived_logs_directory/$BACKUP_DIRECTORY_NAME"; 694 if ( !-e $temp_backup_file || $g_child_status != 0 ) { 695 log_message("Error executing pg_basebackup: $g_child_status"); 696 return 1; 697 } 698 699 my $FILE; 700 if ( !open $FILE, '+<', $temp_backup_file ) { 701 log_message("Error: Can't open file to flush: $!\n"); 702 return 1; 703 } 704 705 my $return_buffer = q{}; 706 if ( !fcntl $FILE, 51, $return_buffer ) { # F_FULLFSYNC 707 log_message("Error flushing pg_basebackup output file to disk: $!"); 708 return 1; 709 } 710 711 close $temp_backup_file; 712 713 if ( !rename $temp_backup_file, $backup_zip_file ) { 714 log_message("Error renaming temp backup file: $!"); 715 } 716 717# Clean up all of the log files that existed before backup, except for the last few in case they are 718# needed later. 719# Sort matched files by file creation time in ascending order 720 my @sorted_files 721 = sort { $file_create_times{$a} <=> $file_create_times{$b} } keys %file_create_times; 722 my $arr_size = @sorted_files; 723 for ( my $i = 0; $i < ( $arr_size - 4 ); $i++ ) { 724 if ( !unlink $g_archived_logs_directory . q{/} . $sorted_files[$i] ) { 725 log_message( 726 "Warning: failed to delete WAL file $g_archived_logs_directory/$sorted_files[$i]: $!" 727 ); 728 } 729 } 730 731 return 0; 732} 733 734############################################################################# 735sub do_restore { 736 my $backup_zip_file 737 = $g_archived_logs_directory . q{/} . $BACKUP_DIRECTORY_NAME . q{/} . $BACKUP_ZIP_FILE_NAME; 738 739 if ( !-e $backup_zip_file ) { 740 if ( $g_debug_enabled ) { 741 log_message("Could not find a backup to restore at $backup_zip_file"); 742 } 743 return 1; 744 } 745 746 if ( -e $g_postgres_data_directory ) { 747 log_message('Moving aside previous data directory.'); 748 if ( -e $g_postgres_data_directory . '.previous' ) { 749 if ( !rmtree $g_postgres_data_directory . '.previous' ) { 750 log_message("Error deleting previous data directory: $!"); 751 return 1; 752 } 753 } 754 if ( !rename $g_postgres_data_directory, $g_postgres_data_directory . '.previous' ) { 755 log_message("Error moving aside previous data directory: $!"); 756 return 1; 757 } 758 } 759 760 if ( !mkdir $g_postgres_data_directory, 0700 ) { 761 log_message("Error: could not create data directory: $!"); 762 return 1; 763 } 764 765 # Unpack our backup file 766 system $GNUTAR_PATH, '-xz', '-f', $backup_zip_file, '-C', $g_postgres_data_directory; 767 if ( $g_child_status != 0 ) { 768 log_message("gnutar returned error: $g_child_status"); 769 return 1; 770 } 771 772 # Remove any existing recovery.done file 773 if ( -e $g_postgres_data_directory . '/recovery.done' ) { 774 if ( !unlink $g_postgres_data_directory . '/recovery.done' ) { 775 log_message("Error deleting recovery.done: $!"); 776 777 # We depend on this file later so continuing causes a race condition. 778 return 1; 779 } 780 } 781 782 # Remove any existing dotfile 783 if ( -e $g_postgres_data_directory . q{/} . $RESTORE_ON_ABSENCE_FILE ) { 784 if ( !unlink $g_postgres_data_directory . q{/} . $RESTORE_ON_ABSENCE_FILE ) { 785 log_message("Error removing $RESTORE_ON_ABSENCE_FILE: $!"); 786 787 # This will break future restores. 788 return 1; 789 } 790 } 791 792 # Create a recovery.conf file 793 my $archived_logs_directory = escape_string_for_shell($g_archived_logs_directory); 794 my $restore_command = "restore_command = \'/bin/cp \"$archived_logs_directory/%f\" %p\'"; 795 my $FILE; 796 if ( !open $FILE, '>', $g_postgres_data_directory . '/recovery.conf' ) { 797 log_message("Error writing a recovery.conf file: $!"); 798 return 1; 799 } 800 print $FILE "$restore_command\n"; 801 close $FILE; 802 803 # Disallow client connections for now 804 enable_connection_restriction(); 805 806 # Disable WAL archiving if it is enabled in the restored backup cluster 807 toggle_wal_archiving_state(0); 808 809 my $original_working_directory = Cwd::abs_path(); 810 if ( !chdir $g_archived_logs_directory ) { 811 log_message("chdir error: $!"); 812 } 813 814 log_message('Starting postgres for restore...'); 815 816 if ( start_postgres() == 0 ) { 817 while (1) { 818 819 # XXX: Is there any chance that this could hang indefinitely? 820 if ( -e $g_postgres_data_directory . '/recovery.done' ) { 821 log_message('Postgres recovery completed: recovery.done found'); 822 last; 823 } 824 my $output = `$PG_CTL_PATH status -D "$g_postgres_data_directory"`; 825 if ( $output !~ /server is running/ ) { 826 log_message('Postgres recovery completed: pg_ctl status shows server not running'); 827 last; 828 } 829 sleep 1; 830 } 831 } 832 833 stop_postgres(); 834 835 if ( !chdir $original_working_directory ) { 836 log_message("chdir error: $!"); 837 } 838 839 disable_connection_restriction(); 840 841 return 0; 842} 843 844############################################################################# 845# Utilities 846############################################################################# 847 848############################################################################# 849sub usage { 850 print "$MY_NAME : Apple wrapper for postgres with WAL archive management.\n"; 851 print "\tThis tool is a wrapper for postgres that enables archive\n"; 852 print "\tlevel WAL logging and handles backups and restores.\n"; 853 print "\tIt is intended to be invoked via pg_ctl.\n"; 854 print "\tThis is not intended for direct use by customers.\n"; 855 print "\n"; 856 print "Usage:\n"; 857 print "\t$MY_NAME [args to be forwarded to the real postgres]\n"; 858 print "\t$MY_NAME -a <path> [args to be forwarded to the real postgres]\n"; 859 print "\tOptions:\n"; 860 print "\t-a <path>: Specify a .plist containing an array of arguments\n"; 861 print "\t that will be forwarded to postgres, in lieu of or\n"; 862 print "\t in addition to other command line arguments.\n"; 863 print "\n"; 864} 865 866############################################################################# 867sub timestamp { 868 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime; 869 $year += 1900; 870 $mon += 1; 871 if ( $mday =~ / \A \d \z /xms ) { $mday = '0' . $mday; } 872 if ( $mon =~ / \A \d \z /xms ) { $mon = '0' . $mon; } 873 if ( $hour =~ / \A \d \z /xms ) { $hour = '0' . $hour; } 874 if ( $min =~ / \A \d \z /xms ) { $min = '0' . $min; } 875 if ( $sec =~ / \A \d \z /xms ) { $sec = '0' . $sec; } 876 877 my $ret = "$year-$mon-$mday $hour:$min:$sec"; 878 879 return $ret; 880} 881 882############################################################################# 883sub log_message { 884 885 # We currently output only to STDOUT / STDERR and launchd is configured 886 # to redirect this to the postgres log. 887 print timestamp() . q{ } . $MY_NAME . ": @_\n"; 888 889 return 0; 890} 891 892############################################################################# 893sub handle_signal { 894 $g_kill_signal = shift; 895} 896 897############################################################################# 898sub reaper { 899 my $pid; 900 while ( ( $pid = waitpid -1, WNOHANG ) > 0 ) { 901 if ( $pid == -1 ) { 902 next; 903 } 904 elsif ( defined $g_postgres_proccess_pid && $g_postgres_proccess_pid == $pid ) { 905 if ($g_debug_enabled) { 906 log_message('postgres child has died'); 907 } 908 $g_postgres_proccess_pid = 0; 909 } 910 else { 911 $g_child_status = $? >> 8; 912 } 913 } 914 $SIG{CHLD} = \&reaper; 915} 916 917############################################################################# 918# Main 919############################################################################# 920 921if ( $< == 0 ) { 922 print "postgres will not run as root. Try using a different user account.\n"; 923 usage(); 924 exit 1; 925} 926 927STDERR->autoflush(1); 928STDOUT->autoflush(1); 929 930# We want to allow for the data directory and socket directory to be specified in 931# a number of supported ways: 932# A. on the command-line as -D, -k 933# B. PGDATA may be defined in the environment 934# C. unix_socket_directory may be specified on the command-line 935# D. we support loading postgres args from a file, so they may be there 936 937@g_argv = @ARGV; 938our ( $opt_D, $opt_k ); 939getopt('D:k:'); 940 941# Try to find where the data directory has been specified 942if ( defined $opt_D ) { 943 $g_postgres_data_directory = $opt_D; 944} 945else { 946 947 # check environment for PGDATA and set the data directory if found 948 if ( defined $ENV{PGDATA} ) { 949 $g_postgres_data_directory = $ENV{PGDATA}; 950 } 951} 952 953# Try to find where the socket directory has been specified 954$g_postgres_socket_directory = $DEFAULT_SOCKET_DIR; 955if ( defined $opt_k ) { 956 $g_postgres_socket_directory = $opt_k; 957} 958else { 959 my $arr_size = @g_argv; 960 for ( my $i = 0; $i < $arr_size; $i++ ) { 961 if ( $g_argv[$i] eq '-c' ) { 962 if ( $g_argv[ $i + 1 ] =~ /unix_socket_directory=(.+)/ ) { 963 $g_postgres_socket_directory = $1; 964 } 965 elsif ( $g_argv[ $i + 1 ] =~ /log_directory=(.+)/ ) { 966 $g_postgres_log_directory = $1; 967 } 968 } 969 elsif ( $g_argv[$i] eq '-a' ) { 970 971 # This is where the config options for "server services" cluster managed by 972 # servermgr_postgres_server are found. 973 # Note '-a' is not a currently support postgres option, so we can use it. 974 # TODO: Remove this and fix the code that uses it to pass options directly. 975 $g_apple_configuration = $g_argv[ $i + 1 ]; 976 977 # We don't want to pass this on to postgres 978 splice @g_argv, $i, 2; 979 if ( ( $i - 2 ) == -2 ) { 980 $i -= 1; 981 } 982 else { 983 $i -= 2; 984 } 985 $arr_size -= 2; 986 } 987 } 988} 989 990# Load postgres arguments from file if it has been specified 991if ( defined $g_apple_configuration && $g_apple_configuration ne q{} ) { 992 if ( !-e $g_apple_configuration ) { 993 log_message('Error: received -a but could not find specified file'); 994 exit 1; 995 } 996 997 # XXX: It would be better to use Foundation here. Or not even have this option. 998 my @lines = `$PLIST_BUDDY -c 'Print :ProgramArguments' "$g_apple_configuration"`; 999 1000 # Skipping the first and last lines, which specify the class type. 1001 for ( my $i = 1; $i < $#lines; $i++ ) { 1002 1003 # Append the args to the list that we will past to postgres 1004 if ( $lines[$i] =~ / \A \s* (.+?) \s* \z /xms ) { 1005 push @g_argv, $1; 1006 } 1007 1008 # Look for the specifiers that we care about 1009 if ( $lines[$i] =~ /\A \s* unix_socket_directory=(.+?) \s* \z /xms ) { 1010 $g_postgres_socket_directory = $1; 1011 } 1012 elsif (( $lines[$i] =~ / \A \s* -k \s* \z /xms ) 1013 && ( $lines[ $i + 1 ] =~ / \A \s* (.+?) \s* \z /xms ) ) 1014 { 1015 $g_postgres_socket_directory = $1; 1016 } 1017 elsif (( $lines[$i] =~ / \A \s* -D \s* \z /xms ) 1018 && ( $lines[ $i + 1 ] =~ / \A \s* (.+?) \s* \z /xms ) ) 1019 { 1020 $g_postgres_data_directory = $1; 1021 } 1022 elsif ( $lines[$i] =~ /\A \s* log_directory=(.+?) \s* \z /xms ) { 1023 $g_postgres_log_directory = $1; 1024 } 1025 } 1026} 1027 1028# Create the log directory if it has been specified 1029if ( defined $g_postgres_log_directory && $g_postgres_log_directory ne q{} ) { 1030 if ( !-e $g_postgres_log_directory ) { 1031 if ( !mkdir $g_postgres_log_directory, 0755 ) { 1032 log_message("Could not create log directory: $!"); 1033 return 1; 1034 } 1035 } 1036} 1037 1038if ( $g_postgres_data_directory eq q{} || $g_postgres_socket_directory eq q{} ) { 1039 log_message('Error: missing required argument.'); 1040 exit 1; 1041} 1042 1043# Determine our WAL file directory based on the data directory path 1044my $base_name = basename($g_postgres_data_directory); 1045my $data_dir_name = dirname($g_postgres_data_directory); 1046 1047$g_archived_logs_directory = $data_dir_name . q{/} . 'backup'; 1048 1049if ( postgres_is_running() ) { 1050 log_message('Postgres is already running using the specified socket directory, exiting.'); 1051 exit 1; 1052} 1053 1054if ( !-e $g_archived_logs_directory ) { 1055 if ( !mkdir $g_archived_logs_directory, 0700 ) { 1056 log_message("mkdir failed for WAL file directory $g_archived_logs_directory: $!"); 1057 exit 1; 1058 } 1059} 1060 1061if ( !-e $g_postgres_data_directory . q{/} . $RESTORE_ON_ABSENCE_FILE ) { 1062 if ( !( do_restore() == 0 ) ) { 1063 if ( !-e $g_postgres_data_directory ) { 1064 log_message('Error: Restore failed and we are missing the data directory'); 1065 exit 1; 1066 } 1067 } 1068} 1069 1070# Exclude our data directory from Time Machine backups 1071system $TMUTIL_PATH, 'addexclusion', $g_postgres_data_directory; 1072if ( $g_child_status != 0 ) { 1073 log_message("Warning: tmutil appears to have failed: $g_child_status"); 1074} 1075 1076# Ensure that WAL archiving has been enabled. 1077toggle_wal_archiving_state(1); 1078 1079log_message('Starting up'); 1080if ( !( start_postgres() == 0 ) ) { 1081 log_message('Could not start postgres.'); 1082 exit 1; 1083} 1084log_message('PostgreSQL started.'); 1085 1086my $previous_heartbeat = 0; 1087while (1) { 1088 if ( $g_kill_signal ne q{} ) { 1089 if ( ( $g_kill_signal eq 'INT' ) || ( $g_kill_signal eq 'TERM' ) ) { 1090 log_message("Received signal $g_kill_signal, shutting down"); 1091 stop_postgres($g_kill_signal); 1092 exit 0; 1093 } 1094 $g_kill_signal = q{}; # empty string 1095 } 1096 1097 if ( $g_postgres_proccess_pid == 0 ) { 1098 1099 # postgres was seen to be shut down by SIGCHLD handler 1100 exit 1; 1101 } 1102 1103 my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = gmtime; 1104 my $now = timegm( $sec, $min, $hour, $mday, $mon, $year ); 1105 if ( ( $now - $previous_heartbeat ) >= $HEARTBEAT_SECS ) { 1106 do_backup(); 1107 1108 ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = gmtime; 1109 $previous_heartbeat = timegm( $sec, $min, $hour, $mday, $mon, $year ); 1110 } 1111 sleep 1; 1112} 1113 1114exit 0; 1115 1116############################################################################# 1117# This is out of place for XCode 1118sub escape_string_for_shell { 1119 my $string = shift; 1120 $string =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; 1121 return $string; 1122} 1123