众所周知,步入到2021年红帽已不在维护CentOS了,转而取代的是伪滚动发行版 CentOS Stream,但少有Centos的运维人员对其青睐,而许多第三方应用程序(如cPanel)均不支持Stream。为此,博主对红帽这一草率的决定感到惋惜,同时也对自己初学Linux时用过的Centos将逐渐淡出人们视野而表示同情。

  不过,别担心,随后就有大佬们推出了Centos的替代分支Linux。第一个宣布的是 Rocky Linux,来自最初为我们带来 CentOS 的人。紧随其后的是 AlmaLinux两者都是当前 Red Hat Enterprise Linux 二进制兼容并得到社区支持的开源操作系统的替代品,并且已经在成为 CentOS 的事实上的替代品方面取得了长足的进步 。

  博主在Github找到了AlmaLinux的工作人员提供的Centos在线升级更新AlmaLinux的方式,可以说为已在数据中心部署了大量CentOS的运维人员带来了福音,且过程很简便。

  需求:

  ● CentOS 8 的运行实例
  ● 具有 sudo 运行权限的用户

  一、如何将 CentOS 乔迁 AlmaLinux

  首先是更新Centos服务器环境:

- 1|	sudo dnf upgrade -y	很明显这是CentOS 8
- 2|	
- 3|	不管是哪版本,当然也可用yum:
- 4|	yum update && yum upgrade -y

在这里插入图片描述

  创建升级更新时的脚本文件路径:

- 1|	mkdir AlmaLinux
- 2|	博主是在/root下创建的
- 3|
- 4|	进入新建目录:
- 5|	cd  AlmaLinux

  从Github上下载升级脚本:

- 1|	curl -O https://raw.githubusercontent.com/AlmaLinux/almalinux-deploy/master/almalinux-deploy.sh

  若有读者在测试过程中如下图所示被墙了,可通过添加github的ip到hosts文件中来解决: 在这里插入图片描述

- 1|	报错代码:
- 2|	Failed to connect to raw.githubusercontent.com port 443: Connection refused

  解决方法: 访问 https://www.ipaddress.com/ ,查询 raw.githubusercontent.com 的IP 在这里插入图片描述   添加到/etc/hosts文件中:

- 1|	vim /etc/hosts
- 2|
- 3|	添加内容:
- 4|	#Github
- 5|	199.232.68.133  raw.githubusercontent.com

在这里插入图片描述   再次执行curl下载:

- 1|	curl -O https://raw.githubusercontent.com/AlmaLinux/almalinux-deploy/master/almalinux-deploy.sh

  而已下载好文件的朋友,授权执行即可:

- 1|	给almalinux-deploy.sh脚本文件授权:
- 2|	chmod u+x almalinux-deploy.sh
- 3|	
- 4|	执行:
- 5|	sudo ./almalinux-deploy.sh

在这里插入图片描述   等待,直到出现如下消息,即说明乔迁成功!

- 1|	Complete!
- 2|	Run dnf distro-sync -y                                                OK
- 3|	Restoring of alternatives is done                                     OK
- 4|	Generating grub configuration file ...
- 5|	done
- 6|	All Secure Boot related packages which were released by not AlmaLinux are reinstalledOK
- 7|
- 8|	Migration to AlmaLinux is completed

在这里插入图片描述

  重启!
- 1|	reboot
  同时,我们发现新增了 AlmaLinux 8.4 的内核
  重启后发现已乔迁成功!!
  若有读者仍旧无法获取 almalinux-deploy.sh 升级脚本,也莫担心,博主提供脚本内容:
#!/bin/bash

# Description: EL to AlmaLinux migration script.
# License: GPLv3.
# Environment variables:
#   ALMA_RELEASE_URL - almalinux-release package download URL.
#   ALMA_PUBKEY_URL - RPM-GPG-KEY-AlmaLinux download URL.

set -euo pipefail

BASE_TMP_DIR='/root'
OS_RELEASE_PATH='/etc/os-release'
REDHAT_RELEASE_PATH='/etc/redhat-release'
STAGE_STATUSES_DIR='/var/run/almalinux-deploy-statuses'
ALT_ADM_DIR="/var/lib/alternatives"
BAK_DIR="/tmp/alternatives_backup"
ALT_DIR="/etc/alternatives"

# AlmaLinux OS 8.3
MINIMAL_SUPPORTED_VERSION='8.3'
VERSION='0.1.12'

BRANDING_PKGS=("centos-backgrounds" "centos-logos" "centos-indexhtml" \
                "centos-logos-ipa" "centos-logos-httpd" \
                "oracle-backgrounds" "oracle-logos" "oracle-indexhtml" \
                "oracle-logos-ipa" "oracle-logos-httpd" \
                "oracle-epel-release-el8" \
                "redhat-backgrounds" "redhat-logos" "redhat-indexhtml" \
                "redhat-logos-ipa" "redhat-logos-httpd" \
                "rocky-backgrounds" "rocky-logos" "rocky-indexhtml" \
                "rocky-logos-ipa" "rocky-logos-httpd")

REMOVE_PKGS=("centos-linux-release" "centos-gpg-keys" "centos-linux-repos" \
                "libreport-plugin-rhtsupport" "libreport-rhel" "insights-client" \
                "libreport-rhel-anaconda-bugzilla" "libreport-rhel-bugzilla" \
                "oraclelinux-release" "oraclelinux-release-el8" \
                "redhat-release" "redhat-release-eula" \
                "rocky-release" "rocky-gpg-keys" "rocky-repos" \
                "rocky-obsolete-packages")

setup_log_files() {
    exec > >(tee /var/log/almalinux-deploy.log)
    exec 5> /var/log/almalinux-deploy.debug.log
    BASH_XTRACEFD=5
}

# Save the successful status of a stage for future continue of it
# $1 - name of a stage
save_status_of_stage() {
    if [[ 0 != "$(id -u)" ]]; then
        # the function is called in tests and should be skipped
        return 0
    fi
    local -r stage_name="${1}"
    if [[ ! -d "${STAGE_STATUSES_DIR}" ]]; then
        mkdir -p "${STAGE_STATUSES_DIR}"
    fi
    touch "${STAGE_STATUSES_DIR}/${stage_name}"
}

# Get a status of a stage for continue of it
# $1 - name of a stage
# The function returns 1 if stage isn't completed and 0 if it's completed
get_status_of_stage() {
    if [[ 0 != "$(id -u)" ]]; then
        # the function is called in tests and should be skipped
        return 1
    fi
    local -r stage_name="${1}"
    if [[ ! -d "${STAGE_STATUSES_DIR}" ]]; then
        return 1
    fi
    if [[ ! -f "${STAGE_STATUSES_DIR}/${stage_name}" ]]; then
        return 1
    fi
    return 0
}

is_migration_completed() {
    if get_status_of_stage "completed"; then
        printf '\n\033[0;32mMigration to AlmaLinux was already completed\033[0m\n'
        exit 0
    fi
}

# Reports a completed step using a green color.
#
# $1 - Message to print.
report_step_done() {
    local -r message="${1}"
    printf '\033[0;32m%-70sOK\033[0m\n' "${message}"
}

# Reports a failed step using a red color.
#
# $1 - Message to print.
# $2 - Additional information to show (optional).
report_step_error() {
    local -r message="${1}"
    local -r trace="${2:-}"
    printf '\033[0;31m%-70sERROR\033[0m\n' "${message}" 1>&2
    if [[ -n "${trace}" ]]; then
        echo "${trace}" | while read -r line; do
            printf '    %s\n' "${line}" 1>&2
        done
    fi
}

# Prints program usage information.
show_usage() {
    echo -e 'Migrates an EL system to AlmaLinux\n'
    echo -e 'Usage: almalinux-deploy.sh [OPTION]...\n'
    echo '  -h, --help           show this message and exit'
    echo '  -v, --version        print version information and exit'
}

# Terminates the program if it is not run with root privileges
assert_run_as_root() {
    if [[ $(id -u) -ne 0 ]]; then
        report_step_error 'Check root privileges' \
            'Migration tool must be run as root'
        exit 2
    fi
    report_step_done 'Check root privileges'
}

# Terminates the program if UEFI Secure Boot is enabled
assert_secureboot_disabled() {
    local -r message='Check Secure Boot disabled'
    if LC_ALL='C' mokutil --sb-state 2>/dev/null | grep -P '^SecureBoot\s+enabled' 1>/dev/null; then
        report_step_error "${message}" 'Secure Boot is not supported yet'
        exit 1
    fi
    report_step_done "${message}"
}

# Prints a system architecture.
get_system_arch() {
    uname -i
}

# Reads a variable value from /etc/os-release.
#
# $1 - variable name.
#
# Returns the variable value.
get_os_release_var() {
    local -r var="${1}"
    local val
    if ! val="$(grep -oP "^${var}=\"\K.*?(?=\")" "${OS_RELEASE_PATH}")"; then
        echo "Error: ${var} is not found in ${OS_RELEASE_PATH}" >&2
        exit 1
    fi
    echo "${val}"
}

# Detects an operational system version.
#
# $1 - operational system type.
#
# Prints OS version.
get_os_version() {
    local -r os_type="${1}"
    local os_version
    if [[ "${os_type}" == 'centos' ]]; then
        if ! os_version="$(grep -oP 'CentOS\s+Linux\s+release\s+\K(\d+\.\d+)' \
                                    "${REDHAT_RELEASE_PATH}" 2>/dev/null)"; then
            report_step_error "Detect ${os_type} version"
        fi
    else
        os_version="$(get_os_release_var 'VERSION_ID')"
    fi
    echo "${os_version}"
}

# Prints control type and version.
get_panel_info() {
    local panel_type=''
    local panel_version=''
    local -r cpanel_file='/usr/local/cpanel/cpanel'
    local -r plesk_file='/usr/local/psa/version'
    if [[ -x "${cpanel_file}" ]]; then
        panel_type='cpanel'
        panel_version=$("${cpanel_file}" -V 2>/dev/null | grep -oP '^[\d.]+')
    elif [[ -f "${plesk_file}" ]]; then
        panel_type='plesk'
        panel_version=$(grep -oP '^[\d.]+' "${plesk_file}" 2>/dev/null)
    fi
    echo "${panel_type} ${panel_version}"
}

# Terminates the program if a platform is not supported by AlmaLinux.
#
# $1 - Operational system id (ID).
# $2 - Operational system version (e.g. 8 or 8.3).
# $3 - System architecture (e.g. x86_64).
assert_supported_system() {
    if get_status_of_stage "assert_supported_system"; then
        return 0
    fi
    local -r os_type="${1}"
    local -r os_version="${2:0:1}"
    local -r arch="${3}"
    case "${arch}" in
        x86_64|aarch64)
            ;;
        *)
            report_step_error "Check ${arch} architecture is supported"
            exit 1
            ;;
    esac
    if [[ ${os_version} -ne ${MINIMAL_SUPPORTED_VERSION:0:1} ]]; then
        report_step_error "Check EL${os_version} is supported"
        exit 1
    fi
    os_types=("centos" "almalinux" "ol" "rhel" "rocky")
    if [[ ! " ${os_types[*]} " =~ ${os_type} ]]; then
        report_step_error "Check ${os_type} operating system is supported"
        exit 1
    fi
    report_step_done "Check ${os_type}-${os_version}.${arch} is supported"
    save_status_of_stage "assert_supported_system"
    return 0
}

# Terminates the program if a control panel is not supported by AlmaLinux.
#
# $1 - Control panel type.
# $2 - Control panel version.
assert_supported_panel() {
    if get_status_of_stage "assert_supported_panel"; then
        return 0
    fi
    local -r panel_type="${1}"
    local -r panel_version="${2}"
    local plesk_min_major=18
    local plesk_min_minor=0
    local plesk_min_micro=35
    local major
    local minor
    local micro
    local error_msg="${panel_type} version \"${panel_version}\" is not supported. Please update the control panel to version \"${plesk_min_major}.${plesk_min_minor}.${plesk_min_micro}\"."
    if [[ "${panel_type}" == 'plesk' ]]; then
    local IFS=.
read -r major minor micro << EOF
${panel_version}
EOF
        if [[ -z ${micro} ]]; then
            micro=0
        fi
        if [[ -z ${minor} ]]; then
            minor=0
        fi
        if [[ ${major} -lt ${plesk_min_major} ]]; then
            report_step_error "${error_msg}"
            exit 1
        elif [[ ${major} -eq ${plesk_min_major} && ${minor} -lt ${plesk_min_minor} ]]; then
            report_step_error "${error_msg}"
            exit 1
        elif [[ ${major} -eq ${plesk_min_major} && ${minor} -eq ${plesk_min_minor} && ${micro} -lt ${plesk_min_micro} ]]; then
            report_step_error "${error_msg}"
            exit 1
        fi
    fi
    save_status_of_stage "assert_supported_panel"
}

# Returns a latest almalinux-release RPM package download URL.
#
# $1 - AlmaLinux major version (e.g. 8).
# $2 - System architecture (e.g. x86_64).
#
# Prints almalinux-release RPM package download URL.
get_release_file_url() {
    local -r os_version="${1:0:1}"
    local -r arch="${2}"
    echo "${ALMA_RELEASE_URL:-https://repo.almalinux.org/almalinux/almalinux-release-latest-${os_version}.${arch}.rpm}"
}

# Downloads and installs the AlmaLinux public PGP key.
#
# $1 - Temporary directory path.
install_rpm_pubkey() {
    if get_status_of_stage "install_rpm_pubkey"; then
        return 0
    fi
    local -r tmp_dir="${1}"
    local -r pubkey_url="${ALMA_PUBKEY_URL:-https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux}"
    local -r pubkey_path="${tmp_dir}/RPM-GPG-KEY-AlmaLinux"
    local -r step='Download RPM-GPG-KEY-AlmaLinux'
    local output
    if ! output=$(curl -f -s -S -o "${pubkey_path}" "${pubkey_url}" 2>&1); then
        report_step_error "${step}" "${output}"
        exit 1
    else
        report_step_done "${step}"
    fi
    rpm --import "${pubkey_path}"
    report_step_done 'Import RPM-GPG-KEY-AlmaLinux to RPM DB'
    rm -f "${pubkey_path}"
    save_status_of_stage "install_rpm_pubkey"
}

# Downloads almalinux-release package.
#
# $1 - almalinux-release package download URL.
# $2 - Temporary directory path.
#
# Prints downloaded file path.
download_release_file() {
    local -r release_url="${1}"
    local -r tmp_dir="${2}"
    local -r release_path="${tmp_dir}/almalinux-release-latest.rpm"
    local output
    if ! output=$(curl -f -s -S -o "${release_path}" "${release_url}" 2>&1); then
        report_step_error 'Download almalinux-release package' "${output}"
        exit 1
    fi
    echo "${release_path}"
}

# Terminates the program if a given RPM package checksum/signature is invalid.
#
# $1 - RPM package path.
assert_valid_package() {
    local -r pkg_path="${1}"
    local output
    if ! output=$(rpm -K "${pkg_path}" 2>&1); then
        report_step_error "Verify $(basename "${pkg_path}") package" \
            "${output}"
        exit 1
    fi
    report_step_done 'Verify almalinux-release package'
}

# Terminates the program if OS version doesn't match AlmaLinux version.
#
# $1 - OS version.
# $2 - almalinux-release package file path.
assert_compatible_os_version() {
    if get_status_of_stage "assert_compatible_os_version"; then
        return 0
    fi
    local -r os_version="${1}"
    local -r release_path="${2}"
    local alma_version
    alma_version=$(rpm -qp --queryformat '%{version}' "${release_path}")

    if [[ "${os_version:2:3}" -lt "${MINIMAL_SUPPORTED_VERSION:2:3}" ]]; then
        report_step_error "Please upgrade your OS from ${os_version} to" \
        "at least ${MINIMAL_SUPPORTED_VERSION} and try again"
        exit 1
    fi
    if [[ "${os_version:2:3}" -gt "${alma_version:2:3}" ]]; then
        report_step_error "Version of you OS ${os_version} is not supported yet"
        exit 1
    fi
    report_step_done 'Your OS is supported'
    save_status_of_stage "assert_compatible_os_version"
}

# Backup /etc/issue* files
backup_issue() {
    if get_status_of_stage "backup_issue"; then
        return 0
    fi
    for file in $(rpm -Vf /etc/issue | cut -d' ' -f4); do
        if [[ ${file} =~ "/etc/issue" ]]; then
            cp "${file}" "${file}.bak"
        fi
    done
    save_status_of_stage "backup_issue"
}

# Restore /etc/issue* files
restore_issue() {
    if get_status_of_stage "restore_issue"; then
        return 0
    fi
    for file in /etc/issue /etc/issue.net; do
        [ ! -f "${file}.bak" ] || mv -f ${file}.bak ${file}
    done
    save_status_of_stage "restore_issue"
}

# Recursively removes a given directory.
#
# $1 - Directory path.
cleanup_tmp_dir() {
    rm -fr "${1:?}"
}

# Remove OS specific packages
remove_os_specific_packages_before_migration() {
    if get_status_of_stage "remove_os_specific_packages_before_migration"; then
        return 0
    fi
    for i in "${!REMOVE_PKGS[@]}"; do
        if ! rpm -q "${REMOVE_PKGS[i]}" &> /dev/null; then
            # remove an erased package from the list if it isn't installed
            unset "REMOVE_PKGS[i]"
        fi
    done
    if [[ "${#REMOVE_PKGS[@]}" -ne 0 ]]; then
        rpm -e --nodeps --allmatches "${REMOVE_PKGS[@]}"
    fi
    report_step_done 'Remove OS specific rpm packages'
    save_status_of_stage "remove_os_specific_packages_before_migration"
}

# Remove not needed Red Hat directories
remove_not_needed_redhat_dirs() {
    if get_status_of_stage "remove_not_needed_redhat_dirs"; then
        return 0
    fi
    [ -d /usr/share/doc/redhat-release ] && rm -r /usr/share/doc/redhat-release
    [ -d /usr/share/redhat-release ] && rm -r /usr/share/redhat-release
    save_status_of_stage "remove_not_needed_redhat_dirs"
}

# Install package almalinux-release
install_almalinux_release_package() {
    if get_status_of_stage "install_almalinux_release_package"; then
        return 0
    fi
    local -r release_path="${1}"
    rpm -Uvh "${release_path}"
    report_step_done 'Install almalinux-release package'
    save_status_of_stage "install_almalinux_release_package"
}

# Remove brand packages and install the same AlmaLinux packages
replace_brand_packages() {
    if get_status_of_stage "replace_brand_packages"; then
        return 0
    fi
    local alma_pkgs=()
    local alma_pkg
    local output
    local pkg_name
    # replace GUI packages
    for i in "${!BRANDING_PKGS[@]}"; do
        pkg_name="${BRANDING_PKGS[i]}"
        if rpm -q "${pkg_name}" &>/dev/null; then
            # shellcheck disable=SC2001
            case "${pkg_name}" in
                oracle-epel-release-el8)
                    alma_pkg="epel-release"
                    ;;
                *)
                    # shellcheck disable=SC2001
                    alma_pkg="$(echo "${pkg_name}" | sed 's#centos\|oracle\|redhat\|rocky#almalinux#')"
                    ;;
            esac
            alma_pkgs+=("${alma_pkg}")
        else
            unset "BRANDING_PKGS[i]"
        fi
    done
    if [[ "${#BRANDING_PKGS[@]}" -ne 0 ]]; then
        rpm -e --nodeps --allmatches "${BRANDING_PKGS[@]}"
        report_step_done "Remove ${BRANDING_PKGS[*]} packages"
    fi
    if [[ "${#alma_pkgs[@]}" -ne 0 ]]; then
        if ! output=$(dnf install -y "${alma_pkgs[@]}" 2>&1); then
            report_step_error "Install ${alma_pkgs[*]} packages" "${output}"
        fi
        report_step_done "Install ${alma_pkgs[*]} packages"
    fi
    save_status_of_stage "replace_brand_packages"
}

# Converts a CentOS like system to AlmaLinux
#
# $1 - almalinux-release RPM package path.
migrate_from_centos() {
    if get_status_of_stage "migrate_from_centos"; then
        return 0
    fi
    local -r release_path="${1}"
    # replace OS packages with almalinux-release
    # and OS centos-specific packages
    remove_os_specific_packages_before_migration
    remove_not_needed_redhat_dirs
    install_almalinux_release_package "${release_path}"
    replace_brand_packages
    save_status_of_stage "migrate_from_centos"
}

# Executes the 'dnf distro-sync -y' command.
#
distro_sync() {
    if get_status_of_stage "distro_sync"; then
        return 0
    fi
    local -r step='Run dnf distro-sync -y'
    local ret_code=0
    local dnf_repos="--enablerepo=powertools"
    # create needed repo
    if [ "${panel_type}" == "plesk" ]; then
        plesk installer --select-release-current --show-components --skip-cleanup
        dnf_repos+=",PLESK_*-dist"
    fi
    dnf check-update || {
        ret_code=${?}
        if [[ ${ret_code} -ne 0 ]] && [[ ${ret_code} -ne 100 ]]; then
            report_step_error "${step}. Exit code: ${ret_code}"
            exit ${ret_code}
        fi
    }
    dnf distro-sync -y "${dnf_repos}" || {
        ret_code=${?}
        report_step_error "${step}. Exit code: ${ret_code}"
        exit ${ret_code}
    }
    # remove unnecessary repo
    if [ "${panel_type}" == "plesk" ]; then
        plesk installer --select-release-current --show-components
    fi
    report_step_done "${step}"
    save_status_of_stage "distro_sync"
}

install_kernel() {
    if get_status_of_stage "install_kernel"; then
        return 0
    fi
    if ! output=$(rpm -q kernel 2>&1); then
        if output=$(dnf -y install kernel 2>&1); then
            report_step_done "Install AlmaLinux kernel"
        else
            report_step_error "Install AlmaLinux kernel"
        fi
    fi
    save_status_of_stage "install_kernel"
}

grub_update() {
    if get_status_of_stage "grub_update"; then
        return 0
    fi
    if [ -d /sys/firmware/efi ]; then
        if [ -d /boot/efi/EFI/almalinux ]; then
            grub2-mkconfig -o /boot/efi/EFI/almalinux/grub.cfg
        elif [ -d /boot/efi/EFI/centos ]; then
            grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
        else
            grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg
        fi
    else
        grub2-mkconfig -o /boot/grub2/grub.cfg
    fi
    save_status_of_stage "grub_update"
}

# Check do we have custom kernel (e.g. kernel-uek) and print warning
check_custom_kernel() {
    if get_status_of_stage "check_custom_kernel"; then
        return 0
    fi
    local output
    output=$(rpm -qa | grep kernel-uek) || :
    if [ -n "${output}" ]; then
        if [ -x /usr/bin/mokutil ] && /usr/bin/mokutil --sb-state 2>&1 | grep -q enabled; then
            echo -ne "\n!! There are kernels left from previous operating system
that won't boot in Secure Boot mode anymore:\n"
        else
            echo "There are kernels left from previous operating system:"
        fi
        # shellcheck disable=SC2001,SC2086
        echo "$output" | sed 's# #\n#'
        echo ""
        echo "If you don't need them, you can remove them by using the 'dnf remove" \
            "${output}' command"
    fi
    save_status_of_stage "check_custom_kernel"
}

_backup_alternative() {
    local path="${1}"
    local bak_dir="${2}"
    local bak_prefix="current_point"
    local alt_name=
    local alt_dest=
    local alt_link=
    alt_name="$(basename "${path}")"
    alt_link="${ALT_DIR}/${alt_name}"
    alt_dest="$(readlink "${alt_link}")"
    mkdir -p "${bak_dir}"

    # backup the current state of an alternative
    echo "${alt_dest} ${alt_link}" > "${bak_dir}/${bak_prefix}.${alt_name}"
}

_restore_alternative() {
    local path="${1}"
    local bak_dir="${2}"
    local bak_point="${bak_dir}/current_point"
    local alt_name=
    local shift_begin=3
    local shift_middle=2
    local main_link=
    local alternatives=()
    local priorities=()
    local links=()
    local dests=()
    local names=()
    local line=
    local _alt_link=
    local _alt_dest=
    local alt_link=
    local alt_dest=
    local i=
    local empty_num=
    local len=
    local begin_index=
    local end_index=
    len="$(wc -l < "${path}")"
    # name of a backup alternatives, e.g. python or java
    alt_name="$(basename "${path}")"
    # system link which points to an alternative, e.g.
    # /usr/bin/unversioned-python -> /etc/alternatives/python
    main_link="$(sed -n -e 2p "${path}")"
    i=0
    # this cycle saves info about slave's links
    # and slave's names of an alternative
    # e.g.
    # unversioned-python - name
    # /usr/bin/python - link
    # python2 - name
    # /usr/bin/python2 - link
    # unversioned-python-man - name
    # /usr/share/man/man1/python.1.gz - link
    while read -r line; do
        i=$(("${i}" + 1))
        if [[ -z $line ]]; then
            empty_num=$(("${shift_begin}" + "${i}"))
            break
        fi
        if [[ $(( "${i}" % 2 )) -eq 0 ]]; then
            links+=("$line")
        else
            names+=("$line")
        fi
    done < <(tail -n +${shift_begin} "${path}")

    # this cycle saves info about available alternatives and them priorites
    # e.g.
    # /usr/libexec/no-python alternative with priority 404
    # /usr/bin/python3 alternative with priority 300
    while [[ "${len}" -gt $(("${#names[@]}" + "${empty_num}")) ]]; do
        alternatives+=("$(sed -n -e "${empty_num}"p "${path}")")
        priorities+=("$(sed -n -e $(("${empty_num}" + 1))p "${path}")")
        begin_index="$(("${empty_num}" + "${shift_middle}"))"
        end_index="$(("${#names[@]}" + "${shift_middle}" + "${empty_num}"))"
        # this cycle saves info about slave's dests for an each alternative,
        # e.g.
        # /usr/bin/python3 - dest for /usr/bin/python
        #/usr/share/man/man1/python3.1.gz - dest for /usr/share/man/man1/python.1.gz
        # destination can be empty and in this case we don't create symlink
        if [[ "${begin_index}" -ne "${end_index}" ]]; then
            while read -r line; do
                dests+=("$line")
            done < <(sed -n -e "${begin_index}","${end_index}"p "${path}")
        fi
        empty_num=$(("${empty_num}" + "${#names[@]}" + "${shift_middle}"))
    done
    # read and restore current state of an alternative
    # e.g.
    # /etc/alternatives/python -> /usr/libexec/no-python
    while read -r _alt_dest _alt_link; do
        alt_link="${_alt_link}"
        alt_dest="${_alt_dest}"
        if [[ ! -e "${alt_link}" ]]; then
            ln -sf "${alt_dest}" "${alt_link}"
        fi
    done < "${bak_point}.${alt_name}"

    for i in "${!alternatives[@]}"; do
        if [[ ! -e "${main_link}" ]]; then
            # restore system symlink for alternative, e.g.
            # /usr/bin/unversioned-python -> /etc/alternatives/python
            ln -sf "${alt_link}" "${main_link}"
        fi
        for j in "${!names[@]}"; do
            if [[ "${alt_dest}" == "${alternatives[$i]}" ]]; then
                if [[ -e "${dests[$(( "${j}" + "${#links[@]}" * "${i}"))]}" && ! -e "${ALT_DIR}/${names[$j]}" ]]; then
                    # restore system slave link to an alternative, e.g.
                    # /etc/alternatives/unversioned-python-man -> /usr/share/man/man1/unversioned-python.1.gz
                    ln -sf "${dests[$(( "${j}" + "${#links[@]}" * "${i}"))]}" "${ALT_DIR}/${names[$j]}"
                fi
                if [[ -e "${ALT_DIR}/${names[$j]}" && ! -e "${links[$j]}" ]]; then
                    # restore slave link for an alternative
                    # e.g. /usr/share/man/man1/python.1.gz -> /etc/alternatives/unversioned-python-man
                    ln -sf "${ALT_DIR}/${names[$j]}" "${links[$j]}"
                fi
            fi
        done
    done

}

# backup existing alternatives, including the current states of alternatives
backup_alternatives() {
    if get_status_of_stage "backup_alternatives"; then
        return 0
    fi
    for alt_file in "${ALT_ADM_DIR}"/*; do
        _backup_alternative "${alt_file}" "${BAK_DIR}"
    done
    /usr/bin/cp -rf "${ALT_ADM_DIR}" "${BAK_DIR}"/.
    report_step_done "Backup of alternatives is done"
    save_status_of_stage "backup_alternatives"
}

# restore existing alternatives, including the current states of alternatives
restore_alternatives() {
    if get_status_of_stage "restore_alternatives"; then
        return 0
    fi
    for alt_file in "${BAK_DIR}/alternatives/"*; do
        _restore_alternative "${alt_file}" "${BAK_DIR}"
    done
    /usr/bin/cp -rn "${BAK_DIR}/alternatives/"* "${ALT_ADM_DIR}/"
    report_step_done "Restoring of alternatives is done"
    save_status_of_stage "restore_alternatives"
}

add_efi_boot_record() {
    if get_status_of_stage "add_efi_boot_record"; then
        return 0
    fi
    if [[ ! -d /sys/firmware/efi ]]; then
        return
    fi
    local device
    local disk_name
    local disk_num
    device="$(df -T /boot/efi | sed -n 2p | awk '{ print $1}')"
    disk_name="$(echo "${device}" | sed -re 's/(p|)[0-9]$//g')"
    disk_num="$(echo "${device}" | tail -c 2|sed 's|[^0-9]||g')"
    efibootmgr -c -L "AlmaLinux" -l "\EFI\almalinux\shimx64.efi" -d "${disk_name}" -p "${disk_num}"
    report_step_done "The new EFI boot record for AlmaLinux is added"
    save_status_of_stage "add_efi_boot_record"
}

reinstall_secure_boot_packages() {
    if get_status_of_stage "reinstall_secure_boot_packages"; then
        return 0
    fi
    local kernel_package
    for pkg in $(rpm -qa | grep -E 'shim|fwupd|grub2'); do
        if [[ "AlmaLinux" != "$(rpm -q --queryformat '%{vendor}' "$pkg")" ]]; then
            yum reinstall "${pkg}" -y
        fi
    done
    kernel_package="$(rpm -qf "$(grubby --default-kernel)")"
    if [[ "AlmaLinux" != "$(rpm -q --queryformat '%{vendor}' "${kernel_package}")" ]]; then
        yum reinstall "${kernel_package}" -y
    fi
    report_step_done "All Secure Boot related packages which were released by not AlmaLinux are reinstalled"
    save_status_of_stage "reinstall_secure_boot_packages"
}


main() {
    is_migration_completed
    local arch
    local os_version
    local os_type
    local release_url
    local tmp_dir
    local release_path
    local panel_type
    local panel_version
    assert_run_as_root
    arch="$(get_system_arch)"
    os_type="$(get_os_release_var 'ID')"
    os_version="$(get_os_version "${os_type}")"
    #os_version="$(get_os_release_var 'VERSION_ID')"
    #os_version="${os_version:0:1}"
    assert_supported_system "${os_type}" "${os_version}" "${arch}"

    read -r panel_type panel_version < <(get_panel_info)
    assert_supported_panel "${panel_type}" "${panel_version}"

    release_url=$(get_release_file_url "${os_version}" "${arch}")
    tmp_dir=$(mktemp -d --tmpdir="${BASE_TMP_DIR}" .alma.XXXXXX)
    # shellcheck disable=SC2064
    trap "cleanup_tmp_dir ${tmp_dir}" EXIT
    install_rpm_pubkey "${tmp_dir}"

    release_path=$(download_release_file "${release_url}" "${tmp_dir}")
    report_step_done 'Download almalinux-release package'

    assert_valid_package "${release_path}"
    assert_compatible_os_version "${os_version}" "${release_path}"

    case "${os_type}" in
    almalinux|centos|ol|rhel|rocky)
        backup_issue
        migrate_from_centos "${release_path}"
        ;;
    *)
        report_step_error "Migrate ${os_type}: not supported"
        exit 1
        ;;
    esac

    backup_alternatives
    distro_sync
    restore_alternatives
    restore_issue
    install_kernel
    grub_update
    reinstall_secure_boot_packages
    add_efi_boot_record
    check_custom_kernel
    save_status_of_stage "completed"
    printf '\n\033[0;32mMigration to AlmaLinux is completed\033[0m\n'
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    for opt in "$@"; do
        case ${opt} in
        -h | --help)
            show_usage
            exit 0
            ;;
        -v | --version)
            echo "${VERSION}"
            exit 0
            ;;
        -t | --tests)
            exit 0
            ;;
        *)
            echo "Error: unknown option ${opt}" >&2
            exit 2
            ;;
        esac
    done
    setup_log_files
    set -x
    main
    set +x
fi
  授权后执行即可!

  二、如何将CentOS 乔迁 Rocky Linux

  与乔迁 AlmaLinux 同理,我们采用Github上的脚本在线升级:

- 1|	创建脚本下载路径,并进入:
- 2|	mkdir Rocky-Linux  && cd Rocky-Linux 
- 3|	
- 4|	下载脚本:
- 5|	curl -O https://raw.githubusercontent.com/rocky-linux/rocky-tools/main/migrate2rocky/migrate2rocky.sh

  若遇到 " Failed to connect to raw.githubusercontent.com port 443 “的报错时,解决方法和前文一致。

- 1|	授权:
- 2|	chmod +x migrate2rocky.sh
- 3|	
- 4|	选 -r 运行 !!
- 5|	sudo ./migrate2rocky.sh -r

在这里插入图片描述   若你的Centos 8 dnf包管理需更新:

- 1|	dnf install dnf && dnf update -y
- 2|	更新后再 sudo ./migrate2rocky.sh -r

  如下图,正式升级更换Rocky,耐心等候: 在这里插入图片描述

  出现如下执行消息表示,升级完成:

- 1|	Done, please reboot your system.
- 2|	A log of this installation can be found at /var/log/migrate2rocky.log

在这里插入图片描述   若不在订阅 REDHAT(红帽) 则可执行卸载删除其已无用文件:

- 1|	dnf remove subscription-manager && dnf-plugin-subscription-manager

在这里插入图片描述

  重启!
- 1|	reboot
  博主同样为无法下Rocky 的 migrate2rocky.sh升级脚本的小伙伴提供脚本内容,自主创建授权就可运行:
#!/bin/bash
# 
# migrate2rocky - Migrate another EL8 distribution to RockyLinux 8.
# By: Peter Ajamian <peter@pajamian.dhs.org>
# Adapted from centos2rocky.sh by label <label@rockylinux.org>
#
# The latest version of this script can be found at:
# https://github.com/rocky-linux/rocky-tools
#
# Copyright (c) 2021 Rocky Enterprise Software Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

## Using this script means you accept all risks of system instability.

# These checks need to be right at the top because we start with bash-isms right
# away in this script.
if [ -n "$POSIXLY_CORRECT" ] || [ -z "$BASH_VERSION" ]; then
    printf '%s\n' "bash >= 4.2 is required for this script." >&2
    exit 1
fi

# We need bash version >= 4.2 for associative arrays and other features.
if (( BASH_VERSINFO[0]*100 + BASH_VERSINFO[1] < 402 )); then
    printf '%s\n' "bash >= 4.2 is required for this script." >&2
    exit 1
fi

# Make sure we're root.
if (( EUID != 0 )); then
    printf '%s\n' "You must run this script as root.  Either use sudo or 'su -c ${0}'" >&2
    exit 1
fi

# Path to logfile
logfile=/var/log/migrate2rocky.log

# Send all output to the logfile as well as stdout.
# After the following we get:
# Output to 1 goes to stdout and the logfile.
# Output to 2 goes to stderr and the logfile.
# Output to 3 just goes to stdout.
# Output to 4 just goes to stderr.
# Output to 5 just goes to the logfile.
truncate -s0 "$logfile"
# shellcheck disable=SC2094
exec \
    3>&1 \
    4>&2 \
    5>> "$logfile" \
    > >(tee -a "$logfile") \
    2> >(tee -a "$logfile" >&2)

# List nocolor last here so that -x doesn't bork the display.
errcolor=$(tput setaf 1)
infocolor=$(tput setaf 6)
nocolor=$(tput op)

# Single arg just gets returned verbatim, multi arg gets formatted via printf.
# First arg is the name of a variable to store the results.
msg_format () {
    local _var
    _var="$1"
    shift
    if (( $# > 1 )); then
	# shellcheck disable=SC2059
	printf -v "$_var" "$@"
    else
	printf -v "$_var" "%s" "$1"
    fi
}

# Send an info message to the log file and stdout (with color)
infomsg () {
    local msg
    msg_format msg "$@"
    printf '%s' "$msg" >&5
    printf '%s%s%s' "$infocolor" "$msg" "$nocolor" >&3
}

# Send an error message to the log file and stderr (with color)
errmsg () {
    local msg
    msg_format msg "$@"
    printf '%s' "$msg" >&5
    printf '%s%s%s' "$errcolor" "$msg" "$nocolor" >&4
}

export LC_ALL=en_US.UTF-8 LANGUAGE=en_US
shopt -s nullglob

SUPPORTED_MAJOR="8"
SUPPORTED_PLATFORM="platform:el$SUPPORTED_MAJOR"
ARCH=$(arch)

gpg_key_url="https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-rockyofficial"
gpg_key_sha512="88fe66cf0a68648c2371120d56eb509835266d9efdf7c8b9ac8fc101bdf1f0e0197030d3ea65f4b5be89dc9d1ef08581adb068815c88d7b1dc40aa1c32990f6a"

sm_ca_dir=/etc/rhsm/ca
unset tmp_sm_ca_dir

# all repos must be signed with the same key given in $gpg_key_url
declare -A repo_urls
repo_urls=(
    [rockybaseos]="https://dl.rockylinux.org/pub/rocky/${SUPPORTED_MAJOR}/BaseOS/$ARCH/os/"
    [rockyappstream]="https://dl.rockylinux.org/pub/rocky/${SUPPORTED_MAJOR}/AppStream/$ARCH/os/"
)

unset CDPATH

exit_message() {
  errmsg $'\n'"$1"$'\n\n'
  final_message
  exit 1
}

final_message() {
    errmsg '%s ' \
	"An error occurred while we were attempting to convert your system to" \
	"Rocky Linux. Your system may be unstable. Script will now exit to" \
	"prevent possible damage."$'\n\n'
    logmessage
}

logmessage(){
    printf '%s%s%s\n' "$infocolor" \
	"A log of this installation can be found at $logfile" \
	"$nocolor" >&3
}

# This just grabs a field from os-release and returns it.
os-release () (
    . /etc/os-release
    if ! [[ ${!1} ]]; then
	return 1
    fi
    printf '%s\n' "${!1}"
)

# Check the version of a package against a supplied version number.  Note that
# this uses sort -V to compare the versions which isn't perfect for rpm package
# versions, but to do a proper comparison we would need to use rpmdev-vercmp in
# the rpmdevtools package which we don't want to force-install.  sort -V should
# be adequate for our needs here.
pkg_ver() (
    ver=$(rpm -q --qf '%{VERSION}\n' "$1") || return 2
    if [[ $(sort -V <<<"$ver"$'\n'"$2" | head -1) != "$2" ]]; then
	return 1
    fi
    return 0
)

# Set up a temporary directory.
pre_setup () {
    if ! tmp_dir=$(mktemp -d) || [[ ! -d "$tmp_dir" ]]; then
	exit_message "Error creating temp dir"
    fi
    # failglob makes pathname expansion fail if empty, dotglob adds files
    # starting with . to pathname expansion
    if ( shopt -s failglob dotglob; : "$tmp_dir"/* ) 2>/dev/null ; then
	exit_message "Temp dir not empty"
    fi
}

# Cleanup function gets rid of the temporary directory.
exit_clean () {
    if [[ -d "$tmp_dir" ]]; then
	rm -rf "$tmp_dir"
    fi
}

pre_check () {
    if [[ -e /etc/rhsm/ca/katello-server-ca.pem ]]; then
	    exit_message "Migration from Katello-modified systems is not supported by migrate2rocky. See the README file for details."
    fi
    if [[ -e /etc/salt/minion.d/susemanager.conf ]]; then
        exit_message "Migration from Uyuni/SUSE Manager-modified systems is not supported by migrate2rocky. See the README file for details."
    fi
}

# All of the binaries used by this script are available in a EL8 minimal install
# and are in /bin, so we should not encounter a system where the script doesn't
# work unless it's severly broken.  This is just a simple check that will cause
# the script to bail if any expected system utilities are missing.
bin_check() {
    # Check the platform.
    if [[ $(os-release PLATFORM_ID) != "$SUPPORTED_PLATFORM" ]]; then
	exit_message "This script must be run on an EL8 distribution.  Migration from other distributions is not supported."
    fi

    local -a missing bins
    bins=(
	rpm dnf awk column tee tput mkdir
	cat arch sort uniq rmdir rm head
	curl sha512sum mktemp
    )
    if [[ $update_efi ]]; then
	bins+=(findmnt grub2-mkconfig efibootmgr grep mokutil lsblk)
    fi
    for bin in "${bins[@]}"; do
	if ! type "$bin" >/dev/null 2>&1; then
	    missing+=("$bin")
	fi
    done

    local -A pkgs
    pkgs=(
	[dnf]=4.2
	[dnf-plugins-core]=0
    )

    for pkg in "${!pkgs[@]}"; do
	ver=${pkgs[$pkg]}
	if ! pkg_ver "$pkg" "$ver"; then
	    exit_message \
"$pkg >= $ver is required for this script.  Please run "\
"\"dnf install $pkg; dnf update\" first."
	fi
    done;

    if (( ${#missing[@]} )); then
	exit_message "Commands not found: ${missing[*]}.  Possible bad PATH setting or corrupt installation."
    fi
}

# This function will overwrite the repoquery_results associative array with the
# info for the resulting package.  Note that we explicitly disable the epel repo
# as a special-case below to avoid having the extras repository map to epel.
repoquery () {
    local name val prev result
    result=$(
	dnf -q --setopt=epel.excludepkgs=epel-release repoquery -i "$1" ||
	    exit_message "Failed to fetch info for package $1."
    )
    if ! [[ $result ]]; then
	# We didn't match this package, the repo could be disabled.
	return 1
    fi
    declare -gA repoquery_results=()
    while IFS=" :" read -r name val; do
	if [[ -z $name ]]; then
	    repoquery_results[$prev]+=" $val"
	else
	    prev=$name
	    repoquery_results[$name]=$val
	fi
    done <<<"$result"
}

# This function will overwrite the repoinfo_results associative array with the
# info for the resulting repository.
repoinfo () {
    local name val result
    result=$(dnf -q repoinfo "$1") ||
    	exit_message "Failed to fetch info for repository $1."
    if [[ $result == 'Total packages: 0' ]]; then
	# We didn't match this repo.
	return 1
    fi
    declare -gA repoinfo_results=()
    while IFS=" :" read -r name val; do
	if [[ ! ( $name || $val) ]]; then
		continue
	fi
	if [[ -z $name ]]; then
	    repoinfo_results[$prev]+=" $val"
	else
	    prev=$name
	    repoinfo_results[$name]=$val
	fi
    done <<<"$result"

    # dnf repoinfo doesn't return the gpgkey, but we need that so we have to get
    # it from the repo file itself.
    # "end_of_file" is a hack here.  Since it is not a valid dnf setting we know
    # it won't appear in a .repo file on a line by itself, so it's safe to
    # search for the string to make the awk parser look all the way to the end
    # of the file.
    # shellcheck disable=SC2154
    repoinfo_results[Repo-gpgkey]=$(
	awk '
	    $0=="['"${repoinfo_results[Repo-id]}"']",$0=="end_of_file" {
		if (l++ < 1) {next}
		else if (/^\[.*\]$/) {nextfile}
		else if (sub(/^gpgkey\s*=\s*file:\/\//,"")) {print; nextfile}
		else {next}
	    }
	' < "${repoinfo_results[Repo-filename]}"
    )

    # Add an indicator of whether this is a subscription-manager managed
    # repository.
    # shellcheck disable=SC2154
    repoinfo_results[Repo-managed]=$(
	awk '
            BEGIN {FS="[)(]"}
            /^# Managed by \(.*\) subscription-manager$/ {print $2}
        ' < "${repoinfo_results[Repo-filename]}"
    )
}

provides_pkg () (
    if [[ ! $1 ]]; then
	return 0
    fi

    set -o pipefail
    provides=$(dnf -q provides "$1" | awk '{print $1; nextfile}') ||
	return 1
    set +o pipefail
    pkg=$(rpm -q --queryformat '%{NAME}\n' "$provides") ||
    	pkg=$(dnf -q repoquery --queryformat '%{NAME}\n' "$provides") ||
    	exit_message "Can't get package name for $provides."
    printf '%s\n' "$pkg"
)

# If you pass an empty arg as one of the package specs to rpm it will match
# every package on the system.  This funtion simply strips out any empty args
# and passes the rest to rpm to avoid this side-effect.
saferpm () (
    args=()
    for a in "$@"; do
	if [[ $a ]]; then
	    args+=("$a")
	fi
    done
    rpm "${args[@]}"
)

# And a similar function for dnf
safednf () (
    args=()
    for a in "$@"; do
	if [[ $a ]]; then
	    args+=("$a")
	fi
    done
    dnf "${args[@]}"
)

collect_system_info () {
    # Dump the DNF cache first so we start with a clean slate.
    infomsg $'\nRemoving dnf cache\n'
    rm -rf /var/cache/{yum,dnf}
    # Check the efi mount first, so we can bail before wasting time on all these
    # other checks if it's not there.
    if [[ $update_efi ]]; then
	local efi_mount kname
	declare -g -a efi_disk efi_partition
	efi_mount=$(findmnt --mountpoint /boot/efi --output SOURCE \
	    --noheadings) ||
	    exit_message "Can't find EFI mount.  No EFI  boot detected."
	kname=$(lsblk -dno kname "$efi_mount")
	efi_disk=$(lsblk -dno pkname "/dev/$kname")

	if [[ $efi_disk ]]; then
	    efi_partition=$(<"/sys/block/$efi_disk/$kname/partition")
	else
	    # This is likely an md-raid or other type of virtual disk, we need
	    # to dig a little deeper to find the actual physical disks and
	    # partitions.
	    kname=$(lsblk -dno kname "$efi_mount")
	    cd "/sys/block/$kname/slaves" || exit_message \
"Unable to gather EFI data: Can't cd to /sys/block/$kname/slaves."
	    if ! (shopt -s failglob; : *) 2>/dev/null; then
		exit_message \
"Unable to gather EFI data: No slaves found in /sys/block/$kname/slaves."
	    fi
	    efi_disk=()
	    for d in *; do
		efi_disk+=("$(lsblk -dno pkname "/dev/$d")")
		efi_partition+=("$(<"$d/partition")")
		if [[ ! ${efi_disk[-1]} || ! ${efi_partition[-1]} ]]; then
		    exit_message \
"Unable to gather EFI data: Can't find disk name or partition number for $d."
		fi
	    done
	    cd -
	fi
    fi

    # check if EFI secure boot is enabled
    if [[ $update_efi ]]; then
	if mokutil --sb-state 2>&1 | grep -q "SecureBoot enabled"; then
	    exit_message "EFI Secure Boot is enabled but Rocky Linux doesn't provide a signed shim yet. Disable EFI Secure Boot and reboot."
	fi
    fi

    # Don't enable these module streams, even if they are enabled in the source
    # distro.
    declare -g -a module_excludes
    module_excludes=(
	libselinux-python:2.8
    )

    # Some OracleLinux modules have stream names of ol8 instead of rhel8 and ol
    # instead of rhel.  This is a map that does a glob match and replacement.
    local -A module_glob_map
    module_glob_map=(
	['%:ol8']=:rhel8
	['%:ol']=:rhel
    );

    # We need to map rockylinux repository names to the equivalent repositories
    # in the source distro.  To do that we look for known packages in each
    # repository and see what repo they came from.  We need to use repoquery for
    # this which requires downloading the package, so we pick relatively small
    # packages for this.
    declare -g -A repo_map pkg_repo_map
    declare -g -a managed_repos
    pkg_repo_map=(
	[baseos]=rootfiles.noarch
	[appstream]=apr-util-ldap.$ARCH
	[ha]=pacemaker-doc.noarch
	[powertools]=libaec-devel.$ARCH
	[extras]=epel-release.noarch
    )
#	[devel]=quota-devel.$ARCH

    PRETTY_NAME=$(os-release PRETTY_NAME)
    infomsg '%s' \
	"Preparing to migrate $PRETTY_NAME to Rocky Linux 8."$'\n\n' \
	"Determining repository names for $PRETTY_NAME"

    for r in "${!pkg_repo_map[@]}"; do
	printf '.'
	p=${pkg_repo_map[$r]}
	repoquery "$p" || continue
	repo_map[$r]=${repoquery_results[Repository]}
    done

    printf '%s\n' '' '' "Found the following repositories which map from $PRETTY_NAME to Rocky Linux 8:"
    column -t -s $'\t' -N "$PRETTY_NAME,Rocky Linux 8" < <(for r in "${!repo_map[@]}"; do
	printf '%s\t%s\n' "${repo_map[$r]}" "$r"
	done)

    infomsg $'\n'"Getting system package names for $PRETTY_NAME"

    # We don't know what the names of these packages are, we have to discover
    # them via various means. The most common means is to look for either a
    # distro-agnostic provides or a filename.  In a couple of cases we need to
    # jump through hoops to get a filename that is provided specifically by the
    # source distro.
    # Get info for each repository to determine which ones are subscription
    # managed.
    # system-release here is a bit of a hack, but it ensures that the
    # rocky-repos package will get installed.
    for r in "${!repo_map[@]}"; do
	repoinfo "${repo_map[$r]}"
	if [[ $r == "baseos" ]]; then
	    local baseos_filename=system-release
	    if [[ ! ${repoinfo_results[Repo-managed]} ]]; then
		baseos_filename="${repoinfo_results[Repo-filename]}"
	    fi
	    local baseos_gpgkey="${repoinfo_results[Repo-gpgkey]}"
	fi
	if [[ ${repoinfo_results[Repo-managed]} ]]; then
	    managed_repos+=("${repo_map[$r]}")
	fi
    done

    # First get info for the baseos repo
    repoinfo "${repo_map[baseos]}"
    declare -g -A pkg_map provides_pkg_map
    declare -g -a addl_provide_removes addl_pkg_removes
    provides_pkg_map=(
	[rocky-backgrounds]=system-backgrounds
	[rocky-indexhtml]=redhat-indexhtml
	[rocky-repos]="$baseos_filename"
	[rocky-logos]=system-logos
	[rocky-logos-httpd]=system-logos-httpd
	[rocky-logos-ipa]=system-logos-ipa
	[rocky-gpg-keys]="$baseos_gpgkey"
	[rocky-release]=system-release
    )
    addl_provide_removes=(
	redhat-release
	redhat-release-eula
    )

    # Check to make sure that we don't already have a full or partial
    # RockyLinux install.
    if [[ $(rpm -qa "${!provides_pkg_map[@]}") ]]; then
	exit_message \
$'Found a full or partial RockyLinux install already in place.  Aborting\n'
$'because continuing with the migration could cause further damage to system.'
    fi

    for pkg in "${!provides_pkg_map[@]}"; do
	printf '.'
	prov=${provides_pkg_map[$pkg]}
	pkg_map[$pkg]=$(provides_pkg "$prov") ||
	    exit_message "Can't get package that provides $prov."
    done
    for prov in "${addl_provide_removes[@]}"; do
	printf '.'
	local pkg;
	pkg=$(provides_pkg "$prov") || continue
	addl_pkg_removes+=("$pkg")
    done

    printf '%s\n' '' '' "Found the following system packages which map from $PRETTY_NAME to Rocky Linux 8:"
    column -t -s $'\t' -N "$PRETTY_NAME,Rocky Linux 8" < <(for p in "${!pkg_map[@]}"; do
	printf '%s\t%s\n' "${pkg_map[$p]}" "$p"
	done)

    infomsg $'\n'"Getting list of installed system packages."$'\n'

    readarray -t installed_packages < <(saferpm -qa --queryformat="%{NAME}\n" "${pkg_map[@]}")
    declare -g -A installed_pkg_check installed_pkg_map
    for p in "${installed_packages[@]}"; do
	installed_pkg_check[$p]=1
    done
    for p in "${!pkg_map[@]}"; do
	if [[ ${pkg_map[$p]} && ${installed_pkg_check[${pkg_map[$p]}]} ]]; then
	    installed_pkg_map[$p]=${pkg_map[$p]}
 	fi
    done;

    printf '%s\n' '' "We will replace the following $PRETTY_NAME packages with their Rocky Linux 8 equivalents"
    column -t -s $'\t' -N "Packages to be Removed,Packages to be Installed" < <(
	for p in "${!installed_pkg_map[@]}"; do
	    printf '%s\t%s\n' "${installed_pkg_map[$p]}" "$p"
	done
    )

    if (( ${#addl_pkg_removes[@]} )); then
	printf '%s\n' '' "In addition to the above the following system packages will be removed:" \
	    "${addl_pkg_removes[@]}"
    fi

    # Release packages that are part of SIG's should be listed below when they
    # are available.
    # UPDATE: We may or may not do something with SIG's here, it could just be
    # left as a separate excersize to swap out the sig repos.
    #sigs_to_swap=()

    infomsg '%s' $'\n' \
	$'Getting a list of enabled modules for the system repositories.\n'

    # Get a list of system enabled modules.
    readarray -t enabled_modules < <(
	set -e -o pipefail
	safednf -q "${repo_map[@]/#/--repo=}" module list --enabled |
	awk '
	    $1 == "@modulefailsafe", /^$/ {next}
	    $1 == "Name", /^$/ {if ($1!="Name" && !/^$/) print $1":"$2}
    	' | sort -u
	set +e +o pipefail
    )

    # Map the known module name differences.
    disable_modules=()
    local i gl repl mod
    for i in "${!enabled_modules[@]}"; do
	mod=${enabled_modules[$i]}
	for gl in "${!module_glob_map[@]}"; do
	    repl=${module_glob_map[$gl]}
	    mod=${mod/$gl/$repl}
	done
	if [[ $mod != ${enabled_modules[$i]} ]]; then
	    disable_modules+=(${enabled_modules[$i]})
	    enabled_modules[$i]=$mod
	fi
    done

    # Remove entries matching any excluded modules.
    if (( ${#module_excludes[@]} )); then
	printf '%s\n' '' "Excluding modules:" "${module_excludes[@]}"
	local -A module_check='()'
	local -a tmparr='()'
	for m in "${module_excludes[@]}"; do
	    module_check[$m]=1
	done
	for m in "${enabled_modules[@]}"; do
	    if [[ ! ${module_check[$m]} ]]; then
		tmparr+=("$m")
	    fi
	done
	enabled_modules=("${tmparr[@]}")
    fi

    printf '%s\n' '' "Found the following modules to re-enable at completion:" \
	"${enabled_modules[@]}" ''

    if (( ${#managed_repos[@]} )); then
	printf '%s\n' '' "In addition, since this system uses subscription-manger the following managed repos will be disabled:" \
	    "${managed_repos[@]}"
    fi
}

convert_info_dir=/root/convert
unset convert_to_rocky reinstall_all_rpms verify_all_rpms update_efi

usage() {
  printf '%s\n' \
      "Usage: ${0##*/} [OPTIONS]" \
      '' \
      'Options:' \
      '-h Display this help' \
      '-r Convert to rocky' \
      '-V Verify switch' \
      '   !! USE WITH CAUTION !!'
  exit 1
} >&2

generate_rpm_info() {
  mkdir /root/convert
  infomsg  "Creating a list of RPMs installed: $1"$'\n'
  rpm -qa --qf "%{NAME}|%{VERSION}|%{RELEASE}|%{INSTALLTIME}|%{VENDOR}|%{BUILDTIME}|%{BUILDHOST}|%{SOURCERPM}|%{LICENSE}|%{PACKAGER}\n" | sort > "${convert_info_dir}/$HOSTNAME-rpm-list-$1.log"
  infomsg "Verifying RPMs installed against RPM database: $1"$'\n\n'
  rpm -Va | sort -k3 > "${convert_info_dir}/$HOSTNAME-rpm-list-verified-$1.log"
}

# Run a dnf update before the actual migration.
pre_update() {
    infomsg '%s\n' "Running dnf update before we attempt the migration."
    dnf -y update || exit_message \
$'Error running pre-update.  Stopping now to avoid putting the system in an\n'\
$'unstable state.  Please correct the issues shown here and try again.'
}

package_swaps() {
    # Save off any subscription-manger keys, just in case.
    if ( shopt -s failglob dotglob; : "$sm_ca_dir"/* ) 2>/dev/null ; then
	tmp_sm_ca_dir=$tmp_dir/sm-certs
	mkdir "$tmp_sm_ca_dir" ||
	    exit_message "Could not create directory: $tmp_sm_ca_dir"
	cp -f -dR --preserve=all "$sm_ca_dir"/* "$tmp_sm_ca_dir/" ||
	    exit_message "Could not copy certs to $tmp_sm_ca_dir"
    fi

    # prepare repo parameters
    local -a dnfparameters
    for repo in "${!repo_urls[@]}"; do
	dnfparameters+=( "--repofrompath=${repo},${repo_urls[${repo}]}" )
	dnfparameters+=( "--setopt=${repo}.gpgcheck=1" )
	dnfparameters+=( "--setopt=${repo}.gpgkey=file://${gpg_key_file}" )
    done

    # Use dnf shell to swap the system packages out.
    safednf -y shell --disablerepo=\* --noautoremove \
	--setopt=protected_packages= --setopt=keepcache=True \
	"${dnfparameters[@]}" \
	<<EOF
	remove ${installed_pkg_map[@]} ${addl_pkg_removes[@]}
	install ${!installed_pkg_map[@]}
	run
	exit
EOF

    # rocky-repos and rocky-gpg-keys are now installed, so we don't need the key file anymore
    rm -rf "$gpg_tmp_dir"

    # We need to check to make sure that all of the original system packages
    # have been removed and all of the new ones have been added. If a package
    # was supposed to be removed and one with the same name added back then
    # we're kind of screwed for this check, as we can't be certain, but all the
    # packages we're adding start with "rocky-*" so this really shouldn't happen
    # and we can safely not check for it.  The worst that will happen is a rocky
    # linux package will be removed and then installed again.
    local -a check_removed check_installed
    readarray -t check_removed < <(
	saferpm -qa --qf '%{NAME}\n' "${installed_pkg_map[@]}" \
	    "${addl_pkg_removes[@]}" | sort -u
    )

    if (( ${#check_removed[@]} )); then
	infomsg '%s' $'\n' \
	    "Packages found on system that should still be removed.  Forcibly" \
	    " removing them with rpm:"$'\n'
	# Removed packages still found on the system.  Forcibly remove them.
	for pkg in "${check_removed[@]}"; do
	    # Extra safety measure, skip if empty string
	    if [[ -z $pkg ]]; then
		continue
	    fi
	    printf '%s\n' "$pkg"
	    saferpm -e --allmatches --nodeps "$pkg" ||
	    saferpm -e --allmatches --nodeps --noscripts --notriggers "$pkg"
	done
    fi

    # Check to make sure we installed everything we were supposed to.
    readarray -t check_installed < <(
	{
	    printf '%s\n' "${!installed_pkg_map[@]}" | sort -u
	    saferpm -qa --qf '%{NAME}\n' "${!installed_pkg_map[@]}" | sort -u
	} | sort | uniq -u
    )
    if (( ${#check_installed[@]} )); then
	infomsg '%s' $'\n' \
	    "Some required packages were not installed by dnf.  Attempting to" \
	    " force with rpm:"$'\n'

	# Get a list of rpm packages to package names
	local -A rpm_map
	local -a file_list
	for rpm in /var/cache/dnf/{rockybaseos,rockyappstream}-*/packages/*.rpm
	do
	    rpm_map[$(
		    rpm -q --qf '%{NAME}\n' --nodigest "$rpm" 2>/dev/null
		    )]=$rpm
	done

	# Attempt to install.
	for pkg in "${check_installed[@]}"; do
	    printf '%s\n' "$pkg"
	    if ! rpm -i --force --nodeps --nodigest "${rpm_map[$pkg]}" \
		2>/dev/null; then
		# Try to install the package in just the db, then clean it up.
		rpm -i --force --justdb --nodeps --nodigest "${rpm_map[$pkg]}" \
		    2>/dev/null

		# Get list of files that are still causing problems and donk
		# them.
		readarray -t file_list < <(
		    rpm -V "$pkg" 2>/dev/null | awk '$1!="missing" {print $2}'
		)
		for file in "${file_list[@]}"; do
		    rmdir "$file" ||
		    rm -f "$file" ||
		    rm -rf "$file"
		done

		# Now try re-installing the package to replace the missing
		# files.  Regardless of the outcome here we just accept it and
		# move on and hope for the best.
		rpm -i --reinstall --force --nodeps --nodigest \
		    "${rpm_map[$pkg]}" 2>/dev/null
	    fi
	done
    fi

    # Distrosync
    infomsg $'Ensuring repos are enabled before the package swap\n'
    safednf -y --enableplugin=config-manager config-manager \
	--set-enabled "${!repo_map[@]}" || {
	printf '%s\n' 'Repo name missing?'
	exit 25
    }

    if (( ${#managed_repos[@]} )); then
	# Filter the managed repos for ones still in the system.
	readarray -t managed_repos < <(
	    safednf -q repolist "${managed_repos[@]}" | awk '$1!="repo" {print $1}'
	)

	if (( ${#managed_repos[@]} )); then
	    infomsg $'\nDisabling subscription managed repos\n'
	    safednf -y --enableplugin=config-manager config-manager \
		--disable "${managed_repos[@]}"
	fi
    fi

    if (( ${#disable_modules[@]} )); then
	infomsg $'Disabling modules\n\n'
	safednf -y module disable "${disable_modules[@]}" ||
	    exit_message "Can't disable modules ${disable_modules[*]}"
    fi

    if (( ${#enabled_modules[@]} )); then
	infomsg $'Enabling modules\n\n'
	safednf -y module enable "${enabled_modules[@]}" ||
    	    exit_message "Can't enable modules ${enabled_modules[*]}"
    fi

    # Make sure that excluded repos are disabled.
    infomsg $'Disabling excluded modules\n\n'
    safednf -y module disable "${module_excludes[@]}" ||
    	exit_message "Can't disable modules ${module_excludes[*]}"

    infomsg $'\nSyncing packages\n\n'
    dnf -y distro-sync || exit_message "Error during distro-sync."

    if rpm --quiet -q subscription-manager; then
	infomsg $'Subscription Manager found on system.\n'
	cat <<EOF
If you're converting from a subscription-managed distribution such as RHEL then
you may no longer need subscription-manager or dnf-plugin-subscription-manager.
While it won't hurt anything to have it on your system you may be able to safely
remove it with:

"dnf remove subscription-manager dnf-plugin-subscription-manager".

Take care that it doesn't remove something that you want to keep.

The subscription-manager dnf plugin may be enabled for the benefit of
Subscription Management. If no longer desired, you can use
"subscription-manager config --rhsm.auto_enable_yum_plugins=0" to block this
behavior.

EOF
    fi
    if [[ $tmp_sm_ca_dir ]]; then
	# Check to see if there's Subscription Manager certs which have been
	# removed
	local -a removed_certs
	readarray -t removed_certs < <((
	    shopt -s nullglob dotglob
	    local -a certs
	    cd "$sm_ca_dir" && certs=(*)
	    cd "$tmp_sm_ca_dir" && certs+=(*)
	    IFS=$'\n'
	    printf '%s' "${certs[*]}"
	) | sort | uniq -u)

	if (( ${#removed_certs[@]} )); then
	    cp -n -dR --preserve=all "$tmp_sm_ca_dir"/* "$sm_ca_dir/" ||
		exit_message "Could not copy certs back to $sm_ca_dir"
	    
	    infomsg '%s' \
		$'Some Subscription Manager certificates ' \
		"were restored to $sm_ca_dir after"$'\n' \
		$'migration so that the subscription-manager ' \
		$'command will continue to work:\n\n'
	    printf '%s\n' "${removed_certs[@]}" ''
	    cat <<EOF
If you no longer need to use the subscription-manager command then you may
safely remove these files.
EOF
	fi
    fi
}

# Check if this system is running on EFI
# If yes, we'll need to run fix_efi() at the end of the conversion
efi_check () {
    # Check if we have /sys mounted and it is looking sane
    if ! [[ -d /sys/class/block ]]; then
	exit_message "/sys is not accessible."
    fi
    
    # Now that we know /sys is reliable, use it to check if we are running on EFI or not
    if [[ -d /sys/firmware/efi/ ]]; then
	declare -g update_efi
	update_efi=true
    fi
}

# Called to update the EFI boot.
fix_efi () (
    grub2-mkconfig -o /boot/efi/EFI/rocky/grub.cfg ||
    	exit_message "Error updating the grub config."
    for i in "${!efi_disk[@]}"; do
	efibootmgr -c -d "/dev/${efi_disk[$i]}" -p "${efi_partition[$i]}" \
	    -L "Rocky Linux" -l /EFI/rocky/grubx64.efi ||
	    exit_message "Error updating uEFI firmware."
    done
)

# Download and verify the Rocky Linux package signing key
establish_gpg_trust () {
    # create temp dir and verify it is really created and empty, so we are sure deleting it afterwards won't cause any harm
    declare -g gpg_tmp_dir
    gpg_tmp_dir=$tmp_dir/gpg
    if ! mkdir "$gpg_tmp_dir" || [[ ! -d "$gpg_tmp_dir" ]]; then
	exit_message "Error creating temp dir"
    fi
    # failglob makes pathname expansion fail if empty, dotglob adds files starting with . to pathname expansion
    if ( shopt -s failglob dotglob; : "$gpg_tmp_dir"/* ) 2>/dev/null ; then
	exit_message "Temp dir not empty"
    fi

    # extract the filename from the url, use the temp dir just created
    declare -g gpg_key_file="$gpg_tmp_dir/${gpg_key_url##*/}"

    if ! curl -L -o "$gpg_key_file" --silent --show-error "$gpg_key_url"; then
	rm -rf "$gpg_tmp_dir"
	exit_message "Error downloading the Rocky Linux signing key."
    fi

    if ! sha512sum --quiet -c <<<"$gpg_key_sha512 $gpg_key_file"; then
	rm -rf "$gpg_tmp_dir"
	exit_message "Error validating the signing key."
    fi
}

## End actual work

noopts=0
while getopts "hrVR" option; do
  (( noopts++ ))
  case "$option" in
    h)
      usage
      ;;
    r)
      convert_to_rocky=true
      ;;
    V)
      verify_all_rpms=true
      ;;
    *)
      errmsg $'Invalid switch\n'
      usage
      ;;
  esac
done
if (( ! noopts )); then
    usage
fi

pre_setup
trap exit_clean EXIT
pre_check
efi_check
bin_check

if [[ $verify_all_rpms ]]; then
  generate_rpm_info begin
fi

if [[ $convert_to_rocky ]]; then
    collect_system_info
    establish_gpg_trust
    pre_update
    package_swaps
fi

if [[ $verify_all_rpms && $convert_to_rocky ]]; then
  generate_rpm_info finish
  infomsg $'You may review the following files:\n'
  find /root/convert -type f -name "$HOSTNAME-rpms-*.log"
fi

if [[ $update_efi && $convert_to_rocky ]]; then
    fix_efi
fi

printf '\n\n\n'
if [[ $convert_to_rocky ]]; then
    infomsg $'\nDone, please reboot your system.\n'
fi
logmessage

  好啦,CentOS 乔迁 AlmaLinux 和 Rocky Linux 的教程就到此为止啦,感谢您的阅读与支持!!