ssh批量机器免密操作

一、模块

安装 fabric和invoke自动化模块,该模块的主要用途是简化 ssh 远程服务器的管理和运维操作,支持在多台机器并行操作执行命令或脚本,支持文件上传和下载等功能

安装Python3:

yum install -y python3

安装pip3:

yum install -y python3-pip

使用pip3安装fabric和invoke:

pip3 install fabric invoke paramiko

二、 配置master和node节点,对应四个字段分别为ip、ssh端口、用户、密码,中间用空格隔开。

cat master.txt

10.10.43.10 22 root 密码

cat node.txt

10.10.43.19 22 root 密码

三、批量配置的python脚本,该脚本主要分为三个函数,具体如下

3.1check_host_passwd为检查master和node配置ip端口密码的可用性。

3.2gen_master_ssh_key函数为在管理端上生成ssh证书

3.3 ssh_to_other函数为将master节点的的公钥下发到个个节点上

复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SSH免密登录批量配置脚本 - 兼容Python 3.7+和Fabric所有版本
"""

import os
import sys
import time
import socket
import logging
import paramiko
from typing import List, Tuple, Optional
from dataclasses import dataclass

# 设置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('ssh_config.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

@dataclass
class HostInfo:
    """主机信息类"""
    ip: str
    port: int
    user: str
    passwd: str

def parse_host_file(file_path: str) -> List[HostInfo]:
    """
    解析主机配置文件
    """
    hosts = []
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line_num, line in enumerate(f, 1):
                line = line.strip()
                if not line or line.startswith('#'):
                    continue
                
                # 使用制表符或空格分割
                parts = line.split()
                if len(parts) >= 4:
                    ip = parts[0]
                    port = int(parts[1])
                    user = parts[2]
                    passwd = ' '.join(parts[3:])  # 密码可能包含空格
                    
                    hosts.append(HostInfo(ip=ip, port=port, user=user, passwd=passwd))
                    logger.debug(f"解析成功: {ip}:{port} {user}")
                else:
                    logger.warning(f"第{line_num}行格式错误: {line}")
    except Exception as e:
        logger.error(f"解析文件 {file_path} 失败: {e}")
        sys.exit(1)
    
    return hosts

def test_port(host: str, port: int, timeout: int = 5) -> bool:
    """测试端口是否开放"""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        sock.close()
        return result == 0
    except Exception as e:
        logger.debug(f"端口测试异常 {host}:{port}: {e}")
        return False

def ssh_connect(host: str, port: int, username: str, password: str, timeout: int = 10) -> Optional[paramiko.SSHClient]:
    """
    使用paramiko创建SSH连接
    """
    # 先测试端口
    if not test_port(host, port, 3):
        logger.debug(f"端口不可达: {host}:{port}")
        return None
    
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    try:
        ssh.connect(
            hostname=host,
            port=port,
            username=username,
            password=password,
            timeout=timeout,
            banner_timeout=30,
            allow_agent=False,
            look_for_keys=False
        )
        
        # 测试连接是否正常
        stdin, stdout, stderr = ssh.exec_command('echo "CONNECT_TEST_OK"', timeout=5)
        output = stdout.read().decode('utf-8', errors='ignore').strip()
        
        if "CONNECT_TEST_OK" in output:
            logger.debug(f"SSH连接成功: {host}:{port}")
            return ssh
        else:
            logger.debug(f"连接测试失败: {host}:{port}")
            ssh.close()
            return None
            
    except paramiko.AuthenticationException:
        logger.error(f"认证失败: {host}:{port} - 用户名或密码错误")
        return None
    except paramiko.SSHException as e:
        logger.error(f"SSH连接失败: {host}:{port} - {e}")
        return None
    except socket.timeout:
        logger.error(f"连接超时: {host}:{port}")
        return None
    except Exception as e:
        logger.error(f"连接异常: {host}:{port} - {e}")
        return None

def check_hosts(host_list: List[HostInfo]) -> List[HostInfo]:
    """
    检测主机连接
    """
    valid_hosts = []
    logger.info("开始检测主机连接...")
    
    for i, host in enumerate(host_list, 1):
        logger.info(f"检测主机 {i}/{len(host_list)}: {host.ip}:{host.port}")
        
        ssh = ssh_connect(host.ip, host.port, host.user, host.passwd)
        if ssh:
            try:
                # 获取主机信息
                stdin, stdout, stderr = ssh.exec_command('uname -s -r -m', timeout=5)
                if stdout.channel.recv_exit_status() == 0:
                    os_info = stdout.read().decode('utf-8', errors='ignore').strip()
                    logger.info(f"✓ {host.ip} - 连接成功 ({os_info})")
                    valid_hosts.append(host)
                else:
                    logger.warning(f"✗ {host.ip} - 命令执行失败")
                ssh.close()
            except Exception as e:
                logger.error(f"✗ {host.ip} - 连接异常: {e}")
                try:
                    ssh.close()
                except:
                    pass
        else:
            logger.warning(f"✗ {host.ip} - 连接失败")
    
    logger.info(f"检测完成,有效主机: {len(valid_hosts)}/{len(host_list)}")
    return valid_hosts

def generate_ssh_key(host: HostInfo) -> Tuple[bool, str]:
    """
    为主机生成SSH密钥
    """
    logger.info(f"为 {host.ip} 生成SSH密钥...")
    
    ssh = ssh_connect(host.ip, host.port, host.user, host.passwd)
    if not ssh:
        return False, f"连接失败: {host.ip}"
    
    try:
        # 检查是否已有密钥
        stdin, stdout, stderr = ssh.exec_command(
            '[ -f ~/.ssh/id_rsa ] && echo "EXISTS" || echo "NOT_EXISTS"',
            timeout=5
        )
        has_key = stdout.read().decode('utf-8', errors='ignore').strip()
        
        if has_key == "EXISTS":
            logger.info(f"✓ {host.ip} - 已存在SSH密钥")
            ssh.close()
            return True, "密钥已存在"
        
        # 生成新密钥
        logger.info(f"{host.ip} - 生成SSH密钥...")
        
        # 确保.ssh目录存在
        ssh.exec_command('mkdir -p ~/.ssh', timeout=5)
        ssh.exec_command('chmod 700 ~/.ssh', timeout=5)
        
        # 生成RSA密钥(非交互式)
        keygen_cmd = 'ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" -q'
        stdin, stdout, stderr = ssh.exec_command(keygen_cmd, timeout=10)
        
        exit_status = stdout.channel.recv_exit_status()
        if exit_status == 0:
            logger.info(f"✓ {host.ip} - SSH密钥生成成功")
            
            # 设置权限
            ssh.exec_command('chmod 600 ~/.ssh/id_rsa', timeout=5)
            ssh.exec_command('chmod 644 ~/.ssh/id_rsa.pub', timeout=5)
            
            ssh.close()
            return True, "密钥生成成功"
        else:
            error_msg = stderr.read().decode('utf-8', errors='ignore')
            logger.error(f"✗ {host.ip} - SSH密钥生成失败: {error_msg}")
            ssh.close()
            return False, f"密钥生成失败: {error_msg}"
            
    except Exception as e:
        logger.error(f"✗ {host.ip} - 处理异常: {e}")
        try:
            ssh.close()
        except:
            pass
        return False, str(e)

def distribute_ssh_key(source: HostInfo, target: HostInfo) -> Tuple[bool, str]:
    """
    将源主机的公钥分发到目标主机
    """
    logger.info(f"分发公钥: {source.ip} -> {target.ip}")
    
    if source.ip == target.ip:
        logger.info(f"跳过自身: {source.ip}")
        return True, "跳过自身"
    
    # 连接到源主机获取公钥
    source_ssh = ssh_connect(source.ip, source.port, source.user, source.passwd)
    if not source_ssh:
        return False, f"源主机连接失败: {source.ip}"
    
    try:
        # 读取公钥
        stdin, stdout, stderr = source_ssh.exec_command('cat ~/.ssh/id_rsa.pub', timeout=5)
        pub_key = stdout.read().decode('utf-8', errors='ignore').strip()
        
        if not pub_key:
            source_ssh.close()
            return False, f"无法读取公钥: {source.ip}"
        
        # 连接到目标主机
        target_ssh = ssh_connect(target.ip, target.port, target.user, target.passwd)
        if not target_ssh:
            source_ssh.close()
            return False, f"目标主机连接失败: {target.ip}"
        
        try:
            # 确保.ssh目录存在
            target_ssh.exec_command('mkdir -p ~/.ssh', timeout=5)
            target_ssh.exec_command('chmod 700 ~/.ssh', timeout=5)
            
            # 检查是否已存在该公钥
            check_cmd = f'grep -Fx "{pub_key}" ~/.ssh/authorized_keys 2>/dev/null || echo "NOT_FOUND"'
            stdin, stdout, stderr = target_ssh.exec_command(check_cmd, timeout=5)
            result = stdout.read().decode('utf-8', errors='ignore').strip()
            
            if "NOT_FOUND" in result:
                # 添加公钥
                append_cmd = f'echo "{pub_key}" >> ~/.ssh/authorized_keys'
                target_ssh.exec_command(append_cmd, timeout=5)
                
                # 设置权限
                target_ssh.exec_command('chmod 600 ~/.ssh/authorized_keys', timeout=5)
                
                logger.info(f"✓ {source.ip} -> {target.ip} - 公钥分发成功")
                success = True
                msg = "公钥分发成功"
            else:
                logger.info(f"✓ {source.ip} -> {target.ip} - 公钥已存在")
                success = True
                msg = "公钥已存在"
            
            target_ssh.close()
            source_ssh.close()
            return success, msg
            
        except Exception as e:
            logger.error(f"✗ {target.ip} - 处理异常: {e}")
            try:
                target_ssh.close()
            except:
                pass
            source_ssh.close()
            return False, str(e)
            
    except Exception as e:
        logger.error(f"✗ {source.ip} - 处理异常: {e}")
        try:
            source_ssh.close()
        except:
            pass
        return False, str(e)

def test_passwordless_login(source: HostInfo, target: HostInfo) -> Tuple[bool, str]:
    """
    测试免密登录
    """
    if source.ip == target.ip:
        return True, "跳过自身"
    
    ssh = ssh_connect(source.ip, source.port, source.user, source.passwd)
    if not ssh:
        return False, f"源主机连接失败: {source.ip}"
    
    try:
        # 测试SSH连接
        test_cmd = f'ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o ConnectTimeout=5 {target.user}@{target.ip} "echo SSH_TEST_SUCCESS" 2>&1'
        stdin, stdout, stderr = ssh.exec_command(test_cmd, timeout=10)
        
        output = stdout.read().decode('utf-8', errors='ignore')
        error = stderr.read().decode('utf-8', errors='ignore')
        
        if "SSH_TEST_SUCCESS" in output:
            logger.info(f"✓ {source.ip} -> {target.ip} - 免密登录成功")
            ssh.close()
            return True, "免密登录成功"
        else:
            error_msg = error if error else output[:100]
            logger.warning(f"✗ {source.ip} -> {target.ip} - 免密登录失败: {error_msg}")
            ssh.close()
            return False, error_msg
            
    except Exception as e:
        logger.error(f"✗ 测试异常: {source.ip} -> {target.ip}: {e}")
        try:
            ssh.close()
        except:
            pass
        return False, str(e)

def main():
    """主函数"""
    print("=" * 60)
    print("SSH免密登录批量配置工具")
    print("=" * 60)
    
    # 检查依赖
    try:
        import paramiko
    except ImportError:
        print("错误: 需要安装 paramiko 库")
        print("请运行: pip3 install paramiko")
        sys.exit(1)
    
    # 检查配置文件
    config_files = ['master.txt', 'node.txt']
    for file in config_files:
        if not os.path.exists(file):
            print(f"错误: 缺少配置文件 {file}")
            print(f"请创建 {file} 文件,格式如下:")
            print("# IP地址 端口 用户名 密码")
            print("192.168.1.100 22 root your_password")
            sys.exit(1)
    
    # 1. 读取配置
    print("\n[1/5] 读取配置文件...")
    try:
        masters = parse_host_file('master.txt')
        nodes = parse_host_file('node.txt')
        
        print(f"   读取到 {len(masters)} 个Master节点")
        print(f"   读取到 {len(nodes)} 个Node节点")
        
        if not masters:
            print("错误: master.txt 中没有有效的主机配置")
            sys.exit(1)
        if not nodes:
            print("错误: node.txt 中没有有效的主机配置")
            sys.exit(1)
            
    except Exception as e:
        print(f"错误: 读取配置文件失败: {e}")
        sys.exit(1)
    
    # 2. 检测连接
    print("\n[2/5] 检测主机连接...")
    valid_masters = check_hosts(masters)
    valid_nodes = check_hosts(nodes)
    
    if not valid_masters:
        print("错误: 没有有效的Master节点")
        print("请检查:")
        print("  1. master.txt 文件格式")
        print("  2. 网络连接")
        print("  3. SSH服务状态")
        print("  4. 用户名和密码")
        sys.exit(1)
    
    if not valid_nodes:
        print("错误: 没有有效的Node节点")
        print("请检查:")
        print("  1. node.txt 文件格式")
        print("  2. 网络连接")
        print("  3. SSH服务状态")
        print("  4. 用户名和密码")
        sys.exit(1)
    
    print(f"   ✓ 有效Master节点: {len(valid_masters)}/{len(masters)}")
    print(f"   ✓ 有效Node节点: {len(valid_nodes)}/{len(nodes)}")
    
    # 3. 生成SSH密钥
    print("\n[3/5] 为Master节点生成SSH密钥...")
    for i, master in enumerate(valid_masters, 1):
        print(f"   处理Master节点 {i}/{len(valid_masters)}: {master.ip}")
        success, msg = generate_ssh_key(master)
        if not success:
            print(f"   警告: {master.ip} - {msg}")
    
    # 4. 分发公钥
    print("\n[4/5] 分发SSH公钥到Node节点...")
    total_pairs = len(valid_masters) * len(valid_nodes)
    processed = 0
    
    for i, master in enumerate(valid_masters, 1):
        print(f"   Master节点 {i}/{len(valid_masters)}: {master.ip}")
        
        for j, node in enumerate(valid_nodes, 1):
            if master.ip == node.ip:
                continue
                
            processed += 1
            print(f"     分发到Node节点 {j}/{len(valid_nodes)}: {node.ip} ({processed}/{total_pairs})")
            success, msg = distribute_ssh_key(master, node)
            if not success:
                print(f"     警告: {master.ip} -> {node.ip}: {msg}")
    
    # 5. 测试免密登录
    print("\n[5/5] 测试SSH免密登录...")
    successful_tests = 0
    total_tests = 0
    
    for i, master in enumerate(valid_masters, 1):
        print(f"   测试Master节点 {i}/{len(valid_masters)}: {master.ip}")
        
        for j, node in enumerate(valid_nodes, 1):
            if master.ip == node.ip:
                continue
                
            total_tests += 1
            print(f"     测试连接到Node节点 {j}/{len(valid_nodes)}: {node.ip}")
            success, msg = test_passwordless_login(master, node)
            if success:
                successful_tests += 1
    
    # 输出结果
    print("\n" + "=" * 60)
    print("配置完成!")
    print("=" * 60)
    
    print(f"\n配置统计:")
    print(f"  Master节点: {len(valid_masters)} 台")
    print(f"  Node节点: {len(valid_nodes)} 台")
    print(f"  免密登录测试: {successful_tests}/{total_tests} 成功")
    
    if successful_tests == total_tests:
        print("\n✅ 所有测试通过!SSH免密登录配置成功!")
    else:
        print(f"\n⚠️  部分测试失败 ({successful_tests}/{total_tests})")
        print("   请检查以下可能的问题:")
        print("   1. 防火墙设置")
        print("   2. SELinux策略")
        print("   3. ~/.ssh/authorized_keys 文件权限")
        print("   4. 查看详细日志: ssh_config.log")
    
    print(f"\n日志文件: ssh_config.log")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n操作被用户中断")
        sys.exit(0)
    except Exception as e:
        print(f"\n未预期的错误: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

四、在服务器上执行脚本,会先在管理端上生成ssh证书,然后在公钥分发到每个客户端上

python3 ssh_batch.py

然后检查是否执行成功

相关推荐
a程序小傲2 小时前
得物Java面试被问:反射机制的原理和应用场景
java·python·面试
于越海2 小时前
学习小项目:用 Python 自动统计编程课绩点(5.0 制|百分制直算|重修取最高)
开发语言·笔记·python·学习·学习方法
jerryinwuhan2 小时前
1231_linux_shell_1
linux
Guistar~~2 小时前
【Linux驱动开发IMX6ULL】使用NXP MfgTool 烧写系统到eMMC
linux·运维·驱动开发
xingzhemengyou12 小时前
Python GUI中常用的after
开发语言·python
郝学胜-神的一滴2 小时前
Python抽象基类与abc模块详解:优雅设计接口的利器
开发语言·python·程序人生
小南知更鸟2 小时前
前端静态项目快速启动:python -m http.server 4173 与 npx serve . 全解析
前端·python·http
小钟不想敲代码2 小时前
Python(三)
java·python·servlet
啵啵啵啵哲2 小时前
【输入法】Ubuntu 22.04 终极输入法方案:Fcitx5 + 雾凇拼音 (Flatpak版)
linux·运维·ubuntu