0
votes

Comment envoyer plusieurs commandes en une seule session mais enregistrer les sorties séparément

Mon code est censé SSH sur un hôte distant (disons Routeurs) et exécuter plusieurs commandes sur l'hôte distant et renvoyer les sorties.

Le code ci-joint est simplifié et comporte trois parties:

  • Fonction Main : lit la liste des commandes puis, en utilisant la fonction ExecCommands , compose / ssh vers un hôte distant pour exécuter les commandes.
  • ExecCommands fonction ExecCommands prend l'adresse IP de l'hôte distant, la liste des commandes et SSH ClientConfig utilisé pour SSH. Ensuite, il se connecte à l'adresse IP et exécute les commandes une par une. À la fin, renvoie la sortie de toutes les commandes dans une seule chaîne
  • Fonction InsecureClientConfig qui ne fait en réalité pas grand-chose sauf la création d'un SSH ClientConfig qui est utilisé pour la fonction ExecCommands

Ce programme fonctionne bien lorsque je veux simplement appliquer certaines commandes ou config et enregistrer le résultat complet. Je veux dire ExecCommands prend le tas de commandes, les pousse toutes vers l'hôte distant et renvoie (ou enregistre) toute la sortie des commandes appliquées dans une chaîne en tant que sortie.

Problème:

  • Je ne peux pas traiter la sortie de chaque commande individuellement. Par exemple, supposons que j'applique CMD1, CMD2, CMD3,… à l'hôte distant # 1 en utilisant la fonction ExecCommands. Puisqu'il me rend la sortie entière dans une chaîne, il est difficile de trouver quelle sortie appartient à quel CMD

Objectif:

  • Modifiez ou ExecCommands fonction ExecCommands de manière à ce qu'elle fournisse une sortie distincte pour chaque commande qu'elle applique. Cela signifie que si pour remote-host#1 il applique 10 commandes, je devrais avoir 10 chaînes distinctes en sortie.

Conditions / Restrictions:

  • Je ne peux pas créer de session supplémentaire pour les commandes et dois appliquer toutes les commandes dans la première session SSH que j'ai créée, c'est-à-dire ne peut pas créer plusieurs sessions et utiliser la Run , Shell , Output , Start dans le package SSH
  • Aucune ré-authentification n'est autorisée. Par exemple, je n'ai qu'un seul mot de passe à usage unique qui peut être utilisé pour tous les hôtes distants.
  • Les hôtes distants ne prennent pas en charge les commandes "echo" similaires à celles que vous avez sous Linux
  • Les hôtes distants ne prennent en charge aucun type d'API

Points:

  • Le focus principal est la fonction ExecCommands . J'ai mis une version simplifiée de l'ensemble du code pour donner une idée
  • J'utilise stdout, err := session.StdoutPipe() pour exécuter plusieurs commandes, ce qui signifie -as pipe - il est seulement possible de lire Reader lorsque le travail est terminé.
  • Une option consiste à utiliser Session.Stdout et Session.Stdin intérieur de la boucle for dans la fonction ExecCommands . Essayé mais sans succès.

Code:

package main

import (
    "errors"
    "fmt"
    "io/ioutil"
    "log"
    "time"

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

func main() {

    // List of the commands should be sent to the devices
    listCMDs := []string{
        "set cli op-command-xml-output on",
        "test routing fib-lookup virtual-router default ip 1.1.1.1",
        "test routing fib-lookup virtual-router default ip 2.2.2.2",
        "show interface ethernet1/1",
        "show interface ethernet1/2",
        "test security-policy-match protocol 6 source 1.1.1.1 destination 2.2.2.2 destination-port 443 from ZONE1 to ZONE2",
        "test security-policy-match protocol 6 source 10.0.0.1 destination 10.0.2.1 destination-port 443 from ZONE1 to ZONE2",
        "exit",
    }

    sshconfig := InsecureClientConfig("admin", "admin")

    s, err := ExecCommands("192.168.1.250", listCMDs, sshconfig)
    fmt.Println(s, err)
}

// ExecCommands ...
func ExecCommands(ipAddr string, commands []string, sshconfig *ssh.ClientConfig) (string, error) {

    // Gets IP, credentials and config/commands, SSH Config (Timeout, Ciphers, ...) and returns
    // output of the device as "string" and an error. If error == nil, means program was able to SSH with no issue

    // Creating outerr as Output Error.
    outerr := errors.New("nil")
    outerr = nil

    // Creating Output as String
    var outputStr string

    // Dial to the remote-host
    client, err := ssh.Dial("tcp", ipAddr+":22", sshconfig)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Create sesssion
    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    // StdinPipee() returns a pipe that will be connected to the remote command's standard input when the command starts.
    // StdoutPipe() returns a pipe that will be connected to the remote command's standard output when the command starts.
    stdin, err := session.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }

    stdout, err := session.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }

    // Start remote shell
    err = session.Shell()
    if err != nil {
        log.Fatal(err)
    }

    // Send the commands to the remotehost one by one.
    for _, cmd := range commands {
        _, err := stdin.Write([]byte(cmd + "\n"))
        if err != nil {
            log.Fatal(err)
        }
    }

    // Wait for session to finish
    err = session.Wait()
    if err != nil {
        log.Fatal(err)
    }

    strByte, _ := ioutil.ReadAll(stdout)
    outputStr = string(strByte)

    return outputStr, outerr
}

// InsecureClientConfig ...
func InsecureClientConfig(userStr, passStr string) *ssh.ClientConfig {

    SSHconfig := &ssh.ClientConfig{
        User:    userStr,
        Timeout: 5 * time.Second,
        Auth:    []ssh.AuthMethod{ssh.Password(passStr)},

        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Config: ssh.Config{
            Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-cbc", "aes192-cbc",
                "aes256-cbc", "3des-cbc", "des-cbc"},
            KeyExchanges: []string{"diffie-hellman-group1-sha1",
                "diffie-hellman-group-exchange-sha1",
                "diffie-hellman-group14-sha1"},
        },
    }
    return SSHconfig
}


10 commentaires

IIRC, une nouvelle session n'aura pas besoin de réauthentification. Seul le cadran nécessite une authentification.


Si vous devez les exécuter dans un seul shell, vous êtes limité par ce que ce shell peut faire. Il n'y a rien que ssh ou Go puisse faire pour corriger la sortie de ce shell distant. Pouvez-vous faire écho à un délimiteur pour le flux de sortie séparé la sortie de chaque commande?


@Burak Serdar. Merci pour la réponse rapide. cela vous dérangerait-il de me donner un exemple? La dernière fois que j'ai essayé n'a pas réussi


@JimB. Merci à vous aussi pour la réponse rapide. Je n'ai pas suivi exactement ce que tu veux dire


Une session est censée exécuter une commande. Vous pouvez créer une session, exécuter une commande, obtenir la réponse et créer une autre session pour la suivante. Quelle erreur avez-vous obtenu? Le seul exemple que je peux fournir est dans ce projet que j'ai écrit il y a quelque temps, qui fait en fait ce dont vous avez besoin: ssh dans de nombreux hôtes et faire des choses dessus: github.com/bserdar/watermelon/blob/master/server/backends / ... (c'est assez gros, mais cela crée une connexion et plusieurs sessions)


@Burak Serdar C'est ce que je comprends: session1, err := client.NewSession() puis session1.Shell() puis session1.Output("COMMAND") enfin session1.Wait() Et même pour session2, session3 Si c'est le cas, après l'exécution de session1.Output je reçois Stdout already set ce Stdout already set


Aussi j'ai juste essayé ceci: session1, err := client.NewSession() puis session1.Run("COMMAND") et enregistrer la sortie dans un Buffer var b1 bytes.Buffer et session1.Stdout = &b1 Et même pour session2, session3 mais pas de chance :(


@Deibedyn: quelle partie ne comprenez-vous pas? Vous interagissez avec un seul shell et n'avez donc qu'un seul flux de sortie. Si vous avez besoin de séparer la sortie de chaque commande, pouvez-vous délimiter la sortie de l'intérieur du shell?


@JimB Merci d'avoir essayé de vous aider. Compris. Pas sûr que je puisse mettre n'importe quel caractère ou quoi que ce soit d'autre pour délimiter la sortie. Le fait est que de nombreuses commandes ont une sortie similaire (quel processus est en cours d'exécution, quelle est la route vers cette adresse IP, etc.) J'ai essayé d'utiliser une session différente [ play.golang.org/p/HYbLcrAlNz6] et ici [ play.golang.org/ p / c3anBjdKM9c] en utilisant la commande Run et Output . Pas de chance


@ mh-cbon Merci pour votre commentaire. Le fait est que presque aucun des périphériques réseau - comme illustré dans ma question, ne prend pas en charge l'écho


3 Réponses :


-1
votes

Vérifiez ceci: https://github.com/yahoo/vssh Vous pouvez définir les sessions sur le nombre de commandes à exécuter simultanément, puis envoyer chaque commande à l'hôte distant via la méthode d'exécution et obtenir le résultat individuellement!


3 commentaires

Merci @Mehrdad pour cela. Êtes-vous sûr qu'il prend en charge plusieurs commandes par session? Dans la documentation, la méthode run ressemble à une seule commande?


il crée une connexion puis pour chaque exécution, il crée une session. (vous devez définir le nombre maximum de sessions)


Merci. essaiera de vous faire savoir quel est le résultat



0
votes

Cela fonctionne correctement:

package main

import (
    "bufio"
    "errors"
    "fmt"
    "log"
    "time"

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

func main() {

    // List of the commands should be sent to the devices
    listCMDs := []string{
        "set cli op-command-xml-output on\n",
        "test routing fib-lookup virtual-router default ip 1.1.1.1\n",
        "test routing fib-lookup virtual-router default ip 2.2.2.2\n",
        "show interface ethernet1/1\n",
        "show interface ethernet1/2\n",
        "test security-policy-match protocol 6 source 1.1.1.1 destination 2.2.2.2 destination-port 443 from ZONE1 to ZONE2\n",
        "test security-policy-match protocol 6 source 10.0.0.1 destination 10.0.2.1 destination-port 443 from ZONE1 to ZONE2\n",
        "exit",
    }

    sshconfig := InsecureClientConfig("admin", "Ghazanfar1!")

    s, _ := ExecCommands("192.168.1.249", listCMDs, sshconfig)

    for _, item := range s {
        fmt.Println(item)
        fmt.Println("-------------------------------")
    }
}

// ExecCommands ...
func ExecCommands(ipAddr string, commands []string, sshconfig *ssh.ClientConfig) ([]string, error) {

    // Gets IP, credentials and config/commands, SSH Config (Timeout, Ciphers, ...) and returns
    // output of the device as "string" and an error. If error == nil, means program was able to SSH with no issue

    // Creating outerr as Output Error.
    outerr := errors.New("nil")
    outerr = nil

    // Creating Output as String
    var outputStr []string
    var strTmp string

    // Dial to the remote-host
    client, err := ssh.Dial("tcp", ipAddr+":22", sshconfig)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Create sesssion
    session, err := client.NewSession()
    if err != nil {
        log.Fatal(err)
    }
    defer session.Close()

    // StdinPipee() returns a pipe that will be connected to the remote command's standard input when the command starts.
    // StdoutPipe() returns a pipe that will be connected to the remote command's standard output when the command starts.
    stdin, err := session.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }

    stdout, err := session.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }

    // Start remote shell
    err = session.Shell()
    if err != nil {
        log.Fatal(err)
    }

    stdinLines := make(chan string)
    go func() {
        scanner := bufio.NewScanner(stdout)
        for scanner.Scan() {
            stdinLines <- scanner.Text()
        }
        if err := scanner.Err(); err != nil {
            log.Printf("scanner failed: %v", err)
        }
        close(stdinLines)
    }()

    // Send the commands to the remotehost one by one.
    for i, cmd := range commands {
        _, err := stdin.Write([]byte(cmd + "\n"))
        if err != nil {
            log.Fatal(err)
        }
        if i == len(commands)-1 {
            _ = stdin.Close() // send eof
        }

        // wait for command to complete
        // we'll assume the moment we've gone 1 secs w/o any output that our command is done
        timer := time.NewTimer(0)
    InputLoop:
        for {
            timer.Reset(time.Second)
            select {
            case line, ok := <-stdinLines:
                if !ok {
                    log.Println("Finished processing")
                    break InputLoop
                }
                strTmp += line
                strTmp += "\n"
            case <-timer.C:
                break InputLoop
            }
        }
        outputStr = append(outputStr, strTmp)
        //log.Printf("Finished processing %v\n", cmd)
        strTmp = ""
    }

    // Wait for session to finish
    err = session.Wait()
    if err != nil {
        log.Fatal(err)
    }

    return outputStr, outerr
}

// InsecureClientConfig ...
func InsecureClientConfig(userStr, passStr string) *ssh.ClientConfig {

    SSHconfig := &ssh.ClientConfig{
        User:    userStr,
        Timeout: 5 * time.Second,
        Auth:    []ssh.AuthMethod{ssh.Password(passStr)},

        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Config: ssh.Config{
            Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-cbc", "aes192-cbc",
                "aes256-cbc", "3des-cbc", "des-cbc"},
            KeyExchanges: []string{"diffie-hellman-group1-sha1",
                "diffie-hellman-group-exchange-sha1",
                "diffie-hellman-group14-sha1"},
        },
    }
    return SSHconfig
}


0 commentaires

0
votes

Puisque vous avez un nombre limité de commandes à exécuter sur des matériels spéciaux et que vous connaissez le modèle de sortie de chaque commande, vous pouvez utiliser des strings.Split ou regexp pour diviser la sortie.
Et si vous n'avez pas de commande echo , mais que vous connaissez une commande avec une réponse rapide avec un modèle de sortie unique, vous pouvez la remplacer par la commande echo dans l'exemple suivant (numéro 2).


Depuis une session accepte un seul appel à Run , Start , Shell , Output ou CombinedOutput , et vous ne voulez pas commencer une nouvelle session par commande:

La clé est d'utiliser un strings.Builder et de le vider en utilisant sb.Reset() avant d'envoyer la commande, et en utilisant io.Copy pour copier simultanément le stdout de la session dans strings.Builder (en supposant que vous n'avez pas besoin de stderr de session):

sb := new(strings.Builder)
go io.Copy(sb, stdout)
commands := []string{"uname -a", "sleep 1", "pwd", "whoami"}
delim := "********--------========12345678"
for _, cmd := range commands {
    _, err = stdin.Write([]byte("echo " + delim + "\n"))
    if err != nil {
        log.Fatal(err)
    }
    _, err := stdin.Write([]byte(cmd + "\n"))
    if err != nil {
        log.Fatal(err)
    }
}
_, err = stdin.Write([]byte("exit\n"))
if err != nil {
    log.Fatal(err)
}
err = session.Wait() // Wait for session to exit
if err != nil {
    log.Fatal(err)
}
ans := strings.Split(sb.String(), delim)
ans = ans[1:] // remove ssh greetings
  1. Cela fonctionne si vous savez combien attendre chaque commande (testée):
sb := new(strings.Builder)
go io.Copy(sb, stdout)

commands := []string{"uname -a", "sleep 1", "pwd", "whoami", "exit"}
wait := []time.Duration{10, 1200, 20, 10, 10} // * time.Millisecond
ans := []string{}

time.Sleep(10 * time.Millisecond) // wait for the ssh greetings

// Send the commands to the remotehost one by one.
for i, cmd := range commands {
    sb.Reset()
    fmt.Println("*** command:\t", cmd)
    _, err := stdin.Write([]byte(cmd + "\n"))
    if err != nil {
        log.Fatal(err)
    }
    time.Sleep(wait[i] * time.Millisecond) // wait for the command to finish
    s := sb.String()
    fmt.Println("*** response:\t", s)
    ans = append(ans, s)
}

  1. Utilisation du délimiteur de chaîne et des strings.Split (Remarque: vous pouvez remplacer echo par n'importe quelle commande rapide avec un modèle de sortie connu):
sb := new(strings.Builder)
go io.Copy(sb, stdout)


0 commentaires