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:
- Check device documentation for API/NETCONF support.
- Use APIs by default when available and appropriate.
- 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.
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 extendBaseConnection
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
, andsend_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
orssh2-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
andconn_timeout
for slower devices or commands with large outputs. - For platform-specific quirks, subclass
BaseConnection
and override methods likesession_preparation
andsend_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 fromBaseConnection
, 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()
, supportssecret
- Config mode logic: Navigates "conf t", "line", "interface", etc.
- Class:
-
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
- Class:
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 bydevice_type
.
ConnectHandler Workflow Diagram
- Accept device dictionary as input (with at minimum,
device_type
,host
,username
,password
). - Looks up the correct connection class based on
device_type
. - Instantiates the connection object, establishing an SSH/Telnet session.
- 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 usewith
statement/context manager where available - For production, validate changes using
send_command
before and aftersend_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
andread_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 explicitverify_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, thessh2-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 usingparamiko.SSHClient
orssh2-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 setsession_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 withdelay_factor
or increasedread_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 thedevice_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. WhenConnectHandler
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()
— ReturnsTrue
if session is in config mode. -
config_mode()
/exit_config_mode()
— Enter or exit configuration mode as needed for a device/platform. -
is_alive()
— ReturnsTrue
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
andread_timeout
on slower links—or when running commands that may produce massive outputs. - Integrate utility methods (like
clear_buffer
andis_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()
andnormalize_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
-
Create a Jinja2 Template (
device_config.j2
):hostname {{ hostname }} interface {{ interface }} description {{ description }}
-
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')
-
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’sConnectHandler
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 thedevice_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 (forsend_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: passuse_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. Callenable()
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)
Ifconn.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 theexpect_string
argument insend_command()
if needed. -
Step 3: Handle Connection Errors Gracefully
Always wrap device access intry/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 likepython-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: ...
Increaseglobal_delay_factor
or adjustconn.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’ssend_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
Usetry/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. Create the Device Configuration Jinja2 Template (
device_config.j2
):hostname {{ hostname }} interface {{ interface }} description {{ description }}
Save this template in your script directory asdevice_config.j2
. -
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. 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.
-
Install required dependencies (if not done already):
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. Create the Jinja2 Device Configuration Template (
device_config.j2
):hostname {{ hostname }} interface {{ interface }} description {{ description }}
Save this asdevice_config.j2
in your script’s directory. -
2. Create a Nornir Inventory Structure (YAML)
The following files should be in a folder likeinventory/
:-
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
data:
field holds variables you can pass directly to your Jinja template. -
-
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"
-
Install necessary dependencies:
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. Create the Jinja2 Device Configuration Template (
device_config.j2
):hostname {{ hostname }} interface {{ interface }} description {{ description }}
Save this template asdevice_config.j2
in your script directory. -
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. 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
Replacenb_url
andnb_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.
-
Install required dependencies:
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. 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. 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. 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
Replacenb_url
andnb_token
as needed.
-
Dependencies:
-
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.
-
Secrets: Store your
-
5. Git Workflow: Change Process
- Create a feature branch for your change:
git checkout -b feat-update-access-desc
- Edit device variables (either in code context/YAML or by updating config context in NetBox via UI/API).
- Commit the change:
git add . && git commit -m "Update access description for sw-access-1"
- Push branch:
git push origin feat-update-access-desc
- Open a Pull Request (PR) targeting
main
. - Peer review & approval of PR.
- CI/CD pipeline triggers on PR approval and merge, running
deploy.py
with live inventory and templates, sending the desired configs to devices.
- Create a feature branch for your change:
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 atemplates/
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.
- All variable placeholders (e.g.,
Validating Your Template (Before Automation)
-
Provide Example Variables (for individual testing before integrating with NetBox/Nornir):
test_vars = { "hostname": "test-switch-01", "interface": "Gig1/0/5", "description": "Uplink Port" }
-
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)
-
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
-
Define Required Variables
Identify all variables referenced in your Jinja2 template (e.g.,{{ hostname }}
,{{ interface }}
,{{ description }}
). -
Choose Data Structure:
Store per-device data in the Config Context for each device. Config context supports flexible JSON/YAML-like data assignment. -
Create or Edit Config Contexts (Web UI)
- Log in to NetBox and navigate to Devices.
- Select the target device. Go to the Config Context tab (or use Global Config Contexts for batch assignment).
- Add the required variables as JSON:
{ "hostname": "sw-access-1", "interface": "Gig0/10", "description": "ACCESS FLOOR 10" }
- Click Save. These variables can now be consumed by your automation pipeline and will be merged with other inherited contexts.
-
(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.
-
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
-
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.
-
-
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
andnb_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.
-
Replace
-
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"]
.
- In your pipeline or local Python shell, run:
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 yourconfig.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.
- Extract all template variables from
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
-
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.
- Create a feature branch in your Git repository:
-
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).
- Commit changes:
-
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.
-
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.
- Store secrets (like
-
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
- Feature branch created for change (e.g., updating an interface description).
- Config context in NetBox (step 2) updated for the device(s) impacted.
- Pull request submitted and reviewed for those changes.
- 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
-
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.
-
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.
- Optionally, build validation steps into your pipeline or as follow-up jobs (e.g., using
-
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).
-
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.
-
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!