Site icon Karneliuk

From Python to Go 014. Basic SSH Interaction With Network Devices.

Hello my friend,

As mentioned in the previous blogpost, we started talking about practical usage of Python and Go (Golang) for network and IT infrastructure automation. Today we’ll take a look how we can interact with any SSH-speaking device, whether it is a network device, server, or anything else.

You Put So Much Content For Free Online, Why To Join Trainings Then?

Our ultimate goal is to make you successful with software developing for IT infrastructure management. Out blogs are the first step so that you can get up to speed if you already well equipped with fundamentals as protocols, data formats, etc. We believe that sharing is caring, hence we share back our knowledge with you, so that your path could be a little bit easier and quicker, so that you have more time to focus on what matters. If that’s enough for you to move forward, that’s great.

At the same time, if you feel you need more, you want to have finely-curated labs, slack support and deep dive not just in coding, but really in fundamentals, our training programs are here for you:

We offer the following training programs in network automation for you:

During these trainings you will learn the following topics:

Moreover, we put all mentions technologies in the context of real use cases, which our team has solved and are solving in various projects in the service providers, enterprise and data center networks and systems across the Europe and USA. That gives you opportunity to ask questions to understand the solutions in-depth and have discussions about your own projects. And on top of that, each technology is provided with online demos and labs to master your skills thoroughly. Such a mixture creates a unique learning environment, which all students value so much. Join us and unleash your potential.

Start your automation training today.

What Are We Going To Talk Today?

Nowadays, SSH is the most popular protocol to interact with network devices as well as compute nodes, such as servers, virtual machines (VMs) and to a degree even containers. System and network engineers use SSH daily to get data from devices for analysis, for health checking or to perform configuration change. Having such a wide spread, it is no wonder that we put it first to the queue of network management.

Today we are going to discuss:

  1. How to connect to network devices using SSH with Python and Go (Golang)?
  2. How to retrieve operational status?

Explanation

Interaction with network and IT infrastructure devices via SSH meaning opening the interactive terminal session remotely, which gives look and feel as if you are directly connected to the device console. This interactive session lasts for as long as you interact with the devices and involves exchange of data between you / your application and destination node, which raises two important questions to address:

  1. How to ensure that you is you when session is established? In other words, there shall be some authentication and authorization mechanisms in place.
  2. How to ensure that data exchange between your host and node, even if eavesdropped, aren’t altered and/or its content visible to the attacker? This requires some encryption techniques.

Both these issues were not addressed in telnet, which made it very unfavorable for network and IT infrastructure management, although this is typically the first protocol engineers learn to use. And both these issues are addressed in SSH.

Once the SSH session is established between the destination node and your application, free form text is sent back and force. Typically, you send some command and receive some response to them. Albeit seems extremely simple, the important part of this process is to have mechanism, which will detect, when you have received the output in full so that you can send new instruction. Some SSH client have this functionality built-in, whilst others would require you do it yourself.

Join Zero-to-Hero Network Automation Training to learn the details of SSH, which are important for building scalable network and IT automation solutions.

Examples

In this blog we’ll show the basics of SSH interaction. Basics mean that we will actually execute one off commands and collect their output, whilst in the next blog post we will show advanced interaction with network and IT infrastructure devices using SSH.

The today lab’s scenario:

Python

First comes Python as our series is called from Python to Go. Before we can create an appliation in Python, which will be connecting to remote devices using SSH, we need to install corresponding package. We’ll use today paramiko, which is by far the most popular and widely used SSH library. Some other great libraries, such as netmiko is based on paramiko. We’ll also install pyyaml to parse input YAML file:


1
$ pip install pyyaml paramiko

Once libraries are available, we can develop our software with Python:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""From Python to Go: Python: 015 - Basic SSH."""

# Modules
import argparse
import datetime
import os
import sys
import time
import re
from dataclasses import dataclass
from typing import List
import yaml
import paramiko


# Classes
@dataclass
class Credentials:
    """Class to store credentials."""
    username: str
    password: str


@dataclass
class Result:
    """Class to store command execution data."""
    command: str
    output: str
    timestamp: datetime.datetime


class Device:
    """Class to interact with netowrk device."""
    def __init__(self, hostname: str, ip_address: str, credentials: Credentials):
        self.hostname = hostname
        self.ip_address = ip_address
        self.credentials = credentials

        self.results: List[Result] = []

    def execute_command(self, command: str) -> None:
        """Method to execute a command."""
        # Create a new SSH client
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        # Connect to the device
        client.connect(
            self.ip_address,
            username=self.credentials.username,
            password=self.credentials.password,
            look_for_keys=False,
            allow_agent=False,
        )

        # Invoke the session
        session = client.invoke_shell()
        session.recv(65535)

        # Execute the command
        session.send(command + "\n")
        output = ""
        print(f"{regex=}")
        while not regex.search(output):
            time.sleep(.1)
            output += session.recv(65535).decode("utf-8")

        # Store the result
        self.results.append(Result(command, output, datetime.datetime.now()))

        # Close the connection
        session.close()


# Functions
def read_args() -> argparse.Namespace:
    """Helper function to read CLI arguments."""
    parser = argparse.ArgumentParser(description="User input.")
    parser.add_argument("-i", "--inventory", type=str, help="Path to inventory file.")
    return parser.parse_args()


def load_inventory(filename: str, credentials: Credentials) -> List[Device]:
    """Function to load inventory data."""
    # Open file
    try:
        with open(filename, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

    except FileNotFoundError as e:
        print(e)
        sys.exit(1)

    # Populate list of devices
    result = []
    for device in data:
        result.append(Device(credentials=credentials, **device))

    return result


def get_credentials() -> Credentials:
    """Function to get credentials."""
    username = os.getenv("AUTOMATION_USER")
    password = os.getenv("AUTOMATION_PASS")
    return Credentials(username, password)


# Main code
if __name__ == "__main__":
    # Read CLI arguments
    args = read_args()

    # Get credentials
    credentials = get_credentials()

    # Load inventory
    devices = load_inventory(args.inventory, credentials=credentials)

    # Execute command
    for device in devices:
        device.execute_command("show version")

    # Print results
    for device in devices:
        print(f"Device: {device.hostname}")
        for result in device.results:
            print(f"Command: {result.command}", f"Output: {result.output}", f"Timestamp: {result.timestamp}", sep="\n")

This code is long and it is assumes that you have read and practiced all our previous blog posts in “From Python to Go” series or have relevant knowledge already.

We’ll focus our explanation on “Device” class as all other things were already explained:

Break down for this blog post:

  1. All the user inputs (credentials via environment variables and inventory via YAML file) is passed for class Device, which contains method “execute_command” for interacting with network devices.
  2. This method takes a single external argument, which is the command, which shall be executed on the remote device and returns the result of execution.
  3. Within this method, the new SSH client is created via instantiation of object of “SSHClient” class from paramiko library.
  4. Once the client is created, the policy policy is set to automatically add SSH keys for device, which allows to connect to devices for the first time, before its SSH key is known to your host “client.set_missing_host_key_policy(paramiko.AutoAddPolicy())“.
  5. Then the SSH session is established to the remote node (network device, server) using “connect()” method of “SSHClient” class. This method takes as arguments connectivity details, such as target node IP address or FQDN, port, credentials, etc.
  6. After the SSH session is established, you need to open interactive shell using “invoke_shell()” method, which allows you to send and receive information from the endpoint. The interactive shell is a new object, which you need to store.
  7. Finally, you can send and receive messages. Be mindful though that this is a raw SSH session and you need to handle sending and receiving on buffer level, using “send()” and “recv()” methods. For the latter you need to specify how many bytes you want to read from the buffer.
  8. The tricky part is to detect when the output of your command is received in full. In network devices and in servers this is typically signaled with prompt with hostname followed by some special character. So the following approach is taken:
    • Regular expression is created, which is looking for hostname followed by “#” or “>” symbol.
    • This regular expression is capable to read multiline strings.
    • the output is read from buffer and is added to the received data. It is then checked against regular expression and if there is no match, the data read again. It is repeated unlimited amount of time until the pattern is matched.
  9. Then result is sent back to the caller.

Let’s test this application. First of all, here is the inventory:


1
2
3
4
$ cat data/inventory.yaml
---
- hostname: dev-pygnmi-eos-001
  ip_address: 192.168.51.79

Set the credentials to environment:


1
2
$ export AUTOMATION_USER="user"
$ export AUTOMATION_PASS="password"

And execute the Python application:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ python main.py -i ../data/inventory.yaml
Device: dev-pygnmi-eos-001
Command: show version
Output: show version
show version
dev-pygnmi-eos-001>show version
 vEOS
Hardware version:
Serial number:
Hardware MAC address: b239.c742.24ec
System MAC address: b239.c742.24ec

Software image version: 4.26.0.1F
Architecture: i686
Internal build version: 4.26.0.1F-21994874.42601F
Internal build ID: e41b7ab2-f5ed-45cb-ba9c-f320cb81332f

Uptime: 0 weeks, 6 days, 7 hours and 58 minutes
Total memory: 2006640 kB
Free memory: 1188532 kB

dev-pygnmi-eos-001>
Timestamp: 2025-02-22 17:31:21.631414

It works!

Go (Golang)

Same as with Python, we need to install extra package to be able to establish SSH connectivity, which is called “crypto/ssh” and it is also one of the foundational ones. Same as with Python, we install package yaml for parsing YAML files:


1
2
$ go get golang.org/x/crypto/ssh
$ go get gopkg.in/yaml.v3

And here is code for our application written in Go (Golang) to communicate with network devices (switches, routers, firewalls, load balancers) as well as other IT infrastructure devices (servers, virtual machines, containers, etc) using SSH:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/* From Python to Go: Go: 015 - Basic SSH. */

package main

import (
    "bytes"
    "flag"
    "fmt"
    "log"
    "os"
    "time"

    "golang.org/x/crypto/ssh"
    "gopkg.in/yaml.v3"
)

// Imports

// Types and Receivers
type Arguments struct {
    /* Class to starte CLI arguments */
    Inventory string
}
type Crendetials struct {
    /* Struct to store credentials. */
    Username string
    Password string
}

type Result struct {
    /* Struct to store command execution result. */
    Command   string
    Output    string
    Timestamp time.Time
}

type Device struct {
    /* Struct to interact with netowrk device. */
    Hostname    string `yaml:"hostname"`
    IpAddress   string `yaml:"ip_address"`
    Crendetials Crendetials
    Result      []Result
}

func (d *Device) executeCommand(c string) {
    /* Method to execute command */
    interactiveAuth := ssh.KeyboardInteractive(
        func(user, instruction string, questions []string, echos []bool) ([]string, error) {
            answers := make([]string, len(questions))
            for i := range answers {
                answers[i] = (*d).Crendetials.Password
            }

            return answers, nil
        },
    )

    // Create a new SSH client
    sshClientConfig := &ssh.ClientConfig{
        User:            (*d).Crendetials.Username,
        Auth:            []ssh.AuthMethod{interactiveAuth},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    sshClient, err := ssh.Dial("tcp", fmt.Sprintf("%v:22", (*d).IpAddress), sshClientConfig)
    if err != nil {
        log.Fatalln("Failed to dial: ", err)
    }
    defer sshClient.Close()

    // Create session
    session, err := sshClient.NewSession()
    if err != nil {
        log.Fatalln("Failed to open the session: ", err)
    }
    defer session.Close()

    // Execute the command
    buffer := bytes.Buffer{}
    session.Stdout = &buffer
    if err := session.Run(c); err != nil {
        log.Fatalln("Failed to execute command: ", err)
    }

    // Update the result
    (*d).Result = append((*d).Result, Result{
        Command:   c,
        Output:    buffer.String(),
        Timestamp: time.Now(),
    })
}

// Functions
func readArgs() Arguments {
    /* Helper function to read CLI arguments */
    result := Arguments{}

    flag.StringVar(&result.Inventory, "i", "", "Path to the inventory file")

    flag.Parse()

    return result
}

func loadInventory(p string) *[]Device {
    /* Function to load inventory data. */

    // Open file
    bs, err := os.ReadFile(p)
    if err != nil {
        fmt.Println("Get error ", err)
        os.Exit(1)
    }

    // Load inventory
    result := &[]Device{}

    err = yaml.Unmarshal(bs, result)
    if err != nil {
        fmt.Println("Get error ", err)
        os.Exit(1)
    }

    // Return result
    return result
}

func getCredentials() Crendetials {
    /* Function to get credentials. */
    return Crendetials{
        Username: os.Getenv("AUTOMATION_USER"),
        Password: os.Getenv("AUTOMATION_PASS"),
    }
}

// Main
func main() {
    /* Core logic */
    // Read CLI arguments
    cliArgs := readArgs()

    // Get credentials
    sshCreds := getCredentials()

    // Load inventory
    inventory := loadInventory(cliArgs.Inventory)

    // Execute commands
    for i := 0; i < len(*inventory); i++ {
        (*inventory)[i].Crendetials = sshCreds
        (*inventory)[i].executeCommand("show version")
    }

    // Print results
    for i := 0; i < len(*inventory); i++ {
        for j := 0; j < len((*inventory)[i].Result); j++ {
            fmt.Printf(
                "Command: %v\nOutput: %v\nTimestamp: %v\n",
                (*inventory)[i].Result[j].Command,
                (*inventory)[i].Result[j].Output,
                (*inventory)[i].Result[j].Timestamp,
            )
        }
    }
}

As mentioned above, this code is long and it is assumes that you have read and practiced all our previous blog posts in “From Python to Go” series or have relevant knowledge already.

And also repeating here for your convenience, we’ll focus our explanation on “Device” struct as all other things were already explained and repeating them will take a lot of time:

This is how we interact with network device using Go (Golang):

  1. Struct “Device” contains hostname, ip address and credentials to connect to the network device as well as field to store the result of collection. As we initially read it from YAML, some extra instructions such as “yaml:”xxx”” are needed.
  2. The receiver function “executeCommand()” takes one argument with command content. It is applied to the pointer towards struct device, as it requires changing the content of the original struct.
  3. First thing within this receiver function is to create the SSH authentication method. Go (Golang) is a low-level programming language, therefore it is a little bit more involved that Python, where you just specify credentials and that’s it. There are a lot of different authentication mechanisms available such as password, SSH keys, etc. From our experiment, to connect to network devices you need to use function “KeyboardInteractive()” from “crypto/ssh” package.
  4. After than you create struct with the configuration of SSH “ClientConfig“, which contains username, authentication object and HostKeyCallback. The latter is similar to SSH key policy in Python as it controls if you can or cannot connect the devices, which you don’t have SSH key on your system yet.
  5. Then you open SSH channel to the device using “Dial()” function from “crypto/ssh” package, to which arguments you pass IP address, port and your ssh ClientConfig. Don’t forget to auto-close session before exit using “defer” instruction.
  6. Similar to Python, your create interactive session using “NewSession()” receiver function of created SSH channel.
  7. To read data from session, you need first to create buffer. As you receive bytes from wire, you create buffer using “Buffer” struct from Go (Golang) built-in package “bytes“.
  8. You points the Stdout of your session to the memory address (pointer) of the created duffer, so that you can save it for processing.
  9. Finally, using “Run()” receiver function of created session, you sends the command to the device. the output is stored in the buffer defined above. Once the output is received, interactive SSH session is terminated, which is a big difference with Python.

In the previous part (Python) we’ve shown the inventory and set environment variables; as such, here we show only execution:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ go run . -i ../data/inventory.yaml
Command: show version
Output:  vEOS
Hardware version:
Serial number:
Hardware MAC address: b239.c742.24ec
System MAC address: b239.c742.24ec

Software image version: 4.26.0.1F
Architecture: i686
Internal build version: 4.26.0.1F-21994874.42601F
Internal build ID: e41b7ab2-f5ed-45cb-ba9c-f320cb81332f

Uptime: 0 weeks, 6 days, 8 hours and 31 minutes
Total memory: 2006640 kB
Free memory: 1189920 kB


Timestamp: 2025-02-22 18:04:34.242902767 +0000 GMT m=+1.248006951

And it works too.

Lessons in GitHub

You can find the final working versions of the files from this blog at out GitHub page.

Conclusion

As you can see, it is possible to interact with network devices and IT infrastructure using both Python and Go (Golang) and it is not very complicated. However, there are nuances both in Python and in Go (Golang). The fact that “crypto/ssh” closes SSH session after sending command required us to look for a better SSH library and we found one. The good thing, the same library exists both for Python and Go (Golang) with syntax being identical as much as it could be. And in the next blog we will cover it. Take care and good bye.

Support us






P.S.

If you have further questions or you need help with your networks, I’m happy to assist you, just send me message. Also don’t forget to share the article on your social media, if you like it.

BR,

Anton Karneliuk 

Exit mobile version