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:
2026-09-06 16:45:00 +02:00
parent f507c50623
commit 7edf01b021
3 changed files with 119 additions and 9 deletions

View File

@@ -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