The information provided in this post is for educational and informational purposes only. All rights to the software used belong to their respective owners.

I’ve read quite a few tutorials on Netmiko with GNS3, but none of them show you how to directly utilize the console connection as the management interface for Cisco devices.

In addition to the CiscoIosSSH driver, Kirk Byers also implemented the CiscoIosTelnet driver for establishing a connection to a Cisco device. This is perfect for GNS3 because the default console connection is essentially a Telnet connection without authentication.

What are some of the benefits? Well, for starters, the following are not required:

This drastically reduces the friction for staging labs in GNS3 while providing a simple approach to learning network automation with Python.

Prerequisites

Steps

  1. Open your favorite terminal emulator.

  2. Clone the netmiko-gns3 repo from GitHub.

git clone https://github.com/mweisel/netmiko-gns3.git
  1. Change to the netmiko-gns3 directory.
cd netmiko-gns3
  1. Create a Python virtual environment for Netmiko.

Debian-based Linux distros may require the Python package installer before running the script: sudo apt update && sudo apt install python3-pip

source init_venv.sh
  1. Verify the netmiko package is available.
pip show netmiko

GNS3

Network topology

I am using GNS3 on Apple Silicon , but the following steps are platform agnostic. The OSPF lab is intentionally simple to allow us to focus on the core concepts. The network topology contains two routers:

  • R1 is an emulated Cisco 7200 Series Router using Dynamips .
  • R2 is the current Cisco IOL image.
  1. Open the GNS3 application.

  2. Create a new project.

  3. Drag and drop a c7200 Router from the Devices toolbar onto the Workspace.

  4. Drag and drop an IOL Router from the Devices toolbar onto the Workspace.

  5. Add a link between the f0/0 interface of R1 and e0/0 interface of R2.

  6. Start the devices.

  7. Identify the GNS3 server IP address (or name) in the Topology Summary dock.

GNS3 server address
  1. Identify the Telnet port number for R1 and R2 in the Topology Summary dock.
Device telnet port numbers

Text Editor and Terminal Emulator

  1. Open the netmiko-gns3 project directory in your favorite text editor or IDE.

  2. Open the devices.yaml file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---
- host: R1
  ip: "gns3.orb.local"
  port: 5000
  device_type: cisco_ios_telnet
- host: R2
  ip: "gns3.orb.local"
  port: 5001
  device_type: cisco_ios_telnet
  default_enter: "\n" # IOL

If you’re familiar with Ansible , this file is analogous to the inventory file.

Remember the GNS3 server IP address (or name), and the device Telnet port numbers, from a few steps back? This is where we use those values for the device connection attributes.

Note the default_enter variable on line 10. Netmiko implicitly uses "\r\n" for the characters to send correlated with the enter key. A Cisco IOL device expects"\n".

  1. Open the display_version.py script file.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from concurrent.futures import ThreadPoolExecutor

import yaml
from netmiko import ConnectHandler


def get_version(device):
    try:
        with ConnectHandler(**device) as conn:
            output = conn.send_command("show version")
        return output
    except ConnectionRefusedError as err:
        print(f"{device["host"]}: {err}\n")


if __name__ == "__main__":
    with open("devices.yaml", encoding="utf-8") as f:
        devices = yaml.safe_load(f)
    with ThreadPoolExecutor() as executor:
        results = executor.map(get_version, devices)
        for i, result in enumerate(results, start=1):
            if result is not None:
                print("//", i)
                print(result)

A general outline of what the script does:

  • Import the R1 and R2 routers from the devices.yaml file.
  • Establish concurrent Telnet connections to the routers.
  • Return unstructured output of the Cisco show version command from each router.
  • Print the output for each router.
  1. Run the display_version.py script from the terminal.
python display_version.py

output:

// 1
Cisco IOS Software, 7200 Software (C7200-ADVENTERPRISEK9-M), Version 12.4(24)T5, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2011 by Cisco Systems, Inc.
Compiled Fri 04-Mar-11 06:49 by prod_rel_team

ROM: ROMMON Emulation Microcode
BOOTLDR: 7200 Software (C7200-ADVENTERPRISEK9-M), Version 12.4(24)T5, RELEASE SOFTWARE (fc3)

R1 uptime is 6 hours, 39 minutes
System returned to ROM by unknown reload cause - suspect boot_data[BOOT_COUNT] 0x0, BOOT_COUNT 0, BOOTDATA 19
System image file is "tftp://255.255.255.255/unknown"


This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
[email protected].

Cisco 7206VXR (NPE400) processor (revision A) with 491520K/32768K bytes of memory.
Processor board ID 4279256517
R7000 CPU at 150MHz, Implementation 39, Rev 2.1, 256KB L2 Cache
6 slot VXR midplane, Version 2.1

Last reset from power-on

PCI bus mb0_mb1 (Slots 0, 1, 3 and 5) has a capacity of 600 bandwidth points.
Current configuration on bus mb0_mb1 has a total of 200 bandwidth points.
This configuration is within the PCI bus capacity and is supported.

PCI bus mb2 (Slots 2, 4, 6) has a capacity of 600 bandwidth points.
Current configuration on bus mb2 has a total of 0 bandwidth points
This configuration is within the PCI bus capacity and is supported.

Please refer to the following document "Cisco 7200 Series Port Adaptor
Hardware Configuration Guidelines" on Cisco.com <http://www.cisco.com>
for c7200 bandwidth points oversubscription and usage guidelines.


1 FastEthernet interface
509K bytes of NVRAM.

8192K bytes of Flash internal SIMM (Sector size 256K).
Configuration register is 0x2102

// 2
Cisco IOS Software [IOSXE], Linux Software (X86_64BI_LINUX-ADVENTERPRISEK9-M), Version 17.15.1, RELEASE SOFTWARE (fc4)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2024 by Cisco Systems, Inc.
Compiled Sun 11-Aug-24 22:07 by mcpre

ROM: Bootstrap program is Linux

R2 uptime is 6 hours, 40 minutes
System returned to ROM by unknown reload cause - suspect boot_data[BOOT_COUNT] 0x9, BOOT_COUNT 0, BOOTDATA 19
System image file is "unix:/opt/gns3/images/IOU/x86_64_crb_linux-adventerprisek9-ms.iol"
Last reload reason: Unknown reason



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
[email protected].

Linux Unix (i686) processor with 538788K bytes of memory.
Processor board ID 2045953
8 Ethernet interfaces
8 Serial interfaces
20K bytes of NVRAM.

Configuration register is 0x0
  1. Open the display_router_info.py script file.
 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
from concurrent.futures import ThreadPoolExecutor

import yaml
from netmiko import ConnectHandler


def get_version_and_interfaces(device):
    commands = ["show version", "show interfaces"]
    results = {}
    try:
        with ConnectHandler(**device) as conn:
            for cmd in commands:
                output = conn.send_command(cmd, use_textfsm=True)
                results[cmd] = output
        return results
    except ConnectionRefusedError as err:
        print(f"{device["host"]}: {err}\n")


if __name__ == "__main__":
    with open("devices.yaml", encoding="utf-8") as f:
        devices = yaml.safe_load(f)
    with ThreadPoolExecutor() as executor:
        results = executor.map(get_version_and_interfaces, devices)
        for result in results:
            if result is not None:
                # from pprint import pprint
                # pprint(result)
                ver = result["show version"][0]  # type: ignore
                print(
                    f"{ver["hostname"]} is version {ver["version"]} using the {ver["software_image"]} image."
                )
                first_intf = next(iter(result["show interfaces"]))  # type: ignore
                print(
                    f"The first interface of {ver["hostname"]} is {first_intf["interface"]} ({first_intf["mac_address"]}).\n"
                )

This script combines the power of Netmiko with NTC Templates for structured output. The show version and show interfaces commands will map to the cisco_ios_show_version.textfsm and cisco_ios_show_interfaces.textfsm templates respectively.

A general outline of what the script does:

  • Import the R1 and R2 routers from the devices.yaml file.
  • Establish concurrent Telnet connections to the routers.
  • Return structured output of the Cisco show version and show interfaces commands from each router.
  • Print a couple of statements with f-strings using embedded values from the structured output for each router.
  1. Run the display_router_info.py script from the terminal.
python display_router_info.py

output:

R1 is version 12.4(24)T5 using the C7200-ADVENTERPRISEK9-M image.
The first interface of R1 is FastEthernet0/0 (ca01.0421.0000).

R2 is version 17.15.1 using the X86_64BI_LINUX-ADVENTERPRISEK9-M image.
The first interface of R2 is Ethernet0/0 (aabb.cc00.0100).
  1. Open the configure_devices.py script file.
 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
from concurrent.futures import ThreadPoolExecutor
from difflib import unified_diff
from pathlib import Path

import yaml
from netmiko import ConnectHandler


def set_configuration(device):
    try:
        with ConnectHandler(**device) as conn:
            before_config = conn.send_command("show running-config")
            conn.send_config_from_file(
                f"{Path("configs").resolve()}/{device["host"]}.txt"
            )
            after_config = conn.send_command("show running-config")
            diff = unified_diff(
                before_config.splitlines(),  # type: ignore
                after_config.splitlines(),  # type: ignore
                fromfile=f"{device["host"]}-> before",
                tofile=f"{device["host"]}-> after",
                lineterm="",
            )
        return "\n".join(list(diff))
    except ConnectionRefusedError as err:
        print(f"{device["host"]}: {err}\n")


if __name__ == "__main__":
    with open("devices.yaml", encoding="utf-8") as f:
        devices = yaml.safe_load(f)
    with ThreadPoolExecutor() as executor:
        results = executor.map(set_configuration, devices)
        for result in results:
            if result is not None:
                print(result)

A general outline of what the script does:

  • Import the R1 and R2 routers from the devices.yaml file.
  • Establish concurrent Telnet connections to the routers.
  • Capture the current configuration of the router.
  • Send a list of configuration commands, from a file, to be entered on each of the routers.
  • Capture the modified configuration of the router.
  • Return a diff output of the configuration changes from each router.
  • Print the output for each router.
  1. Run the following from the terminal to display the configuration commands sent to each router:
tail -n +1 configs/R?.txt

output:

==> configs/R1.txt <==
interface Loopback0
ip address 1.1.1.1 255.255.255.255
exit
interface FastEthernet0/0
duplex full
ip address 10.0.0.1 255.255.255.252
no shutdown
exit
router ospf 1
passive-interface Loopback0
exit
interface Loopback0
ip ospf 1 area 0
exit
interface FastEthernet0/0
ip ospf 1 area 0
ip ospf network point-to-point
exit

==> configs/R2.txt <==
ip cef
interface Loopback0
ip address 2.2.2.2 255.255.255.255
exit
interface Ethernet0/0
ip address 10.0.0.2 255.255.255.252
no shutdown
exit
router ospf 1
passive-interface Loopback0
exit
interface Loopback0
ip ospf 1 area 0
exit
interface Ethernet0/0
ip ospf 1 area 0
ip ospf network point-to-point
exit
  1. Run the configure_devices.py script from the terminal.
python configure_devices.py

output:

--- R1-> before
+++ R1-> after
@@ -1,6 +1,6 @@
 Building configuration...

-Current configuration : 901 bytes
+Current configuration : 1107 bytes
 !
 upgrade fpd auto
 version 12.4
@@ -56,10 +56,19 @@
 !
 !
 !
+interface Loopback0
+ ip address 1.1.1.1 255.255.255.255
+ ip ospf 1 area 0
+!
 interface FastEthernet0/0
- no ip address
- shutdown
- duplex half
+ ip address 10.0.0.1 255.255.255.252
+ ip ospf network point-to-point
+ ip ospf 1 area 0
+ duplex full
+!
+router ospf 1
+ log-adjacency-changes
+ passive-interface Loopback0
 !
 ip forward-protocol nd
 no ip http server
--- R2-> before
+++ R2-> after
@@ -1,6 +1,8 @@
 Building configuration...

-Current configuration : 1767 bytes
+Current configuration : 2008 bytes
+!
+! Last configuration change at 12:11:52 PST Sat Dec 14 2024
 !
 version 17.15
 service timestamps debug datetime msec
@@ -29,7 +31,7 @@
 !
 !
 no ip domain lookup
-no ip cef
+ip cef
 login on-success log
 no ipv6 cef
 !
@@ -72,9 +74,14 @@
 !
 !
 !
+interface Loopback0
+ ip address 2.2.2.2 255.255.255.255
+ ip ospf 1 area 0
+!
 interface Ethernet0/0
- no ip address
- shutdown
+ ip address 10.0.0.2 255.255.255.252
+ ip ospf network point-to-point
+ ip ospf 1 area 0
 !
 interface Ethernet0/1
  no ip address
@@ -144,6 +151,9 @@
  shutdown
  serial restart-delay 0
 !
+router ospf 1
+ passive-interface Loopback0
+!
 ip forward-protocol nd
 !
 ip tcp synwait-time 5
  1. Open the display_cdp_ospf_info.py script file.
 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
from concurrent.futures import ThreadPoolExecutor
from pprint import pprint

import yaml
from netmiko import ConnectHandler


def get_cdp_and_ospf(device):
    commands = ["show cdp neighbors detail", "show ip ospf neighbor"]
    results = {}
    try:
        with ConnectHandler(**device) as conn:
            for cmd in commands:
                output = conn.send_command(cmd, use_textfsm=True)
                results[cmd] = output
        return results
    except ConnectionRefusedError as err:
        print(f"{device["host"]}: {err}\n")


if __name__ == "__main__":
    with open("devices.yaml", encoding="utf-8") as f:
        devices = yaml.safe_load(f)
    with ThreadPoolExecutor() as executor:
        results = executor.map(get_cdp_and_ospf, devices)
        for result in results:
            if result is not None:
                pprint(result)

Verify the CDP and OSPF neighborship after the configuration change.

A general outline of what the script does:

  • Import the R1 and R2 routers from the devices.yaml file.
  • Establish concurrent Telnet connections to the routers.
  • Return structured output of the Cisco show cdp neighbors detail and show ip ospf neighbor commands from each router.
  • Print the output for each router.
  1. Run the display_cdp_ospf_info.py script from the terminal.
python display_cdp_ospf_info.py

output:

{'show cdp neighbors detail': [{'capabilities': 'Router',
                                'local_interface': 'FastEthernet0/0',
                                'mgmt_address': '10.0.0.2',
                                'neighbor_description': 'Cisco IOS Software '
                                                        '[IOSXE], Linux '
                                                        'Software '
                                                        '(X86_64BI_LINUX-ADVENTERPRISEK9-M), '
                                                        'Version 17.15.1, '
                                                        'RELEASE SOFTWARE '
                                                        '(fc4)',
                                'neighbor_interface': 'Ethernet0/0',
                                'neighbor_name': 'R2',
                                'platform': 'Linux Unix'}],
 'show ip ospf neighbor': [{'dead_time': '00:00:38',
                            'interface': 'FastEthernet0/0',
                            'ip_address': '10.0.0.2',
                            'neighbor_id': '2.2.2.2',
                            'priority': '0',
                            'state': 'FULL/  -'}]}
{'show cdp neighbors detail': [{'capabilities': 'Router',
                                'local_interface': 'Ethernet0/0',
                                'mgmt_address': '10.0.0.1',
                                'neighbor_description': 'Cisco IOS Software, '
                                                        '7200 Software '
                                                        '(C7200-ADVENTERPRISEK9-M), '
                                                        'Version 12.4(24)T5, '
                                                        'RELEASE SOFTWARE '
                                                        '(fc3)',
                                'neighbor_interface': 'FastEthernet0/0',
                                'neighbor_name': 'R1',
                                'platform': 'Cisco 7206VXR'}],
 'show ip ospf neighbor': [{'dead_time': '00:00:37',
                            'interface': 'Ethernet0/0',
                            'ip_address': '10.0.0.1',
                            'neighbor_id': '1.1.1.1',
                            'priority': '0',
                            'state': 'FULL/  -'}]}
  1. Keep going.

Ideally, we would take this a step further by verifying the configuration and operational state with a Python testing framework.

Pytest with Cisco router