package InstallerUbuntu; # Copyright 2025 WebPros International, LLC # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited. use strict; use warnings; use CpanelLogger; use CpanelMySQL (); use Installer (); our @ISA = qw/Installer/; sub distro_type { return 'debian' } # Ubuntu support for public installs of 20.04 has been dropped and there is a separate installer. use constant CPANEL_UBUNTU_SUPPORT => [qw{22 24}]; sub check_system_support { my ($self) = @_; $self->SUPER::check_system_support; my $distro_name = $self->distro_name; my $distro_major = $self->distro_major; if ( $distro_name ne 'ubuntu' ) { return $self->invalid_system("cPanel, L.L.C. does not support $distro_name for new installations."); } return if ( grep { $distro_major == $_ } @{ CPANEL_UBUNTU_SUPPORT() } ); return $self->invalid_system("cPanel, L.L.C. does not support $distro_name version $distro_major."); } sub check_networking_scripts { # We don't do this check on ubuntu?? return; } sub check_system_files { my ($self) = @_; $self->SUPER::check_system_files; # Unset the kernel flag that prevents even root from accessing other users files in /tmp Common::ssystem( "sysctl", "--ignore", "fs.protected_regular=0" ); # Then configure this to be the default upon reboot my $prot_file = '/usr/lib/sysctl.d/protect-links.conf'; my @orig_prot_file_contents; if ( -f $prot_file ) { open( my $prot_rd_fh, '<', $prot_file ); while (<$prot_rd_fh>) { push( @orig_prot_file_contents, $_ ); } close($prot_rd_fh); open( my $prot_wr_fh, '>', $prot_file ); foreach my $line (@orig_prot_file_contents) { if ( $line =~ m/fs\.protected_regular/ ) { print $prot_wr_fh "fs.protected_regular = 0\n"; } else { print $prot_wr_fh "$line"; } } close($prot_wr_fh); } verify_usrmerge_is_installed(); # Configure alternate temp dir for debconf since /tmp is often mounted noexec mkdir '/root/tmp'; open( my $debconf_fh, '>>', '/etc/apt/apt.conf.d/50extracttemplates' ); print $debconf_fh "APT\n{\n ExtractTemplates\n\t\{\n\t\tTempDir /root/tmp;\n\t};\n};\n"; close($debconf_fh); # Check if libc6 package is properly installed my $dpkg_exit_code = Common::ssystem( qw{/usr/bin/dpkg -s libc6}, { ignore_errors => 1, quiet => 1 } ); if ( $dpkg_exit_code != 0 ) { ERROR( q{Your operating system's package update method } . qq{(apt) could not locate the libc6 package. } . q{This is an indication of an improper setup. } . q{You must correct this error before you proceed. } ); DEBUG("dpkg -s libc6 failed with exit code: $dpkg_exit_code"); FATAL("\n\n"); } # This package is needed for File::FcntlLock, used by installd/apt-get-wait $self->apt_nohang_ssystem( '/usr/bin/apt-get', 'install', '-y', 'libfile-fcntllock-perl' ); # CPANEL-48815: Pre-flight check: Verify keyserver connectivity for MariaDB GPG keys # This must happen early to catch connectivity issues before the main installation $self->check_keyserver_connectivity(); return; } # This system clobbers resolv.conf even if you update it manually. sub setup_and_check_resolv_conf { my ($self) = @_; INFO("Checking if systemd-resolved is enabled..."); my $systemd_resolved_enabled = !Common::ssystem( qw{systemctl is-enabled systemd-resolved}, { ignore_errors => 1 } ); # Only take action if systemd-resolved is actually enabled (exit code 0 means enabled) if ($systemd_resolved_enabled) { WARN("systemd-resolved is enabled and will be disabled to prevent DNS conflicts during installation"); INFO("Disabling systemd-resolved..."); Common::ssystem(qw{systemctl disable --now systemd-resolved}); unlink '/etc/resolv.conf'; # Check if cloud-init has network configuration with DNS settings my $cloud_network_config = '/etc/cloud/cloud.cfg.d/99_network.cfg'; my $use_cloud_init = 0; if ( -f $cloud_network_config ) { if ( open( my $cloud_fh, '<', $cloud_network_config ) ) { my $config_content = do { local $/; <$cloud_fh> }; close($cloud_fh); # Check if both resolv_conf and nameservers patterns are present if ( $config_content =~ /resolv_conf/ && $config_content =~ /nameservers/ ) { INFO("Found cloud-init network configuration with DNS settings at /etc/cloud/cloud.cfg.d/99_network.cfg"); $use_cloud_init = 1; } } } if ($use_cloud_init) { # Use cloud-init to regenerate resolv.conf from cloud configuration INFO("Re-running cloud-init to regenerate /etc/resolv.conf from cloud configuration"); Common::ssystem(qw{cloud-init single --name cc_resolv_conf}); INFO("Successfully regenerated /etc/resolv.conf using cloud-init DNS configuration"); } else { # Fallback to hardcoded DNS servers INFO("No suitable cloud-init DNS configuration found, configuring fallback DNS servers"); if ( open( my $fh, '>', '/etc/resolv.conf' ) ) { print $fh "nameserver 1.1.1.1\nnameserver 8.8.8.8\n"; close($fh); INFO("Replaced /etc/resolv.conf with fallback DNS servers: 1.1.1.1 and 8.8.8.8"); } else { WARN( 'Could not create new /etc/resolv.conf : ' . $! ); } } } else { INFO("systemd-resolved is not enabled, leaving existing DNS configuration unchanged"); } return $self->SUPER::setup_and_check_resolv_conf; } sub install_basic_precursor_packages { my ($self) = @_; INFO("Installing packages needed to download and run the cPanel initial install."); # Assure wget/bzip2/gpg are installed for centhat. These packages are needed prior to sysup my @packages_to_install = qw/wget bzip2 gpg-agent xz-utils nscd psmisc python3 rdate cron sysstat net-tools debconf-utils libnet-ssleay-perl/; $self->apt_nohang_ssystem( './apt-get-wait', '-y', 'install', @packages_to_install ); return; } # we need to call update first to ensure we have a full package list, otherwise it won't be able to find packages for install sub update_apt { my ($self) = @_; return unless $self->{'update_apt'}++ == 0; # Run once. $self->apt_nohang_ssystem( '/usr/bin/apt-get', 'update' ); return; } sub apt_nohang_ssystem { my ( $self, @cmd ) = @_; $self->update_apt; #circular but it's ok because we bumped $update_apt already. my $failcount = 0; my $result = 1; while ($result) { # While apt is failing. $result = Common::ssystem(@cmd); last if ( !$result ); # apt came back clean. Stop re-trying $failcount++; if ( $failcount > 5 ) { FATAL("apt failed $failcount times. The installation process cannot continue."); } } return; } sub remove_distro_software { my ($self) = @_; my @remove_pkgs = qw( apache2-data apache2-utils dovecot-core dovecot-imapd dovecot-lmtpd dovecot-pop3d exim4 exim4-base exim4-config exim4-daemon-heavy exim4-daemon-light exim4-dev exim4-doc-html exim4-doc-info mysql-server mysql-server-8.0 mysql-server-core-8.0 mysql-common libmysqlclient21 mysql-client mysql-client-8.0 mysql-client-core-8.0 portreserve postfix sendmail spamassassin libapache2-mod-perl2 mariadb-client mariadb-client-10.3 mariadb-client-core-10.3 mariadb-common mariadb-plugin-connect mariadb-server mariadb-server-10.3 mariadb-server-core-10.3 mariadb-test mycli pure-ftpd proftpd-basic ); INFO('Ensuring that conflicting services are not installed...'); Common::ssystem( '/usr/bin/apt-get', '-y', 'purge', @remove_pkgs, { ignore_errors => 1 } ); return; } sub verify_mysql_version { my ( $self, $cpanel_config ) = @_; my $mysql_version = $cpanel_config->{'mysql-version'}; return unless length $mysql_version; # The only supported installable versions are: # 8.0, 8.4, 10.5, 10.6, 10.11, 11.4 my $supported_versions = qr{^(?: | 8(?:\.[04])? | 10(?:\.(?:[5-6]|11)) | 11(?:\.(?:[4])) )$}x; unless ( $mysql_version =~ $supported_versions ) { FATAL('The mysql-version value in /root/cpanel_profile/cpanel.config is either invalid or references an unsupported MySQL/MariaDB version. See https://go.cpanel.net/supported-mysql-mariadb-versions for a list of supported versions.'); } my $lts_version = $self->lts_version; if ( CpanelMySQL::version_is_mariadb($mysql_version) && $lts_version < 117 ) { FATAL("cPanel & WHM version $lts_version does not support MariaDB® on Ubuntu®. See https://go.cpanel.net/supported-mysql-mariadb-versions for a list of supported versions."); } my $distro_major = $self->distro_major; my %db_id = CpanelMySQL::get_db_identifiers($mysql_version); my $advice = CpanelMySQL::get_db_version_advice( $mysql_version, $lts_version, $distro_major, $db_id{'plain'} ); if ( $advice->{ver} && $advice->{action} ) { FATAL("You must set $db_id{'stylized'} to version $advice->{ver} or $advice->{action} in the /root/cpanel_profile/cpanel.config file for cPanel & WHM version $lts_version."); } return; } # These packages are needed for MySQL later in the install # By downloading them now we do not have to wait for them later sub background_download_packages_used_during_initial_install { my ($self) = @_; my @sysup_packages_to_install = qw{quota expat libexpat1-dev}; my @ea4_packages_to_install = qw{elinks libssh2-1 libssh2-1-dev libvpx-dev libwww-perl libkrb5-dev libcompress-raw-bzip2-perl libcompress-raw-zlib-perl autoconf automake}; my $distro_major = int( $self->distro_major ); if ( $distro_major == 20 ) { push @ea4_packages_to_install, 'libvpx6'; } elsif ( $distro_major == 22 ) { push @ea4_packages_to_install, 'libvpx7'; } elsif ( $distro_major == 24 ) { push @ea4_packages_to_install, 'libvpx9'; } # Only way this happens is during development or if the customer uses --force else { warn "Unrecognized OS version detected for EA4 software. Unable to install all prereq packages\n"; } my @mysql_support_packages_to_install = qw{libnuma1 grep libuser coreutils libdbi-perl}; my @packages_to_install = ( @mysql_support_packages_to_install, @sysup_packages_to_install, @ea4_packages_to_install ); return $self->run_in_background( sub { $self->apt_nohang_ssystem( './apt-get-wait', '--download-only', '-y', 'install', @packages_to_install ); } ); } sub verify_usrmerge_is_installed { my $required_symlinks = { '/bin' => 'usr/bin', '/sbin' => 'usr/sbin', '/lib' => 'usr/lib', }; foreach my $link ( keys %{$required_symlinks} ) { my $target = readlink $link; if ( !length $target || $target ne $required_symlinks->{$link} ) { my $ubuntu_major_csv = join( ", ", map { "$_.04" } @{ CPANEL_UBUNTU_SUPPORT() } ); my $errmsg = "You can only install cPanel & WHM on a fresh Ubuntu installation of the following versions: $ubuntu_major_csv"; FATAL($errmsg); } } return; } sub check_keyserver_connectivity { my ($self) = @_; INFO("Performing pre-flight check for keyserver connectivity..."); # Test connectivity to keyserver.ubuntu.com which is required for MariaDB GPG keys # These keys (3A79BD29, 5072E1F5, C74CD1D8) are downloaded during cPanel installation my $keyserver = 'keyserver.ubuntu.com'; my $test_key = '3A79BD29'; # One of the MariaDB keys # Quick connectivity test with short timeout my @gpg_cmd = ('timeout', '30', 'gpg', '--batch', '--no-tty', '--keyserver', $keyserver, '--recv-keys', $test_key); INFO("Testing connectivity to $keyserver..."); DEBUG("Running command: [" . join( ' ', @gpg_cmd) . "]" ); my $output = Common::ssystem(@gpg_cmd); my $exit_code = $? >> 8; if ($exit_code != 0) { # Keyserver connectivity failed - provide detailed error message ERROR("Failed to connect to keyserver $keyserver (exit code: $exit_code)"); DEBUG("GPG output: $output"); my $error_message = $self->_generate_keyserver_error_message($keyserver, $output, $exit_code); FATAL($error_message); } INFO("Keyserver connectivity check passed - $keyserver is accessible"); # Clean up the test key from the temporary keyring my @clean_cmd = ('gpg', '--batch', '--yes', '--delete-keys', $test_key); Common::ssystem(@clean_cmd); return; } sub _generate_keyserver_error_message { my ($self, $keyserver, $output, $exit_code) = @_; my $message = <