Mantra Networking Mantra Networking

Netmiko: Deep Dive

Netmiko: Deep Dive
Created By: Lauren Garcia

Table of Contents

  • Overview
  • Core Components
  • Installation and Setup
  • Basic Usage Example
  • Advanced Use Cases
  • Troubleshooting and Best Practices
  • Common Examples
  • Configuration Guide
  • Conclusion

Netmiko: Overview, Importance, and How It Works

What is Netmiko?

Netmiko is an open-source Python library designed to simplify and automate the process of connecting to and managing network devices over SSH. It’s built on top of the Paramiko library and provides a user-friendly interface for network engineers and automation professionals to interact with routers, switches, firewalls, and other network equipment from various vendors.

Why You Need to Know About Netmiko

  • Network Automation: Netmiko is a foundational tool for anyone interested in automating network tasks, such as configuration changes, device monitoring, or collecting data from network devices.
  • Multi-Vendor Support: It abstracts away the differences between device vendors, allowing you to use a consistent set of commands and methods with devices from Cisco, Juniper, Arista, HP, and many others.
  • Efficiency and Consistency: Automating repetitive tasks with Netmiko reduces the risk of human error and saves significant time, especially in large-scale environments.
  • Integration: It integrates well with other Python libraries and automation frameworks, making it a flexible choice for custom scripts and larger automation projects.

How Netmiko Works

  • SSH Connection Management: Netmiko manages the underlying SSH connections to network devices, handling authentication, session management, and command execution.
  • Device Abstraction: When you specify the device type (e.g., "cisco_ios" or "juniper"), Netmiko adapts its behavior to match the quirks and requirements of that platform, such as handling login banners, CLI prompts, and mode transitions.
  • Command Execution: You can send commands to devices and receive output as if you were typing directly into the device’s CLI. Netmiko can send single commands or sequences of commands for configuration changes.
  • Error Handling: The library includes built-in mechanisms for dealing with timeouts, authentication failures, unexpected prompts, and other common issues encountered during network automation.
  • Output Parsing: Netmiko can be used with parsing libraries like TextFSM or Genie to convert raw command output into structured data for easier analysis and reporting.
Netmiko is a vital tool for modern network automation, streamlining the process of managing diverse network environments and enabling scalable, reliable, and efficient automation workflows.

When Should You Use Netmiko?

Deciding when to use Netmiko versus an API-driven approach is essential for effective and future-proof network automation.

  • Prefer APIs When Available: Most modern network devices support RESTful APIs, NETCONF, or gRPC interfaces, which provide:
    • Consistent, structured data (JSON, XML)
    • Improved error handling and status reporting
    • Support for bulk or transactional operations

    Best Practice: When your network device offers a robust, documented API, always use the API. APIs are generally faster, safer, and more stable than CLI screen-scraping and are less likely to break with operating system changes.

  • Netmiko for CLI/SSH-Only Devices: Many legacy, proprietary, or budget network devices only support SSH access for automation—no API at all.
    • Netmiko leverages SSH to automate interaction with CLI-only devices.
    • It’s indispensable when you must manage older hardware or platforms with limited programmability.

    Use Netmiko when:

    • Your device does not offer an API (REST, NETCONF, etc.).
    • APIs exist but are insufficient or unsupported for your scenario.
    • Your workflow requires sending CLI commands over standard SSH.

  • Hybrid Scenarios: In mixed environments, you can combine API-based and CLI-based automation. Use APIs where possible and rely on Netmiko for legacy or outlier devices.

Practical Workflow:

  1. Check device documentation for API/NETCONF support.
  2. Use APIs by default when available and appropriate.
  3. Fallback on Netmiko for CLI/SSH-only devices or special scenarios.
Scenario Recommended Approach
Modern device with API Use API/NETCONF/REST
Legacy or no API device Use Netmiko (SSH/CLI)
Partial API coverage Combine API + Netmiko

Summary: Favor APIs for their reliability and structure, but Netmiko is the go-to tool for CLI/SSH-only or legacy devices. This flexible strategy ensures you can automate any network.

Reference: Paramiko, Netmiko, NAPALM, or Nornir?

Core Components of Netmiko

Netmiko’s overall architecture is built with modular core components, each serving a dedicated role in network device automation. Below are the essential components, followed by practical inspiration for integrating Jinja2 templating:

  • BaseConnection Class: The foundation for device communication. It manages low-level SSH channel details, command execution, prompt detection, and session management for all device types.
  • Platform-Specific Classes: Subclasses (e.g., CiscoIosSSH, AristaEOSSSH, JuniperJunosSSH) that extend BaseConnection to handle device-specific CLI quirks for each vendor and platform.
  • ConnectHandler: A top-level factory function evaluating device dictionary parameters and instantiating the correct platform-specific connection object, simplifying multi-vendor automation.
  • Command Execution Methods: Includes key functions such as send_command, send_command_expect, send_config_set, and send_config_from_file for operational and configuration command processing.
  • File Transfer Tools: Features like FileTransfer and related helpers abstract secure transfer (SCP/SFTP) operations for tasks like software image or bulk configuration distribution.
  • SSH Channel Abstraction: Uses paramiko or ssh2-python libraries underneath, abstracted by Netmiko for uniform connection management and resilience.
  • Dispatcher Functions: Utilities like redispatch allow dynamic device-type switching or jump host handling, enabling complex workflows and session reuse.
  • Utility Modules: Robust support for error handling, device prompt recovery, retries, timeouts, and command verification (e.g., find_prompt, check_config_mode).
BaseConnection Class

BaseConnection is the foundational class at the heart of Netmiko’s architecture. It abstracts and manages the complex, repetitive interactions needed to automate network devices over SSH (and sometimes Telnet), allowing higher-level scripts and platform drivers to focus on business logic rather than low-level protocol details.

What Does BaseConnection Do?

  • Session Establishment: Handles all aspects of opening an SSH/Telnet channel to the network device, managing authentication, connection timeouts, privilege elevation (enable mode), and session reuse.
  • Prompt Detection & Management: Continuously monitors the device prompt for synchronization, ensuring automation scripts remain context-aware and robust against CLI quirks.
  • Command Execution: Provides the backbone for the main Netmiko command methods (send_command, send_config_set, etc.), handling buffering, parsing, error and exception management, and device output collection.
  • Privilege Escalation & Mode Switching: Implements methods to move between user and privileged exec modes as required by device operations (enable(), check_enable_mode()).
  • Session Cleanup & Exception Handling: Safely closes out connections—handling interrupted sessions or exceptions gracefully to prevent device resource exhaustion.
  • Device Abstraction: Provides a consistent, normalized Python API so platform-specific subclasses only need to handle unique CLI behaviors.

Most Important Initialization Parameters

Parameter Description Example
device_type Identifies the target network OS/platform. cisco_ios
host Target device IP or DNS name. 192.0.2.10
username CLI username for authentication. admin
password Password for device authentication. s3cr3t
secret Enable/privileged-mode password (if needed). enablepass
port SSH port to connect to (default 22). 22
timeout Timeout (in seconds) for session establishment. 100

Basic Usage Example

from netmiko import ConnectHandler

device = {
    'device_type': 'cisco_ios',
    'host': '192.0.2.10',
    'username': 'admin',
    'password': 's3cr3t',
    'secret': 'enablepass',
    'port': 22,
    'timeout': 100,
}

net_connect = ConnectHandler(**device)   # Instantiates proper BaseConnection subclass
net_connect.enable()                    # Enter privilege mode

# Run commands
print(net_connect.send_command('show version'))

# Configure device
commands = [
    'interface Loopback123',
    'description Managed by automation',
]
net_connect.send_config_set(commands)

net_connect.disconnect()
  

Critical Methods in BaseConnection

  • send_command(command, ...): Sends a CLI command and collects the response. Supports tuning output parsing (expect_string), timeouts, and more.
  • send_config_set(commands): Enters configuration mode, sends a set of config commands (single string or Python list), and returns aggregate output.
  • enable(): Elevates session to privileged exec mode if device supports it.
  • find_prompt(): Returns the device’s current CLI prompt—a core part of context awareness.
  • disconnect(): Closes the SSH/Telnet session gracefully.
  • read_channel(), write_channel(): Exposes direct access to the underlying transport for advanced/tailored interactions.

Practical Tips & Best Practices

  • Always ensure disconnect() is called to free resources and avoid orphaned sessions.
  • Wrap Netmiko activities in try/finally blocks for cleaner error handling:
try:
    net_connect = ConnectHandler(**device)
    print(net_connect.send_command('show interfaces'))
finally:
    net_connect.disconnect()
  
  • Adjust timeout and conn_timeout for slower devices or commands with large outputs.
  • For platform-specific quirks, subclass BaseConnection and override methods like session_preparation and send_command.

Advanced: Custom Session Preparation Example

from netmiko.base_connection import BaseConnection

class CustomOSConnection(BaseConnection):
    def session_preparation(self):
        self._test_channel_read()
        self.set_base_prompt()
        self.clear_buffer()
        # Add custom device handling here
  

References

Platform-Specific Classes

Platform-Specific Classes extend Netmiko’s BaseConnection to support the CLI quirks, authentication, and operational behaviors unique to each network operating system. Every major vendor and device platform (Cisco, Arista, Juniper, HP, Dell, etc.) is represented by its own specialized subclass, allowing Netmiko to "speak the native language" of each platform and deliver seamless multi-vendor automation.

Role & Structure of Platform-Specific Classes

  • Inheritance: Each platform class (e.g., CiscoIosSSH, AristaEOSSSH, JuniperJunosSSH) inherits from BaseConnection, adding device-specific logic on top of the shared transport, session management, and command handling.
  • Device Quirk Handling: Implements or overrides methods to handle:
    • Prompt detection and variations (for different CLI levels/modes)
    • Entering/exiting/from configuration modes or sub-modes
    • Privilege escalation and enable/disable procedures
    • CLI pagination handling (e.g., "--More--" prompts)
    • Command output parsing and unique error messages
  • Multi-Protocol Support: May implement SSH-only or also Telnet, Serial, or Console methods—using class naming conventions to indicate transport (e.g., CiscoIosSerial).
  • Parameter/Feature Support: Adds or changes arguments relevant to that platform (e.g., special authentication flows, custom secrets, additional modes).

Example: Cisco IOS vs Juniper Junos

  • Cisco IOS:
    • Class: CiscoIosSSH
    • Prompt patterns: Detects both user EXEC (">") and privileged EXEC ("#") mode
    • Privilege escalation: Uses enable(), supports secret
    • Config mode logic: Navigates "conf t", "line", "interface", etc.
  • Juniper Junos:
    • Class: JuniperJunosSSH
    • Prompt: Supports operational (">") and configuration ("#") modes
    • Config mode logic: Uses "configure", "edit", "commit", "exit"
    • Does not use "enable"—has different privilege escalation

Usage Example – Multi-Platform Script

from netmiko import ConnectHandler

devices = [
    {
        'device_type': 'cisco_ios',
        'host': '192.0.2.100',
        'username': 'admin',
        'password': 'ciscoPW',
        'secret': 'enablePW',
    },
    {
        'device_type': 'arista_eos',
        'host': '192.0.2.200',
        'username': 'admin',
        'password': 'aristaPW',
    },
    {
        'device_type': 'juniper_junos',
        'host': '192.0.2.201',
        'username': 'netops',
        'password': 'juniperPW',
    },
]

for dev in devices:
    net_connect = ConnectHandler(**dev)
    print(f"Connected to {dev['host']} as {dev['device_type']}")
    output = net_connect.send_command('show version')
    print(output)
    if dev['device_type'] == 'cisco_ios':
        net_connect.enable()  # Privilege escalation only for Cisco IOS
        net_connect.send_config_set([
            'interface Loopback124',
            'description Platform-specific automation',
        ])
    elif dev['device_type'] == 'juniper_junos':
        net_connect.config_mode()  # Enters 'configure' mode for Junos
        net_connect.send_config_set([
            'set system host-name Automated-Junos',
        ])
        net_connect.exit_config_mode()  # Commits and exits config mode
    net_connect.disconnect()
  

How Are Platform-Specific Classes Registered?

  • Device dictionary's device_type parameter (e.g., "cisco_ios") is matched to its class (CiscoIosSSH) via Netmiko’s SSH Dispatcher (ssh_dispatcher.py).
  • New platforms can be added by subclassing BaseConnection and registering the new class to a string name in the dispatcher for use in the device dictionary.

Customizing for Unsupported Platforms

  • Subclass BaseConnection, implement custom prompt patterns, config mode transitions, and error handling as needed.
  • Register your subclass under a unique device_type string so it can be used in ConnectHandler.
  • Refer to existing implementations (e.g., cisco_ios, arista_eos) for patterns and best practices.

Example: Creating a Custom Platform-Specific Class

from netmiko.base_connection import BaseConnection

class VendorXosSSH(BaseConnection):
    def session_preparation(self):
        self._test_channel_read()
        self.set_base_prompt()
        self.clear_buffer()  # Custom prep for XOS
    
    def check_enable_mode(self):
        # If XOS has its own privilege model, customize here
        pass

    def enable(self):
        # Custom enable logic if needed
        pass

    # Implement/override other methods as needed

# Then register 'vendor_xos' with the dispatcher for usage in ConnectHandler
  

Best Practices

  • Use clear, descriptive device_type names in your inventory files for clarity and automation resilience.
  • Test platform-specific features and privilege logic before using at scale.
  • For advanced troubleshooting (e.g., new prompts or CLI quirks after a device OS upgrade), review and override methods in your subclass as needed.
  • Contribute back to Netmiko if you build support for a new platform – the community benefits, and so do you!

References

ConnectHandler

ConnectHandler is Netmiko’s high-level convenience function and factory that abstracts away the complexity of selecting and instantiating the correct platform-specific connection class. Most scripts and automation workflows use ConnectHandler as their single entry-point for establishing SSH (or Telnet) connections to devices, making it both powerful and extremely user-friendly.

What Does ConnectHandler Do?

  • Device Abstraction and Class Selection: Takes a device dictionary of parameters (platform, credentials, IP/hostname, and options) and automatically determines which Netmiko platform-specific connection class to instantiate (e.g., CiscoIosSSH, AristaEOSSSH).
  • Parameter Validation: Checks required fields for the target device and raises helpful errors if parameters are missing or platform is unsupported.
  • Unified Interface: Always returns an object conforming to the BaseConnection API—regardless of vendor, so subsequent code does not need to handle platform-specific logic.
  • Extensibility: Custom classes and platforms can be registered, and ConnectHandler will use them when matched by device_type.

ConnectHandler Workflow Diagram

  1. Accept device dictionary as input (with at minimum, device_type, host, username, password).
  2. Looks up the correct connection class based on device_type.
  3. Instantiates the connection object, establishing an SSH/Telnet session.
  4. Returns this connection object for all script activities.

Basic Usage Example

from netmiko import ConnectHandler

device = {
    'device_type': 'cisco_ios',
    'host': '192.0.2.101',
    'username': 'admin',
    'password': 'myPassword',
    'secret': 'enablepw',
    'port': 22,
}

net_connect = ConnectHandler(**device)
output = net_connect.send_command('show ip interface brief')
print(output)

net_connect.disconnect()
  

Multi-Vendor Example

devices = [
    {'device_type': 'cisco_ios', 'host': '192.0.2.11', 'username': 'net', 'password': 'pw', 'secret': 'enable'},
    {'device_type': 'arista_eos', 'host': '192.0.2.12', 'username': 'net', 'password': 'pw'},
]

for dev in devices:
    with ConnectHandler(**dev) as net_connect:
        print(f"{dev['host']}:")
        print(net_connect.send_command('show version'))
  

Supported Parameters

Essential: device_type, host, username, password
Optional/Platform Specific: secret (enable mode), port, timeout, global_delay_factor, proxy/jumphost support, and others as documented per driver.

Handling Sessions and Exceptions

  • It’s recommended to use either a try/finally block or Python’s with statement:
    from netmiko import ConnectHandler
    
    try:
        connection = ConnectHandler(**device)
        print(connection.send_command('show clock'))
    finally:
        connection.disconnect()
          
    Or, using context manager syntax (since Netmiko v3.3.0):
    with ConnectHandler(**device) as conn:
        print(conn.send_command('show clock'))
          

Advanced: Using Custom Platform Classes

  • If you have implemented a custom connection class, register it with Netmiko’s dispatcher and specify your device_type in the device dictionary. ConnectHandler will use your class automatically.
  • This makes Netmiko extensible for in-house or emerging platforms without changing your automation code patterns.

Common Pitfalls and Best Practices

  • Double check the device_type string! A typo here will cause a lookup error.
  • Always disconnect (or use with) to avoid leaving open sessions.
  • For non-standard SSH ports use the port parameter.
  • If your platform is not supported, consider writing a custom class as detailed in the Platform-Specific section.
  • Use global_delay_factor to adjust Netmiko’s internal response timing for slow or "chatty" devices.

References

Command Execution Methods

Command Execution Methods are at the heart of what makes Netmiko valuable to network automation. These methods securely execute show/operational or configuration commands on remote network devices, handle CLI quirks, collect and parse outputs, and facilitate both basic and advanced workflows (including configuration file uploads). They abstract away tedious channel handling and error resilience, allowing you to focus on orchestration logic rather than CLI idiosyncrasies.

Main Command Methods

  • send_command() — Execute a single show/operational command and retrieve its output (as a string).
  • send_command_expect() — Like send_command(), but allows custom expectations for command output (prompt or pattern).
  • send_config_set() — Enter configuration mode (as required), send one or more config commands, and exit, all in a transaction.
  • send_config_from_file() — Load commands from a local file and send as a config set.
  • send_multiline() (advanced) — Send multiline commands or blocks as a single logical transaction.
  • send_command_timing() — Like send_command() but uses fixed timeouts, useful for oddball CLIs with non-standard prompts.

Basic Usage Examples

  • Show Command:
    from netmiko import ConnectHandler
    
    net_connect = ConnectHandler(**device)
    output = net_connect.send_command('show ip interface brief')
    print(output)
    net_connect.disconnect()
          
  • Multiple Configuration Commands:
    (Automatically handles entering/exiting config mode)
    commands = [
        'interface Loopback1',
        'ip address 10.10.10.10 255.255.255.255',
        'description Automated by Netmiko'
    ]
    output = net_connect.send_config_set(commands)
    print(output)
          
  • Read Config Commands From File:
    output = net_connect.send_config_from_file('configs/edge-switch.txt')
    print(output)
          

Advanced Usage and Parameters

  • send_command() options:
    • expect_string: Custom regular expression for prompt or expected output (for more accurate script timing or output delimiting)
    • delay_factor: Multiplies all internal Netmiko delays/timers (helpful for slow/underpowered devices)
    • use_textfsm: If supported, parse structured output with TextFSM (returns lists/dictionaries)
    • read_timeout: Explicit read timeout (seconds) for long-running show commands
  • send_config_set() options:
    • exit_config_mode: Leave configuration mode automatically (default True)
    • delay_factor: As above, for timing tuning on slow devices
    • config_mode_command: Override platform’s default (e.g. ‘configure terminal’ for Cisco IOS)
  • For send_command_timing(), no prompt detection is performed, so it’s appropriate for non-standard CLIs but requires more manual management.

Example: Parsing Output with TextFSM

output = net_connect.send_command('show ip int brief', use_textfsm=True)
for entry in output:
    print(entry['intf'], entry['ipaddr'], entry['status'])
  

Note: Requires TextFSM templates (Netmiko includes many for popular platforms).

When to Use Each Method

  • send_command: For show or operational commands, retrieving states, or quick validation
  • send_config_set: For sending config changes (single commands or sets)
  • send_multiline/send_config_from_file: For large blocks, repeatable batch changes, or templated text
  • send_command_timing: For legacy platforms or devices with no clear prompt
  • send_command_expect: When you must precisely control output capture with a custom expected pattern

Best Practices

  • Always disconnect() after sessions, or use with statement/context manager where available
  • For production, validate changes using send_command before and after send_config_set
  • Use TextFSM parsing with send_command for structured data to feed into further automation pipelines or state validation
  • Avoid putting interactive commands in send_config_set() unless you have automated responses prepared
  • Monitor buffer sizes and read_timeouts for large configuration deployments, especially over slow links or with verbose platforms

Caveats & Gotchas

  • Some commands (especially show tech, show log, etc.) might exceed internal buffers or timeouts—test and tune delay_factor and read_timeout
  • Non-standard prompts or pagers may require tuning with expect_string or pre/post command hooks
  • TextFSM parsing requires templates for your platform/command—verify support or add needed templates

References

File Transfer Tools

File Transfer Tools in Netmiko provide robust, programmatic means to push and pull files to and from network devices over secure channels. This is essential for tasks such as software/firmware upgrades, loading large configuration files, collecting backups, or distributing supporting files like licenses or certificates.
The FileTransfer class (and related helpers) abstracts the complexity of SCP/SFTP operations across platforms, handling device-specific nuances in command sets, directories, and file verification.

Primary Responsibilities of File Transfer Tools

  • File Upload / Download: Securely copy (SCP/SFTP) files from your management host to the remote device and vice versa, regardless of device prompt structure or quirks.
  • Integrity Checking: Automates file existence, size, and sometimes MD5/SHA hash verification on both local and remote sides—minimizing data corruption or partial transfers.
  • Cross-Platform Consistency: Wraps device-specific logic and quirks (e.g., command syntax for file operations or verifying directories) so the API stays consistent across vendors (IOS, NX-OS, EOS, Junos, etc.).
  • Automation Workflow: Enables scripting of complex maintenance tasks such as rolling out a new OS image to a fleet or collecting running-config archives before and after changes.

Basic Usage Example: Uploading a File

from netmiko import ConnectHandler
from netmiko import file_transfer

device = {
    "device_type": "cisco_ios",
    "host": "192.0.2.20",
    "username": "admin",
    "password": "ciscoPW",
    "secret": "enablePW"
}

net_connect = ConnectHandler(**device)
source_file = "firmware/cat9k_iosxe.17.03.06.SPA.bin"
dest_file = "cat9k_iosxe.17.03.06.SPA.bin"

transfer_result = file_transfer(
    net_connect,
    source_file=source_file,
    dest_file=dest_file,
    file_system="flash:",
    direction="put",           # "put" for upload, "get" for download
    overwrite_file=True,
    verify=True                # Enable MD5/SHA verification if supported
)
print(transfer_result)
net_connect.disconnect()
  

Advanced Example: Using the FileTransfer Class Directly

from netmiko import ConnectHandler
from netmiko import file_transfer
from netmiko.file_transfer import FileTransfer

with ConnectHandler(**device) as net_connect:
    with FileTransfer(
        net_connect,
        source_file="configs/backup.cfg",
        dest_file="backup.cfg",
        file_system="bootflash:"
    ) as scp_transfer:
        if not scp_transfer.check_file_exists():
            scp_transfer.transfer_file()
        if not scp_transfer.verify_file():
            raise Exception("File integrity check failed!")
        print("Transfer complete and verified.")
  

Parameters/Options (file_transfer and FileTransfer)

  • net_connect: Active Netmiko connection object (already logged in/privileged)
  • source_file: Local file path to read/send, or destination on download
  • dest_file: Target filename/path on the remote device/file system
  • file_system: Device storage identifier (e.g., "flash:", "bootflash:", "/var/tmp" for Junos)
  • direction: "put" (upload) or "get" (download)
  • overwrite_file: (bool) Overwrite destination if already present
  • verify: (bool) Check file size and/or hash after transfer

Supported File Transfer Methods

  • SCP (Secure Copy) is the default/primary protocol for IOS, NX-OS, EOS, etc.
  • SFTP and TFTP may be available but less commonly automated due to security/features.
  • Support varies across platforms - always check the device’s file system/CLI support before large rollouts.

Typical Use Cases

  • Upgrading device OS images (push new firmware, verify, then trigger reload)
  • Archiving running/startup configuration files regularly
  • Pushing large text or license files prior to automated CLI changes
  • Disaster recovery: fetching configs or logs automatically from all devices

Best Practices & Tips

  • Always use verify=True or explicit verify_file() after all transfers for integrity and troubleshooting.
  • Be sure you have permissions to read/write in the chosen file_system directory.
  • For bulk or multi-device upgrades, check available free space with net_connect.send_command('dir flash:') first.
  • Remember to disconnect() or use a context manager for connections.
  • Some older devices/platforms may require ip scp server enable or similar config prior to using SCP.
  • For very large files, monitor for timeouts; consider splitting across maintenance windows.

Caveats & Troubleshooting

  • Authentication failures: SCP requires both SSH access and SCP server enabled on the device side.
  • Path/Filesystem errors: File system syntax can vary by platform, e.g., "flash:" vs. "/var/tmp".
  • SCP limitations: Some devices or access control policies block SCP by default—test before automating fleet upgrades.
  • Device busy/locked files: Prevents overwrite or deletion; script should check existence before upload.

References

SSH Channel Abstraction

SSH Channel Abstraction is a critical underpinning within Netmiko, enabling seamless, programmatic communication with network devices via SSH. Rather than working directly with Python SSH libraries (paramiko, ssh2-python), Netmiko’s channel abstraction layer offers a consistent interface for sending commands, receiving output, and handling device CLI idiosyncrasies—making network automation reliable and portable across vendors and platforms.

What Is SSH Channel Abstraction?

  • Unified Transport Interface: Shields Netmiko users from low-level SSH protocol details and differences between SSH libraries. You interact with network devices using a normalized Python API, regardless of what’s running “under the hood.”
  • Channel Management: Encapsulates session creation, I/O buffering, blocking/non-blocking read/write logic, and prompt detection on interactive SSH/Telnet channels.
  • Underlying Implementations: Out of the box, Netmiko uses paramiko (a pure Python SSHv2 library) as its default transport. Optionally, the ssh2-python backend is available for better performance in high-scale use cases.
  • CLI and Prompt Handling: Abstracts differences in device consoles, including echo, CLI pagers, shell navigation, prompt changes, and asynchronous output, presenting consistent read/write methods for higher-level components.
  • Extensible Architecture: Enables support for new platforms, alternate transports (e.g., Telnet, Serial), or new SSH libraries underneath without changing Netmiko’s user-facing API.

How Netmiko Uses SSH Channel Abstraction

  • New Device Session: When ConnectHandler is called, Netmiko (via its platform-specific subclass) creates a session using paramiko.SSHClient or ssh2-python.
  • I/O Operations: All command sends and output collections pass through wrapper methods (e.g., write_channel(), read_channel()) that abstract and unify channel management.
  • Prompt & Synchronization: Reads from the channel are used both to collect command output and to determine when a command is complete (prompt is detected), which is crucial for reliable automation.

Example: Reading and Writing Directly to the Channel

Advanced users and developers can use low-level channel methods for custom or troubleshooting workflows.

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",
    "host": "192.0.2.33",
    "username": "admin",
    "password": "pw"
}

net_connect = ConnectHandler(**device)

# Send a custom string directly over the session
net_connect.write_channel("show clock\n")

# Read the output buffer directly
import time
time.sleep(1)  # Wait for the device to respond
output = net_connect.read_channel()
print(output)

net_connect.disconnect()
  
  • Note: Most use cases don’t require direct channel I/O; these methods are essential for debugging, dealing with unusual CLI flows, or extending Netmiko.

Changing or Selecting SSH Backends

  • Netmiko defaults to paramiko, but you can optionally select ssh2-python (where available) for improved performance.
  • To use ssh2-python, ensure it’s installed and set session_log=True and/or relevant parameters per the driver documentation.
  • Some platforms also support Telnet or Serial channels, abstracted similarly for code portability.

Key Methods Exposed by the Abstraction Layer

  • write_channel(data): Write a Unicode string directly to the transport stream (simulate keystrokes).
  • read_channel(): Read all currently available output from the device’s buffer as a string (non-blocking).
  • read_until_pattern(pattern, ...): Read until the given regular expression pattern is detected (used internally to find the command prompt and delimit output).
  • flush_read(): Clear buffer contents, discarding any “leftover” output (used in state transitions).

Best Practices & Tips

  • For almost all standard operations, rely on high-level Netmiko methods (send_command, send_config_set), which use channel abstraction under the hood.
  • Use direct channel methods only for debugging, very custom device interactions, or new platform class development.
  • If you encounter odd CLI behaviors, enable a session_log when initializing your connection to capture all raw SSH interactions for troubleshooting:
net_connect = ConnectHandler(
    **device,
    session_log="debug_channel.log"
)
  

Potential Issues & Troubleshooting

  • Unicode/encoding problems: Rare on modern devices, but some older legacy platforms may require special encoding flags.
  • Buffer overflow or loss: For very verbose commands (like show tech), ensure sufficient buffer time and handle with delay_factor or increased read_timeout.
  • Prompt matching failures: May need custom expect_string or subclass-level prompt handling tweaks.

References

Dispatcher Functions

Dispatcher Functions in Netmiko are specialized utilities that dynamically select, convert, or route connection types and handlers at runtime. They make it possible for Netmiko to support a vast range of devices, platforms, protocols, and flexible workflows—all without requiring the user to manage class imports, inheritance relationships, or platform-specific logic directly. Dispatchers underpin multi-vendor automation, dynamic device type changes ("redispatch"), and advanced workflows involving jump hosts, session re-use, or hybrid transports.

Primary Responsibilities of Dispatcher Functions

  • Device Type Resolution: Identifies and instantiates the correct platform-specific connection class (e.g., CiscoIosSSH, AristaEOSSSH) based on the device_type string provided in the device dictionary.
  • Dynamic Connection Reassignment (redispatch): Allows an open connection to be converted on-the-fly to a different device class—useful after a device transitions from a generic SSH shell (like a jump host or terminal server) to a real network OS prompt.
  • Protocol Abstraction: Enables switching or supporting SSH, Telnet, or Serial sessions uniformly in those platforms that support multiple connection types.
  • Support for Custom Extensions: Lets users register their own platform-specific classes or override built-ins flexibly for in-house platforms or new vendor support.

How Dispatcher Functions Work

  • ssh_dispatcher.py: Contains internal dictionaries mapping device_type strings to their associated connection classes. When ConnectHandler is called, the dispatcher uses this mapping to instantiate the proper class.
  • redispatch(): Allows you to change an existing connection to a new device class without closing and reopening the SSH/Telnet session—especially handy in jump host/Mikrotik-style multi-hop scenarios.
  • Custom Registration: Advanced users can add new mappings (i.e., device_type to class) dynamically, making Netmiko extensible for proprietary or emerging platforms.

Basic Example: Class Selection by Dispatcher

from netmiko import ConnectHandler

device = {
    'device_type': 'arista_eos',
    'host': '192.0.2.21',
    'username': 'admin',
    'password': 'aristaPW'
}

# Under the hood, ConnectHandler calls the dispatcher to pick AristaEOSSSH
net_connect = ConnectHandler(**device)
print(net_connect.find_prompt())
net_connect.disconnect()
  

Advanced Example: Using redispatch()

Suppose you're first connected to a generic terminal server or Linux jump host, and after SSH/telnetting from there, you reach a device prompt. redispatch() lets you "switch" the open Netmiko session to use the correct connection class for your end target:

from netmiko import ConnectHandler, redispatch

# First connect to a jump host or terminal server
jump_device = {
    "device_type": "terminal_server",
    "host": "192.0.2.30",
    "username": "jumper",
    "password": "jumpPW",
}
conn = ConnectHandler(**jump_device)
conn.write_channel("ssh cisco@10.10.10.1\n")
# (insert logic to handle login prompts, etc.)

# After authentication and reaching the network device prompt:
redispatch(conn, device_type='cisco_ios')

print(conn.send_command('show version'))  # Now uses CiscoIOSSSH logic!
conn.disconnect()
  

Registering Custom Device Types

If you've created your own subclass of BaseConnection for a new or in-house platform, you can add it directly to the dispatcher:

from netmiko.ssh_dispatcher import CLASS_MAPPER_BASE
from my_platform_pack import MySwitchSSH

CLASS_MAPPER_BASE['my_switchos'] = MySwitchSSH

device = {
    "device_type": "my_switchos",
    "host": "192.0.2.99",
    # ... other params ...
}
conn = ConnectHandler(**device)
print(conn.find_prompt())
conn.disconnect()
  
  • Note: This lets you automate against custom or not-yet-supported device types using standard Netmiko logic.

When to Use Dispatcher Functions

  • Always: When using ConnectHandler; dispatcher logic ensures you get the right class every time with just a dictionary.
  • Advanced Scenarios: For multi-hop (jump host/terminal server) workflows, vendor OS shells that evolve during a session, or session upgrades (e.g., generic-to-specific).
  • When introducing new platform support to Netmiko for organizational or vendor-specific extensions.

Best Practices & Tips

  • Always set correct and supported device_type values in your inventory to avoid lookup errors.
  • Use redispatch() judiciously; make sure you’re at the expected prompt/CLI before calling.
  • If developing custom connection classes, follow Netmiko patterns and test extensively before production use.
  • Monitor Netmiko releases—new platform strings and classes are added frequently.

Caveats & Troubleshooting

  • Wrong device_type: Results in dispatch errors; double check spelling/casing and supported platforms.
  • redispatch() issues: If the session is not truly at the remote device prompt, commands may fail or behave unexpectedly.
  • Multiple platform support: Be careful not to register multiple classes to the same device_type string.

References

Utility Modules

Utility Modules in Netmiko provide a suite of supporting functions and helper methods that simplify error handling, CLI prompt recognition, config mode detection, timeouts, retries, state verification, and more. These are critical “glue components” underpinning robust automation, highly resilient sessions, and seamless user experience across a range of device platforms and behaviors.

What Are Netmiko Utility Modules?

  • Prompt Matching & Normalization: Identify, parse, and normalize CLI prompts (using regular expressions) to ensure correct command execution and context synchronization.
  • Config Mode Management: Helpers to check if device is in config mode, enter/exit config mode, and confirm correct context before applying configuration commands.
  • Error Pattern Detection: Built-in logic to scan CLI output for common fatal errors, command rejections, or warning patterns—triggering retries or exceptions as needed.
  • Robust Timeout/Delay Handling: Functions to control read, write, and overall session timeouts, as well as delay factors for handling slow or highly interactive devices.
  • Session Recovery: Methods to clear input/output buffers, resynchronize session state, and recover from CLI "misalignments."
  • Command Verification: Functions to confirm intended changes actually took effect (via pre/post verification commands or output scans).
  • Additional Helpers: Utilities for linefeed normalization, string sanitization, ANSI escape code stripping, and more.

Key Utility Methods & Where to Find Them

  • find_prompt() — Returns the current CLI prompt (used for synchronization and to confirm device state).
  • check_config_mode() — Returns True if session is in config mode.
  • config_mode() / exit_config_mode() — Enter or exit configuration mode as needed for a device/platform.
  • is_alive() — Returns True if the session and underlying TCP transport are still active (used for resilience/monitoring).
  • clear_buffer() — Flushes any residual data from the input buffer, avoiding out-of-sync issues.
  • strip_ansi_escape_codes(), normalize_linefeeds() — Remove unwanted formatting from output.
  • delay_factor / read_timeout (parameters or methods) — Adapt all operations to slower or laggy networks/devices.

Example: Using Utility Methods for Resilient Automation

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",
    "host": "192.0.2.50",
    "username": "admin",
    "password": "pw"
}

net_connect = ConnectHandler(**device)

# Always check and synchronize state
prompt = net_connect.find_prompt()
print(f"Current prompt: {prompt}")

# Safely enter config mode, if not already
if not net_connect.check_config_mode():
    net_connect.config_mode()

output = net_connect.send_config_set(["hostname Automated-Router"])
print(output)

# Optionally, verify hostname change took effect
hostname = net_connect.send_command("show running-config | include hostname")
if "Automated-Router" in hostname:
    print("Change verified!")

net_connect.exit_config_mode()
net_connect.disconnect()
  

Advanced Example: Handling Buffer Issues and Resiliency

try:
    net_connect = ConnectHandler(**device, timeout=60)
    net_connect.clear_buffer()
    if not net_connect.is_alive():
        raise Exception("SSH session not alive; aborting automation.")

    # Increasing delay factor for a slow remote link
    output = net_connect.send_command("show tech", delay_factor=3, read_timeout=120)
    cleaned_output = net_connect.strip_ansi_escape_codes(output)
    print(cleaned_output)
finally:
    net_connect.disconnect()
  

Typical Use Cases for Utility Modules

  • Ensuring CLI context is correct before making config changes
  • Verifying successful command execution and device state
  • Cleaning up noisy or decorated output for further parsing/validation
  • Increasing reliability on networks with unpredictable latency
  • Programmatically confirming device health before rolling out batch changes

Best Practices & Tips

  • Always check for config mode and prompt before executing critical commands (especially in loops or bulk automation).
  • Use delay_factor and read_timeout on slower links—or when running commands that may produce massive outputs.
  • Integrate utility methods (like clear_buffer and is_alive) in your error handling and recovery logic for maximum reliability.
  • Clean and normalize output before feeding it to other automation tools or parsers.
  • Check for device support—some utility methods may be redefined/overridden in platform-specific classes for best compatibility and results.

Caveats & Troubleshooting

  • Platform Variations: Prompt detection and config mode recognition may behave differently across platforms; verify on your devices when automating at scale.
  • Compatibility: Ensure that methods like strip_ansi_escape_codes() and normalize_linefeeds() are available in your Netmiko version or supplement with equivalent logic.
  • False Positives: Overly broad prompt or pattern matching can lead to premature session sync—refine regex as needed for accuracy.

References

Installation and Setup of Netmiko and Jinja2

Before starting your automation projects, you need both Netmiko (for network connectivity) and Jinja2 (for configuration templating). The following detailed steps will ensure both are set up correctly in your environment:

  • Step 1: Check Python Installation
    Both Netmiko and Jinja2 require Python 3.7 or later. Confirm your Python version:
    python --version
    or, if you use multiple Python versions:
    python3 --version
    Example output: Python 3.10.12
    If not present, download Python from its official site and install it.
  • Step 2: Set Up a Virtual Environment (Recommended)
    A virtual environment isolates dependencies, preventing version conflicts:
    python -m venv netauto-env
    Activate it:
    Linux/macOS: source netauto-env/bin/activate
    Windows: netauto-env\Scripts\activate
    You’ll see your prompt prefixed by (netauto-env) when active.
  • Step 3: Install Netmiko and Jinja2
    With the environment active, install both libraries:
    pip install netmiko jinja2
    Example output (truncated):
    Collecting netmiko
    Collecting jinja2
    ...
    Successfully installed netmiko-x.x.x jinja2-x.x
        
  • Step 4: (Optional) Install Parsing/Reporting Libraries
    Add support for structured parsing with:
    pip install textfsm genie ttp
  • Step 5: Verify the Installation
    Test both libraries in a Python shell:
    $ python
    >>> import netmiko
    >>> import jinja2
        
    If no errors are displayed, your setup is ready.

Example: Using Netmiko and Jinja2 Together

  1. Create a Jinja2 Template (device_config.j2):
    hostname {{ hostname }}
    interface {{ interface }}
      description {{ description }}
    
  2. Render the Template with Python:
    from jinja2 import Environment, FileSystemLoader
    device_vars = {
        "hostname": "SwitchA",
        "interface": "Gig1/0/1",
        "description": "Uplink"
    }
    env = Environment(loader=FileSystemLoader("."))
    template = env.get_template("device_config.j2")
    config = template.render(**device_vars)
    config_lines = config.strip().split('\n')
    
  3. Push Config to Device with Netmiko:
    from netmiko import ConnectHandler
    device = {
        "device_type": "cisco_ios",
        "host": "192.0.2.1",
        "username": "admin",
        "password": "yourpass"
    }
    with ConnectHandler(**device) as conn:
        output = conn.send_config_set(config_lines)
        print(output)
    

With this setup, your environment and workflow support both scalable network connectivity and automated, parameterized configuration templating.

Basic Usage Example of Netmiko

Netmiko allows you to automate command execution on single or multiple network devices, supporting various vendors. Below is a comprehensive example covering all you need to know for getting started, including handling device lists, multi-vendor scenarios, error handling, and results collection.

  • Step 1: Import Required Libraries
    Import Netmiko’s ConnectHandler class and optionally exception handling utilities for robust scripting:
    from netmiko import ConnectHandler from netmiko.ssh_exception import NetmikoTimeoutException, NetmikoAuthenticationException
  • Step 2: Prepare a List of Device Dictionaries
    Define your devices in a list, specifying the connection parameters for each. This supports different vendors by changing the device_type field.
    devices = [
        {
            "device_type": "cisco_ios",
            "host": "192.168.0.10",
            "username": "admin",
            "password": "cisco"
        },
        {
            "device_type": "arista_eos",
            "host": "192.168.0.20",
            "username": "admin",
            "password": "arista"
        }
        # Add more devices as needed (incl. different vendors)
    ]
        
  • Step 3: Define the Command(s) to Run
    Set the command(s) you want to execute. This can be a single string or a list of commands (for send_config_set).
    command = "show version"
  • Step 4: Iterate Through Devices, Connect, and Execute
    Loop through the device list, establish an SSH connection, run the command, and print or store the output. Robust scripts should handle connection/authentication errors.
    for device in devices:
        print(f"--- Connecting to {device['host']} ({device['device_type']}) ---")
        try:
            with ConnectHandler(**device) as conn:
                output = conn.send_command(command)
                print(output)
        except NetmikoTimeoutException:
            print(f"Connection timed out to {device['host']}")
        except NetmikoAuthenticationException:
            print(f"Authentication failed for {device['host']}")
        except Exception as e:
            print(f"Unknown error with {device['host']}: {e}")
        
  • Step 5: Review the Results
    The script prints the output for each device. Consider collecting results in a dictionary or list if you need to process or save them later.

Complete Example: Sending a Command to Multiple Vendors


from netmiko import ConnectHandler
from netmiko.ssh_exception import NetmikoTimeoutException, NetmikoAuthenticationException

devices = [
    {
        "device_type": "cisco_ios",
        "host": "192.168.0.10",
        "username": "admin",
        "password": "cisco"
    },
    {
        "device_type": "arista_eos",
        "host": "192.168.0.20",
        "username": "admin",
        "password": "arista"
    }
]

command = "show version"

for device in devices:
    print(f"--- Connecting to {device['host']} ({device['device_type']}) ---")
    try:
        with ConnectHandler(**device) as conn:
            output = conn.send_command(command)
            print(output)
    except NetmikoTimeoutException:
        print(f"Connection timed out to {device['host']}")
    except NetmikoAuthenticationException:
        print(f"Authentication failed for {device['host']}")
    except Exception as e:
        print(f"Unknown error with {device['host']}: {e}")

Tips:
- You can extend the devices list to include as many devices and vendors as needed.
- Use send_config_set([...]) to send configuration commands in config mode.
- For production use, prefer reading device info from a file (CSV, YAML, etc.) and avoid hardcoding passwords.
- Consider logging or saving outputs for auditing or troubleshooting.

Advanced Use Cases of Netmiko

Netmiko goes beyond basic connectivity, enabling powerful automation and orchestration across complex network environments. Here’s a step-by-step look at some advanced use cases, each with a specific example:

  • Multi-Device Configuration Management:
    Automate config changes across fleets by looping through device inventories and pushing updates:
    from netmiko import ConnectHandler
    
    devices = [
        {"device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "cisco"},
        {"device_type": "arista_eos", "host": "10.0.0.2", "username": "admin", "password": "arista"},
    ]
    
    config_commands = [
        "interface Loopback99",
        "description Configured by Netmiko",
        "no shutdown"
    ]
    
    for device in devices:
        with ConnectHandler(**device) as conn:
            output = conn.send_config_set(config_commands)
            print(f"{device['host']} configuration applied:\n{output}")
        
  • Automated Backup and Restore:
    Retrieve and save device configs for backup or disaster recovery:
    from netmiko import ConnectHandler
    from datetime import datetime
    
    devices = [
        {"device_type": "cisco_ios", "host": "10.0.0.3", "username": "admin", "password": "cisco"}
    ]
    
    for device in devices:
        with ConnectHandler(**device) as conn:
            hostname = conn.find_prompt().strip('#>\n')
            output = conn.send_command("show running-config")
            filename = f"{hostname}_backup_{datetime.now():%Y%m%d}.txt"
            with open(filename, "w") as f:
                f.write(output)
            print(f"Config backed up for {hostname} in {filename}")
        
  • Network Monitoring and Reporting:
    Collect interface stats, error logs, or health metrics for dashboards:
    from netmiko import ConnectHandler
    
    device = {
        "device_type": "cisco_ios",
        "host": "10.0.0.4",
        "username": "admin",
        "password": "cisco"
    }
    
    with ConnectHandler(**device) as conn:
        output = conn.send_command("show interfaces")
        print("Interface statistics:")
        print(output)
        
    For structured output, add parsing with TextFSM:
    output = conn.send_command("show interfaces", use_textfsm=True)
    for intf in output:
        print(f"{intf['intf']} - InErrors: {intf.get('in_errors', 'N/A')}, Status: {intf.get('status', 'N/A')}")
        
  • Automated Software Upgrades:
    Upload and trigger a firmware or OS update:
    from netmiko import ConnectHandler, file_transfer
    
    device = {
        "device_type": "cisco_ios",
        "host": "10.0.0.5",
        "username": "admin",
        "password": "cisco"
    }
    
    with ConnectHandler(**device) as conn:
        # Transfer image file (requires 'scp' and image file path)
        transfer_result = file_transfer(
            conn,
            source_file="c2960x-universalk9-mz.152-7.E2.bin",
            dest_file="c2960x-universalk9-mz.152-7.E2.bin",
            file_system="flash:",
            direction="put",
            overwrite_file=True
        )
        print("Image transfer result:", transfer_result)
    
        # Start upgrade
        output = conn.send_command("reload")
        print("Device reload initiated. Confirm manually if prompted.")
        
    Note: Real upgrades require planning, checks, and may need additional logic for verification and reload confirmation.
  • Custom Workflow Automation:
    Chain multiple operations—zero-touch provisioning (ZTP), troubleshooting, or VLAN assignments:
    from netmiko import ConnectHandler
    
    device = {
        "device_type": "arista_eos",
        "host": "10.0.0.6",
        "username": "admin",
        "password": "arista"
    }
    
    with ConnectHandler(**device) as conn:
        # ZTP: configure interface, verify, and gather state
        config = ["interface Ethernet1", "description Uplink", "no shutdown"]
        print(conn.send_config_set(config))
        # Troubleshoot
        print(conn.send_command("show interfaces status"))
        print(conn.send_command("show logging | last 20"))
        
  • Integration with Other Tools:
    Use Netmiko with TextFSM or Genie for structured parsing, or with Ansible for orchestration:
    from netmiko import ConnectHandler
    
    device = {
        "device_type": "cisco_ios",
        "host": "10.0.0.7",
        "username": "admin",
        "password": "cisco"
    }
    
    with ConnectHandler(**device) as conn:
        output = conn.send_command("show ip interface brief", use_textfsm=True)
        for intf in output:
            print(f"Interface: {intf['intf']}, IP: {intf['ipaddr']}, Status: {intf['status']}, Protocol: {intf['protocol']}")
        
    For Genie: pass use_genie=True on Cisco devices with Genie/pyATS installed for even richer data models.

Each example above illustrates a specific Netmiko use case, from mass configuration to monitoring, software upgrades, and integration with industry-standard automation tools.

Troubleshooting and Best Practices for Netmiko

Ensure reliable, efficient, and *resilient* Netmiko automation with these actionable troubleshooting tips, best practices, and illustrative code examples—especially when integrating Netmiko with Jinja2 templating.

  • Step 1: Verify Privilege Levels
    Some tasks require privileged EXEC mode. Call enable() right after connecting:
    from netmiko import ConnectHandler
    
    device = {...}
    with ConnectHandler(**device) as conn:
        conn.enable()  # Enter enable mode if needed
        output = conn.send_command("show running-config")
        print(output)
        
    If conn.enable() fails, check your user’s enable password or privilege configuration.
  • Step 2: Check Device Prompts
    Netmiko needs to recognize the device prompt. If scripts hang or never return, verify device prompts with:
    with ConnectHandler(**device) as conn:
        prompt = conn.find_prompt()
        print(f"Device prompt: {prompt}")
        # If unexpected, adjust Netmiko params (e.g., 'session_log' or 'global_delay_factor')
        
    You can customize prompt patterns with the expect_string argument in send_command() if needed.
  • Step 3: Handle Connection Errors Gracefully
    Always wrap device access in try/except blocks to manage network, authentication, or prompt issues and ensure useful logging:
    from netmiko.ssh_exception import NetmikoTimeoutException, NetmikoAuthenticationException
    try:
        with ConnectHandler(**device) as conn:
            ...
    except NetmikoTimeoutException as e:
        print(f"Timeout: {e}")
    except NetmikoAuthenticationException as e:
        print(f"Authentication failed: {e}")
    except Exception as e:
        print(f"Other error: {e}")
        
    For transient errors, consider simple loop-based retry logic.
  • Step 4: Manage Output Pagination
    If output is truncated (e.g., due to "--More--"), ensure terminal paging is disabled:
    with ConnectHandler(**device) as conn:
        conn.send_command("terminal length 0")
        output = conn.send_command("show running-config")
        
    Most Netmiko drivers auto-disable pagination; set explicitly if you still see paged output.
  • Step 5: Use Structured Parsing for Output
    Reduce parsing errors using tools like TextFSM or Genie for structured results:
    output = conn.send_command("show ip interface brief", use_textfsm=True)
    for intf in output:
        print(intf)
        
    This simplifies extracting IPs, statuses, etc., from complex outputs.
  • Step 6: Isolate Environments
    Use virtual environments to separate tools and prevent dependency issues:
    python -m venv netauto-env
    source netauto-env/bin/activate  # Linux/macOS
    netauto-env\Scripts\activate     # Windows
    pip install netmiko jinja2
        
  • Step 7: Secure Your Credentials
    Never hardcode passwords. Use environment variables or a secrets manager. Example for env variables:
    import os
    
    device = {
        ...
        "username": os.environ.get("DEVICE_USER"),
        "password": os.environ.get("DEVICE_PASS")
    }
        
    Set these in your shell or with tools like python-dotenv.
  • Step 8: Test with Non-Production Devices
    Validate new scripts in a sandbox or test lab. This minimizes unintentional outages.
  • Step 9: Monitor Script Performance
    If Netmiko scripts are unreliable (due to slow CLI responses), tweak delays:
    with ConnectHandler(**device, global_delay_factor=2) as conn:
        ...
        
    Increase global_delay_factor or adjust conn.send_command(..., delay_factor=2) to give devices more time to respond.
  • Step 10: Stay Updated
    Keep Netmiko, Jinja2, and dependencies current:
    pip install --upgrade netmiko jinja2
        
    This ensures new device support, features, and bug fixes.

Best Practices Integrating Netmiko with Jinja2 Templating

  • Validate Rendered Configurations
    Always print or review the config rendered by Jinja2 before pushing:
    from jinja2 import Environment, FileSystemLoader
    
    env = Environment(loader=FileSystemLoader("."))
    template = env.get_template("device_config.j2")
    config = template.render(hostname="Switch1", interface="Gig0/1", description="Uplink")
    print(config)  # Review before applying
        
  • Split Config Into Lines
    Netmiko’s send_config_set() expects a list of lines:
    config_lines = config.strip().split('\n')
    with ConnectHandler(**device) as conn:
        output = conn.send_config_set(config_lines)
        print(output)
        
  • Template Variables by Device
    For multi-device deployments, use data sources (CSV, YAML, etc.) to pass variables to Jinja2 for each device:
    devices = [{"hostname": "R1", "interface": "G0/1", "description": "WAN"}, ...]
    for vars in devices:
        config = template.render(**vars)
        ...
        
  • Catch Rendering and CLI Push Errors
    Use try/except around both rendering and config push steps to catch Jinja2/template errors and network errors:
    try:
        config = template.render(**vars)
        config_lines = config.strip().split('\n')
        with ConnectHandler(**device) as conn:
            conn.send_config_set(config_lines)
    except Exception as e:
        print(f"Error processing {vars['hostname']}: {e}")
        
  • Version and Audit Configurations
    Save generated configs to disk before pushing for auditing and rollback:
    with open(f"{vars['hostname']}_to_apply.txt", "w") as f:
        f.write(config)
        

These strategies will help you troubleshoot quickly and write robust, reusable automation for enterprise networks. Integrating Jinja2 boosts your ability to generate dynamic, error-free configurations, while best Netmiko practices ensure safe, scalable delivery.

Common Examples:

Example: Multi-Device Automation Script with Netmiko

Automating tasks across multiple network devices is a core strength of Netmiko. These examples demonstrate how to build scalable, robust scripts for collecting data or configuring devices, including with Jinja2 templating for dynamic configurations.

Example 1: Collecting Output from Multiple Devices (with Robust Error Handling)

  • Step 1: Import Netmiko & Exceptions
    from netmiko import ConnectHandler from netmiko.ssh_exception import NetmikoTimeoutException, NetmikoAuthenticationException
  • Step 2: Create Device Inventory
    devices = [ {"device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "cisco"}, {"device_type": "juniper", "host": "10.0.0.2", "username": "admin", "password": "juniper"}, {"device_type": "arista_eos", "host": "10.0.0.3", "username": "admin", "password": "arista"} ]
  • Step 3: Loop Through Devices, Connect, and Collect
    for device in devices: print(f"Connecting to {device['host']}...") try: with ConnectHandler(**device) as ssh: output = ssh.send_command("show version") print(f"--- {device['host']} Output ---\n{output}\n") except NetmikoTimeoutException: print(f"Timeout connecting to {device['host']}") except NetmikoAuthenticationException: print(f"Auth failed for {device['host']}") except Exception as err: print(f"Error with {device['host']}: {err}")

Complete Example:


from netmiko import ConnectHandler
from netmiko.ssh_exception import NetmikoTimeoutException, NetmikoAuthenticationException

devices = [
    {"device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "cisco"},
    {"device_type": "juniper", "host": "10.0.0.2", "username": "admin", "password": "juniper"},
    {"device_type": "arista_eos", "host": "10.0.0.3", "username": "admin", "password": "arista"}
]

for device in devices:
    print(f"Connecting to {device['host']}...")
    try:
        with ConnectHandler(**device) as ssh:
            output = ssh.send_command("show version")
            print(f"--- {device['host']} Output ---\n{output}\n")
    except NetmikoTimeoutException:
        print(f"Timeout connecting to {device['host']}")
    except NetmikoAuthenticationException:
        print(f"Auth failed for {device['host']}")
    except Exception as err:
        print(f"Error with {device['host']}: {err}")

Example 2: Multi-Device Configuration Push Using Jinja2 Templates

This workflow: For each device, render a configuration using Jinja2 (parameters unique per device), then push the config with Netmiko.

Preparation: Create a Jinja2 template file config_template.j2 in your script directory:

hostname {{ hostname }}
interface {{ interface }}
  description {{ description }}
  • Step 1: Device Data Per Host
    devices = [ { "device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "cisco", "vars": {"hostname": "R1", "interface": "Gig0/1", "description": "WAN Link"} }, { "device_type": "arista_eos", "host": "10.0.0.3", "username": "admin", "password": "arista", "vars": {"hostname": "Leaf3", "interface": "Ethernet1", "description": "Uplink"} } ]
  • Step 2: Render Config & Push With Netmiko
    from jinja2 import Environment, FileSystemLoader env = Environment(loader=FileSystemLoader(".")) template = env.get_template("config_template.j2") for device in devices: device_vars = device["vars"] config = template.render(**device_vars) config_lines = config.strip().split("\n") print(f"Pushing config to {device['host']}:\n{config}\n") try: with ConnectHandler(**device) as ssh: output = ssh.send_config_set(config_lines) print(f"Config applied to {device['host']}.\nResult:\n{output}\n") except Exception as err: print(f"Error on {device['host']}: {err}")

from netmiko import ConnectHandler
from jinja2 import Environment, FileSystemLoader

devices = [
    {
        "device_type": "cisco_ios",
        "host": "10.0.0.1",
        "username": "admin",
        "password": "cisco",
        "vars": {"hostname": "R1", "interface": "Gig0/1", "description": "WAN Link"}
    },
    {
        "device_type": "arista_eos",
        "host": "10.0.0.3",
        "username": "admin",
        "password": "arista",
        "vars": {"hostname": "Leaf3", "interface": "Ethernet1", "description": "Uplink"}
    }
]

env = Environment(loader=FileSystemLoader("."))
template = env.get_template("config_template.j2")

for device in devices:
    config = template.render(**device["vars"])
    config_lines = config.strip().split("\n")
    print(f"Pushing config to {device['host']}:\n{config}\n")
    try:
        with ConnectHandler(**device) as ssh:
            output = ssh.send_config_set(config_lines)
            print(f"Config applied to {device['host']}.\nResult:\n{output}\n")
    except Exception as err:
        print(f"Error on {device['host']}: {err}")

Best Practices:

  • Store credentials securely (not hardcoded; use environment variables or a secrets store)
  • Add robust error handling as shown above.
  • Save or log device outputs for auditing and troubleshooting.
  • Test on lab hardware before production changes!

Netmiko, combined with Jinja2, makes it easy to automate large, diverse networks with clear, repeatable scripts that are flexible for varying device needs.

Example: Using Netmiko with Jinja2 and YAML Variables

Best practice for large-scale or repeatable automation: separate your templates and variables! Below is a complete example to automate device provisioning with Netmiko, Jinja2 (template), and YAML (device-specific data).

  1. 1. Create the Device Configuration Jinja2 Template (device_config.j2):
    hostname {{ hostname }}
    interface {{ interface }}
      description {{ description }}
        
    Save this template in your script directory as device_config.j2.
  2. 2. Create a YAML Variables File (variables.yaml):
    device_params:
      hostname: R1
      interface: Gig0/1
      description: BACKBONE LINK
    
    device_connection:
      device_type: cisco_ios
      host: 192.168.1.1
      username: admin
      password: yourpass
        
    This file keeps your config variables separate from your Python code, making versioning, auditing, and environment changes much easier.
  3. 3. Python Script: Load YAML Variables, Render Jinja2 Template, Push Config via Netmiko
    import yaml
    from jinja2 import Environment, FileSystemLoader
    from netmiko import ConnectHandler
    
    # Load YAML data (requires: pip install pyyaml)
    with open("variables.yaml") as f:
        data = yaml.safe_load(f)
    
    device_params = data["device_params"]
    device_connection = data["device_connection"]
    
    # Render Jinja2 template
    env = Environment(loader=FileSystemLoader("."))
    template = env.get_template("device_config.j2")
    config = template.render(**device_params)
    config_lines = config.strip().split('\n')
    
    print("Rendered configuration:")
    print(config)
    print("="*40)
    
    # Connect and push using Netmiko
    with ConnectHandler(**device_connection) as conn:
        output = conn.send_config_set(config_lines)
        print("Device response:")
        print(output)
        
    • Install required dependencies (if not done already):
      pip install netmiko jinja2 pyyaml
    • To support multiple devices: Structure your YAML as a devices: list, and loop through each one in your Python logic.

Key Advantages of This Pattern:

  • Separation of logic and data: Your configs and connection info live outside of code – great for security and scale.
  • Easy auditing/rollbacks: All changes/variables are tracked in files.
  • Extensible: Pass per-device parameters, support multi-vendor, and scale to large environments by expanding your YAML.

Example: Netmiko with Jinja2 Templating, YAML Variables, and Nornir Inventory

For scalable, multi-device automation, combine Netmiko (for device communication), Jinja2 (for configuration templating), YAML (for variables), and Nornir (for inventory management). This makes it easy to manage large, dynamic network environments and push templated configs to many devices using a declarative inventory.

  1. 1. Create the Jinja2 Device Configuration Template (device_config.j2):
    hostname {{ hostname }}
    interface {{ interface }}
      description {{ description }}
        
    Save this as device_config.j2 in your script’s directory.
  2. 2. Create a Nornir Inventory Structure (YAML)
    The following files should be in a folder like inventory/:
    • hosts.yaml
      rtr1:
        hostname: 192.168.1.1
        platform: ios
        username: admin
        password: yourpass
        data:
          hostname: R1
          interface: Gig0/1
          description: BACKBONE LINK
      
      rtr2:
        hostname: 192.168.1.2
        platform: ios
        username: admin
        password: yourpass
        data:
          hostname: R2
          interface: Gig0/2
          description: USER ACCESS
              
    • groups.yaml
      ios:
        platform: ios
              
    • defaults.yaml
      username: admin
      password: yourpass
              
    Each host’s data: field holds variables you can pass directly to your Jinja template.
  3. 3. Python Automation Script: Render Jinja2 Template per Device & Push via Netmiko with Nornir
    from nornir import InitNornir
    from nornir.core.task import Task, Result
    from nornir_netmiko.tasks import netmiko_send_config
    from jinja2 import Environment, FileSystemLoader
    
    # Initialize Nornir inventory (expects inventory/ directory as above)
    nr = InitNornir(config_file="config.yaml")
    
    # Jinja2 environment setup (template must be in same directory as script)
    env = Environment(loader=FileSystemLoader("."))
    
    def deploy_template(task: Task):
        # Load Jinja2 template
        template = env.get_template("device_config.j2")
        # task.host.data contains variables defined under 'data:' in hosts.yaml
        config = template.render(**task.host["data"])
        config_lines = config.strip().split('\n')
    
        # Send configuration via Netmiko using the nornir_netmiko task plugin
        result = task.run(
            task=netmiko_send_config,
            config_commands=config_lines
        )
        # Optionally, log rendered config per device for audit/rollback
        with open(f"{task.host.name}_rendered_config.txt", "w") as f:
            f.write(config)
        return Result(
            host=task.host,
            result=f"Rendered config:\n{config}\nResult:\n{result.result}"
        )
    
    results = nr.run(task=deploy_template)
    
    for host, multi_result in results.items():
        print(f"--- {host} ---\n{multi_result[1].result}\n")
        
    • Install necessary dependencies:
      pip install nornir nornir_netmiko jinja2
    • Nornir config file (config.yaml):
      ---
      core:
        num_workers: 20
      inventory:
        plugin: nornir.plugins.inventory.simple.SimpleInventory
        options:
          host_file: "inventory/hosts.yaml"
          group_file: "inventory/groups.yaml"
          defaults_file: "inventory/defaults.yaml"
              

Key Points & Advantages:

  • Inventory abstraction: Manage devices, groups, and defaults cleanly with Nornir YAML.
  • Per-device templating: Each device gets Jinja2 config generated from its own variables.
  • Scales easily: Add hundreds of devices by expanding hosts.yaml.
  • Auditing/rollback: All rendered configs optionally saved for review or rollback.
  • Multi-vendor ready: If you add Juniper, Arista, etc. devices to inventory, just adjust the template and vars.

Best practice: manage device connection data and per-device variables entirely outside your code (in inventory YAML) and generate/review configs before pushing. You gain repeatability, audit trails, security (no passwords in code), and easy multi-vendor extensibility.

Example: Netmiko with Jinja2 Templating, NetBox Inventory via Nornir

For highly scalable, dynamic automation, combine Netmiko (device communication), Jinja2 (template rendering), and Nornir (framework for orchestrating tasks), but use NetBox as your source of truth for device inventory and variables. This allows you to manage inventory and per-device data via the NetBox web UI/APIs while automating configuration rollout with Python.

  1. 1. Create the Jinja2 Device Configuration Template (device_config.j2):
    hostname {{ hostname }}
    interface {{ interface }}
      description {{ description }}
        
    Save this template as device_config.j2 in your script directory.
  2. 2. Populate NetBox with Device Variables
    In NetBox (https://your-netbox/):
    • Add devices under Devices with attributes such as hostname, platform, primary IP, etc.
    • For per-device Jinja2 variables, add custom fields or device config context:
      {
        "hostname": "R1",
        "interface": "Gig0/1",
        "description": "BACKBONE LINK"
      }
              
      Assign config context to each device. This way, each device in NetBox can have its own set of template variables (inherited via roles/sites/regions or set directly).
  3. 3. Python Automation Script: Connect to NetBox via Nornir NetBox Plugin, Render Jinja2, Push via Netmiko
    from nornir import InitNornir
    from nornir_netmiko.tasks import netmiko_send_config
    from nornir.core.task import Task, Result
    from jinja2 import Environment, FileSystemLoader
    
    # Initialize Nornir with NetBox Inventory
    nr = InitNornir(config_file="config.yaml")
    
    # Jinja2 setup
    env = Environment(loader=FileSystemLoader("."))
    
    def deploy_template(task: Task):
        # Use NetBox config context or custom fields as template variables
        # Nornir-NetBox inventory exposes these as 'task.host["data"]'
        j2_vars = dict(task.host.get("data", {}))
        j2_vars.setdefault("hostname", task.host.name)
        # You can also enrich with task.host.hostname, etc.
        template = env.get_template("device_config.j2")
        config = template.render(**j2_vars)
        config_lines = config.strip().split('\n')
    
        result = task.run(
            task=netmiko_send_config,
            config_commands=config_lines
        )
        with open(f"{task.host.name}_rendered_config.txt", "w") as f:
            f.write(config)
        return Result(
            host=task.host,
            result=f"Rendered config:\n{config}\nResult:\n{result.result}"
        )
    
    results = nr.run(task=deploy_template)
    
    for host, multi_result in results.items():
        print(f"--- {host} ---\n{multi_result[1].result}\n")
        
    • Install required dependencies:
      pip install nornir nornir_netmiko nornir_netbox jinja2 pynetbox
    • Configure Nornir to use NetBox Inventory: Your config.yaml file:
      ---
      core:
        num_workers: 20
      inventory:
        plugin: nornir_netbox.plugins.inventory.NetBoxInventory
        options:
          nb_url: "https://netbox.example.com"
          nb_token: "0123456789abcdef0123456789abcdef01234567"
          ssl_verify: false
          flatten_custom_fields: true
              
      Replace nb_url and nb_token with your NetBox instance and API token.
      nb_token should have read rights to devices and config contexts.
    • Per-device Jinja2 data: For each device in NetBox, set per-device variables in config context or custom fields. These get synced into Nornir automatically.

Key Points & Best Practices:

  • Central Source of Truth: Device info and per-device config data are managed in NetBox, not scripts or YAML files.
  • Templating Driven: Configs are rendered per-device using Jinja2, fed from NetBox variables for maximum flexibility.
  • Auditing: Rendered configs are saved per device.
  • Multi-vendor/scale: Just add or edit devices in NetBox, no script code changes required.
  • API tokens: Manage NetBox access securely using tokens with the least privilege.

This approach lets you orchestrate and automate network configuration at scale while managing your entire inventory in NetBox’s UI or API. Each config push draws its vars directly from NetBox, keeping deployments dynamic and operations in sync with your source of truth.

Example: Automated Netmiko Change Deployment Pipeline with Jinja2, NetBox, Nornir, and CI/CD

This example covers a complete, modern workflow for automated network changes: using Netmiko for configuration delivery, Jinja2 for CLI templates, NetBox as the source-of-truth inventory, Nornir as the orchestration framework, and a CI/CD pipeline (triggered by Pull Requests) ensuring changes are peer-reviewed and reproducible.

  1. 1. Prepare the Jinja2 Template (device_cli_config.j2):
    hostname {{ hostname }}
    interface {{ interface }}
      description {{ description }}
        
    Store this file in your repository, e.g., templates/device_cli_config.j2.
  2. 2. Store Per-Device Variables in NetBox
    In the NetBox web UI, for each device:
    • Set required attributes (hostname, platform, IP, etc.)
    • Add a config context with the template variables, for example:
      {
        "hostname": "sw-access-1",
        "interface": "Gig0/10",
        "description": "ACCESS FLOOR 10"
      }
              
      Edit in the Config Context tab or assign via roles/sites/groups.
  3. 3. Python Deployment Script (deploy.py):
    from nornir import InitNornir
    from nornir_netmiko.tasks import netmiko_send_config
    from nornir.core.task import Task, Result
    from jinja2 import Environment, FileSystemLoader
    
    # Initialize Nornir with NetBox inventory (configured below)
    nr = InitNornir(config_file="config.yaml")
    env = Environment(loader=FileSystemLoader("templates"))
    
    def push_config(task: Task):
        vars = dict(task.host.get("data", {}))
        vars.setdefault("hostname", task.host.name)
        template = env.get_template("device_cli_config.j2")
        config = template.render(**vars)
        config_lines = config.strip().split("\n")
    
        result = task.run(
            task=netmiko_send_config,
            config_commands=config_lines
        )
        with open(f"rendered_{task.host.name}_config.txt", "w") as f:
            f.write(config)
        return Result(
            host=task.host,
            result=f"Rendered config:\n{config}\nNetmiko result:\n{result.result}"
        )
    
    results = nr.run(task=push_config)
    for host, multi_result in results.items():
        print(f"--- {host} ---\n{multi_result[1].result}\n")
        
    • Dependencies: pip install nornir nornir_netbox nornir_netmiko jinja2 pynetbox
    • NetBox inventory config (config.yaml):
      ---
      core:
        num_workers: 10
      inventory:
        plugin: nornir_netbox.plugins.inventory.NetBoxInventory
        options:
          nb_url: "https://netbox.example.com"
          nb_token: "YOUR_NETBOX_TOKEN"
          ssl_verify: false
              
      Replace nb_url and nb_token as needed.
  4. 4. Add a CI/CD Pipeline File (.github/workflows/netmiko-deploy.yaml)
    name: Netmiko Deployment
    
    on:
      pull_request:
        branches:
          - main
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout Repository
            uses: actions/checkout@v4
    
          - name: Setup Python
            uses: actions/setup-python@v5
            with:
              python-version: '3.10'
    
          - name: Install Dependencies
            run: |
              pip install nornir nornir_netbox nornir_netmiko jinja2 pynetbox
    
          - name: Run Deployment Script
            env:
              NETBOX_TOKEN: ${{ secrets.NETBOX_TOKEN }}
            run: |
              echo "nb_token: $NETBOX_TOKEN" >> config.yaml
              python deploy.py
        
    • Secrets: Store your NETBOX_TOKEN in GitHub Actions secrets.
    • Audit & rollback: Artifacts like rendered_{hostname}_config.txt can be uploaded for review.
  5. 5. Git Workflow: Change Process
    1. Create a feature branch for your change: git checkout -b feat-update-access-desc
    2. Edit device variables (either in code context/YAML or by updating config context in NetBox via UI/API).
    3. Commit the change: git add . && git commit -m "Update access description for sw-access-1"
    4. Push branch: git push origin feat-update-access-desc
    5. Open a Pull Request (PR) targeting main.
    6. Peer review & approval of PR.
    7. CI/CD pipeline triggers on PR approval and merge, running deploy.py with live inventory and templates, sending the desired configs to devices.
    This process ensures all automation changes are code-reviewed, tested, tracked, and repeatable.

Core Benefits of This End-to-End Approach:

  • Source of Truth Fully In NetBox: Device details and variables are maintained centrally and can be updated via UI/API.
  • DRY (Don’t Repeat Yourself) Automation: Template and variables managed separately; reusable for any device.
  • Peer Review and Change Control: Pull request workflow ensures visibility and traceability.
  • Automated & Auditable: Every run is logged, and configurations rendered per-device can be reviewed in artifacts.
  • Safe for Production: No direct script changes required for normal variable updates—just edit NetBox or PR variable files.

Configuration Guide

Step 1: Prepare Your Jinja2 CLI Payload Template

Goal: Build a clear, reusable, version-controlled Jinja2 template to standardize CLI configuration for your network devices.

Why Use Jinja2 for CLI Templates?

  • Dynamic Configuration: Easily swap out variables (e.g., hostname, interface, description) per device without code changes.
  • Standardization: Templates enforce approved configuration formats, minimizing manual error and ensuring compliance.
  • Version Control: Store templates in Git for change history, peer review, and rollbacks.
  • Separation of Logic vs. Data: Templating means the automation script doesn’t contain hardcoded CLI, making audits and updates much easier.

Recommended Layout

  • Templates Directory:
    Place all Jinja2 templates in a templates/ subdirectory in your repository:
    your-repo/
      templates/
        device_cli_config.j2
      deploy.py
      config.yaml
      ...
        

Example device_cli_config.j2 Template

hostname {{ hostname }}
interface {{ interface }}
  description {{ description }}
  • Notes:
    • All variable placeholders (e.g., {{ hostname }}) should match the names in your NetBox config context or YAML variable files.
    • Indentation and syntax must be valid for the target device and OS―make your templates vendor-specific or use if/elif logic in Jinja2 as needed.
    • For multi-line or advanced configs, leverage Jinja2 control structures (loops, conditions) to handle dynamic interface lists, VLANs, etc.

Validating Your Template (Before Automation)

  1. Provide Example Variables (for individual testing before integrating with NetBox/Nornir):
    test_vars = {
      "hostname": "test-switch-01",
      "interface": "Gig1/0/5",
      "description": "Uplink Port"
    }
        
  2. Render the Template:
    from jinja2 import Environment, FileSystemLoader
    
    env = Environment(loader=FileSystemLoader("templates"))
    template = env.get_template("device_cli_config.j2")
    config = template.render(**test_vars)
    print(config)
        
  3. Review Output: Ensure output matches expected CLI configuration.
    hostname test-switch-01
    interface Gig1/0/5
      description Uplink Port
        

Best Practices

  • Keep one template per device type or config use-case for clarity.
  • Store templates in source control (Git) so every change is reviewable and recoverable.
  • Use descriptive template names (ios_core_vlan.j2, nxos_portchannel.j2, etc.) as you scale.
  • Document variable expectations (README, comments) for easier onboarding and peer review.

Next Steps After Template Creation:
You will connect this template to device variables from NetBox or YAML, then use Nornir to orchestrate rendering and Netmiko to push the configuration in your automation pipeline.

Step 2: Store Per-Device Variables in NetBox

Goal: Manage device-specific configuration variables centrally and dynamically by leveraging NetBox's config contexts and custom fields. These serve as the single source of truth for Jinja2 template rendering during automated change delivery.

Why Use NetBox for Variable Management?

  • Centralization: All device metadata, IPs, roles, and configuration variables live in one system, reducing duplication and drift.
  • Dynamic Updates: Changes made in NetBox (via GUI or API) are instantly available to automation—no need to edit files or scripts.
  • Inheritance: Use NetBox’s context assignment logic (on device, group, site, or role) to apply variables to a single device or entire fleet.
  • Visibility and Control: Track and approve variable changes, with audit histories and change tracking in NetBox.

Storing Variables in NetBox

  1. Define Required Variables
    Identify all variables referenced in your Jinja2 template (e.g., {{ hostname }}, {{ interface }}, {{ description }}).
  2. Choose Data Structure:
    Store per-device data in the Config Context for each device. Config context supports flexible JSON/YAML-like data assignment.
  3. Create or Edit Config Contexts (Web UI)
    1. Log in to NetBox and navigate to Devices.
    2. Select the target device. Go to the Config Context tab (or use Global Config Contexts for batch assignment).
    3. Add the required variables as JSON:
      {
        "hostname": "sw-access-1",
        "interface": "Gig0/10",
        "description": "ACCESS FLOOR 10"
      }
              
    4. Click Save. These variables can now be consumed by your automation pipeline and will be merged with other inherited contexts.
  4. (Optional) Assign Variables via API or in Bulk
    • Use the NetBox API to create or update config contexts programmatically for one or many devices.
    • Assign at higher levels—role, site, region, etc.—and NetBox will cascade variables as "effective config context" per device.
  5. Test Effective Config Context
    • After assignment, view a device’s Config Context Data in the UI to verify that all expected fields and values are present.

Best Practices

  • Keep variable names in NetBox exactly matching your Jinja2 template fields for seamless automation.
  • Use NetBox config context precedence (device > role > site > region > global) to avoid defining the same data repeatedly.
  • Document variables required per device type or template in NetBox or your repo README for clarity and onboarding.
  • Regularly audit config context data for completeness and avoid unused or stale keys.

Example: Device with Config Context Variables

{
  "hostname": "dist-sw-1",
  "interface": "Gig1/0/24",
  "description": "Distribution Uplink"
}

Next Steps: With per-device variables centrally managed in NetBox, your automation scripts can dynamically pull these values during pipeline runs, render the Jinja2 template, and push configs safely and repeatably to each device.

Step 3: Configure Nornir for NetBox Inventory and Automation

Goal: Connect your orchestration framework (Nornir) to use NetBox as the live, dynamic source of truth for device inventory, and prepare your automation project for seamless device targeting, variable mapping, and deployment execution.

Why Use Nornir with NetBox?

  • Dynamic Inventory: Nornir fetches the latest device list, platform type, and per-device config variables directly from NetBox—no manual YAML lists or duplication.
  • Device Grouping and Filtering: Easily filter by device type, role, site, or custom attributes, enabling role-based or site-based rollouts.
  • Scalable Orchestration: Run changes against hundreds or thousands of devices, controlling parallelism and error handling.

Configure Nornir for NetBox Inventory

  1. Install Required Python Packages
    • pip install nornir nornir_netbox nornir_netmiko jinja2 pynetbox
      This ensures Nornir, the NetBox plugin, Netmiko task modules, and Jinja2 are all available to your automation scripts.
  2. Create a Nornir Configuration File (config.yaml)
    Place this at your project root:
    ---
    core:
      num_workers: 10  # Adjust per your concurrency needs
    inventory:
      plugin: nornir_netbox.plugins.inventory.NetBoxInventory
      options:
        nb_url: "https://netbox.example.com"
        nb_token: "YOUR_NETBOX_TOKEN"
        ssl_verify: false
        flatten_custom_fields: true
        transform_function: null
        filter_parameters: {}
        use_hosts_cache: false
        use_custom_fields: true
        timeout: 30
        validate: true
        grp_plugin: null
        add_inventory_name: false
        add_ip_address_host: true
        skip_unreachable: true
        add_host_if_missing: true
        include_disabled: false
        filter_loopback: false
        host_filtering: []
        group_filtering: []
        override_platform: false
        platform_map: {}
        secondary_address_family: null
        netbox_inventory: {}
        nornir_config_path: "."
        test: false
        test_only: false
        static_groups: []
        exclude_virtual: true
        ignore_vrf: false
        limit: []
        use_schemas: false
        schemas_file: null
        extract_config_from_context: true
        synchronize_schema: false
        force_cache_reload: false
        config_file: null
        environment: null
        load_plugins: true
        extra_plugin_args: {}
        extra_group_args: {}
        extra_inv_args: {}
        extra_config_args: {}
        use_group_platform_if_missing: true
        flatten_inventory: false
        include_secrets: false
        platform_map_include_regex: []
        platform_map_exclude_regex: []
        platform_map_regex_priority: false
        ignore_secrets_default: false
        raise_on_platform_mismatch: false
        use_plugin_args: true
        use_plugin_kwargs: true
        dynamic_inventory: false
        refresh_interval: 3600
        parallel_plugins: false
        log_host_errors: false
        raise_host_errors: true
        log_plugin_errors: false
        skip_test: false
        logging: null
        config: {}
        disable_ssl: null
        profile_host: true
        use_profile: true
    
        
    • Replace nb_url and nb_token with your actual NetBox URL and API token (secure the token via environment variables in CI).
    • Tweak num_workers for parallelism based on your environment and device constraints.
  3. Verify Inventory Connectivity
    • In your pipeline or local Python shell, run:
      from nornir import InitNornir
      nr = InitNornir(config_file="config.yaml")
      print(f'Device inventory loaded: {len(nr.inventory.hosts)} devices')
      for name, host in nr.inventory.hosts.items():
          print(name, host.platform, host["hostname"], host.get("data", {}))
              
    • This confirms Nornir has loaded all live devices, with config contexts from NetBox automatically available in each host’s host["data"].

Best Practices

  • Keep all sensitive inventory and tokens outside source control; use CI/CD secrets and environment variable injection.
  • Document your config.yaml parameters in project README for onboarding and operational transparency.
  • Test loading inventory and variable mapping with a dry run before running the deployment pipeline.
  • For large inventories, define filtering in config.yaml or in your deploy script to target subsets (by site, role, etc.) during each run.

Next Steps: With Nornir dynamically connected to NetBox, you’re ready to build the deployment script that ties together inventory loading, Jinja2 templating, Netmiko execution, and output collection for your CI/CD pipeline.

Step 4: Build the Deployment Script — Render Templates & Push Using Netmiko/Nornir

Goal: Develop a robust Python script to tie everything together: fetch the live device inventory and per-device variables from NetBox via Nornir, render your Jinja2 CLI template for each device, and reliably deliver configurations using Netmiko—all as a single repeatable, auditable pipeline step.

Main Responsibilities of the Deployment Script

  • Initialize Nornir: Load inventory and per-device data from NetBox using your config.yaml settings.
  • Set up Jinja2 Environment: Load your CLI payload template (e.g., templates/device_cli_config.j2).
  • Per Device:
    • Extract all template variables from host["data"] or config context.
    • Render configuration for each device using the appropriate values.
    • Push configuration to each device using Netmiko with Nornir’s netmiko_send_config plugin.
    • Collect and log results, saving rendered configs for audit/rollback.

Recommended Script Structure (deploy.py)

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_config
from nornir.core.task import Task, Result
from jinja2 import Environment, FileSystemLoader

# Step 1: Start Nornir with NetBox-powered inventory
nr = InitNornir(config_file="config.yaml")

# Step 2: Set up Jinja2 environment (assumes templates/ directory)
env = Environment(loader=FileSystemLoader("templates"))

def push_config(task: Task):
    # Step 3: Collect per-device variables from NetBox config context
    vars = dict(task.host.get("data", {}))
    vars.setdefault("hostname", task.host.name)
    # Step 4: Render the CLI configuration from Jinja2 template
    template = env.get_template("device_cli_config.j2")
    config = template.render(**vars)
    config_lines = config.strip().split("\n")
    # Step 5: Push to device via Netmiko (Nornir plugin)
    result = task.run(
        task=netmiko_send_config,
        config_commands=config_lines
    )
    # Step 6: Save rendered config for audit
    with open(f"rendered_{task.host.name}_config.txt", "w") as f:
        f.write(config)
    return Result(
        host=task.host,
        result=f"Rendered config:\n{config}\nNetmiko result:\n{result.result}"
    )

# Step 7: Execute workflow on all devices in inventory
results = nr.run(task=push_config)

for host, multi_result in results.items():
    print(f"--- {host} ---\n{multi_result[1].result}\n")

Key Tips and Best Practices

  • Resilience: Catch and handle exceptions for unreachable or failed devices; optionally log errors for reporting in CI/CD artifacts.
  • Audit Trail: Always create a file containing the rendered config per host for post-run review, rollback, and compliance checks.
  • Scalability: Use filtering (by role, site, etc.) with Nornir’s inventory API to limit blast radius during staged deployments.
  • Flexibility: Support multiple Jinja2 templates if pushing heterogeneous device configs; select the template dynamically per device/platform.

Next Steps: With this script in place, you are now ready to integrate into your CI/CD pipeline as a repeatable deployment job, ensuring every configuration change is only applied after code review, approval, and artifact logging.

Step 5: Integrate with a CI/CD Pipeline for Automated, Audited Deployment

Goal: Automate and control your network change workflow with a CI/CD pipeline (e.g., GitHub Actions, GitLab CI/CD, Azure Pipelines) to ensure every configuration update is version-controlled, peer-reviewed, and only deployed when approved.

Why Use a CI/CD Pipeline?

  • Repeatability and Auditability: Every deployment is logged with full history—who changed what, when, and why.
  • Peer Review: Pull requests and merge approvals ensure all config and variable changes are reviewed before rollout.
  • Consistency: Configuration is applied the same way every time, eliminating "it works on my machine" scenarios.
  • Separation of Duties: Engineering teams can propose changes; only automation applies them after approval.

Recommended Pipeline Steps

  1. Change Initiation:
    • Create a feature branch in your Git repository: git checkout -b feat-update-uplink-desc.
    • Update device variables in NetBox's Config Context (via the UI or API) or in a variables/config data file if managed in-repo.
    • Optionally, make changes to the CLI Jinja2 template or the deployment script as needed.
  2. Commit & Pull Request:
    • Commit changes: git add . && git commit -m "Update uplink description for access switches"
    • Push branch: git push origin feat-update-uplink-desc
    • Open a Pull Request (PR) to main (or production branch).
  3. Peer Review & Approval:
    • Another team member reviews the PR: confirms logic, variables, and templates are correct and safe.
    • The PR is approved and merged—this triggers the pipeline.
  4. CI/CD Pipeline Executes (Example: GitHub Actions)
    name: Automated Netmiko Deployment
    
    on:
      push:
        branches:
          - main
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout Code
            uses: actions/checkout@v4
          - name: Setup Python
            uses: actions/setup-python@v5
            with:
              python-version: '3.10'
          - name: Install Dependencies
            run: |
              pip install nornir nornir_netbox nornir_netmiko jinja2 pynetbox
          - name: Run Deployment Script
            env:
              NETBOX_TOKEN: ${{ secrets.NETBOX_TOKEN }}
            run: |
              echo "nb_token: $NETBOX_TOKEN" >> config.yaml
              python deploy.py
        
    • Store secrets (like NETBOX_TOKEN) securely in the CI/CD environment.
    • The script fetches inventory and variables live from NetBox, renders configs, and pushes changes with Netmiko.
  5. Post-Deployment Reporting & Artifact Handling:
    • Rendered configs and device outputs are stored as artifacts for audit and rollback.
    • Pipeline status (success/failure per device) is published in the job summary and sent to stakeholders.
    • Failures can be captured for retry or manual resolution.

Best Practices for Pipeline Automation

  • Restrict pipeline triggers to PR merges/approvals (not every push)—this ensures all changes are reviewed.
  • Enforce branch protection rules requiring approvals before merge.
  • Log every rendered configuration and store a summary per run for compliance and incident management.
  • Perform "dry run" or validation steps before any configuration is pushed to production devices.

Example End-to-End Workflow

  1. Feature branch created for change (e.g., updating an interface description).
  2. Config context in NetBox (step 2) updated for the device(s) impacted.
  3. Pull request submitted and reviewed for those changes.
  4. On PR approval and merge, the pipeline runs deploy.py, which:
    • Loads up-to-date inventory and variables from NetBox.
    • Renders the Jinja2 CLI config per device.
    • Pushes configuration safely using Netmiko, tracking results and saving artifacts.
    • Posts a summary for review and compliance.

With this complete pipeline integration, your network automation practice achieves repeatability, resilience, compliance, and team transparency.

Next Steps: Refine your review process and post-deployment verifications, and expand on notifications, testing, and maintenance for robust ongoing operations.

Step 6: Post-Deployment Review, Validation & Ongoing Operations

Goal: After the pipeline deploys the configuration, conduct validation checks, audit the results, and establish ongoing operational practices for a safe, controlled, and continually improving automation environment.

Why Post-Deployment Validation and Review Matter

  • Early Error Detection: Quickly catch configuration errors, missed changes, or device failures before they impact users or services.
  • Compliance: Prove and record that intended changes were accurately applied and enforceable against policy requirements.
  • Continuous Improvement: Learn from each run, improving automation reliability and organizational trust in the network as code practice.

Recommended Activities After Each Pipeline Run

  1. Review Pipeline Results and Artifacts
    • Inspect the CI/CD job log for overall and per-device status (success, errors, unreachable devices).
    • Download artifacts, such as rendered_{hostname}_config.txt, Netmiko outputs, and exception logs for internal auditing and potential rollback.
    • Archive these according to your compliance and incident management needs.
  2. Validate Device State
    • Optionally, build validation steps into your pipeline or as follow-up jobs (e.g., using netmiko_send_command with "show run", "show interface", or other verification commands).
    • Example:
      from nornir_netmiko.tasks import netmiko_send_command
      
      def validate_config(task):
          output = task.run(
              task=netmiko_send_command,
              command_string="show run | section interface"
          )
          # Add your custom logic to check presence/absence of config lines
              
    • Report on or alert for any validation failures immediately.
  3. Cross-Check Against Policy or Golden Config
    • Compare post-push configs to your organization's golden config or intended state (can be done using Nornir, Nautobot/NetBox diff tools, or external compliance scripts).
  4. Notify Stakeholders
    • Send success/failure notifications to change control boards or stakeholders (via pipeline notifications, chat, email, or ticketing integrations).
    • Summary should include: which devices changed, which succeeded/failed, artifact locations, and validation outcome.
  5. Rollback as Needed
    • If validation shows a critical misconfiguration, use saved rendered configs and network automation to revert devices to their previous state via the pipeline.

Operational and Process Best Practices

  • Implement pipeline dry runs in a test environment before deploying to production.
  • Retain change artifacts for compliance review, especially in regulated industries.
  • Iterate on pipeline logic—add pre/post-checks, improved exception handling, granular notifications, and traffic-safe maintenance windows.
  • Foster a feedback loop: conduct blameless postmortems on failed changes to improve both automation and process safety.

Example CI Pipeline Extension for Validation

Add a validation job to your pipeline:

jobs:
  deploy:
    # ... as before ...
  validate:
    runs-on: ubuntu-latest
    needs: deploy
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - name: Install Dependencies
        run: |
          pip install nornir nornir_netbox nornir_netmiko jinja2 pynetbox
      - name: Run Validation Script
        env:
          NETBOX_TOKEN: ${{ secrets.NETBOX_TOKEN }}
        run: |
          python validate.py

This closes the DevOps loop: starting from template, through variable and inventory management, pipeline-controlled deployment, and finally post-deployment validation and audit for true network automation at scale.


Conclusion

Throughout this blog post, we’ve taken a comprehensive journey into Netmiko, one of the most powerful and accessible tools for network automation with Python. Here’s a quick recap of what we’ve covered:

  • Overview: We learned what Netmiko is, why it’s essential for network engineers, and how it simplifies SSH-based automation across multi-vendor environments.
  • Key Features: We explored Netmiko’s strengths, including its multi-vendor support, robust error handling, output parsing, and performance optimization.
  • Installation and Setup: Step-by-step instructions showed how easy it is to get started with Netmiko, from Python environment setup to installing additional parsing libraries.
  • Basic Usage Example: We walked through a simple script to connect to a device and run a command, demonstrating Netmiko’s straightforward API.
  • Advanced Use Cases: We saw how Netmiko scales to complex tasks like multi-device configuration, automated backups, monitoring, and integration with other tools.
  • Troubleshooting and Best Practices: Practical tips were provided to help you avoid common pitfalls and ensure your automation scripts are reliable and secure.
  • Common Issues Solved: We highlighted how Netmiko abstracts away vendor quirks, handles CLI transitions, and manages output pagination, making automation more robust.
  • References & Resources: We shared a curated list of documentation, tutorials, community forums, and books to help you continue your learning journey.
  • Multi-Device Script Example: A real-world script demonstrated how to automate tasks across different device types efficiently.

Takeaways:

  • Netmiko makes network automation accessible, even for those new to Python.
  • Its vendor-agnostic approach and rich feature set empower you to automate repetitive tasks, reduce errors, and manage networks at scale.
  • With a supportive community and extensive documentation, you’ll never be alone on your automation journey.

Thank you for joining us on this deep dive into Netmiko! Whether you’re just starting out or looking to level up your network automation skills, Netmiko is a tool you’ll want in your toolkit. Happy automating—and don’t hesitate to explore, experiment, and share your successes with the community!

Read next

OpsMill InfraHub: Deep Dive

OpsMill InfraHub: Deep Dive

Table of Contents * Overview * Core Components * Comparative Analysis * Use Cases and Applications * Roadmap and Future Enhancements * Schema Design Example * Getting Started with InfraHub * End-to-end Example

Lauren Garcia 13 min read
Nautobot: Deep Dive

Nautobot: Deep Dive

Table of Contents * Overview * Core Components * Comparative Analysis * Nautobot Use Cases * Nautobot App Ecosystem * Deep Dive: Nautobot Jobs * Data Integrity and Governance * Deployment and Integration

Lauren Garcia 32 min read
Normir Automation: Deep Dive

Normir Automation: Deep Dive

Nornir Automation Framework: Deep Dive Everything You Need to Know Table of Contents * Overview * Themes and Symbolic Aspects * Project Structure * Comparative Analysis * Nornir Architecture Flow

Lauren Garcia 26 min read