Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

ptauto: declarative networks for Cisco Packet Tracer

CI PyPI

Describe a network in YAML; ptauto builds it in a running Packet Tracer, keeps it that way, and gives a pytest suite the fixtures to prove it works.

Try it now

  1. Make sure Packet Tracer is open with the MCP Control Center extension loaded (see Requirements if not set up yet).
  2. uvx amorale-ptauto status — confirms ptauto can reach it. No install needed.
  3. git clone https://github.com/AlessioMorale/amorale-ptauto && cd amorale-ptauto — the example spec and tests live here.
  4. Run the example:
uvx amorale-ptauto validate examples/two-site-guest-wifi.yaml     # check the file
uvx amorale-ptauto apply    examples/two-site-guest-wifi.yaml     # build it in Packet Tracer
uvx amorale-ptauto apply    examples/two-site-guest-wifi.yaml     # ...and again: nothing happens
uvx amorale-ptauto test     examples/two-site-guest-wifi.yaml tests_network/two_site/

The second apply reports "Nothing to do: Packet Tracer matches the specification" and sends nothing over the bridge. That's the point: the YAML file is the network, and any drift (an address changed in the GUI, an interface shut, a DNS record deleted) shows up as a named difference on the next run.

For a persistent ptauto on PATH instead of uvx each time:

pip install amorale-ptauto        # or: uv tool install amorale-ptauto
ptauto validate examples/two-site-guest-wifi.yaml

Working on ptauto itself from a clone, rather than the published package:

uv sync
uv run ptauto validate examples/two-site-guest-wifi.yaml

The package on PyPI is amorale-ptauto; the import stays import ptauto either way. amorale- is only a namespacing prefix on the distribution name.

Motivation

This started as a side effect of a Master's module in Computer Science with Artificial Intelligence that requires Cisco Packet Tracer for lab work. Packet Tracer's GUI buries every setting behind its own dialog, several clicks deep. Comparing two topology versions, or two instances of the "same" router that quietly drifted apart, means clicking through devices one by one and holding the differences in your head.

As a software engineer, that has no diff, no history, no way to tell at a glance what changed. ptauto brings the pattern that already works for infrastructure (a single declarative, textual description of the desired state) to Packet Tracer labs. A topology becomes a YAML file: diffable with git diff, reviewable, reproducible from scratch, safe to re-build.

Requirements

  • Python 3.12+ and uv
  • Cisco Packet Tracer 8.x/9.x, open, with the MCP Control Center extension loaded (Extensions > MCP BUILDER), the same extension the MCP-Packet-Tracer project installs

How it talks to Packet Tracer

ptauto reuses packet-tracer-mcp's connectivity rather than inventing its own: the local HTTP command bridge the extension's webview polls, its shared-token authentication, and the file mailbox the PT Script Engine drains when the extension window is closed. ptauto.transport is the client side of those two channels; one command travels over exactly one of them, never both.

The device catalog (models, their real port names, cabling rules) is imported from the same project, so a model that PT accepts is a model ptauto accepts.

The specification

Three sections (components, connections, configurations), as in examples/two-site-guest-wifi.yaml:

version: 1

components:                      # what exists
  Router-A:  {model: 2911, position: [280, 120]}
  SW-A:      {model: 2960-24TT, position: [160, 280]}
  PC-A1:     {model: PC-PT, position: [80, 420]}

connections:                     # how it is cabled
  - [Router-A:g0/0, SW-A:g0/1]
  - [SW-A:fa0/1, PC-A1:fa0]

configurations:                  # what it is configured with
  Router-A:
    interfaces:
      GigabitEthernet0/0:
        address: 192.168.1.1/27
        description: Site A LAN
    dhcp:
      excluded: [192.168.1.1]
      pools:
        SITE_A:
          network: 192.168.1.0/27
          default_router: 192.168.1.1
  PC-A1:
    dhcp_client: true

Notes on the schema:

  • Ports may be abbreviated. g0/0, Gi0/0 and GigabitEthernet0/0 are the same port; connections are direction-independent.
  • Cable types are inferred from the two device categories, and can be overridden per connection with cable:.
  • components can carry its own config: block, in which case configurations is optional. Where both describe a device they are merged, and configurations wins.
  • Unknown keys are errors. A mistyped setting fails the file rather than doing nothing quietly.

What a device can be given

section applies to keys
IOS devices routers, switches hostname, interfaces, vlans, dhcp, static_routes, default_gateway, domain_lookup, banner, enable_secret, extra_cli
hosts PCs, servers, printers, laptops dhcp_client, address, gateway, dns
servers Server-PT only services.dns (records), services.http (pages, port)

Interfaces take address, description, shutdown, and for switch ports mode: access|trunk, vlan:, trunk_vlans:, plus extra: for anything the schema does not model.

Bare CLI, for anything the schema does not model

extra_cli on a router or switch takes raw IOS commands, applied at global-config scope after everything else. Write it as a | block exactly the way show running-config would print it: indentation is what tells ptauto a line is a submode's child rather than a new global command:

configurations:
  Router-A:
    extra_cli: |
      line vty 0 4
       login
       transport input telnet
      service timestamps log datetime msec

ptauto groups these the same way IOS does: one block per top-level line, with its indented children checked against that line's section, and enters/exits the submode itself, so line vty 0 4 does not need a trailing exit. Each line is still verified literally, so a value IOS rewrites on the way in (a plaintext password once service password-encryption is on, the way enable_secret is always stored hashed) will show as pending on every run rather than being reported as converged when it is not; put the one credential ptauto does understand in enable_secret instead of extra_cli for that reason.

ptauto validate checks all of it before anything is sent to PT: unknown models, ports a model does not have, a port cabled twice, duplicate addresses, a host whose gateway is outside its own subnet, a DHCP pool pointing at a default-router no device owns, host settings on a router, IOS settings on a PC.

Commands

command what it does
ptauto validate SPEC parse and cross-check the file; never touches PT
ptauto plan SPEC what apply would change, and why (exit 2 if anything would)
ptauto apply SPEC make PT match the file; --prune also removes what the file does not describe, --save FILE.pkt saves the workspace
ptauto show [SPEC] what PT currently has
ptauto render SPEC the IOS configuration the file implies, without touching PT
ptauto destroy SPEC remove the devices the file describes
ptauto test SPEC [PATH] run a pytest suite against the live network
ptauto status how ptauto can reach PT right now
ptauto models [TEXT] the device models a file can use

How idempotency is decided

Nothing is re-applied blindly. Before each run ptauto reads what PT has and compares it with the specification:

part of the spec what is compared against
devices, positions the workspace's device list and centre coordinates
cables the link list, by endpoint pair, direction-independent
IOS configuration the device's own configuration text, block by block
host addressing the port's IP/mask, the DHCP flag, and the gateway/DNS in the device's XML
server services the DNS record database and the HTTP service's state

For IOS devices each piece of configuration is a block with both the commands that apply it and the evidence that proves it is already applied, so no shutdown, which never appears in a configuration, is checked as the absence of shutdown rather than as a line to look for. ptauto reads the device's saved configuration, issuing a write memory first so that what it reads is what the device is actually running, including changes someone made by hand in the GUI.

When something genuinely cannot be verified, the plan says (unverified) instead of claiming the change was needed.

Testing a network

The pytest fixtures ship with the package: no conftest.py required.

def test_the_two_sites_can_reach_each_other(pt_network):
    assert pt_network.ping("PC-A1", "PC-B1").ok

def test_nothing_has_drifted(pt_network):
    assert pt_network.plan().is_empty
pytest --pt-spec examples/two-site-guest-wifi.yaml tests_network/two_site/
pytest --pt-spec examples/two-site-guest-wifi.yaml --pt-apply tests_network/two_site/   # build first
fixture what it gives
pt_network the network under test: ping, ping_hostname, host, interface, services, plan, address_of
pt_client the raw PTClient for anything the facade does not cover
pt_spec the parsed specification
pt_ping pt_ping("A", "B"), asserts, with a readable failure

Options: --pt-spec PATH, --pt-apply (build before testing), --pt-require (fail instead of skip when PT is not running). Without --pt-require a suite skips when Packet Tracer is closed, so it stays runnable in CI.

pt_network.ping repeats a partial result once: the first packet between two hosts is always lost to ARP resolution in Packet Tracer, and that is a warm-up artefact rather than a fault. pt_network.ping(..., retry_partial=False) shows the raw first attempt.

Using it as a library

from ptauto import load_spec, PTClient, Planner, Applier

spec = load_spec("network.yaml")
client = PTClient()

plan = Planner(client, spec).build()
for action in plan.actions:
    print(action.summary, ":", action.reason)

report = Applier(client).run(plan)
print(report.ok, len(report.applied))

print(client.ping("PC-Admin1", "192.168.30.2").verdict)

Layout

src/ptauto/
  transport.py   the two channels into Packet Tracer (reused from packet-tracer-mcp)
  client.py      typed operations: topology, devices, links, IOS, hosts, services, ping
  model.py       the YAML schema
  loader.py      parsing, merging and cross-validation
  ios.py         IOS rendering, and the evidence that proves it is applied
  plan.py        the diff engine
  apply.py       execution, in an order the network can survive
  testing.py     the facade a test suite talks to
  pytest_plugin.py  the fixtures, registered as a pytest plugin
  cli.py         the `ptauto` command
examples/        a worked specification
tests/           unit tests, no Packet Tracer needed
tests_network/   acceptance tests, run against a live Packet Tracer

Run them with uv run pytest.

Continuous integration and releasing

.github/workflows/ci.yml runs on every push and pull request: the offline unit suite (tests/) on Python 3.12 and 3.13, ptauto validate against the example spec, and a packaging check (uv build + twine check --strict). tests_network/ (the acceptance suite) is deliberately not run there: it needs a real, GUI Packet Tracer instance with the MCP Control Center extension open, which no hosted runner can provide.

Versioning is automatic and driven by Conventional Commits on main: fix: bumps the patch version, feat: bumps minor, and a BREAKING CHANGE: footer (or ! after the type, e.g. feat!:) bumps major. Commits that don't match a recognized type (or with no releasable change) don't trigger a release. .github/workflows/release.yml runs python-semantic-release after ci.yml succeeds on main; when a release is due it bumps version in pyproject.toml, updates CHANGELOG.md, commits that as chore(release): X.Y.Z [skip ci], and pushes a vX.Y.Z tag with a matching GitHub Release.

.github/workflows/publish.yml builds and publishes to PyPI whenever a vX.Y.Z tag is pushed, i.e. automatically, right after release.yml creates one, using trusted publishing: OIDC, not a stored API token, so there is no secret in this repository to rotate or leak. That needs a one-time link on PyPI's side before the first release:

  1. Push this repository to GitHub and publish a release manually (or create the PyPI project first some other way), so amorale-ptauto exists on PyPI to attach a publisher to.
  2. On pypi.org, under the project's Publishing settings, add a trusted publisher with:
    • Owner: AlessioMorale
    • Repository name: amorale-ptauto
    • Workflow name: publish.yml
    • Environment name: pypi
  3. From then on, every merge to main with a releasable Conventional Commit gets tagged and published automatically: no manual version bump, tag, or PyPI-side action needed.

.github/dependabot.yml keeps both the Python dependencies and the workflow actions themselves on a weekly update check.

Known limits

  • Packet Tracer must be open with the extension loaded; there is no headless mode.
  • An error inside PT's Script Engine opens a modal dialog that freezes the bridge until it is dismissed. ptauto guards every command it sends, so it does not cause one, but a dialog opened by something else will make ptauto time out; ptauto status will say so.
  • --prune never removes PT's own infrastructure objects (the power distribution device), and by default a model mismatch is reported rather than replaced.

Release files for amorale-ptauto 1.1.2.dev0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for amorale-ptauto 1.1.2.dev0
File Size Uploaded
amorale_ptauto-1.1.2.dev0.tar.gz 102.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for amorale-ptauto 1.1.2.dev0
File Interpreter ABI Platform
amorale_ptauto-1.1.2.dev0-py3-none-any.whl Python 3 none any Details

Total release size: 154.8 kB

Release files / amorale_ptauto-1.1.2.dev0.tar.gz

Download URL amorale_ptauto-1.1.2.dev0.tar.gz
Size 102.6 kB
Tags Source
SHA-256 checksum
How to use checksums
becf94d8573bab9be907be97b32da287bbbd2a604ebeba0b6b8367bfce3c9ed8
BLAKE2b-256 checksum
How to use checksums
0742f4567e358fa2b92a915fd4a6fa8f3f70e03dfa1a8bb92ce4d4d956a715f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / amorale_ptauto-1.1.2.dev0-py3-none-any.whl

Download URL amorale_ptauto-1.1.2.dev0-py3-none-any.whl
Size 52.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5cb17439a718e21b434439d7f419aff967eacf7937ab7109fe46076606d6c49c
BLAKE2b-256 checksum
How to use checksums
e91356a949a4ee361f2b5bde6d14de0809745fce95fb3297f348ac8ff040dca7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.2.dev0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page