GIF89a; Mini Shell

Mini Shell

Direktori : /scripts/
Upload File :
Current File : //scripts/manage_mysql_profiles

#!/usr/local/cpanel/3rdparty/bin/perl

# cpanel - scripts/manage_mysql_profiles           Copyright 2022 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package scripts::manage_mysql_profiles;

use strict;
use Try::Tiny;

use Cpanel::JSON                                    ();
use Getopt::Long                                    ();
use Cpanel::Backup::Config                          ();
use Cpanel::Logger                                  ();
use Cpanel::LoadFile                                ();
use Cpanel::Exception                               ();
use Cpanel::Hooks                                   ();
use Cpanel::DIp::MainIP                             ();
use Cpanel::SafeFile                                ();
use Cpanel::SafeRun::Errors                         ();
use Cpanel::Services::Enabled                       ();
use Cpanel::MysqlUtils::MyCnf::Basic                ();
use Cpanel::MysqlUtils::MyCnf                       ();
use Cpanel::MysqlUtils::Command                     ();
use Cpanel::MysqlUtils::Connect                     ();
use Cpanel::MysqlUtils::Quote                       ();
use Cpanel::MysqlUtils::Integration                 ();
use Cpanel::MysqlUtils::Version                     ();
use Cpanel::MysqlUtils::RemoteMySQL::ActivationJob  ();
use Cpanel::MysqlUtils::RemoteMySQL::ProfileManager ();
use Cpanel::Config::LoadCpConf                      ();
use Cpanel::Sys::Hostname                           ();

exit run(@ARGV) unless caller();

sub run {
    my @cmdline_args = @_;
    return usage(1) if !@cmdline_args;

    unless ( $> == 0 && $< == 0 ) {
        return usage( 1, "[!] This program can only be run by root!\n" );
    }

    my $opts = {};
    Getopt::Long::GetOptionsFromArray(
        \@cmdline_args,
        'activate=s'              => \$opts->{'activate'},
        'import=s'                => \$opts->{'import'},
        'force'                   => \$opts->{'force'},
        'export=s@'               => \$opts->{'export'},
        'export_to=s'             => \$opts->{'export_to'},
        'recreate_active_profile' => \$opts->{'recreate_active_profile'},
        'help|h'                  => \$opts->{'help'},
    );

    return usage(0)                                                   if $opts->{'help'};
    return activate_profile( $opts->{'activate'} )                    if $opts->{'activate'};
    return import_profiles( $opts->{'import'}, $opts->{'force'} )     if $opts->{'import'};
    return export_profiles( $opts->{'export'}, $opts->{'export_to'} ) if $opts->{'export'} && scalar @{ $opts->{'export'} };
    return recreate_active_profile( $opts->{'force'} )                if $opts->{'recreate_active_profile'};
    return usage(1);
}

sub recreate_active_profile {
    my $force = shift;
    try {
        my $profile_manager = Cpanel::MysqlUtils::RemoteMySQL::ProfileManager->new();

        my $active_profile = $profile_manager->get_active_profile('dont_die');
        if ( !$force && $active_profile ) {
            print "[!] '$active_profile' is already active.\n";
        }
        else {
            print "[*] Recreating active profile …\n";
            $profile_manager->generate_active_profile_if_none_set($force);
            $profile_manager->save_changes_to_disk();
            my $active_profile = $profile_manager->get_active_profile('dont_die');
            print "[+] New active profile created: $active_profile\n";
        }
    }
    catch {
        _handle_failure( { 'action' => 'recreate', 'exception' => $_ } );
    };

    return 0;
}

sub import_profiles {
    my ( $input_file, $force ) = @_;

    try {
        open( my $input_file_fh, '<', $input_file )
          or die Cpanel::Exception::create( 'IO::FileReadError', [ path => $input_file, error => $! ] );

        my $json = Cpanel::JSON::LoadFile( $input_file_fh, $input_file );
        die Cpanel::Exception::create( 'IO::FileReadError', [ path => $input_file, error => 'Invalid JSON data' ] )
          if !$json || ref $json ne 'HASH';

        print "[*] Importing MySQL profiles: " . join( ', ', ( keys %{$json} ) ) . "\n\n";
        my $profile_manager   = Cpanel::MysqlUtils::RemoteMySQL::ProfileManager->new();
        my $existing_profiles = $profile_manager->read_profiles();

        my $active_profile_exists = $profile_manager->get_active_profile('dont_die');
        foreach my $profile_name ( keys %{$json} ) {
            my $overwrite = $force ? 1 : 0;
            if ( !$overwrite and exists $existing_profiles->{$profile_name} ) {
                print "[*] '$profile_name' already exists …\n";
                next;    # skip
            }

            # If an active profile is already present, then mark the newly
            # imported profile as inactive to avoid conflicts.
            if ( $json->{$profile_name}->{'active'} && $active_profile_exists ) {
                $json->{$profile_name}->{'active'} = 0;
            }
            $profile_manager->create_profile( { 'name' => $profile_name, %{ $json->{$profile_name} } }, { 'overwrite' => $overwrite } );
            print "[+] '$profile_name' imported.\n";
        }
        $profile_manager->save_changes_to_disk();
    }
    catch {
        _handle_failure( { 'action' => 'import', 'exception' => $_ } );
    };

    return 0;
}

sub export_profiles {
    my ( $profiles_to_export_ar, $output_file ) = @_;

    print STDERR "[*] Exporting MySQL profiles: " . join( ', ', @{$profiles_to_export_ar} ) . "\n\n";
    try {
        my $profile_manager   = Cpanel::MysqlUtils::RemoteMySQL::ProfileManager->new( { 'read_only' => 1 } );
        my $existing_profiles = $profile_manager->read_profiles();
        my $output            = {};
        my $output_fh;

        if ($output_file) {
            open $output_fh, '>', $output_file or die Cpanel::Exception::create( 'IOError', 'Failed to open “[_1]” for writing: [_2]', [ $output_file, $! ] );
            print STDERR "[*] Saving to '$output_file'\n";
        }

        foreach my $profile_to_export ( @{$profiles_to_export_ar} ) {
            if ( not exists $existing_profiles->{$profile_to_export} ) {
                print STDERR "[!] Profile not found: $profile_to_export\n";
                next;
            }
            $output->{$profile_to_export} = $existing_profiles->{$profile_to_export};
        }

        # If there are any profiles to export, then output the json
        if ( scalar keys %{$output} ) {
            print { $output_fh ? $output_fh : \*STDOUT } Cpanel::JSON::pretty_dump($output);
        }
        else {
            print "\n[!] No profiles to export.\n";
        }
    }
    catch {
        _handle_failure( { 'action' => 'export', 'exception' => $_ } );
    };

    return 0;
}

sub activate_profile {
    my $profile_name = shift;

    Cpanel::Hooks::hook(
        {
            'category' => 'Whostmgr',
            'event'    => 'RemoteMySQL::activate_profile',
            'stage'    => 'pre',
        },
        { 'profile_name' => $profile_name, },
    );

    my $activation_job  = Cpanel::MysqlUtils::RemoteMySQL::ActivationJob->new($profile_name);
    my $profile_manager = Cpanel::MysqlUtils::RemoteMySQL::ProfileManager->new();
    try {
        my $current_active_profile = $profile_manager->get_active_profile('dont_die');

        # TODO: should we care about situations where the profile being switched to is the same as the active profile?
        print "[*] Current active MySQL profile: " . ( $current_active_profile || 'N/A' ) . " \n";
        print "[*] Activating MySQL profile: $profile_name\n\n";

        # Step 1: Validation.
        $activation_job->start_step('Validating profile');
        try {
            $profile_manager->validate_profile($profile_name);
        }
        catch {
            $activation_job->fail_step( 'Validating profile', { 'error' => $_->to_string() } );
            die Cpanel::Exception::create( 'RemoteMySQL::ActivationFailed', 'Failed to activate [asis,MySQL] profile, “[_1]”: [_2]', [ $profile_name, 'Failed to validate profile' ] );
        };
        $activation_job->done_step('Validating profile');

        my $profile = $profile_manager->read_profiles()->{$profile_name};

        # STEP 2: Update local root .my.cnf file
        $activation_job->start_step('Updating /root/.my.cnf');
        my $old_my_cnf   = Cpanel::LoadFile::loadfile('/root/.my.cnf');
        my $is_localhost = Cpanel::Services::Enabled::is_enabled('mysql') && Cpanel::MysqlUtils::MyCnf::Basic::is_local_mysql( $profile->{'mysql_host'} );
        if ( _update_local_mycnf( $profile, { localhost => $is_localhost } ) ) {
            $activation_job->done_step('Updating /root/.my.cnf');
        }
        else {
            $activation_job->fail_step( 'Updating /root/.my.cnf', { 'error' => 'Failed to update /root/.my.cnf' } );
            die Cpanel::Exception::create( 'RemoteMySQL::ActivationFailed', 'Failed to activate [asis,MySQL] profile, “[_1]”: [_2]', [ $profile_name, 'Failed to update /root/.my.cnf' ] );
        }

        # STEP 3: Verify root cnf changes allows mysql* tools to work. Test with mysqladmin
        $activation_job->start_step('Testing /root/.my.cnf changes with mysqladmin');

        my $ping_ok = eval {
            Cpanel::MysqlUtils::Connect::get_dbi_handle();
            1;
        };

        if ( !$ping_ok ) {
            my $err = $@;

            $activation_job->fail_step( 'Testing /root/.my.cnf changes with mysqladmin', { 'error' => $err } );

            $activation_job->start_step('Restoring /root/.my.cnf to previous version');
            if ( _restore_old_mycnf($old_my_cnf) ) {
                $activation_job->done_step('Restoring /root/.my.cnf to previous version');
            }
            else {
                $activation_job->fail_step( 'Restoring /root/.my.cnf to previous version', { 'error' => 'Failed to restore previous config.' } );
            }
            die Cpanel::Exception::create(
                'RemoteMySQL::ActivationFailed',
                'Failed to activate [asis,MySQL] profile, “[_1]”: [_2]',
                [ $profile_name, 'Failed to connect to MySQL server after updating /root/.my.cnf' ]
            );
        }
        $activation_job->done_step('Testing /root/.my.cnf changes with mysqladmin');

        # STEP 4: Update DB cache
        $activation_job->start_step('Updating DB Cache');
        Cpanel::SafeRun::Errors::saferunnoerror('/usr/local/cpanel/scripts/update_db_cache');
        $activation_job->done_step('Updating DB Cache');

        # STEP 5: Update cPanel Apps using MySQL
        $activation_job->start_step('Updating cPanel Apps that use MySQL');
        Cpanel::MysqlUtils::Integration::update_apps_that_use_mysql();
        $activation_job->done_step('Updating cPanel Apps that use MySQL');

        my $cpconf_ref = Cpanel::Config::LoadCpConf::loadcpconf();
        if ( !$is_localhost ) {

            # STEP 6: If going to remote host, then transfermysqlusers
            $activation_job->start_step('Transferring MySQL users to remote MySQL host');
            Cpanel::SafeRun::Errors::saferunnoerror( '/usr/local/cpanel/bin/transfermysqlusers', '--from', $current_active_profile );
            $activation_job->done_step('Transferring MySQL users to remote MySQL host');

            # This addresses concerns with edge cases where 'skip_name_resolve' is set
            # on the remote MySQL server, and we can't depend on the 'hostname' authentication.
            # Especially, when the mysql server sees the connecting ip as something different than what the 'main ip' is, etc.
            #
            # In order to ensure that we are granting access to the 'proper' IP - we check the process list in mysql
            # to see what the 'connecting' IP is according to the remote MySQL server, and apply the grants on that IP address.
            my $mainip   = Cpanel::MysqlUtils::Quote::safesqlstring( Cpanel::DIp::MainIP::getmainserverip() );
            my $hostname = Cpanel::MysqlUtils::Quote::safesqlstring( Cpanel::Sys::Hostname::gethostname() );
            my $clientip = Cpanel::MysqlUtils::Quote::safesqlstring( Cpanel::MysqlUtils::Command::sqlcmd('SELECT SUBSTRING_INDEX(`host`,":",1) FROM `information_schema`.`processlist` WHERE ID = CONNECTION_ID();') );

            # STEP 7: Configure MyDNS DB user if needed.
            if ( $cpconf_ref->{'local_nameserver_type'} eq 'mydns' ) {
                $activation_job->start_step('Configuring MyDNS Database user');
                try {
                    require Cpanel::NameServer::Conf::Mydns;
                    my $mydns_conf = Cpanel::NameServer::Conf::Mydns->new();
                    $mydns_conf->load_settings();
                    my $mydns_dbname = Cpanel::MysqlUtils::Quote::safesqlstring( $mydns_conf->{'dns_settings'}->{'database'} );
                    my $mydns_dbuser = Cpanel::MysqlUtils::Quote::safesqlstring( $mydns_conf->{'dns_settings'}->{'db-user'} );
                    my $mydns_dbpass = Cpanel::MysqlUtils::Quote::safesqlstring( $mydns_conf->{'dns_settings'}->{'db-password'} );
                    if ( not( $mydns_dbname && $mydns_dbuser && $mydns_dbpass ) ) {
                        die Cpanel::Exception::create(
                            'Services::NotConfigured',
                            [
                                'service' => 'mydns',
                                'reason'  => 'Failed to load current settings.'
                            ]
                        );
                    }

                    # Get the remote server version and construct the SQL as needed.
                    my $remote_version = Cpanel::MysqlUtils::Version::mysqlversion();
                    foreach my $host_to_use ( $hostname, $mainip, $clientip ) {
                        if ( $remote_version == 8 ) {    # GRANTs don't automagically create users anymore
                            Cpanel::MysqlUtils::Command::sqlcmd("CREATE USER `$mydns_dbuser`\@`$host_to_use` IDENTIFIED BY '$mydns_dbpass';");
                            Cpanel::MysqlUtils::Command::sqlcmd("GRANT SELECT, INSERT, UPDATE, DELETE ON `$mydns_dbname`.* TO '$mydns_dbuser'\@'$host_to_use'");
                        }
                        else {
                            Cpanel::MysqlUtils::Command::sqlcmd("GRANT SELECT, INSERT, UPDATE, DELETE ON `$mydns_dbname`.* TO '$mydns_dbuser'\@'$host_to_use' IDENTIFIED BY '$mydns_dbpass';");
                        }

                    }

                    # Need to run this to create the DB
                    Cpanel::SafeRun::Errors::saferunnoerror( '/usr/local/cpanel/bin/build_mydns_conf', '--createdb' );
                    $activation_job->done_step('Configuring MyDNS Database user');
                }
                catch {
                    $activation_job->fail_step( 'Configuring MyDNS Database user', { 'error' => $_->to_string() } );
                };
            }
        }

        $profile_manager->mark_profile_as_active($profile_name);
        $profile_manager->save_changes_to_disk();

        # STEP 8: Update MyDNS settings
        if ( $cpconf_ref->{'local_nameserver_type'} eq 'mydns' ) {
            $activation_job->start_step('Updating MyDNS settings');
            try {
                require Cpanel::NameServer::Conf::Mydns;
                my $mydns_conf = Cpanel::NameServer::Conf::Mydns->new();
                $mydns_conf->load_settings();
                $mydns_conf->{'dns_settings'}->{'db-host'} = $profile->{'mysql_host'} . ':' . $profile->{'mysql_port'};
                $mydns_conf->flushsettings();

                # If they are using MyDNS then, we should try to queue a background 'sync' event
                # to ensure that the records are up to date.
                system( '/usr/local/cpanel/bin/servers_queue', '--plugin=MydnsTasks', 'queue', 'importmydnsdb' );
                $activation_job->done_step('Updating MyDNS settings');
            }
            catch {
                $activation_job->fail_step( 'Updating MyDNS settings', { 'error' => 'Failed to update MyDNS settings' } );
            }
        }

        # STEP 9: Update the backup config to not backup directories if remote
        if ( !$is_localhost ) {

            $activation_job->start_step('Updating backup settings');

            my $backup_conf = Cpanel::Backup::Config::load();

            if ( $backup_conf->{'MYSQLBACKUP'} eq 'both' or $backup_conf->{'MYSQLBACKUP'} eq 'dir' ) {

                $backup_conf->{'MYSQLBACKUP'} = 'accounts';
                Cpanel::Backup::Config::save($backup_conf);
            }

            $activation_job->done_step('Updating backup settings');
        }
        else {
            print "No need to adjust backup configuration since we are activating a local MySQL profile\n";
        }

        $activation_job->mark_job_done();
    }
    catch {
        $activation_job->mark_job_failed();
        _handle_failure( { 'action' => 'activate', 'exception' => $_ } );
    };

    Cpanel::Hooks::hook(
        {
            'category' => 'Whostmgr',
            'event'    => 'RemoteMySQL::activate_profile',
            'stage'    => 'post',
        },
        {
            'profile_name' => $profile_name,
            'result'       => $activation_job->{progress}{status} eq 'DONE' ? 1 : 0,
        },
    );

    return 0;
}

sub _restore_old_mycnf {
    my $old_my_cnf = shift;
    my $mylock     = Cpanel::SafeFile::safeopen( \*MYCNF, '>', '/root/.my.cnf' );
    if ($mylock) {
        print MYCNF $old_my_cnf;
        Cpanel::SafeFile::safeclose( \*MYCNF, $mylock );
        return 1;
    }
    return;
}

sub _update_local_mycnf {
    my ( $profile_hr, $opts_hr ) = @_;
    my $unix_socket_connection = $opts_hr->{'localhost'} && $profile_hr->{'mysql_port'} == 3306;
    return Cpanel::MysqlUtils::MyCnf::update_mycnf(
        user  => 'root',
        items => [
            {
                host => ( $unix_socket_connection ? undef : $profile_hr->{'mysql_host'} ),
                user => $profile_hr->{'mysql_user'},
                pass => $profile_hr->{'mysql_pass'},
                port => $profile_hr->{'mysql_port'},
            }
        ],
    );
}

sub usage {
    my ( $retval, $msg ) = @_;
    my $fh = $retval ? \*STDERR : \*STDOUT;

    if ( !defined $msg ) {
        $msg = <<USAGE;
$0

Utility to manage the MySQL profiles configured on the server. Available options:

    --import [/path/to/json/file]

            Imports the profiles contained in the specified JSON file.
            To force import and overwrite any existing profiles use the '--force' switch:
                $0 --import import.json --force

    --export [profile name]

            Exports one or more profiles.
            To export more than one profile, specify multiple switches:
                $0 --export profile1 --export profile2

            To export the profiles to a file, specify the 'export_to' switch:
                $0 --export profile1 --export_to  export.json
            Or redirect stdout:
                $0 --export profile1 > export.json

    --activate [profile name]

            Activates the profile specified.

    --recreate_active_profile

            If no active profile is present on the system, this option will allow you to (re)create
            the profile. It reads the current '/root/.my.cnf' file and creates a profile based on that information.

            If the 'force' flag is specified, this option will recreate the active profile, regardless of
            whether one exists or not, based on the content of '/root/.my.cnf'.

    --help

            Displays this help message.

USAGE
    }

    print {$fh} $msg;
    return $retval;
}

sub _handle_failure {
    my $opts = shift;

    my $action     = $opts->{'action'};
    my $exceptions = ref $opts->{'exception'} eq 'HASH' ? $opts->{'exception'}->{'exceptions'} : [ $opts->{'exception'} ];

    my $logger = Cpanel::Logger->new();
    $logger->info( "Failed to $action MySQL profile(s). " . scalar @{$exceptions} . " error(s) occurred." );

    my $index = 1;
    foreach my $error ( @{$exceptions} ) {
        $logger->info( "Error $index:  " . Cpanel::Exception::get_string($error) );
        $index++;
    }

    return 1;
}

1;

./BlackJoker Mini Shell 1.0