title: Init/managing concurrent SSH connections
body: <p>I've searched high and low and I'm not sure if I'm using the wrong keywords, but I can't get my head around this. I'm building an app that take in a list of hostnames and connects to these over SSH. It is intended to maintain these connections (and reconnect when disconnects occur). Periodically, my program will take instruction and execute commands to some/all of these hosts.</p>
  
  <p>My issue at the moment is, knowing that you cannot initialise a variable and not use it, I must dynamically create variables for these SSH connections so I can monitor/manage them independently (read/write, reconnect when necessary, etc). With my limited knowledge of go and a tendency to accidentally over-complicate things, the best I've come up with so far is using a struct and appending for each connection and its parameters (hostname, username, password, SSH config details, logfile location, etc).</p>
  
  <p>Currently my dodgy code looks something like this:</p>
  
  <pre><code>package main
  
  import (
      "os"
      "fmt"
      "flag"
      "golang.org/x/crypto/ssh"
      "io"
      "log"
      "strings"
      "time"
      "encoding/xml"
      "bufio"
      "net"
  )
  
  // SSH connection struct
  type SSHConnections struct {
      Host string
      User string
      Password string
      CLILogfile string
      SSHConn ssh.Client
      SSHConfig ssh.ClientConfig
  }
  
  func main() {
      //parse list of addresses
      var ipaddr = []string{"a.b.c.d","e.f.g.h"} //hard-coded for now
  
      //build out SSHConnections struct
  
      for i := 0; i &lt; len(ipaddr); i++ {
          tempsshConfig := &amp;ssh.ClientConfig{
              User:   "administrator",
              Auth: []ssh.AuthMethod{
                  ssh.Password("Password!"),
              },
              HostKeyCallback: ssh.InsecureIgnoreHostKey(), 
          }
          tempsshConfig.Config.Ciphers = append(tempsshConfig.Config.Ciphers, "aes128-cbc")
          var newitem = SSHConnections{
              Host: ipaddr[i],
              User: "administrator",
              Password: "Password!",
              CLILogfile: ipaddr[1],
              SSHConn: ssh.Client,
              SSHConfig: *tempsshConfig,
          }
  
          SSHConnections = append(SSHConnections, newitem)
      }
  </code></pre>
  
  <p>With the above code I get compile errors:</p>
  
  <pre><code>type ssh.Client is not an expression
  type SSHConnections is not an expression
  </code></pre>
  
  <p>After this there are also connection-specific parameters (logfile declaration, additional SSH parameters, the actual SSH connection procedure) I will need to manage, presumably in the same struct as the above. With the above being my current stumbling block, I'm quite clueless how to fix that let alone integrate the below single-connection code into the above.</p>
  
  <pre><code>//extra SSH parameters required before connecting
  sshConfig.Config.Ciphers = append(sshConfig.Config.Ciphers, "aes128-cbc")
  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
  }
  
  //prepare logfiles
  f, ferr := os.OpenFile("outputfile.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
  if ferr != nil {
      panic(ferr)
  }
  defer f.Close() 
  
  //SSH connection procedure
  connection, err := ssh.Dial("tcp", hostname+":22", sshConfig)
  if err != nil {
      log.Fatalf("Failed to dial: %s", err)
  }
  session, err := connection.NewSession()
  handleError(err, true, "Failed to create session: %s")
  sshOut, err := session.StdoutPipe()
  handleError(err, true, "Unable to setup stdin for session: %v")
  sshIn, err := session.StdinPipe()
  handleError(err, true, "Unable to setup stdout for session: %v")
  if err := session.RequestPty("xterm", 0, 200, modes); err != nil {
      session.Close()
      handleError(err, true, "request for pseudo terminal failed: %s")
  }
  if err := session.Shell(); err != nil {
      session.Close()
      handleError(err, true, "request for shell failed: %s")
  }
  </code></pre>
  
  <p>There's a whole load of irrelevant code I've cut out to make my post clearer. I hope I've remotely succeeded in that.</p>
  
  <p>I'm not even sure a struct is what I want, but the dynamism of receiving a variable number of hostnames, and the requirement to control them individually is key. Later, I may need to tear-down connections individually as well, so a structure where they can be manipulated is what I'm looking for.</p>
  
  <p>Any help to point me in the right direction is greatly appreciated. Thanks!</p>
  
  <p>------------- update below this line ---------------</p>
  
  <p>After mu's post below and some googling, my code now looks like this (removed imports):</p>
  
  <pre><code>var connections []SSHConnections
  
  // SSH connection struct
  type SSHConnections struct {
      Host string
      User string
      Password string
      CLILogfilepath string
      CLILogfile *os.File
      SSHConn *ssh.Client
      Session *ssh.Session
      SSHOut  io.Reader
      SSHIn   io.WriteCloser
      SSHConfig *ssh.ClientConfig
      CLIReady string
      SSHConnErr error
      SessionErr  error
      SSHOutErr   error
      SSHInErr    error
      CLILogfileErr   error
      CommandQueue    chan string
  }
  
  func handleError(e error, fatal bool, customMessage ...string) {
      var errorMessage string
      if e != nil {
          if len(customMessage) &gt; 0 {
              errorMessage = strings.Join(customMessage, " ")
          } else {
              errorMessage = "%s"
          }
          if fatal == true {
              log.Fatalf(errorMessage, e)
          } else {
              log.Print(errorMessage, e)
          }
      }
  }
  func writeToFile(f *os.File, err error, inputString string) {
      if _, err = f.WriteString(inputString); err != nil {
          panic(err)
      }   
  }
  
  func connectionWorker(i int) {
      //this function is to monitor SSH connections and reconnect if/when they are terminated
      fmt.Println("goroutine for: "+connections[i].Host)
      for { //not sure if this is even necessary? yet to test
          select {
              case command := &lt;-connections[i].CommandQueue:
                  fmt.Printf("received %v from queue\n", command)
              default:
                  time.Sleep(1000 * time.Millisecond)
          }
  
      }
  }
  
  func main() {
  
      //parse list of addresses
      var ipaddr = []string{"host1","host2"}
  
      //build out SSHConnections struct
      for i := 0; i &lt; len(ipaddr); i++ {
          tempsshConfig := &amp;ssh.ClientConfig{
              User:   "administrator",
              Auth: []ssh.AuthMethod{
                  ssh.Password("Password!"),
              },
              HostKeyCallback: ssh.InsecureIgnoreHostKey(),
          }
          tempsshConfig.Config.Ciphers = append(tempsshConfig.Config.Ciphers, "aes128-cbc")
          var newitem = SSHConnections{
              Host: ipaddr[i],
              User: "administrator",
              Password: "Password!",
              CLILogfilepath: ipaddr[i],
              SSHConfig: tempsshConfig,
          }
  
          connections = append(connections, newitem)
      }
  
      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
      }
  
      //establish connections, check each for relevant 'admin:' readiness
      for i := 0; i &lt; len(connections); i++ {
          connections[i].SSHConn, connections[i].SSHConnErr = ssh.Dial("tcp", connections[i].Host+":22", connections[i].SSHConfig)
          if connections[i].SSHConnErr != nil {
              fmt.Println("Failed to dial: %s")
          } else {
              connections[i].Session, connections[i].SessionErr = connections[i].SSHConn.NewSession()
              handleError(connections[i].SessionErr, true, "Failed to create session: %s")
              connections[i].SSHOut, connections[i].SSHOutErr = connections[i].Session.StdoutPipe()
              handleError(connections[i].SSHOutErr, true, "Unable to setup stdin for session: %v")
              connections[i].SSHIn, connections[i].SSHInErr = connections[i].Session.StdinPipe()
              handleError(connections[i].SSHInErr, true, "Unable to setup stdout for session: %v")
              if err := connections[i].Session.RequestPty("xterm", 0, 200, modes); err != nil {
                  connections[i].Session.Close()
                  handleError(connections[i].SSHInErr, true, "request for pseudo terminal failed: %s")
              }
          }
          if err := connections[i].Session.Shell(); err != nil {
              connections[i].Session.Close()
              handleError(err, true, "request for shell failed: %s")
          }
          //initialise the buffered CommandQueue channel
          connections[i].CommandQueue = make(chan string, 1000)
  
          //prepare logfiles
          connections[i].CLILogfile, connections[i].CLILogfileErr = os.OpenFile(connections[i].CLILogfilepath+".txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
          if connections[i].CLILogfileErr != nil {
              panic(connections[i].CLILogfileErr)
          }
          defer connections[i].CLILogfile.Close()
          writeToFile(connections[i].CLILogfile, connections[i].CLILogfileErr, "testing output to file\r\n")
  
          //monitor/maintain connections, monitor work queue and execute
          go connectionWorker(i)
      }
  }
  </code></pre>
  
  <p>The above is not pretty and not perfect by any stretch, and may also need some tweaking if someone were to copy/paste so please keep that in mind. I'm certain there are lots of bugs that I'll stumble upon with proper testing, but for now it works as intended. My original queries are all answered now, thanks!</p>