Use Go to write a method for a lightweight ssh batch manipulation tool

  • 2020-06-15 09:13:18
  • OfStack

preface

This is 1 wheel.

Ansible is known as a super powerful automated operation and maintenance tool. It is so big that it is a little unaccustomed to low-end operation and maintenance. There are three points:

Ansible is based on Python and the installation under Python is 1 heap dependent... Don't laugh! For many Win users, it's enough to fill a jug of Python. The yaml syntax used by Ansible's paybook is certainly very powerful. However, for the new, just start is not play, need to learn. While the Ansible has a very easy learning curve compared to other automated operations tools, there's still a lot to learn Ansible Automated operations Linux servers benefit from python's default support on Linux and are very powerful. However, when it comes to switches, which usually don't have an python environment, the functionality is significantly reduced. Basically, you execute a series 1 combination of commands. And we have a large park network of traditional units, the big head of operation and maintenance is officially the switch ~

So the starting point of building this wheel is based on the following considerations:

Cross-platform, no dependencies, out of the box. Using Go for one stroke can well meet this requirement. If you look at the agent of ES25en-ES26en and the beats of ELK, they both choose Go for this reason. Simple brainless, no need to learn. Just stack the command line, just like the command-line composite template we used to initialize the switch. As long as cli can play, just copy it. Support concurrency. This is Go's strong point, needless to say. And finally, of course, Go.

One o 'clock doesn't mean black Ansible. We are also using Ansible for automated operations and maintenance. I think it is best for all operations and maintenance to learn Ansible, and we will always move towards automation in the future. The purpose of this wheel is to have a simple, brainless tool to solve immediate needs before learning Ansible

Set up the ssh session

Go itself does not carry ssh bags. His ssh bag on https: / / godoc org/golang org/x/crypto/ssh here. import he's good


import "golang.org/x/crypto/ssh"

First we need to set up an ssh session, like this.


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 contains the ssh authentication method. For password authentication, load the password with ssh.Password (). To authenticate with the key, read the key using ssh.ParsePrivateKey () or ssh.ParsePrivateKeyWithPassphrase () and load it through ES72en.PublicKeys ().

This struct stores the configuration parameters of ssh and has the following configuration options, which are referenced from 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.
  //  The seed used for encryption. It is good to the default 
  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.
  //  The maximum number of bytes transferred after the key is negotiated, by default 
  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.
  //  The encryption algorithm allowed for the connection 
  Ciphers []string

  // The allowed MAC algorithms. If unspecified then a sensible default
  // is used.
  //  Connection-Allowed  MAC (Message Authentication Code  The message digest ) Algorithm, default is good 
  MACs []string
}

It's basically the default. However, Ciphers needs to be modified. The Ciphers package provided by Go in the default configuration contains the following encryption method


aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com arcfour256 arcfour128

It is usually ok to connect to linux, but many switches only provide es94EN128-ES95en 3ES96en-ES97en es98EN192-ES99en aes256-ES101en by default. So we might as well add all 1 points.

There are two things I want to mention here

1. There is such a paragraph in clientConfig


HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
  return nil
},

This is because Go's ssh packet will kill the connection in HostKeyCallback if the default key is not trusted. But when we connect with a username and password, this is pretty normal, right, so just let him return nil.

2. After NewSession(), we defined modes and RequestPty. This is because of the terminal parameters established for the later session.Shell () simulation of the terminal. If not, the default value may cause execution to fail on some terminals. For example, in some H3C switches, the default Copyright may cause an ssh connection exception and then time out or break. For example:


******************************************************************************
* 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.          *
******************************************************************************

Just copy the configuration parameters from the example on 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)
}

Execute the command

Once session is set up, it is easy to execute our command using ES138en.Run () and the result is returned to session.Studout. Let's run a simple test.


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
}

Target 1 switch, test 1


=== RUN  Test_SSH_run
--- PASS: Test_SSH_run (0.69s)
  ssh_test.go:30: 07:55:52.598 UTC Wed Jan 17 2018
PASS

You can see that the show clock command has executed successfully and returned the result.

session.Run () is limited to executing a single command; to execute several command combinations, you need to use session.Shell (). The meaning is very clear, is to simulate 1 terminal to 1 execution command, and return the result. Just like we did with Shell 1, we print out the whole process and print it out. Enter the commands one by one from ES157en.StdinPipe () and get the output on Shell from session.Stdout and session.Stderr. 1. Let's do a test.


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
}

Same switch, test 1


=== 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

As you can see, both commands are executed and the connection exits after exit is executed.

Comparing 1 with ES173en. Run(), you can see that in ES175en. Shell() mode, the output includes the name of the host, the command entered, and so on. Because this is the result of the tty implementation. It doesn't matter if we only need to execute the command, but if we also need to read 1 bit of information from the result of executing the command, it becomes a bit bloated. Let's take an ubuntu and run it 1 time


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
}

0

At the very least, you don't need the pile of System information. The switch has no way. Can Linux execute the command combination with a single command, session.Run ()?

The answer is yes, pass the order & & Just connect it up. LInux's Shell will run it separately for us. For example, the above command can be combined into one command cd /opt & & pwd & & exit


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
}

1

It's immediately simpler, right?

The wheel

ssh executes the command and that's about it. To become an ssh batch operation tool, we have to add concurrent execution, concurrency limitation, timeout control, input parameter resolution, output format, and so on

Here is not opened, and ultimately the wheels made so long: https: / / github com/shanghai - edu/multissh

Can be executed directly from the command line, through; The number, or number, ACTS as a separator between the command and the host.


# ./multissh -cmds "show clock" -hosts "192.168.31.21;192.168.15.102" -u admin -p password

Master and command groups can also be stored in text, separated by newline characters.


# ./multissh -cmdfile cmd1.txt.example -hostfile host.txt.example -u admin -p password

In particular, if the input is IP (-ES232en or -ES233en), then the IP address segment is allowed, for example 192.168.15.101-192.168.15.110. (Remember swcollector, something like that)


# ./multissh -cmds "show clock" -ips "192.168.15.101-192.168.15.110" -u admin -p password

Support for ssh key authentication, in which case if password is entered, it is used as the password for key


# ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key"

For linux, the linuxMode mode is supported, which is to combine commands through & & Once connected, run using session.Run ().


# ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key" -l

You can also define different configuration parameters for each host to load the configuration in json format.


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
}

2

Output can be typed as json for easy processing.


# ./multissh -c ssh.json.example -j

You can also store the output in text named after the host name, for example as a configuration backup


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
}

4

Related articles: