amorale-ptauto 1.1.2.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amorale_ptauto-1.1.2.dev0/.github/dependabot.yml +16 -0
- amorale_ptauto-1.1.2.dev0/.github/workflows/ci.yml +71 -0
- amorale_ptauto-1.1.2.dev0/.github/workflows/publish.yml +85 -0
- amorale_ptauto-1.1.2.dev0/.github/workflows/release.yml +58 -0
- amorale_ptauto-1.1.2.dev0/.gitignore +10 -0
- amorale_ptauto-1.1.2.dev0/.python-version +1 -0
- amorale_ptauto-1.1.2.dev0/CHANGELOG.md +7 -0
- amorale_ptauto-1.1.2.dev0/LICENSE +21 -0
- amorale_ptauto-1.1.2.dev0/PKG-INFO +369 -0
- amorale_ptauto-1.1.2.dev0/README.md +335 -0
- amorale_ptauto-1.1.2.dev0/examples/two-site-guest-wifi.yaml +143 -0
- amorale_ptauto-1.1.2.dev0/pyproject.toml +130 -0
- amorale_ptauto-1.1.2.dev0/requirements.md +17 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/__init__.py +57 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/apply.py +208 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/catalog.py +157 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/cli.py +419 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/client.py +597 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/errors.py +32 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/ios.py +396 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/loader.py +377 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/model.py +360 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/plan.py +542 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/py.typed +0 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/pytest_plugin.py +140 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/testing.py +155 -0
- amorale_ptauto-1.1.2.dev0/src/ptauto/transport.py +244 -0
- amorale_ptauto-1.1.2.dev0/tests/conftest.py +98 -0
- amorale_ptauto-1.1.2.dev0/tests/test_catalog.py +68 -0
- amorale_ptauto-1.1.2.dev0/tests/test_ios.py +234 -0
- amorale_ptauto-1.1.2.dev0/tests/test_loader.py +179 -0
- amorale_ptauto-1.1.2.dev0/tests/test_plan.py +301 -0
- amorale_ptauto-1.1.2.dev0/tests/test_transport.py +90 -0
- amorale_ptauto-1.1.2.dev0/tests_network/two_site/test_two_site_guest_wifi.py +99 -0
- amorale_ptauto-1.1.2.dev0/uv.lock +862 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# pyproject.toml / uv.lock: dependencies of the library itself.
|
|
4
|
+
- package-ecosystem: "pip"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
groups:
|
|
9
|
+
python-dependencies:
|
|
10
|
+
patterns: ["*"]
|
|
11
|
+
|
|
12
|
+
# The workflow files under .github/workflows/.
|
|
13
|
+
- package-ecosystem: "github-actions"
|
|
14
|
+
directory: "/"
|
|
15
|
+
schedule:
|
|
16
|
+
interval: "weekly"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Runs the offline unit suite and a packaging check on every push and PR.
|
|
4
|
+
# tests_network/ (the acceptance suite) is deliberately not run here: it needs
|
|
5
|
+
# a real, GUI Packet Tracer instance with the MCP Control Center extension
|
|
6
|
+
# open, which no hosted runner can provide.
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
pull_request:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ci-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
test:
|
|
22
|
+
name: test (py${{ matrix.python-version }})
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
python-version: ["3.12", "3.13"]
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
|
|
31
|
+
- name: Install uv
|
|
32
|
+
uses: astral-sh/setup-uv@v7
|
|
33
|
+
with:
|
|
34
|
+
enable-cache: true
|
|
35
|
+
python-version: ${{ matrix.python-version }}
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: uv sync --all-extras
|
|
39
|
+
|
|
40
|
+
- name: Run unit tests
|
|
41
|
+
run: uv run pytest tests/ -v
|
|
42
|
+
|
|
43
|
+
- name: Validate the example specification
|
|
44
|
+
# Pure YAML parsing and cross-checking, no Packet Tracer required,
|
|
45
|
+
# so this still catches a schema regression even though the live
|
|
46
|
+
# build/apply/test path cannot run in CI.
|
|
47
|
+
run: uv run ptauto validate examples/two-site-guest-wifi.yaml
|
|
48
|
+
|
|
49
|
+
build:
|
|
50
|
+
name: build package
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v7
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v7
|
|
57
|
+
with:
|
|
58
|
+
enable-cache: true
|
|
59
|
+
python-version: "3.12"
|
|
60
|
+
|
|
61
|
+
- name: Build sdist and wheel
|
|
62
|
+
run: uv build
|
|
63
|
+
|
|
64
|
+
- name: Check metadata with twine
|
|
65
|
+
run: uvx twine check --strict dist/*
|
|
66
|
+
|
|
67
|
+
- uses: actions/upload-artifact@v7
|
|
68
|
+
with:
|
|
69
|
+
name: dist
|
|
70
|
+
path: dist/
|
|
71
|
+
retention-days: 7
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Builds and publishes the package once release.yml has tagged a new
|
|
4
|
+
# version. Triggered by that workflow's completion rather than by the tag
|
|
5
|
+
# push itself: a tag pushed with the default GITHUB_TOKEN does not raise a
|
|
6
|
+
# new `push` event, so `on: push: tags` would never fire here. Checking out
|
|
7
|
+
# `main`'s current tip and looking for a tag on it sidesteps that.
|
|
8
|
+
#
|
|
9
|
+
# `fetch-tags: true` is required: actions/checkout does not fetch tags even
|
|
10
|
+
# with fetch-depth: 0 unless told to, and without it the tag lookup below
|
|
11
|
+
# always comes back empty, silently skipping both jobs.
|
|
12
|
+
#
|
|
13
|
+
# Uses PyPI's trusted publishing (OIDC): no API token lives in this repo's
|
|
14
|
+
# secrets at all; PyPI trusts this exact repo + workflow + environment
|
|
15
|
+
# combination once it is registered there. See README.md for the one-time
|
|
16
|
+
# setup on pypi.org that this depends on.
|
|
17
|
+
on:
|
|
18
|
+
workflow_run:
|
|
19
|
+
workflows: [Release]
|
|
20
|
+
types: [completed]
|
|
21
|
+
workflow_dispatch: # allows a manual re-run against the current tag
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
name: build distribution
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
|
31
|
+
outputs:
|
|
32
|
+
tag: ${{ steps.tag.outputs.tag }}
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v7
|
|
35
|
+
with:
|
|
36
|
+
ref: main
|
|
37
|
+
fetch-depth: 0
|
|
38
|
+
fetch-tags: true
|
|
39
|
+
|
|
40
|
+
- name: Check for a release tag on this commit
|
|
41
|
+
id: tag
|
|
42
|
+
run: echo "tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)" >> "$GITHUB_OUTPUT"
|
|
43
|
+
|
|
44
|
+
- name: Install uv
|
|
45
|
+
if: steps.tag.outputs.tag != ''
|
|
46
|
+
uses: astral-sh/setup-uv@v7
|
|
47
|
+
with:
|
|
48
|
+
enable-cache: true
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
|
|
51
|
+
- name: Run unit tests
|
|
52
|
+
if: steps.tag.outputs.tag != ''
|
|
53
|
+
run: |
|
|
54
|
+
uv sync --all-extras
|
|
55
|
+
uv run pytest tests/ -v
|
|
56
|
+
|
|
57
|
+
- name: Build sdist and wheel
|
|
58
|
+
if: steps.tag.outputs.tag != ''
|
|
59
|
+
run: uv build
|
|
60
|
+
|
|
61
|
+
- uses: actions/upload-artifact@v7
|
|
62
|
+
if: steps.tag.outputs.tag != ''
|
|
63
|
+
with:
|
|
64
|
+
name: dist
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
publish:
|
|
68
|
+
name: publish to PyPI
|
|
69
|
+
needs: build
|
|
70
|
+
if: needs.build.outputs.tag != ''
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
environment:
|
|
73
|
+
name: pypi
|
|
74
|
+
url: https://pypi.org/p/amorale-ptauto
|
|
75
|
+
permissions:
|
|
76
|
+
# id-token: write is what lets PyPI verify this run came from this
|
|
77
|
+
# workflow via OIDC, the entire point of trusted publishing.
|
|
78
|
+
id-token: write
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/download-artifact@v7
|
|
81
|
+
with:
|
|
82
|
+
name: dist
|
|
83
|
+
path: dist/
|
|
84
|
+
|
|
85
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Runs after CI succeeds on main. python-semantic-release inspects the
|
|
4
|
+
# Conventional Commits since the last release to decide whether a version
|
|
5
|
+
# bump is due (fix: -> patch, feat: -> minor, a `BREAKING CHANGE:` footer or
|
|
6
|
+
# `!` -> major), and if so:
|
|
7
|
+
# - tags the current commit on main `vX.Y.Z` and pushes the tag
|
|
8
|
+
# - publishes a GitHub Release for it, with generated notes
|
|
9
|
+
#
|
|
10
|
+
# No release commit: `commit: false` below means it never bumps a version
|
|
11
|
+
# file or touches CHANGELOG.md in the repo. The package version comes from
|
|
12
|
+
# hatch-vcs reading the tag at build time (see pyproject.toml), so there is
|
|
13
|
+
# nothing to bump.
|
|
14
|
+
#
|
|
15
|
+
# This workflow's own completion is what triggers publish.yml to build and
|
|
16
|
+
# upload to PyPI (see that file); this workflow never publishes a package
|
|
17
|
+
# itself.
|
|
18
|
+
on:
|
|
19
|
+
workflow_run:
|
|
20
|
+
workflows: [CI]
|
|
21
|
+
types: [completed]
|
|
22
|
+
branches: [main]
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
|
|
25
|
+
concurrency:
|
|
26
|
+
group: release-${{ github.ref }}
|
|
27
|
+
cancel-in-progress: false
|
|
28
|
+
|
|
29
|
+
permissions:
|
|
30
|
+
contents: write
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
release:
|
|
34
|
+
name: semantic release
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v7
|
|
39
|
+
with:
|
|
40
|
+
fetch-depth: 0
|
|
41
|
+
ref: main
|
|
42
|
+
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v7
|
|
45
|
+
with:
|
|
46
|
+
enable-cache: true
|
|
47
|
+
python-version: "3.12"
|
|
48
|
+
|
|
49
|
+
- name: Python Semantic Release
|
|
50
|
+
uses: python-semantic-release/python-semantic-release@v10.6.2
|
|
51
|
+
with:
|
|
52
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
53
|
+
commit: false
|
|
54
|
+
tag: true
|
|
55
|
+
push: true
|
|
56
|
+
changelog: true
|
|
57
|
+
vcs_release: true
|
|
58
|
+
build: false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alessio Morale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: amorale-ptauto
|
|
3
|
+
Version: 1.1.2.dev0
|
|
4
|
+
Summary: Declarative, idempotent network building and testing for Cisco Packet Tracer
|
|
5
|
+
Project-URL: Homepage, https://github.com/AlessioMorale/amorale-ptauto
|
|
6
|
+
Project-URL: Repository, https://github.com/AlessioMorale/amorale-ptauto
|
|
7
|
+
Project-URL: Issues, https://github.com/AlessioMorale/amorale-ptauto/issues
|
|
8
|
+
Author-email: Alessio Morale <alessiomorale@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cisco,infrastructure-as-code,network-automation,networking,packet-tracer,pytest,yaml
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Framework :: Pytest
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Education
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Requires-Dist: packet-tracer-mcp<1,>=0.9
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
28
|
+
Requires-Dist: pyyaml>=6
|
|
29
|
+
Requires-Dist: rich>=13
|
|
30
|
+
Requires-Dist: typer>=0.12
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# ptauto: declarative networks for Cisco Packet Tracer
|
|
36
|
+
|
|
37
|
+
[](https://github.com/AlessioMorale/amorale-ptauto/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/amorale-ptauto/)
|
|
39
|
+
|
|
40
|
+
Describe a network in YAML; `ptauto` builds it in a running Packet Tracer,
|
|
41
|
+
keeps it that way, and gives a pytest suite the fixtures to prove it works.
|
|
42
|
+
|
|
43
|
+
## Try it now
|
|
44
|
+
|
|
45
|
+
1. Make sure Packet Tracer is open with the **MCP Control Center** extension
|
|
46
|
+
loaded (see [Requirements](#requirements) if not set up yet).
|
|
47
|
+
2. `uvx amorale-ptauto status` — confirms ptauto can reach it. No install needed.
|
|
48
|
+
3. `git clone https://github.com/AlessioMorale/amorale-ptauto && cd amorale-ptauto`
|
|
49
|
+
— the example spec and tests live here.
|
|
50
|
+
4. Run the example:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uvx amorale-ptauto validate examples/two-site-guest-wifi.yaml # check the file
|
|
54
|
+
uvx amorale-ptauto apply examples/two-site-guest-wifi.yaml # build it in Packet Tracer
|
|
55
|
+
uvx amorale-ptauto apply examples/two-site-guest-wifi.yaml # ...and again: nothing happens
|
|
56
|
+
uvx amorale-ptauto test examples/two-site-guest-wifi.yaml tests_network/two_site/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The second `apply` reports *"Nothing to do: Packet Tracer matches the
|
|
60
|
+
specification"* and sends nothing over the bridge. That's the point: the YAML
|
|
61
|
+
file is the network, and any drift (an address changed in the GUI, an
|
|
62
|
+
interface shut, a DNS record deleted) shows up as a named difference on the
|
|
63
|
+
next run.
|
|
64
|
+
|
|
65
|
+
For a persistent `ptauto` on PATH instead of `uvx` each time:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install amorale-ptauto # or: uv tool install amorale-ptauto
|
|
69
|
+
ptauto validate examples/two-site-guest-wifi.yaml
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Working on ptauto itself from a clone, rather than the published package:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
uv run ptauto validate examples/two-site-guest-wifi.yaml
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The package on PyPI is `amorale-ptauto`; the import stays `import ptauto`
|
|
80
|
+
either way. `amorale-` is only a namespacing prefix on the distribution name.
|
|
81
|
+
|
|
82
|
+
## Motivation
|
|
83
|
+
|
|
84
|
+
This started as a side effect of a Master's module in Computer Science with
|
|
85
|
+
Artificial Intelligence that requires Cisco Packet Tracer for lab work.
|
|
86
|
+
Packet Tracer's GUI buries every setting behind its own dialog, several
|
|
87
|
+
clicks deep. Comparing two topology versions, or two instances of the "same"
|
|
88
|
+
router that quietly drifted apart, means clicking through devices one by one
|
|
89
|
+
and holding the differences in your head.
|
|
90
|
+
|
|
91
|
+
As a software engineer, that has no diff, no history, no way to tell at a
|
|
92
|
+
glance what changed. `ptauto` brings the pattern that already works for
|
|
93
|
+
infrastructure (a single declarative, textual description of the desired
|
|
94
|
+
state) to Packet Tracer labs. A topology becomes a YAML file: diffable with
|
|
95
|
+
`git diff`, reviewable, reproducible from scratch, safe to re-build.
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
* Python 3.12+ and [uv](https://docs.astral.sh/uv/)
|
|
100
|
+
* Cisco Packet Tracer 8.x/9.x, open, with the **MCP Control Center** extension
|
|
101
|
+
loaded (*Extensions > MCP BUILDER*), the same extension the
|
|
102
|
+
[MCP-Packet-Tracer](https://github.com/Mats2208/MCP-Packet-Tracer) project
|
|
103
|
+
installs
|
|
104
|
+
|
|
105
|
+
## How it talks to Packet Tracer
|
|
106
|
+
|
|
107
|
+
ptauto reuses `packet-tracer-mcp`'s connectivity rather than inventing its own:
|
|
108
|
+
the local HTTP command bridge the extension's webview polls, its shared-token
|
|
109
|
+
authentication, and the file mailbox the PT Script Engine drains when the
|
|
110
|
+
extension window is closed. `ptauto.transport` is the client side of those two
|
|
111
|
+
channels; one command travels over exactly one of them, never both.
|
|
112
|
+
|
|
113
|
+
The device catalog (models, their real port names, cabling rules) is imported
|
|
114
|
+
from the same project, so a model that PT accepts is a model ptauto accepts.
|
|
115
|
+
|
|
116
|
+
## The specification
|
|
117
|
+
|
|
118
|
+
Three sections (components, connections, configurations), as in
|
|
119
|
+
[`examples/two-site-guest-wifi.yaml`](examples/two-site-guest-wifi.yaml):
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
version: 1
|
|
123
|
+
|
|
124
|
+
components: # what exists
|
|
125
|
+
Router-A: {model: 2911, position: [280, 120]}
|
|
126
|
+
SW-A: {model: 2960-24TT, position: [160, 280]}
|
|
127
|
+
PC-A1: {model: PC-PT, position: [80, 420]}
|
|
128
|
+
|
|
129
|
+
connections: # how it is cabled
|
|
130
|
+
- [Router-A:g0/0, SW-A:g0/1]
|
|
131
|
+
- [SW-A:fa0/1, PC-A1:fa0]
|
|
132
|
+
|
|
133
|
+
configurations: # what it is configured with
|
|
134
|
+
Router-A:
|
|
135
|
+
interfaces:
|
|
136
|
+
GigabitEthernet0/0:
|
|
137
|
+
address: 192.168.1.1/27
|
|
138
|
+
description: Site A LAN
|
|
139
|
+
dhcp:
|
|
140
|
+
excluded: [192.168.1.1]
|
|
141
|
+
pools:
|
|
142
|
+
SITE_A:
|
|
143
|
+
network: 192.168.1.0/27
|
|
144
|
+
default_router: 192.168.1.1
|
|
145
|
+
PC-A1:
|
|
146
|
+
dhcp_client: true
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Notes on the schema:
|
|
150
|
+
|
|
151
|
+
* **Ports may be abbreviated.** `g0/0`, `Gi0/0` and `GigabitEthernet0/0` are the
|
|
152
|
+
same port; connections are direction-independent.
|
|
153
|
+
* **Cable types are inferred** from the two device categories, and can be
|
|
154
|
+
overridden per connection with `cable:`.
|
|
155
|
+
* **`components` can carry its own `config:` block**, in which case
|
|
156
|
+
`configurations` is optional. Where both describe a device they are merged,
|
|
157
|
+
and `configurations` wins.
|
|
158
|
+
* **Unknown keys are errors.** A mistyped setting fails the file rather than
|
|
159
|
+
doing nothing quietly.
|
|
160
|
+
|
|
161
|
+
### What a device can be given
|
|
162
|
+
|
|
163
|
+
| section | applies to | keys |
|
|
164
|
+
| ----------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| IOS devices | routers, switches | `hostname`, `interfaces`, `vlans`, `dhcp`, `static_routes`, `default_gateway`, `domain_lookup`, `banner`, `enable_secret`, `extra_cli` |
|
|
166
|
+
| hosts | PCs, servers, printers, laptops | `dhcp_client`, `address`, `gateway`, `dns` |
|
|
167
|
+
| servers | Server-PT only | `services.dns` (records), `services.http` (pages, port) |
|
|
168
|
+
|
|
169
|
+
Interfaces take `address`, `description`, `shutdown`, and for switch ports
|
|
170
|
+
`mode: access|trunk`, `vlan:`, `trunk_vlans:`, plus `extra:` for anything the
|
|
171
|
+
schema does not model.
|
|
172
|
+
|
|
173
|
+
### Bare CLI, for anything the schema does not model
|
|
174
|
+
|
|
175
|
+
`extra_cli` on a router or switch takes raw IOS commands, applied at
|
|
176
|
+
global-config scope after everything else. Write it as a `|` block exactly the
|
|
177
|
+
way `show running-config` would print it: indentation is what tells ptauto a
|
|
178
|
+
line is a submode's child rather than a new global command:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
configurations:
|
|
182
|
+
Router-A:
|
|
183
|
+
extra_cli: |
|
|
184
|
+
line vty 0 4
|
|
185
|
+
login
|
|
186
|
+
transport input telnet
|
|
187
|
+
service timestamps log datetime msec
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
ptauto groups these the same way IOS does: one block per top-level line, with
|
|
191
|
+
its indented children checked against *that* line's section, and enters/exits
|
|
192
|
+
the submode itself, so `line vty 0 4` does not need a trailing `exit`. Each line
|
|
193
|
+
is still verified literally, so a value IOS rewrites on the way in (a plaintext
|
|
194
|
+
`password` once `service password-encryption` is on, the way `enable_secret` is
|
|
195
|
+
always stored hashed) will show as pending on every run rather than being
|
|
196
|
+
reported as converged when it is not; put the one credential ptauto does
|
|
197
|
+
understand in `enable_secret` instead of `extra_cli` for that reason.
|
|
198
|
+
|
|
199
|
+
`ptauto validate` checks all of it before anything is sent to PT: unknown
|
|
200
|
+
models, ports a model does not have, a port cabled twice, duplicate addresses, a
|
|
201
|
+
host whose gateway is outside its own subnet, a DHCP pool pointing at a
|
|
202
|
+
default-router no device owns, host settings on a router, IOS settings on a PC.
|
|
203
|
+
|
|
204
|
+
## Commands
|
|
205
|
+
|
|
206
|
+
| command | what it does |
|
|
207
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
208
|
+
| `ptauto validate SPEC` | parse and cross-check the file; never touches PT |
|
|
209
|
+
| `ptauto plan SPEC` | what `apply` would change, and why (exit 2 if anything would) |
|
|
210
|
+
| `ptauto apply SPEC` | make PT match the file; `--prune` also removes what the file does not describe, `--save FILE.pkt` saves the workspace |
|
|
211
|
+
| `ptauto show [SPEC]` | what PT currently has |
|
|
212
|
+
| `ptauto render SPEC` | the IOS configuration the file implies, without touching PT |
|
|
213
|
+
| `ptauto destroy SPEC` | remove the devices the file describes |
|
|
214
|
+
| `ptauto test SPEC [PATH]` | run a pytest suite against the live network |
|
|
215
|
+
| `ptauto status` | how ptauto can reach PT right now |
|
|
216
|
+
| `ptauto models [TEXT]` | the device models a file can use |
|
|
217
|
+
|
|
218
|
+
## How idempotency is decided
|
|
219
|
+
|
|
220
|
+
Nothing is re-applied blindly. Before each run ptauto reads what PT has and
|
|
221
|
+
compares it with the specification:
|
|
222
|
+
|
|
223
|
+
| part of the spec | what is compared against |
|
|
224
|
+
| ------------------ | -------------------------------------------------------------------------- |
|
|
225
|
+
| devices, positions | the workspace's device list and centre coordinates |
|
|
226
|
+
| cables | the link list, by endpoint pair, direction-independent |
|
|
227
|
+
| IOS configuration | the device's own configuration text, block by block |
|
|
228
|
+
| host addressing | the port's IP/mask, the DHCP flag, and the gateway/DNS in the device's XML |
|
|
229
|
+
| server services | the DNS record database and the HTTP service's state |
|
|
230
|
+
|
|
231
|
+
For IOS devices each piece of configuration is a block with both the commands
|
|
232
|
+
that apply it and the evidence that proves it is already applied, so `no
|
|
233
|
+
shutdown`, which never appears in a configuration, is checked as *the absence of
|
|
234
|
+
`shutdown`* rather than as a line to look for. ptauto reads the device's saved
|
|
235
|
+
configuration, issuing a `write memory` first so that what it reads is what the
|
|
236
|
+
device is actually running, including changes someone made by hand in the GUI.
|
|
237
|
+
|
|
238
|
+
When something genuinely cannot be verified, the plan says `(unverified)`
|
|
239
|
+
instead of claiming the change was needed.
|
|
240
|
+
|
|
241
|
+
## Testing a network
|
|
242
|
+
|
|
243
|
+
The pytest fixtures ship with the package: no `conftest.py` required.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
def test_the_two_sites_can_reach_each_other(pt_network):
|
|
247
|
+
assert pt_network.ping("PC-A1", "PC-B1").ok
|
|
248
|
+
|
|
249
|
+
def test_nothing_has_drifted(pt_network):
|
|
250
|
+
assert pt_network.plan().is_empty
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pytest --pt-spec examples/two-site-guest-wifi.yaml tests_network/two_site/
|
|
255
|
+
pytest --pt-spec examples/two-site-guest-wifi.yaml --pt-apply tests_network/two_site/ # build first
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| fixture | what it gives |
|
|
259
|
+
| ------------ | ------------------------------------------------------------------------------------------------------ |
|
|
260
|
+
| `pt_network` | the network under test: `ping`, `ping_hostname`, `host`, `interface`, `services`, `plan`, `address_of` |
|
|
261
|
+
| `pt_client` | the raw `PTClient` for anything the facade does not cover |
|
|
262
|
+
| `pt_spec` | the parsed specification |
|
|
263
|
+
| `pt_ping` | `pt_ping("A", "B")`, asserts, with a readable failure |
|
|
264
|
+
|
|
265
|
+
Options: `--pt-spec PATH`, `--pt-apply` (build before testing), `--pt-require`
|
|
266
|
+
(fail instead of skip when PT is not running). Without `--pt-require` a suite
|
|
267
|
+
skips when Packet Tracer is closed, so it stays runnable in CI.
|
|
268
|
+
|
|
269
|
+
`pt_network.ping` repeats a partial result once: the first packet between two
|
|
270
|
+
hosts is always lost to ARP resolution in Packet Tracer, and that is a warm-up
|
|
271
|
+
artefact rather than a fault. `pt_network.ping(..., retry_partial=False)` shows
|
|
272
|
+
the raw first attempt.
|
|
273
|
+
|
|
274
|
+
## Using it as a library
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from ptauto import load_spec, PTClient, Planner, Applier
|
|
278
|
+
|
|
279
|
+
spec = load_spec("network.yaml")
|
|
280
|
+
client = PTClient()
|
|
281
|
+
|
|
282
|
+
plan = Planner(client, spec).build()
|
|
283
|
+
for action in plan.actions:
|
|
284
|
+
print(action.summary, ":", action.reason)
|
|
285
|
+
|
|
286
|
+
report = Applier(client).run(plan)
|
|
287
|
+
print(report.ok, len(report.applied))
|
|
288
|
+
|
|
289
|
+
print(client.ping("PC-Admin1", "192.168.30.2").verdict)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Layout
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
src/ptauto/
|
|
296
|
+
transport.py the two channels into Packet Tracer (reused from packet-tracer-mcp)
|
|
297
|
+
client.py typed operations: topology, devices, links, IOS, hosts, services, ping
|
|
298
|
+
model.py the YAML schema
|
|
299
|
+
loader.py parsing, merging and cross-validation
|
|
300
|
+
ios.py IOS rendering, and the evidence that proves it is applied
|
|
301
|
+
plan.py the diff engine
|
|
302
|
+
apply.py execution, in an order the network can survive
|
|
303
|
+
testing.py the facade a test suite talks to
|
|
304
|
+
pytest_plugin.py the fixtures, registered as a pytest plugin
|
|
305
|
+
cli.py the `ptauto` command
|
|
306
|
+
examples/ a worked specification
|
|
307
|
+
tests/ unit tests, no Packet Tracer needed
|
|
308
|
+
tests_network/ acceptance tests, run against a live Packet Tracer
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Run them with `uv run pytest`.
|
|
312
|
+
|
|
313
|
+
## Continuous integration and releasing
|
|
314
|
+
|
|
315
|
+
`.github/workflows/ci.yml` runs on every push and pull request: the offline
|
|
316
|
+
unit suite (`tests/`) on Python 3.12 and 3.13, `ptauto validate` against the
|
|
317
|
+
example spec, and a packaging check (`uv build` + `twine check --strict`).
|
|
318
|
+
`tests_network/` (the acceptance suite) is deliberately not run there: it
|
|
319
|
+
needs a real, GUI Packet Tracer instance with the MCP Control Center extension
|
|
320
|
+
open, which no hosted runner can provide.
|
|
321
|
+
|
|
322
|
+
Versioning is automatic and driven by [Conventional Commits][conventional-commits]
|
|
323
|
+
on `main`: `fix:` bumps the patch version, `feat:` bumps minor, and a
|
|
324
|
+
`BREAKING CHANGE:` footer (or `!` after the type, e.g. `feat!:`) bumps major.
|
|
325
|
+
Commits that don't match a recognized type (or with no releasable change)
|
|
326
|
+
don't trigger a release. `.github/workflows/release.yml` runs
|
|
327
|
+
[python-semantic-release][python-semantic-release] after `ci.yml` succeeds on
|
|
328
|
+
`main`; when a release is due it bumps `version` in `pyproject.toml`, updates
|
|
329
|
+
`CHANGELOG.md`, commits that as `chore(release): X.Y.Z [skip ci]`, and pushes
|
|
330
|
+
a `vX.Y.Z` tag with a matching GitHub Release.
|
|
331
|
+
|
|
332
|
+
`.github/workflows/publish.yml` builds and publishes to PyPI whenever a
|
|
333
|
+
`vX.Y.Z` tag is pushed, i.e. automatically, right after release.yml creates
|
|
334
|
+
one, using [trusted publishing][trusted-publishing]: OIDC, not a stored API
|
|
335
|
+
token, so there is no secret in this repository to rotate or leak. That needs
|
|
336
|
+
a one-time link on PyPI's side before the first release:
|
|
337
|
+
|
|
338
|
+
1. Push this repository to GitHub and publish a release manually (or
|
|
339
|
+
[create the PyPI project first][first-release] some other way), so
|
|
340
|
+
`amorale-ptauto` exists on PyPI to attach a publisher to.
|
|
341
|
+
2. On [pypi.org][pypi-publishing], under the project's *Publishing* settings,
|
|
342
|
+
add a trusted publisher with:
|
|
343
|
+
- Owner: `AlessioMorale`
|
|
344
|
+
- Repository name: `amorale-ptauto`
|
|
345
|
+
- Workflow name: `publish.yml`
|
|
346
|
+
- Environment name: `pypi`
|
|
347
|
+
3. From then on, every merge to `main` with a releasable Conventional Commit
|
|
348
|
+
gets tagged and published automatically: no manual version bump, tag, or
|
|
349
|
+
PyPI-side action needed.
|
|
350
|
+
|
|
351
|
+
[trusted-publishing]: https://docs.pypi.org/trusted-publishers/
|
|
352
|
+
[pypi-publishing]: https://pypi.org/manage/account/publishing/
|
|
353
|
+
[first-release]: https://docs.pypi.org/trusted-publishers/adding-a-publisher/#adding-a-pending-trusted-publisher-for-pypi
|
|
354
|
+
[conventional-commits]: https://www.conventionalcommits.org/
|
|
355
|
+
[python-semantic-release]: https://python-semantic-release.readthedocs.io/
|
|
356
|
+
|
|
357
|
+
`.github/dependabot.yml` keeps both the Python dependencies and the workflow
|
|
358
|
+
actions themselves on a weekly update check.
|
|
359
|
+
|
|
360
|
+
## Known limits
|
|
361
|
+
|
|
362
|
+
* Packet Tracer must be open with the extension loaded; there is no headless mode.
|
|
363
|
+
* An error inside PT's Script Engine opens a modal dialog that freezes the
|
|
364
|
+
bridge until it is dismissed. ptauto guards every command it sends, so it does
|
|
365
|
+
not cause one, but a dialog opened by something else will make ptauto time
|
|
366
|
+
out; `ptauto status` will say so.
|
|
367
|
+
* `--prune` never removes PT's own infrastructure objects (the power
|
|
368
|
+
distribution device), and by default a model mismatch is reported rather than
|
|
369
|
+
replaced.
|