I am writing a Go library to represent various networking devices, such as a switch, router, wireless controller, access point, etc, in order to automate configuring these devices. So far, I have a single Device
struct that has a public Host
field and various private fields for handling SSH-specific actions, as well as methods for connecting to a device, sending a set of configuration commands to it, and retrieving the output of the commands. As of now, none of the methods are implemented to be concurrent, mainly because I don't know which methods (if any) would benefit from concurrency.
My problem as a whole, configuring a list of devices over SSH, seems like a good case for using concurrency (not blindly trying to use concurrency to "go fast"), since the process for configuring a single device can be expensive, but I am unsure of where to implement the concurrency in my application and how to synchronize everything (thread safety?). With mutexes, waitgroups, channels, and goroutines, it is a bit confusing for a beginner like me to know where to start. I'd like to at least get a single method working concurrently just to get a better understanding of (idiomatic) concurrency in Go.
Here is my Device
struct and its methods. It is heavily commented for clarity on what I want to accomplish and ideas I have for implementation details.
package device
import (
"golang.org/x/crypto/ssh"
"io"
)
// A Device represents a network device, such as a switch, router, controller, etc.
type Device struct {
Host string // Hostname or IP address
client *ssh.Client // the SSH client connection
session *ssh.Session // the connection to the remote shell
stdin io.WriteCloser // a pipe connected to the remote shell's standard input
stdout io.Reader // a pipe connected to the remote shell's standard output
stderr io.Reader // a pipe connected to the remote shell's standard error
}
// NewDevice constructs a new device with the given hostname or IP address.
func NewDevice(host string) *Device {
return &Device{Host: host}
}
// Connect starts a client connection to the device, starts a remote
// shell, and creates pipes connected to the remote shell's standard input,
// standard output, and standard error.
func (d *Device) Connect(config *ssh.ClientConfig) error {
// TODO: connect to client, start session, setup IO
// Use a goroutine to handle each step? One goroutine for all steps?
return nil
}
// setupIO connects pipes to the remote shell's standard input, output and error.
func (d *Device) setupIO() error {
sshIn, err := d.session.StdinPipe()
if err != nil {
return err
}
d.stdin = sshIn
sshOut, err := d.session.StdoutPipe()
if err != nil {
return err
}
d.stdout = sshOut
sshErr, err := d.session.StderrPipe()
if err != nil {
return err
}
d.stderr = sshErr
return nil
}
// SendConfigSet writes a set of configuration commands to the remote shell's
// standard input then waits for the remote commands to exit.
func (d *Device) SendConfigSet(cmds []string) error {
// TODO: send a set of configuration commands
// Make concurrent? Commands need to be sent in a specific order.
//
// This function will have different setup and cleanup commands
// that will need to be sent depending on a Device's vendor.
// For example, a Cisco device and an HPE device have
// different sets of setup commands needed before sending
// the `cmds` passed to this function, and have different sets of
// cleanup commands that must be sent before exiting.
return nil
}
// sendCmd writes a remote command to the remote shell's standard input
func (d *Device) sendCmd(cmd string) error {
if _, err := d.stdin.Write([]byte(cmd + "
")); err != nil {
return err
}
return nil
}
// Output reads the remote shell's standard output line by line into a
// slice of strings.
func (d *Device) Output() ([]string, error) {
// TODO: read contents of session standard output
// Concurrently read from stdout and send to channel?
// If so, use a local channel or add an output channel to `Device`?
return nil, nil
}
// Output reads the remote shell's standard error line by line into a
// slice of strings.
func (d *Device) Err() ([]string, error) {
// TODO: read contents of session standard error
// Concurrently read from stderr and send to channel?
// If so, use a local channel or add an error channel to `Device`?
return nil, nil
}
func (d *Device) Close() error {
if err := d.stdin.Close(); err != nil {
return err
}
if err := d.session.Close(); err != nil {
return err
}
if err := d.client.Close(); err != nil {
return err
}
return nil
}
Here is an example usage of my device
package:
package main
import (
"fmt"
"github.com/mwalto7/concurrency/device"
"golang.org/x/crypto/ssh"
"strings"
"time"
)
func main() {
var hosts, cmds []string
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{ssh.Password("password")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: time.Second * 5,
}
outputs := make(chan string)
for _, host := range hosts {
go configure(host, cmds, config, outputs)
}
for i := 0; i < len(hosts); i++ {
res := <-outputs
fmt.Println(res)
}
}
func configure(host string, cmds []string, config *ssh.ClientConfig, outputs <-chan string) {
// omitted error handling for brevity
netDev := device.NewDevice(host)
defer netDev.Close()
netDev.Connect(config)
netDev.SendConfigSet(cmds)
out, _ := netDev.Output()
outputs <- strings.Join(out, "
")
}
I am not asking for someone to write this code for me. If you have a code example, great, but I am simply trying to organize implementing concurrency and learn about concurrency in general.