内容简介:这篇文章主要介绍了用Go写一个轻量级的ssh批量操作工具的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
前言
这是一个轮子。
大家都知道 Ansible 是功能超级强大的自动化运维工具,十分的高大上。太高大上了以至于在低端运维有点水土不服,在于三点:
- Ansible 是基于 Python 的,而 Python 下的安装是有一堆依赖的。。。不要笑!对于很多使用 Win 的用户而言,光是装 Python, 装 pip 就够喝一壶的了。
- Ansible 的 paybook 所使用的 yaml 语法当然非常强大了。然而对于新人而言,刚入手是玩不转的,需要学习。虽然 Ansible 相比其他的自动化运维工具,它的学习曲线已经非常平易近人了,但毕竟还是要学一下的不是么
- Ansible 自动化运维 Linux 服务器得益于 Linux 上 python 的默认支持,功能非常强大。然而如果拿来跑交换机的话,因为交换机上通常没有 python 环境,功能就要打很多折扣了。基本上也就是执行一系列的命令组合。而我们这种有大片园区网的传统单位,运维的大头正式是交换机~
所以造这个轮子的出发点是基于以下考虑的:
- 要跨平台,木有依赖,开箱即用。用 Go 来撸一个就能很好的满足这个需求。你看 Open-Falcon 的 agent,ELK 的 beats ,都选择用 Go 来实现,就是这个原因。
- 简单无脑,无需学习。直接堆砌命令行就行,就像我们初始化交换机的那种命令行组合模板。只要 cli 会玩,直接照搬过来就行。
- 要支持并发。这个是 Go 的强项了,无需多言。
- 最后当然是学习 Go 啦。
一点都没有黑 Ansible 的意思。我们也有在用 Ansible 来做自动化运维的工作,我觉得所有运维最好都学习下 Ansible,将来总是要往自动化的方向走的。这个轮子的目的在于学习 Ansible 之前,先有个够简单无脑的 工具 解决下眼前的需求~
建立 ssh 会话
Go 自身不带 ssh 包。他的 ssh 包放在了 https://godoc.org/golang.org/x/crypto/ssh 这里。import 他就好
import "golang.org/x/crypto/ssh"
首先我们需要建立一个 ssh 会话,比如这样。
func connect(user, password, host, key string, port int, cipherList []string) (*ssh.Session, error) {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
client *ssh.Client
config ssh.Config
session *ssh.Session
err error
)
// get auth method
auth = make([]ssh.AuthMethod, 0)
if key == "" {
auth = append(auth, ssh.Password(password))
} else {
pemBytes, err := ioutil.ReadFile(key)
if err != nil {
return nil, err
}
var signer ssh.Signer
if password == "" {
signer, err = ssh.ParsePrivateKey(pemBytes)
} else {
signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password))
}
if err != nil {
return nil, err
}
auth = append(auth, ssh.PublicKeys(signer))
}
if len(cipherList) == 0 {
config = ssh.Config{
Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},
}
} else {
config = ssh.Config{
Ciphers: cipherList,
}
}
clientConfig = &ssh.ClientConfig{
User: user,
Auth: auth,
Timeout: 30 * time.Second,
Config: config,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
// connet to ssh
addr = fmt.Sprintf("%s:%d", host, port)
if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
return nil, err
}
// create session
if session, err = client.NewSession(); err != nil {
return nil, err
}
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
return nil, err
}
return session, nil
}
ssh.AuthMethod 里存放了 ssh 的认证方式。使用密码认证的话,就用 ssh.Password()来加载密码。使用密钥认证的话,就用 ssh.ParsePrivateKey() 或 ssh.ParsePrivateKeyWithPassphrase() 读取密钥,然后通过 ssh.PublicKeys() 加载进去。
ssh.config 这个 struct 存了 ssh 的配置参数,他有以下几个配置选项,以下引用自GoDoc 。
type Config struct {
// Rand provides the source of entropy for cryptographic
// primitives. If Rand is nil, the cryptographic random reader
// in package crypto/rand will be used.
// 加密时用的种子。默认就好
Rand io.Reader
// The maximum number of bytes sent or received after which a
// new key is negotiated. It must be at least 256. If
// unspecified, a size suitable for the chosen cipher is used.
// 密钥协商后的最大传输字节,默认就好
RekeyThreshold uint64
// The allowed key exchanges algorithms. If unspecified then a
// default set of algorithms is used.
//
KeyExchanges []string
// The allowed cipher algorithms. If unspecified then a sensible
// default is used.
// 连接所允许的加密算法
Ciphers []string
// The allowed MAC algorithms. If unspecified then a sensible default
// is used.
// 连接允许的 MAC (Message Authentication Code 消息摘要)算法,默认就好
MACs []string
}
基本上默认的就好啦。但是 Ciphers 需要修改下,默认配置下 Go 的 SSH 包提供的 Ciphers 包含以下加密方式
连 linux 通常没有问题,但是很多交换机其实默认只提供 aes128-cbc 3des-cbc aes192-cbc aes256-cbc 这些。因此我们还是加全一点比较好。
这里有两个地方要提一下
1、在 clientConfig 里有这么一段
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
这是因为默认密钥不受信任时,Go 的 ssh 包会在 HostKeyCallback 里把连接干掉(1.8 之后加的应该)。但是我们使用用户名密码连接的时候,这个太正常了不是么,所以让他 return nil 就好了。
2、在 NewSession() 后,我们定义了 modes 和 RequestPty。这是因为为之后使用 session.Shell() 模拟终端时,所建立的终端参数。如果不配的话,默认值可能导致在某些终端上执行失败。例如一些 H3C 的交换机,连接建立后默认推出来的 Copyright 可能会导致 ssh 连接异常,然后超时或者直接断掉。例如这样:
****************************************************************************** * Copyright (c) 2004-2016 Hangzhou H3C Tech. Co., Ltd. All rights reserved. * * Without the owner's prior written consent, * * no decompiling or reverse-engineering shall be allowed. * ******************************************************************************
配置的参数照搬 GoDoc 上的示例就好了:
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := session.RequestPty("xterm", 40, 80, modes); err != nil {
log.Fatal("request for pseudo terminal failed: ", err)
}
执行命令
建立起 session 后,执行命令就很简单了,用 session.Run() 就可以执行我们的命令,结果则返回到 session.Studout 里。我们跑个简单的测试。
const (
username = "admin"
password = "password"
ip = "192.168.15.101"
port = 22
cmd = "show clock"
)
func Test_SSH_run(t *testing.T) {
ciphers := []string{}
session, err := connect(username, password, ip, port, ciphers)
if err != nil {
t.Error(err)
return
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Run(cmd)
t.Log(session.Stdout)
return
}
目标是一台交换机,测试一下
=== RUN Test_SSH_run --- PASS: Test_SSH_run (0.69s) ssh_test.go:30: 07:55:52.598 UTC Wed Jan 17 2018 PASS
可以看到 show clock 的命令已经成功执行了,并返回了结果。
session.Run() 仅限定执行单条命令,要执行若干命令组合就需要用到 session.Shell() 了。意思很明确,就是模拟一个终端去一条一条执行命令,并返回结果。就像我们用 Shell 一样,我们把整过过程打印出来输出就好了。从 session.StdinPipe() 逐个输入命令,从session.Stdout 和 session.Stderr 获取 Shell 上的输出。一样来做个测试。
const (
username = "admin"
password = "password"
ip = "192.168.15.101"
port = 22
cmds = "show clock;show env power;exit"
)
func Test_SSH(t *testing.T) {
var cipherList []string
session, err := connect(username, password, ip, key, port, cipherList)
if err != nil {
t.Error(err)
return
}
defer session.Close()
cmdlist := strings.Split(cmd, ";")
stdinBuf, err := session.StdinPipe()
if err != nil {
t.Error(err)
return
}
var outbt, errbt bytes.Buffer
session.Stdout = &outbt
session.Stderr = &errbt
err = session.Shell()
if err != nil {
t.Error(err)
return
}
for _, c := range cmdlist {
c = c + "\n"
stdinBuf.Write([]byte(c))
}
session.Wait()
t.Log((outbt.String() + errbt.String()))
return
}
还是那台交换机,测试一下
=== RUN Test_SSH
--- PASS: Test_SSH (0.69s)
ssh_test.go:51: sw-1#show clock
07:59:52.598 UTC Wed Jan 17 2018
sw-1#show env power
SW PID Serial# Status Sys Pwr PoE Pwr Watts
-- ------------------ ---------- --------------- ------- ------- -----
1 Built-in Good
sw-1#exit
PASS
可以看到,两个命令都得到执行了,并在执行完 exit 后退出连接。
比较一下和 session.Run() 的区别,可以发现在 session.Shell() 模式下,输出的内容包含了主机的名字,输入的命令等等。因为这是 tty 执行的结果嘛。如果我们只需要执行命令倒也无所谓,但是如果我们还需要从执行命令的结果中读取一些信息,这些内容就显得有些臃肿了。比如我们在一台 ubuntu 上跑一下看看
=== RUN Test_SSH
--- PASS: Test_SSH (0.98s)
ssh_test.go:50: Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-98-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Thu Jan 18 16:34:56 CST 2018
System load: 0.0 Processes: 335
Usage of /: 10.0% of 90.18GB Users logged in: 0
Memory usage: 2% IP address for eth0: 192.168.80.131
Swap usage: 0% IP address for docker0: 172.17.0.1
Graph this data and manage this system at:
https://landscape.canonical.com/
16 个可升级软件包。
16 个安全更新。
New release '17.10' available.
Run 'do-release-upgrade' to upgrade to it.
You have new mail.
Last login: Thu Jan 18 16:31:41 2018 from 192.168.95.104
root@ubuntu-docker-node3:~# root@ubuntu-docker-node3:/opt# /opt
root@ubuntu-docker-node3:/opt# 注销
最起码,上面那一堆 System information 就用不着嘛。交换机是没有办法,Linux 上能不能通过一条命令,也就是想办法 session.Run() 来执行命令组合呢?
答案是可以的,把命令通过 && 连接起来就好了嘛。LInux 的 Shell 会帮我们拆开来分别运行的,比如上面的这个命令我们就可以合并成一条命令 cd /opt&&pwd&&exit
=== RUN Test_SSH_run --- PASS: Test_SSH_run (0.91s) ssh_test.go:76: /opt
立马就简洁了对不对?
轮子
ssh 执行命令这样就差不多了。要变成一个可以用 ssh 批量操作工具,我们还要给他加上并发执行,并发限制,超时控制,输入参数解析,输出格式等等
这里就不展开了,最终这个造出来的轮子长这样:https://github.com/shanghai-edu/multissh
可以直接命令行来执行,通过 ; 号或者 , 号作为命令和主机的分隔符。
# ./multissh -cmds "show clock" -hosts "192.168.31.21;192.168.15.102" -u admin -p password
也可以通过文本来存放主机组和命令组,通过换行符分隔。
# ./multissh -cmdfile cmd1.txt.example -hostfile host.txt.example -u admin -p password
特别的,如果输入的是 IP (-ips 或 -ipfile),那么允许 IP 地址段方式的输入,例如 192.168.15.101-192.168.15.110 。(还记得 swcollector 么,类似的实现方式)
# ./multissh -cmds "show clock" -ips "192.168.15.101-192.168.15.110" -u admin -p password
支持使用 ssh 密钥认证,此时如果输入 password ,则为作为 key 的密码
# ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key"
对于 linux ,支持 linuxMode 模式,也就是将命令组合通过 && 连接后,使用 session.Run() 运行。
# ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key" -l
也可以为每个主机定义不同的配置参数,以 json 格式加载配置。
# ./multissh -c ssh.json.example
输出可以打成 json 格式,方便程序处理。
# ./multissh -c ssh.json.example -j
也可以把输出结果存到以主机名命名的文本中,比如用来做配置备份
# ./multissh -c ssh.json.example -outTxt
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Is Parallel Programming Hard, And, If So, What Can You Do About
Paul E. McKenney
The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!
图片转BASE64编码
在线图片转Base64编码工具
HTML 编码/解码
HTML 编码/解码