一、模块
安装 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
然后检查是否执行成功
