发送指令执行 session.Run() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package mainimport ( "bytes" "fmt" "golang.org/x/crypto/ssh" "log" ) func main () { client, err := ssh.Dial("tcp" , "127.0.0.1:2222" , &ssh.ClientConfig{ User: "root" , Auth: []ssh.AuthMethod{ssh.Password("123456" )}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), }) if err != nil { log.Fatalf("SSH dial error: %s" , err.Error()) } session, err := client.NewSession() if err != nil { log.Fatalf("new session error: %s" , err.Error()) } defer session.Close() var b bytes.Buffer session.Stdout = &b if err := session.Run("ls" ); err != nil { panic ("Failed to run: " + err.Error()) } fmt.Println(b.String()) }
发送指令执行 session.Output() session.run(command) 是直接在 host 执行命令,不关心执行结果。session.Output 是将执行命令之后的 Stdout 返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "fmt" "golang.org/x/crypto/ssh" "log" "os" ) func test () { client, err := ssh.Dial("tcp" , "127.0.0.1:2222" , &ssh.ClientConfig{ User: "root" , Auth: []ssh.AuthMethod{ssh.Password("123456" )}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), }) if err != nil { log.Fatalf("SSH dial error: %s" , err.Error()) } session, err := client.NewSession() defer session.Close() if err != nil { log.Fatalf("new session error: %s" , err.Error()) } result, err := session.Output("ls -al" ) if err != nil { fmt.Fprintf(os.Stdout, "Failed to run command, Err:%s" , err.Error()) os.Exit(0 ) } fmt.Println(string (result)) combo,err := session.CombinedOutput("whoami; cd /; ls -al;echo https://github.com/dejavuzhou/felix" ) if err != nil { log.Fatal("远程执行cmd 失败" ,err) } log.Println("命令输出:" ,string (combo)) }
模拟交互 terminal 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "golang.org/x/crypto/ssh" "log" "os" ) func main () { client, err := ssh.Dial("tcp" , "127.0.0.1:2222" , &ssh.ClientConfig{ User: "root" , Auth: []ssh.AuthMethod{ssh.Password("123456" )}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), }) if err != nil { log.Fatalf("SSH dial error: %s" , err.Error()) } session, err := client.NewSession() defer session.Close() if err != nil { log.Fatalf("new session error: %s" , err.Error()) } session.Stdout = os.Stdout session.Stderr = os.Stderr session.Stdin = os.Stdin modes := ssh.TerminalModes{ ssh.ECHO: 0 , ssh.TTY_OP_ISPEED: 14400 , ssh.TTY_OP_OSPEED: 14400 , } if err = session.RequestPty("linux" , 32 , 160 , modes); err != nil { log.Fatalf("request pty error: %s" , err.Error()) } if err = session.Shell(); err != nil { log.Fatalf("start shell error: %s" , err.Error()) } if err = session.Wait(); err != nil { log.Fatalf("return error: %s" , err.Error()) } }
1 2 3 4 5 6 7 8 9 //如果不禁用回显 [root@65a9c031a770 ~]# ls ls anaconda-ks.cfg //禁用回显 [root@65a9c031a770 ~]# ls anaconda-ks.cfg
注意:
这里的 ssh.InsecureIgnoreHostKey 是不检查 host key,需要检查的话得参考 client 源码重写函数
ssh 公钥登录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func SSHConnect (user, host string , port int ) (*ssh.Client, error) { var ( addr string clientConfig *ssh.ClientConfig client *ssh.Client err error ) homePath, err := os.UserHomeDir() if err != nil { return nil , err } key, err := ioutil.ReadFile(path.Join(homePath, ".ssh" , "id_rsa" )) if err != nil { return nil , err } signer, err := ssh.ParsePrivateKey(key) if err != nil { return nil , err } clientConfig = &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, Timeout: 30 * time.Second, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } addr = fmt.Sprintf("%s:%d" , host, port) if client, err = ssh.Dial("tcp" , addr, clientConfig); err != nil { err = errors.Wrapf(err, "" ) return nil , err } return client, nil }
或者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport ( "fmt" "golang.org/x/crypto/ssh" ) const privateKey = `content of id_rsa` func main () { signer, _ := ssh.ParsePrivateKey([]byte (privateKey)) clientConfig := &ssh.ClientConfig{ User: "jedy" , Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, } client, err := ssh.Dial("tcp" , "127.0.0.1:22" , clientConfig) if err != nil { panic ("Failed to dial: " + err.Error()) } session, err := client.NewSession() if err != nil { panic ("Failed to create session: " + err.Error()) } defer session.Close() go func () { w, _ := session.StdinPipe() defer w.Close() content := "123456789\n" fmt.Fprintln(w, "D0755" , 0 , "testdir" ) fmt.Fprintln(w, "C0644" , len (content), "testfile1" ) fmt.Fprint(w, content) fmt.Fprint(w, "\x00" ) fmt.Fprintln(w, "C0644" , len (content), "testfile2" ) fmt.Fprint(w, content) fmt.Fprint(w, "\x00" ) }() if err := session.Run("/usr/bin/scp -tr ./" ); err != nil { panic ("Failed to run: " + err.Error()) } }
通过 SSH 执行远程主机的命令或脚本时,经常会出现找不到自定义环境变量的问题。但是,如果通过 SSH 登录远程主机,然后再执行相同的命令或脚本,那么此时执行又是成功的。
两种相似的方法,得到的结果却截然不同,看起来很诡异的现象,根本原因在于这两种方式使用的 bash 模式不同!
1. 通过 SSH 登录后再执行命令和脚本 这种方式会使用 Bash 的 interactive + login shell 模式,这里面有两个概念需要解释:interactive
和 login
。
login
: 故名思义,即登陆。login shell 是指用户以非图形化界面或者以 ssh 登陆到机器上时获得的第一个 shell,简单些说就是需要输入用户名和密码的 shell。因此通常不管以何种方式登陆机器后用户获得的第一个 shell 就是 login shell。
interactive
: 意为交互式,这也很好理解,interactive shell 会有一个输入提示符,并且它的标准输入、输出和错误输出都会显示在控制台上。所以一般来说只要是需要用户交互的,即一个命令一个命令的输入的 shell 都是 interactive shell。而如果无需用户交互,它便是 non-interactive shell。通常来说如 bash script.sh
此类执行脚本的命令就会启动一个 non-interactive shell,它不需要与用户进行交互,执行完后它便会退出创建的 Shell。
在 interactive + login shell 模式中,Shell 首先会加载 /etc/profile
文件,然后再尝试依次去加载下列三个配置文件之一,一旦找到其中一个便不再接着寻找:
~/.bash_profile
~/.bash_login
~/.profile
2. 通过 SSH 直接执行远程命令和脚本 这种方式会使用 Bash 的 non-interactive + non-login shell 模式,它会创建一个 shell,执行完脚本之后便退出,不再需要与用户交互。
no-login shell,顾名思义就是不是在登录 Linux 系统时启动的(比如你在命令行提示符上输入 bash 启动)。它不会去执行 /etc/profile
文件,而会去用户的 HOME 目录检查.bashrc
并加载。
系统执行 Shell 脚本的时候,就是属于这种 non-interactive shell。Bash 通过 BASH_ENV
环境变量来记录要加载的文件,默认情况下这个环境变量并没有设置。如果有指定文件,那么 Shell 会先去加载这个文件里面的内容,然后再开始执行 Shell 脚本。
3. 结论 由此可见,如果要解决 SSH 远程执行命令时找不到自定义环境变量的问题,那么可以在登录用户的 HOME 目录的.bashrc
中添加需要的环境变量。
4. 示例 当登录之后,直接在某台远程主机:10.0.63.9 上执行日期格式化的命令时,打印的是正确的,如下:
1 2 [root@dev-appserver2 ~] 170927165847
当在另外一台主机(10.0.251.216)上远程执行 ssh 命令时,打印的结果不正确(差了八个时区),如下:
1 2 3 4 5 6 7 8 9 [root@host-10-0-251-216 ~] buildTimeStamp=2017-09-27T16:58:47.291+08:00; buildTimeStampStr=$(date -d $buildTimeStamp "+%y%m%d%H%M%S" ); echo $buildTimeStampStr [root@host-10-0-251-216 ~] [root@host-10-0-251-216 ~] buildTimeStamp=2017-09-27T16:58:47.291+08:00; buildTimeStampStr=$(date -d $buildTimeStamp "+%y%m%d%H%M%S" ); echo $buildTimeStampStr [root@host-10-0-251-216 ~] root@10.0.63.9's password: 170927085847
此时修改 10.0.63.9 上,root 根目录下的.bashrc 文件,增加 TZ 的设置,再次执行 ssh 打印的结果是正确的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@dev-appserver2 ~] alias rm='rm -i' alias cp='cp -i' alias mv='mv -i' export TZ="Asia/Shanghai" if [ -f /etc/bashrc ]; then . /etc/bashrc fi [root@dev-appserver2 ~]
1 2 3 [root@host-10-0-251-216 ~] root@10.0.63.9's password: 170927165847
5. 在 crypto/ssh 库中设置环境变量 简单来说就是,需要执行source /etc/profile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "golang.org/x/crypto/ssh" "log" ) func main () { client, err := ssh.Dial("tcp" , "127.0.0.1:22" , &ssh.ClientConfig{ User: "heyingliang" , Auth: []ssh.AuthMethod{ssh.Password("liangbo4869" )}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), }) if err != nil { log.Fatalf("SSH dial error: %s" , err.Error()) } session, err := client.NewSession() if err != nil { log.Fatalf("new session error: %s" , err.Error()) } defer session.Close() combo,err := session.CombinedOutput("source /etc/profile; docker ps" ) if err != nil { log.Fatal("远程执行cmd 失败" ,err) } log.Println("命令输出:" ,string (combo)) }
非交互模式是不能从 shell 进入到另一个 shell 的 所以,执行docker exec -i redis bash -c "ls;pwd"
。注意 exec 的参数是-i
,而不是-it
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "golang.org/x/crypto/ssh" "log" ) func main () { client, err := ssh.Dial("tcp" , "127.0.0.1:22" , &ssh.ClientConfig{ User: "heyingliang" , Auth: []ssh.AuthMethod{ssh.Password("liangbo4869" )}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), }) if err != nil { log.Fatalf("SSH dial error: %s" , err.Error()) } session, err := client.NewSession() if err != nil { log.Fatalf("new session error: %s" , err.Error()) } defer session.Close() combo,err := session.CombinedOutput(`source /etc/profile; docker exec -i redis bash -c "ls;pwd"` ) if err != nil { log.Fatal("远程执行cmd 失败" ,err) } log.Println("命令输出:" ,string (combo)) }
Reference 使用 GO 语言灵活批量 ssh 登录服务器执行操作: https://www.cnblogs.com/findumars/p/5930584.html
github 一个非常好的 web ssh 项目: https://github.com/libragen/felix