netcanon 0.1.0__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.
- netcanon-0.1.0/LICENSE +21 -0
- netcanon-0.1.0/PKG-INFO +185 -0
- netcanon-0.1.0/README.md +130 -0
- netcanon-0.1.0/netcanon/__init__.py +23 -0
- netcanon-0.1.0/netcanon/api/__init__.py +1 -0
- netcanon-0.1.0/netcanon/api/deps.py +84 -0
- netcanon-0.1.0/netcanon/api/routes/__init__.py +1 -0
- netcanon-0.1.0/netcanon/api/routes/_migration_helpers.py +183 -0
- netcanon-0.1.0/netcanon/api/routes/backups.py +507 -0
- netcanon-0.1.0/netcanon/api/routes/configs.py +293 -0
- netcanon-0.1.0/netcanon/api/routes/definitions.py +91 -0
- netcanon-0.1.0/netcanon/api/routes/device_profiles.py +172 -0
- netcanon-0.1.0/netcanon/api/routes/health.py +43 -0
- netcanon-0.1.0/netcanon/api/routes/migration.py +677 -0
- netcanon-0.1.0/netcanon/api/routes/sanitize.py +84 -0
- netcanon-0.1.0/netcanon/api/routes/schedules.py +342 -0
- netcanon-0.1.0/netcanon/api/routes/ui.py +503 -0
- netcanon-0.1.0/netcanon/cli.py +142 -0
- netcanon-0.1.0/netcanon/collectors/__init__.py +25 -0
- netcanon-0.1.0/netcanon/collectors/base.py +132 -0
- netcanon-0.1.0/netcanon/collectors/netmiko_collector.py +210 -0
- netcanon-0.1.0/netcanon/collectors/paramiko_collector.py +438 -0
- netcanon-0.1.0/netcanon/collectors/probe.py +107 -0
- netcanon-0.1.0/netcanon/config.py +85 -0
- netcanon-0.1.0/netcanon/definitions/__init__.py +29 -0
- netcanon-0.1.0/netcanon/definitions/loader.py +282 -0
- netcanon-0.1.0/netcanon/definitions/schema.py +268 -0
- netcanon-0.1.0/netcanon/logging_config.py +165 -0
- netcanon-0.1.0/netcanon/main.py +288 -0
- netcanon-0.1.0/netcanon/migration/__init__.py +76 -0
- netcanon-0.1.0/netcanon/migration/_naming.py +72 -0
- netcanon-0.1.0/netcanon/migration/_tier3_detection.py +215 -0
- netcanon-0.1.0/netcanon/migration/_user_secrets.py +228 -0
- netcanon-0.1.0/netcanon/migration/canonical/__init__.py +8 -0
- netcanon-0.1.0/netcanon/migration/canonical/intent.py +640 -0
- netcanon-0.1.0/netcanon/migration/canonical/loader.py +61 -0
- netcanon-0.1.0/netcanon/migration/canonical/local_user_names.py +275 -0
- netcanon-0.1.0/netcanon/migration/canonical/port_names.py +614 -0
- netcanon-0.1.0/netcanon/migration/canonical/snmp_names.py +273 -0
- netcanon-0.1.0/netcanon/migration/canonical/snmpv3_user_names.py +277 -0
- netcanon-0.1.0/netcanon/migration/canonical/transforms.py +373 -0
- netcanon-0.1.0/netcanon/migration/canonical/vlan_names.py +364 -0
- netcanon-0.1.0/netcanon/migration/codecs/__init__.py +14 -0
- netcanon-0.1.0/netcanon/migration/codecs/_mock/__init__.py +18 -0
- netcanon-0.1.0/netcanon/migration/codecs/_mock/codec.py +153 -0
- netcanon-0.1.0/netcanon/migration/codecs/arista_eos/__init__.py +53 -0
- netcanon-0.1.0/netcanon/migration/codecs/arista_eos/codec.py +302 -0
- netcanon-0.1.0/netcanon/migration/codecs/arista_eos/parse.py +1145 -0
- netcanon-0.1.0/netcanon/migration/codecs/arista_eos/port_names.py +190 -0
- netcanon-0.1.0/netcanon/migration/codecs/arista_eos/render.py +765 -0
- netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/__init__.py +63 -0
- netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/_svi_absorption.py +111 -0
- netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/codec.py +301 -0
- netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/parse.py +1037 -0
- netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/port_names.py +219 -0
- netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/render.py +771 -0
- netcanon-0.1.0/netcanon/migration/codecs/base.py +364 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe/__init__.py +20 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe/codec.py +1184 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/__init__.py +28 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/codec.py +492 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/parse.py +1369 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/port_names.py +321 -0
- netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/render.py +684 -0
- netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/__init__.py +61 -0
- netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/codec.py +280 -0
- netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/parse.py +860 -0
- netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/port_names.py +317 -0
- netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/render.py +858 -0
- netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/vlan_heuristics.py +156 -0
- netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/__init__.py +81 -0
- netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/codec.py +309 -0
- netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/parse.py +2095 -0
- netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/port_names.py +191 -0
- netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/render.py +1262 -0
- netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/__init__.py +43 -0
- netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/codec.py +297 -0
- netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/parse.py +1022 -0
- netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/port_names.py +345 -0
- netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/render.py +866 -0
- netcanon-0.1.0/netcanon/migration/codecs/opnsense/__init__.py +38 -0
- netcanon-0.1.0/netcanon/migration/codecs/opnsense/codec.py +325 -0
- netcanon-0.1.0/netcanon/migration/codecs/opnsense/parse.py +630 -0
- netcanon-0.1.0/netcanon/migration/codecs/opnsense/port_names.py +201 -0
- netcanon-0.1.0/netcanon/migration/codecs/opnsense/render.py +570 -0
- netcanon-0.1.0/netcanon/migration/codecs/registry.py +72 -0
- netcanon-0.1.0/netcanon/migration/target_profiles.py +544 -0
- netcanon-0.1.0/netcanon/migration/vendors/__init__.py +82 -0
- netcanon-0.1.0/netcanon/models/__init__.py +47 -0
- netcanon-0.1.0/netcanon/models/backup.py +109 -0
- netcanon-0.1.0/netcanon/models/device.py +76 -0
- netcanon-0.1.0/netcanon/models/device_profile.py +144 -0
- netcanon-0.1.0/netcanon/models/diff.py +124 -0
- netcanon-0.1.0/netcanon/models/migration.py +712 -0
- netcanon-0.1.0/netcanon/models/schedule.py +103 -0
- netcanon-0.1.0/netcanon/models/validators.py +29 -0
- netcanon-0.1.0/netcanon/security/__init__.py +1 -0
- netcanon-0.1.0/netcanon/security/credentials.py +101 -0
- netcanon-0.1.0/netcanon/security/migration.py +35 -0
- netcanon-0.1.0/netcanon/services/__init__.py +1 -0
- netcanon-0.1.0/netcanon/services/diff.py +251 -0
- netcanon-0.1.0/netcanon/services/migration_detect.py +117 -0
- netcanon-0.1.0/netcanon/services/migration_pipeline.py +706 -0
- netcanon-0.1.0/netcanon/services/migration_validate.py +243 -0
- netcanon-0.1.0/netcanon/storage/__init__.py +13 -0
- netcanon-0.1.0/netcanon/storage/base.py +95 -0
- netcanon-0.1.0/netcanon/storage/device_profile_store.py +108 -0
- netcanon-0.1.0/netcanon/storage/file_store.py +337 -0
- netcanon-0.1.0/netcanon/storage/job_store.py +74 -0
- netcanon-0.1.0/netcanon/storage/schedule_store.py +115 -0
- netcanon-0.1.0/netcanon/templates/base.html +507 -0
- netcanon-0.1.0/netcanon/templates/configs.html +311 -0
- netcanon-0.1.0/netcanon/templates/definitions.html +918 -0
- netcanon-0.1.0/netcanon/templates/devices.html +509 -0
- netcanon-0.1.0/netcanon/templates/diff.html +284 -0
- netcanon-0.1.0/netcanon/templates/index.html +350 -0
- netcanon-0.1.0/netcanon/templates/jobs.html +181 -0
- netcanon-0.1.0/netcanon/templates/migrate.html +2281 -0
- netcanon-0.1.0/netcanon/templates/schedules.html +294 -0
- netcanon-0.1.0/netcanon/tools/__init__.py +13 -0
- netcanon-0.1.0/netcanon/tools/sanitize.py +486 -0
- netcanon-0.1.0/netcanon.egg-info/PKG-INFO +185 -0
- netcanon-0.1.0/netcanon.egg-info/SOURCES.txt +138 -0
- netcanon-0.1.0/netcanon.egg-info/dependency_links.txt +1 -0
- netcanon-0.1.0/netcanon.egg-info/entry_points.txt +2 -0
- netcanon-0.1.0/netcanon.egg-info/requires.txt +31 -0
- netcanon-0.1.0/netcanon.egg-info/top_level.txt +2 -0
- netcanon-0.1.0/netcanon_desktop/__init__.py +22 -0
- netcanon-0.1.0/netcanon_desktop/__main__.py +100 -0
- netcanon-0.1.0/netcanon_desktop/app.py +161 -0
- netcanon-0.1.0/netcanon_desktop/icons.py +119 -0
- netcanon-0.1.0/netcanon_desktop/preferences.py +117 -0
- netcanon-0.1.0/netcanon_desktop/preferences_dialog.py +311 -0
- netcanon-0.1.0/netcanon_desktop/server.py +150 -0
- netcanon-0.1.0/netcanon_desktop/settings.py +98 -0
- netcanon-0.1.0/netcanon_desktop/single_instance.py +77 -0
- netcanon-0.1.0/netcanon_desktop/tray.py +136 -0
- netcanon-0.1.0/netcanon_desktop/window.py +189 -0
- netcanon-0.1.0/pyproject.toml +114 -0
- netcanon-0.1.0/setup.cfg +4 -0
netcanon-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Netcanon contributors
|
|
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.
|
netcanon-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: netcanon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-vendor network config translator with a verifiable cross-vendor audit
|
|
5
|
+
Author: Netcanon contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/netcanon/netcanon
|
|
8
|
+
Project-URL: Repository, https://github.com/netcanon/netcanon
|
|
9
|
+
Project-URL: Issues, https://github.com/netcanon/netcanon/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/netcanon/netcanon/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://github.com/netcanon/netcanon/blob/main/README.md
|
|
12
|
+
Keywords: network-automation,network-configuration,cisco,juniper,fortinet,aruba,mikrotik,opnsense,arista,vendor-translation,config-migration
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: fastapi>=0.115.0
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0.0
|
|
29
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
30
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Requires-Dist: netmiko>=4.4.0
|
|
32
|
+
Requires-Dist: paramiko>=3.4.0
|
|
33
|
+
Requires-Dist: jinja2>=3.1.0
|
|
34
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
35
|
+
Requires-Dist: aiofiles>=23.0.0
|
|
36
|
+
Requires-Dist: apscheduler>=3.10.4
|
|
37
|
+
Requires-Dist: cryptography>=41.0.0
|
|
38
|
+
Requires-Dist: keyring>=24.0.0
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-playwright>=0.5.0; extra == "dev"
|
|
43
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
|
|
45
|
+
Requires-Dist: coverage>=7.4.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
47
|
+
Provides-Extra: desktop
|
|
48
|
+
Requires-Dist: PySide6>=6.7.0; extra == "desktop"
|
|
49
|
+
Requires-Dist: pystray>=0.19.0; extra == "desktop"
|
|
50
|
+
Requires-Dist: Pillow>=10.0.0; extra == "desktop"
|
|
51
|
+
Provides-Extra: desktop-build
|
|
52
|
+
Requires-Dist: netcanon[desktop]; extra == "desktop-build"
|
|
53
|
+
Requires-Dist: cx_Freeze>=7.0.0; extra == "desktop-build"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# Netcanon
|
|
57
|
+
|
|
58
|
+
**Multi-vendor network config translator with a verifiable cross-vendor audit.**
|
|
59
|
+
|
|
60
|
+
Translates running-config between Cisco IOS-XE, Juniper Junos, Aruba
|
|
61
|
+
AOS-S, Arista EOS, FortiGate, MikroTik RouterOS, and OPNsense.
|
|
62
|
+
Per-field capability declarations and a cross-mesh audit catch silent
|
|
63
|
+
translation errors before they ship. Explicit Tier-3 boundary on
|
|
64
|
+
firewall / NAT / VPN / QoS — see
|
|
65
|
+
[`docs/CAPABILITIES.md`](docs/CAPABILITIES.md). See also
|
|
66
|
+
[`docs/COMPARISON.md`](docs/COMPARISON.md) for positioning vs Batfish /
|
|
67
|
+
Capirca / NAPALM and the rest of the network-automation landscape.
|
|
68
|
+
|
|
69
|
+
Two concerns, one FastAPI application:
|
|
70
|
+
|
|
71
|
+
1. **Backup** — pull `running-config` (or vendor equivalent) from network
|
|
72
|
+
devices over SSH / NETCONF / REST, store verbatim in
|
|
73
|
+
`configs/<hostname>.<ext>`. Runs on a schedule or on demand.
|
|
74
|
+
2. **Migration** — translate a stored backup from one vendor's config
|
|
75
|
+
grammar to another through a shared canonical intent tree. Cisco
|
|
76
|
+
IOS-XE → Aruba AOS-S, FortiGate → OPNsense, etc.
|
|
77
|
+
|
|
78
|
+
Ships on two platforms kept at strict feature parity:
|
|
79
|
+
|
|
80
|
+
| Platform | Package | Entry point |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| Web (browser) | `netcanon/` | `uvicorn netcanon.main:app` |
|
|
83
|
+
| Desktop (Windows) | `netcanon_desktop/` | `python -m netcanon_desktop` |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Quickstart
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install -e ".[dev]"
|
|
91
|
+
uvicorn netcanon.main:app --host 127.0.0.1 --port 8000
|
|
92
|
+
# -> http://127.0.0.1:8000 (UI)
|
|
93
|
+
# -> http://127.0.0.1:8000/docs (Swagger)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Desktop shell:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install -e ".[desktop]"
|
|
100
|
+
python -m netcanon_desktop
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run the test suite:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pytest # unit + integration + desktop (fast)
|
|
107
|
+
pytest -m e2e # Playwright browser tests (slower)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Tests run across four layers: unit (pure functions, no I/O — the
|
|
111
|
+
real-capture validation harness lives here as a unit subset),
|
|
112
|
+
integration (TestClient + mocked SSH), e2e (Playwright against a
|
|
113
|
+
live Uvicorn), and desktop (PySide6 + pystray mocked). CI output
|
|
114
|
+
is the source of truth for pass counts.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Where to go next
|
|
119
|
+
|
|
120
|
+
| You want to… | Start here |
|
|
121
|
+
|---|---|
|
|
122
|
+
| Understand the architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) — four-layer model, canonical bridge, codec types |
|
|
123
|
+
| Follow the contributor rules | [`CLAUDE.md`](CLAUDE.md) — hard rules, parity checklist, gotchas |
|
|
124
|
+
| Look up project jargon | [`docs/glossary.md`](docs/glossary.md) — canonical, codec, mesh, ship-before-wire, target profile, etc. |
|
|
125
|
+
| Read the canonical model overview | [`netcanon/migration/canonical/README.md`](netcanon/migration/canonical/README.md) — Tier 1 / 2 / 3 fields and promotion rules |
|
|
126
|
+
| Add or change an HTTP route | [`netcanon/api/routes/README.md`](netcanon/api/routes/README.md) — frozen pipeline-stage signatures, endpoint inventory |
|
|
127
|
+
| Add a new codec (vendor parser/renderer) | [`netcanon/migration/codecs/README.md`](netcanon/migration/codecs/README.md) |
|
|
128
|
+
| Add a new device definition / target profile | [`definitions/README.md`](definitions/README.md) — layered definitions (family base + os_version / model overlays), target-profile module-variant schema |
|
|
129
|
+
| Add a new canonical field | [`docs/adding-a-canonical-field.md`](docs/adding-a-canonical-field.md) — MTU as a worked example |
|
|
130
|
+
| Add a new target-profile YAML | [`docs/adding-a-target-profile.md`](docs/adding-a-target-profile.md) — flat-port + module-variant shapes, fit-check propagation |
|
|
131
|
+
| Ship a feature across web + desktop | [`docs/feature-parity-walkthrough.md`](docs/feature-parity-walkthrough.md) — SNMPv3 USM rename as a worked example |
|
|
132
|
+
| See what's shipped recently / current state | [`CHANGELOG.md`](CHANGELOG.md) — authoritative per-wave shipping log |
|
|
133
|
+
| Read the slower-changing architectural sketch | [`translator-plans.txt`](translator-plans.txt) — dense, grep-friendly long-term roadmap; most R / GAP / Phase items now `[SHIPPED]` |
|
|
134
|
+
| Check codec certification tiers | [`tests/fixtures/real/RESULTS.md`](tests/fixtures/real/RESULTS.md) |
|
|
135
|
+
| Manually exercise recent changes | [`HUMAN_TESTING.md`](HUMAN_TESTING.md) |
|
|
136
|
+
| Write tests | [`tests/README.md`](tests/README.md) |
|
|
137
|
+
| Review the security model | [`SECURITY.md`](SECURITY.md) — threat model, controls, known limitations |
|
|
138
|
+
| Look up what Netcanon translates and what it doesn't | [`docs/CAPABILITIES.md`](docs/CAPABILITIES.md) — operator-facing capabilities, per-codec unsupported/lossy paths, Tier-3 boundary, notification surfaces |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Layout
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
netcanon/ FastAPI application (shared by both platforms)
|
|
146
|
+
├── api/routes/ HTTP endpoints (backups, migration, configs, …)
|
|
147
|
+
├── collectors/ SSH/NETCONF/REST fetchers — one factory,
|
|
148
|
+
│ one mock-point (`get_collector`)
|
|
149
|
+
├── migration/ Cross-vendor translation pipeline
|
|
150
|
+
│ ├── canonical/ CanonicalIntent model + shared transforms
|
|
151
|
+
│ ├── codecs/ Per-vendor parse/render implementations
|
|
152
|
+
│ └── ...
|
|
153
|
+
├── services/ Plain-function orchestrators (pipeline, detect, …)
|
|
154
|
+
├── storage/ FileConfigStore
|
|
155
|
+
└── templates/ Jinja2 templates (every interactive element
|
|
156
|
+
must carry a data-testid — see CLAUDE.md)
|
|
157
|
+
|
|
158
|
+
netcanon_desktop/ Windows tray/webview shell around the same server
|
|
159
|
+
definitions/ Device definition YAMLs (shared with backup layer)
|
|
160
|
+
tests/unit/ Pure-function tests, no I/O
|
|
161
|
+
tests/integration/ FastAPI TestClient tests, SSH mocked at get_collector
|
|
162
|
+
tests/e2e/ Playwright browser tests against a live Uvicorn
|
|
163
|
+
tests/desktop/ PySide6/pystray-mocked desktop shell tests
|
|
164
|
+
tests/fixtures/real/ Real-capture validation corpus (see RESULTS.md)
|
|
165
|
+
scripts/ One-off utilities (Aruba template renderer, …)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Certification status
|
|
171
|
+
|
|
172
|
+
Per-codec certainty (read from `CanonicalCodec.certainty` at module load,
|
|
173
|
+
surfaced via `GET /api/v1/migration/adapters`). See
|
|
174
|
+
[`tests/fixtures/real/RESULTS.md`](tests/fixtures/real/RESULTS.md) for
|
|
175
|
+
the live per-codec status — RESULTS.md is the source of truth and this
|
|
176
|
+
README intentionally omits per-codec counts to avoid drift.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
See [`SECURITY.md`](SECURITY.md) for responsible-disclosure policy.
|
|
183
|
+
Project licence is per-file (most files are MIT; third-party fixtures
|
|
184
|
+
keep their upstream licences — see
|
|
185
|
+
[`tests/fixtures/real/NOTICE.md`](tests/fixtures/real/NOTICE.md)).
|
netcanon-0.1.0/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Netcanon
|
|
2
|
+
|
|
3
|
+
**Multi-vendor network config translator with a verifiable cross-vendor audit.**
|
|
4
|
+
|
|
5
|
+
Translates running-config between Cisco IOS-XE, Juniper Junos, Aruba
|
|
6
|
+
AOS-S, Arista EOS, FortiGate, MikroTik RouterOS, and OPNsense.
|
|
7
|
+
Per-field capability declarations and a cross-mesh audit catch silent
|
|
8
|
+
translation errors before they ship. Explicit Tier-3 boundary on
|
|
9
|
+
firewall / NAT / VPN / QoS — see
|
|
10
|
+
[`docs/CAPABILITIES.md`](docs/CAPABILITIES.md). See also
|
|
11
|
+
[`docs/COMPARISON.md`](docs/COMPARISON.md) for positioning vs Batfish /
|
|
12
|
+
Capirca / NAPALM and the rest of the network-automation landscape.
|
|
13
|
+
|
|
14
|
+
Two concerns, one FastAPI application:
|
|
15
|
+
|
|
16
|
+
1. **Backup** — pull `running-config` (or vendor equivalent) from network
|
|
17
|
+
devices over SSH / NETCONF / REST, store verbatim in
|
|
18
|
+
`configs/<hostname>.<ext>`. Runs on a schedule or on demand.
|
|
19
|
+
2. **Migration** — translate a stored backup from one vendor's config
|
|
20
|
+
grammar to another through a shared canonical intent tree. Cisco
|
|
21
|
+
IOS-XE → Aruba AOS-S, FortiGate → OPNsense, etc.
|
|
22
|
+
|
|
23
|
+
Ships on two platforms kept at strict feature parity:
|
|
24
|
+
|
|
25
|
+
| Platform | Package | Entry point |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Web (browser) | `netcanon/` | `uvicorn netcanon.main:app` |
|
|
28
|
+
| Desktop (Windows) | `netcanon_desktop/` | `python -m netcanon_desktop` |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
uvicorn netcanon.main:app --host 127.0.0.1 --port 8000
|
|
37
|
+
# -> http://127.0.0.1:8000 (UI)
|
|
38
|
+
# -> http://127.0.0.1:8000/docs (Swagger)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Desktop shell:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[desktop]"
|
|
45
|
+
python -m netcanon_desktop
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run the test suite:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pytest # unit + integration + desktop (fast)
|
|
52
|
+
pytest -m e2e # Playwright browser tests (slower)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Tests run across four layers: unit (pure functions, no I/O — the
|
|
56
|
+
real-capture validation harness lives here as a unit subset),
|
|
57
|
+
integration (TestClient + mocked SSH), e2e (Playwright against a
|
|
58
|
+
live Uvicorn), and desktop (PySide6 + pystray mocked). CI output
|
|
59
|
+
is the source of truth for pass counts.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Where to go next
|
|
64
|
+
|
|
65
|
+
| You want to… | Start here |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Understand the architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) — four-layer model, canonical bridge, codec types |
|
|
68
|
+
| Follow the contributor rules | [`CLAUDE.md`](CLAUDE.md) — hard rules, parity checklist, gotchas |
|
|
69
|
+
| Look up project jargon | [`docs/glossary.md`](docs/glossary.md) — canonical, codec, mesh, ship-before-wire, target profile, etc. |
|
|
70
|
+
| Read the canonical model overview | [`netcanon/migration/canonical/README.md`](netcanon/migration/canonical/README.md) — Tier 1 / 2 / 3 fields and promotion rules |
|
|
71
|
+
| Add or change an HTTP route | [`netcanon/api/routes/README.md`](netcanon/api/routes/README.md) — frozen pipeline-stage signatures, endpoint inventory |
|
|
72
|
+
| Add a new codec (vendor parser/renderer) | [`netcanon/migration/codecs/README.md`](netcanon/migration/codecs/README.md) |
|
|
73
|
+
| Add a new device definition / target profile | [`definitions/README.md`](definitions/README.md) — layered definitions (family base + os_version / model overlays), target-profile module-variant schema |
|
|
74
|
+
| Add a new canonical field | [`docs/adding-a-canonical-field.md`](docs/adding-a-canonical-field.md) — MTU as a worked example |
|
|
75
|
+
| Add a new target-profile YAML | [`docs/adding-a-target-profile.md`](docs/adding-a-target-profile.md) — flat-port + module-variant shapes, fit-check propagation |
|
|
76
|
+
| Ship a feature across web + desktop | [`docs/feature-parity-walkthrough.md`](docs/feature-parity-walkthrough.md) — SNMPv3 USM rename as a worked example |
|
|
77
|
+
| See what's shipped recently / current state | [`CHANGELOG.md`](CHANGELOG.md) — authoritative per-wave shipping log |
|
|
78
|
+
| Read the slower-changing architectural sketch | [`translator-plans.txt`](translator-plans.txt) — dense, grep-friendly long-term roadmap; most R / GAP / Phase items now `[SHIPPED]` |
|
|
79
|
+
| Check codec certification tiers | [`tests/fixtures/real/RESULTS.md`](tests/fixtures/real/RESULTS.md) |
|
|
80
|
+
| Manually exercise recent changes | [`HUMAN_TESTING.md`](HUMAN_TESTING.md) |
|
|
81
|
+
| Write tests | [`tests/README.md`](tests/README.md) |
|
|
82
|
+
| Review the security model | [`SECURITY.md`](SECURITY.md) — threat model, controls, known limitations |
|
|
83
|
+
| Look up what Netcanon translates and what it doesn't | [`docs/CAPABILITIES.md`](docs/CAPABILITIES.md) — operator-facing capabilities, per-codec unsupported/lossy paths, Tier-3 boundary, notification surfaces |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Layout
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
netcanon/ FastAPI application (shared by both platforms)
|
|
91
|
+
├── api/routes/ HTTP endpoints (backups, migration, configs, …)
|
|
92
|
+
├── collectors/ SSH/NETCONF/REST fetchers — one factory,
|
|
93
|
+
│ one mock-point (`get_collector`)
|
|
94
|
+
├── migration/ Cross-vendor translation pipeline
|
|
95
|
+
│ ├── canonical/ CanonicalIntent model + shared transforms
|
|
96
|
+
│ ├── codecs/ Per-vendor parse/render implementations
|
|
97
|
+
│ └── ...
|
|
98
|
+
├── services/ Plain-function orchestrators (pipeline, detect, …)
|
|
99
|
+
├── storage/ FileConfigStore
|
|
100
|
+
└── templates/ Jinja2 templates (every interactive element
|
|
101
|
+
must carry a data-testid — see CLAUDE.md)
|
|
102
|
+
|
|
103
|
+
netcanon_desktop/ Windows tray/webview shell around the same server
|
|
104
|
+
definitions/ Device definition YAMLs (shared with backup layer)
|
|
105
|
+
tests/unit/ Pure-function tests, no I/O
|
|
106
|
+
tests/integration/ FastAPI TestClient tests, SSH mocked at get_collector
|
|
107
|
+
tests/e2e/ Playwright browser tests against a live Uvicorn
|
|
108
|
+
tests/desktop/ PySide6/pystray-mocked desktop shell tests
|
|
109
|
+
tests/fixtures/real/ Real-capture validation corpus (see RESULTS.md)
|
|
110
|
+
scripts/ One-off utilities (Aruba template renderer, …)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Certification status
|
|
116
|
+
|
|
117
|
+
Per-codec certainty (read from `CanonicalCodec.certainty` at module load,
|
|
118
|
+
surfaced via `GET /api/v1/migration/adapters`). See
|
|
119
|
+
[`tests/fixtures/real/RESULTS.md`](tests/fixtures/real/RESULTS.md) for
|
|
120
|
+
the live per-codec status — RESULTS.md is the source of truth and this
|
|
121
|
+
README intentionally omits per-codec counts to avoid drift.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
See [`SECURITY.md`](SECURITY.md) for responsible-disclosure policy.
|
|
128
|
+
Project licence is per-file (most files are MIT; third-party fixtures
|
|
129
|
+
keep their upstream licences — see
|
|
130
|
+
[`tests/fixtures/real/NOTICE.md`](tests/fixtures/real/NOTICE.md)).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
netcanon — multi-vendor network configuration backup and translation engine.
|
|
3
|
+
|
|
4
|
+
Package layout
|
|
5
|
+
--------------
|
|
6
|
+
netcanon.config Application settings (env-var driven via pydantic-settings).
|
|
7
|
+
netcanon.models Domain models: DeviceTarget, BackupJob, ConfigRecord, etc.
|
|
8
|
+
netcanon.definitions YAML definition loader and Pydantic schema.
|
|
9
|
+
netcanon.storage Pluggable config storage (file-based v1).
|
|
10
|
+
netcanon.collectors SSH collection strategies (Netmiko, Paramiko shell).
|
|
11
|
+
netcanon.api FastAPI router modules.
|
|
12
|
+
netcanon.main Application factory (create_app).
|
|
13
|
+
|
|
14
|
+
Entry point
|
|
15
|
+
-----------
|
|
16
|
+
Run the server:
|
|
17
|
+
uvicorn netcanon.main:app --reload
|
|
18
|
+
|
|
19
|
+
Or programmatically (e.g. in tests):
|
|
20
|
+
from netcanon.main import create_app
|
|
21
|
+
from netcanon.config import Settings
|
|
22
|
+
app = create_app(Settings(configs_dir=tmp_path))
|
|
23
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI router modules for the Netcanon REST API."""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI dependency providers.
|
|
3
|
+
|
|
4
|
+
These functions are injected via ``Depends()`` into route handlers so
|
|
5
|
+
that handlers never reference ``app.state`` directly. Indirection makes
|
|
6
|
+
unit testing easier: swap the dependency override instead of patching
|
|
7
|
+
``app.state``.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
@router.get("/definitions")
|
|
12
|
+
def list_defs(definitions: Definitions = Depends(get_definitions)):
|
|
13
|
+
...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from fastapi import Request
|
|
19
|
+
|
|
20
|
+
from ..definitions.loader import DefinitionLoader
|
|
21
|
+
from ..definitions.schema import DeviceDefinition
|
|
22
|
+
from ..models.backup import BackupJob
|
|
23
|
+
from ..models.device_profile import DeviceProfile
|
|
24
|
+
from ..models.schedule import BackupSchedule
|
|
25
|
+
from ..storage.base import BaseConfigStore
|
|
26
|
+
from ..storage.device_profile_store import FileDeviceProfileStore
|
|
27
|
+
from ..storage.job_store import FileJobStore
|
|
28
|
+
from ..storage.schedule_store import FileScheduleStore
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_definitions(request: Request) -> dict[str, DeviceDefinition]:
|
|
35
|
+
"""Inject the loaded device-definition registry from application state."""
|
|
36
|
+
return request.app.state.definitions
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_definition_loader(request: Request) -> DefinitionLoader:
|
|
40
|
+
"""Inject the DefinitionLoader instance so backup routes can call
|
|
41
|
+
:meth:`DefinitionLoader.resolve` for overlay lookup. The dict
|
|
42
|
+
returned by :func:`get_definitions` stays as the family-base
|
|
43
|
+
registry for endpoints that iterate type_keys."""
|
|
44
|
+
return request.app.state.definition_loader
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_storage(request: Request) -> BaseConfigStore:
|
|
48
|
+
"""Inject the config storage backend from application state."""
|
|
49
|
+
return request.app.state.storage
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_jobs(request: Request) -> dict[str, BackupJob]:
|
|
53
|
+
"""Inject the in-memory backup-job registry from application state."""
|
|
54
|
+
return request.app.state.jobs
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_job_store(request: Request) -> FileJobStore:
|
|
58
|
+
"""Inject the job persistence store from application state."""
|
|
59
|
+
return request.app.state.job_store
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_schedules(request: Request) -> dict[str, BackupSchedule]:
|
|
63
|
+
"""Inject the in-memory schedule registry from application state."""
|
|
64
|
+
return request.app.state.schedules
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_schedule_store(request: Request) -> FileScheduleStore:
|
|
68
|
+
"""Inject the schedule persistence store from application state."""
|
|
69
|
+
return request.app.state.schedule_store
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_scheduler(request: Request):
|
|
73
|
+
"""Inject the APScheduler instance from application state."""
|
|
74
|
+
return request.app.state.scheduler
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_device_profiles(request: Request) -> dict[str, DeviceProfile]:
|
|
78
|
+
"""Inject the in-memory device profile registry from application state."""
|
|
79
|
+
return request.app.state.device_profiles
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_device_profile_store(request: Request) -> FileDeviceProfileStore:
|
|
83
|
+
"""Inject the device profile persistence store from application state."""
|
|
84
|
+
return request.app.state.device_profile_store
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API route modules — imported and included by the application factory."""
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helpers extracted from :mod:`netcanon.api.routes.migration` for the
|
|
3
|
+
``refactor/god-file-cleanup`` branch.
|
|
4
|
+
|
|
5
|
+
Public surface:
|
|
6
|
+
|
|
7
|
+
* :func:`resolve_adapter_or_422` — translate adapter-name lookup
|
|
8
|
+
errors into 422s with side-aware ``source`` / ``target`` framing.
|
|
9
|
+
* :func:`resolve_input_text` — return the raw config text referenced
|
|
10
|
+
by a :class:`MigrationPlanRequest` body, enforcing the
|
|
11
|
+
``raw_text`` XOR ``source_filename`` invariant and translating
|
|
12
|
+
storage misses into 404s.
|
|
13
|
+
* :func:`get_target_profiles` — pull the target-profile registry
|
|
14
|
+
from ``request.app.state``; returns an empty dict when the
|
|
15
|
+
attribute is absent (some unit-test fixtures don't run the full
|
|
16
|
+
lifespan).
|
|
17
|
+
* :func:`build_codec_info_list` — shape the registered codec list
|
|
18
|
+
into :class:`CodecInfo` records for ``GET /adapters``, joining
|
|
19
|
+
each codec's :class:`CapabilityMatrix` with the corresponding
|
|
20
|
+
vendor's ``display_name``.
|
|
21
|
+
* :func:`request_has_overrides_or_profile` — boolean predicate that
|
|
22
|
+
decides whether ``POST /plan`` should route through the
|
|
23
|
+
rename-aware :func:`run_plan_with_overrides` (any per-category
|
|
24
|
+
map present, or a target profile selected).
|
|
25
|
+
|
|
26
|
+
Routes orchestrate; these helpers compute. None of these touch the
|
|
27
|
+
frozen pipeline-stage signatures in
|
|
28
|
+
:mod:`netcanon.services.migration_pipeline` — they live one layer
|
|
29
|
+
above the pipeline, on the request-shaping / response-shaping side.
|
|
30
|
+
|
|
31
|
+
Why a separate module? The route file used to mix request
|
|
32
|
+
validation, capability-matrix shaping, target-profile resolution,
|
|
33
|
+
and pipeline dispatch in one ~750-LOC file. Lifting these helpers
|
|
34
|
+
into a sibling keeps ``migration.py`` focussed on FastAPI route
|
|
35
|
+
declarations + thin glue, and lets each helper acquire focussed
|
|
36
|
+
unit-test coverage in :mod:`tests.unit.api.test_migration_helpers`
|
|
37
|
+
without spinning up a TestClient.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from fastapi import HTTPException, Request
|
|
43
|
+
|
|
44
|
+
from ...migration.codecs.registry import get_codec, list_codecs
|
|
45
|
+
from ...migration.target_profiles import TargetProfile
|
|
46
|
+
from ...models.migration import CodecInfo, MigrationPlanRequest
|
|
47
|
+
from ...storage.base import BaseConfigStore
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_adapter_or_422(name: str, side: str):
|
|
51
|
+
"""Return the named adapter or raise a 422 with a helpful message.
|
|
52
|
+
|
|
53
|
+
Uses 422 not 404 because the adapter name is REQUEST-PAYLOAD data;
|
|
54
|
+
callers should fix their body, not their URL.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
return get_codec(name)
|
|
58
|
+
except LookupError as exc:
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
status_code=422,
|
|
61
|
+
detail=f"unknown {side} adapter: {exc}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_input_text(
|
|
66
|
+
body: MigrationPlanRequest, storage: BaseConfigStore
|
|
67
|
+
) -> str:
|
|
68
|
+
"""Return the raw config text referenced by *body*.
|
|
69
|
+
|
|
70
|
+
Exactly one of ``raw_text`` / ``source_filename`` MUST be set.
|
|
71
|
+
Raises:
|
|
72
|
+
HTTPException 422: If both are set or neither is set.
|
|
73
|
+
HTTPException 404: If ``source_filename`` refers to a file
|
|
74
|
+
that doesn't exist.
|
|
75
|
+
"""
|
|
76
|
+
has_text = body.raw_text is not None
|
|
77
|
+
has_file = body.source_filename is not None
|
|
78
|
+
if has_text == has_file:
|
|
79
|
+
raise HTTPException(
|
|
80
|
+
status_code=422,
|
|
81
|
+
detail=(
|
|
82
|
+
"Exactly one of `raw_text` or `source_filename` is required."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
if has_text:
|
|
86
|
+
return body.raw_text or ""
|
|
87
|
+
try:
|
|
88
|
+
return storage.get_content(body.source_filename or "")
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
raise HTTPException(
|
|
91
|
+
status_code=404,
|
|
92
|
+
detail=f"source_filename not found: {body.source_filename!r}",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_target_profiles(request: Request) -> dict[str, TargetProfile]:
|
|
97
|
+
"""Pull the target-profile registry from ``request.app.state``.
|
|
98
|
+
|
|
99
|
+
Profiles are loaded once at app startup (see ``main.py`` lifespan)
|
|
100
|
+
and exposed through ``app.state.target_profiles``. ``getattr`` with
|
|
101
|
+
a default makes this safe under the bare-app fixtures used by some
|
|
102
|
+
unit tests, which don't populate the full lifespan-loaded state —
|
|
103
|
+
those tests get an empty dict and the route returns an empty list
|
|
104
|
+
rather than raising AttributeError.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
request: The current request; only its ``app.state`` is consulted.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Mapping of ``"<vendor>/<model>"`` keys to ``TargetProfile``
|
|
111
|
+
instances. Empty when no profiles are loaded.
|
|
112
|
+
"""
|
|
113
|
+
return getattr(request.app.state, "target_profiles", {})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_codec_info_list(vendors: dict) -> list[CodecInfo]:
|
|
117
|
+
"""Return one :class:`CodecInfo` per registered codec.
|
|
118
|
+
|
|
119
|
+
Each entry includes the linked vendor's ``display_name`` (resolved
|
|
120
|
+
from *vendors*, typically ``request.app.state.vendors``) so the UI
|
|
121
|
+
can group codecs by vendor without a second round-trip.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
vendors: Mapping of ``vendor_id`` to a vendor record exposing
|
|
125
|
+
a ``display_name`` attribute. Pass ``{}`` when the vendor
|
|
126
|
+
registry isn't populated; missing entries fall back to an
|
|
127
|
+
empty display name.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
One :class:`CodecInfo` per name in
|
|
131
|
+
:func:`netcanon.migration.codecs.registry.list_codecs`, in
|
|
132
|
+
registry-iteration order.
|
|
133
|
+
"""
|
|
134
|
+
result: list[CodecInfo] = []
|
|
135
|
+
for name in list_codecs():
|
|
136
|
+
codec = get_codec(name)
|
|
137
|
+
caps = codec.capabilities
|
|
138
|
+
vendor = vendors.get(caps.vendor_id)
|
|
139
|
+
result.append(
|
|
140
|
+
CodecInfo(
|
|
141
|
+
name=caps.adapter,
|
|
142
|
+
vendor_id=caps.vendor_id,
|
|
143
|
+
vendor_display_name=vendor.display_name if vendor else "",
|
|
144
|
+
version_range=caps.version_range,
|
|
145
|
+
device_classes=list(caps.device_classes),
|
|
146
|
+
input_format=getattr(codec, "input_format", "unknown"),
|
|
147
|
+
direction=getattr(codec, "direction", "bidirectional"),
|
|
148
|
+
certainty=getattr(codec, "certainty", "experimental"),
|
|
149
|
+
canonical_model=getattr(codec, "canonical_model", "openconfig-lite"),
|
|
150
|
+
supported_count=len(caps.supported),
|
|
151
|
+
lossy_count=len(caps.lossy),
|
|
152
|
+
unsupported_count=len(caps.unsupported),
|
|
153
|
+
description=getattr(codec, "description", ""),
|
|
154
|
+
sample_input=getattr(codec, "sample_input", ""),
|
|
155
|
+
output_extension=getattr(codec, "output_extension", ""),
|
|
156
|
+
unsupported_rename_categories=sorted(
|
|
157
|
+
getattr(codec, "unsupported_rename_categories", frozenset())
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def request_has_overrides_or_profile(body: MigrationPlanRequest) -> bool:
|
|
165
|
+
"""Decide whether ``POST /plan`` should engage the rename-aware pipeline.
|
|
166
|
+
|
|
167
|
+
Returns ``True`` when *body* carries ANY per-category override map
|
|
168
|
+
(port / vlan / local-user / SNMP community / SNMPv3 user) OR a
|
|
169
|
+
``target_profile`` selection. Target-profile alone means "run
|
|
170
|
+
auto-heuristic + return diagnostics the UI can render," and still
|
|
171
|
+
needs the rename-aware pipeline.
|
|
172
|
+
|
|
173
|
+
Legacy callers that supply none of these get the plain
|
|
174
|
+
:func:`run_plan` path unchanged.
|
|
175
|
+
"""
|
|
176
|
+
return (
|
|
177
|
+
body.port_rename_map is not None
|
|
178
|
+
or body.vlan_rename_map is not None
|
|
179
|
+
or body.local_user_rename_map is not None
|
|
180
|
+
or body.snmp_community_rename_map is not None
|
|
181
|
+
or body.snmpv3_user_rename_map is not None
|
|
182
|
+
or body.target_profile is not None
|
|
183
|
+
)
|