Initial import from bf-network-r6.opencode — opencode migration

This commit is contained in:
2026-08-26 12:44:21 +02:00
commit 5ee0716dda
297 changed files with 72324 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
# NAS (DS923) → VLAN20 Docker-Migration — Runbook
> Stand: 2026-07-09. Ziel: NAS-NIC ist auf VLAN20; **ein** Docker-Macvlan `vlan20`
> (10.40.20.0/24, gw .1, parent `ovs_eth0`), **alle** Container darauf, `default_external`
> (10.40.41.0/24) weg. Mapping-Regel: **letztes Oktett behalten**, Subnetz .41→.20.
## Root-Cause der Dauer-Fehler (diagnostiziert)
1. `vlan20` war ein **Config-Only-Macvlan** (`ConfigOnly:true`, `Driver:null`) — Portainers
zweistufiger Assistent, nur Schritt 1. An Config-Only-Netze kann Docker **keine** Container
hängen → jeder Container auf `vlan20` blieb `created`/`exited`. **(gelöscht am 2026-07-09)**
2. Macvlan belegt seinen Parent (`ovs_eth0`) **exklusiv** → `vlan20` kann erst existieren,
wenn `default_external` weg ist. Deshalb ist „alle LANs raus" technisch zwingend.
3. `default_internal` (172.18er Bridge) **existiert nicht mehr** (404); der Range ist von
`gitea_default` (172.18.0.0/16) belegt. Darum sind Multi-Netz-Stacks doppelt kaputt.
→ Lösung „nur vlan20": interne Bridge wird nicht neu gebaut, jeder Service nur auf `vlan20`
(Container am selben Macvlan erreichen sich gegenseitig; DB-/Sidecar-Refs waren fast alle
auskommentiert).
## IP-Mapping (default_external .41.X → vlan20 .20.X)
| Container | Stack (ep14) | alt | neu |
|---|---|---|---|
| omada-controller | omada (198) | 10.40.41.175 | 10.40.20.175 |
| prometheus | oc-stack (112) | .41.151 | .20.151 |
| grafana | oc-stack (112) | .41.109 | .20.109 |
| alertmanager | oc-stack (112) | .41.153 | .20.153 |
| node-exporter | oc-stack (112) | .41.154 | .20.154 |
| iob_slave | iob_slave (64) | .41.33 | .20.33 |
| vaultwarden | vaultwarden (73) | .41.127 | .20.127 |
| oc-tzcharlie-gateway | oc-charlie (158) | .41.162 | .20.162 |
| oc-johnny2-gateway | oc-johnny2 (150) | .41.161 | .20.161 |
| oc-qdrant-central | oc-central (160) | .41.159 | .20.159 |
| dashboard-web | nginx (101) | .41.148 | .20.148 |
| hms-mqtt | default (72) | .41.124 | .20.124 |
| gitea | gitea (111) | .41.157 | .20.157 |
| drawio-server | **standalone** | .41/.20.139 | .20.139 |
| pve-mcp | **standalone** | .41.173 | .20.173 |
Unangetastet: `cloudflared` (host-Netz), `portainer_agent` (bridge).
Transformierte Compose-Files: `docs/nas-vlan20/stacks/*.yml` (Netz-Block auf `vlan20`,
IPs .41→.20, `dns:`→10.40.20.1). Standalone-Specs: `docs/nas-vlan20/inspect_*.json`.
## Phase A — Netz-Cutover (irreversibel für laufende Container-States; Volumes bleiben)
Container-Prozesse werden neu gestartet, **Volumes werden NICHT angefasst** (`v=false`).
Skript: `docs/nas-vlan20/phaseA-...py` (bzw. /tmp/nasmig/phaseA.py). Es:
1. entfernt alle Container auf `default_external` + die 2 auf dem toten `vlan20`,
2. löscht `default_external`,
3. legt `vlan20` an: `-d macvlan --subnet 10.40.20.0/24 --gateway 10.40.20.1 -o parent=ovs_eth0`.
Manuell (falls per Hand): in Portainer → NAS(ep14) → Networks → `default_external`:
alle Container disconnecten/stoppen → Netz löschen → „Add network": macvlan, parent `ovs_eth0`,
Subnet 10.40.20.0/24, GW 10.40.20.1, Name `vlan20`.
## Phase B — Redeploy (macht Christoph selbst in Portainer)
Je Stack den Inhalt aus `docs/nas-vlan20/stacks/<id>-*.yml` einspielen → Update/Redeploy.
Standalone neu bauen (drawio-server, pve-mcp) siehe inspect_*.json (image/env/binds), Netz `vlan20`.
## Post-Migration (erst NACH Redeploy, wenn Container real auf .20.x)
- **ap1 DNS (bf.lan A-Records):** alle gewanderten Hosts .41.X→.20.X nachziehen (MikroMCP `manage_dns_entry`).
- **CF-Tunnel-Ingress** (`70d6a4cf…`, remote-managed): NAS-Origins .41→.20 repointen —
hass(→schon .20.105?), gitea .41.157→.20.157, grafana .109, pve-mcp .173, vw .127, ds-bf, portainer …
SSoT: `docs/cf-tunnel-mapping.md`. Vorher pve→VLAN20-Reachability smoke-testen.
- **omada-mcp** (`~/.claude.json` + Stack ep2/ep14): `OMADA_URL` .41.175→.20.175.
- ⚠️ **omada-controller Inform-Adresse:** adoptierter Omada-Switch sw2 kennt Controller unter
.41.175 → nach Umzug im Controller die Controller/Inform-Adresse auf .20.175 setzen, sonst sw2
„Disconnected". UI wandert auf https://10.40.20.175:8043.
- **CLAUDE.md / memory:** .41→.20 Referenzen nachziehen.
## CF-Access-Profil für Claude (Thread „ich komm schwer durch den Tunnel")
- `*.emsenhuber.net` = CF-Access-App. Policies: `christoph` (E-Mail-Login), `bf4`
(non_identity, **IP-Bypass** = BF-Egress → darum on-LAN offen), **`claude-mcp`
(allow, service_token)** = mein Profil, **policy-seitig bereits vorhanden**.
- Fehlt nur das **Secret** des `claude-mcp`-Tokens (liegt nirgends lokal). Ohne
`CF-Access-Client-Id/Secret`-Header komme ich nur über den BF-IP-Bypass durch.
- **Offen (braucht Freigabe):** Service-Token `claude-mcp` rotieren → neues Secret →
ablegen in `mcp/cloudflare/.env` (chmod 600) als `CF_ACCESS_CLIENT_ID` / `CF_ACCESS_CLIENT_SECRET`.
Rotieren invalidiert das alte Secret (das aber ohnehin verloren ist).
- `hass, vw`-App = `bypass everyone` → hass/vw sind **öffentlich** (nur HA-Login davor). Separat bewerten.

View File

@@ -0,0 +1,102 @@
# TP14 — Übergabestatements für neue Sessions
Steuerung: `docs/teilprojekte.md` → **TP14**. Runbook: `docs/nas-vlan20/RUNBOOK.md`.
Reihenfolge: **A → B → C → (D jederzeit nach Token-Scope) → E zuletzt.**
Entscheidung: **sequenzielle Sessions, keine parallelen Branches** (geteilte Live-Infra: 1× NAS,
1× pve-LXC, 1× CF, 1× ap1 → Branches isolieren nur Repo-Files, nicht Infra-State → Kollisionsrisiko).
Portainer-API `https://10.40.41.126:9443` (nach TP14-Session-B → `.20.126`), Key `mcp/portainer/.env`.
Transform-Regeln + Scratch-Skripte: Runbook §„Transform-Regeln". Stack-Files (mit Secrets, gitignored):
`docs/nas-vlan20/stacks/`. Mapping-Regel durchgehend: **letztes Oktett behalten, `.41→.20`.**
---
## TP14.1 — Session A: NAS fertigstellen ✅ DURCH (2026-07-10, außer Omada-Inform)
**Ergebnis:** alle ep14-Container laufen auf vlan20/.20.x (prometheus .151, node-exporter .154,
grafana .109, hms-mqtt .124, dashboard-web .148, qdrant .159, gitea .157 [DB .20.23 ✓],
drawio .139 [intern], oc-tzcharlie .162+cf, oc-johnny2 .161+cf). pve-mcp NEU als Stack id 202
`.20.173` (⚠️ StreamableHTTP am **Root `/`**, nicht `/sse`); `~/.claude.json` `mcpServers.pve-mcp`
neu = `{type:http,url:http://10.40.20.173:3000/}`. Tunnel-Smoke gitea/grafana/vw = 200/302/200.
**Rest-/Nacharbeiten:** (a) ⏸️ **Omada-Inform-Adresse VERTAGT** (2026-07-10): Controller-UI-Login an
`.20.175` schlägt fehl → blockiert, verschoben. sw2 „Disconnected" aber switcht weiter (management-only,
unkritisch). Vor Recovery zuerst **Controller-Login klären** (Passwort-Reset, Creds nicht lokal). (b) ⚠️ alertmanager .153 crash-loopt auf
**pre-existing** Config-Bug (`${TELEGRAM_CHAT_ID}`-Literal), nicht migrationsbedingt. (c) ⚠️ openclaw
shared-DB `.41.122:3306` down (pre-existing). (d) Merke: `OLLAMA_BASE_URL=.41.167`=Mac-mini bleibt
`.41` — **kein Blanket-Env-.41→.20**. Helper: `/private/tmp/nasmig/{redeploy.py,pt.py,omada.py}`.
**Omada-Inform-Fix ⏸️ VERTAGT (2026-07-10):** Controller-UI-Login an `https://10.40.20.175:8043`
klappt nicht → erst **Login/Passwort-Recovery** klären (Creds nicht lokal). Danach: UI → Settings →
Controller → „Controller Hostname/IP" = `10.40.20.175`; dann sw2 forget + re-adopt (L3-Discovery).
Kein Auto-Fix: Open API kein Inform-Setting, kein Controller-Admin-Login lokal, sw2 = statische IP (kein DHCP-138).
<details><summary>Original-Aufgabe (erledigt)</summary>
Kontext: NAS(ep14) ist auf `vlan20`-Macvlan migriert (DSM `.20.10`). Container teils exited.
Aufgabe:
1. Restcontainer starten/redeployen aus `docs/nas-vlan20/stacks/`: **oc-stack(112)** (prometheus/
alertmanager/node-exporter), **default(72)** (hms-mqtt), **nginx(101)** (dashboard-web),
**oc-central(160)**, **oc-charlie(158)**, **oc-johnny2(150)**.
2. **gitea(111)** deployen → `.20.157`; ⚠️ `GITEA__database__HOST` `.20.23` verifizieren (Rewrite-Annahme).
3. **drawio(107)** mit `transform.py` auf vlan20 bringen (`.20.139`) → deployen.
4. **pve-mcp NEU als Stack** (vlan20-only): Spec `docs/nas-vlan20/inspect_pve-mcp.json` — image
`ghcr.io/samik081/mcp-pve:latest`, `PVE_BASE_URL=https://10.40.41.100:8006`, `MCP_PORT=3000`,
`MCP_HOST=0.0.0.0`, `MCP_TRANSPORT=http`; Netz `vlan20` `ipv4_address: 10.40.20.173`. Danach
`~/.claude.json` pve-mcp-URL `.41.173→.20.173`.
5. **omada-controller Inform-Adresse** (UI `https://10.40.20.175:8043`) → `10.40.20.175`, sonst sw2 down.
Verifikation: `pt.py containers` alle `running`/`.20.x`; von `.41`-Host (gw ap1) `curl`/`ping` auf `.20`-IP;
Tunnel `curl -m8 -w %{http_code} https://<host>.emsenhuber.net/` → 200/302.
</details>
## TP14.2 — Session B: pve-Docker-LXC (.126) → VLAN20 ✅ DURCH (2026-07-10)
**Ergebnis:** LXC 400 (`docker`) net0 `tag=20 / 10.40.20.126/24 / gw .20.1`; Docker `vlan20`=ipvlan
**`parent=veth20`** (⚠️ in-Container-Iface heißt jetzt `veth20`, nicht eth0 — UI-Namensfeld). Container:
omada-mcp `.20.176` (OMADA_BASE_URL→`.20.175`), adguard_bf1 `.20.16`, adguard_fm2 `.20.17` — alle running/reachable.
CF-Connector `db713b30` jetzt VLAN20 → **#1033-Zielbild gelöst** (Tunnel healthy; portainer/gitea/vw 200).
Refs nachgezogen: `mcp/portainer/.env`, `~/.claude.json` omada-mcp, CLAUDE.md, ip-plan, cf-tunnel-mapping.
**⚠️ Gotcha:** Proxmox-Token `claude@pam!claude` hat **kein `VM.Config.Network`** → Retag muss Christoph
(UI) machen; Reboot danach via API (`VM.PowerMgmt` vorhanden). Transf. Files: `stacks/{195-adguard,200-omada-mcp}.yml`.
<details><summary>Original-Aufgabe (erledigt)</summary>
Kontext: pve-LXC(ep2) hängt auf `.41`, Netz `default_external` = **ipvlan** parent `eth0`:
omada-mcp `.176`, adguard_bf1 `.16`, adguard_fm2 `.17`. **Portainer-Server + aktiver CF-Connector laufen
hier.** ⚠️ Reihenfolge kritisch.
Aufgabe:
1. ep2-Stacks (`adguard`, `omada-mcp`) transformieren `.41→.20` (omada-mcp `OMADA_URL` `.41.175→.20.175`).
2. Portainer-Zugangspfad für den Move sichern (URL wandert auf `https://10.40.20.126:9443`).
3. LXC in Proxmox auf VLAN20 taggen + IP `.126→.20.126`.
4. `default_external`(ipvlan) weg → `vlan20`(ipvlan) neu: `-d ipvlan --subnet 10.40.20.0/24
--gateway 10.40.20.1 -o parent=eth0`. Container erst entfernen (`v=false`), dann Netz, dann Stacks redeploy.
5. `mcp/portainer/.env` + `PORTAINER_URL` + CF-Ingress (portainer `.20.126`) nachziehen.
Ergebnis: CF-Connector in VLAN20 = **#1033-Zielbild** (TP11-Track-B/C erledigt). Verifikation: Portainer
unter neuer URL, ep2-Container `.20.x`, Tunnel healthy, pve-lokale Origins (pve-Host `.41.100`) noch erreichbar.
</details>
## TP14.3 — Session C: Dormante Stacks vlan20-tauglich + Referenz-Sweep
Aufgabe:
1. `transform.py` auf dormante ep14-Stacks: core(43), openclaw(92), openclaw2(97), oc20260307(119),
openspeedtest(74), speedtest_tracker(80), technikscheiss(42) → `.new`-Files (nicht zwingend deployen;
42/43 waren `default_internal`→wird vlan20-only).
2. Referenz-Sweep `.41→.20` für gewanderte Hosts: **ap1 bf.lan-DNS** (MikroMCP `manage_dns_entry`,
ap1-Mgmt via `10.40.10.1` — s. Memory `ref_ap1_mgmt_access_vlan10`), **CLAUDE.md**, **`~/.claude.json`**
(omada-mcp `.20.176`, pve-mcp `.20.173`), **`docs/ip-plan.md`**, **CF-Ingress-Rest**. SSoT `docs/cf-tunnel-mapping.md`.
## TP14.4 — Session D: CF-Access-Profil für Claude (nicht cutoff-kritisch)
Kontext: `*.emsenhuber.net`-Access-App hat Policy `claude-mcp` (service_token) bereits; Secret fehlt lokal.
CF-Token „claude code mcp" hat jetzt **`Access: Service Tokens: Write`+`Read`** (User bestätigt 2026-07-10).
Aufgabe: `claude-mcp` rotieren (`POST /accounts/{acct}/access/service_tokens/{id}/rotate`) → `CF_ACCESS_CLIENT_ID`/
`CF_ACCESS_CLIENT_SECRET` in `mcp/cloudflare/.env` (chmod600) → dokumentieren. Details: Memory `ref-cf-access-claude-profile`.
## TP14.5 — Session E: Freigabe-Gate → TP5-Firewall (ZULETZT)
Erst wenn ALLE Origins/Connectoren in VLAN20 (nach A+B+C). Least-Privilege je VLAN; Deny-by-default fertigziehen.
---
## Kritische Gotchas (gelten für alle Sessions)
- Macvlan/ipvlan-Parent **exklusiv** → altes `default_external` MUSS weg, bevor `vlan20` anlegbar.
- **Host↔eigene-Macvlan-Isolation:** ein Connector auf demselben Host erreicht dessen Macvlan-Container NICHT
→ Connector muss auf anderem Host in VLAN20 laufen (darum pve-LXC → VLAN20).
- **Portainer lebt im pve-LXC** → dessen Move ändert die Mgmt-URL; Zugang vorher sichern.
- **Container-Removal unkritisch** (Stack-Def + Ownership bleiben); **Recreate via API** macht admin-owned →
besser via Stack-Redeploy (bleibt User-owned). `v=false` = Volumes bleiben.
- Portainer-Macvlan-Assistent legt bei nur Schritt 1 ein **Config-Only-Netz** an (`Driver:null`) → echtes
Netz nur per API in einem Schritt anlegen.

View File

@@ -0,0 +1,29 @@
# Nautobot + Golden Config + Nornir, extending the official image in place
# (not a satellite/side image). Reconstructed 2026-07-22 from the running
# container's actual `docker image history` after the original build context
# (referenced in git history as "scratch/nautobot-gc-build") turned out to
# never have been committed to this repo -- this file replaces that dangling
# reference as the real, versioned source of truth going forward.
#
# Rebuild: docker build -t bf-nautobot-golden-config:<tag> docs/nas-vlan20/nautobot-image/
# then point docs/nas-vlan20/stacks/210-nautobot.yml's 3 `image:` lines at <tag>
# and redeploy the Portainer stack (id 203, endpoint 14).
# Rollback: point those 3 image lines back at ghcr.io/nautobot/nautobot:3.1.7
# (drops the golden-config/nornir plugins; Nautobot core itself is unmodified).
FROM ghcr.io/nautobot/nautobot:3.1.7
USER root
RUN apt-get update && apt-get install -y --no-install-recommends procps && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir \
nautobot-golden-config \
nautobot-plugin-nornir \
nornir-netmiko \
nornir-napalm \
napalm-ros \
librouteros \
"setuptools<81"
COPY --chown=nautobot:nautobot nautobot_config.py /opt/nautobot/nautobot_config.py
COPY --chown=nautobot:nautobot allnet_sg8316m_driver.py /opt/nautobot/allnet_sg8316m_driver.py
USER nautobot

View File

@@ -0,0 +1,89 @@
"""Custom Netmiko driver for the ALL-SG8316M (sw1) -- Telnet-only, IOS-like but
non-standard CLI (no `terminal length`, `--More--` paging, `end` from
config-vlan skips straight to the top level). No stock Netmiko/NAPALM driver
matches this device.
NOT YET WIRED IN: this module is imported nowhere yet. The `claudecode`
Telnet account on sw1 is currently rejected ("Incorrect User Name!!", found
2026-07-16) so this driver is unverified against the real device. Once
working credentials exist, activate it from nautobot_config.py:
import importlib
import allnet_sg8316m_driver # noqa: F401 (registers itself on import)
and add to PLUGINS_CONFIG["nautobot_plugin_nornir"]["dispatcher_mapping"]:
"allnet_sg8316m": "allnet_sg8316m_driver.GoldenConfigAllnetSg8316m"
(dispatcher class still needs writing once the connection class below is
confirmed to log in cleanly -- see project_nautobot_allnet_cudy_scope_20260716
memory for the full status and the two netmiko gotchas that cost the most
time: device_type must contain the literal substring "_telnet" for Netmiko to
pick the Telnet transport at all (BaseConnection.__init__ checks this by
string, not by class hierarchy), and `netmiko.ssh_dispatcher` at package level
is the dispatcher *function*, not the submodule -- reach CLASS_MAPPER via
`importlib.import_module("netmiko.ssh_dispatcher")`.
"""
import importlib
import time
from netmiko.cisco_base_connection import CiscoBaseConnection
_nm_dispatch = importlib.import_module("netmiko.ssh_dispatcher")
class AllnetSg8316mTelnet(CiscoBaseConnection):
def session_preparation(self):
self._test_channel_read(pattern=r"[>#]")
self.set_base_prompt()
self.ansi_escape_codes = True
def disable_paging(self, *args, **kwargs):
# No config knob for this on this firmware; paging is handled
# per-command in send_command_paged() below instead.
return ""
def telnet_login(
self,
pri_prompt_terminator=r"#\s*$",
alt_prompt_terminator=r">\s*$",
username_pattern=r"Username",
pwd_pattern=r"Password",
delay_factor=1,
max_loops=20,
):
return super().telnet_login(
pri_prompt_terminator=pri_prompt_terminator,
alt_prompt_terminator=alt_prompt_terminator,
username_pattern=username_pattern,
pwd_pattern=pwd_pattern,
delay_factor=delay_factor,
max_loops=max_loops,
)
def send_command_paged(self, command_string, max_pages=200, delay=0.3):
"""sg8316m paginates with '--More--' and has no length-0 setting;
page through by sending a space each time it appears."""
self.write_channel(command_string + self.RETURN)
time.sleep(delay)
output = ""
for _ in range(max_pages):
chunk = self.read_channel()
output += chunk
if "--More--" in chunk:
self.write_channel(" ")
time.sleep(delay)
continue
if any(p in chunk for p in ["sw1#", "sw1>"]) and "--More--" not in chunk:
break
if chunk == "":
time.sleep(delay)
return output.replace("--More--", "")
# Registered under the "_telnet" suffix required by Netmiko's own transport
# selection (see module docstring) -- registration is safe/inert on its own,
# nothing calls ConnectHandler(device_type="allnet_sg8316m_telnet") yet.
_nm_dispatch.CLASS_MAPPER["allnet_sg8316m_telnet"] = AllnetSg8316mTelnet
_nm_dispatch.platforms = list(_nm_dispatch.CLASS_MAPPER.keys())

View File

@@ -0,0 +1,91 @@
import os
import sys
from nautobot.core.settings import * # noqa F401,F403
from nautobot.core.settings_funcs import is_truthy, parse_redis_connection
if DATABASES["default"]["ENGINE"].endswith("mysql"):
DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "05^)=zgn3ytglmp_iv4@era^e23u3#_wtop-@yeaeazempdy^=")
INSTALLATION_METRICS_ENABLED = is_truthy(os.getenv("NAUTOBOT_INSTALLATION_METRICS_ENABLED", "True"))
# --- BF-Netzwerk additions: Golden Config + Nornir (TP21) ---
#
# Extends the stock config shipped in the base ghcr.io/nautobot/nautobot image
# (unchanged above this point) instead of replacing it, so a rollback is just
# "point the compose file's image tag back at ghcr.io/nautobot/nautobot:3.1.7".
PLUGINS = [
"nautobot_plugin_nornir",
"nautobot_golden_config",
]
PLUGINS_CONFIG = {
"nautobot_plugin_nornir": {
"nornir_settings": {
# Per-device creds via each Device's SecretsGroup (ap1/ap2 -> "RouterOS
# Nornir" group, access_type=Generic to match this class's default --
# see get_device_creds()/_get_access_type_value() in nautobot_secrets.py).
# Switched 2026-07-22 from env_vars.CredentialsEnvVars (a single global
# NAPALM_USERNAME/PASSWORD pair for the whole fleet) so future devices
# (CRS326 etc.) can carry their own credentials.
"credentials": "nautobot_plugin_nornir.plugins.credentials.nautobot_secrets.CredentialsNautobotSecrets",
"runner": {"plugin": "threaded", "options": {"num_workers": 5}},
},
# nautobot-plugin-nornir 2.0+ dropped PLUGINS_CONFIG["dispatcher_mapping"]
# (it now errors on boot if present) -- per-platform dispatcher overrides
# moved onto the Platform model itself (network_driver_mappings). The
# ALLNET sg8316m custom driver will be wired in there once credentials
# are sorted, not here.
},
"nautobot_golden_config": {
# Backup + Intended-Config + Compliance enabled 2026-07-22 (TP21 follow-up
# session) once a Jinja template + Git repo target existed.
#
# Plan + Deploy enabled 2026-08-13 (TP22 Schritt 3), after backup scrubbing
# (ConfigReplace) and compliance were green on all four RouterOS devices.
# Two binding governance rules apply -- see docs/teilprojekte.md TP22:
# 1. Config Plans ONLY feature-/manual-scoped, never "full intended":
# a full push would write the literal `passphrase="<<REDACTED>>"` to the
# APs (kicking every WLAN client) and `private-key="<<REDACTED>>"` to the
# core (killing the WireGuard hub). Sections containing the mask are
# excluded from any push.
# 2. The AP template is never pushed line-by-line to a live device: it
# transiently sets vlan-filtering=no, which severs the vlan99 management
# path mid-session. Full re-provisioning goes via file upload + /import.
#
# enable_postprocessing stays OFF on purpose: render_secrets would not fix the
# compliance side (the stored intended would keep the un-rendered Jinja
# expression -> permanent false positive) and pushing PSKs has no use here.
"enable_backup": True,
"enable_compliance": True,
"enable_intended": True,
"enable_sotagg": True,
"enable_plan": True,
"enable_deploy": True,
"enable_postprocessing": False,
# REQUIRED for deploy on RouterOS (found the hard way, 2026-08-13):
# the default dispatcher resolves to NAPALM because Platform.network_driver_mappings
# carries `napalm: ros` -- but napalm-ros is a READ-ONLY driver. Backup (get_config)
# works, merge_config does not and fails with an empty error:
# NornirNautobotException: E1015: The method merge_config failed ... ``
# nornir-nautobot ships a RouterOS-specific Netmiko dispatcher whose merge_config
# pushes via netmiko_send_config -- pin it explicitly. Key = network_driver name.
"custom_dispatcher": {
"mikrotik_routeros": "nornir_nautobot.plugins.tasks.dispatcher.mikrotik_routeros.NetmikoMikrotikRouteros",
},
"postprocessing_callables": [],
"postprocessing_subscribed": [],
"per_feature_bar": True,
"per_feature_width": 0.13,
"per_feature_height": 4,
"default_deploy_status": "Not Approved",
"get_custom_compliance": None,
"sot_agg_transposer": None,
},
}
NAPALM_USERNAME = os.getenv("NAUTOBOT_NAPALM_USERNAME", "")
NAPALM_PASSWORD = os.getenv("NAUTOBOT_NAPALM_PASSWORD", "")
NAPALM_TIMEOUT = int(os.getenv("NAUTOBOT_NAPALM_TIMEOUT", "30"))