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.
Files changed (35) hide show
  1. amorale_ptauto-1.1.2.dev0/.github/dependabot.yml +16 -0
  2. amorale_ptauto-1.1.2.dev0/.github/workflows/ci.yml +71 -0
  3. amorale_ptauto-1.1.2.dev0/.github/workflows/publish.yml +85 -0
  4. amorale_ptauto-1.1.2.dev0/.github/workflows/release.yml +58 -0
  5. amorale_ptauto-1.1.2.dev0/.gitignore +10 -0
  6. amorale_ptauto-1.1.2.dev0/.python-version +1 -0
  7. amorale_ptauto-1.1.2.dev0/CHANGELOG.md +7 -0
  8. amorale_ptauto-1.1.2.dev0/LICENSE +21 -0
  9. amorale_ptauto-1.1.2.dev0/PKG-INFO +369 -0
  10. amorale_ptauto-1.1.2.dev0/README.md +335 -0
  11. amorale_ptauto-1.1.2.dev0/examples/two-site-guest-wifi.yaml +143 -0
  12. amorale_ptauto-1.1.2.dev0/pyproject.toml +130 -0
  13. amorale_ptauto-1.1.2.dev0/requirements.md +17 -0
  14. amorale_ptauto-1.1.2.dev0/src/ptauto/__init__.py +57 -0
  15. amorale_ptauto-1.1.2.dev0/src/ptauto/apply.py +208 -0
  16. amorale_ptauto-1.1.2.dev0/src/ptauto/catalog.py +157 -0
  17. amorale_ptauto-1.1.2.dev0/src/ptauto/cli.py +419 -0
  18. amorale_ptauto-1.1.2.dev0/src/ptauto/client.py +597 -0
  19. amorale_ptauto-1.1.2.dev0/src/ptauto/errors.py +32 -0
  20. amorale_ptauto-1.1.2.dev0/src/ptauto/ios.py +396 -0
  21. amorale_ptauto-1.1.2.dev0/src/ptauto/loader.py +377 -0
  22. amorale_ptauto-1.1.2.dev0/src/ptauto/model.py +360 -0
  23. amorale_ptauto-1.1.2.dev0/src/ptauto/plan.py +542 -0
  24. amorale_ptauto-1.1.2.dev0/src/ptauto/py.typed +0 -0
  25. amorale_ptauto-1.1.2.dev0/src/ptauto/pytest_plugin.py +140 -0
  26. amorale_ptauto-1.1.2.dev0/src/ptauto/testing.py +155 -0
  27. amorale_ptauto-1.1.2.dev0/src/ptauto/transport.py +244 -0
  28. amorale_ptauto-1.1.2.dev0/tests/conftest.py +98 -0
  29. amorale_ptauto-1.1.2.dev0/tests/test_catalog.py +68 -0
  30. amorale_ptauto-1.1.2.dev0/tests/test_ios.py +234 -0
  31. amorale_ptauto-1.1.2.dev0/tests/test_loader.py +179 -0
  32. amorale_ptauto-1.1.2.dev0/tests/test_plan.py +301 -0
  33. amorale_ptauto-1.1.2.dev0/tests/test_transport.py +90 -0
  34. amorale_ptauto-1.1.2.dev0/tests_network/two_site/test_two_site_guest_wifi.py +99 -0
  35. 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,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ dist/
6
+ *.egg-info/
7
+ .DS_Store
8
+ .mypy_cache/
9
+ .vscode/
10
+ build/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,7 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
4
+
5
+ ## v1.0.0 (2026-09-19)
6
+
7
+ - Initial Release
@@ -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
+ [![CI](https://github.com/AlessioMorale/amorale-ptauto/actions/workflows/ci.yml/badge.svg)](https://github.com/AlessioMorale/amorale-ptauto/actions/workflows/ci.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/amorale-ptauto.svg)](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.