← Engineering articles

Practical guide and proposed lab · CLI automation

Scrapli vs Netmiko: Which Versions and APIs Are Compared?

Compare pinned Scrapli and Netmiko APIs, host-key policy, timeouts and result handling with equivalent IOS XE examples. No untested speed claims.

Two equal CLI automation paths to one router — conceptual illustration

What you’ll learn

  • Compare exact package generations and platform drivers.
  • Adapt strings and Response objects to one acceptance contract.
  • Run equivalent read-only commands with strict SSH identity checks.
  • Separate functional evidence from future performance measurements.

Scrapli vs Netmiko is a workflow decision, not a reliable shortcut around an unexplained SSH timeout. A new library cannot repair a wrong management VRF, an unavailable AAA server or a command waiting for confirmation. Compare the behavior your application needs before committing to a migration.

Lab status: This is a proposed exercise, not a completed device test or client case study. Code examples require validation against your image and dependency versions. My documented background is NOC/data center operations, multi-vendor Netmiko workflows and Python/Flask integration.

A release-specific comparison and equivalent IOS XE collection example, with failure evidence instead of untested speed claims.

Release scope: Netmiko 4.7.0 and Scrapli 2025.1.30 APIs

This guide compares Netmiko 4.7.0 with the Scrapli 2025.1.30 Python API on Python 3.10+. Scrapli’s source tag is spelled 2025.01.30; PyPI normalizes the distribution version to 2025.1.30. These are fixed example baselines, not a statement that both are current releases. Do not copy this legacy Scrapli API into another major generation without checking its migration documentation.

Netmiko provides device-specific CLI connections through ConnectHandler. The selected Scrapli release provides network drivers, response objects and selectable transports. Both still need a correct platform, trusted SSH identity, permitted commands and application-level interpretation of output.

Decision point Netmiko 4.7.0 Scrapli 2025.1.30 Operational consequence
IOS XE entry point ConnectHandler(device_type='cisco_ios', ...) IOSXEDriver(...) Driver names are not interchangeable
Raw command result String by default Response; text in .result A migration needs a response adapter
SSH identity policy Set ssh_strict=True explicitly auth_strict_key=True is the documented default; set it explicitly here Compare security policies, not defaults left implicit
Timeout vocabulary Connection/auth/banner settings and command read_timeout Socket/transport/operation settings Equal numbers do not guarantee identical timeout semantics
CLI failure signal Caller checks output for show-command errors Driver-specific .failed plus caller validation Neither is proof that the business objective succeeded
Execution model Synchronous calls in this exercise Synchronous and async drivers documented for this release Async requires compatible transport and orchestration, not just async def

The Scrapli release documents core drivers for IOS XE, IOS XR, NX-OS, EOS and Junos, with community or custom drivers for other platforms. Netmiko maintains a separate platform registry. Verify exact platform support and privilege behavior in the tagged source; the existence of a driver is not proof that your image and workflow have been validated. Scrapli release documentation, Netmiko platform registry.

How do strings and Response objects change the output contract?

A legacy function returning str cannot be replaced with a function returning Response without adapting its callers. Similarly, “no Python exception” does not mean a show command succeeded. A device may return an authorization error over a perfectly healthy SSH connection.

The adapters below return strings to a shared validator. Scrapli’s adapter first checks .failed. The shared validator then rejects empty text, common IOS CLI error/paging indicators and output without an IOS version marker. This is a narrow acceptance contract for show version, not an OS inventory parser.

Parsing boundary: structured output adds a separate contract. If a later implementation uses TextFSM, verify template availability, returned type, required fields and empty-result handling. Do not treat an empty list or a raw-string fallback as a valid inventory record. Preserve a sanitized fixture per supported platform and command. See the TextFSM troubleshooting guide.

Compare the same workflow fairly
Conceptual diagnostic sequence; no measured outcomes or real device screenshots are shown. Open full-size diagram.

Proposed GNS3/EVE-NG lab: compare the same IOS XE command

  1. Use one supported IOS XE image and a Linux automation VM on an isolated management segment. Record the exact image and manually confirm show version with the test account. Additional vendors need separate adapters and validators.
  2. Verify the device host key through the console and place it in the same dedicated known_hosts file for both clients. Use the same account, destination port and privilege policy. Do not make one implementation appear easier by disabling identity checks.
  3. Use a disposable Python environment. Pin the two distributions below, record the resolved Paramiko version and keep the full dependency lock. Review package security and platform compatibility before deployment; historical API reproducibility is not a security endorsement.
  4. Run the two adapters sequentially, then reverse order using --first scrapli. The example intentionally uses the Paramiko transport for both to reduce one source of variation. This does not make their channel and privilege handling identical.
  5. Test an unknown host key, denied command and unreachable owned lab address. Confirm every failure is visible. Never publish failed attempts as zero-second successful collections.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "netmiko==4.7.0" "scrapli[paramiko]==2025.1.30"
python -m pip freeze > requirements.lock.txt
python compare_cli.py 192.0.2.11 ./known_hosts
python compare_cli.py 192.0.2.11 ./known_hosts --first scrapli

Replace the documentation address with your approved lab device. Passwords are prompted, not passed as command-line arguments. The example assumes the account reaches the necessary exec privilege directly. A separate enable-secret or unusual login dialog needs an explicitly reviewed profile.

Python: adapt both clients to one read-only acceptance check

Save as compare_cli.py. It checks the documented library versions before connecting, records the resolved Paramiko version and prints only metadata. It never prints raw version output, which may include serial numbers, hostnames and boot paths.

"""Equivalent read-only IOS XE operations; does not claim a benchmark winner."""
import argparse
from datetime import datetime, timezone
import getpass
from importlib.metadata import version
import json
from pathlib import Path
import re
from time import perf_counter

COMMAND = 'show version'


def netmiko_read(host, username, password, known_hosts):
    from netmiko import ConnectHandler
    with ConnectHandler(device_type='cisco_ios', host=host, username=username,
            password=password, ssh_strict=True, alt_host_keys=True,
            alt_key_file=str(known_hosts), allow_agent=False, use_keys=False,
            conn_timeout=10, auth_timeout=15, banner_timeout=15) as conn:
        return conn.send_command(COMMAND, read_timeout=30)


def scrapli_read(host, username, password, known_hosts):
    from scrapli.driver.core import IOSXEDriver
    with IOSXEDriver(host=host, auth_username=username, auth_password=password,
            auth_strict_key=True, ssh_known_hosts_file=str(known_hosts),
            transport='paramiko', timeout_socket=10,
            timeout_transport=15, timeout_ops=30) as conn:
        response = conn.send_command(COMMAND)
        if response.failed:
            raise RuntimeError('scrapli_command_failure')
        return response.result


def observe(label, operation):
    started = perf_counter()
    row = {'library': label, 'command': COMMAND,
           'observed_at': datetime.now(timezone.utc).isoformat()}
    try:
        text = operation()
        if not isinstance(text, str) or not text.strip():
            raise ValueError('empty_or_nontext')
        if re.search(r'(?im)^\s*%|--More--', text):
            raise ValueError('cli_error_or_paging')
        if not re.search(r'Cisco IOS', text, re.I):
            raise ValueError('unexpected_platform_output')
        row.update(status='collected_unverified', characters=len(text))
    except Exception as exc:
        row.update(status='failed', error_class=type(exc).__name__)
    row['session_seconds'] = round(perf_counter() - started, 3)
    return row


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('host')
    parser.add_argument('known_hosts', type=Path)
    parser.add_argument('--first', choices=['netmiko', 'scrapli'], default='netmiko')
    args = parser.parse_args()
    try:
        if not args.known_hosts.is_file():
            raise ValueError('missing_known_hosts')
        versions = {name: version(name) for name in ['netmiko', 'scrapli', 'paramiko']}
        if versions['netmiko'] != '4.7.0' or versions['scrapli'] != '2025.1.30':
            raise ValueError('example_requires_documented_package_versions')
        username = input('SSH username: ').strip()
        password = getpass.getpass('SSH password: ')
        operations = {'netmiko': netmiko_read, 'scrapli': scrapli_read}
        order = [args.first] + [k for k in operations if k != args.first]
        rows = [observe(k, lambda k=k: operations[k](args.host, username, password,
                                                    args.known_hosts)) for k in order]
        print(json.dumps({'versions': versions, 'results': rows}, indent=2))
        return 1 if any(r['status'] == 'failed' for r in rows) else 0
    except Exception as exc:
        print(json.dumps({'status': 'setup_failed', 'error_class': type(exc).__name__}))
        return 2


if __name__ == '__main__':
    raise SystemExit(main())

The adapters use context managers for normal connection cleanup. Connection/open failures also depend on the library’s implementation; validate file-descriptor and session cleanup under repeated failures in your deployment environment. A worker-level deadline is still needed for a service integration. These per-stage timeout settings are not a hard total process deadline.

The timing field covers connection setup, session preparation, command execution, cleanup and local validation. It is not just device execution time. The first successful import can add startup cost as well. This output is useful for diagnosing one attempt, but it is not a fair performance benchmark by itself.

What would a fair performance comparison require?

Separate a functional acceptance pass from any later benchmark. First require correct output and failure classification on each supported image. Then define connection reuse, cold versus warm imports, command set, concurrency, worker resources and transport settings before measuring.

For later measurements: warm imports consistently, alternate library order, capture repeated trials, and report sample size and failure counts alongside median/tail timings. Keep connection setup and command-only measurements separate. Do not mix timeouts into successful latency summaries or discard slow successes without explanation. No comparative latency results have been measured for this article.

If both libraries slow down at the same site, inspect management-path loss, AAA latency, device CPU and terminal output size. In physical estates, correlate interface errors and link events with timestamps. Changing the client library may alter the symptoms while leaving the underlying bottleneck untouched.

Which library fits an existing automation service?

Situation Starting decision Evidence required before approval
Stable Netmiko fleet with one explainable prompt timeout Repair and validate the existing workflow Reproduced cause and a regression fixture
Requirement for an async integration using the documented Scrapli generation Evaluate a bounded Scrapli proof of concept Compatible driver/transport, cancellation and concurrency behavior
Long-tail or customized device OS Investigate exact driver coverage first Login, privilege, paging and failure fixtures for the image
Team needs a consistent result schema across tools Add adapters before changing the caller Same acceptance tests and explicit failure states
Migration justified only by a generic “faster” claim Defer the rewrite Representative measurements and a maintenance-cost assessment

Both raw SSH approaches remain CLI integrations. If the requirement is a model-driven configuration transaction, evaluate device-supported NETCONF or RESTCONF capabilities separately. Selecting Scrapli’s SSH driver does not automatically introduce NETCONF transaction semantics. Compare CLI, NETCONF and RESTCONF scope.

Dashboard integration: report versions, outcomes and stale results

Store library/version, platform profile, command identity, execution outcome, acceptance outcome and UTC observation time as separate fields. Preserve the approved device ID in a protected inventory mapping. An error class without stage or device context is useful for this small example but insufficient for a fleet service; add sanitized structured stages in the worker.

A Flask/Django dashboard should consume worker results through a bounded job queue. Apply per-site concurrency limits so a dashboard refresh cannot flood SSH or AAA services. Make retries explicit, track stale results and keep tenant credentials out of request logs. Do not expose a free-form command runner as a convenience feature.

For USA, UK and Canadian engineering teams, scope access method, support hours, data location and ownership of the dependency lock during handover. A consulting deliverable should explain why the selected adapter meets their device matrix and how failures reach an operator. It should not promise that changing libraries alone will eliminate incidents.

Validation record: local API checks and pending device trials

Local synthetic tests cover accepted version text, empty/error output and exceptions at the reporting boundary. They verify both adapters’ argument mapping and response handling using fake connections. They do not establish driver interoperability, SSH negotiation, device cleanup or relative performance.

The real Netmiko 4.7.0 and Scrapli 2025.1.30 driver constructors were also checked locally with Paramiko 3.5.1, without opening a network connection. This confirms that the demonstrated constructor arguments are accepted in that environment; it does not validate authentication or command execution.

Live IOS XE runs, alternate vendors and repeated performance trials remain pending. Keep the source versions, actual device outputs and test conditions with the eventual acceptance record. The decision table is engineering guidance, not a claim that a customer migration has been completed.

What should a CLI-library migration review include?

I can assess an existing Netmiko collector, document its failure modes and scope a limited comparative proof of concept before a rewrite. Bring the device matrix, current dependency versions, sanitized failure evidence and desired dashboard output. Request a remote automation design review.

Explore the network automation troubleshooting collection to connect this issue to adjacent workflows. The Netmiko, NETCONF and RESTCONF comparison helps when you are choosing an interface rather than fixing an existing implementation.

Technical references

Discuss a related technical problem

Need help selecting or repairing a CLI automation stack?

I can assess a Netmiko workflow and design a small comparative proof of concept. Share the device matrix, current failure and deployment environment before committing to a migration.

Discuss your project Review Imran Sarwar’s experience

Remote English-language inquiries: USA, UK, Canada, Europe and beyond. UAE-based (UTC+4); working overlap is agreed per project.