Go批量ssh连接设备执行命令

撰写这段代码的初衷是在于,20年之后入职于网易,由于园区有千百来台设备交换机设备,由于当时正处于基础架构升级改造搬迁优化的阶段,可能会涉及一些配置的变更,而接入层的设备基本上都是相同的配置,除了使用的VLAN或者部分特殊配置,大部分情况下都是相同的,所以便写了这样的一个脚本,便于自己操作。

Go语言编写出来的,相较于Python执行的,可能更多的是在于一个执行速度上差异,具体情况可以自测……

这段代码的核心功能是 “批量通过 SSH 连接设备并执行命令”,整体流程如下:

  1. 准备阶段:读取配置(用户名密码)、IP 列表、命令列表。
  2. 并发控制:通过信号量限制同时连接的设备数量(50 个)。
  3. 执行阶段:为每个 IP 启动 goroutine,建立 SSH 连接,发送命令,记录结果和错误。
  4. 收尾阶段:等待所有操作完成,确保资源释放。

代码通过 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 // 所有命令执行成功
}

核心功能与设计亮点

  1. 并发控制:通过信号量限制最大并发数(50)(可以修改自定义),避免同时连接过多设备导致资源耗尽。
  2. 超时机制:为每个 SSH 操作设置 定时超时,防止因设备无响应导致程序卡住。
  3. 日志记录
    • 每个设备的命令输出单独记录到IP.log文件,方便追溯。
    • 连接或执行错误集中记录到err.log,便于排查问题。
  4. 健壮性处理
    • 过滤配置文件中的空行和注释,提高兼容性。
    • 完善的错误处理,避免单个设备失败导致整个程序崩溃。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇