【问题标题】:Concurrently Configure Network Devices同时配置网络设备
【发布时间】:2018-08-24 16:27:38
【问题描述】:

我正在编写一个 Go 库来表示各种网络设备,例如交换机、路由器、无线控制器、接入点等,以便自动配置这些设备。到目前为止,我有一个 Device 结构,它有一个公共 Host 字段和各种私有字段,用于处理特定于 SSH 的操作,以及连接到设备、向其发送一组配置命令的方法,以及检索命令的输出。到目前为止,没有一个方法被实现为并发的,主要是因为我不知道哪些方法(如果有的话)会从并发中受益。

作为一个整体,我的问题是通过 SSH 配置设备列表,这似乎是使用并发的一个很好的案例(不要盲目地尝试使用并发来“快速”),因为配置单个设备的过程可能很昂贵,但我不确定在我的应用程序中在哪里实现并发以及如何同步所有内容(线程安全?)。对于互斥锁、等待组、通道和 goroutine,对于像我这样的初学者来说,知道从哪里开始有点令人困惑。我想至少让一个方法同时工作,以便更好地理解 Go 中的(惯用的)并发性。

这是我的Device 结构及其方法。为了清楚说明我想要完成什么以及我对实施细节的想法,它被大量评论。

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 + "\n")); 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
}

这是我的device 包的示例用法:

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, "\n")
}

我并不是要求有人为我编写此代码。如果您有代码示例,那很好,但我只是在尝试组织实现并发并了解一般的并发。 p>

【问题讨论】:

  • 那么,你的问题是什么?
  • @Peter 我的方法可以使用并发实现吗?如果是这样,我的 cmets 是否在正确的轨道上实现并发?

标签: go concurrency parallel-processing


【解决方案1】:

我编写了一个高度并发的应用程序,同时与多个设备通信。我的设备主要是串行通信(请求、响应、请求等),因此对单个设备使用并发不是一种选择。如果这是你想要的,那么这就是给你的。

设备结构

我假设您的设备无法同时处理多个请求。如果可以,您将需要明确表明它们属于哪个请求的答案。如果是后者,请告诉我——我也有这种情况的经验。

关于串行通信的设备最困难的部分是请求与答案的同步。这里有一些示例代码我是如何解决这个问题的:playground, not runnable

免责声明:快速将其复制在一起。希望你能从混乱中看到这个想法。

上面的示例代码使用一个例程来同步对设备的调用,而每个调用在完成之前都等待应答。您可以通过在每次使用连接时使用互斥锁来实现相同的目的。同时,我更喜欢使用互斥锁来实现这一点,因为它可以节省 goroutine 的启动和停止时间——如果操作不当,可能会导致 goroutine 泄漏。

顺便说一句:ConnAdapter 结构是完全并发安全的,可以同时在多个例程中使用。

使用设备结构

从您的代码看来,您启动了应用程序,获取主机列表(从某处),获取 cmd 列表(可能是从某处的每个主机),然后想要在其主机上执行所有命令并等待一切完成了。

如果是这种情况,您在这里处理并发非常好。

我要添加的一件事是超时:playground

【讨论】:

  • 感谢您的详细解答!这很有帮助。基本上,我的程序包含两个文件,一个包含主机名/IP,另一个包含命令列表,每行一个主机/IP/命令。对于主机文件中的每个主机,我想从命令文件向它发送相同的命令集。这些设备可以相互独立配置,这就是为什么我想为每个主机配置使用并发/并行化进程。
  • 听起来和我想象的一样。我认为您不需要我的示例的完整复杂性,但您应该能够从它们中获得有关并发代码的想法。
【解决方案2】:

如果我正在编写这个程序,我会从以序列化方式编写程序开始。让它工作后,考虑如何同时执行这些任务,以便更有效地利用资源。

通过查看代码,考虑不共享 &ssh.ClientConfig 对象,为每个正在运行的 goroutine 创建一个新对象。

此外,您不需要使用通道来打印每个 goroutine 的输出(除非您想以某种方式序列化输出)。在 goroutine 中打印并简化 configure() 函数的签名。

您需要使用 sync.WaitGroup 来等待所有配置完成,以防止程序在所有 goroutine 启动后退出。

【讨论】:

  • 好的,我可以通过删除config 参数来简化configure() 签名。那么,我应该将configure() 的主体分解成更多的函数,以便在单独的 Goroutines 中调用吗?例如,一个 goroutine 到 Connect(),一个到 Output() 等等,然后使用 sync.WaitGroup 同步这些?如果我跨越 N 个主机,我将如何同步所有这些 goroutine? WaitGroup 会跟踪吗?
猜你喜欢
  • 2016-01-11
  • 2018-12-09
  • 1970-01-01
  • 2018-01-29
  • 2022-10-06
  • 2021-02-06
  • 1970-01-01
  • 2014-01-17
  • 1970-01-01
相关资源
最近更新 更多