Postgres.pm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. package Troglodyne::API::Postgres;
  2. use strict;
  3. use warnings;
  4. use Cpanel::LoadModule::Custom;
  5. our $dir = '/var/cpanel/logs/troglodyne/pgupgrade';
  6. sub get_postgresql_versions {
  7. Cpanel::LoadModule::Custom::load_perl_module('Troglodyne::CpPostgreSQL');
  8. my @ver_arr = ( Troglodyne::CpPostgreSQL::get_version() );
  9. my $running = eval { readlink("$dir/INSTALL_IN_PROGRESS"); } || 0;
  10. return {
  11. 'installed_version' => { 'major' => $ver_arr[0], 'minor' => $ver_arr[1] },
  12. 'minimum_supported_version' => $Troglodyne::CpPostgreSQL::MINIMUM_SUPPORTED_VERSION,
  13. 'available_versions' => \%Troglodyne::CpPostgreSQL::SUPPORTED_VERSIONS_MAP,
  14. 'eol_versions' => \%Troglodyne::CpPostgreSQL::CP_UNSUPPORTED_VERSIONS_MAP,
  15. 'install_currently_running' => $running,
  16. };
  17. }
  18. sub enable_community_repositories {
  19. Cpanel::LoadModule::Custom::load_perl_module('Troglodyne::CpPostgreSQL');
  20. require Cpanel::Sys::OS;
  21. my $centos_ver = substr( Cpanel::Sys::OS::getreleaseversion(), 0, 1 );
  22. my $repo_rpm_url = $Troglodyne::CpPostgreSQL::REPO_RPM_URLS{$centos_ver};
  23. # TODO Use Cpanel::SafeRun::Object to run the install?
  24. require Capture::Tiny;
  25. my @cmd = qw{/bin/rpm -q pgdg-redhat-repo};
  26. my ( $stdout, $stderr, $ret ) = Capture::Tiny::capture( sub {
  27. system(@cmd);
  28. });
  29. my $installed = !$ret;
  30. if( !$installed ) {
  31. @cmd = ( qw{/bin/rpm -i}, $repo_rpm_url );
  32. ( $stdout, $stderr, $ret ) = Capture::Tiny::capture( sub {
  33. system(@cmd);
  34. } );
  35. }
  36. return {
  37. 'last_yum_command' => join( " ", @cmd ),
  38. 'already_installed' => $installed,
  39. 'stdout' => $stdout,
  40. 'stderr' => $stderr,
  41. 'exit_code' => $ret,
  42. };
  43. }
  44. sub start_postgres_install {
  45. my ( $args_hr ) = @_;
  46. my $version = $args_hr->{'version'};
  47. require Cpanel::Mkdir;
  48. Cpanel::Mkdir::ensure_directory_existence_and_mode( $dir, 0711 );
  49. require Cpanel::FileUtils::Touch;
  50. my $time = time;
  51. my $lgg = "$dir/pgupgrade-to-$version-at-$time.log";
  52. Cpanel::FileUtils::Touch::touch_if_not_exists($lgg);
  53. require Cpanel::Autodie;
  54. Cpanel::Autodie::unlink_if_exists("$dir/last");
  55. require Cpanel::Chdir;
  56. {
  57. my $chdir = Cpanel::Chdir->new($dir);
  58. symlink( "pgupgrade-to-$version-at-$time.log", "last" );
  59. }
  60. # OK. We are logging, now return the log loc after kicking it off.
  61. # Yeah, yeah, I'm forking twice. who cares
  62. unlink( "$dir/INSTALL_EXIT_CODE" );
  63. require Cpanel::Daemonizer::Tiny;
  64. my $pid = Cpanel::Daemonizer::Tiny::run_as_daemon( \&_real_install, $version, $lgg );
  65. symlink( $pid, "$dir/INSTALL_IN_PROGRESS" ) if $pid;
  66. return {
  67. 'log' => $lgg,
  68. 'pid' => $pid,
  69. };
  70. }
  71. our @ROLLBACKS;
  72. sub _real_install {
  73. my ( $ver2install, $log ) = @_;
  74. @ROLLBACKS = ();
  75. my $no_period_version = $ver2install =~ s/\.//r;
  76. my @RPMS = (
  77. "postgresql$no_period_version",
  78. "postgresql$no_period_version-libs",
  79. "postgresql$no_period_version-server",
  80. "postgresql$no_period_version-devel", # For CCS
  81. );
  82. # TODO: Use Cpanel::Yum::Install based module, let all that stuff handle this "for you".
  83. open( my $lh, ">", $log ) or return _cleanup("255");
  84. select $lh;
  85. $| = 1;
  86. select $lh;
  87. # So, since I can't know what all crap to eval here,
  88. # let's just catch all the dies and throw generic
  89. # errors in this event then trigger rollback.
  90. local $SIG{__DIE__} = sub {
  91. print $lh "# [ERROR] ", @_;
  92. require Devel::StackTrace;
  93. my $trace = Devel::StackTrace->new();
  94. print $lh $trace->as_string(), "\n";
  95. _cleanup('255', $lh);
  96. die @_;
  97. };
  98. print $lh "# [INFO] Beginning install...\n";
  99. my $exit;
  100. require Cpanel::AccessIds::ReducedPrivileges;
  101. require Cpanel::SafeRun::Object;
  102. require Cpanel::Services::Enabled;
  103. # FORCE UNCACHED LOAD YOU FUCKER
  104. unlink '/root/.cpanel/datastore/_usr_bin_psql_--version';
  105. Cpanel::LoadModule::Custom::load_perl_module('Troglodyne::CpPostgreSQL');
  106. my @cur_ver = ( Troglodyne::CpPostgreSQL::get_version() );
  107. my $str_ver = join( '.', @cur_ver );
  108. my $postgres_enabled = 0;
  109. if( $str_ver + 0 < 9.3 ) {
  110. $postgres_enabled = Cpanel::Services::Enabled::is_enabled('postgresql');
  111. if( Cpanel::Services::Enabled::is_enabled('postgresql') ) {
  112. print $lh "# [INFO] Disabling postgresql during the upgrade window since it is currently enabled...\n";
  113. # Don't use Whostmgr::Services, as that bungles the __DIE__ overwrite.
  114. $exit = _saferun( $lh, qw{/usr/local/cpanel/bin/whmapi1 configureservice service=postgresql enabled=0 monitored=0} );
  115. return _cleanup("$exit", $lh) if $exit;
  116. print $lh "# [INFO] Adding 're-enable postgresql' to \@ROLLBACKS stack...\n";
  117. my $rb = sub {
  118. _saferun( $lh, qw{/usr/local/cpanel/bin/whmapi1 configureservice service=postgresql enabled=1 monitored=1} );
  119. };
  120. push @ROLLBACKS, $rb;
  121. }
  122. }
  123. else {
  124. print $lh "# [INFO] Ensuring postgresql-$str_ver is stopped during the upgrade window...\n";
  125. $exit = _saferun( $lh, qw{systemctl stop}, "postgresql-$str_ver" );
  126. return _cleanup("$exit", $lh) if $exit;
  127. print $lh "# [INFO] Adding 'restart postgresql-$str_ver' to \@ROLLBACKS stack...\n";
  128. my $rb = sub { _saferun( $lh, qw{systemctl start}, "postgresql-$str_ver" ) };
  129. push @ROLLBACKS, $rb;
  130. }
  131. # Check for CCS. Temporarily disable it if so.
  132. require Cpanel::RPM;
  133. my $ccs_installed = Cpanel::RPM->new()->get_version('cpanel-ccs-calendarserver');
  134. $ccs_installed = $ccs_installed->{'cpanel-ccs-calendarserver'};
  135. my $ccs_enabled = Cpanel::Services::Enabled::is_enabled('cpanel-ccs');
  136. if( $ccs_installed ) {
  137. print $lh "# [INFO] cpanel-ccs-calendarserver is installed.\nDisabling the service while the upgrade is in process.\n\n";
  138. $exit = _saferun( $lh, qw{/usr/local/cpanel/bin/whmapi1 configureservice service=cpanel-ccs enabled=0 monitored=0} );
  139. return _cleanup("$exit", $lh) if $exit;
  140. print $lh "# [INFO] Adding 're-enable cpanel-ccs' to \@ROLLBACKS stack...\n";
  141. my $rb = sub { _saferun( $lh, qw{/usr/local/cpanel/bin/whmapi1 configureservice service=cpanel-ccs enabled=1 monitored=1} ) };
  142. push @ROLLBACKS, $rb;
  143. }
  144. $exit = _saferun( $lh, 'yum', qw{install -y}, @RPMS );
  145. return _cleanup("$exit", $lh) if $exit;
  146. print $lh "# [INFO] Adding 'yum remove new pg version' to \@ROLLBACKS stack...\n";
  147. my $rollbck = sub {
  148. _saferun( $lh, 'yum', qw{remove -y}, @RPMS );
  149. unlink('/root/.cpanel/datastore/_usr_bin_psql_--version');
  150. };
  151. push @ROLLBACKS, $rollbck;
  152. # Init the DB
  153. my $locale = $ENV{'LANG'} || 'en_US.UTF-8';
  154. {
  155. my $pants_on_the_ground = Cpanel::AccessIds::ReducedPrivileges->new('postgres');
  156. $exit = _saferun( $lh, "/usr/pgsql-$ver2install/bin/initdb", '--locale', $locale, '-E', 'UTF8', '-D', "/var/lib/pgsql/$ver2install/data/" );
  157. }
  158. return _cleanup("$exit", $lh) if $exit;
  159. # probably shouldn't do it this way. Whatever
  160. print $lh "# [INFO] Adding 'Clean up new pgdata dir' to \@ROLLBACKS stack...\n";
  161. my $rd_rllr = sub { _saferun( $lh, qw{rm -rf}, "/var/lib/pgsql/$ver2install/data" ) };
  162. push @ROLLBACKS, $rd_rllr;
  163. require File::Slurper;
  164. require File::Copy;
  165. # Move some bullcrap out of the way if we're on old PGs
  166. if( $str_ver + 0 < 9.3 ) {
  167. print $lh "# [INFO] Installed version is less than 9.3 ($str_ver), Implementing workaround in pg_ctl to ensure pg_upgrade works...\n";
  168. print $lh "# [BACKUP] Backing up /usr/bin/pg_ctl to /usr/bin/pg_ctl.orig\n";
  169. File::Copy::cp('/usr/bin/pg_ctl','/usr/bin/pg_ctl.orig') or do {
  170. print $lh "Backup of /usr/bin/pg_ctl to /usr/bin/pg_ctl.orig failed: $!\n";
  171. return _cleanup("255", $lh);
  172. };
  173. chmod(0755, '/usr/bin/pg_ctl.orig');
  174. print $lh "# [INFO] Adding 'Restore old pg_ctl process to \@ROLLBACKS stack...\n";
  175. my $rb = sub { File::Copy::mv('/usr/bin/pg_ctl.orig','/usr/bin/pg_ctl'); };
  176. push @ROLLBACKS, $rb;
  177. my $pg_ctl_contents = "#!/bin/bash\n\"\$0\".orig \"\${@/unix_socket_directory/unix_socket_directories}\"";
  178. File::Slurper::write_binary( "/usr/bin/pg_ctl", $pg_ctl_contents );
  179. chmod( 0755, '/usr/bin/pg_ctl' );
  180. print $lh "# [INFO] Workaround should be in place now. Proceeding with pg_upgrade.\n\n";
  181. }
  182. print $lh "# [INFO] Setting things up for pg_upgrade -- delete postmaster.pid, ensure .pgpass in place.\n";
  183. require Cpanel::Chdir;
  184. # Upgrade the cluster
  185. # /usr/pgsql-9.6/bin/pg_upgrade --old-datadir /var/lib/pgsql/data/ --new-datadir /var/lib/pgsql/9.6/data/ --old-bindir /usr/bin/ --new-bindir /usr/pgsql-9.6/bin/
  186. my ( $old_datadir, $old_bindir ) = ( $str_ver + 0 < 9.5 ) ? ( '/var/lib/pgsql/data', '/usr/bin' ) : ( "/var/lib/pgsql/$str_ver/data/", "/usr/pgsql-$str_ver/bin/" );
  187. unlink '/var/lib/pgsql/data/postmaster.pid';
  188. # Copy over the .pgpass file for the postgres user so that it knows how to connect as itself (oof)
  189. File::Copy::cp('/root/.pgpass', '/var/lib/pgsql/.pgpass');
  190. print $lh "# [INFO] Adding 'cleanup .pgpass file to \@ROLLBACKS stack...\n";
  191. my $reb = sub { unlink '/var/lib/pgsql/.pgpass' };
  192. push @ROLLBACKS, $reb;
  193. require Cpanel::SafetyBits::Chown;
  194. Cpanel::SafetyBits::Chown::safe_chown( 'postgres', 'postgres', '/var/lib/pgsql/.pgpass' );
  195. chmod( 0600, '/var/lib/pgsql/.pgpass' );
  196. print "# [INFO] Now running pg_upgrade on the default cluster...\n";
  197. {
  198. my $pants_on_the_ground = Cpanel::AccessIds::ReducedPrivileges->new('postgres');
  199. my $cd_obj = Cpanel::Chdir->new('/var/lib/pgsql');
  200. $exit = _saferun( $lh, "/usr/pgsql-$ver2install/bin/pg_upgrade",
  201. '-d', $old_datadir,
  202. '-D', "/var/lib/pgsql/$ver2install/data/",
  203. '-b', $old_bindir,
  204. '-B', "/usr/pgsql-$ver2install/bin/",
  205. );
  206. }
  207. return _cleanup("$exit", $lh) if $exit;
  208. # Start the server.
  209. print $lh "# [INFO] Starting up postgresql-$ver2install...\n";
  210. $exit = _saferun( $lh, qw{systemctl start}, "postgresql-$ver2install" );
  211. return _cleanup("$exit", $lh) if $exit;
  212. if( $ccs_installed ) {
  213. my $ccs_pg_datadir = '/opt/cpanel-ccs/data/Data/Database/cluster';
  214. print $lh "# [INFO] Old PG datadir is being moved to '$ccs_pg_datadir.$str_ver'...\n";
  215. File::Copy::mv( $ccs_pg_datadir, "$ccs_pg_datadir.$str_ver" );
  216. mkdir($ccs_pg_datadir);
  217. chmod( 0700, $ccs_pg_datadir );
  218. Cpanel::SafetyBits::Chown::safe_chown( 'cpanel-ccs', 'cpanel-ccs', $ccs_pg_datadir );
  219. print $lh "# [INFO] Adding 'restore old CCS cluster' to \@ROLLBACKS stack...\n";
  220. my $rb = sub {
  221. require File::Path;
  222. File::Path::remove_tree($ccs_pg_datadir, { 'error' => \my $err } );
  223. print $lh join( "\n", @$err ) if ( $err && @$err );
  224. File::Copy::mv( "$ccs_pg_datadir.$str_ver", $ccs_pg_datadir );
  225. };
  226. push @ROLLBACKS, $rb;
  227. print $lh "# [INFO] Now initializing the new PG cluster for cpanel-ccs-calendarserver...\n";
  228. unlink '/opt/cpanel-ccs/data/Data/Database/cluster/postmaster.pid';
  229. # Init the DB
  230. {
  231. my $pants_on_the_ground = Cpanel::AccessIds::ReducedPrivileges->new('cpanel-ccs');
  232. $exit = _saferun( $lh, "/usr/pgsql-$ver2install/bin/initdb", '-D', $ccs_pg_datadir, '-U', 'caldav', '--locale', $locale, '-E', 'UTF8' );
  233. }
  234. return _cleanup("$exit", $lh) if $exit;
  235. print $lh "# [INFO] Now upgrading the PG cluster for cpanel-ccs-calendarserver...\n";
  236. # Upgrade the DB
  237. {
  238. my $pants_on_the_ground = Cpanel::AccessIds::ReducedPrivileges->new('cpanel-ccs');
  239. my $cd_obj = Cpanel::Chdir->new('/opt/cpanel-ccs');
  240. $exit = _saferun( $lh, "/usr/pgsql-$ver2install/bin/pg_upgrade",
  241. '-d', "$ccs_pg_datadir.$str_ver",
  242. '-D', $ccs_pg_datadir,
  243. '-b', $old_bindir,
  244. '-B', "/usr/pgsql-$ver2install/bin/",
  245. qw{-U caldav},
  246. );
  247. }
  248. return _cleanup("$exit", $lh) if $exit;
  249. }
  250. # At this point we're at the point where we don't need to restore. Just clean up.
  251. print $lh "# [INFO] Clearing \@ROLLBACKS stack, as we have cleared the necessary checkpoint.\n";
  252. @ROLLBACKS = ();
  253. if( $str_ver + 0 < 9.4 ) {
  254. print $lh "# [INFO] Workaround resulted in successful start of the server. Reverting workaround changes to pg_ctl...\n\n";
  255. rename( '/usr/bin/pg_ctl.orig', '/usr/bin/pg_ctl' ) or do {
  256. print $lh "# [ERROR] Restore of /usr/bin/pg_ctl.orig to /usr/bin/pg_ctl failed: $!\n";
  257. return _cleanup("255", $lh);
  258. };
  259. }
  260. print $lh "# [INFO] Now cleaning up old postgresql version...\n";
  261. my $svc2remove = ( $str_ver + 0 < 9.5 ) ? 'postgresql' : "postgresql-$str_ver";
  262. $exit = _saferun( $lh, qw{systemctl disable}, $svc2remove );
  263. return _cleanup("$exit", $lh) if $exit;
  264. $exit = _saferun( $lh, qw{yum -y remove}, $svc2remove );
  265. return _cleanup("$exit", $lh) if $exit;
  266. print $lh "# [INFO] Now enabling postgresql-$ver2install on startup...\n";
  267. $exit = _saferun( $lh, qw{systemctl enable}, "postgresql-$ver2install" );
  268. return _cleanup("$exit", $lh) if $exit;
  269. # Update alternatives. Should be fine to use --auto, as no other alternatives will exist for the installed version.
  270. # Create alternatives for pg_ctl, etc. as those don't get made by the RPM.
  271. print $lh "# [INFO] Updating alternatives to ensure the newly installed version is considered canonical...\n";
  272. my %prio_map = (
  273. '95' => '95',
  274. '96' => '96',
  275. '10' => '100',
  276. '11' => '110',
  277. '12' => '120',
  278. );
  279. my @normie_alts = qw{pg_ctl initdb pg_config pg_upgrade};
  280. my @manual_alts = qw{clusterdb createdb createuser dropdb droplang dropuser pg_basebackup pg_dump pg_dumpall pg_restore psql};
  281. foreach my $alt ( @normie_alts ) {
  282. my @cmd = ( qw{update-alternatives --install}, "/usr/bin/$alt", "pgsql-$alt", "/usr/pgsql-$ver2install/bin/$alt", $prio_map{$no_period_version} );
  283. $exit = _saferun( $lh, @cmd );
  284. return _cleanup("$exit", $lh) if $exit;
  285. @cmd = ( qw{update-alternatives --auto}, "pgsql-$alt" );
  286. $exit = _saferun( $lh, @cmd );
  287. return _cleanup("$exit", $lh) if $exit;
  288. }
  289. foreach my $alt ( @manual_alts ) {
  290. my @cmd = ( qw{update-alternatives --auto}, "pgsql-$alt" );
  291. $exit = _saferun( $lh, @cmd );
  292. return _cleanup("$exit", $lh) if $exit;
  293. @cmd = ( qw{update-alternatives --auto}, "pgsql-${alt}man" );
  294. $exit = _saferun( $lh, @cmd );
  295. return _cleanup("$exit", $lh) if $exit;
  296. }
  297. print $lh "# [INFO] Writing new .bash_profile for the 'postgres' user...\n";
  298. my $bash_profile = "[ -f /etc/profile ] && source /etc/profile
  299. PGDATA=/var/lib/pgsql/$ver2install/data
  300. export PGDATA
  301. [ -f /var/lib/pgsql/.pgsql_profile ] && source /var/lib/pgsql/.pgsql_profile
  302. export PATH=\$PATH:/usr/pgsql-$ver2install/bin\n";
  303. File::Slurper::write_text( '/var/lib/pgsql/.bash_profile', $bash_profile );
  304. if($ccs_installed && $ccs_enabled) {
  305. print $lh "# [INFO] Re-Enabling cpanel-ccs-calendarserver...\n";
  306. $exit = _saferun( $lh, qw{/usr/local/cpanel/bin/whmapi1 configureservice service=cpanel-ccs enabled=1 monitored=1} );
  307. return _cleanup("$exit", $lh) if $exit;
  308. }
  309. # XXX Now the postgres service appears as "disabled" for cPanel's sake. Frowny faces everywhere.
  310. # Not sure how to fix yet.
  311. if($postgres_enabled) {
  312. print $lh "# [INFO] Re-Enabling postgres services for cPanel...\n";
  313. print $lh "# [TODO] Actually do this!\n";
  314. }
  315. local $SIG{__DIE__} = 'DEFAULT';
  316. return _cleanup("0", $lh);
  317. }
  318. sub _saferun {
  319. my ( $lh, $prog, @args ) = @_;
  320. print $lh "# [EXEC] $prog " . join( ' ', @args ) . "\n";
  321. my $run_result = Cpanel::SafeRun::Object->new(
  322. 'program' => $prog,
  323. 'args' => [ @args ],
  324. 'stdout' => $lh,
  325. 'stderr' => $lh,
  326. );
  327. my $exit = $run_result->error_code() || 0;
  328. return $exit;
  329. }
  330. sub _cleanup {
  331. my ( $code, $lh ) = @_;
  332. # Do rollbacks in reverse order
  333. if($lh) {
  334. print $lh "# [ERROR] Encountered failure during install!\n" if $code;
  335. print $lh "# [INFO] Now executing rollbacks...\n" if( $code && @ROLLBACKS);
  336. }
  337. foreach my $rb ( reverse @ROLLBACKS ) {
  338. local $@;
  339. eval { $rb->(); };
  340. my $exit = $@ ? 255 : 0;
  341. if($exit) {
  342. $code = $exit;
  343. last;
  344. }
  345. }
  346. # Signal completion
  347. eval { symlink( $code, "$dir/INSTALL_EXIT_CODE" ); };
  348. unlink("$dir/INSTALL_IN_PROGRESS");
  349. return;
  350. }
  351. # Elegance??? Websocket??? Nah. EZ mode actibated
  352. sub get_latest_upgradelog_messages {
  353. my ( $args_hr ) = @_;
  354. my $child_exit;
  355. my $in_progress = -l "$dir/INSTALL_IN_PROGRESS";
  356. if(!$in_progress) {
  357. $child_exit = readlink("$dir/INSTALL_EXIT_CODE");
  358. }
  359. # XXX validate log arg? Don't want arbitrary file reads?
  360. # read from it using seek and tell to control
  361. open( my $rh, "<", $args_hr->{'log'} );
  362. seek( $rh, $args_hr->{'start'}, 0 ) if $args_hr->{'start'};
  363. my $content = '';
  364. while( my $line = <$rh> ) {
  365. $content .= $line;
  366. }
  367. my $pos = tell($rh);
  368. close($rh);
  369. return {
  370. 'in_progress' => $in_progress,
  371. 'child_exit' => $child_exit,
  372. 'next' => $pos,
  373. 'new_content' => $content,
  374. }
  375. }
  376. 1;