撰写这段代码的初衷是在于,20年之后入职于网易,由于园区有千百来台设备交换机设备,由于当时正处于基础架构升级改造搬迁优化的阶段,可能会涉及一些配置的变更,而接入层的设备基本上都是相同的配置,除了使用的VLAN或者部分特殊配置,大部分情况下都是相同的,所以便写了这样的一个脚本,便于自己操作。
Go语言编写出来的,相较于Python执行的,可能更多的是在于一个执行速度上差异,具体情况可以自测……
这段代码的核心功能是 “批量通过 SSH 连接设备并执行命令”,整体流程如下:
- 准备阶段:读取配置(用户名密码)、IP 列表、命令列表。
- 并发控制:通过信号量限制同时连接的设备数量(50 个)。
- 执行阶段:为每个 IP 启动 goroutine,建立 SSH 连接,发送命令,记录结果和错误。
- 收尾阶段:等待所有操作完成,确保资源释放。
代码通过 goroutine 实现并发,通过上下文控制超时,通过日志记录操作结果,适合批量管理服务器、交换机等支持 SSH 的设备。
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh" //这个库go没有,需要自己去下载并导入
)
/*
* 程序功能:批量通过SSH连接多个设备(服务器/交换机等),执行预设命令并记录结果
* 核心逻辑:
* 1. 读取配置文件(用户名、密码)、IP列表、命令列表
* 2. 用goroutine并发处理多个IP的SSH连接(限制最大并发数)
* 3. 为每个IP执行命令,记录成功/失败日志
*/
// 存储SSH登录的账号密码
type configt struct {
Username string // 登录用户名
Password string // 登录密码
}
// 全局变量:存储待执行命令、配置、IP列表和并发控制信号量
var (
commands []string // 从commands.txt读取的命令列表
config configt // 从config.txt读取的账号密码
ipAddresses []string // 从ip.txt读取的目标IP列表
semaphore chan struct{} // 控制最大并发数的信号量
)
func main() {
/* 第一步:读取配置文件(config.txt)- 包含用户名和密码 */
// 读取文件内容
configFile, err := os.ReadFile("config.txt")
if err != nil {
// 致命错误:配置文件读取失败,程序无法继续,直接终止
log.Fatalln("读取配置文件错误:", err)
}
// 过滤配置文件中的空行和注释行(#开头)
var validLines []string
for _, line := range strings.Split(string(configFile), "\n") {
line = strings.TrimSpace(line) // 去除每行前后的空白字符
// 保留非空且非注释的行
if line != "" && !strings.HasPrefix(line, "#") {
validLines = append(validLines, line)
}
}
// 校验有效行数量:至少需要用户名(第1行)和密码(第2行)
if len(validLines) < 2 {
log.Fatalln("配置文件需包含至少2行有效内容(用户名、密码)")
}
// 赋值用户名和密码
config.Username = validLines[0]
config.Password = validLines[1]
fmt.Println("读取用户配置OK")
/* 第二步:读取IP列表文件(ip.txt)- 包含需要连接的设备IP */
ipFile, err := os.ReadFile("ip.txt")
if err != nil {
log.Fatalln("读取IP文件错误:", err) // IP列表是必需的,读取失败终止程序
}
// 过滤空行,提取有效IP
for _, line := range strings.Split(string(ipFile), "\n") {
ip := strings.TrimSpace(line)
if ip != "" {
ipAddresses = append(ipAddresses, ip)
}
}
fmt.Printf("读取IP列表OK(共 %d 个IP)\n", len(ipAddresses))
/* 第三步:读取命令文件(commands.txt)- 包含需要执行的命令 */
cmdFile, err := os.ReadFile("commands.txt")
if err != nil {
log.Fatalln("读取命令文件错误:", err) // 命令文件是核心,缺失则终止
}
// 过滤空行,提取有效命令
for _, line := range strings.Split(string(cmdFile), "\n") {
cmd := strings.TrimSpace(line)
if cmd != "" {
commands = append(commands, cmd)
}
}
fmt.Printf("读取命令列表OK(共 %d 条命令)\n", len(commands))
/* 第四步:初始化并发控制(信号量) */
maxConcurrent := 50 // 最大并发数:同时连接50个设备,避免资源过载,可以自定义
semaphore = make(chan struct{}, maxConcurrent)
/* 第五步:执行批量SSH操作 */
if err := sshSwitch(); err != nil {
log.Fatalln("批量SSH操作失败:", err)
}
}
/*
* sshSwitch:批量处理SSH连接的核心函数
* 功能:为每个IP创建goroutine,并发执行SSH操作,控制超时和错误日志
*/
func sshSwitch() error {
// 打开错误日志文件(记录连接/执行失败的信息)
// os.O_APPEND:追加模式;os.O_CREATE:不存在则创建;os.O_WRONLY:只写
// 0644:文件权限(所有者读写,其他只读,符合日志安全规范)
f, err := os.OpenFile("err.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("打开错误日志失败: %v", err)
}
defer f.Close() // 函数结束时关闭文件,避免资源泄漏
// 用于等待所有goroutine执行完毕的同步工具
waitGroup := &sync.WaitGroup{}
// 配置SSH客户端参数
sshConfig := &ssh.ClientConfig{
Config: ssh.Config{
// 支持的加密算法(按需添加,确保与目标设备兼容)
Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com"},
},
User: config.Username, // SSH登录用户名
Auth: []ssh.AuthMethod{ssh.Password(config.Password)}, // 密码认证
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 测试环境跳过主机密钥验证(生产环境需修改)
Timeout: 20 * time.Second, // 连接超时时间(20秒)
}
// 遍历IP列表,为每个IP启动goroutine处理
for _, ip := range ipAddresses {
waitGroup.Add(1) // 每启动一个goroutine,WaitGroup计数+1
semaphore <- struct{}{} // 信号量控制:若满则阻塞,限制并发数
// 启动goroutine并发处理当前IP
go func(targetIP string) {
// 延迟操作:释放信号量和WaitGroup计数
defer func() {
<-semaphore // 释放一个并发位置
waitGroup.Done() // WaitGroup计数-1
}()
// 设置30秒超时:避免SSH操作长时间无响应导致阻塞
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() // 函数结束时取消上下文
// 建立SSH连接(连接目标IP的22端口)
client, err := ssh.Dial("tcp", targetIP+":22", sshConfig)
if err != nil {
// 连接失败:记录错误到日志并打印
errMsg := fmt.Sprintf("IP %s 连接失败: %v\n", targetIP, err)
if _, writeErr := f.WriteString(errMsg); writeErr != nil {
fmt.Printf("写入错误日志失败: %v\n", writeErr)
}
fmt.Print(errMsg)
return // 终止当前goroutine,处理下一个IP
}
defer client.Close() // 操作结束后关闭SSH连接
// 超时监听:30秒后强制关闭连接
go func() {
<-ctx.Done() // 等待超时信号
client.Close()
}()
// 获取远程设备地址(用于日志)
remoteAddr := client.RemoteAddr().String()
startTime := time.Now()
// 执行命令并处理结果
if err := handleSession(client, commands, remoteAddr); err != nil {
// 命令执行失败:记录错误
errMsg := fmt.Sprintf("IP %s 命令执行失败: %v\n", remoteAddr, err)
if _, writeErr := f.WriteString(errMsg); writeErr != nil {
fmt.Printf("写入错误日志失败: %v\n", writeErr)
}
fmt.Print(errMsg)
} else {
// 命令执行成功:打印完成信息
fmt.Printf("IP %s 执行完毕,时间: %v\n", remoteAddr, startTime.Format("2006-01-02 15:04:05"))
}
}(ip) // 传入当前IP作为参数,避免goroutine引用循环变量
}
waitGroup.Wait() // 等待所有goroutine执行完毕
return nil
}
/*
* handleSession:单个SSH会话的命令执行逻辑
* 功能:创建会话、发送命令、记录输出到日志文件
*/
func handleSession(client *ssh.Client, cmds []string, ipAddr string) error {
// 创建日志文件(替换IP中的冒号,避免文件名非法)
logFileName := strings.ReplaceAll(ipAddr, ":", "_") + ".log"
logFile, err := os.Create(logFileName)
if err != nil {
return fmt.Errorf("创建日志文件失败: %v", err)
}
defer logFile.Close() // 函数结束时关闭日志文件
// 创建SSH会话(用于执行命令)
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("创建会话失败: %v", err)
}
defer session.Close() // 会话结束后关闭
// 配置伪终端(部分设备需要终端环境才能执行命令)
terminalModes := ssh.TerminalModes{
ssh.ECHO: 0, // 关闭回显(避免命令重复输出)
ssh.TTY_OP_ISPEED: 9600, // 输入波特率
ssh.TTY_OP_OSPEED: 9600, // 输出波特率
}
if err := session.RequestPty("xterm", 80, 40, terminalModes); err != nil {
return fmt.Errorf("请求伪终端失败: %v", err)
}
// 绑定输出流:命令输出和错误都写入日志文件
session.Stdout = logFile
session.Stderr = logFile
// 获取标准输入管道(用于发送命令)
stdin, err := session.StdinPipe()
if err != nil {
return fmt.Errorf("获取输入管道失败: %v", err)
}
// 启动交互式shell(进入命令行环境)
if err := session.Shell(); err != nil {
return fmt.Errorf("启动shell失败: %v", err)
}
// 发送命令列表中的每条命令
for _, cmd := range cmds {
// 发送命令(加换行符模拟回车)
if _, err := fmt.Fprintf(stdin, "%s\n", cmd); err != nil {
return fmt.Errorf("发送命令 '%s' 失败: %v", cmd, err)
}
// 等待300ms:设备处理命令需要时间,避免命令堆积
time.Sleep(300 * time.Millisecond)
}
// 发送退出命令,正常结束会话
if _, err := fmt.Fprintf(stdin, "exit\n"); err != nil {
return fmt.Errorf("发送退出命令失败: %v", err)
}
// 等待所有命令执行完毕
if err := session.Wait(); err != nil {
return fmt.Errorf("命令执行超时或失败: %v", err)
}
return nil // 所有命令执行成功
}
核心功能与设计亮点
- 并发控制:通过信号量限制最大并发数(50)(可以修改自定义),避免同时连接过多设备导致资源耗尽。
- 超时机制:为每个 SSH 操作设置 定时超时,防止因设备无响应导致程序卡住。
- 日志记录:
- 每个设备的命令输出单独记录到
IP.log
文件,方便追溯。 - 连接或执行错误集中记录到
err.log
,便于排查问题。
- 每个设备的命令输出单独记录到
- 健壮性处理:
- 过滤配置文件中的空行和注释,提高兼容性。
- 完善的错误处理,避免单个设备失败导致整个程序崩溃。