TP23 Stufe 5: HA-Self-Service + Dashboard-Draft + Doku-Nachzug
- agh-exception.py: Creds aus Env/Datei (Mac ~/.secrets + HAOS /config), Domain/IP-Validierung - HA-Self-Service dokumentiert: shell_command + Helper/Timer/2 Automations (adguard/ha-self-service.md) - Eigenes Dashboard 'dns-ausnahmen' (1. Draft, sections) inkl. Repro-JSON - teilprojekte.md: TP23 Quick-Index + dated Update + Ist-Stand-Block nachgezogen (Stufe 1-5 durch; offen nur Stufe 6 Bypass-Sperre -> separate Session) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,19 +14,40 @@ Aufrufer (HA-Timer) bzw. der RouterOS-Backstop (Stufe 6). Diese CLI ist reines a
|
||||
|
||||
Auth: AGH_AUTOMATION_USER/_PASS aus ~/.secrets/credentials.env (nie in argv).
|
||||
"""
|
||||
import sys, os, json, base64, urllib.request, urllib.error, pathlib
|
||||
import sys, os, json, base64, urllib.request, urllib.error, pathlib, re
|
||||
|
||||
DOMAIN_RE = re.compile(r"^(?=.{1,253}$)([a-z0-9](-?[a-z0-9]+)*\.)+[a-z]{2,}$", re.I)
|
||||
IP_RE = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$")
|
||||
|
||||
INST = ["10.40.20.36", "10.40.20.37"]
|
||||
SECRETS = pathlib.Path.home() / ".secrets/credentials.env"
|
||||
# Creds-Quellen in Reihenfolge: erste, die User+Pass liefert, gewinnt.
|
||||
# So läuft derselbe Code auf dem Mac (~/.secrets) und auf HAOS (/config).
|
||||
CRED_FILES = [
|
||||
os.environ.get("AGH_CREDS_FILE"),
|
||||
str(pathlib.Path.home() / ".secrets/credentials.env"),
|
||||
"/config/agh-exception.secrets",
|
||||
]
|
||||
|
||||
|
||||
def _from_file(path):
|
||||
u = p = None
|
||||
try:
|
||||
for line in open(path).read().splitlines():
|
||||
if line.startswith("AGH_AUTOMATION_USER="): u = line.split("=", 1)[1].strip()
|
||||
if line.startswith("AGH_AUTOMATION_PASS="): p = line.split("=", 1)[1].strip()
|
||||
except OSError:
|
||||
return None
|
||||
return (u, p) if (u and p) else None
|
||||
|
||||
|
||||
def creds():
|
||||
u = p = None
|
||||
for line in SECRETS.read_text().splitlines():
|
||||
if line.startswith("AGH_AUTOMATION_USER="): u = line.split("=", 1)[1].strip()
|
||||
if line.startswith("AGH_AUTOMATION_PASS="): p = line.split("=", 1)[1].strip()
|
||||
if not (u and p): sys.exit("AGH_AUTOMATION_USER/_PASS fehlen in ~/.secrets")
|
||||
return u, p
|
||||
eu, ep = os.environ.get("AGH_AUTOMATION_USER"), os.environ.get("AGH_AUTOMATION_PASS")
|
||||
if eu and ep:
|
||||
return eu, ep
|
||||
for f in CRED_FILES:
|
||||
if f and (r := _from_file(f)):
|
||||
return r
|
||||
sys.exit("AGH_AUTOMATION_USER/_PASS: weder Env noch eine Creds-Datei gefunden")
|
||||
|
||||
|
||||
AUTH = "Basic " + base64.b64encode(":".join(creds()).encode()).decode()
|
||||
@@ -53,6 +74,9 @@ def rule(domain):
|
||||
|
||||
|
||||
def allow(domain, remove=False):
|
||||
domain = domain.strip().lower()
|
||||
if not DOMAIN_RE.match(domain):
|
||||
sys.exit(f"ungültige Domain: {domain!r}")
|
||||
r = rule(domain)
|
||||
for ip in INST:
|
||||
st = api(ip, "/control/filtering/status")
|
||||
@@ -66,6 +90,8 @@ def allow(domain, remove=False):
|
||||
|
||||
|
||||
def set_filter(ip_addr, enabled):
|
||||
if not IP_RE.match(ip_addr):
|
||||
sys.exit(f"ungültige IP: {ip_addr!r}")
|
||||
for ip in INST:
|
||||
data = api(ip, "/control/clients")
|
||||
c = None
|
||||
|
||||
63
adguard/ha-self-service.md
Normal file
63
adguard/ha-self-service.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# TP23 Stufe 5 — HA-Self-Service für DNS-Ausnahmen
|
||||
|
||||
Home Assistant (HAOS-VM 100, `10.40.20.105`) gibt eine Ein-Klick-Oberfläche, um eine Domain
|
||||
temporär (JIT) über das AGH-Paar `.36`/`.37` zu entsperren. Ablauf/Revert nativ per `timer`.
|
||||
|
||||
**Warum kein `rest_command`:** die AGH-Endpunkte sind read-modify-write (`set_rules` ersetzt die
|
||||
ganze Liste). HA ruft deshalb die deployte RMW-CLI `agh-exception.py` per `shell_command`.
|
||||
|
||||
## Bausteine (alle in HA .storage, via ha-mcp angelegt — keine YAML außer shell_command)
|
||||
| Typ | entity_id | Zweck |
|
||||
|---|---|---|
|
||||
| input_text | `input_text.dns_ausnahme_domain` | einzugebende Domain |
|
||||
| input_text | `input_text.dns_ausnahme_aktiv` | aktuell aktive Ausnahme (für leak-freien Revert; init `""`) |
|
||||
| input_select | `input_select.dns_ausnahme_dauer` | 15 Minuten / 1 Stunde / 4 Stunden |
|
||||
| timer | `timer.dns_ausnahme` | JIT-Countdown (restore=false) |
|
||||
| input_button | `input_button.dns_ausnahme_starten` | Auslöser |
|
||||
| automation | `automation.dns_ausnahme_starten` | Button → validieren, alte Ausnahme aufräumen, `agh_allow_domain`, Timer starten |
|
||||
| automation | `automation.dns_ausnahme_ablauf` | `timer.finished`/`timer.cancelled` → `agh_unallow_domain`, `_aktiv` leeren |
|
||||
|
||||
## `configuration.yaml` (der einzige YAML-Teil, Backup `.bak-tp23-*`)
|
||||
```yaml
|
||||
shell_command:
|
||||
agh_allow_domain: "python3 /config/agh-exception.py allow-domain '{{ domain }}'"
|
||||
agh_unallow_domain: "python3 /config/agh-exception.py unallow-domain '{{ domain }}'"
|
||||
agh_unfilter_client: "python3 /config/agh-exception.py unfilter '{{ ip }}'"
|
||||
agh_refilter_client: "python3 /config/agh-exception.py refilter '{{ ip }}'"
|
||||
```
|
||||
Neue Domain → **HA-Neustart** nötig (reload_all lädt eine erstmals hinzugefügte Domain nicht).
|
||||
|
||||
## Deploy auf HAOS (Zugang `ssh admin@10.40.20.105`, sudo passwortlos)
|
||||
- `/config/agh-exception.py` — Kopie von `adguard/agh-exception.py` (liest Creds via `/config/agh-exception.secrets`).
|
||||
- `/config/agh-exception.secrets` (chmod 600, root) — `AGH_AUTOMATION_USER`/`_PASS`.
|
||||
- Redeploy nach Script-Änderung: `base64 < adguard/agh-exception.py | ssh admin@10.40.20.105 "base64 -d | sudo tee /config/agh-exception.py >/dev/null"`.
|
||||
|
||||
## Bedienung
|
||||
Domain eintragen → Dauer wählen → **DNS-Ausnahme starten**. Bei Ablauf/Abbruch wird die Regel auf
|
||||
beiden AGH automatisch zurückgenommen. Ungültige Domains werden HA- **und** CLI-seitig abgelehnt.
|
||||
|
||||
## Dashboard (1. Draft, 2026-09-06)
|
||||
Eigenes storage-mode Dashboard `url_path: dns-ausnahmen` (Sidebar, `require_admin: false`),
|
||||
angelegt via `ha_config_set_dashboard`. Da es in HA-`.storage` lebt, hier die Repro-Quelle:
|
||||
```json
|
||||
{"views":[{"title":"DNS-Ausnahmen","path":"dns","type":"sections","icon":"mdi:dns","max_columns":2,"sections":[
|
||||
{"type":"grid","cards":[
|
||||
{"type":"heading","heading":"Domain temporaer freigeben","icon":"mdi:lock-open-variant"},
|
||||
{"type":"entities","entities":[{"entity":"input_text.dns_ausnahme_domain","name":"Domain"},{"entity":"input_select.dns_ausnahme_dauer","name":"Dauer"}]},
|
||||
{"type":"button","name":"DNS-Ausnahme starten","icon":"mdi:lock-open-check","tap_action":{"action":"perform-action","perform_action":"input_button.press","target":{"entity_id":"input_button.dns_ausnahme_starten"}},"grid_options":{"columns":"full"}}]},
|
||||
{"type":"grid","cards":[
|
||||
{"type":"heading","heading":"Status","icon":"mdi:information-outline"},
|
||||
{"type":"tile","entity":"timer.dns_ausnahme","name":"Restlaufzeit"},
|
||||
{"type":"tile","entity":"input_text.dns_ausnahme_aktiv","name":"Aktive Ausnahme"},
|
||||
{"type":"markdown","content":"**AGH-Resolver-Paar** — .36: {{ states('binary_sensor.adguard_bf1_status') }} / .37: {{ states('binary_sensor.adguard_bf2_status') }}"}]},
|
||||
{"type":"grid","cards":[
|
||||
{"type":"heading","heading":"VLAN-Filter-Policy (AGH-Gruppen)","heading_style":"subtitle"},
|
||||
{"type":"markdown","content":"MAIN/IOT/UNTRUSTED=an · INFRA/MEDIA/MGMT=aus · GUEST=an+SafeSearch/Parental · dns-bypass-exempt=aus"}]}
|
||||
]}]}
|
||||
```
|
||||
⚠️ Draft-Notiz: `binary_sensor.adguard_bf1_status` ist stale (hing am alten `.16`-Container, TP42) —
|
||||
zeigt evtl. `unavailable`, obwohl `.36` läuft. Sensor-Quelle gehört bei Gelegenheit auf `.36` gefixt.
|
||||
|
||||
## Verifiziert (2026-09-04)
|
||||
Button → `@@||doubleclick.net^` auf `.36`+`.37`, Timer 15:00, `_aktiv` gesetzt; Timer-Abbruch →
|
||||
Regel auf beiden entfernt, `_aktiv` geleert, `.36==.37`.
|
||||
Reference in New Issue
Block a user