From b6003b6d3ffc254f5ca8bb9fa6ff1cf6304a052b Mon Sep 17 00:00:00 2001 From: Travis Holloway Date: Wed, 4 Dec 2024 16:08:24 -0600 Subject: [PATCH] Add component to manage port 1022 for do-release-upgrade Case RE-943: When performing an elevation from u20->u22, we rely on the do-release-upgrade script to perform the OS upgrade. do-release-upgrade will create a secondary SSH connection on port 1022 as a fallback mechanism in the event that the primary SSH connection is disrupted during the upgrade process. This change ensures that port 1022 is open before executing do-release-upgrade so that the fallback method will work if it is required during the upgrade. It also ensures that port 1022 is closed once the upgrade is complete if it was closed before the upgrade started. Changelog: Add component to manage port 1022 for ELevations that rely on the do-release-upgrade script to perform the OS upgrade --- elevate-cpanel | 86 +++++++++++- lib/Elevate/Components.pm | 2 + lib/Elevate/Components/Ufw.pm | 85 ++++++++++++ script/elevate-cpanel.PL | 10 +- t/components-Ufw.t | 248 ++++++++++++++++++++++++++++++++++ 5 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 lib/Elevate/Components/Ufw.pm create mode 100644 t/components-Ufw.t diff --git a/elevate-cpanel b/elevate-cpanel index 3ef4df0a..31c1bf0d 100755 --- a/elevate-cpanel +++ b/elevate-cpanel @@ -56,6 +56,7 @@ BEGIN { # Suppress load of all of these at earliest point. $INC{'Elevate/Components/RpmDB.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/SSH.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/Softaculous.pm'} = 'script/elevate-cpanel.PL.static'; + $INC{'Elevate/Components/Ufw.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/UnconvertedModules.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/UpdateReleaseUpgrades.pm'} = 'script/elevate-cpanel.PL.static'; $INC{'Elevate/Components/UpdateSystem.pm'} = 'script/elevate-cpanel.PL.static'; @@ -338,6 +339,7 @@ BEGIN { # Suppress load of all of these at earliest point. use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); + use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -403,6 +405,7 @@ BEGIN { # Suppress load of all of these at earliest point. RmMod RpmDB Softaculous + Ufw UnconvertedModules UpdateReleaseUpgrades UpdateSystem @@ -6386,6 +6389,79 @@ EOS } # --- END lib/Elevate/Components/Softaculous.pm +{ # --- BEGIN lib/Elevate/Components/Ufw.pm + + package Elevate::Components::Ufw; + + use cPstrict; + + use Elevate::OS (); + use Elevate::StageFile (); + + # use Log::Log4perl qw(:easy); + INIT { Log::Log4perl->import(qw{:easy}); } + + # use Elevate::Components::Base(); + our @ISA; + BEGIN { push @ISA, qw(Elevate::Components::Base); } + + use constant UFW => '/usr/sbin/ufw'; + + sub pre_distro_upgrade ($self) { + return unless $self->upgrade_distro_manually(); # skip when --upgrade-distro-manually is provided + return unless Elevate::OS::needs_do_release_upgrade(); + + if ( !-x UFW ) { + my $ufw = UFW; + WARN( <<~"EOS" ); + '$ufw' is either missing or not executable on this server. Unable to + ensure that port 1022 is open as a secondary ssh option for + do-release-upgrade. + EOS + + return; + } + + my $current_status = $self->ssystem_capture_output( UFW, 'status' ); + my $is_active = grep { $_ =~ m/^Status:\sactive$/ } @{ $current_status->{stdout} }; + my $is_open = grep { $_ =~ m{^1022/tcp.*ALLOW.*Anywhere} } @{ $current_status->{stdout} }; + + my $data = { + is_active => $is_active, + is_open => $is_open, + }; + + Elevate::StageFile::update_stage_file( { ufw => $data } ); + + return if $is_active && $is_open; + + $self->ssystem_and_die( UFW, 'allow', '1022/tcp' ); + + $is_active ? $self->ssystem_and_die( UFW, 'reload' ) : $self->ssystem_and_die( UFW, '--force', 'enable' ); + + return; + } + + sub post_distro_upgrade ($self) { + my $ufw_data = Elevate::StageFile::read_stage_file( 'ufw', '' ); + + return unless ref $ufw_data && ref $ufw_data eq 'HASH'; + + return if $ufw_data->{is_active} && $ufw_data->{is_open}; + + $self->ssystem_and_die( UFW, 'delete', 'allow', '1022/tcp' ); + + return if $ufw_data->{is_active}; + + $self->ssystem_and_die( UFW, 'disable' ); + + return; + } + + 1; + +} # --- END lib/Elevate/Components/Ufw.pm + { # --- BEGIN lib/Elevate/Components/UnconvertedModules.pm package Elevate::Components::UnconvertedModules; @@ -10188,6 +10264,7 @@ use Elevate::Components::RmMod (); use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); +use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -10928,6 +11005,10 @@ sub run_stage_3 ($self) { $self->run_once('run_final_components_pre_distro_upgrade'); + # The server should not reboot between executing this + # and executing do-release-upgrade + $self->run_component_once( 'Ufw' => 'pre_distro_upgrade' ); + if ( !$self->upgrade_distro_manually() ) { return $self->_request_to_upgrade_distro_manually(); } @@ -11040,8 +11121,9 @@ sub run_stage_4 ($self) { $stash->{stage4} //= {}; # run once each blocks - $self->run_component_once( 'Lists', => 'post_distro_upgrade' ); - $self->run_component_once( 'RpmDB', => 'post_distro_upgrade' ); + $self->run_component_once( 'Ufw' => 'post_distro_upgrade' ); + $self->run_component_once( 'Lists' => 'post_distro_upgrade' ); + $self->run_component_once( 'RpmDB' => 'post_distro_upgrade' ); $self->run_once( restore_cpanel_services => sub { diff --git a/lib/Elevate/Components.pm b/lib/Elevate/Components.pm index 0ba4eca1..86f3779c 100644 --- a/lib/Elevate/Components.pm +++ b/lib/Elevate/Components.pm @@ -60,6 +60,7 @@ use Elevate::Components::RmMod (); use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); +use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -128,6 +129,7 @@ our @NOOP_CHECKS = qw{ RmMod RpmDB Softaculous + Ufw UnconvertedModules UpdateReleaseUpgrades UpdateSystem diff --git a/lib/Elevate/Components/Ufw.pm b/lib/Elevate/Components/Ufw.pm new file mode 100644 index 00000000..2bf51128 --- /dev/null +++ b/lib/Elevate/Components/Ufw.pm @@ -0,0 +1,85 @@ +package Elevate::Components::Ufw; + +=encoding utf-8 + +=head1 NAME + +Elevate::Components::Ufw + +=head2 check + +noop + +=head2 pre_distro_upgrade + +Open port 1022 for upgrades using do-release-upgrade + +=head2 post_distro_upgrade + +Close port 1022 for upgrades using do-release-upgrade + +=cut + +use cPstrict; + +use Elevate::OS (); +use Elevate::StageFile (); + +use Log::Log4perl qw(:easy); + +use parent qw{Elevate::Components::Base}; + +use constant UFW => '/usr/sbin/ufw'; + +sub pre_distro_upgrade ($self) { + return unless $self->upgrade_distro_manually(); # skip when --upgrade-distro-manually is provided + return unless Elevate::OS::needs_do_release_upgrade(); + + if ( !-x UFW ) { + my $ufw = UFW; + WARN( <<~"EOS" ); + '$ufw' is either missing or not executable on this server. Unable to + ensure that port 1022 is open as a secondary ssh option for + do-release-upgrade. + EOS + + return; + } + + my $current_status = $self->ssystem_capture_output( UFW, 'status' ); + my $is_active = grep { $_ =~ m/^Status:\sactive$/ } @{ $current_status->{stdout} }; + my $is_open = grep { $_ =~ m{^1022/tcp.*ALLOW.*Anywhere} } @{ $current_status->{stdout} }; + + my $data = { + is_active => $is_active, + is_open => $is_open, + }; + + Elevate::StageFile::update_stage_file( { ufw => $data } ); + + return if $is_active && $is_open; + + $self->ssystem_and_die( UFW, 'allow', '1022/tcp' ); + + $is_active ? $self->ssystem_and_die( UFW, 'reload' ) : $self->ssystem_and_die( UFW, '--force', 'enable' ); + + return; +} + +sub post_distro_upgrade ($self) { + my $ufw_data = Elevate::StageFile::read_stage_file( 'ufw', '' ); + + return unless ref $ufw_data && ref $ufw_data eq 'HASH'; + + return if $ufw_data->{is_active} && $ufw_data->{is_open}; + + $self->ssystem_and_die( UFW, 'delete', 'allow', '1022/tcp' ); + + return if $ufw_data->{is_active}; + + $self->ssystem_and_die( UFW, 'disable' ); + + return; +} + +1; diff --git a/script/elevate-cpanel.PL b/script/elevate-cpanel.PL index dd9847c9..e02b3934 100755 --- a/script/elevate-cpanel.PL +++ b/script/elevate-cpanel.PL @@ -280,6 +280,7 @@ use Elevate::Components::RmMod (); use Elevate::Components::RpmDB (); use Elevate::Components::SSH (); use Elevate::Components::Softaculous (); +use Elevate::Components::Ufw (); use Elevate::Components::UnconvertedModules (); use Elevate::Components::UpdateReleaseUpgrades (); use Elevate::Components::UpdateSystem (); @@ -1020,6 +1021,10 @@ sub run_stage_3 ($self) { $self->run_once('run_final_components_pre_distro_upgrade'); + # The server should not reboot between executing this + # and executing do-release-upgrade + $self->run_component_once( 'Ufw' => 'pre_distro_upgrade' ); + if ( !$self->upgrade_distro_manually() ) { return $self->_request_to_upgrade_distro_manually(); } @@ -1132,8 +1137,9 @@ sub run_stage_4 ($self) { $stash->{stage4} //= {}; # run once each blocks - $self->run_component_once( 'Lists', => 'post_distro_upgrade' ); - $self->run_component_once( 'RpmDB', => 'post_distro_upgrade' ); + $self->run_component_once( 'Ufw' => 'post_distro_upgrade' ); + $self->run_component_once( 'Lists' => 'post_distro_upgrade' ); + $self->run_component_once( 'RpmDB' => 'post_distro_upgrade' ); $self->run_once( restore_cpanel_services => sub { diff --git a/t/components-Ufw.t b/t/components-Ufw.t new file mode 100644 index 00000000..362cd1de --- /dev/null +++ b/t/components-Ufw.t @@ -0,0 +1,248 @@ +#!/usr/local/cpanel/3rdparty/bin/perl + +# Copyright 2024 WebPros International, LLC +# All rights reserved. +# copyright@cpanel.net http://cpanel.net +# This code is subject to the cPanel license. Unauthorized copying is prohibited. + +package test::cpev::components; + +use FindBin; + +use Test2::V0; +use Test2::Tools::Explain; +use Test2::Plugin::NoWarnings; +use Test2::Tools::Exception; + +use Test::MockFile 0.032; +use Test::MockModule qw/strict/; + +use lib $FindBin::Bin . "/lib"; +use Test::Elevate; + +use cPstrict; + +my $ufw = cpev->get_component('Ufw'); +my $mock_ufw = Test::MockModule->new('Elevate::Components::Ufw'); + +my $ssystem_and_die_params = []; +$mock_ufw->redefine( + ssystem_and_die => sub { + shift; + my @args = @_; + push @$ssystem_and_die_params, \@args; + return; + }, +); + +my $mock_stagefile = Test::MockModule->new('Elevate::StageFile'); +$mock_stagefile->redefine( + update_stage_file => sub { die "should not be called\n"; }, +); + +{ + note "checking pre_distro_upgrade"; + + set_os_to('ubuntu'); + + $mock_ufw->redefine( + upgrade_distro_manually => 0, + ); + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if the user is updating the OS' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + no_messages_seen(); + + $mock_ufw->redefine( + upgrade_distro_manually => 1, + ); + + set_os_to('cent'); + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if the upgrade method is NOT do-release-upgrade' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + no_messages_seen(); + + set_os_to('ubuntu'); + + my $mock_sbin_ufw = Test::MockFile->file( '/usr/sbin/ufw', '' ); + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if ufw is not an executable file' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + message_seen( WARN => qr/Unable to\nensure that port 1022 is open as a secondary ssh option/ ); + no_messages_seen(); + + chmod 0755, '/usr/sbin/ufw'; + + my $ssystem_stdout = []; + $mock_ufw->redefine( + ssystem_capture_output => sub { + return { + stdout => $ssystem_stdout, + }; + }, + ); + + my $stage_data = {}; + $mock_stagefile->redefine( + update_stage_file => sub { + $stage_data = shift @_; + return; + }, + ); + + $ssystem_stdout = [ + 'Status: active', + '1022/tcp ALLOW Anywhere' + ]; + + is( $ufw->pre_distro_upgrade(), undef, 'Returns early if the firewall is active and port 1022 is open' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + is( + $stage_data, + { + ufw => { + is_active => 1, + is_open => 1, + }, + }, + 'The stage file was updated with the expected data' + ) or diag explain $stage_data; + no_messages_seen(); + + $ssystem_stdout = [ + 'Status: active', + ]; + + is( $ufw->pre_distro_upgrade(), undef, 'Opens the port and reloads the firewall when it is active but the port is not open' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'allow', + '1022/tcp', + ], + [ + '/usr/sbin/ufw', + 'reload', + ], + ], + 'The expected system calls were made', + ); + is( + $stage_data, + { + ufw => { + is_active => 1, + is_open => 0, + }, + }, + 'The stage file was updated with the expected data' + ) or diag explain $stage_data; + no_messages_seen(); + + $ssystem_stdout = []; + $ssystem_and_die_params = []; + + is( $ufw->pre_distro_upgrade(), undef, 'Opens the port and activates the firewall when the port is not open and the firewall is not active' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'allow', + '1022/tcp', + ], + [ + '/usr/sbin/ufw', + '--force', + 'enable', + ], + ], + 'The expected system calls were made', + ); + is( + $stage_data, + { + ufw => { + is_active => 0, + is_open => 0, + }, + }, + 'The stage file was updated with the expected data' + ) or diag explain $stage_data; + no_messages_seen(); +} + +{ + note "testing post_distro_upgrade"; + + set_os_to('ubuntu'); + + my $stage_data = ''; + $mock_stagefile->redefine( + read_stage_file => sub { + return $stage_data; + }, + ); + + $ssystem_and_die_params = []; + + is( $ufw->post_distro_upgrade(), undef, 'returns early if there is no stage data for ufw' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + + $stage_data = { + is_active => 1, + is_open => 1, + }; + + is( $ufw->post_distro_upgrade(), undef, 'Returns early if the firewall was active and the port was open before we started' ); + is( $ssystem_and_die_params, [], 'No system commands were called' ); + + $stage_data = { + is_active => 1, + is_open => 0, + }; + + is( $ufw->post_distro_upgrade(), undef, 'Closes the port and returns if the firewall was active prior to starting and the port was NOT open prior to starting' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'delete', + 'allow', + '1022/tcp', + ], + ], + 'The expected system calls were made', + ); + + $stage_data = { + is_active => 0, + is_open => 0, + }; + + $ssystem_and_die_params = []; + + is( $ufw->post_distro_upgrade(), undef, 'Closes the port and disables the firewall if the firewall was disabled and the port was closed prior to starting' ); + is( + $ssystem_and_die_params, + [ + [ + '/usr/sbin/ufw', + 'delete', + 'allow', + '1022/tcp', + ], + [ + '/usr/sbin/ufw', + 'disable', + ], + ], + 'The expected system calls were made', + ); +} + +done_testing();