Edit File: maintenance
#!/bin/sh eval 'if [ -x /usr/local/cpanel/3rdparty/bin/perl ]; then exec /usr/local/cpanel/3rdparty/bin/perl -x -- $0 ${1+"$@"}; else exec /usr/bin/perl -x $0 ${1+"$@"}; fi;' if 0; #!/usr/bin/perl # cpanel - scripts/maintenance Copyright 2016 cPanel, Inc. # All rights Reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited package scripts::maintenance; # We need this protection if /usr/bin/perl needs to be used for maintenance. BEGIN { unshift @INC, '/usr/local/cpanel' if $INC[0] ne '/usr/local/cpanel' } use strict; use Cpanel::AccessIds (); use Cpanel::Binaries (); use Cpanel::CleanINC (); use Cpanel::TimeHiRes (); use Cpanel::CloudLinux (); use Cpanel::Config::LoadCpConf (); use Cpanel::Config::LoadUserDomains (); use Cpanel::Config::Sources (); use Cpanel::ConfigFiles (); use Cpanel::Crypt::GPG::Settings (); use Cpanel::Crypt::GPG::VendorKeys::TimestampCache (); use Cpanel::Env (); use Cpanel::MysqlUtils::Version (); use Cpanel::PwCache (); use Cpanel::RPM::Versions::Directory (); use Cpanel::SafeRun::Errors (); use Cpanel::ServerTasks (); use Cpanel::Services::Enabled (); use Cpanel::Sync::CheckRestore (); use Cpanel::Update::Config (); use Cpanel::Update::Crontab (); use Cpanel::Update::Logger (); use IO::Handle (); use IO::Select (); use Cpanel::SafeRun::Object (); use Cpanel::IOCallbackWriteLine (); # hash we'll use to process each request our $RPM_IS_BROKEN = 0; our $_UPGRADE_IN_PROGRESS_FILE = '/usr/local/cpanel/upgrade_in_progress.txt'; my $DRY_RUN; # Internal documentation: https://cpanel.wiki/x/zwwFAw sub script { my ( $class, @args ) = @_; if ( $> != 0 ) { print "This cPanel maintenance script must be run as root, not uid $>.\n"; return 2; } umask(0022); my $security_token = $ENV{'cp_security_token'} || ''; # default pcent when none are defined my $starting_pbar = 0; my $finishing_pbar = 100; my $per = 10; # should be at least this high, and should be overwritten by the cmd line arg below # in case we are called before and outside of upcp setupenv(); my $only_run; # create a default logfile path, if called from upcp, use the log it passes my $now = time(); my $logfile_path = '/var/cpanel/updatelogs/maintenance' . $now . '.log'; my $custom_pbar; foreach my $arg (@args) { if ( $arg =~ m/^--log\=(.*)/ ) { $logfile_path = $1; } elsif ( $arg =~ m/^--pbar-start=([0-9]+)/ ) { $custom_pbar = 1; $starting_pbar = int($1); } elsif ( $arg =~ m/^--pbar-stop=([0-9]+)/ ) { $custom_pbar = 1; $finishing_pbar = int($1); } elsif ( $arg =~ m/^--dry-run$/ ) { # no doc required: dev only $DRY_RUN = 1; } elsif ( $arg =~ m/^--pre$/ ) { # no doc required: upcp only $only_run = 'pre'; } elsif ( $arg =~ m/^--post$/ ) { # no doc required: upcp only $only_run = 'post'; } elsif ( $arg =~ m/^--help/ ) { return usage(); } } open( STDERR, ">&STDOUT" ); $| = 1; # when start pbar is unset progress bar is not displayed setup_logger( $logfile_path, $custom_pbar ? $starting_pbar : undef ); # helper which normalize all percentage to be in [ $starting_pbar .. $finishing_pbar ] # the only thing that we should care are the capping values which should be between [ 0..100 ] my $increment_pbar; # initialize later as we can count how many tasks to run my $do_progress_bar = sub { my (@args) = @_; return bless sub { $increment_pbar->(@args) }, 'PBAR'; }; ############################################################################# # maintenance actions are split in 2 groups: pre and post # by default maintenance is going to run both groups: pre than post # /scripts/maintenance is similar to run # 1. ~maintenance --pre # 2. ~maintenance --post # but we can now run only one of these groups, this allow to run post_sync_cleanup earlier during upcp # /scripts/upcp is going to use maintenance script using 2 different calls # 1. update.now # 2. maintenance --pre # 3. post_sync_cleanup # 4. maintenance --post my $blocks = { pre => [], post => [] }; # This is the only pre block in this script. Only things that MUST happen before post_sync_cleanup should live here. push @{ $blocks->{'pre'} }, ( show_status('Assuring needed symlinks in 3rdparty/bin are in place.'), '/usr/local/cpanel/scripts/link_3rdparty_binaries', show_status('Setting clock'), '/usr/local/cpanel/scripts/rdate', action_find_and_fix_rpm_issues(), # set RPM_IS_BROKEN (at run time): used by sysup and check_cpanel_rpms action_rpmup(), ); push @{ $blocks->{'post'} }, ( show_status('Purging cpupdate.conf of invalid entries'), \&purge_cpupdate_conf, \&purge_upcp_logs, action_updatesigningkey(), action_sysup(), #We do it right after post sync cleanup and sysup #to ensure cPanel services have been restarted before we #do any system ones so that they can access cPanel #and monitor the system while the system services are #being restarted. show_status('Restarting any outdated services'), run( '/usr/local/cpanel/scripts/find_outdated_services --always-restart', { exit_ok => [1] } ), action_vps_optimizer(), # not on dnsonly show_status('Checking for a valid C Compiler.'), '/usr/local/cpanel/scripts/checkccompiler', action_build_locale_databases(), show_status('Migrating feature lists to current version (if needed)'), '/usr/local/cpanel/bin/migrate_all_feature_lists_to_current', show_status('Checking for main IP changes'), '/usr/local/cpanel/scripts/mainipcheck', show_status('Updating neighbor netblocks'), '/usr/local/cpanel/scripts/update_neighbor_netblocks', show_status('Updating known proxy ips'), '/usr/local/cpanel/scripts/update_known_proxy_ips', show_status('Validating server hostname'), '/usr/local/cpanel/scripts/check_valid_server_hostname --notify', show_status('Validating cPanel system users'), '/usr/local/cpanel/scripts/checkusers', action_fixrndc(), action_init_wwwacct_conf(), action_ipaliases(), action_check_cpanel_rpms(), show_status('Running env auto repair'), '/usr/local/cpanel/scripts/vzzo-fixer', '/usr/local/cpanel/scripts/quota_auto_fix', '/usr/local/cpanel/scripts/clear_orphaned_virtfs_mounts --inactiveonly', show_status('Cleaning up orphaned filesystem quotas'), '/usr/local/cpanel/scripts/cleanquotas', '/usr/local/cpanel/scripts/autorepair autorepair', '/usr/local/cpanel/scripts/purge_old_config_caches', '/usr/local/cpanel/scripts/cleansessions', '/usr/local/cpanel/scripts/checkbashshell', action_passwd(), \&setupcrontab, '/usr/local/cpanel/scripts/dnsqueuecron', show_status('Rebuild WHM chrome cache'), '/usr/local/cpanel/scripts/rebuild_whm_chrome', # checkallsslcerts needs to run on DNSONLY # because we need an ssl cert for dovecot for it to startup # Ensure /var/cpanel/ssl/*-SIGNATURE_CHAIN_VERIFIED and # /var/cpanel/ssl/*-NO_AFTER is updated so Cpanel::Redirect # can make good descisions. This also ensures that # admins get timely notice of the expire time being # reached on their ssl certificates. show_status('Checking service default SSL/TLS certificates'), '/usr/local/cpanel/bin/checkallsslcerts --allow-retry --verbose', show_status('Purging invalid or soon-to-expire Domain TLS entries for service domains'), '/usr/local/cpanel/scripts/check_domain_tls_service_domains.pl --prune', show_status('Cleaning up temporary wheel users'), '/usr/local/cpanel/scripts/clean_up_temp_wheel_users', ); if ( !is_dnsonly() ) { push @{ $blocks->{'post'} }, ( show_status('Rebuilding sprites'), '/usr/local/cpanel/bin/sprite_generator', show_status('Updating services and databases'), '/usr/local/cpanel/scripts/findphpversion', '/usr/local/cpanel/scripts/checkerrorlogsafe', '/usr/local/cpanel/scripts/listcheck', action_purge_modsec(), action_ftpquotacheck(), '/usr/local/cpanel/scripts/updateuserdomains', '/usr/local/cpanel/bin/empty_user_trash --quiet --all', '/usr/local/cpanel/scripts/build_maxemails_config', '/usr/local/cpanel/scripts/updateuserdatacache --force', show_status('Checking system maxmem setting'), '/usr/local/cpanel/scripts/check_maxmem_against_domains_count --always-fix', show_status('Running various cleanup scripts'), '/usr/local/cpanel/scripts/resetmailmanurls', show_status('Checking MySQL to ensure we can connect'), '/usr/local/cpanel/scripts/mysqlconnectioncheck', # POST or leave it there ?? show_status('Updating Apache SpamAssassin™ configuration'), '/usr/local/cpanel/scripts/update_spamassassin_config --verbose', show_status('Checking PostgreSQL to ensure we can connect'), '/usr/local/cpanel/bin/postgrescheck --check-auth --reset-pass-on-fail', # POST or leave it there ?? action_repair_mailman(), action_repair_mysql(), show_status('Running sanity checks and notifications'), # status update '/usr/local/cpanel/scripts/chkpaths', '/usr/local/cpanel/scripts/hackcheck', '/usr/local/cpanel/scripts/oopscheck', '/usr/local/cpanel/scripts/fixetchosts', '/usr/local/cpanel/scripts/check_unreliable_resolvers --notify', '/usr/local/cpanel/bin/is_script_stuck --script=autossl_check --time=22h --kill --notify=root', '/usr/local/cpanel/scripts/quotacheck', '/usr/local/cpanel/scripts/email_archive_maintenance', '/usr/local/cpanel/scripts/expunge_expired_certificates_from_sslstorage', '/usr/local/cpanel/scripts/notify_expiring_certificates', '/usr/local/cpanel/scripts/expunge_expired_transfer_sessions', '/usr/local/cpanel/scripts/expunge_expired_pkgacct_sessions', '/usr/local/cpanel/scripts/smartcheck', '/usr/local/cpanel/scripts/compilerscheck', '/usr/local/cpanel/scripts/updatephpconf', '/usr/local/cpanel/scripts/check_mount_procfs', '/usr/local/cpanel/scripts/perform_sqlite_auto_rebuild_db_maintenance', '/usr/local/cpanel/scripts/setup_modsec_db', '/usr/local/cpanel/scripts/modsec_vendor update --auto', '/usr/local/cpanel/bin/check_cpstore_in_sync_with_local_storage', action_purge_dead_comet_files(), action_update_spamassassin_rules(), action_update_freshclam(), show_status('Restoring compiler permissions'), '/usr/local/cpanel/scripts/compilers restore', show_status('Cleaning up mailbox trash'), '/usr/local/cpanel/scripts/dovecot_maintenance --background', show_status('Checking MySQL Version'), sub { check_mysql_version() }, ); } # end !dnsonly push @{ $blocks->{'post'} }, ( action_buildexim(), action_eximstats(), action_exim_purge_old_tracker_files(), sub { Cpanel::Sync::CheckRestore::check_and_restore("img-sys/powered_by_cpanel.svg") }, action_cleanup_signature(), action_shrink_modsec_ip_database(), ); if ( !is_dnsonly() ) { # not dnsonly push @{ $blocks->{'post'} }, ( show_status('Cleaning SpamAssassin DBM files'), '/usr/local/cpanel/scripts/spamassassin_dbm_cleaner', show_status('Cleaning Squirrelmail attachment directories'), \&clean_user_squirrelmail_attachment_dirs, show_status('Cleaning Roundcube attachment directory'), \&clean_roundcube_attachment_directory, show_status('Checking for new security advice'), '/usr/local/cpanel/scripts/check_security_advice_changes --notify', show_status('Running former postinstall scripts'), '/usr/local/cpanel/bin/dcpumon --killproc', '/usr/local/cpanel/bin/setupdbmap', '/usr/local/cpanel/bin/fix_userdata_perms', '/usr/local/cpanel/scripts/detect_env_capabilities', show_status('Updating cPGreyList Common Mail Providers'), '/usr/local/cpanel/scripts/manage_greylisting --init --update_common_mail_providers', show_status('Checking for deprecated PHP local.ini'), '/usr/local/cpanel/scripts/migrate_local_ini_to_php_ini --run --verbose', show_status('Ensuring an "Active" MySQL profile is set'), \&ensure_active_mysql_profile_is_present, run( '/usr/local/cpanel/scripts/check_mysql', { 'exit_ok' => [ 2, 255 ] } ), action_cloudlinux_update(), show_status('Updating plugins data cache'), '/usr/local/cpanel/bin/refresh_plugin_cache', ); } my $maintenance_complete = is_dnsonly() ? q{DNSONLY maintenance complete.} : q{Maintenance complete.}; # build the todo list depending which block we want to run # default = pre + post my @todo = ( @{ $blocks->{'pre'} }, @{ $blocks->{'post'} } ); # Remove the todo once done if ( $only_run && ref $blocks->{$only_run} ) { @todo = @{ $blocks->{$only_run} }; $maintenance_complete .= " [state=$only_run]"; } # we have now reach 100%, move the progress bar push @todo, ( $do_progress_bar->( complete => 1 ), show_status("\n\n$maintenance_complete\n"), ); # how many actions do we have to run which are neither a status nor a pbar item my $total_actions = grep { my $ref = ref $_; $ref ne 'PBAR' && $ref !~ /Action::(?:Status|Command)/ } @todo; # initialize progress bar with: from % to % and number of elements $increment_pbar = increment_pbar( $starting_pbar, $finishing_pbar, $total_actions ); run_actions( \@todo, $increment_pbar ); return logger()->get_need_notify() ? 1 : 0; } # Utilities for mockery. our $dnsonly; sub is_dnsonly { if ( !defined $dnsonly ) { $dnsonly = touch_file_exists('/var/cpanel/dnsonly') ? 1 : 0; } return $dnsonly; } our %touch_file_mock; # Used to fake if touch files are present. sub touch_file_exists { my ($file) = @_; $file or return; # Provide an easy way to mock file existance. return $touch_file_mock{$file} if exists $touch_file_mock{$file}; return -e $file; } sub file_is_executable { my $file = shift; # for now use the same mock hash return $touch_file_mock{$file} if exists $touch_file_mock{$file}; return -x $file; } sub populated_touch_file_exists { my ($file) = @_; $file or return; # Provide an easy way to mock file existance. return $touch_file_mock{$file} if exists $touch_file_mock{$file}; return -e $file && !-z _; } ################################################################ ####[ Subroutines ]############################################# ################################################################ sub run_action { # avoid to use array ref when using it from a single action my (@todo) = @_; return run_actions( \@todo ); } sub run_actions { my ( $todo, $increment_pbar ) = @_; die unless ref $todo; my $action = {}; foreach my $cmd (@$todo) { my $start_time = Cpanel::TimeHiRes::time(); my $type = ref $cmd; if ( $type eq 'Action::Command' ) { %$action = ( %$action, %$cmd ); $cmd = $action->{'cmd'}; } elsif ($type) { if ( $type eq 'Action::Status' ) { $action->{status} = $cmd->[0]; } elsif ( $type eq 'CODE' ) { if ($DRY_RUN) { print "[dry-run mode] CodeRef\n"; } else { # custom cases with some extra code around the action # let them do what they want $cmd->(); } $increment_pbar->() if ref $increment_pbar; my $runtime = sprintf( "%0.3f", Cpanel::TimeHiRes::time() - $start_time ); logger()->info(" - Finished in $runtime seconds"); } elsif ( $type eq 'PBAR' ) { $cmd->(); } next; } $action->{cmd} = [ split( /\s+/, $cmd ) ]; process($action); my $runtime = sprintf( "%0.3f", Cpanel::TimeHiRes::time() - $start_time ); logger()->info(" - Finished command `$cmd` in $runtime seconds"); $increment_pbar->() if ref $increment_pbar; } return; } sub show_status { my $msg = shift; return unless defined $msg or length $msg; my $status = [$msg]; bless $status, 'Action::Status'; return $status; } sub run { my ( $cmd, $options ) = @_; my $status = { %$options, cmd => $cmd }; bless $status, 'Action::Command'; return $status; } { my $prev_logger_status = ''; sub process { my ($action) = @_; my @cmd = @{ $action->{'cmd'} }; if ($DRY_RUN) { print "[dry-run mode] " . join( ' ', @cmd, "\n" ); return; } $action->{'status'} = '' if !defined $action->{'status'}; if ( $prev_logger_status ne $action->{'status'} && length( $action->{'status'} ) ) { logger()->info("Processing: $action->{'status'}"); $prev_logger_status = $action->{'status'}; } logger()->info(" - Processing command `@cmd`"); my ( $program, @args ) = @cmd; my $logger = logger(); my $run = eval { Cpanel::SafeRun::Object->new( 'program' => $program, 'args' => \@args, 'stdout' => Cpanel::IOCallbackWriteLine->new( sub { $logger->info(" [$program] $_[0]"); } ) ); }; $? = -1; ## no critic qw(Variables::RequireLocalizedPunctuationVars) -- needed for compat if ($@) { logger()->error( " [$program] $@", 1 ); logger()->set_need_notify(); return; } $? = $run->CHILD_ERROR(); ## no critic qw(Variables::RequireLocalizedPunctuationVars) -- needed for compat if ( my $exit = $run->CHILD_ERROR() ) { $exit >>= 8; return if $exit && $action->{'exit_ok'} && grep { $exit == $_ } @{ $action->{'exit_ok'} }; logger()->error( " [$program] " . $run->autopsy(), 1 ); logger()->set_need_notify(); return; } } } # list of actions which need some extra logic or being postponed sub action_vps_optimizer { return if is_dnsonly(); # idea move this check to the script itself return ( show_status('Running platform specific optimizations'), '/usr/local/cpanel/scripts/vps_optimizer' ); } sub action_update_spamassassin_rules { return unless Cpanel::Update::Config::is_permitted( 'SARULESUP', get_update_conf() ); return ( show_status('Updating Apache SpamAssassin™ rules'), '/usr/local/cpanel/scripts/sa-update_wrapper', ); } sub background_freshclam { return sub { eval { Cpanel::ServerTasks::queue_task( ['ClamTasks'], 'freshclam --quiet -l /var/log/clam-update.log' ); }; }; } sub action_update_freshclam { return sub { # postpone the check if the binary is restored by RPM transaction my $freshclam_bin = Cpanel::Binaries::get_binary_location('freshclam'); return unless file_is_executable($freshclam_bin); return run_action( show_status('Updating virus patterns'), background_freshclam(), ); } } sub action_purge_dead_comet_files { return ( show_status('Purging old comet files'), '/usr/local/cpanel/bin/purge_dead_comet_files --quiet', ); } sub action_rpmup { return sub { # need RPM_IS_BROKEN to be set return if !Cpanel::Update::Config::is_permitted( 'RPMUP', get_update_conf() ) or $RPM_IS_BROKEN; return run_action( show_status('Running rpmup (yum -y update)'), '/usr/local/cpanel/scripts/rpmup' ); }; } sub action_sysup { return sub { # need RPM_IS_BROKEN to be set if ($RPM_IS_BROKEN) { logger()->error('RPM is not functioning. Skipping sysup.'); return; } return run_action( show_status('Updating system packages: sysup'), '/usr/local/cpanel/scripts/sysup' ); }; } sub _find_and_fix_rpm_issue_script { # for testing purpose return '/usr/local/cpanel/scripts/find_and_fix_rpm_issues'; } sub action_find_and_fix_rpm_issues { return sub { local $?; my %action; $action{'status'} = 'Checking RPM DB for corruption'; $action{'cmd'} = [ _find_and_fix_rpm_issue_script() ]; # OK process( \%action ); if ( ( $? >> 8 ) > 0 ) { logger()->error('RPM is not functioning and automatic repair failed.'); logger()->error('Tasks for rpmup, sysup, and check_cpanel_rpms will be skipped.'); $RPM_IS_BROKEN = 1; } return; }; } sub action_updatesigningkey { # Update signing keys, if enabled. return unless Cpanel::Crypt::GPG::Settings::signature_validation_enabled(); return ( show_status('Updating cPanel signing keys.'), '/usr/local/cpanel/scripts/updatesigningkey' ); } sub action_cloudlinux_update { return unless Cpanel::CloudLinux::supported_os(); return ( show_status('Checking CloudLinux installation'), '/usr/local/cpanel/bin/cloudlinux_update', ); } sub action_build_locale_databases { return if $ENV{'CPANEL_BASE_INSTALL'}; return ( show_status('Ensuring locale databases are up to date'), sub { Cpanel::ServerTasks::schedule_task( ['LocaleTasks'], 300, "build_locale_databases" ); } ); } sub action_init_wwwacct_conf { return if touch_file_exists('/etc/wwwacct.conf'); return ( show_status('Creating account configuration file /etc/wwwacct.conf'), '/usr/local/cpanel/scripts/mkwwwacctconf', ); } sub _var_named_path { return '/var/named' } # for unit tests purpose sub action_fixrndc { return if touch_file_exists('/etc/nameddisable') or touch_file_exists('/etc/binddisable'); my $status = 'Checking and repairing nameserver config'; if ( !touch_file_exists( _var_named_path() ) ) { return sub { mkdir( _var_named_path(), 0755 ); # probably safe to do it earlier and simplify this code return run_action( show_status($status), '/usr/local/cpanel/scripts/fixrndc -f', ); }; } return ( show_status($status), '/usr/local/cpanel/scripts/fixrndc' ); } sub action_ipaliases { return ( show_status('Setting up IP aliases startup'), '/usr/local/cpanel/whostmgr/bin/setupipaliases', ); } sub action_check_cpanel_rpms { return sub { # need RPM_IS_BROKEN to be set my $cpconf = get_cpconf(); if ($RPM_IS_BROKEN) { logger()->error('RPM is not functional. Skipping check_cpanel_rpms.'); } elsif ( $cpconf->{'maintenance_rpm_version_check'} ) { my $status = 'Checking cPanel RPMs'; my @rpmcheck_args = (qw{ --list-only --long-list --notify }); if ( !$cpconf->{'maintenance_rpm_version_digest_check'} ) { # the user doesn't want a digest check and we're not being forced into it # $status .= ' (with no RPM digest check)'; push @rpmcheck_args, '--no-digest'; } return run_action( show_status($status), join( ' ', '/usr/local/cpanel/scripts/check_cpanel_rpms', @rpmcheck_args ), ); } else { logger()->info('Skipping cPanel RPM check due to configuration'); } return; }; } sub action_ftpquotacheck { return unless Cpanel::Services::Enabled::is_enabled('ftp'); return '/usr/local/cpanel/scripts/ftpquotacheck'; } sub action_repair_mailman { return sub { # postpone it as can binary can be there only once RPM is fixed my $cpconf = get_cpconf(); return unless ( !$cpconf->{'skipmailman'} && file_is_executable("$Cpanel::ConfigFiles::MAILMAN_ROOT/bin/check_perms") ); chdir("$Cpanel::ConfigFiles::MAILMAN_ROOT/bin"); my $ok = run_action( show_status('Repairing Mailman Permissions'), './check_perms -f --noarchives', ); chdir('/usr/local/cpanel'); # return where we should be ? return $ok; }; } sub _mysqld_sh_path { '/usr/local/etc/rc.d/mysqld.sh' } # for unit test purpose only sub action_repair_mysql { return unless touch_file_exists( _mysqld_sh_path() ); return sub { unlink _mysqld_sh_path(); return run_action( show_status('Repairing MySQL startup'), '/usr/local/cpanel/scripts/restartsrv mysql', ); }; } sub action_purge_modsec { return if $ENV{'CPANEL_BASE_INSTALL'}; return '/usr/local/cpanel/scripts/purge_modsec_log'; } # for unit test purpose only sub _passwd_files_to_chmod { return [ '/etc/shadow.tmpeditlib', '/etc/master.passwd.tmpeditlib' ]; } sub action_passwd { return sub { my $files = _passwd_files_to_chmod(); # Quick security check on the tmpeditlib files in case they are there foreach my $f (@$files) { chmod( 0600, $f ) if touch_file_exists($f); } return; }; } sub action_buildexim { return sub { # postponed if the file is created/touched by one rpm (hook)... to preserve original behavior return if populated_touch_file_exists('/etc/exim.conf') and populated_touch_file_exists('/etc/exim.pl.local'); # run only if one of the file is missing / empty return run_action( show_status('EXIM sanity checking'), '/usr/local/cpanel/scripts/buildeximconf --no_chown_spool', ); }; } sub action_eximstats { return sub { # Eximstats recover of /var/cpanel/sql/eximstats.sql, if it exists return unless touch_file_exists('/var/cpanel/sql/eximstats.sql'); return run_action( show_status('Recovering data stored in /var/cpanel/sql/eximstats.sql'), '/usr/local/cpanel/scripts/restartsrv_eximstats' ); }; } sub action_shrink_modsec_ip_database { return if -e $_UPGRADE_IN_PROGRESS_FILE; require q{/usr/local/cpanel/scripts/shrink_modsec_ip_database}; my $msg = "Removing expired entries from ModSecurity's IP persistence storage."; my $shrink = scripts::shrink_modsec_ip_database->new(); return ( show_status($msg), sub { logger()->info($msg); return $shrink->run(); }, ); } # If eximstats is disabled, we need to handle clearing of old tracker files here. sub action_exim_purge_old_tracker_files { return sub { my $cpconf = get_cpconf(); return unless $cpconf->{'skipeximstats'} && $cpconf->{'skipeximstats'} eq '1'; require Cpanel::EmailTracker::Purge; logger()->info('Purging old email tracker files'); Cpanel::EmailTracker::Purge::purge_old_tracker_files(); return; }; } sub action_cleanup_signature { return ( show_status('Cleaning Signature Timestamp Cache'), sub { my $sig_cache = Cpanel::Crypt::GPG::VendorKeys::TimestampCache->new(); $sig_cache->cleanup_signature_cache(); return; }, ); } # </end of actions> # local cache configuration to be able to use them { my $conf; # state like variable sub get_update_conf { $conf = { Cpanel::Update::Config::load() } if !defined $conf; return $conf; } sub reset_update_conf { # for testing purpose undef($conf); return; } } # loadcpconf already caches this so we don't need to re-cache it here. *get_cpconf = \&Cpanel::Config::LoadCpConf::loadcpconf; { my $logger; sub setup_logger { my ( $logfile_path, $starting_pbar ) = @_; return $logger = Cpanel::Update::Logger->new( { 'logfile' => $logfile_path, 'stdout' => 1, 'log_level' => 'info', defined $starting_pbar ? ( 'pbar' => $starting_pbar ) : () } ); } sub logger { $logger = setup_logger() unless defined $logger; # mainly for mocking return $logger; } } sub increment_pbar { my ( $start, $end, $items ) = @_; my $_current_ratio = $start; my $_last_update = 0; # do a -1 to the total to leave it to the end my $points = ( $end - $start - 1 ) || 1; $items ||= $points; # number of elements in our array my $w = $points / $items; # default points to increase +1 return sub { my (%opts) = @_; if ( $opts{complete} ) { # we reach the end we can now use the final result $_last_update = $_current_ratio = $end; logger()->update_pbar($end); return; } my $previous = $_current_ratio; $_current_ratio += $w; $_current_ratio = $end if $_current_ratio > $end; my $normalize = int($_current_ratio); # includes a protection to avoid displaying duplicates logger()->update_pbar($normalize); return; } } sub setupenv { Cpanel::Env::clean_env(); delete $ENV{'DOCUMENT_ROOT'}; delete $ENV{'SERVER_SOFTWARE'}; if ( $ENV{'WHM50'} ) { $ENV{'GATEWAY_INTERFACE'} = 'CGI/1.1'; } ( $ENV{'USER'}, $ENV{'HOME'} ) = ( getpwuid($>) )[ 0, 7 ]; $ENV{'PATH'} .= ':/sbin:/usr/sbin:/usr/bin:/bin:/usr/local/bin'; $ENV{'LANG'} = 'C'; $ENV{'LC_ALL'} = 'C'; } sub _tmp_crontab_search { # last entry is fallback return [ $ENV{'HOME'}, '/root', '/usr/local/cpanel/scripts' ]; } sub _tmp_crontab_file { # for testing purpose my $homedir; # relies on setupenv to be called earlier my $candidates = _tmp_crontab_search(); my $fallback = pop @$candidates; foreach my $candidate (@$candidates) { next unless defined $candidate && length $candidate; next if $candidate =~ qr{\Q..\E}; my ( $mode, $uid ) = ( lstat($candidate) )[ 2, 4 ]; next unless -d _ or -l _; next if $uid; next if $mode & 022; # can another user write to this location ? # could also be more restrictive with mask 077 $homedir = $candidate; last; } # fallback to original behavior $homedir = $fallback unless defined $homedir; return qq{$homedir/.cpcrontab.maintenance.tmp}; } sub setupcrontab { my @crontab_lines = split( /\n/, Cpanel::SafeRun::Errors::saferunnoerror( 'crontab', '-l' ) ); my %CPSRC = Cpanel::Config::Sources::loadcpsources(); if ( !exists $CPSRC{'HTTPUPDATE'} ) { $CPSRC{'HTTPUPDATE'} = 'httpupdate.cpanel.net'; } logger()->info('Setting up cronjobs'); logger()->info('Setting Up update_db_cache Crontab'); logger()->info('Setting Up update_maiman_cache Crontab'); logger()->info('Setting Up dcpumon Crontab'); ## _get_cron_updates returns a set of new cron lines, or nothing at all (signaling no change) my $cron_updates = Cpanel::Update::Crontab::_get_cron_updates( \@crontab_lines, $CPSRC{'HTTPUPDATE'}, is_dnsonly() ); if ( defined $cron_updates ) { my $tmp = _tmp_crontab_file; open( my $cr_fh, '>', $tmp ); print {$cr_fh} join( "\n", @$cron_updates ) . "\n"; close($cr_fh); my %action; $action{'cmd'} = [ 'crontab', $tmp ]; process( \%action ); unlink $tmp; } return; } ## Iterates through users' .sqmailattach directory, deleting files that are more than 7 days old sub clean_user_squirrelmail_attachment_dirs { my %TRUEDOMAINS; Cpanel::Config::LoadUserDomains::loadtrueuserdomains( \%TRUEDOMAINS ); my @users = values %TRUEDOMAINS; my $time = time(); for my $username (@users) { my $homedir = ( Cpanel::PwCache::getpwnam($username) )[7]; my $sqmailattach = "$homedir/.sqmailattach"; next unless ( -d $sqmailattach ); ## Find all the files that are over 7 days old (excluding . and ..) ## Skip to the next user if there are none opendir( my $dirh, $sqmailattach ); my @old_files = grep { -M $_ > 7.0 } map { "$sqmailattach/$_" } grep { !m/^\.{1,2}$/ } readdir($dirh); next unless (@old_files); ## defined as a closure, as the coderef accepted by do_as_user takes no args (see @old_files) my $clean_sqmailattach_as_user = sub { for my $old_file (@old_files) { ## this prevents unlink from failing, by offering a regex to ## the taint gods or something my ($untaint) = ( $old_file =~ m/^(.*)$/ ); unlink($untaint); } }; Cpanel::AccessIds::do_as_user( $username, $clean_sqmailattach_as_user ); } return; } sub clean_roundcube_attachment_directory { my $roundcube_tmp = '/var/cpanel/roundcube/tmp'; opendir my $dh, $roundcube_tmp or return; my @old_files = grep { -M $_ > 7.0 } map { "$roundcube_tmp/$_" } grep { !m/^\.{1,2}$/ } readdir $dh; closedir $dh; my $clean = sub { unlink @old_files }; Cpanel::AccessIds::do_as_user( 'cpanelroundcube', $clean ); } sub check_mysql_version { my $reco_version = $Cpanel::MysqlUtils::Version::DEFAULT_MYSQL_RELEASE_TO_ASSUME_IS_INSTALLED; my $current_version = Cpanel::MysqlUtils::Version::current_mysql_version(); if ( !$current_version->{'short'} ) { logger()->info("Unable to determine MySQL version. Skipping MySQL version check..."); return; } # We check the remote mysql version in Install::CheckRemoteMySQLVersion, # so we'll skip the check here return if $current_version->{'is_remote'}; $current_version = $current_version->{'short'}; # if less than the default. recommend an update. if ( Cpanel::MysqlUtils::Version::is_at_least( $current_version, $reco_version ) ) { logger()->info("MySQL or MariaDB version “$current_version” is greater than or equal to the recommended minimum version, “$reco_version”."); } else { logger()->info("MySQL or MariaDB version “$current_version” is less than the recommended minimum version, “$reco_version”."); my @outdated = (); push @outdated, { 'label' => "MySQL", 'current_version' => $current_version, 'min_reco_version' => $reco_version, 'upgrade_url' => 'https://go.cpanel.net/mysqlup', }; require Cpanel::Notify; Cpanel::Notify::notification_class( 'class' => 'OutdatedSoftware::Notify', 'application' => 'OutdatedSoftware::Notify', 'constructor_args' => [ 'origin' => 'scripts/maintenance', 'outdated_software' => \@outdated, ] ); } } sub ensure_active_mysql_profile_is_present { eval { require Cpanel::MysqlUtils::RemoteMySQL::ProfileManager; Cpanel::MysqlUtils::RemoteMySQL::ProfileManager->new()->generate_active_profile_if_none_set(); }; return 1; } ############################################################################# ## Goes through cpupdate.conf and purge entries invalid since 11.36 sub purge_cpupdate_conf { my $dir = Cpanel::RPM::Versions::Directory->new( { 'directory' => '/var/cpanel/rpm.versions.d', 'logger' => logger() } ); if ( $dir->config_changed() ) { $dir->save(); } return; } our $upcp_log_dir = '/var/cpanel/updatelogs'; sub purge_upcp_logs { my ($days) = @_; if ( !defined $days ) { my $cpconf = get_cpconf(); $days = $cpconf->{'upcp_log_retention_days'}; } # On initial upgrade, upcp_log_retention_days isn't set yet. We'll enforce the default here. if ( $days < 3 ) { logger()->warning("upcp_log_retention_days unexpectedly set to $days. Temporarily setting to 45 days."); $days = 45; } logger()->info("Purging upcp logs older than $days days."); my $purge_older_than = time - ( 86400 * $days ); opendir( my $dir_fh, $upcp_log_dir ) or do { logger()->warning("Cannot read '$upcp_log_dir' for purging."); }; while ( my $file = readdir($dir_fh) ) { # Special files we don't purge next if ( $file eq '.' or $file eq '..' or $file eq 'summary.log' ); # Skip if the file is new enough. my @stats = lstat("$upcp_log_dir/$file"); next unless ( @stats && $stats[9] < $purge_older_than ); # The file can be removed. unlink "$upcp_log_dir/$file"; } closedir($dir_fh); } sub usage { print <<EOM; Usage: $0 [options] Perform cPanel nightly maintenance tasks for upcp. Where the supported options are: --help Display this screen and exit --log={logfile path} Log program output to the file named by {logfile path} EOM return 0; } exit( __PACKAGE__->script(@ARGV) || 0 ) if !caller(); 1;