您的位置:首页 > 编程语言 > Go语言

golang并发ssh执行远程命令

2017-10-20 18:35 567 查看

需求

在kubernetes/docker容器化应用中,业务应用由大量容器组成,由于生产环境中出于安全考虑,一般不会允许用户直接登入集群机器,然后登入机器上的容器。况且数量之多,也没有效率。因此设计了一个命令行工具,以权限受控的账号ssh远程连接到容器所在宿主机,然后docker exec到容器内执行命令。而且该过程必须能够批量化的进行。

实现

下面是并发执行远程ssh命令的核心实现

jobs := make(chan *model.Command, len(instanceList))
results := make(chan *model.CommandResult, len(instanceList))

// 开启多个goroutine去远程登入容器,执行命令
for e := 1; e <= parallelism; e++ {
go service.Executor(e, jobs, results)
}

for _, ins := range instanceList {
jobs <- &model.Command{
Host:         ins.Host,
ContainerId:  ins.ContainerId,
Command:      cmd,
}
}
close(jobs)

failCount := 0
size := len(instanceList)
for j := 1; j <= size; j++ {
rst := <-results
success := "Success"
if rst.CmdError != nil {
success = "Fail"
failCount++
}
fmt.Printf("[%d/%d] - [%s]\t", j, size, success)
fmt.Printf("Host = %s, ContainerId = %s, rst.Host, rst.ContainerId)
fmt.Println(rst.Output)
if rst.CmdError != nil {
if ee, ok := rst.CmdError.(*exec.ExitError); ok {
waitStatus := ee.Sys().(syscall.WaitStatus)
fmt.Printf("%d\n", waitStatus.ExitStatus())
}
fmt.Printf("%s\n", rst.CmdError.Error())
}
}
//结果汇总
fmt.Printf("[INFO] Total = %d, Success = %d, Fail = %d", size, size-failCount, failCount)


下面是service.Executor的关键代码

func Executor(jobs <-chan *model.Command, jobResults chan<- *model.CommandResult) {
for job := range jobs {
out, err := ExecuteCommandInContainer(job.Host, job.ContainerId, job.Command)
jobResults <- &model.CommandResult{
CmdError:     err,
ContainerId:  job.ContainerId,
Host:         job.Host,
Output:       out,
}
}
}

// 登录容器,执行一个具体的命令
func ExecuteCommandInContainer(host string, containerId string, command string) (out string, err error) {
err = AddRsafile()
if err != nil {
return
}
homeDir := os.Getenv("HOME")
dockerHost := fmt.Sprintf(`rd@%s`, host)
containerLoginCmd := fmt.Sprintf("sudo docker exec -it -u rd %s bash -c \"%s\"", containerId, command)
cmd := exec.Command("ssh", "-i", homeDir+"/.ssh/.id_rsa",
"-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no",
"-t", "-t", dockerHost, containerLoginCmd)

cmd.Stdin = os.Stdin

b, err := cmd.Output()
if err != nil {
return
}
out = string(b)
return
}


问题

上述代码,编译成二进制可执行文件后,在shell终端里执行,当并发度大于1时,终端会被打乱,同时执行完了之后,终端已经假死,必须reset才能继续使用。但是,放到crontab里执行时,并无该问题,这是为什么?

追踪

初步怀疑是ssh并发写终端stdout问题,但是代码中明明是串行写的。于是去查ssh相关参数的用法。

注意到,上面ssh命令,带有2个-t参数,这是做什么的?参见ssh的帮助

-T      Disable pseudo-tty allocation.

-t      Force pseudo-tty allocation.  This can be used to execute arbitrary screen-based programs on a remote machine, which can be very useful, e.g., when implementing menu services.  Multiple -t options force tty allocation,
even if ssh has no local tty.


两个-t是强制ssh分配tty,尝试去掉一个,我们发现,在命令行里执行并没有什么问题,但是在crontab里就有问题了,会提示

Pseudo-terminal will not be allocated because stdin is not a terminal.


首先crontab是非登录式shell的环境,分配伪终端时,无法将stdin分配为一个terminal,也就是上面提示的含义。使用2个-t,强制分配。完美解决了crontab里无法正确执行的问题。但是并发执行是什么问题呢?

受到这个启发,由初期怀疑是并发写到控制台导致的,转入怀疑是多个ssh的线程公用了同一个stdin导致的,因为上述代码中,设定了cmd.Stdin = os.Stdin,于是将cmd.Stdin = nil, 本来这个工具也无需输入,调整之后,并发执行问题完美解决。

参考

有关如何在golang中执行shell命令,可参考这篇文章 Shelled-out Commands In Golang
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  golang ssh