ssh.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package ssh
  15. import (
  16. "bytes"
  17. "context"
  18. "fmt"
  19. "io"
  20. "net"
  21. "os"
  22. "strconv"
  23. "strings"
  24. "time"
  25. "golang.org/x/crypto/ssh"
  26. "golang.org/x/crypto/ssh/terminal"
  27. "yunion.io/x/log"
  28. "yunion.io/x/pkg/errors"
  29. )
  30. const (
  31. ErrBadConfig = errors.Error("bad config")
  32. ErrNetwork = errors.Error("network error")
  33. ErrProtocol = errors.Error("ssh protocol error")
  34. )
  35. type ClientConfig struct {
  36. Username string
  37. Password string
  38. Host string
  39. Port int
  40. PrivateKey string
  41. }
  42. func parsePrivateKey(keyBuff string) (ssh.Signer, error) {
  43. return ssh.ParsePrivateKey([]byte(keyBuff))
  44. }
  45. func (conf ClientConfig) ToSshConfig() (*ssh.ClientConfig, error) {
  46. cliConfig := &ssh.ClientConfig{
  47. User: conf.Username,
  48. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  49. Timeout: 15 * time.Second,
  50. }
  51. auths := make([]ssh.AuthMethod, 0)
  52. if conf.Password != "" {
  53. auths = append(auths, ssh.Password(conf.Password))
  54. }
  55. if conf.PrivateKey != "" {
  56. signer, err := parsePrivateKey(conf.PrivateKey)
  57. if err != nil {
  58. return nil, errors.Wrapf(ErrBadConfig, "parse private key: %v", err)
  59. }
  60. auths = append(auths, ssh.PublicKeys(signer))
  61. }
  62. cliConfig.Auth = auths
  63. return cliConfig, nil
  64. }
  65. func (conf ClientConfig) Connect() (*ssh.Client, error) {
  66. cliConfig, err := conf.ToSshConfig()
  67. if err != nil {
  68. return nil, err
  69. }
  70. addr := net.JoinHostPort(conf.Host, strconv.Itoa(conf.Port))
  71. client, err := ssh.Dial("tcp", addr, cliConfig)
  72. if err != nil {
  73. return nil, err
  74. }
  75. return client, nil
  76. }
  77. func (conf ClientConfig) ConnectContext(ctx context.Context) (*ssh.Client, error) {
  78. cliConfig, err := conf.ToSshConfig()
  79. if err != nil {
  80. return nil, err
  81. }
  82. addr := net.JoinHostPort(conf.Host, strconv.Itoa(conf.Port))
  83. d := &net.Dialer{}
  84. netconn, err := d.DialContext(ctx, "tcp", addr)
  85. if err != nil {
  86. return nil, errors.Wrapf(ErrNetwork, "tcp dial: %v", err)
  87. }
  88. sshconn, chans, reqs, err := ssh.NewClientConn(netconn, addr, cliConfig)
  89. if err != nil {
  90. netconn.Close()
  91. return nil, errors.Wrap(ErrProtocol, err.Error())
  92. }
  93. sshc := ssh.NewClient(sshconn, chans, reqs)
  94. return sshc, nil
  95. }
  96. type Client struct {
  97. config ClientConfig
  98. client *ssh.Client
  99. }
  100. func (conf ClientConfig) NewClient() (*Client, error) {
  101. cli, err := conf.Connect()
  102. if err != nil {
  103. return nil, err
  104. }
  105. return &Client{
  106. config: conf,
  107. client: cli,
  108. }, nil
  109. }
  110. func NewClient(
  111. host string,
  112. port int,
  113. username string,
  114. password string,
  115. privateKey string,
  116. ) (*Client, error) {
  117. config := &ClientConfig{
  118. Host: host,
  119. Port: port,
  120. Username: username,
  121. Password: password,
  122. PrivateKey: privateKey,
  123. }
  124. return config.NewClient()
  125. }
  126. func (s *Client) GetConfig() ClientConfig {
  127. return s.config
  128. }
  129. func (s *Client) RawRun(cmds ...string) ([]string, error) {
  130. return s.run(false, cmds, nil, false)
  131. }
  132. func (s *Client) RunCmd(cmd string) ([]string, error) {
  133. return s.Run(cmd)
  134. }
  135. func (s *Client) Run(cmds ...string) ([]string, error) {
  136. return s.run(true, cmds, nil, false)
  137. }
  138. func (s *Client) RunWithInput(input io.Reader, cmds ...string) ([]string, error) {
  139. return s.run(true, cmds, input, false)
  140. }
  141. // RunWithTTY request Pty before run command.
  142. func (s *Client) RunWithTTY(cmds ...string) ([]string, error) {
  143. return s.run(false, cmds, nil, true)
  144. }
  145. func (s *Client) run(parseOutput bool, cmds []string, input io.Reader, withPty bool) ([]string, error) {
  146. ret := []string{}
  147. for _, cmd := range cmds {
  148. session, err := s.client.NewSession()
  149. if err != nil {
  150. return nil, err
  151. }
  152. defer session.Close()
  153. if withPty {
  154. modes := ssh.TerminalModes{
  155. ssh.ECHO: 1, // enable echoing
  156. ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
  157. ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
  158. }
  159. if err := session.RequestPty("xterm", 24, 80, modes); err != nil {
  160. return nil, errors.Wrap(err, "Setup TTY")
  161. }
  162. }
  163. log.Debugf("Run command(%s@%s): %s", s.config.Username, s.config.Host, cmd)
  164. var stdOut bytes.Buffer
  165. var stdErr bytes.Buffer
  166. session.Stdout = &stdOut
  167. session.Stderr = &stdErr
  168. session.Stdin = input
  169. err = session.Run(cmd)
  170. if err != nil {
  171. var outputErr error
  172. errMsg := stdErr.String()
  173. if len(stdOut.String()) != 0 {
  174. errMsg = fmt.Sprintf("%s %s", errMsg, stdOut.String())
  175. }
  176. outputErr = errors.Error(errMsg)
  177. err = errors.Wrapf(outputErr, "%q error: %v, cmd error", cmd, err)
  178. return nil, err
  179. }
  180. if parseOutput {
  181. ret = append(ret, ParseOutput(stdOut.Bytes())...)
  182. } else {
  183. ret = append(ret, stdOut.String())
  184. }
  185. }
  186. return ret, nil
  187. }
  188. func ParseOutput(output []byte) []string {
  189. lines := make([]string, 0)
  190. for _, line := range strings.Split(string(output), "\n") {
  191. lines = append(lines, strings.TrimSpace(line))
  192. }
  193. return lines
  194. }
  195. func (s *Client) Close() {
  196. s.client.Close()
  197. }
  198. func updateTermSize(session *ssh.Session, quit <-chan int) {
  199. sigwinchCh := make(chan os.Signal, 1)
  200. setsignal(sigwinchCh)
  201. fd := int(os.Stdin.Fd())
  202. width, height, err := terminal.GetSize(fd)
  203. if err != nil {
  204. log.Errorf("get terminal size: %v", err)
  205. }
  206. for {
  207. select {
  208. case <-quit:
  209. return
  210. case sigwinCh := <-sigwinchCh:
  211. if sigwinCh == nil {
  212. <-quit
  213. return
  214. }
  215. termWidth, termHeight, err := terminal.GetSize(fd)
  216. if err != nil {
  217. log.Errorf("get terminal size: %v", err)
  218. }
  219. if termHeight == height && termWidth == width {
  220. continue
  221. }
  222. err = session.WindowChange(termHeight, termWidth)
  223. if err != nil {
  224. log.Errorf("send window-change request: %v", err)
  225. continue
  226. }
  227. width = termWidth
  228. height = termHeight
  229. }
  230. }
  231. }
  232. func (s *Client) RunTerminal() error {
  233. defer s.Close()
  234. session, err := s.client.NewSession()
  235. if err != nil {
  236. return errors.Wrap(err, "open new session")
  237. }
  238. defer session.Close()
  239. fd := int(os.Stdin.Fd())
  240. state, err := terminal.MakeRaw(fd)
  241. if err != nil {
  242. return errors.Wrap(err, "make raw terminal")
  243. }
  244. defer terminal.Restore(fd, state)
  245. w, h, err := terminal.GetSize(fd)
  246. if err != nil {
  247. return errors.Wrap(err, "get terminal size")
  248. }
  249. modes := ssh.TerminalModes{
  250. ssh.ECHO: 1,
  251. ssh.TTY_OP_ISPEED: 14400,
  252. ssh.TTY_OP_OSPEED: 14400,
  253. }
  254. term := os.Getenv("TERM")
  255. if term == "" {
  256. term = "xterm-256color"
  257. }
  258. if err := session.RequestPty(term, h, w, modes); err != nil {
  259. return errors.Wrap(err, "session xterm")
  260. }
  261. session.Stdout = os.Stdout
  262. session.Stderr = os.Stderr
  263. session.Stdin = os.Stdin
  264. if err := session.Shell(); err != nil {
  265. return errors.Wrap(err, "session shell")
  266. }
  267. quit := make(chan int)
  268. go updateTermSize(session, quit)
  269. if err := session.Wait(); err != nil {
  270. if e, ok := err.(*ssh.ExitError); ok {
  271. switch e.ExitStatus() {
  272. case 130:
  273. quit <- 1
  274. return nil
  275. }
  276. }
  277. quit <- 1
  278. return errors.Wrap(err, "ssh wait")
  279. }
  280. quit <- 1
  281. return nil
  282. }
  283. func IsExitMissingError(err error) bool {
  284. errStr := new(ssh.ExitMissingError).Error()
  285. if strings.Contains(err.Error(), errStr) {
  286. return true
  287. }
  288. return false
  289. }