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.
Files changed (140) hide show
  1. netcanon-0.1.0/LICENSE +21 -0
  2. netcanon-0.1.0/PKG-INFO +185 -0
  3. netcanon-0.1.0/README.md +130 -0
  4. netcanon-0.1.0/netcanon/__init__.py +23 -0
  5. netcanon-0.1.0/netcanon/api/__init__.py +1 -0
  6. netcanon-0.1.0/netcanon/api/deps.py +84 -0
  7. netcanon-0.1.0/netcanon/api/routes/__init__.py +1 -0
  8. netcanon-0.1.0/netcanon/api/routes/_migration_helpers.py +183 -0
  9. netcanon-0.1.0/netcanon/api/routes/backups.py +507 -0
  10. netcanon-0.1.0/netcanon/api/routes/configs.py +293 -0
  11. netcanon-0.1.0/netcanon/api/routes/definitions.py +91 -0
  12. netcanon-0.1.0/netcanon/api/routes/device_profiles.py +172 -0
  13. netcanon-0.1.0/netcanon/api/routes/health.py +43 -0
  14. netcanon-0.1.0/netcanon/api/routes/migration.py +677 -0
  15. netcanon-0.1.0/netcanon/api/routes/sanitize.py +84 -0
  16. netcanon-0.1.0/netcanon/api/routes/schedules.py +342 -0
  17. netcanon-0.1.0/netcanon/api/routes/ui.py +503 -0
  18. netcanon-0.1.0/netcanon/cli.py +142 -0
  19. netcanon-0.1.0/netcanon/collectors/__init__.py +25 -0
  20. netcanon-0.1.0/netcanon/collectors/base.py +132 -0
  21. netcanon-0.1.0/netcanon/collectors/netmiko_collector.py +210 -0
  22. netcanon-0.1.0/netcanon/collectors/paramiko_collector.py +438 -0
  23. netcanon-0.1.0/netcanon/collectors/probe.py +107 -0
  24. netcanon-0.1.0/netcanon/config.py +85 -0
  25. netcanon-0.1.0/netcanon/definitions/__init__.py +29 -0
  26. netcanon-0.1.0/netcanon/definitions/loader.py +282 -0
  27. netcanon-0.1.0/netcanon/definitions/schema.py +268 -0
  28. netcanon-0.1.0/netcanon/logging_config.py +165 -0
  29. netcanon-0.1.0/netcanon/main.py +288 -0
  30. netcanon-0.1.0/netcanon/migration/__init__.py +76 -0
  31. netcanon-0.1.0/netcanon/migration/_naming.py +72 -0
  32. netcanon-0.1.0/netcanon/migration/_tier3_detection.py +215 -0
  33. netcanon-0.1.0/netcanon/migration/_user_secrets.py +228 -0
  34. netcanon-0.1.0/netcanon/migration/canonical/__init__.py +8 -0
  35. netcanon-0.1.0/netcanon/migration/canonical/intent.py +640 -0
  36. netcanon-0.1.0/netcanon/migration/canonical/loader.py +61 -0
  37. netcanon-0.1.0/netcanon/migration/canonical/local_user_names.py +275 -0
  38. netcanon-0.1.0/netcanon/migration/canonical/port_names.py +614 -0
  39. netcanon-0.1.0/netcanon/migration/canonical/snmp_names.py +273 -0
  40. netcanon-0.1.0/netcanon/migration/canonical/snmpv3_user_names.py +277 -0
  41. netcanon-0.1.0/netcanon/migration/canonical/transforms.py +373 -0
  42. netcanon-0.1.0/netcanon/migration/canonical/vlan_names.py +364 -0
  43. netcanon-0.1.0/netcanon/migration/codecs/__init__.py +14 -0
  44. netcanon-0.1.0/netcanon/migration/codecs/_mock/__init__.py +18 -0
  45. netcanon-0.1.0/netcanon/migration/codecs/_mock/codec.py +153 -0
  46. netcanon-0.1.0/netcanon/migration/codecs/arista_eos/__init__.py +53 -0
  47. netcanon-0.1.0/netcanon/migration/codecs/arista_eos/codec.py +302 -0
  48. netcanon-0.1.0/netcanon/migration/codecs/arista_eos/parse.py +1145 -0
  49. netcanon-0.1.0/netcanon/migration/codecs/arista_eos/port_names.py +190 -0
  50. netcanon-0.1.0/netcanon/migration/codecs/arista_eos/render.py +765 -0
  51. netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/__init__.py +63 -0
  52. netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/_svi_absorption.py +111 -0
  53. netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/codec.py +301 -0
  54. netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/parse.py +1037 -0
  55. netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/port_names.py +219 -0
  56. netcanon-0.1.0/netcanon/migration/codecs/aruba_aoss/render.py +771 -0
  57. netcanon-0.1.0/netcanon/migration/codecs/base.py +364 -0
  58. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe/__init__.py +20 -0
  59. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe/codec.py +1184 -0
  60. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/__init__.py +28 -0
  61. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/codec.py +492 -0
  62. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/parse.py +1369 -0
  63. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/port_names.py +321 -0
  64. netcanon-0.1.0/netcanon/migration/codecs/cisco_iosxe_cli/render.py +684 -0
  65. netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/__init__.py +61 -0
  66. netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/codec.py +280 -0
  67. netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/parse.py +860 -0
  68. netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/port_names.py +317 -0
  69. netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/render.py +858 -0
  70. netcanon-0.1.0/netcanon/migration/codecs/fortigate_cli/vlan_heuristics.py +156 -0
  71. netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/__init__.py +81 -0
  72. netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/codec.py +309 -0
  73. netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/parse.py +2095 -0
  74. netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/port_names.py +191 -0
  75. netcanon-0.1.0/netcanon/migration/codecs/juniper_junos/render.py +1262 -0
  76. netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/__init__.py +43 -0
  77. netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/codec.py +297 -0
  78. netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/parse.py +1022 -0
  79. netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/port_names.py +345 -0
  80. netcanon-0.1.0/netcanon/migration/codecs/mikrotik_routeros/render.py +866 -0
  81. netcanon-0.1.0/netcanon/migration/codecs/opnsense/__init__.py +38 -0
  82. netcanon-0.1.0/netcanon/migration/codecs/opnsense/codec.py +325 -0
  83. netcanon-0.1.0/netcanon/migration/codecs/opnsense/parse.py +630 -0
  84. netcanon-0.1.0/netcanon/migration/codecs/opnsense/port_names.py +201 -0
  85. netcanon-0.1.0/netcanon/migration/codecs/opnsense/render.py +570 -0
  86. netcanon-0.1.0/netcanon/migration/codecs/registry.py +72 -0
  87. netcanon-0.1.0/netcanon/migration/target_profiles.py +544 -0
  88. netcanon-0.1.0/netcanon/migration/vendors/__init__.py +82 -0
  89. netcanon-0.1.0/netcanon/models/__init__.py +47 -0
  90. netcanon-0.1.0/netcanon/models/backup.py +109 -0
  91. netcanon-0.1.0/netcanon/models/device.py +76 -0
  92. netcanon-0.1.0/netcanon/models/device_profile.py +144 -0
  93. netcanon-0.1.0/netcanon/models/diff.py +124 -0
  94. netcanon-0.1.0/netcanon/models/migration.py +712 -0
  95. netcanon-0.1.0/netcanon/models/schedule.py +103 -0
  96. netcanon-0.1.0/netcanon/models/validators.py +29 -0
  97. netcanon-0.1.0/netcanon/security/__init__.py +1 -0
  98. netcanon-0.1.0/netcanon/security/credentials.py +101 -0
  99. netcanon-0.1.0/netcanon/security/migration.py +35 -0
  100. netcanon-0.1.0/netcanon/services/__init__.py +1 -0
  101. netcanon-0.1.0/netcanon/services/diff.py +251 -0
  102. netcanon-0.1.0/netcanon/services/migration_detect.py +117 -0
  103. netcanon-0.1.0/netcanon/services/migration_pipeline.py +706 -0
  104. netcanon-0.1.0/netcanon/services/migration_validate.py +243 -0
  105. netcanon-0.1.0/netcanon/storage/__init__.py +13 -0
  106. netcanon-0.1.0/netcanon/storage/base.py +95 -0
  107. netcanon-0.1.0/netcanon/storage/device_profile_store.py +108 -0
  108. netcanon-0.1.0/netcanon/storage/file_store.py +337 -0
  109. netcanon-0.1.0/netcanon/storage/job_store.py +74 -0
  110. netcanon-0.1.0/netcanon/storage/schedule_store.py +115 -0
  111. netcanon-0.1.0/netcanon/templates/base.html +507 -0
  112. netcanon-0.1.0/netcanon/templates/configs.html +311 -0
  113. netcanon-0.1.0/netcanon/templates/definitions.html +918 -0
  114. netcanon-0.1.0/netcanon/templates/devices.html +509 -0
  115. netcanon-0.1.0/netcanon/templates/diff.html +284 -0
  116. netcanon-0.1.0/netcanon/templates/index.html +350 -0
  117. netcanon-0.1.0/netcanon/templates/jobs.html +181 -0
  118. netcanon-0.1.0/netcanon/templates/migrate.html +2281 -0
  119. netcanon-0.1.0/netcanon/templates/schedules.html +294 -0
  120. netcanon-0.1.0/netcanon/tools/__init__.py +13 -0
  121. netcanon-0.1.0/netcanon/tools/sanitize.py +486 -0
  122. netcanon-0.1.0/netcanon.egg-info/PKG-INFO +185 -0
  123. netcanon-0.1.0/netcanon.egg-info/SOURCES.txt +138 -0
  124. netcanon-0.1.0/netcanon.egg-info/dependency_links.txt +1 -0
  125. netcanon-0.1.0/netcanon.egg-info/entry_points.txt +2 -0
  126. netcanon-0.1.0/netcanon.egg-info/requires.txt +31 -0
  127. netcanon-0.1.0/netcanon.egg-info/top_level.txt +2 -0
  128. netcanon-0.1.0/netcanon_desktop/__init__.py +22 -0
  129. netcanon-0.1.0/netcanon_desktop/__main__.py +100 -0
  130. netcanon-0.1.0/netcanon_desktop/app.py +161 -0
  131. netcanon-0.1.0/netcanon_desktop/icons.py +119 -0
  132. netcanon-0.1.0/netcanon_desktop/preferences.py +117 -0
  133. netcanon-0.1.0/netcanon_desktop/preferences_dialog.py +311 -0
  134. netcanon-0.1.0/netcanon_desktop/server.py +150 -0
  135. netcanon-0.1.0/netcanon_desktop/settings.py +98 -0
  136. netcanon-0.1.0/netcanon_desktop/single_instance.py +77 -0
  137. netcanon-0.1.0/netcanon_desktop/tray.py +136 -0
  138. netcanon-0.1.0/netcanon_desktop/window.py +189 -0
  139. netcanon-0.1.0/pyproject.toml +114 -0
  140. 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.
@@ -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)).
@@ -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
+ )