MySQL數(shù)據(jù)庫二進制版安裝

MySQL 數(shù)據(jù)庫二進制版本安裝操作步驟:

1. 首先先上傳數(shù)據(jù)庫安裝包到linux操作系統(tǒng)

2. 添加一塊新的硬盤, 用來存儲數(shù)據(jù)庫數(shù)據(jù)信息

3. 格式化硬盤

  mkfs.xfs /dev/sdb 

4. 創(chuàng)建數(shù)據(jù)庫存放數(shù)據(jù)的路徑

mkdir -p /data

5. 掛載并檢查

mount  /dev/sdb   /data
df -h  

6. 添加磁盤為永久掛載

vim /etc/fstab 
/dev/sdb      /data/      xfs   defaults  0 0 
umount -lf  /data
mount -a  

7. 加壓上傳的數(shù)據(jù)庫安裝包

[root@shell~]#cd /usr/local/
[root@shell/usr/local]#tar xf mysql-5.7.26-linux-glibc2.12-x86_64.tar.gz 

8. 解壓完成, 創(chuàng)建連接文件 或者使用mv命令修改文件名稱

[root@shell/usr/local]#ln -s mysql-5.7.26-linux-glibc2.12-x86_64 mysql

9. 創(chuàng)建數(shù)據(jù)庫數(shù)據(jù)存放目錄

mkdir  -p   /data/mysql/data

10 創(chuàng)建MySQL數(shù)據(jù)庫虛擬用戶用來管理MySQL數(shù)據(jù)庫

 useradd -M -s /sbin/nologin mysql

11. 進行授權(quán)路徑

[root@shell/usr/local]#chown -R mysql.mysql /usr/local/mysql /data/mysql/data/

12. 定義軟連接 讓系統(tǒng)可以直接調(diào)用數(shù)據(jù)庫命令

ln -s /usr/local/mysql/bin/*   /usr/local/bin

13 . 編寫系統(tǒng)環(huán)境變量配置文件, 讓系統(tǒng)可以直接調(diào)用數(shù)據(jù)庫命令 并刷新讓其生效

vim /etc/profile
PATH=/usr/local/mysql/bin:$PATH
source /etc/profile

14. 復(fù)制mysql數(shù)據(jù)庫的啟動腳本到 /etc/init.d/目錄下 添加執(zhí)行權(quán)限, 添加到開機自啟動

 [root@shell/usr/local]#cp /usr/local/mysql/support-files/mysql.server   [root@shell/usr/local]#/etc/init.d/mysqld
[root@shell/usr/local]# chmod +x /etc/init.d/mysqld 
[root@shell/usr/local]#  chkconfig --add mysqld

15. 編寫mysql 數(shù)據(jù)庫主配置文件

vim /etc/my.cnf
[mysqld]
user=mysql
mysqlx=0
basedir=/usr/local/mysql
datadir=/data/mysql/data
port=3306
server_id=13
socket=/tmp/mysql.sock
[mysql]
socket=/tmp/mysql.sock

16. 初始化數(shù)據(jù)庫

mysqld --initialize-insecure --user=mysql --basedir=/usr/local/mysql --datadir=/data/mysql/data

17. 啟動數(shù)據(jù)庫

systemctl start mysqld

18. 檢查數(shù)據(jù)庫服務(wù)是否啟動成功

ps -ef |grep mysqld  &&  netstat -anpt |grep mysqld

19.登錄數(shù)據(jù)庫測試

mysql 直接登錄

腳本化

#!/bin/bash

# MySQL生產(chǎn)環(huán)境安裝腳本
# 版本: 1.0
# 作者: 系統(tǒng)管理員
# 描述: 用于在生產(chǎn)環(huán)境安裝MySQL 9.4.0

set -euo pipefail  # 嚴格模式: 遇到錯誤退出,使用未定義變量退出,管道錯誤退出

# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 日志函數(shù)
log() {
    echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
}

warn() {
    echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $*${NC}"
}

error() {
    echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}" >&2
    exit 1
}

# 檢查是否以root用戶運行
check_root() {
    if [[ $EUID -ne 0 ]]; then
        error "此腳本必須以root用戶運行"
    fi
}

# 定義變量
MYSQL_TAR="mysql-9.4.0-linux-glibc2.28-x86_64.tar.xz"
MYSQL_DIR="/usr/local/mysql"
DATA_DIR="/data/mysql/data"
MYSQL_USER="mysql"
MYSQL_GROUP="mysql"
SERVICE_FILE="/etc/init.d/mysqld"
CONFIG_FILE="/etc/my.cnf"

# 檢查文件是否存在
check_files() {
    log "檢查必要的文件..."
    
    if [[ ! -f "$MYSQL_TAR" ]]; then
        error "MySQL安裝包 $MYSQL_TAR 不存在"
    fi
    
    if [[ ! -d "/usr/local" ]]; then
        error "/usr/local 目錄不存在"
    fi
}

# 備份原有配置
backup_existing() {
    log "備份現(xiàn)有MySQL配置..."
    
    # 備份my.cnf
    if [[ -f "$CONFIG_FILE" ]]; then
        cp -p "$CONFIG_FILE" "${CONFIG_FILE}.bak.$(date +%Y%m%d%H%M%S)"
        log "已備份 $CONFIG_FILE"
    fi
    
    # 備份服務(wù)腳本
    if [[ -f "$SERVICE_FILE" ]]; then
        cp -p "$SERVICE_FILE" "${SERVICE_FILE}.bak.$(date +%Y%m%d%H%M%S)"
        log "已備份 $SERVICE_FILE"
    fi
}

# 創(chuàng)建MySQL用戶和組
create_mysql_user() {
    log "創(chuàng)建MySQL用戶和組..."
    
    if ! id "$MYSQL_USER" &>/dev/null; then
        groupadd -r "$MYSQL_GROUP" || warn "用戶組 $MYSQL_GROUP 可能已存在"
        useradd -r -g "$MYSQL_GROUP" -s /bin/false "$MYSQL_USER" || warn "用戶 $MYSQL_USER 可能已存在"
        log "MySQL用戶和組創(chuàng)建完成"
    else
        log "MySQL用戶已存在"
    fi
}

# 創(chuàng)建數(shù)據(jù)目錄
create_data_dir() {
    log "創(chuàng)建數(shù)據(jù)目錄..."
    
    if [[ ! -d "$DATA_DIR" ]]; then
        mkdir -p "$DATA_DIR"
        log "數(shù)據(jù)目錄 $DATA_DIR 創(chuàng)建完成"
    else
        warn "數(shù)據(jù)目錄 $DATA_DIR 已存在"
    fi
}

# 安裝MySQL
install_mysql() {
    log "開始安裝MySQL..."
    
    # 解壓安裝包
    log "解壓MySQL安裝包..."
    if ! tar xf "$MYSQL_TAR" -C /usr/local/; then
        error "解壓MySQL安裝包失敗"
    fi
    
    # 重命名目錄
    log "重命名MySQL目錄..."
    local extracted_dir="/usr/local/mysql-9.4.0-linux-glibc2.28-x86_64"
    if [[ -d "$extracted_dir" ]]; then
        if [[ -d "$MYSQL_DIR" ]]; then
            mv "$MYSQL_DIR" "${MYSQL_DIR}.bak.$(date +%Y%m%d%H%M%S)"
            warn "原有MySQL目錄已備份"
        fi
        mv "$extracted_dir" "$MYSQL_DIR"
    else
        error "解壓后的目錄不存在: $extracted_dir"
    fi
    
    # 設(shè)置權(quán)限
    log "設(shè)置目錄權(quán)限..."
    chown -R "${MYSQL_USER}.${MYSQL_GROUP}" "$MYSQL_DIR" "/data"
    
    # 創(chuàng)建符號鏈接
    log "創(chuàng)建二進制文件符號鏈接..."
    for bin_file in "$MYSQL_DIR"/bin/*; do
        if [[ -x "$bin_file" ]]; then
            ln -sf "$bin_file" "/usr/local/bin/"
        fi
    done
}

# 配置MySQL服務(wù)
configure_service() {
    log "配置MySQL服務(wù)..."
    
    # 復(fù)制服務(wù)腳本
    if [[ -f "$MYSQL_DIR/support-files/mysql.server" ]]; then
        cp -a "$MYSQL_DIR/support-files/mysql.server" "$SERVICE_FILE"
        chmod +x "$SERVICE_FILE"
        log "服務(wù)腳本已復(fù)制"
    else
        warn "MySQL服務(wù)腳本不存在,跳過復(fù)制"
    fi
    
    # 配置chkconfig
    if command -v chkconfig &>/dev/null; then
        chkconfig --add mysqld
        log "MySQL服務(wù)已添加到chkconfig"
    else
        warn "chkconfig命令不存在,跳過服務(wù)配置"
    fi
}

# 創(chuàng)建配置文件
create_config() {
    log "創(chuàng)建MySQL配置文件..."
    
    cat > "$CONFIG_FILE" << EOF
[mysqld]
basedir=$MYSQL_DIR
datadir=$DATA_DIR
port=3306
socket=/tmp/mysql.sock
user=$MYSQL_USER
log-error=$DATA_DIR/mysql.error.log
pid-file=$DATA_DIR/mysql.pid

# 性能調(diào)優(yōu)參數(shù) (根據(jù)實際情況調(diào)整)
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=1000
innodb_buffer_pool_size=1G
innodb_log_file_size=256M

[mysql]
socket=/tmp/mysql.sock
default-character-set=utf8mb4

[client]
socket=/tmp/mysql.sock
EOF

    log "MySQL配置文件已創(chuàng)建: $CONFIG_FILE"
}

# 移除沖突的MariaDB包
remove_mariadb() {
    log "檢查并移除沖突的MariaDB包..."
    
    local mariadb_packages
    mariadb_packages=$(rpm -qa | grep -E '^mariadb' || true)
    
    if [[ -n "$mariadb_packages" ]]; then
        log "發(fā)現(xiàn)以下MariaDB包:"
        echo "$mariadb_packages"
        
        for package in $mariadb_packages; do
            warn "移除包: $package"
            rpm -e --nodeps "$package" || warn "移除包 $package 失敗"
        done
    else
        log "未發(fā)現(xiàn)沖突的MariaDB包"
    fi
}

# 初始化MySQL
initialize_mysql() {
    log "初始化MySQL..."
    
    # 檢查數(shù)據(jù)目錄是否為空
    if [[ "$(ls -A $DATA_DIR 2>/dev/null)" ]]; then
        warn "數(shù)據(jù)目錄 $DATA_DIR 不為空,跳過初始化"
        return 0
    fi
    
    log "執(zhí)行MySQL初始化..."
    if ! mysqld --initialize-insecure \
                --user="$MYSQL_USER" \
                --basedir="$MYSQL_DIR" \
                --datadir="$DATA_DIR" 2>&1 | tee "/tmp/mysql_initialize.log"; then
        error "MySQL初始化失敗,請檢查日志: /tmp/mysql_initialize.log"
    fi
    
    log "MySQL初始化完成"
}

# 啟動MySQL服務(wù)
start_mysql() {
    log "啟動MySQL服務(wù)..."
    
    # 嘗試使用systemctl
    if command -v systemctl &>/dev/null; then
        if systemctl start mysqld; then
            log "使用systemctl啟動MySQL成功"
        else
            warn "systemctl啟動失敗,嘗試使用service命令"
            service mysqld start || error "啟動MySQL服務(wù)失敗"
        fi
    elif command -v service &>/dev/null; then
        service mysqld start || error "啟動MySQL服務(wù)失敗"
        log "使用service啟動MySQL成功"
    else
        warn "無法找到systemctl或service命令,請手動啟動MySQL"
        return 1
    fi
    
    # 檢查服務(wù)狀態(tài)
    sleep 3
    if pgrep mysqld >/dev/null; then
        log "MySQL進程運行正常"
    else
        error "MySQL進程未運行"
    fi
    
    # 檢查端口監(jiān)聽
    if netstat -anpt 2>/dev/null | grep -q ':3306.*LISTEN'; then
        log "MySQL正在監(jiān)聽3306端口"
    else
        warn "MySQL未在3306端口監(jiān)聽,請檢查日志"
    fi
}

# 安全設(shè)置
security_setup() {
    log "執(zhí)行MySQL安全設(shè)置..."
    
    # 等待MySQL完全啟動
    sleep 5
    
    # 設(shè)置root密碼(生產(chǎn)環(huán)境應(yīng)該從安全的地方獲取密碼)
    local mysql_root_password
    mysql_root_password=$(openssl rand -base64 16)
    
    log "生成的MySQL root密碼: $mysql_root_password"
    echo "MySQL root密碼: $mysql_root_password" > /root/mysql_root_password.txt
    chmod 600 /root/mysql_root_password.txt
    
    # 使用mysql_secure_installation的簡化版本
    mysql -u root --skip-password << EOF
ALTER USER 'root'@'localhost' IDENTIFIED BY '${mysql_root_password}';
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
EOF

    log "MySQL安全設(shè)置完成,root密碼已保存到 /root/mysql_root_password.txt"
}

# 主函數(shù)
main() {
    log "開始MySQL生產(chǎn)環(huán)境安裝..."
    
    check_root
    check_files
    backup_existing
    create_mysql_user
    create_data_dir
    remove_mariadb
    install_mysql
    configure_service
    create_config
    initialize_mysql
    start_mysql
    security_setup
    
    log "MySQL安裝完成!"
    log "請立即修改root密碼并執(zhí)行以下命令驗證安裝:"
    echo "  mysql -u root -p"
    echo "  systemctl status mysqld"
    echo "  netstat -anpt | grep 3306"
}

# 腳本執(zhí)行入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi
#!/usr/bin/env python3
"""
MySQL生產(chǎn)環(huán)境安裝腳本
版本: 1.0
描述: 用于在生產(chǎn)環(huán)境安裝MySQL 9.4.0
"""

import os
import sys
import subprocess
import shutil
import time
import logging
import tarfile
import getpass
from pathlib import Path
from datetime import datetime
import secrets
import string

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='\033[92m[%(asctime)s] %(message)s\033[0m',
    datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)

# 顏色代碼
class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    NC = '\033[0m'  # No Color

# 自定義日志級別
def log_info(message):
    logger.info(message)

def log_warning(message):
    print(f"{Colors.YELLOW}[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] WARNING: {message}{Colors.NC}")

def log_error(message, exit_script=True):
    print(f"{Colors.RED}[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ERROR: {message}{Colors.NC}")
    if exit_script:
        sys.exit(1)

def run_command(cmd, check=True, capture_output=False):
    """執(zhí)行shell命令"""
    try:
        log_info(f"執(zhí)行命令: {cmd}")
        if capture_output:
            result = subprocess.run(cmd, shell=True, check=check, 
                                  capture_output=True, text=True)
            return result
        else:
            result = subprocess.run(cmd, shell=True, check=check)
            return result
    except subprocess.CalledProcessError as e:
        if check:
            log_error(f"命令執(zhí)行失敗: {cmd}\n錯誤: {e}")
        else:
            log_warning(f"命令執(zhí)行失敗: {cmd}\n錯誤: {e}")
            return None
    except Exception as e:
        if check:
            log_error(f"執(zhí)行命令時發(fā)生異常: {cmd}\n異常: {e}")
        else:
            log_warning(f"執(zhí)行命令時發(fā)生異常: {cmd}\n異常: {e}")
            return None

class MySQLInstaller:
    def __init__(self):
        self.mysql_tar = "mysql-9.4.0-linux-glibc2.28-x86_64.tar.xz"
        self.mysql_dir = Path("/usr/local/mysql")
        self.data_dir = Path("/data/mysql/data")
        self.mysql_user = "mysql"
        self.mysql_group = "mysql"
        self.service_file = Path("/etc/init.d/mysqld")
        self.config_file = Path("/etc/my.cnf")
        
    def check_root(self):
        """檢查是否以root用戶運行"""
        if os.geteuid() != 0:
            log_error("此腳本必須以root用戶運行")
    
    def check_files(self):
        """檢查必要的文件"""
        log_info("檢查必要的文件...")
        
        if not os.path.isfile(self.mysql_tar):
            log_error(f"MySQL安裝包 {self.mysql_tar} 不存在")
        
        if not os.path.isdir("/usr/local"):
            log_error("/usr/local 目錄不存在")
        
        log_info("文件檢查完成")
    
    def backup_existing(self):
        """備份原有配置"""
        log_info("備份現(xiàn)有MySQL配置...")
        
        timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
        
        # 備份my.cnf
        if self.config_file.exists():
            backup_file = f"{self.config_file}.bak.{timestamp}"
            shutil.copy2(self.config_file, backup_file)
            log_info(f"已備份 {self.config_file} 到 {backup_file}")
        
        # 備份服務(wù)腳本
        if self.service_file.exists():
            backup_file = f"{self.service_file}.bak.{timestamp}"
            shutil.copy2(self.service_file, backup_file)
            log_info(f"已備份 {self.service_file} 到 {backup_file}")
        
        # 備份原有MySQL目錄
        if self.mysql_dir.exists():
            backup_dir = f"{self.mysql_dir}.bak.{timestamp}"
            shutil.move(self.mysql_dir, backup_dir)
            log_info(f"已備份原有MySQL目錄到 {backup_dir}")
    
    def create_mysql_user(self):
        """創(chuàng)建MySQL用戶和組"""
        log_info("創(chuàng)建MySQL用戶和組...")
        
        try:
            # 檢查用戶組是否存在
            run_command(f"getent group {self.mysql_group}", check=False)
            run_command(f"groupadd -r {self.mysql_group}", check=False)
            log_info(f"用戶組 {self.mysql_group} 創(chuàng)建完成")
        except Exception as e:
            log_warning(f"創(chuàng)建用戶組時發(fā)生異常: {e}")
        
        try:
            # 檢查用戶是否存在
            run_command(f"id {self.mysql_user}", check=False)
            run_command(f"useradd -r -g {self.mysql_group} -s /bin/false {self.mysql_user}", check=False)
            log_info(f"用戶 {self.mysql_user} 創(chuàng)建完成")
        except Exception as e:
            log_warning(f"創(chuàng)建用戶時發(fā)生異常: {e}")
    
    def create_data_dir(self):
        """創(chuàng)建數(shù)據(jù)目錄"""
        log_info("創(chuàng)建數(shù)據(jù)目錄...")
        
        try:
            self.data_dir.mkdir(parents=True, exist_ok=True)
            log_info(f"數(shù)據(jù)目錄 {self.data_dir} 創(chuàng)建完成")
        except Exception as e:
            log_error(f"創(chuàng)建數(shù)據(jù)目錄失敗: {e}")
    
    def install_mysql(self):
        """安裝MySQL"""
        log_info("開始安裝MySQL...")
        
        # 解壓安裝包
        log_info("解壓MySQL安裝包...")
        try:
            with tarfile.open(self.mysql_tar, 'r:xz') as tar:
                tar.extractall(path='/usr/local/')
            log_info("MySQL安裝包解壓完成")
        except Exception as e:
            log_error(f"解壓MySQL安裝包失敗: {e}")
        
        # 重命名目錄
        log_info("重命名MySQL目錄...")
        extracted_dir = Path("/usr/local/mysql-9.4.0-linux-glibc2.28-x86_64")
        
        if not extracted_dir.exists():
            log_error(f"解壓后的目錄不存在: {extracted_dir}")
        
        if self.mysql_dir.exists():
            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
            backup_dir = Path(f"{self.mysql_dir}.bak.{timestamp}")
            shutil.move(self.mysql_dir, backup_dir)
            log_warning(f"原有MySQL目錄已備份到 {backup_dir}")
        
        shutil.move(str(extracted_dir), str(self.mysql_dir))
        log_info("MySQL目錄重命名完成")
        
        # 設(shè)置權(quán)限
        log_info("設(shè)置目錄權(quán)限...")
        run_command(f"chown -R {self.mysql_user}.{self.mysql_group} {self.mysql_dir}")
        run_command(f"chown -R {self.mysql_user}.{self.mysql_group} /data")
        
        # 創(chuàng)建符號鏈接
        log_info("創(chuàng)建二進制文件符號鏈接...")
        bin_dir = self.mysql_dir / "bin"
        if bin_dir.exists():
            for bin_file in bin_dir.iterdir():
                if bin_file.is_file() and os.access(bin_file, os.X_OK):
                    target_link = Path("/usr/local/bin") / bin_file.name
                    if target_link.exists() or target_link.is_symlink():
                        target_link.unlink()
                    target_link.symlink_to(bin_file)
            log_info("二進制文件符號鏈接創(chuàng)建完成")
        else:
            log_warning(f"MySQL bin目錄不存在: {bin_dir}")
    
    def configure_service(self):
        """配置MySQL服務(wù)"""
        log_info("配置MySQL服務(wù)...")
        
        # 復(fù)制服務(wù)腳本
        service_source = self.mysql_dir / "support-files" / "mysql.server"
        if service_source.exists():
            shutil.copy2(service_source, self.service_file)
            self.service_file.chmod(0o755)
            log_info("服務(wù)腳本已復(fù)制并設(shè)置權(quán)限")
        else:
            log_warning(f"MySQL服務(wù)腳本不存在: {service_source}")
        
        # 配置chkconfig
        if shutil.which("chkconfig"):
            run_command("chkconfig --add mysqld")
            log_info("MySQL服務(wù)已添加到chkconfig")
        else:
            log_warning("chkconfig命令不存在,跳過服務(wù)配置")
    
    def create_config(self):
        """創(chuàng)建MySQL配置文件"""
        log_info("創(chuàng)建MySQL配置文件...")
        
        config_content = f"""
[mysqld]
basedir={self.mysql_dir}
datadir={self.data_dir}
port=3306
socket=/tmp/mysql.sock
user={self.mysql_user}
log-error={self.data_dir}/mysql.error.log
pid-file={self.data_dir}/mysql.pid

# 性能調(diào)優(yōu)參數(shù) (根據(jù)實際情況調(diào)整)
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=1000
innodb_buffer_pool_size=1G
innodb_log_file_size=256M

[mysql]
socket=/tmp/mysql.sock
default-character-set=utf8mb4

[client]
socket=/tmp/mysql.sock
"""
        
        try:
            with open(self.config_file, 'w') as f:
                f.write(config_content)
            log_info(f"MySQL配置文件已創(chuàng)建: {self.config_file}")
        except Exception as e:
            log_error(f"創(chuàng)建配置文件失敗: {e}")
    
    def remove_mariadb(self):
        """移除沖突的MariaDB包"""
        log_info("檢查并移除沖突的MariaDB包...")
        
        try:
            result = run_command("rpm -qa | grep -E '^mariadb'", check=False, capture_output=True)
            if result and result.returncode == 0 and result.stdout.strip():
                mariadb_packages = result.stdout.strip().split('\n')
                log_info("發(fā)現(xiàn)以下MariaDB包:")
                for package in mariadb_packages:
                    print(f"  - {package}")
                
                for package in mariadb_packages:
                    log_warning(f"移除包: {package}")
                    run_command(f"rpm -e --nodeps {package}", check=False)
            else:
                log_info("未發(fā)現(xiàn)沖突的MariaDB包")
        except Exception as e:
            log_warning(f"檢查MariaDB包時發(fā)生異常: {e}")
    
    def initialize_mysql(self):
        """初始化MySQL"""
        log_info("初始化MySQL...")
        
        # 檢查數(shù)據(jù)目錄是否為空
        try:
            if any(self.data_dir.iterdir()):
                log_warning(f"數(shù)據(jù)目錄 {self.data_dir} 不為空,跳過初始化")
                return
        except Exception as e:
            log_error(f"檢查數(shù)據(jù)目錄失敗: {e}")
        
        log_info("執(zhí)行MySQL初始化...")
        
        init_cmd = (
            f"mysqld --initialize-insecure "
            f"--user={self.mysql_user} "
            f"--basedir={self.mysql_dir} "
            f"--datadir={self.data_dir}"
        )
        
        try:
            with open("/tmp/mysql_initialize.log", "w") as log_file:
                process = subprocess.Popen(
                    init_cmd,
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    universal_newlines=True
                )
                
                # 實時輸出日志
                for line in process.stdout:
                    log_file.write(line)
                    log_file.flush()
                
                process.wait()
                
                if process.returncode != 0:
                    log_error(f"MySQL初始化失敗,請檢查日志: /tmp/mysql_initialize.log")
                else:
                    log_info("MySQL初始化完成")
                    
        except Exception as e:
            log_error(f"初始化過程中發(fā)生異常: {e}")
    
    def generate_password(self, length=16):
        """生成隨機密碼"""
        alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
        return ''.join(secrets.choice(alphabet) for _ in range(length))
    
    def start_mysql(self):
        """啟動MySQL服務(wù)"""
        log_info("啟動MySQL服務(wù)...")
        
        # 嘗試使用systemctl
        if shutil.which("systemctl"):
            if run_command("systemctl start mysqld", check=False):
                log_info("使用systemctl啟動MySQL成功")
            else:
                log_warning("systemctl啟動失敗,嘗試使用service命令")
                if not run_command("service mysqld start", check=False):
                    log_error("啟動MySQL服務(wù)失敗")
        elif shutil.which("service"):
            if run_command("service mysqld start", check=False):
                log_info("使用service啟動MySQL成功")
            else:
                log_error("啟動MySQL服務(wù)失敗")
        else:
            log_warning("無法找到systemctl或service命令,請手動啟動MySQL")
            return False
        
        # 檢查服務(wù)狀態(tài)
        log_info("檢查MySQL服務(wù)狀態(tài)...")
        time.sleep(3)
        
        # 檢查進程
        if run_command("pgrep mysqld", check=False, capture_output=True):
            log_info("MySQL進程運行正常")
        else:
            log_error("MySQL進程未運行")
        
        # 檢查端口監(jiān)聽
        result = run_command("netstat -anpt 2>/dev/null | grep ':3306' | grep LISTEN", 
                           check=False, capture_output=True)
        if result and result.returncode == 0 and result.stdout.strip():
            log_info("MySQL正在監(jiān)聽3306端口")
        else:
            log_warning("MySQL未在3306端口監(jiān)聽,請檢查日志")
        
        return True
    
    def security_setup(self):
        """安全設(shè)置"""
        log_info("執(zhí)行MySQL安全設(shè)置...")
        
        # 等待MySQL完全啟動
        time.sleep(5)
        
        # 生成隨機密碼
        mysql_root_password = self.generate_password()
        
        log_info(f"生成的MySQL root密碼: {mysql_root_password}")
        
        # 保存密碼到文件
        password_file = Path("/root/mysql_root_password.txt")
        try:
            with open(password_file, 'w') as f:
                f.write(f"MySQL root密碼: {mysql_root_password}\n")
                f.write(f"生成時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            password_file.chmod(0o600)
            log_info(f"密碼已保存到 {password_file}")
        except Exception as e:
            log_error(f"保存密碼文件失敗: {e}")
        
        # 執(zhí)行安全設(shè)置
        security_commands = [
            f"ALTER USER 'root'@'localhost' IDENTIFIED BY '{mysql_root_password}';",
            "DELETE FROM mysql.user WHERE User='';",
            "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');",
            "DROP DATABASE IF EXISTS test;",
            "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\\\_%';",
            "FLUSH PRIVILEGES;"
        ]
        
        # 由于初始化時使用了--initialize-insecure,首次連接不需要密碼
        try:
            for cmd in security_commands:
                mysql_cmd = f"mysql -u root --skip-password -e \"{cmd}\""
                run_command(mysql_cmd, check=False)
            
            log_info("MySQL安全設(shè)置完成")
        except Exception as e:
            log_warning(f"執(zhí)行安全設(shè)置時發(fā)生異常: {e}")
    
    def verify_installation(self):
        """驗證安裝"""
        log_info("驗證MySQL安裝...")
        
        checks = [
            ("檢查MySQL進程", "ps -ef | grep mysqld | grep -v grep"),
            ("檢查端口監(jiān)聽", "netstat -anpt | grep 3306"),
            ("檢查服務(wù)狀態(tài)", "systemctl status mysqld 2>/dev/null || service mysqld status 2>/dev/null"),
        ]
        
        for check_name, cmd in checks:
            log_info(check_name)
            result = run_command(cmd, check=False, capture_output=True)
            if result and result.returncode == 0:
                print(f"  {Colors.GREEN}?{Colors.NC} {check_name} - 成功")
            else:
                print(f"  {Colors.RED}?{Colors.NC} {check_name} - 失敗")
    
    def install(self):
        """執(zhí)行安裝流程"""
        log_info("開始MySQL生產(chǎn)環(huán)境安裝...")
        
        steps = [
            ("檢查root權(quán)限", self.check_root),
            ("檢查文件", self.check_files),
            ("備份現(xiàn)有配置", self.backup_existing),
            ("創(chuàng)建MySQL用戶", self.create_mysql_user),
            ("創(chuàng)建數(shù)據(jù)目錄", self.create_data_dir),
            ("移除MariaDB沖突包", self.remove_mariadb),
            ("安裝MySQL", self.install_mysql),
            ("配置服務(wù)", self.configure_service),
            ("創(chuàng)建配置文件", self.create_config),
            ("初始化MySQL", self.initialize_mysql),
            ("啟動MySQL服務(wù)", self.start_mysql),
            ("安全設(shè)置", self.security_setup),
            ("驗證安裝", self.verify_installation),
        ]
        
        for step_name, step_func in steps:
            log_info(f"{Colors.BLUE}=== {step_name} ==={Colors.NC}")
            try:
                step_func()
            except Exception as e:
                log_error(f"步驟 '{step_name}' 執(zhí)行失敗: {e}")
        
        log_info(f"{Colors.GREEN}MySQL安裝完成!{Colors.NC}")
        log_info("請立即修改root密碼并執(zhí)行以下命令驗證安裝:")
        print("  mysql -u root -p")
        print("  systemctl status mysqld")
        print("  netstat -anpt | grep 3306")
        print(f"  初始root密碼保存在: /root/mysql_root_password.txt")

def main():
    """主函數(shù)"""
    installer = MySQLInstaller()
    installer.install()

if __name__ == "__main__":
    main()

批量拉取鏡像

#!/usr/bin/env python
"""
Docker鏡像批量拉取工具
支持并行拉取、錯誤重試、進度顯示等功能
"""

import argparse
import logging
import os
import subprocess
import sys
import multiprocessing
from typing import List, Set, Tuple

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger(__name__)

class DockerPullManager:
    def __init__(self, max_workers: int = None, retry_count: int = 2):
        self.max_workers = max_workers or min(4, multiprocessing.cpu_count())
        self.retry_count = retry_count

    def run_cmd(self, cmd: str) -> Tuple[int, str, str]:
        """執(zhí)行shell命令"""
        try:
            process = subprocess.Popen(
                cmd, shell=True,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
            )
            out, error = process.communicate(timeout=1800)
            return process.returncode, out.strip(), error.strip()
        except subprocess.TimeoutExpired:
            logger.error(f"Command timed out: {cmd}")
            process.kill()
            return 1, "", "Timeout"
        except Exception as e:
            logger.error(f"Command error: {cmd}, {e}")
            return 1, "", str(e)

    def get_image_list(self, file_path: str) -> Set[str]:
        """讀取鏡像列表"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"Image file not found: {file_path}")

        images = set()
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                for line_num, line in enumerate(f, 1):
                    line = line.strip()
                    if line and not line.startswith("#"):
                        images.add(line)
        except IOError as e:
            raise IOError(f"Failed to read {file_path}: {e}")

        return images

    def pull_single_image(self, image: str) -> Tuple[bool, str]:
        """拉取單個鏡像,支持重試"""
        for attempt in range(self.retry_count + 1):
            logger.info(f"Pulling {image} (attempt {attempt + 1})")
            r, out, error = self.run_cmd(f"docker pull {image}")

            if r == 0:
                logger.info(f"? Success: {image}")
                return True, out
            else:
                logger.warning(f"Attempt {attempt + 1} failed for {image}: {error}")

        logger.error(f"? Failed: {image} after {self.retry_count + 1} attempts")
        return False, error

    def batch_pull(self, file_path: str) -> Tuple[int, int]:
        """批量拉取鏡像"""
        try:
            images = self.get_image_list(file_path)
        except Exception as e:
            logger.error(str(e))
            return 0, 0

        if not images:
            logger.warning("No images found to pull")
            return 0, 0

        logger.info(f"Found {len(images)} unique images, starting pull with {self.max_workers} workers...")

        success_count = 0
        with multiprocessing.Pool(processes=self.max_workers) as pool:
            results = [pool.apply_async(self.pull_single_image, (img,)) for img in images]

            for i, (image, result) in enumerate(zip(images, results)):
                try:
                    success, msg = result.get(timeout=1800)
                    if success:
                        success_count += 1
                    logger.info(f"Progress: {i+1}/{len(images)}")
                except multiprocessing.TimeoutError:
                    logger.error(f"Timeout: {image}")

        failed_count = len(images) - success_count
        logger.info(f"Completed: {success_count} successful, {failed_count} failed")
        return success_count, failed_count

def main():
    parser = argparse.ArgumentParser(description='批量拉取Docker鏡像')
    parser.add_argument('--file', '-f', default='images.txt',
                       help='鏡像列表文件路徑 (默認: images.txt)')
    parser.add_argument('--workers', '-w', type=int,
                       help=f'并行工作進程數(shù) (默認: CPU核心數(shù),最大4)')
    parser.add_argument('--retry', '-r', type=int, default=2,
                       help='失敗重試次數(shù) (默認: 2)')
    parser.add_argument('--verbose', '-v', action='store_true',
                       help='詳細輸出')

    args = parser.parse_args()

    if args.verbose:
        logger.setLevel(logging.DEBUG)

    manager = DockerPullManager(max_workers=args.workers, retry_count=args.retry)
    success, failed = manager.batch_pull(args.file)

    sys.exit(1 if failed > 0 else 0)

if __name__ == '__main__':
    main()

加強版本

#!/usr/bin/env python
"""
Docker鏡像批量拉取工具
支持并行拉取、錯誤重試、進度顯示、鏡像驗證等功能
增強版本包含更完善的錯誤處理和進度展示
"""

import argparse  # 導(dǎo)入命令行參數(shù)解析模塊
import logging  # 導(dǎo)入日志記錄模塊
import os  # 導(dǎo)入操作系統(tǒng)功能模塊
import subprocess  # 導(dǎo)入子進程管理模塊
import sys  # 導(dǎo)入系統(tǒng)相關(guān)功能模塊
import time  # 導(dǎo)入時間相關(guān)功能模塊
import multiprocessing  # 導(dǎo)入多進程處理模塊
from typing import List, Set, Tuple, Dict, Optional  # 導(dǎo)入類型提示功能
from datetime import datetime  # 導(dǎo)入日期時間處理模塊

# 配置日志系統(tǒng),設(shè)置格式和輸出目標
logging.basicConfig(
    level=logging.INFO,  # 設(shè)置默認日志級別為INFO
    format='%(asctime)s - %(levelname)s - %(message)s',  # 設(shè)置日志格式:時間-級別-消息
    handlers=[logging.StreamHandler(sys.stdout)]  # 設(shè)置日志輸出到標準輸出
)
logger = logging.getLogger(__name__)  # 獲取當(dāng)前模塊的日志記錄器

class DockerPullManager:
    """Docker鏡像拉取管理器,處理批量鏡像拉取任務(wù)"""
    
    def __init__(self, max_workers: int = None, retry_count: int = 2, timeout: int = 1800):
        """
        初始化Docker拉取管理器
        
        Args:
            max_workers: 最大并行工作進程數(shù),默認為min(4, CPU核心數(shù))
            retry_count: 失敗重試次數(shù),默認為2次
            timeout: 命令執(zhí)行超時時間(秒),默認30分鐘
        """
        # 設(shè)置最大工作進程數(shù),取4和CPU核心數(shù)的較小值作為默認值
        self.max_workers = max_workers or min(4, multiprocessing.cpu_count())
        self.retry_count = retry_count  # 設(shè)置重試次數(shù)
        self.timeout = timeout  # 設(shè)置超時時間
        self.start_time = time.time()  # 記錄開始時間,用于計算總耗時
        self.results: Dict[str, bool] = {}  # 存儲每個鏡像的拉取結(jié)果

    def run_cmd(self, cmd: str) -> Tuple[int, str, str]:
        """
        執(zhí)行shell命令并返回結(jié)果
        
        Args:
            cmd: 要執(zhí)行的shell命令
            
        Returns:
            返回值元組: (返回碼, 標準輸出, 標準錯誤)
        """
        try:
            # 創(chuàng)建子進程執(zhí)行命令,并捕獲標準輸出和標準錯誤
            process = subprocess.Popen(
                cmd, shell=True,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
            )
            # 等待進程完成并獲取輸出,設(shè)置超時時間
            out, error = process.communicate(timeout=self.timeout)
            # 返回進程返回碼和輸出結(jié)果
            return process.returncode, out.strip(), error.strip()
        except subprocess.TimeoutExpired:
            # 處理命令執(zhí)行超時情況
            logger.error(f"命令執(zhí)行超時: {cmd}")
            process.kill()  # 終止超時進程
            return 1, "", "執(zhí)行超時"
        except Exception as e:
            # 處理其他異常情況
            logger.error(f"命令執(zhí)行錯誤: {cmd}, {e}")
            return 1, "", str(e)

    def get_image_list(self, file_path: str) -> Set[str]:
        """
        從文件中讀取Docker鏡像列表
        
        Args:
            file_path: 包含鏡像列表的文件路徑
            
        Returns:
            唯一鏡像名稱集合
            
        Raises:
            FileNotFoundError: 文件不存在時拋出
            IOError: 文件讀取錯誤時拋出
        """
        # 檢查文件是否存在
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"鏡像列表文件未找到: {file_path}")

        images = set()  # 使用集合存儲唯一的鏡像名稱
        try:
            # 打開文件并讀取內(nèi)容
            with open(file_path, 'r', encoding='utf-8') as f:
                for line_num, line in enumerate(f, 1):
                    line = line.strip()  # 去除行首尾空白字符
                    # 忽略空行和注釋行
                    if line and not line.startswith("#"):
                        images.add(line)  # 添加到鏡像集合
        except IOError as e:
            # 處理文件讀取錯誤
            raise IOError(f"讀取文件 {file_path} 失敗: {e}")

        return images

    def verify_image(self, image: str) -> bool:
        """
        驗證鏡像是否成功拉取
        
        Args:
            image: 鏡像名稱
            
        Returns:
            驗證成功返回True,否則返回False
        """
        # 執(zhí)行docker images命令檢查鏡像是否存在
        cmd = f"docker images --format '{{{{.Repository}}}}:{{{{.Tag}}}}' | grep '{image}'"
        r, out, _ = self.run_cmd(cmd)
        return r == 0 and out.strip() != ""

    def pull_single_image(self, image: str) -> Tuple[bool, str]:
        """
        拉取單個Docker鏡像,支持多次重試
        
        Args:
            image: 要拉取的鏡像名稱
            
        Returns:
            元組: (是否成功, 輸出信息)
        """
        # 記錄開始時間
        start_time = time.time()
        
        # 嘗試多次拉取鏡像
        for attempt in range(self.retry_count + 1):
            logger.info(f"正在拉取 {image} (第 {attempt + 1} 次嘗試)")
            # 執(zhí)行docker pull命令
            r, out, error = self.run_cmd(f"docker pull {image}")

            if r == 0:
                # 拉取成功
                elapsed = time.time() - start_time
                logger.info(f"? 成功: {image} (耗時: {elapsed:.2f}秒)")
                # 驗證鏡像是否真的拉取成功
                if self.verify_image(image):
                    return True, out
                else:
                    logger.warning(f"鏡像驗證失敗: {image}")
            else:
                # 拉取失敗,記錄錯誤信息
                logger.warning(f"第 {attempt + 1} 次嘗試失敗: {image}: {error}")
                
            # 如果不是最后一次嘗試,等待一段時間后重試
            if attempt < self.retry_count:
                time.sleep(2)  # 失敗后等待2秒再重試

        # 所有嘗試都失敗
        elapsed = time.time() - start_time
        logger.error(f"? 失敗: {image} 經(jīng)過 {self.retry_count + 1} 次嘗試 (耗時: {elapsed:.2f}秒)")
        return False, error

    def format_time(self, seconds: float) -> str:
        """
        格式化時間,將秒數(shù)轉(zhuǎn)換為更易讀的格式
        
        Args:
            seconds: 秒數(shù)
            
        Returns:
            格式化后的時間字符串
        """
        # 將秒數(shù)轉(zhuǎn)換為更易讀的時間格式
        if seconds < 60:
            return f"{seconds:.1f}秒"
        elif seconds < 3600:
            return f"{seconds/60:.1f}分鐘"
        else:
            return f"{seconds/3600:.1f}小時"

    def display_progress(self, completed: int, total: int, success: int, failed: int) -> None:
        """
        顯示當(dāng)前進度信息
        
        Args:
            completed: 已完成的任務(wù)數(shù)
            total: 總?cè)蝿?wù)數(shù)
            success: 成功任務(wù)數(shù)
            failed: 失敗任務(wù)數(shù)
        """
        # 計算已用時間和預(yù)估剩余時間
        elapsed = time.time() - self.start_time
        progress = completed / total if total > 0 else 0
        
        # 計算預(yù)估剩余時間
        remaining = (elapsed / progress) * (1 - progress) if progress > 0 else 0
        
        # 構(gòu)建進度條
        bar_length = 30
        filled_length = int(bar_length * progress)
        bar = '█' * filled_length + '?' * (bar_length - filled_length)
        
        # 顯示進度信息
        logger.info(
            f"進度: [{bar}] {completed}/{total} ({progress*100:.1f}%) "
            f"成功: {success} 失敗: {failed} "
            f"已用時間: {self.format_time(elapsed)} "
            f"預(yù)計剩余: {self.format_time(remaining)}"
        )

    def batch_pull(self, file_path: str) -> Tuple[int, int]:
        """
        批量拉取Docker鏡像
        
        Args:
            file_path: 包含鏡像列表的文件路徑
            
        Returns:
            元組: (成功數(shù)量, 失敗數(shù)量)
        """
        try:
            # 獲取鏡像列表
            images = self.get_image_list(file_path)
        except Exception as e:
            # 處理獲取鏡像列表時的錯誤
            logger.error(str(e))
            return 0, 0

        # 檢查鏡像列表是否為空
        if not images:
            logger.warning("未找到要拉取的鏡像")
            return 0, 0

        # 記錄開始時間和鏡像數(shù)量
        self.start_time = time.time()
        image_list = list(images)  # 將集合轉(zhuǎn)換為列表,便于索引訪問
        total_images = len(image_list)
        
        logger.info(f"找到 {total_images} 個唯一鏡像,使用 {self.max_workers} 個工作進程開始拉取...")

        # 初始化計數(shù)器
        success_count = 0
        failed_count = 0
        completed_count = 0
        
        # 創(chuàng)建進程池進行并行處理
        with multiprocessing.Pool(processes=self.max_workers) as pool:
            # 異步提交所有拉取任務(wù)
            results = [pool.apply_async(self.pull_single_image, (img,)) for img in image_list]
            
            # 處理每個任務(wù)的結(jié)果
            for i, (image, result) in enumerate(zip(image_list, results)):
                try:
                    # 獲取任務(wù)結(jié)果,設(shè)置超時時間
                    success, msg = result.get(timeout=self.timeout)
                    # 更新計數(shù)器
                    if success:
                        success_count += 1
                    else:
                        failed_count += 1
                    
                    completed_count += 1
                    # 存儲結(jié)果
                    self.results[image] = success
                    
                    # 每完成一個任務(wù)顯示一次進度
                    self.display_progress(completed_count, total_images, success_count, failed_count)
                    
                except multiprocessing.TimeoutError:
                    # 處理任務(wù)超時
                    logger.error(f"超時: {image}")
                    failed_count += 1
                    completed_count += 1
                    self.results[image] = False
                except Exception as e:
                    # 處理其他異常
                    logger.error(f"處理 {image} 時出錯: {e}")
                    failed_count += 1
                    completed_count += 1
                    self.results[image] = False

        # 計算總耗時
        total_time = time.time() - self.start_time
        
        # 顯示最終結(jié)果
        logger.info(f"任務(wù)完成: 成功 {success_count} 個, 失敗 {failed_count} 個, 總耗時: {self.format_time(total_time)}")
        
        # 如果有失敗的鏡像,顯示失敗列表
        if failed_count > 0:
            failed_images = [img for img, success in self.results.items() if not success]
            logger.info(f"失敗鏡像列表: {', '.join(failed_images)}")
            
            # 將失敗的鏡像寫入文件,方便后續(xù)重試
            failed_file = f"failed_images_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
            try:
                with open(failed_file, 'w', encoding='utf-8') as f:
                    for img in failed_images:
                        f.write(f"{img}\n")
                logger.info(f"已將失敗鏡像列表寫入文件: {failed_file}")
            except Exception as e:
                logger.error(f"寫入失敗鏡像列表時出錯: {e}")
        
        return success_count, failed_count

def main():
    """主函數(shù),處理命令行參數(shù)并執(zhí)行批量拉取操作"""
    # 創(chuàng)建命令行參數(shù)解析器
    parser = argparse.ArgumentParser(description='批量拉取Docker鏡像工具')
    
    # 添加命令行參數(shù)
    parser.add_argument('--file', '-f', default='image.txt',
                       help='鏡像列表文件路徑 (默認: image.txt)')
    parser.add_argument('--workers', '-w', type=int,
                       help=f'并行工作進程數(shù) (默認: CPU核心數(shù),最大4)')
    parser.add_argument('--retry', '-r', type=int, default=2,
                       help='失敗重試次數(shù) (默認: 2)')
    parser.add_argument('--timeout', '-t', type=int, default=1800,
                       help='單個鏡像拉取超時時間(秒) (默認: 1800)')
    parser.add_argument('--verbose', '-v', action='store_true',
                       help='顯示詳細日志')
    parser.add_argument('--verify', action='store_true',
                       help='拉取后驗證鏡像是否存在')

    # 解析命令行參數(shù)
    args = parser.parse_args()

    # 如果指定了詳細模式,設(shè)置日志級別為DEBUG
    if args.verbose:
        logger.setLevel(logging.DEBUG)
        logger.debug("已啟用詳細日志模式")

    # 創(chuàng)建Docker拉取管理器實例
    manager = DockerPullManager(
        max_workers=args.workers,  # 設(shè)置工作進程數(shù)
        retry_count=args.retry,    # 設(shè)置重試次數(shù)
        timeout=args.timeout       # 設(shè)置超時時間
    )
    
    # 執(zhí)行批量拉取操作
    success, failed = manager.batch_pull(args.file)
    
    # 根據(jù)失敗數(shù)量設(shè)置退出碼
    sys.exit(1 if failed > 0 else 0)

# 當(dāng)腳本直接運行時執(zhí)行main函數(shù)
if __name__ == '__main__':
    main()

批量拉取鏡像 shell版本

#!/bin/bash

# 批量Docker鏡像拉取工具
# 按批次順序拉取鏡像,每次拉取10個,成功后繼續(xù)下一批

set -euo pipefail

# 默認參數(shù)
BATCH_SIZE=10
RETRY_COUNT=2
TIMEOUT=1800
VERBOSE=false
SCRIPT_NAME=$(basename "$0")

# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 全局變量
TOTAL_SUCCESS=0
TOTAL_FAILED=0
FAILED_IMAGES=()
START_TIME=$(date +%s)

# 日志函數(shù)
log_info() {
    echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO]${NC} $1"
}

log_success() {
    echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS]${NC} $1"
}

log_warning() {
    echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARNING]${NC} $1"
}

log_error() {
    echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR]${NC} $1"
}

log_debug() {
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG]${NC} $1"
    fi
}

# 顯示幫助信息
show_help() {
    cat << EOF
批量Docker鏡像拉取工具

用法: $SCRIPT_NAME [選項] 鏡像列表文件

參數(shù):
    鏡像列表文件          包含Docker鏡像列表的文件路徑

選項:
    -b, --batch-size     每批拉取的鏡像數(shù)量 (默認: 10)
    -r, --retry          失敗重試次數(shù) (默認: 2)
    -t, --timeout        單個鏡像拉取超時時間(秒) (默認: 1800)
    -v, --verbose        顯示詳細日志
    -h, --help           顯示此幫助信息

鏡像列表文件格式:
    每行一個鏡像名稱,支持注釋行(以#開頭)和空行

示例:
    $SCRIPT_NAME images.txt
    $SCRIPT_NAME -b 5 -r 3 images.txt
    $SCRIPT_NAME -v images.txt

EOF
}

# 格式化時間
format_time() {
    local seconds=$1
    if [[ $seconds -lt 60 ]]; then
        echo "${seconds}秒"
    elif [[ $seconds -lt 3600 ]]; then
        echo "$(echo "scale=1; $seconds/60" | bc)分鐘"
    else
        echo "$(echo "scale=1; $seconds/3600" | bc)小時"
    fi
}

# 檢查文件是否存在
check_file_exists() {
    local file_path=$1
    if [[ ! -f "$file_path" ]]; then
        log_error "鏡像列表文件未找到: $file_path"
        exit 1
    fi
}

# 讀取鏡像列表
read_image_list() {
    local file_path=$1
    
    check_file_exists "$file_path"
    
    log_debug "開始讀取文件: $file_path"
    
    # 使用grep和sed過濾非空行和非注釋行,然后輸出去重后的結(jié)果
    grep -v '^[[:space:]]*$' "$file_path" | grep -v '^[[:space:]]*#' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sort -u
    
    log_info "從 $file_path 讀取鏡像列表完成"
}

# 驗證鏡像是否成功拉取
verify_image() {
    local image=$1
    local result
    
    result=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -E "^${image}$" || true)
    
    if [[ -n "$result" ]]; then
        return 0
    else
        return 1
    fi
}

# 拉取單個鏡像
pull_single_image() {
    local image=$1
    local start_time=$(date +%s)
    local attempt=0
    local max_attempts=$((RETRY_COUNT + 1))
    
    while [[ $attempt -lt $max_attempts ]]; do
        ((attempt++))
        log_info "正在拉取 ${image} (第 ${attempt} 次嘗試)"
        
        # 使用timeout命令設(shè)置超時
        local pull_output
        local pull_error
        local pull_exit_code
        
        if pull_output=$(timeout "${TIMEOUT}" docker pull "$image" 2>&1); then
            pull_exit_code=0
            pull_error=""
        else
            pull_exit_code=$?
            pull_error="$pull_output"
            pull_output=""
        fi
        
        if [[ $pull_exit_code -eq 0 ]]; then
            local elapsed=$(($(date +%s) - start_time))
            log_success "成功拉取 ${image} (耗時: ${elapsed}秒)"
            
            # 驗證鏡像是否真的拉取成功
            if verify_image "$image"; then
                return 0
            else
                log_warning "鏡像驗證失敗: $image"
            fi
        else
            log_warning "第 ${attempt} 次嘗試失敗: $image"
            if [[ -n "$pull_error" ]]; then
                log_warning "錯誤信息: $pull_error"
            fi
        fi
        
        # 如果不是最后一次嘗試,等待后重試
        if [[ $attempt -lt $max_attempts ]]; then
            log_info "等待2秒后重試..."
            sleep 2
        fi
    done
    
    # 所有嘗試都失敗
    local elapsed=$(($(date +%s) - start_time))
    log_error "失敗: $image 經(jīng)過 ${RETRY_COUNT} 次嘗試 (耗時: ${elapsed}秒)"
    return 1
}

# 拉取一批鏡像
pull_batch() {
    local -n batch_images_ref=$1
    local batch_num=$2
    local total_batches=$3
    
    local batch_start_time=$(date +%s)
    local success_count=0
    local failed_count=0
    local batch_size=${#batch_images_ref[@]}
    
    echo
    log_info "========================================"
    log_info "開始第 ${batch_num}/${total_batches} 批次拉取 (${batch_size} 個鏡像)"
    log_info "========================================"
    echo
    
    for i in "${!batch_images_ref[@]}"; do
        local image="${batch_images_ref[$i]}"
        local image_num=$((i + 1))
        
        echo
        log_info "[${batch_num}/${total_batches}] 鏡像 ${image_num}/${batch_size}: ${image}"
        
        if pull_single_image "$image"; then
            ((success_count++))
        else
            ((failed_count++))
            FAILED_IMAGES+=("$image")
        fi
    done
    
    local batch_elapsed=$(($(date +%s) - batch_start_time))
    echo
    log_info "第 ${batch_num} 批次完成:"
    log_info "  成功: ${success_count} 個"
    log_info "  失敗: ${failed_count} 個"
    log_info "  耗時: ${batch_elapsed} 秒"
    
    echo "$success_count $failed_count"
}

# 顯示最終結(jié)果
show_final_results() {
    local total_images=$1
    local total_time=$(($(date +%s) - START_TIME))
    
    echo
    log_info "========================================"
    log_info "批量拉取任務(wù)完成!"
    log_info "========================================"
    log_info "總鏡像數(shù): $total_images"
    log_info "成功拉取: $TOTAL_SUCCESS"
    log_info "拉取失敗: $TOTAL_FAILED"
    
    local success_rate
    if [[ $total_images -gt 0 ]]; then
        success_rate=$(echo "scale=1; $TOTAL_SUCCESS * 100 / $total_images" | bc)
    else
        success_rate="0.0"
    fi
    
    log_info "成功率: ${success_rate}%"
    log_info "總耗時: $(format_time $total_time)"
    log_info "結(jié)束時間: $(date '+%Y-%m-%d %H:%M:%S')"
    
    if [[ ${#FAILED_IMAGES[@]} -gt 0 ]]; then
        echo
        log_info "失敗的鏡像列表:"
        for i in "${!FAILED_IMAGES[@]}"; do
            log_info "  $((i + 1)). ${FAILED_IMAGES[$i]}"
        done
    fi
}

# 主函數(shù)
main() {
    local file_path=""
    
    # 解析命令行參數(shù)
    while [[ $# -gt 0 ]]; do
        case $1 in
            -b|--batch-size)
                BATCH_SIZE="$2"
                if ! [[ "$BATCH_SIZE" =~ ^[1-9][0-9]*$ ]]; then
                    log_error "批次大小必須是正整數(shù)"
                    exit 1
                fi
                shift 2
                ;;
            -r|--retry)
                RETRY_COUNT="$2"
                if ! [[ "$RETRY_COUNT" =~ ^[0-9]+$ ]]; then
                    log_error "重試次數(shù)必須是非負整數(shù)"
                    exit 1
                fi
                shift 2
                ;;
            -t|--timeout)
                TIMEOUT="$2"
                if ! [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
                    log_error "超時時間必須是正整數(shù)"
                    exit 1
                fi
                shift 2
                ;;
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -h|--help)
                show_help
                exit 0
                ;;
            *)
                if [[ -z "$file_path" ]]; then
                    file_path="$1"
                else
                    log_error "未知參數(shù): $1"
                    show_help
                    exit 1
                fi
                shift
                ;;
        esac
    done
    
    if [[ -z "$file_path" ]]; then
        log_error "請?zhí)峁╃R像列表文件路徑"
        show_help
        exit 1
    fi
    
    # 檢查依賴
    if ! command -v docker &> /dev/null; then
        log_error "Docker 未安裝或未在PATH中"
        exit 1
    fi
    
    if ! command -v bc &> /dev/null; then
        log_error "bc 計算器未安裝,請安裝 bc 工具"
        exit 1
    fi
    
    # 讀取鏡像列表
    local images=()
    
    # 直接從文件讀取鏡像列表
    while IFS= read -r line; do
        # 去除前后空格
        line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        # 忽略空行和注釋行
        if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then
            images+=("$line")
        fi
    done < "$file_path"
    
    if [[ ${#images[@]} -eq 0 ]]; then
        log_warning "沒有找到要拉取的鏡像"
        exit 0
    fi
    
    log_info "從 $file_path 讀取到 ${#images[@]} 個鏡像"
    
    # 計算批次數(shù)量
    local total_images=${#images[@]}
    local total_batches=$(( (total_images + BATCH_SIZE - 1) / BATCH_SIZE ))
    
    echo
    log_info "開始批量拉取任務(wù):"
    log_info "  總鏡像數(shù): $total_images"
    log_info "  批次大小: $BATCH_SIZE"
    log_info "  總批次數(shù): $total_batches"
    log_info "  開始時間: $(date '+%Y-%m-%d %H:%M:%S')"
    
    # 按批次處理鏡像
    for ((batch_num = 1; batch_num <= total_batches; batch_num++)); do
        local start_idx=$(((batch_num - 1) * BATCH_SIZE))
        local end_idx=$((start_idx + BATCH_SIZE))
        if [[ $end_idx -gt $total_images ]]; then
            end_idx=$total_images
        fi
        
        # 提取當(dāng)前批次的鏡像
        local batch_images=("${images[@]:$start_idx:$((end_idx - start_idx))}")
        
        # 拉取當(dāng)前批次
        local result
        result=$(pull_batch batch_images "$batch_num" "$total_batches")
        read -r success failed <<< "$result"
        
        TOTAL_SUCCESS=$((TOTAL_SUCCESS + success))
        TOTAL_FAILED=$((TOTAL_FAILED + failed))
        
        # 如果不是最后一批,顯示進度
        if [[ $batch_num -lt $total_batches ]]; then
            local completed_images=$((batch_num * BATCH_SIZE))
            local progress
            progress=$(echo "scale=1; $completed_images * 100 / $total_images" | bc)
            local elapsed=$(($(date +%s) - START_TIME))
            
            echo
            log_info "當(dāng)前進度: ${completed_images}/${total_images} (${progress}%)"
            log_info "累計成功: $TOTAL_SUCCESS, 累計失敗: $TOTAL_FAILED"
            log_info "已用時間: $(format_time $elapsed)"
            log_info "準備開始下一批次..."
            sleep 1  # 批次間短暫休息
        fi
    done
    
    # 顯示最終結(jié)果
    show_final_results "$total_images"
}

# 捕獲中斷信號
trap 'log_info "用戶中斷操作"; exit 1' INT TERM

# 運行主函數(shù)
main "$@"
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容